browserwright 0.7.2__tar.gz → 0.7.4__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 (114) hide show
  1. {browserwright-0.7.2 → browserwright-0.7.4}/PKG-INFO +2 -2
  2. {browserwright-0.7.2 → browserwright-0.7.4}/README.md +32 -106
  3. {browserwright-0.7.2 → browserwright-0.7.4}/pyproject.toml +2 -2
  4. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/process.py +1 -1
  5. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/api.py +12 -17
  6. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/cdp.py +7 -65
  7. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/cli.py +24 -170
  8. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/_ipc.py +12 -86
  9. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/_stale.py +3 -6
  10. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/__init__.py +1 -8
  11. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/base.py +8 -43
  12. browserwright-0.7.4/src/browserwright/daemon/backends/cdp.py +286 -0
  13. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/backends/extension.py +0 -5
  14. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/cli.py +134 -470
  15. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/config.py +22 -56
  16. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/doctor.py +4 -28
  17. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/launch_chrome.py +0 -8
  18. browserwright-0.7.4/src/browserwright/daemon/observability.py +80 -0
  19. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/platforms.py +6 -29
  20. browserwright-0.7.4/src/browserwright/daemon/resolver.py +57 -0
  21. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/daemon.py +4 -4
  22. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/executor_registry.py +1 -27
  23. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/extension_upstream.py +72 -93
  24. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/facade.py +39 -9
  25. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/facade_extension.py +60 -84
  26. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/listener.py +33 -109
  27. browserwright-0.7.4/src/browserwright/daemon/server/proxy.py +664 -0
  28. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/relay.py +1 -20
  29. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/state.py +9 -122
  30. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/upstream.py +2 -23
  31. browserwright-0.7.4/src/browserwright/daemon/server/verbs.py +1155 -0
  32. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/discovery.py +5 -11
  33. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/errors.py +3 -3
  34. browserwright-0.7.4/src/browserwright/install.py +240 -0
  35. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/__init__.py +0 -3
  36. browserwright-0.7.4/src/browserwright/memory/_lock.py +46 -0
  37. browserwright-0.7.4/src/browserwright/memory/_yaml.py +54 -0
  38. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/global_mem.py +1 -56
  39. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/site_mem.py +1 -32
  40. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/mode_b_client.py +19 -246
  41. browserwright-0.7.4/src/browserwright/primitives/__init__.py +35 -0
  42. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/site.py +2 -2
  43. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/inline.py +0 -10
  44. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/playwright_handle.py +9 -8
  45. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session.py +2 -43
  46. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_create.py +0 -34
  47. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_registry.py +7 -11
  48. browserwright-0.7.4/src/browserwright/session_runtime.py +386 -0
  49. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/skill_doc.py +0 -40
  50. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/skill_runtime.md +1 -1
  51. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/task_runner.py +2 -2
  52. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/PKG-INFO +2 -2
  53. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/SOURCES.txt +3 -13
  54. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/requires.txt +1 -1
  55. browserwright-0.7.2/src/browserwright/daemon/active_tab.py +0 -183
  56. browserwright-0.7.2/src/browserwright/daemon/auth.py +0 -395
  57. browserwright-0.7.2/src/browserwright/daemon/backends/cloud.py +0 -222
  58. browserwright-0.7.2/src/browserwright/daemon/backends/env.py +0 -119
  59. browserwright-0.7.2/src/browserwright/daemon/backends/rdp.py +0 -214
  60. browserwright-0.7.2/src/browserwright/daemon/observability.py +0 -181
  61. browserwright-0.7.2/src/browserwright/daemon/resolver.py +0 -72
  62. browserwright-0.7.2/src/browserwright/daemon/server/proxy.py +0 -2108
  63. browserwright-0.7.2/src/browserwright/install.py +0 -655
  64. browserwright-0.7.2/src/browserwright/memory/_yaml.py +0 -217
  65. browserwright-0.7.2/src/browserwright/memory/repl_mem.py +0 -28
  66. browserwright-0.7.2/src/browserwright/memory/session_decisions.py +0 -53
  67. browserwright-0.7.2/src/browserwright/multitask.py +0 -120
  68. browserwright-0.7.2/src/browserwright/primitives/__init__.py +0 -83
  69. browserwright-0.7.2/src/browserwright/primitives/inspect.py +0 -887
  70. browserwright-0.7.2/src/browserwright/primitives/interact.py +0 -518
  71. browserwright-0.7.2/src/browserwright/primitives/page.py +0 -564
  72. browserwright-0.7.2/src/browserwright/release_install.py +0 -466
  73. browserwright-0.7.2/src/browserwright/session_runtime.py +0 -133
  74. browserwright-0.7.2/src/browserwright/subscriptions.py +0 -213
  75. {browserwright-0.7.2 → browserwright-0.7.4}/setup.cfg +0 -0
  76. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/__init__.py +0 -0
  77. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/__main__.py +0 -0
  78. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/__init__.py +0 -0
  79. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/__main__.py +0 -0
  80. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/client.py +0 -0
  81. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/_executor/protocol.py +0 -0
  82. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/__init__.py +0 -0
  83. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/errors.py +0 -0
  84. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/server/__init__.py +0 -0
  85. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/daemon/userscripts.py +0 -0
  86. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/health.py +0 -0
  87. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/memory/_md.py +0 -0
  88. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/output_schema.py +0 -0
  89. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/discovery_api.py +0 -0
  90. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/primitives/http.py +0 -0
  91. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/__init__.py +0 -0
  92. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/_namespace.py +0 -0
  93. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/_smart_goto.py +0 -0
  94. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/repl/snapshot.py +0 -0
  95. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/session_ctx.py +0 -0
  96. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/SKILL.md +0 -0
  97. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/memory.md +0 -0
  98. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/github.com/tasks/list_issues.py +0 -0
  99. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/SKILL.md +0 -0
  100. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/memory.md +0 -0
  101. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/google.com/tasks/search.py +0 -0
  102. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/SKILL.md +0 -0
  103. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/memory.md +0 -0
  104. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/producthunt.com/tasks/today.py +0 -0
  105. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/SKILL.md +0 -0
  106. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/memory.md +0 -0
  107. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/wikipedia.org/tasks/lookup.py +0 -0
  108. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/SKILL.md +0 -0
  109. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/memory.md +0 -0
  110. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/site_skills_starter/ycombinator.com/tasks/front_page.py +0 -0
  111. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright/version.py +0 -0
  112. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/dependency_links.txt +0 -0
  113. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/entry_points.txt +0 -0
  114. {browserwright-0.7.2 → browserwright-0.7.4}/src/browserwright.egg-info/top_level.txt +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browserwright
