@vymalo/opencode-browser 0.7.0
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.
- package/LICENSE +21 -0
- package/README.md +321 -0
- package/dist/agent-client.d.ts +54 -0
- package/dist/agent-client.js +180 -0
- package/dist/agent-client.js.map +1 -0
- package/dist/broker.d.ts +70 -0
- package/dist/broker.js +457 -0
- package/dist/broker.js.map +1 -0
- package/dist/catalog.d.ts +45 -0
- package/dist/catalog.js +532 -0
- package/dist/catalog.js.map +1 -0
- package/dist/endpoint.d.ts +41 -0
- package/dist/endpoint.js +117 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.js +12 -0
- package/dist/lib.js.map +1 -0
- package/dist/logging.d.ts +15 -0
- package/dist/logging.js +69 -0
- package/dist/logging.js.map +1 -0
- package/dist/opencode.d.ts +20 -0
- package/dist/opencode.js +149 -0
- package/dist/opencode.js.map +1 -0
- package/dist/protocol.d.ts +138 -0
- package/dist/protocol.js +126 -0
- package/dist/protocol.js.map +1 -0
- package/dist/schema.d.ts +51 -0
- package/dist/schema.js +50 -0
- package/dist/schema.js.map +1 -0
- package/dist/token-file.d.ts +12 -0
- package/dist/token-file.js +40 -0
- package/dist/token-file.js.map +1 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +140 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +32 -0
- package/dist/transport.js +71 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vymalo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# @vymalo/opencode-browser
|
|
2
|
+
|
|
3
|
+
> Give an OpenCode agent hands in a real browser — open tabs, click, type, scroll, screenshot —
|
|
4
|
+
> organized into **named tab groups**.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@vymalo/opencode-browser)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
This is the **OpenCode plugin** half of a dual plugin. It registers `browser_*` tools the model
|
|
11
|
+
can call and hosts a localhost WebSocket **bridge**. A companion **browser extension**
|
|
12
|
+
(Chromium MV3 + Firefox) connects to the bridge and drives real tabs.
|
|
13
|
+
|
|
14
|
+
> Browser extensions can't host servers, so the plugin is the server and the extension dials
|
|
15
|
+
> out to it. The plugin runs on Bun (inside OpenCode) and serves the bridge with `Bun.serve`.
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart LR
|
|
19
|
+
Model[OpenCode model] -->|browser_* tools| Plugin["@vymalo/opencode-browser<br/>(bridge host)"]
|
|
20
|
+
Plugin <-->|ws://127.0.0.1:4517<br/>token handshake| Ext[Browser extension]
|
|
21
|
+
Ext -->|CDP / content script| Tabs[(Real browser tabs<br/>in named groups)]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [How it works](#how-it-works)
|
|
27
|
+
- [Install](#install)
|
|
28
|
+
- [Quickstart](#quickstart)
|
|
29
|
+
- [Options](#options)
|
|
30
|
+
- [The 33 tools](#the-33-tools)
|
|
31
|
+
- [Targeting elements](#targeting-elements--prefer-refs)
|
|
32
|
+
- [Screenshots](#screenshots)
|
|
33
|
+
- [Named groups](#named-groups)
|
|
34
|
+
- [Scoping tools per agent](#scoping-tools-per-agent)
|
|
35
|
+
- [Multiple browsers & agents](#multiple-browsers--agents)
|
|
36
|
+
- [Executors: CDP vs content-script](#executors-cdp-vs-content-script)
|
|
37
|
+
- [Stopping / releasing control](#stopping--releasing-control)
|
|
38
|
+
- [Security](#security)
|
|
39
|
+
- [Troubleshooting](#troubleshooting)
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
|
|
43
|
+
1. OpenCode loads the plugin; it binds a WebSocket bridge on `127.0.0.1:<port>` and prints a
|
|
44
|
+
shared `token` (or uses the one you set).
|
|
45
|
+
2. You load the extension once, paste the bridge URL + token into its dashboard, and connect.
|
|
46
|
+
3. The model calls a `browser_*` tool → the plugin sends a command frame over the bridge → the
|
|
47
|
+
extension executes it against a real tab → the result comes back to the model.
|
|
48
|
+
|
|
49
|
+
The extension and plugin agree on a small, dependency-free wire protocol; the same protocol is
|
|
50
|
+
mirrored into the extension so the two never drift.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
// opencode.json
|
|
56
|
+
{
|
|
57
|
+
"$schema": "https://opencode.ai/config.json",
|
|
58
|
+
"plugin": [["@vymalo/opencode-browser", { "port": 4517 }]]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On first run with no `token`, the plugin generates one and logs it once
|
|
63
|
+
(`browser_bridge_token_generated`). Paste that into the extension's dashboard along with the
|
|
64
|
+
bridge URL (`ws://127.0.0.1:4517`).
|
|
65
|
+
|
|
66
|
+
Get the extension from the [Releases page](https://github.com/vymalo/opencode-oauth2/releases)
|
|
67
|
+
(`opencode-browser-extension-<version>-chrome.zip` / `-firefox.zip`), from the Chrome Web Store /
|
|
68
|
+
Firefox Add-ons, or build it from
|
|
69
|
+
[`apps/browser-extension`](https://github.com/vymalo/opencode-oauth2/tree/main/apps/browser-extension).
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
A typical first session, end to end:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
You: Open example.com in a group called "research" and tell me the page heading.
|
|
77
|
+
Model: browser_open({ group: "research", url: "https://example.com" })
|
|
78
|
+
browser_snapshot({ group: "research" }) → refs e1, e2, …
|
|
79
|
+
browser_get_text({ group: "research" })
|
|
80
|
+
→ "Example Domain — This domain is for use in illustrative examples…"
|
|
81
|
+
|
|
82
|
+
You: Screenshot it.
|
|
83
|
+
Model: browser_screenshot({ group: "research", fullPage: true })
|
|
84
|
+
→ "Saved screenshot to .opencode/browser/research/2026-…png (1280×3200)."
|
|
85
|
+
(the model then reads that file to see it)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A titled **tab group** named "research" appears in the browser; the extension dashboard logs
|
|
89
|
+
every action and keeps a screenshot gallery.
|
|
90
|
+
|
|
91
|
+
## Options
|
|
92
|
+
|
|
93
|
+
Second argument to the plugin tuple in `opencode.json`:
|
|
94
|
+
|
|
95
|
+
| Option | Default | Meaning |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `enabled` | `true` | Master switch. |
|
|
98
|
+
| `host` | `"127.0.0.1"` | Bind interface. **Keep it loopback** — see [Security](#security). |
|
|
99
|
+
| `port` | `4517` | Bridge port. |
|
|
100
|
+
| `token` | _generated_ | Shared secret the extension must present. Empty string ⇒ generated + logged. |
|
|
101
|
+
| `groups` | `["page","control"]` | Tool groups to register (`page` \| `control` \| `debug`). |
|
|
102
|
+
| `executor` | `"auto"` | `auto` \| `cdp` \| `content` — forwarded to the extension as a preference. |
|
|
103
|
+
| `timeoutMs` | `30000` | Per-command timeout. |
|
|
104
|
+
| `screenshotDir` | `".opencode/browser"` | Screenshot output dir (relative → resolved against the worktree). |
|
|
105
|
+
|
|
106
|
+
## The 33 tools
|
|
107
|
+
|
|
108
|
+
Names are stable `browser_*` identifiers, partitioned into three **groups** gated by the
|
|
109
|
+
`groups` option. Every tool takes a `group` (the named tab group) unless noted.
|
|
110
|
+
|
|
111
|
+
### `page` — observe (8, default on)
|
|
112
|
+
|
|
113
|
+
| Tool | Key args | Does |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `browser_snapshot` | `group` | Accessibility/DOM outline with stable **refs** the other tools target. Start here. |
|
|
116
|
+
| `browser_get_text` | `group, tabId?` | Visible/readable text of the active tab. |
|
|
117
|
+
| `browser_get_html` | `group, ref?/selector?, outer?` | HTML of an element or the document. |
|
|
118
|
+
| `browser_get_attribute` | `group, ref?/selector, name?` | Tag, text, value, and attributes of an element. |
|
|
119
|
+
| `browser_query` | `group, selector, limit?` | Match a CSS selector → list of elements with fresh refs. |
|
|
120
|
+
| `browser_tabs` | `group?` | List groups and their tabs. Omit `group` for everything you own. |
|
|
121
|
+
| `browser_targets` | — | List connected browsers (for multi-browser routing). |
|
|
122
|
+
| `browser_screenshot` | `group, fullPage?, tabId?` | PNG to disk; returns the path. See [Screenshots](#screenshots). |
|
|
123
|
+
|
|
124
|
+
### `control` — drive (19, default on)
|
|
125
|
+
|
|
126
|
+
| Tool | Key args | Does |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `browser_open` | `group, url?, focus?, target?` | Open a tab in the group (creates the group on first use). |
|
|
129
|
+
| `browser_navigate` | `group, url, tabId?` | Navigate an existing tab. |
|
|
130
|
+
| `browser_back` / `browser_forward` / `browser_reload` | `group, tabId?` | History / reload. |
|
|
131
|
+
| `browser_activate` | `group, tabId?` | Bring a tab to the foreground. |
|
|
132
|
+
| `browser_click` | `group, ref?/selector?/x?,y?, button?` | Click an element or coordinates. |
|
|
133
|
+
| `browser_double_click` | `group, ref?/selector?/x?,y?` | Double-click. |
|
|
134
|
+
| `browser_hover` | `group, ref?/selector?/x?,y?` | Hover. |
|
|
135
|
+
| `browser_drag` | `group, fromRef?/fromSelector?, ref?/selector?` | Drag source → target. |
|
|
136
|
+
| `browser_type` | `group, text, ref?/selector?, submit?` | Type into a field (optionally press Enter). |
|
|
137
|
+
| `browser_fill` | `group, fields:[{ref?/selector,value}]` | Batch-fill several fields. |
|
|
138
|
+
| `browser_select` | `group, ref?/selector, value?/values?` | Choose `<select>` option(s). |
|
|
139
|
+
| `browser_press_key` | `group, key` | Press a key or chord (`"Enter"`, `"Control+a"`). |
|
|
140
|
+
| `browser_scroll` | `group, deltaX?, deltaY?, ref?, to?` | Scroll the page or an element. |
|
|
141
|
+
| `browser_upload` | `group, ref?/selector, paths:[…]` | Attach file(s) to a file input (CDP/Chromium). |
|
|
142
|
+
| `browser_wait` | `group, ms?/selector?, state?` | Wait a fixed delay or for a selector. |
|
|
143
|
+
| `browser_close` | `group, tabId?` | Close a tab, or the whole group if `tabId` omitted. |
|
|
144
|
+
| `browser_release` | `group?` | Hand the browser back (detach CDP) without closing tabs. |
|
|
145
|
+
|
|
146
|
+
### `debug` — powerful / sensitive (6, **off by default**)
|
|
147
|
+
|
|
148
|
+
Enable with `{ "groups": ["page","control","debug"] }`.
|
|
149
|
+
|
|
150
|
+
| Tool | Key args | Does |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `browser_eval` | `group, code, tabId?` | Evaluate arbitrary JavaScript in the page. |
|
|
153
|
+
| `browser_console` | `group` | Recent console messages. |
|
|
154
|
+
| `browser_network` | `group` | Recent network requests. |
|
|
155
|
+
| `browser_handle_dialog` | `group, accept?, promptText?` | Accept/dismiss a native `alert`/`confirm`/`prompt`. |
|
|
156
|
+
| `browser_set_viewport` | `group, width, height, mobile?, deviceScaleFactor?` | Emulate a viewport (CDP/Chromium only). |
|
|
157
|
+
| `browser_cookies` | `op, url?, name?, …` | Get/list/set/clear cookies (profile-wide). |
|
|
158
|
+
|
|
159
|
+
## Targeting elements — prefer refs
|
|
160
|
+
|
|
161
|
+
Two ways to address an element:
|
|
162
|
+
|
|
163
|
+
- **By ref (recommended)** — call `browser_snapshot` (or `browser_query`), get back stable refs
|
|
164
|
+
like `e1`, `e2`, then `browser_click({ group, ref: "e3" })`. Refs survive minor layout shifts
|
|
165
|
+
and are far more reliable than brittle selectors.
|
|
166
|
+
- **By CSS selector or coordinates** — `selector: "#login"` or `x`/`y`. Useful for canvases,
|
|
167
|
+
maps, and elements a snapshot doesn't expose.
|
|
168
|
+
|
|
169
|
+
## Screenshots
|
|
170
|
+
|
|
171
|
+
OpenCode tool output is **text-only** — it can't carry an image. So the plugin writes the PNG to
|
|
172
|
+
`<screenshotDir>/<group>/<timestamp>.png` and returns the path; the model then opens it with
|
|
173
|
+
OpenCode's built-in `read` tool (which renders images). `fullPage: true` captures the whole
|
|
174
|
+
scrollable page (CDP `captureBeyondViewport` on Chromium; scroll-and-stitch on the content
|
|
175
|
+
executor). The extension also keeps a copy in its dashboard gallery.
|
|
176
|
+
|
|
177
|
+
> Driving from a non-OpenCode MCP client instead? `@vymalo/opencode-browser-mcp` returns
|
|
178
|
+
> screenshots as **inline images** (MCP can carry them).
|
|
179
|
+
|
|
180
|
+
## Named groups
|
|
181
|
+
|
|
182
|
+
Every action targets a **group** — a named bucket of tabs the agent created. On Chromium groups
|
|
183
|
+
map to real `chrome.tabGroups` (titled, colored); on Firefox they're a logical registry. Groups
|
|
184
|
+
keep each task's tabs isolated and inspectable, and are the unit of ownership when multiple
|
|
185
|
+
agents share one bridge.
|
|
186
|
+
|
|
187
|
+
## Scoping tools per agent
|
|
188
|
+
|
|
189
|
+
There are **two independent levers**, and they answer different questions:
|
|
190
|
+
|
|
191
|
+
| Lever | Scope | Use it to |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `groups` plugin option | **Global** — which tools are *registered* at all | Set the org-wide default surface. |
|
|
194
|
+
| OpenCode agent `tools` map | **Per-agent** — which registered tools an agent may *call* | Give each agent only the tools it needs. |
|
|
195
|
+
|
|
196
|
+
If your team keeps **all groups enabled by default** (`"groups": ["page","control","debug"]`),
|
|
197
|
+
the `groups` option won't narrow anything — every `browser_*` tool is registered. To restrict an
|
|
198
|
+
individual agent, use OpenCode's per-agent **`tools`** map (a `{ "<tool>": true|false }` record on
|
|
199
|
+
the agent). The tool names are stable `browser_*` identifiers (see [the 33 tools](#the-33-tools)),
|
|
200
|
+
so you target them directly. This is OpenCode's own mechanism — nothing plugin-specific.
|
|
201
|
+
|
|
202
|
+
### Denylist — drop the sensitive tools (most common)
|
|
203
|
+
|
|
204
|
+
Keep everything except the `debug` group and destructive actions for a general agent:
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
// opencode.json
|
|
208
|
+
{
|
|
209
|
+
"plugin": [["@vymalo/opencode-browser", {}]], // all groups stay registered
|
|
210
|
+
"agent": {
|
|
211
|
+
"build": {
|
|
212
|
+
"tools": {
|
|
213
|
+
"browser_eval": false,
|
|
214
|
+
"browser_cookies": false,
|
|
215
|
+
"browser_console": false,
|
|
216
|
+
"browser_network": false,
|
|
217
|
+
"browser_handle_dialog": false,
|
|
218
|
+
"browser_set_viewport": false,
|
|
219
|
+
"browser_close": false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Allowlist — a read-only research agent (observe, never drive)
|
|
227
|
+
|
|
228
|
+
Disable the whole namespace with a wildcard, then re-enable only the `page` tools you want. A
|
|
229
|
+
more specific key wins over the `browser_*` wildcard:
|
|
230
|
+
|
|
231
|
+
```jsonc
|
|
232
|
+
{
|
|
233
|
+
"agent": {
|
|
234
|
+
"researcher": {
|
|
235
|
+
"description": "Reads pages, never changes them.",
|
|
236
|
+
"tools": {
|
|
237
|
+
"browser_*": false,
|
|
238
|
+
"browser_open": true,
|
|
239
|
+
"browser_navigate": true,
|
|
240
|
+
"browser_snapshot": true,
|
|
241
|
+
"browser_get_text": true,
|
|
242
|
+
"browser_query": true,
|
|
243
|
+
"browser_screenshot": true,
|
|
244
|
+
"browser_tabs": true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Markdown agents
|
|
252
|
+
|
|
253
|
+
The same `tools` map works in a `.opencode/agent/<name>.md` front-matter file:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
---
|
|
257
|
+
description: Reads pages, never changes them.
|
|
258
|
+
tools:
|
|
259
|
+
browser_*: false
|
|
260
|
+
browser_snapshot: true
|
|
261
|
+
browser_get_text: true
|
|
262
|
+
browser_screenshot: true
|
|
263
|
+
---
|
|
264
|
+
You are a read-only web researcher. Use browser_snapshot to get refs, then read.
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> The plugin-level `groups` option is still useful as a hard floor — e.g. **never** register
|
|
268
|
+
> `debug` org-wide with `{ "groups": ["page","control"] }`, then no agent can re-enable
|
|
269
|
+
> `browser_eval` no matter its `tools` map. Use `groups` for "nobody gets this," and the agent
|
|
270
|
+
> `tools` map for "this agent only gets these."
|
|
271
|
+
|
|
272
|
+
## Multiple browsers & agents
|
|
273
|
+
|
|
274
|
+
The bridge is an **auto-elect broker**: multiple agents (plugin, MCP, sessions) and multiple
|
|
275
|
+
executors (browsers) can share one bridge. The first process to bind the port becomes the host;
|
|
276
|
+
others join as guests. Groups are owned by the agent that created them (owner-exclusive), and
|
|
277
|
+
ownership is rebuilt automatically on failover. Use `browser_targets` to see connected browsers
|
|
278
|
+
and `browser_open({ target })` to pick one. Full design:
|
|
279
|
+
[`plans/multi-client-routing.md`](https://github.com/vymalo/opencode-oauth2/blob/main/plans/multi-client-routing.md).
|
|
280
|
+
|
|
281
|
+
## Executors: CDP vs content-script
|
|
282
|
+
|
|
283
|
+
- **CDP executor** (`chrome.debugger`, Chromium) — trusted input, full-page capture, console &
|
|
284
|
+
network logs. Shows Chrome's "being debugged" banner — an intentional signal.
|
|
285
|
+
- **Content-script executor** — synthetic events + `captureVisibleTab`; the Firefox-safe
|
|
286
|
+
fallback, also used when the debugger is unavailable.
|
|
287
|
+
|
|
288
|
+
`executor: "auto"` picks CDP on Chromium when the `debugger` permission is granted, else the
|
|
289
|
+
content script. Force one with `"cdp"` / `"content"`.
|
|
290
|
+
|
|
291
|
+
## Stopping / releasing control
|
|
292
|
+
|
|
293
|
+
Control is **plugin-managed** — you never disconnect by hand. It's released when:
|
|
294
|
+
|
|
295
|
+
- the model/plugin calls **`browser_release`** (mid-session), or
|
|
296
|
+
- **OpenCode exits** — the plugin's `exit` hook shuts the bridge down, and the dropped socket
|
|
297
|
+
independently makes the extension release. A hard kill releases too.
|
|
298
|
+
|
|
299
|
+
## Security
|
|
300
|
+
|
|
301
|
+
The bridge binds `127.0.0.1` only and requires a token handshake. It grants the model control of
|
|
302
|
+
a **real browser profile** — **use a dedicated or throwaway Chrome profile**, not your daily one.
|
|
303
|
+
The `chrome.debugger` banner is a deliberate, continuous indicator that automation is active.
|
|
304
|
+
`debug` tools (`browser_eval`, `browser_cookies`, …) are off by default for this reason. See the
|
|
305
|
+
consolidated [`docs/security.md`](https://github.com/vymalo/opencode-oauth2/blob/main/docs/security.md).
|
|
306
|
+
|
|
307
|
+
## Troubleshooting
|
|
308
|
+
|
|
309
|
+
| Symptom | Likely cause |
|
|
310
|
+
| --- | --- |
|
|
311
|
+
| Extension won't connect | URL/token mismatch, or another process already on the port. Re-copy the logged token. |
|
|
312
|
+
| "being debugged" banner stays after a session | Normal until release; `browser_release` or exiting OpenCode clears it. |
|
|
313
|
+
| `set_viewport` / full-page differs on Firefox | CDP-only features; Firefox uses the content executor. |
|
|
314
|
+
| Screenshot path returned but model "can't see" it | The model must `read` the path — output is text-only. |
|
|
315
|
+
|
|
316
|
+
Symptom-keyed fixes: [`docs/troubleshooting.md`](https://github.com/vymalo/opencode-oauth2/blob/main/docs/troubleshooting.md).
|
|
317
|
+
Full architecture, wire protocol, and tool reference: [`docs/browser.md`](https://github.com/vymalo/opencode-oauth2/blob/main/docs/browser.md).
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
[MIT](https://github.com/vymalo/opencode-oauth2/blob/main/LICENSE) © vymalo contributors
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AgentEndpoint } from "./broker.js";
|
|
2
|
+
import type { Logger } from "./logging.js";
|
|
3
|
+
import { type BrowserAction } from "./protocol.js";
|
|
4
|
+
/** Minimal socket the agent-client drives — provided per runtime by the endpoint. */
|
|
5
|
+
export interface AgentSocket {
|
|
6
|
+
send(data: string): void;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface AgentSocketHandlers {
|
|
10
|
+
onOpen(): void;
|
|
11
|
+
onMessage(data: string): void;
|
|
12
|
+
onClose(): void;
|
|
13
|
+
}
|
|
14
|
+
export type AgentSocketFactory = (url: string, handlers: AgentSocketHandlers) => AgentSocket;
|
|
15
|
+
export interface AgentClientOptions {
|
|
16
|
+
url: string;
|
|
17
|
+
token: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentClientDeps {
|
|
22
|
+
logger: Logger;
|
|
23
|
+
createSocket: AgentSocketFactory;
|
|
24
|
+
/** Called when the connection drops (host gone) — the endpoint re-elects. */
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare class AgentClientError extends Error {
|
|
28
|
+
readonly code?: string;
|
|
29
|
+
constructor(message: string, code?: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Connects to a running broker as an `agent` and brokers request/response for the
|
|
33
|
+
* host adapter's tools (the guest path). One connection attempt + lifecycle; the
|
|
34
|
+
* endpoint orchestrates re-election/reconnect via `onClose`.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AgentClient implements AgentEndpoint {
|
|
37
|
+
private readonly opts;
|
|
38
|
+
private readonly deps;
|
|
39
|
+
private socket;
|
|
40
|
+
private ready;
|
|
41
|
+
private closed;
|
|
42
|
+
private readonly pending;
|
|
43
|
+
private readyWaiters;
|
|
44
|
+
constructor(opts: AgentClientOptions, deps: AgentClientDeps);
|
|
45
|
+
connect(): Promise<void>;
|
|
46
|
+
stop(): void;
|
|
47
|
+
release(): void;
|
|
48
|
+
send(action: BrowserAction, group: string, params: Record<string, unknown>, signal?: AbortSignal, target?: string): Promise<unknown>;
|
|
49
|
+
private waitReady;
|
|
50
|
+
private handleResult;
|
|
51
|
+
private clearPending;
|
|
52
|
+
private settleReject;
|
|
53
|
+
private rejectAllPending;
|
|
54
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { decodeFrame, encodeFrame, helloFrame, nextId, PROTOCOL_VERSION } from "./protocol.js";
|
|
2
|
+
export class AgentClientError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, code) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AgentClientError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Connects to a running broker as an `agent` and brokers request/response for the
|
|
12
|
+
* host adapter's tools (the guest path). One connection attempt + lifecycle; the
|
|
13
|
+
* endpoint orchestrates re-election/reconnect via `onClose`.
|
|
14
|
+
*/
|
|
15
|
+
export class AgentClient {
|
|
16
|
+
opts;
|
|
17
|
+
deps;
|
|
18
|
+
socket = null;
|
|
19
|
+
ready = false;
|
|
20
|
+
closed = false;
|
|
21
|
+
pending = new Map();
|
|
22
|
+
readyWaiters = [];
|
|
23
|
+
constructor(opts, deps) {
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
this.deps = deps;
|
|
26
|
+
}
|
|
27
|
+
connect() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let settled = false;
|
|
30
|
+
this.socket = this.deps.createSocket(this.opts.url, {
|
|
31
|
+
onOpen: () => {
|
|
32
|
+
this.socket?.send(encodeFrame(helloFrame(this.opts.token, { role: "agent", client: this.opts.label })));
|
|
33
|
+
// Resolve only once the broker replies `ready`.
|
|
34
|
+
},
|
|
35
|
+
onMessage: (data) => {
|
|
36
|
+
const frame = decodeFrame(data);
|
|
37
|
+
if (!frame) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (frame.type === "ready") {
|
|
41
|
+
this.ready = true;
|
|
42
|
+
for (const w of this.readyWaiters.splice(0)) {
|
|
43
|
+
w();
|
|
44
|
+
}
|
|
45
|
+
if (!settled) {
|
|
46
|
+
settled = true;
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (frame.type === "result") {
|
|
51
|
+
this.handleResult(frame);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
onClose: () => {
|
|
55
|
+
this.ready = false;
|
|
56
|
+
this.rejectAllPending(new AgentClientError("disconnected from bridge", "disconnected"));
|
|
57
|
+
this.socket = null;
|
|
58
|
+
if (!settled) {
|
|
59
|
+
settled = true;
|
|
60
|
+
reject(new AgentClientError("bridge connection closed before ready", "connect_failed"));
|
|
61
|
+
}
|
|
62
|
+
if (!this.closed) {
|
|
63
|
+
this.deps.onClose?.();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
stop() {
|
|
70
|
+
this.closed = true;
|
|
71
|
+
this.socket?.close();
|
|
72
|
+
this.socket = null;
|
|
73
|
+
this.rejectAllPending(new AgentClientError("agent stopped", "stopped"));
|
|
74
|
+
}
|
|
75
|
+
release() {
|
|
76
|
+
// Best-effort: ask the broker to release this agent's browsers.
|
|
77
|
+
void this.send("release", "", {}).catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
async send(action, group, params, signal, target) {
|
|
80
|
+
if (signal?.aborted) {
|
|
81
|
+
throw new AgentClientError("aborted", "aborted");
|
|
82
|
+
}
|
|
83
|
+
await this.waitReady(signal);
|
|
84
|
+
const socket = this.socket;
|
|
85
|
+
if (!socket) {
|
|
86
|
+
throw new AgentClientError("not connected to the bridge", "disconnected");
|
|
87
|
+
}
|
|
88
|
+
const id = nextId();
|
|
89
|
+
const frame = {
|
|
90
|
+
v: PROTOCOL_VERSION,
|
|
91
|
+
type: "command",
|
|
92
|
+
id,
|
|
93
|
+
action,
|
|
94
|
+
group,
|
|
95
|
+
params,
|
|
96
|
+
target
|
|
97
|
+
};
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const timer = this.opts.timeoutMs > 0
|
|
100
|
+
? setTimeout(() => this.settleReject(id, new AgentClientError(`command '${action}' timed out`, "timeout")), this.opts.timeoutMs)
|
|
101
|
+
: null;
|
|
102
|
+
let detachAbort = null;
|
|
103
|
+
if (signal) {
|
|
104
|
+
const onAbort = () => this.settleReject(id, new AgentClientError("aborted", "aborted"));
|
|
105
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
106
|
+
detachAbort = () => signal.removeEventListener("abort", onAbort);
|
|
107
|
+
}
|
|
108
|
+
this.pending.set(id, { resolve, reject, timer, detachAbort });
|
|
109
|
+
try {
|
|
110
|
+
socket.send(encodeFrame(frame));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
this.settleReject(id, new AgentClientError(`failed to send '${action}': ${err instanceof Error ? err.message : String(err)}`, "send_failed"));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
waitReady(signal) {
|
|
118
|
+
if (this.ready) {
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
if (this.closed || !this.socket) {
|
|
122
|
+
return Promise.reject(new AgentClientError("not connected to the bridge", "disconnected"));
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const onAbort = () => reject(new AgentClientError("aborted", "aborted"));
|
|
126
|
+
if (signal) {
|
|
127
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
}
|
|
129
|
+
this.readyWaiters.push(() => {
|
|
130
|
+
if (signal) {
|
|
131
|
+
signal.removeEventListener("abort", onAbort);
|
|
132
|
+
}
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
handleResult(frame) {
|
|
138
|
+
const p = this.pending.get(frame.id);
|
|
139
|
+
if (!p) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.clearPending(frame.id);
|
|
143
|
+
if (frame.ok) {
|
|
144
|
+
p.resolve(frame.data);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
p.reject(new AgentClientError(frame.error?.message ?? "bridge error", frame.error?.code));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
clearPending(id) {
|
|
151
|
+
const p = this.pending.get(id);
|
|
152
|
+
if (!p) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (p.timer) {
|
|
156
|
+
clearTimeout(p.timer);
|
|
157
|
+
}
|
|
158
|
+
p.detachAbort?.();
|
|
159
|
+
this.pending.delete(id);
|
|
160
|
+
}
|
|
161
|
+
settleReject(id, err) {
|
|
162
|
+
const p = this.pending.get(id);
|
|
163
|
+
if (!p) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.clearPending(id);
|
|
167
|
+
p.reject(err);
|
|
168
|
+
}
|
|
169
|
+
rejectAllPending(err) {
|
|
170
|
+
for (const [id, p] of this.pending) {
|
|
171
|
+
if (p.timer) {
|
|
172
|
+
clearTimeout(p.timer);
|
|
173
|
+
}
|
|
174
|
+
p.detachAbort?.();
|
|
175
|
+
this.pending.delete(id);
|
|
176
|
+
p.reject(err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=agent-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-client.js","sourceRoot":"","sources":["../src/agent-client.ts"],"names":[],"mappings":"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"}
|
package/dist/broker.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Logger } from "./logging.js";
|
|
2
|
+
import { type BrowserAction } from "./protocol.js";
|
|
3
|
+
import type { BridgeTransport } from "./transport.js";
|
|
4
|
+
/** Thrown when a command can't be routed or the executor reports failure. */
|
|
5
|
+
export declare class BrokerError extends Error {
|
|
6
|
+
readonly code?: string;
|
|
7
|
+
constructor(message: string, code?: string);
|
|
8
|
+
}
|
|
9
|
+
/** What an agent (local or remote) uses to drive browsers through the broker. */
|
|
10
|
+
export interface AgentEndpoint {
|
|
11
|
+
send(action: BrowserAction, group: string, params: Record<string, unknown>, signal?: AbortSignal, target?: string): Promise<unknown>;
|
|
12
|
+
/** Release this agent's browsers (detach the debugger) without closing tabs. */
|
|
13
|
+
release(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface BrokerOptions {
|
|
16
|
+
host: string;
|
|
17
|
+
port: number;
|
|
18
|
+
token: string;
|
|
19
|
+
/** Executor preference forwarded to extensions in `ready`. */
|
|
20
|
+
executor?: "auto" | "cdp" | "content";
|
|
21
|
+
/** Per-command timeout in ms; `<= 0` disables. */
|
|
22
|
+
timeoutMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface BrokerDeps {
|
|
25
|
+
logger: Logger;
|
|
26
|
+
transport: BridgeTransport;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Hosts the WebSocket server and routes commands between **agents** (producers:
|
|
30
|
+
* the plugin, the MCP server, guest adapters) and **executors** (the browser
|
|
31
|
+
* extensions), keyed by named-group ownership. Generalizes the old single-client
|
|
32
|
+
* bridge. Runtime-agnostic via the injected transport.
|
|
33
|
+
*/
|
|
34
|
+
export declare class Broker {
|
|
35
|
+
private readonly opts;
|
|
36
|
+
private readonly deps;
|
|
37
|
+
private readonly executors;
|
|
38
|
+
private readonly executorById;
|
|
39
|
+
private readonly agents;
|
|
40
|
+
private readonly groupOwner;
|
|
41
|
+
private readonly pending;
|
|
42
|
+
private primaryExecutorId;
|
|
43
|
+
private agentSeq;
|
|
44
|
+
private execSeq;
|
|
45
|
+
private started;
|
|
46
|
+
constructor(opts: BrokerOptions, deps: BrokerDeps);
|
|
47
|
+
get executorCount(): number;
|
|
48
|
+
start(): Promise<void>;
|
|
49
|
+
stop(): void;
|
|
50
|
+
/** An in-process agent (the host adapter's own tools), bypassing a socket. */
|
|
51
|
+
createLocalAgent(): AgentEndpoint;
|
|
52
|
+
private onMessage;
|
|
53
|
+
private handleHello;
|
|
54
|
+
private rebuildFromExecutor;
|
|
55
|
+
private onClose;
|
|
56
|
+
private route;
|
|
57
|
+
private resolveExecutor;
|
|
58
|
+
private pickExecutor;
|
|
59
|
+
private sendToExecutor;
|
|
60
|
+
private handleResult;
|
|
61
|
+
private handleAgentCommand;
|
|
62
|
+
private routeEvent;
|
|
63
|
+
private listTargets;
|
|
64
|
+
private aggregateTabs;
|
|
65
|
+
private releaseForAgent;
|
|
66
|
+
private safeSend;
|
|
67
|
+
private clearPending;
|
|
68
|
+
private settleReject;
|
|
69
|
+
private rejectAllPending;
|
|
70
|
+
}
|