3
- Version: 0.7.2
3
+ Version: 0.7.4
4
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
5
  Requires-Python: >=3.11
6
- Requires-Dist: cdp-use==1.4.5
7
6
  Requires-Dist: websockets==15.0.1
8
7
  Requires-Dist: pillow==12.2.0
9
8
  Requires-Dist: httpx>=0.27
10
9
  Requires-Dist: playwright>=1.60.0
10
+ Requires-Dist: pyyaml>=6.0.3
11
11
  Provides-Extra: ux
12
12
  Requires-Dist: rich>=13; extra == "ux"
@@ -4,21 +4,27 @@ Let an AI/code agent drive a real (or isolated) Chrome from the terminal over CD
4
4
 
5
5
  One installable package, two CLIs that work together:
6
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.
7
+ - **`browserwright-daemon`** (Layer 1) — resolves a Chrome CDP WebSocket URL and proxies it. Backends: `env / rdp / extension`. Also `launch-chrome` to spawn an isolated Chrome.
8
8
  - **`browserwright`** (Layer 2) — the agent-facing surface: sessions, heredoc scripting with pre-imported primitives, reusable site tasks, memory, and userscript management.
9
9
 
10
10
  ```
11
11
  .
12
12
  ├── src/browserwright/ the package
13
13
  │ ├── … Layer 2 — sessions / primitives / site skills / memory / userscripts
14
- │ └── daemon/ Layer 1 — CDP URL resolver + proxy (env/rdp/extension/cloud backends)
14
+ │ └── daemon/ Layer 1 — CDP URL resolver + proxy (env/rdp/extension backends)
15
15
  ├── chrome-extension/ unpacked relay extension for the `extension` backend
16
16
  ├── skill/ Agent skill bundle (symlinked into Claude Code, Codex, and Pi)
17
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)
18
+ └── docs/ deeper docs (architecture.md, daemon.md, session-workspaces.md, …)
20
19
  ```
21
20
 
21
+ Contributing or hacking on this repo? Start at **[AGENTS.md](AGENTS.md)** — it
22
+ points to [ONBOARD.md](ONBOARD.md) (clone → install → test loop),
23
+ [docs/architecture.md](docs/architecture.md) (architecture orientation),
24
+ [TESTING.md](TESTING.md) (test-suite map), and [RELEASING.md](RELEASING.md)
25
+ (releases + updating the global install). This README covers the end-user path
26
+ only.
27
+
22
28
  ## Prerequisites
23
29
 
24
30
  - macOS or Linux
@@ -96,100 +102,11 @@ browserwright-daemon stop
96
102
  browserwright-daemon serve
97
103
  ```
98
104
 
99
- ### PyPI release flow
100
-
101
- PyPI releases are driven by git tags. For published artifacts, the git tag is
102
- the source of truth for the package version; the release workflow rewrites
103
- `pyproject.toml` to that tag version before building the sdist and wheel.
104
-
105
- After a PR has landed on `main`, publish by pushing a version tag:
106
-
107
- ```bash
108
- git checkout main
109
- git pull
110
- git tag v0.6.3
111
- git push origin v0.6.3
112
- ```
113
-
114
- The GitHub Action only runs for `v*` tags. A tag like `v0.6.3` publishes package
115
- version `0.6.3`.
116
-
117
- Each `v*` GitHub Release also includes a
118
- `browserwright-extension-<version>.zip` asset. Use that zip as the official
119
- extension download path when installing outside PyPI or before the Chrome Web
120
- Store path is available.
121
-
122
- ### Local development link
123
-
124
- For development only, `mise run dev-link` links the current checkout into
125
- global PATH and skill dirs. That is convenient for debugging but can break
126
- global agents if the checkout is broken.
127
-
128
- ```bash
129
- mise run dev-link
130
- ```
131
-
132
- The skill bundle is intentionally a thin shell. The authoritative agent
133
- instructions come from `browserwright --print-skill`, so installed agents read
134
- docs generated by the installed CLI.
135
-
136
- ## Update the local global install
137
-
138
- Use this flow when you want the global Code Agent install on this machine to
139
- pick up the latest published release:
140
-
141
- ```bash
142
- # 1. Install/upgrade browserwright from PyPI.
143
- # 2. Download the matching GitHub Release extension artifact.
144
- # 3. Restart the daemon, reload the unpacked extension, and verify versions.
145
- mise run upgrade-global
146
-
147
- # 4. Optional explicit verification.
148
- browserwright version check
149
- browserwright-daemon version check
150
- ```
151
-
152
- If the daemon is running manually instead of as the macOS LaunchAgent, restart it manually:
153
-
154
- ```bash
155
- browserwright-daemon stop
156
- browserwright-daemon serve
157
- ```
158
-
159
- `upgrade-global` unpacks the GitHub Release asset into the stable local load
160
- path:
161
-
162
- ```bash
163
- /Users/metajs/Library/Mobile Documents/com~apple~CloudDocs/etc/chrome-extension/browserwright
164
- ```
165
-
166
- If the task reports that the extension changed, it restarts the daemon, runs
167
- `browserwright-daemon extension reload`, and waits for Chrome to reconnect with
168
- the new extension version. The stable path does not change across releases; the
169
- task overwrites its contents. If the reload cannot be confirmed, fall back to
170
- Chrome's manual reload button in `chrome://extensions/`.
171
- Reload any existing tab that already shows a duplicated `👀` attach marker so
172
- the extension can normalize the title marker after the upgrade.
105
+ ### Releases, upgrades, and local development
173
106
 
174
- ## Release discipline
175
-
176
- For PyPI releases, the git tag is the source of truth and the publish workflow
177
- writes that version into `pyproject.toml` before building. The release workflow
178
- also writes the same tag version into `chrome-extension/manifest.json` before
179
- uploading `browserwright-extension-<version>.zip` to the GitHub Release.
180
-
181
- After installing a release:
182
-
183
- ```bash
184
- browserwright version check
185
- browserwright-daemon version check
186
- browserwright-daemon restart # when installed as the macOS LaunchAgent
187
- browserwright-daemon extension reload
188
- ```
189
-
190
- The extension reload command asks connected unpacked extensions to call
191
- `chrome.runtime.reload()`. Reload is immediate; the extension reconnect loop
192
- restores the relay connection after the service worker restarts.
107
+ Cutting a release, updating a machine's global install (`mise run
108
+ upgrade-global`), the extension reload flow, and the dev-link workflow are all
109
+ documented in **[RELEASING.md](RELEASING.md)** and **[ONBOARD.md](ONBOARD.md)**.
193
110
 
194
111
  ## Smoke test
195
112
 
@@ -254,7 +171,6 @@ browserwright task wikipedia.org/lookup --title="Wikipedia"
254
171
  | Scripts / iterative work in throwaway profiles | `rdp` + isolated Chrome | `browserwright-daemon launch-chrome --port 9333 --profile bs-dev` + `BD_PORT=9333 BD_BACKEND=rdp` |
255
172
  | Fingerprint browser (AdsPower / MultiLogin / 比特浏览器) | `rdp` | point `BD_PORT` at the tool's exposed port |
256
173
  | Externally-owned CDP endpoint (anti-detect profile, e.g. CloakBrowser) | `env` | `BD_CDP_WS=ws://… browserwright-daemon serve --backend env`, then `browserwright session new --backend=env --name=…` (attach-owned — never closed on `session end`) |
257
- | Remote Chrome (Browser Use / Browserless / Hyperbrowser) | `cloud` | `browserwright-daemon serve --provider <name>` + auth env vars |
258
174
 
259
175
  Interactive wizard: `browserwright install` — walks the decision tree and writes your pick.
260
176
 
@@ -263,6 +179,16 @@ external profiles with N isolated daemons — each with its own `XDG_RUNTIME_DIR
263
179
  (distinct socket), `--facade-port`, and `BD_CDP_WS`, one `env` session apiece.
264
180
  See [docs/session-workspaces.md](docs/session-workspaces.md) §"Env Backend".
265
181
 
182
+ **Reaching the facade from another machine (Tailscale/LAN):** the Playwright
183
+ facade binds `127.0.0.1` by default and is *never* exposed off-box unless you
184
+ opt in. Pass `--facade-host <tailnet-ip>` (or `BD_FACADE_HOST` / `facade_host`
185
+ in config.toml; `0.0.0.0` to bind all interfaces) and a remote client can
186
+ `connect_over_cdp("http://<tailnet-ip>:19990/cdp")` — the facade's
187
+ `/json/version` bootstrap rewrites the advertised `webSocketDebuggerUrl` from
188
+ the request's `Host` header, so the ws URL points back at the address the
189
+ client actually used. No auth is added, so only bind an interface you trust
190
+ (a Tailscale IP is private to your tailnet).
191
+
266
192
  ### Userscripts
267
193
 
268
194
  Author Tampermonkey-style scripts the `extension` backend injects on matching sites:
@@ -297,7 +223,7 @@ Non-browser helpers (also pre-imported):
297
223
 
298
224
  - **HTTP (no browser, for static pages):** `http_get(url)`
299
225
  - **Memory:** `remember`, `remember_global`, `remember_preference`, `memory_read`
300
- - **Site-skills / tasks:** `list_site_skills`, `load_site_skill`, `run_task`, `run_tasks_concurrent`, `bootstrap_site`
226
+ - **Site-skills / tasks:** `list_site_skills`, `load_site_skill`, `run_task`, `bootstrap_site`
301
227
 
302
228
  Full catalogue and guidance in `skill/SKILL.md`.
303
229
 
@@ -305,9 +231,8 @@ Full catalogue and guidance in `skill/SKILL.md`.
305
231
 
306
232
  ```bash
307
233
  browserwright-daemon doctor # which backends are live, why each is/isn't usable
308
- browserwright-daemon list-backends
234
+ browserwright-daemon status --json # daemon liveness + endpoint + facade port
309
235
  browserwright doctor # skill-side health
310
- browserwright-daemon stats # observability counters when `serve` is running
311
236
  ```
312
237
 
313
238
  ## Agent integrations
@@ -327,12 +252,13 @@ rm -rf ~/.cache/browserwright-daemon ~/.browserwright
327
252
 
328
253
  ## Further reading
329
254
 
330
- - `TESTING.md` — map of the test suites and how to run them
331
- - `browser-connection.md` — *why* this stack exists (CDP discovery paths, Chrome 144+ popup mechanics)
255
+ - `AGENTS.md` — entry point for contributors and code agents
256
+ - `docs/architecture.md` — architecture tour + isolated local dev
257
+ - `docs/session-workspaces.md` — the session workspace / isolation model
332
258
  - `docs/daemon.md` — backend internals, env vars, `config.toml`
333
- - `docs/skill.md` — full primitive surface and release notes
334
- - `docs/session-model.md` — the session isolation model
335
- - `ONBOARDING.md` — contributor-oriented architecture tour
259
+ - `docs/skill.md` — the Layer 2 agent-facing surface
260
+ - `TESTING.md` — map of the test suites and how to run them
261
+ - `docs/archive/browser-connection.md` — *why* this stack exists (CDP discovery paths, Chrome 144+ popup mechanics)
336
262
 
337
263
  ## License
338
264
 
@@ -4,11 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "browserwright"
7
- version = "0.7.2"
7
+ version = "0.7.4"
8
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
9
  requires-python = ">=3.11"
10
10
  dependencies = [
11
- "cdp-use==1.4.5",
12
11
  "websockets==15.0.1",
13
12
  "pillow==12.2.0",
14
13
  "httpx>=0.27",
@@ -18,6 +17,7 @@ dependencies = [
18
17
  # for tests that launch a browser; the daemon-facade path drives the
19
18
  # daemon-resolved Chrome and does not need Playwright's bundled browser.
20
19
  "playwright>=1.60.0",
20
+ "pyyaml>=6.0.3",
21
21
  ]
22
22
 
23
23
  [project.optional-dependencies]
@@ -192,7 +192,7 @@ class _Worker:
192
192
  Reuses the shared connect+bind free functions (single source of truth
193
193
  for the FATAL "no Playwright CDP session over the extension facade"
194
194
  constraint). Bind the session FIRST so ``current_session()`` /
195
- ``current_page()`` resolve the right ledger record."""
195
+ ``resolve_current_target()`` resolve the right ledger record."""
196
196
  if self._connected:
197
197
  return
198
198
  from ..session import Session, set_session
@@ -5,23 +5,20 @@ this module. Keeping the list in one place means an agent who imports
5
5
  ``browserwright`` directly from a saved task gets the same names the REPL
6
6
  gave them.
7
7
 
8
- Phase C PR3 (terminal state): the legacy CDP browser-driving primitives
9
- (``open``/``goto_url``/``click_at_xy``/``js``/``cdp``/``capture_screenshot``/
10
- ``snapshot``/… — the whole page/tab interaction surface) are GONE from the
11
- agent surface. The agent now drives the browser with **real Playwright** via
12
- the injected ``page`` / ``context`` (bound to the session's current tab,
13
- reused across heredocs) and observes with ``snapshot()`` (a first-party AI
14
- aria snapshot whose ``[ref=eN]`` refs feed ``page.locator("aria-ref=eN")``).
15
- Those three names are injected per-heredoc by ``repl/_namespace.build_globals``,
16
- NOT exported here.
8
+ The legacy CDP browser-driving primitives (``open``/``goto_url``/
9
+ ``click_at_xy``/``js``/``cdp``/``capture_screenshot``/``snapshot``/… — the
10
+ whole page/tab interaction surface) are DELETED. The agent drives the browser
11
+ with **real Playwright** via the injected ``page`` / ``context`` (bound to the
12
+ session's current tab, reused across heredocs) and observes with
13
+ ``snapshot()`` (a first-party AI aria snapshot whose ``[ref=eN]`` refs feed
14
+ ``page.locator("aria-ref=eN")``). Those three names are injected per-heredoc
15
+ by ``repl/_namespace.build_globals``, NOT exported here. The internal tab
16
+ lifecycle the Playwright binding glue relies on lives in
17
+ ``browserwright.session_runtime``.
17
18
 
18
19
  What remains in EXPORTS is the set of NON-browser-driving helpers that do not
19
20
  overlap Playwright: ``http_get`` (no-browser escape hatch), the memory verbs,
20
- and the site-skill / task layer. The implementation modules under
21
- ``primitives/`` still define the old functions (``current_page``, ``list_tabs``,
22
- the daemon-driving glue, …); they are kept as INTERNAL functions the Phase C
23
- binding glue (``repl/playwright_handle.py``) and the memory/site helpers rely
24
- on — they are simply no longer part of the agent-callable surface.
21
+ and the site-skill / task layer.
25
22
  """
26
23
  from .errors import (
27
24
  AuthWall,
@@ -34,7 +31,6 @@ from .errors import (
34
31
  NetworkError,
35
32
  PageLoadFailed,
36
33
  )
37
- from .multitask import run_tasks_concurrent
38
34
  from .primitives import (
39
35
  bootstrap_site,
40
36
  http_get,
@@ -53,10 +49,9 @@ EXPORTS = [
53
49
  # memory + site
54
50
  "bootstrap_site", "remember", "remember_global", "remember_preference",
55
51
  "memory_read",
56
- # task / fan-out (site-skills run on the Playwright surface — see
52
+ # task (site-skills run on the Playwright surface — see
57
53
  # task_runner.run_task, which injects page/context into the task module)
58
54
  "list_site_skills", "load_site_skill", "run_task",
59
- "run_tasks_concurrent",
60
55
  # errors
61
56
  "BrowserwrightError", "PageLoadFailed", "ElementNotFound", "AuthWall",
62
57
  "Captcha", "NetworkError", "DaemonUnavailable", "CDPError",
@@ -6,21 +6,17 @@ Design:
6
6
  no session for ``Target.*`` etc.
7
7
  - Auto-attach to a tab on demand via ``attach(targetId)``; the resulting
8
8
  session id is cached so subsequent calls reuse it.
9
- - Events for the attached session are stashed in a per-session ring buffer
10
- and exposed via ``drain_events()``.
11
9
 
12
- We intentionally do not depend on cdp-use here. The whole client is < 200
13
- lines of plain websockets — easier to reason about, easier to unit test,
14
- and we never need typed wrappers (spec §3 "raw CDP strings over typed
15
- wrappers").
10
+ The whole client is < 200 lines of plain websockets — easier to reason
11
+ about, easier to unit test, and we never need typed wrappers (spec §3
12
+ "raw CDP strings over typed wrappers").
16
13
  """
17
14
  from __future__ import annotations
18
15
 
19
16
  import json
20
17
  import threading
21
18
  import time
22
- from collections import deque
23
- from typing import Any, Optional
19
+ from typing import Optional
24
20
 
25
21
  from websockets.exceptions import ConnectionClosed
26
22
  from websockets.sync.client import connect as ws_connect
@@ -28,9 +24,6 @@ from websockets.sync.client import connect as ws_connect
28
24
  from .errors import CDPError
29
25
 
30
26
 
31
- _EVENT_RING_LIMIT = 1024
32
-
33
-
34
27
  class _UnixSocketAdapter:
35
28
  """Wrap an ``AF_UNIX`` socket so ``setsockopt(IPPROTO_TCP, ...)`` becomes
36
29
  a no-op. websockets unconditionally calls
@@ -113,7 +106,7 @@ class CDPSession:
113
106
 
114
107
  All sends are synchronous: send → block on response with matching id →
115
108
  return result. Events arrive on the same socket; the reader thread
116
- routes them by sessionId into per-session deques.
109
+ discards them (nothing downstream consumes raw CDP events).
117
110
  """
118
111
 
119
112
  def __init__(self, ws_url: str, connect_timeout: float = 8.0):
@@ -145,7 +138,6 @@ class CDPSession:
145
138
  self._next_id = 1
146
139
  self._inflight: dict[int, dict] = {}
147
140
  self._inflight_cv = threading.Condition(self._lock)
148
- self._events: dict[Optional[str], deque] = {None: deque(maxlen=_EVENT_RING_LIMIT)}
149
141
  self._closed = False
150
142
  self._closed_reason: Optional[str] = None
151
143
  self._reader = threading.Thread(target=self._read_loop, name="cdp-reader", daemon=True)
@@ -201,8 +193,7 @@ class CDPSession:
201
193
  res = self.send("Target.attachToTarget", targetId=target_id, flatten=True)
202
194
  sid = res["sessionId"]
203
195
  self._sessions[target_id] = sid
204
- self._events.setdefault(sid, deque(maxlen=_EVENT_RING_LIMIT))
205
- # Enable the usual domains so wait_for_load / drain_events have data.
196
+ # Enable the usual domains so wait_for_load has data.
206
197
  for domain in ("Page", "Runtime", "DOM", "Network"):
207
198
  try:
208
199
  self.send(f"{domain}.enable", session=sid)
@@ -210,49 +201,6 @@ class CDPSession:
210
201
  pass # Some domains are noop in some Chrome builds.
211
202
  return sid
212
203
 
213
- def attach_readonly(self, target_id: str) -> str:
214
- """Daemon v0.3 H7 shared-read attach.
215
-
216
- Requests a session via ``flags.allowSecondaryReadOnly=True`` — daemon
217
- returns a sessionId that receives this target's events but rejects
218
- any command other than ``Target.detachFromTarget`` (`-32602`). Useful
219
- for tail-following another agent's session for monitoring / drift
220
- detection.
221
-
222
- Note: this opens a *second* session on the same target if some other
223
- client / process already owns it. If we own it ourselves, prefer
224
- ``attach()``.
225
- """
226
- res = self.send(
227
- "Target.attachToTarget",
228
- targetId=target_id,
229
- flatten=True,
230
- flags={"allowSecondaryReadOnly": True},
231
- )
232
- sid = res["sessionId"]
233
- self._events.setdefault(sid, deque(maxlen=_EVENT_RING_LIMIT))
234
- # We deliberately *don't* register sid in ``self._sessions`` — that
235
- # map tracks owning attachments, and a readonly attachment isn't one.
236
- return sid
237
-
238
- def detach(self, target_id: str) -> None:
239
- sid = self._sessions.pop(target_id, None)
240
- if sid:
241
- try:
242
- self.send("Target.detachFromTarget", sessionId=sid)
243
- except CDPError:
244
- pass
245
- self._events.pop(sid, None)
246
-
247
- def drain_events(self, session: Optional[str] = None) -> list[dict]:
248
- buf = self._events.get(session)
249
- if not buf:
250
- return []
251
- with self._lock:
252
- out = list(buf)
253
- buf.clear()
254
- return out
255
-
256
204
  def close(self) -> None:
257
205
  if self._closed:
258
206
  return
@@ -279,13 +227,7 @@ class CDPSession:
279
227
  if mid in self._inflight:
280
228
  self._inflight[mid] = msg
281
229
  self._inflight_cv.notify_all()
282
- continue
283
- # Event.
284
- sid = msg.get("sessionId")
285
- buf = self._events.get(sid)
286
- if buf is None:
287
- buf = self._events.setdefault(sid, deque(maxlen=_EVENT_RING_LIMIT))
288
- buf.append({"method": msg.get("method"), "params": msg.get("params", {}), "sessionId": sid})
230
+ # Events (no id) are dropped — nothing consumes them.
289
231
  except ConnectionClosed as e:
290
232
  self._closed_reason = str(e)
291
233
  except Exception as e: # noqa: BLE001
@@ -41,12 +41,6 @@ Usage:
41
41
  browserwright -s <session-id> task <site>/<name> [--key=value ...] [--isolated]
42
42
  browserwright list-tasks [--site SITE] [--query Q] [--json]
43
43
 
44
- browserwright sub add <git-url> [--name NAME]
45
- browserwright sub list [--json]
46
- browserwright sub update [--name NAME]
47
- browserwright sub remove --name NAME
48
- browserwright release {install-local|status|list|activate} ...
49
-
50
44
  browserwright install
51
45
  browserwright doctor [--json]
52
46
  browserwright index rebuild
@@ -384,76 +378,6 @@ def _cmd_index(args: list[str]) -> int:
384
378
  return 1
385
379
 
386
380
 
387
- def _cmd_sub(args: list[str]) -> int:
388
- """``browserwright sub {add|list|update|remove} ...``."""
389
- if not args:
390
- print("usage: browserwright sub {add|list|update|remove} ...", file=sys.stderr)
391
- return 1
392
- sub = args[0]
393
- rest = args[1:]
394
- from . import subscriptions
395
-
396
- if sub == "add":
397
- if not rest or rest[0].startswith("--"):
398
- print("usage: browserwright sub add <git-url> [--name NAME]", file=sys.stderr)
399
- return 1
400
- url = rest[0]
401
- kw = _parse_kv_args(rest[1:])
402
- try:
403
- r = subscriptions.add(url, name=kw.get("name"))
404
- except subscriptions.SubscriptionError as e:
405
- print(f"sub add failed: {e}", file=sys.stderr)
406
- return 1
407
- if kw.get("json"):
408
- sys.stdout.write(json.dumps(r, default=str) + "\n")
409
- else:
410
- print(f"{r['status']}: {r['name']} → {r['path']}")
411
- return 0
412
-
413
- if sub == "list":
414
- kw = _parse_kv_args(rest)
415
- rows = subscriptions.list_all()
416
- if kw.get("json"):
417
- sys.stdout.write(json.dumps(rows, indent=2, default=str) + "\n")
418
- return 0
419
- if not rows:
420
- print("(no subscriptions)")
421
- return 0
422
- for r in rows:
423
- tag = "" if r["exists"] else " [MISSING]"
424
- print(f" {r['name']:24s} {r['url']}{tag}")
425
- return 0
426
-
427
- if sub == "update":
428
- kw = _parse_kv_args(rest)
429
- names = [kw["name"]] if kw.get("name") else None
430
- try:
431
- results = subscriptions.update(names)
432
- except subscriptions.SubscriptionError as e:
433
- print(f"sub update failed: {e}", file=sys.stderr)
434
- return 1
435
- for r in results:
436
- print(f" {r['name']:24s} {r['status']}: {r.get('detail','')}")
437
- return 0 if all(r["status"] in ("updated", "missing") for r in results) else 1
438
-
439
- if sub == "remove":
440
- kw = _parse_kv_args(rest)
441
- name = kw.get("name")
442
- if not name:
443
- print("usage: browserwright sub remove --name NAME", file=sys.stderr)
444
- return 1
445
- try:
446
- subscriptions.remove(name)
447
- except subscriptions.SubscriptionError as e:
448
- print(f"sub remove failed: {e}", file=sys.stderr)
449
- return 1
450
- print(f"removed {name}")
451
- return 0
452
-
453
- print(f"unknown sub subcommand: {sub}", file=sys.stderr)
454
- return 1
455
-
456
-
457
381
  def _cmd_memory(args: list[str]) -> int:
458
382
  if not args:
459
383
  print("usage: browserwright memory {show|forget|replace} ...", file=sys.stderr)
@@ -623,6 +547,16 @@ def _cmd_session(args: list[str], *, session_id: Optional[str] = None) -> int:
623
547
  return 1
624
548
 
625
549
 
550
+ def _fresh_screenshot_path() -> str:
551
+ """A non-colliding /tmp png path for the userscript --verify screenshot."""
552
+ i = 0
553
+ while True:
554
+ cand = Path("/tmp") / f"browserwright-shot-{os.getpid()}-{i}.png"
555
+ if not cand.exists():
556
+ return str(cand)
557
+ i += 1
558
+
559
+
626
560
  def _cmd_userscript(args: list[str], *, session_id: Optional[str] = None) -> int:
627
561
  if not args or args[0] in {"-h", "--help"}:
628
562
  sys.stdout.write(USERSCRIPT_HELP)
@@ -661,14 +595,20 @@ def _cmd_userscript(args: list[str], *, session_id: Optional[str] = None) -> int
661
595
  raise RuntimeError(
662
596
  "no drivable session bound; pass -s <id> or set BD_SESSION"
663
597
  )
664
- # These are internal driving helpers (no longer on the agent
665
- # EXPORTS surface — Phase C PR3); the userscript --verify
666
- # convenience still uses them directly from the primitive modules.
667
- from .primitives.inspect import capture_screenshot
668
- from .primitives.page import reload
669
-
670
- reload()
671
- print(capture_screenshot())
598
+ # Drive the verify through the same Playwright path the agent
599
+ # uses (the legacy CDP primitives are gone): bind the session's
600
+ # current tab, reload it, screenshot it, print the path.
601
+ from .repl.playwright_handle import PlaywrightHandle
602
+
603
+ handle = PlaywrightHandle()
604
+ try:
605
+ page = handle.page
606
+ page.reload(wait_until="load")
607
+ shot = _fresh_screenshot_path()
608
+ page.screenshot(path=shot)
609
+ print(shot)
610
+ finally:
611
+ handle.close()
672
612
  except Exception as e:
673
613
  print(f"pushed OK — --verify skipped (no drivable tab): {e}",
674
614
  file=sys.stderr)
@@ -765,88 +705,6 @@ def _extension_relay_status() -> dict | None:
765
705
  return None
766
706
 
767
707
 
768
- def _cmd_release(args: list[str]) -> int:
769
- if not args:
770
- print(
771
- "usage: browserwright release {install-local|status|list|activate} ...",
772
- file=sys.stderr,
773
- )
774
- return 1
775
- from . import release_install
776
-
777
- sub = args[0]
778
- rest = args[1:]
779
- kw = _parse_kv_args(rest)
780
- try:
781
- if sub == "install-local":
782
- info = release_install.install_local(
783
- force=bool(kw.get("force")),
784
- activate_release=not bool(kw.get("no-activate")),
785
- )
786
- if kw.get("restart-daemon") and info["actions"].get("restart_daemon"):
787
- subprocess.run(["browserwright-daemon", "restart"], check=False)
788
- if kw.get("json"):
789
- sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
790
- else:
791
- print(f"installed browserwright {info['version']} -> {info['path']}")
792
- if info.get("activated"):
793
- print("activated global entry points")
794
- if info["actions"].get("restart_daemon"):
795
- print("next: restart daemon (`browserwright-daemon restart`)")
796
- if info["actions"].get("reload_chrome_extension"):
797
- print(
798
- "next: reload Chrome unpacked extension from "
799
- f"{info.get('chrome_extension_sync', {}).get('path') or info['path'] + '/chrome-extension'}"
800
- )
801
- return 0
802
-
803
- if sub == "status":
804
- info = release_install.status()
805
- if kw.get("json"):
806
- sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
807
- return 0
808
- version = info.get("installed_version") or "(none)"
809
- print(f"installed release: {version}")
810
- daemon = info.get("daemon") or {}
811
- daemon_version = daemon.get("version") or "(not running)"
812
- suffix = " restart required" if daemon.get("restart_required") else ""
813
- print(f"running daemon: {daemon_version}{suffix}")
814
- ok = all(row.get("ok") for row in info.get("skill", []))
815
- print(f"skill install: {'copied ok' if ok else 'needs reinstall'}")
816
- return 0
817
-
818
- if sub == "list":
819
- rows = release_install.list_releases()
820
- if kw.get("json"):
821
- sys.stdout.write(json.dumps(rows, indent=2, sort_keys=True) + "\n")
822
- return 0
823
- if not rows:
824
- print("(no releases installed)")
825
- return 0
826
- for row in rows:
827
- mark = "*" if row.get("active") else " "
828
- print(f"{mark} {row.get('version')} {row.get('path')}")
829
- return 0
830
-
831
- if sub == "activate":
832
- if not rest or rest[0].startswith("--"):
833
- print("usage: browserwright release activate <version>", file=sys.stderr)
834
- return 1
835
- info = release_install.activate(rest[0])
836
- if kw.get("json"):
837
- sys.stdout.write(json.dumps(info, indent=2, sort_keys=True) + "\n")
838
- else:
839
- print(f"activated browserwright {info['version']} -> {info['path']}")
840
- print("next: restart daemon and reload Chrome extension if that release differs")
841
- return 0
842
- except release_install.ReleaseError as e:
843
- print(f"release error: {e}", file=sys.stderr)
844
- return 1
845
-
846
- print(f"unknown release subcommand: {sub}", file=sys.stderr)
847
- return 1
848
-
849
-
850
708
  def main(argv: Optional[list[str]] = None) -> None:
851
709
  argv = list(sys.argv[1:] if argv is None else argv)
852
710
 
@@ -897,10 +755,6 @@ def main(argv: Optional[list[str]] = None) -> None:
897
755
  sys.exit(_cmd_index(rest))
898
756
  if cmd == "memory":
899
757
  sys.exit(_cmd_memory(rest))
900
- if cmd == "sub":
901
- sys.exit(_cmd_sub(rest))
902
- if cmd == "release":
903
- sys.exit(_cmd_release(rest))
904
758
  if cmd == "session":
905
759
  sys.exit(_cmd_session(rest, session_id=global_session))
906
760
  if cmd == "whoami":