yoke-mcp 0.1.2 → 0.1.4
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/PRIVACY.md +20 -6
- package/README.md +97 -9
- package/ROADMAP.md +2 -1
- package/dist/browsers.d.ts +80 -0
- package/dist/browsers.js +232 -0
- package/dist/browsers.js.map +1 -0
- package/dist/cli.js +18 -7
- package/dist/cli.js.map +1 -1
- package/dist/doctor.js +76 -26
- package/dist/doctor.js.map +1 -1
- package/dist/mcp-server.d.ts +17 -1
- package/dist/mcp-server.js +127 -27
- package/dist/mcp-server.js.map +1 -1
- package/dist/native-host.d.ts +1 -1
- package/dist/native-host.js +95 -16
- package/dist/native-host.js.map +1 -1
- package/dist/protocol.d.ts +18 -0
- package/dist/protocol.js.map +1 -1
- package/dist/socket-client.d.ts +4 -5
- package/dist/socket-client.js +7 -18
- package/dist/socket-client.js.map +1 -1
- package/dist/socket-path.d.ts +22 -1
- package/dist/socket-path.js +74 -6
- package/dist/socket-path.js.map +1 -1
- package/extension/browser/background.js +94 -13
- package/extension/browser/cdp.js +31 -0
- package/extension/browser/identity.js +78 -0
- package/extension/browser/popup.js +55 -0
- package/extension/manifest.json +2 -2
- package/extension/popup.html +25 -0
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Privacy policy for Yoke
|
|
2
2
|
|
|
3
|
-
Last updated:
|
|
3
|
+
Last updated: 5 September 2026
|
|
4
4
|
|
|
5
5
|
Yoke handles a lot of what is on your screen, and sends none of it to us.
|
|
6
6
|
|
|
@@ -32,8 +32,8 @@ To one place: the local program you connected. The path is
|
|
|
32
32
|
|
|
33
33
|
extension -> native messaging host -> Unix socket -> MCP server -> your client
|
|
34
34
|
|
|
35
|
-
Every hop is on your computer. The
|
|
36
|
-
your user account. Nothing is written to a remote service by Yoke, and the
|
|
35
|
+
Every hop is on your computer. The sockets, one per Chrome profile, live in a
|
|
36
|
+
directory readable only by your user account. Nothing is written to a remote service by Yoke, and the
|
|
37
37
|
extension makes no network requests of its own.
|
|
38
38
|
|
|
39
39
|
What your client then does with what it receives is outside Yoke's control and
|
|
@@ -49,6 +49,11 @@ Almost nothing, and nothing that outlives the browser:
|
|
|
49
49
|
- Console messages and network requests for tabs being driven, kept in memory
|
|
50
50
|
only, capped at 500 entries per tab, and discarded when the tab closes, when
|
|
51
51
|
the debugger detaches, or when Chrome restarts the extension.
|
|
52
|
+
- A random eight character id for the Chrome profile the extension runs in, and
|
|
53
|
+
the label you type for it in the popup, kept in the extension's own storage in
|
|
54
|
+
that profile. The id names the local socket for that profile and appears in
|
|
55
|
+
tool output so a client can tell two profiles apart. It is not derived from
|
|
56
|
+
anything about you and is not sent anywhere.
|
|
52
57
|
- No cookies, credentials, passwords, form values or browsing history are
|
|
53
58
|
recorded or persisted anywhere by Yoke.
|
|
54
59
|
|
|
@@ -56,9 +61,18 @@ The values of password fields are deliberately excluded from what Yoke reports
|
|
|
56
61
|
about a page. The field is described so a client can type into it; its contents
|
|
57
62
|
are not returned.
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
URL fields in tool output are reduced to origin and path, because query strings
|
|
65
|
+
routinely carry session tokens. `list_tabs` accepts `full_urls` to opt out of
|
|
66
|
+
that, and it is the only tool that does: everywhere else the reduction is
|
|
67
|
+
unconditional.
|
|
68
|
+
|
|
69
|
+
Redaction covers URL fields and not whole payloads, which is worth stating
|
|
70
|
+
plainly rather than leaving to be discovered. A page's own text comes back as it
|
|
71
|
+
is, so a URL printed on the page arrives whole. So do console messages, and the
|
|
72
|
+
value of any expression a caller evaluates, including `location.href` or
|
|
73
|
+
`document.cookie`. Screenshots are pixels and are not filtered at all. The
|
|
74
|
+
redaction stops Yoke from volunteering a URL, and it is not a filter on what a
|
|
75
|
+
page can tell a caller that asks.
|
|
62
76
|
|
|
63
77
|
## Permissions and why each exists
|
|
64
78
|
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[](package.json)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Yoke drives the Chrome you are already using: an extension plus a native messaging host, so there is no browser to launch, no second profile, and no browser-wide debugging port. Chrome spawns the host itself when the extension connects, and the endpoint in between (a Unix socket at mode 0600 inside a directory whose 0700 mode is verified, or a named pipe on Windows) exists only while that connection is live. There is one per Chrome profile, so two profiles with the extension loaded are both reachable and told apart. It does use the Chrome DevTools Protocol, one tab at a time, through the extension's own `debugger` permission, which is why a tab with DevTools open cannot be driven.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
`list_tabs` returns every tab in every window of every connected Chrome profile, and every operation on an existing tab names a `tab_id`, because there is no acting on the active tab when none was named. Any tab yoke drives or reads joins a visible tab group called `yoke`, and `read_page` and `find` describe a password field without returning its value. URL fields in tool output are cut to origin and path by default, with `full_urls: true` on `list_tabs` as the way to opt out, and the package has zero runtime dependencies on Node 22 or later.
|
|
13
13
|
|
|
14
14
|
Chrome requires a three part bridge:
|
|
15
15
|
|
|
@@ -21,6 +21,83 @@ The MCP client starts `yoke mcp`. Chrome starts the native messaging host when t
|
|
|
21
21
|
|
|
22
22
|
The project and repository are named `yoke`. The npm package is `yoke-mcp` because the bare package name belongs to an unrelated project. The command is `yoke`, and the extension is displayed as Yoke.
|
|
23
23
|
|
|
24
|
+
## How this differs
|
|
25
|
+
|
|
26
|
+
### Explicit tab targeting
|
|
27
|
+
|
|
28
|
+
Every operation on an existing tab requires a `tab_id`, with no active tab fallback. `open_tab` creates a tab and therefore has no existing id to name.
|
|
29
|
+
|
|
30
|
+
### Your own Chrome profile
|
|
31
|
+
|
|
32
|
+
Yoke runs through an extension in the Chrome profile you are already using. It can reach every tab in every window of that profile, with its existing cookies, logged in sessions, and other extensions. Load it in a second profile and both are reachable at once: `list_browsers` names each, `list_tabs` covers both, and a tab id is enough to route a call to the profile that owns it, because Chrome numbers tabs once for the whole browser.
|
|
33
|
+
|
|
34
|
+
### No browser-wide debugging endpoint
|
|
35
|
+
|
|
36
|
+
Yoke uses `chrome.debugger`, which is Chrome's extension API for the Chrome DevTools Protocol. It attaches to one tab at a time and does not require a Chrome launch flag or a browser-wide remote debugging port. The local bridge is a per-profile Unix socket or Windows named pipe that exists only while Chrome has the extension connected.
|
|
37
|
+
|
|
38
|
+
### Built for a human and an agent in the same browser
|
|
39
|
+
|
|
40
|
+
Any tab yoke drives or reads joins a visible tab group named `yoke`. Grouping is a visual marker and is not used to find a tab. Chrome permits one debugger client per tab, so yoke cannot drive a tab while DevTools is open on it.
|
|
41
|
+
|
|
42
|
+
### Privacy-conscious defaults
|
|
43
|
+
|
|
44
|
+
URL fields are reduced to origin and path by default, and `list_tabs` can opt out with `full_urls: true`. This redaction does not filter page text, console messages, evaluated JavaScript results, or screenshot pixels. `read_page` and `find` describe password fields without returning their values.
|
|
45
|
+
|
|
46
|
+
### Small and auditable
|
|
47
|
+
|
|
48
|
+
The package declares zero runtime dependencies and uses Node builtins only. The
|
|
49
|
+
published 0.1.3 tarball is 43 files, all first-party.
|
|
50
|
+
|
|
51
|
+
TypeScript throughout with `strict` and `noUncheckedIndexedAccess` on. The wire
|
|
52
|
+
shapes are declared once in `src/protocol.ts` and imported by both halves, so a
|
|
53
|
+
request the server can send is one the extension is compiled to answer, and the
|
|
54
|
+
compiler is what enforces that rather than a comment asking nicely.
|
|
55
|
+
|
|
56
|
+
The permissions and what each one buys are in a table further down, along with
|
|
57
|
+
the native messaging setup and the reasons `debugger` and host access are
|
|
58
|
+
required. None of that makes yoke safe: it drives a browser you are signed into,
|
|
59
|
+
which is privileged by construction. It is documented so the trade is yours to
|
|
60
|
+
judge rather than yours to discover.
|
|
61
|
+
|
|
62
|
+
### Compared with other browser MCP servers
|
|
63
|
+
|
|
64
|
+
The cells describe the verified versions and their defaults. Flags that change an answer are named in the cell or the numbered notes.
|
|
65
|
+
|
|
66
|
+
| Capability | Yoke (npm 0.1.3) | @playwright/mcp 0.0.79 (2026-08) | chrome-devtools-mcp 1.7.0 (pub. 2026-08-10) | BrowserMCP: server 0.1.3 (2025-04-11), extension 1.3.4 (2025-05-07), unmaintained |
|
|
67
|
+
| --- | --- | --- | --- | --- |
|
|
68
|
+
| Runs in the Chrome profile you are already signed in to | Yes. Chrome spawns the host via `connectNative`; there is no browser-launch code | No by default: its own persistent profile keyed to the client's cwd [1]. Yes with `--extension` | No by default: its own profile at `~/.cache/chrome-devtools-mcp/chrome-profile`. Yes with `--autoConnect` [2] | Yes. A Web Store extension in the running browser |
|
|
69
|
+
| Reaches tabs that were already open | Yes, every tab in every window of the Chrome profile that owns the endpoint [3] | No by default (fresh browser, so those tabs do not exist in it). Yes with `--extension` [4] | No by default. With `--autoConnect`, all windows of one profile [2] | One tab at a time, picked by the human in the popup. No tab listing and no tab creation tool [5] |
|
|
70
|
+
| Explicit target on every operation | Yes, `tab_id`, with no active-tab fallback anywhere [6] | No. Implicit current page, switched by `browser_tabs` with a positional index [7] | No. Server-side selected page via `select_page` [8] | No. The single connected tab; no tab id in any of the 12 exposed tool schemas |
|
|
71
|
+
| Needs a Chrome launch flag or a browser-wide debugging port | No | No for the default launch and for `--extension`; yes for `--cdp-endpoint` [9] | Yes on all three attach paths; no when it launches its own browser over a pipe [10] | No |
|
|
72
|
+
| How it reaches the browser, and what listens locally | Native messaging. A per-user endpoint (Unix socket mode 0600 in a directory whose 0700 mode is verified; a named pipe on Windows) that exists only while Chrome has the extension connected. CDP one tab at a time via `chrome.debugger` 1.3 | Playwright drives a browser it launched. `--extension` starts an in-process CDP relay the extension dials over WebSocket | Puppeteer over a pipe for its own browser; a CDP HTTP endpoint or `ws://` URL for the three attach paths | WebSocket server on fixed port 9009, bound to all interfaces, no auth and no origin check, and it kills whatever already holds that port [11] |
|
|
73
|
+
| Runs with your existing cookies and logged-in session | Yes | No by default. Yes with `--extension` [12] | Only with `--autoConnect` [13] | Yes |
|
|
74
|
+
| Marks the tabs it touches | Yes. Any tab it drives or reads joins a visible tab group named "yoke" [14] | No in-tab marker. A separate headed window on its own profile, with automation signals actively suppressed [15] | Nothing added by the server. Chrome's own affordances only, and on `--autoConnect` that is a one-time consent dialog rather than an ongoing marker [16] | Chrome's own debugger infobar, incidental rather than designed, and suppressible [17] |
|
|
75
|
+
| URL redaction on by default | Yes. URL fields are cut to origin plus path; opt out with `full_urls` on `list_tabs` [18] | No. The only mechanism is opt-in substitution of known secret values (`--secrets`) | No, at no setting. Header redaction exists, covers headers only, and is off by default | No. The full page URL is in every snapshot header |
|
|
76
|
+
| Declared runtime dependencies, and install footprint | 0 declared, Node builtins only. The published 0.1.3 tarball is 43 files, all first-party | 2 declared, both pinned to a 1.63.0 alpha. 19 MB installed. No browser download on the default `chrome` channel | 0 declared, and roughly 14 MB of vendored bundle (puppeteer 25.5.0, lighthouse 13.4.0) [19] | 5 declared, 97 transitive as resolved on 2026-08-24, and rising with no new release [20] |
|
|
77
|
+
|
|
78
|
+
#### Notes, including every place a flag or a version changes the answer
|
|
79
|
+
|
|
80
|
+
1. `@playwright/mcp` defaults to `browserName: chromium` with `channel: 'chrome'`, so it drives system-installed Google Chrome, but in `~/Library/Caches/ms-playwright-mcp/mcp-{channel}-{sha256(cwd)[0:7]}`. That profile persists between runs and accumulates its own logins, so it is not your profile and not a throwaway either. Its own `--help` text and README call it a temporary directory, which is wrong. Different project directories silently get different profiles.
|
|
81
|
+
2. `--autoConnect` needs Chrome 144 or later plus a manual toggle at `chrome://inspect/#remote-debugging`, and it attaches to one profile (the default, as Chrome determines it), not to every window on the machine. The documented `--browser-url` path steers users away from their real profile: the README tells them to pass `--user-data-dir` so their browsing data is not exposed.
|
|
82
|
+
3. Scoped to one Chrome profile, because the endpoint path is per user and not per profile. On published 0.1.2 a second profile with the extension loaded could take the endpoint silently. Published 0.1.3 fixes that failure mode. The next release gives each profile its own endpoint, described under Several Chrome profiles below.
|
|
83
|
+
4. `--extension` genuinely attaches to every tab the extension already knows about (`chrome.debugger.attach` per tab), so the honest answer is not by default, and supported with `--extension`.
|
|
84
|
+
5. Only from extension v1.3.3 (2025-04-14) could BrowserMCP automate tabs that existed before the extension was installed. The selected tab id is persisted in extension storage and survives a server restart.
|
|
85
|
+
6. `open_tab` creates the tab, so there is no id to name, and it lands in Chrome's current window. `scroll` without a `ref` uses a fixed viewport point, still inside the named tab.
|
|
86
|
+
7. `browser_click`'s `target` is an element reference from a prior snapshot rather than a tab handle. Element targeting is explicit there; page targeting is not.
|
|
87
|
+
8. `--experimentalPageIdRouting` (default false, inert under `--slim`) injects `pageId` into page-scoped tools, so the stateful selection is a default rather than an architectural limit: a selected page by default, with opt-in `pageId` routing.
|
|
88
|
+
9. `--browser-url` on chrome-devtools-mcp also requires closing every running Chrome first. `@playwright/mcp`'s `--extension` route is gated differently rather than ungated: it throws unless the Playwright Extension is present in the profile.
|
|
89
|
+
10. The three attach paths are `--autoConnect` (reads `DevToolsActivePort` from the user data dir after the manual toggle), `--browserUrl` (needs `--remote-debugging-port=9222` and a non-default user data dir), and `--wsEndpoint`. There is no fourth. The self-launched browser passes `pipe: true` and opens no port.
|
|
90
|
+
11. Verified empirically: `address()` returns `{"address":"::"}`. Startup runs `lsof -ti:9009 | xargs kill -9`. A second connection silently closes the first.
|
|
91
|
+
12. `@playwright/mcp` does not disable extensions: its default chromium switch list contains `--disable-extensions` and the MCP launch path cancels it through `ignoreDefaultArgs`. It carries none of your extensions because the profile is fresh, not because extensions are off. That is why the extensions half was cut from this row's title.
|
|
92
|
+
13. On chrome-devtools-mcp 1.7.0 the two halves land on mutually exclusive paths: session state needs `--autoConnect`, while the extension tooling is documented as unsupported on `autoConnect`, `browserUrl` and `wsEndpoint` until Chrome 149 and works only over the pipe connection, that is the dedicated profile that holds no user extensions. Yoke and BrowserMCP carry the user's other extensions by construction; BrowserMCP has had to fix breakage from 1Password and LastPass injecting elements into snapshots.
|
|
93
|
+
14. Every operation that drives or reads a tab marks it: `navigate`, `get_page_text`, `read_page`, `find`, `run_javascript`, `screenshot`, `click`, `type_text`, `press_key`, `scroll`, `read_console`, `read_network`, and `open_tab` groups the tab it creates. Deliberately unmarked: `list_tabs`, `list_tab_groups`, `close_tab`, `ping`, `release_tab`, `ungroup_tabs`. The tab group is present in published 0.1.3. Chrome's own "started debugging this browser" banner also appears when the debugger attaches, and yoke does not suppress it. That is Chrome's behaviour rather than a guarantee from either project: it is not stated on Chrome's `chrome.debugger` reference page. The same caveat applies to the BrowserMCP cell.
|
|
94
|
+
15. Headed is the default (headless is auto-selected only on Linux with no `DISPLAY`). The server pushes `--disable-blink-features=AutomationControlled`, `--disable-infobars` is in the default switch list, and `--enable-automation` is absent. The indication is structural (a separate window on a separate profile) rather than a badge. What `--extension` does here could not be established from the package, so this row says nothing about it.
|
|
95
|
+
16. chrome-devtools-mcp adds no title change, overlay or badge of its own, and it inherits puppeteer's default args, which contain `--enable-automation` and `--disable-infobars` in the same array. Whether Chrome still draws the infobar with both flags present was not tested, so this row claims nothing either way.
|
|
96
|
+
17. Chrome's `--silent-debugger-extension-api` and force-install via `ExtensionInstallForcelist` both remove the infobar, so in those configurations the answer becomes No. BrowserMCP ships no indicator of its own.
|
|
97
|
+
18. Redaction covers URL fields, not the whole payload. Unredacted by design: `read_console` message text, `run_javascript` return values (so `location.href` or `document.cookie` comes back whole, with no flag needed), the page body from `get_page_text`, and screenshot pixels. `full_urls` exists on `list_tabs` only; everywhere else redaction is unconditional.
|
|
98
|
+
19. This is the devDependencies trap in reverse. `npm view chrome-devtools-mcp@1.7.0 dependencies` is empty and the tarball's `package.json` has no `dependencies` key, so a reader who runs `npm ls` against a "substantial" claim finds nothing. The substance is real but vendored by rollup, not installed. Neither chrome-devtools-mcp nor `@playwright/mcp` downloads a browser on its default path: both use system Chrome.
|
|
99
|
+
20. Five direct deps, none of them the five `workspace:*` entries (those are unpublished devDependencies bundled into `dist` at build time). The transitive count grows with no release because `@modelcontextprotocol/sdk ^1.8.0` floats and now resolves to 1.30.0, pulling in express 5.2.1.
|
|
100
|
+
|
|
24
101
|
## Why yoke exists
|
|
25
102
|
|
|
26
103
|
yoke grew out of measurements of Anthropic's Claude in Chrome MCP bridge. That bridge could only see and act on tabs in its own managed group, and each reply included its full tab list with raw URLs. Those limits could not be fixed by an outside client. yoke addresses every tab by id and redacts URLs by default. [MOTIVATION.md](MOTIVATION.md) records the observations behind those choices.
|
|
@@ -76,7 +153,7 @@ When setup is complete, the last line is:
|
|
|
76
153
|
Working. Every link in the chain answered.
|
|
77
154
|
```
|
|
78
155
|
|
|
79
|
-
If something is wrong, `doctor` stops after the first broken link, names it, and prints a suggested fix. It checks the compiled host, browser registration, local
|
|
156
|
+
If something is wrong, `doctor` stops after the first broken link, names it, and prints a suggested fix. It checks the compiled host, browser registration, the local sockets, which profiles answered, and for each profile whether the extension replies and tabs are visible.
|
|
80
157
|
|
|
81
158
|
For a shorter connection check, run:
|
|
82
159
|
|
|
@@ -111,11 +188,11 @@ Clients use different names and locations for their MCP configuration. If you do
|
|
|
111
188
|
|
|
112
189
|
## What it can do
|
|
113
190
|
|
|
114
|
-
yoke exposes
|
|
191
|
+
yoke exposes 20 MCP tools. They are grouped here by the job they help with.
|
|
115
192
|
|
|
116
193
|
| Job | Tools | What they do |
|
|
117
194
|
| --- | --- | --- |
|
|
118
|
-
| Work with tabs | `list_tabs`, `list_tab_groups`, `open_tab`, `navigate`, `close_tab`, `group_tabs`, `ungroup_tabs` | See every window, open background tabs, visit HTTP or HTTPS URLs, close tabs, and manage Chrome tab groups. |
|
|
195
|
+
| Work with tabs | `list_browsers`, `list_tabs`, `list_tab_groups`, `open_tab`, `navigate`, `close_tab`, `group_tabs`, `ungroup_tabs` | See which profiles are connected and every window in each, open background tabs, visit HTTP or HTTPS URLs, close tabs, and manage Chrome tab groups. |
|
|
119
196
|
| Read a page | `get_page_text`, `read_page`, `find`, `screenshot` | Read visible text, describe interactive elements, find a matching element, or capture a foreground or background tab. |
|
|
120
197
|
| Act on a page | `click`, `type_text`, `press_key`, `scroll`, `run_javascript` | Use trusted input by element reference, scroll the page, or evaluate JavaScript in the page's own world. |
|
|
121
198
|
| Debug a page | `read_console`, `read_network`, `release_tab` | Read recorded console and network activity, then detach yoke and clear that tab's buffers. |
|
|
@@ -126,6 +203,16 @@ Any tool that acts on an existing tab requires an explicit `tab_id` obtained fro
|
|
|
126
203
|
|
|
127
204
|
`navigate` and `open_tab` accept HTTP and HTTPS URLs. New tabs open in the background unless the caller asks to focus one.
|
|
128
205
|
|
|
206
|
+
### Several Chrome profiles
|
|
207
|
+
|
|
208
|
+
Chrome starts one native host per profile, and each host listens on its own endpoint named after an id the extension mints the first time it runs in that profile. `list_browsers` lists every profile that answers, with its id, the label you gave it in the popup, its tab and window counts, and the sites it mostly has open, so a profile is recognisable before it has a name.
|
|
209
|
+
|
|
210
|
+
With one profile connected nothing changes. With more, `list_tabs` and `list_tab_groups` include every profile and add a `browser` column, and every tool that names a `tab_id` reaches the profile holding that tab without being told which: Chrome allocates tab ids from one counter for the whole browser, so two profiles never share one. `open_tab` names no existing tab, so with more than one profile connected it needs `browser`, an id or label from `list_browsers`, and refuses rather than picking.
|
|
211
|
+
|
|
212
|
+
Two different browsers, such as Chrome and Brave, do number tabs independently. A tab id both report is marked in `list_tabs` and refused by the tab tools, naming both browsers.
|
|
213
|
+
|
|
214
|
+
The id and label are stored in the extension's own storage in that profile. They never leave your machine.
|
|
215
|
+
|
|
129
216
|
### Tab groups
|
|
130
217
|
|
|
131
218
|
Every tab opened by yoke goes into a cyan group titled `yoke`, so the tab strip names what is driving those tabs. Pass `group_title` to `open_tab` for a different label, which is worth doing when more than one agent works in the same browser. The extension creates that group when needed and reuses a group with the same title in the same window. Reuse comes from Chrome's current group state, so it still works after Chrome has stopped and restarted the extension service worker. You do not collect a row of identical group pills.
|
|
@@ -159,13 +246,13 @@ Chrome's own pages and the Chrome Web Store still block page script injection. y
|
|
|
159
246
|
|
|
160
247
|
An extension cannot create trusted input through page JavaScript. A click made by a content script reaches the page with `isTrusted: false`, and many sites ignore it. `chrome.debugger` is the extension route to `Input.dispatchMouseEvent`, which creates input the page cannot distinguish from a person's input.
|
|
161
248
|
|
|
162
|
-
Chrome shows its "started debugging this browser" bar when yoke drives a tab. This is expected. Chrome also permits one debugger client per tab. A tab with DevTools open cannot be driven by yoke, and DevTools cannot open on a tab while yoke is attached. Call `release_tab` to detach yoke
|
|
249
|
+
Chrome shows its "started debugging this browser" bar when yoke drives a tab. This is expected. Chrome also permits one debugger client per tab. A tab with DevTools open cannot be driven by yoke, and DevTools cannot open on a tab while yoke is attached. Call `release_tab` to detach yoke and clear that tab's console and network buffers. The tab stays in its group.
|
|
163
250
|
|
|
164
251
|
The same permission lets `screenshot` capture a background tab. `chrome.tabs.captureVisibleTab` can only capture the active tab in a window.
|
|
165
252
|
|
|
166
253
|
## Local connection and process lifetime
|
|
167
254
|
|
|
168
|
-
The bridge does not open a TCP port. On macOS and Linux,
|
|
255
|
+
The bridge does not open a TCP port. On macOS and Linux, each native host listens on a Unix socket inside a directory readable only by your user account, one socket per Chrome profile. The socket itself is also restricted to that account. Windows uses a named pipe.
|
|
169
256
|
|
|
170
257
|
Chrome owns the native host process. It starts the host when the extension calls `connectNative` and stops it when that connection closes. This is why the Unix socket between the MCP server and host is required.
|
|
171
258
|
|
|
@@ -179,7 +266,7 @@ After `npm link`, the command surface is:
|
|
|
179
266
|
| --- | --- |
|
|
180
267
|
| `yoke install` | Register the native messaging host with each detected Chrome family browser. |
|
|
181
268
|
| `yoke doctor` | Check each link from the build through tab visibility and suggest the first fix. |
|
|
182
|
-
| `yoke status` | Report
|
|
269
|
+
| `yoke status` | Report which Chrome profiles answer, one line each with id, label, tab count and socket path. |
|
|
183
270
|
| `yoke uninstall` | Remove the native host registration. |
|
|
184
271
|
| `yoke mcp` | Run the MCP server over standard input and standard output. This is what an MCP client starts. |
|
|
185
272
|
|
|
@@ -193,7 +280,8 @@ From a checkout that has not been linked, replace `yoke` with `node dist/cli.js`
|
|
|
193
280
|
4. A driven tab shows Chrome's debugging bar and cannot share its debugger slot with DevTools.
|
|
194
281
|
5. Element references expire after navigation or a page render. Console and network history starts when yoke first attaches to that tab, not before.
|
|
195
282
|
6. Screenshots capture the page viewport, not browser chrome such as the address bar or tab strip.
|
|
196
|
-
7.
|
|
283
|
+
7. Two different browsers that both run the extension, such as Chrome and Brave, number their tabs independently. A tab id both report is refused rather than routed, because it cannot be sent to both. Two profiles of the same Chrome never collide.
|
|
284
|
+
8. Chrome's internal pages cannot be read or driven. Neither can the Chrome Web Store or the extensions gallery: Chrome refuses both `chrome.scripting` and `chrome.debugger` there, so no extension can automate them, including this one. Publishing an extension is therefore a manual job by design.
|
|
197
285
|
|
|
198
286
|
## Uninstall
|
|
199
287
|
|
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
yoke is a Chrome extension and local MCP server for driving the browser you are already signed in to. It gives any MCP client one
|
|
3
|
+
yoke is a Chrome extension and local MCP server for driving the browser you are already signed in to. Every operation names an explicit tab id, every tab in every window is reachable, and it needs no second profile and no browser-wide debugging port. It gives any MCP client one interface to tabs, page content, trusted input, screenshots, console output and network activity.
|
|
4
4
|
|
|
5
5
|
This roadmap records what has been built and what comes next. Each release is small, shippable on its own, and the non-goals are part of the plan.
|
|
6
6
|
|
|
@@ -39,6 +39,7 @@ Theme: prove the thesis. Built, working end to end, not yet tagged or published.
|
|
|
39
39
|
- The extension id is pinned by a key in its manifest, because native messaging allowlists by id and an unpacked load would otherwise get a fresh one each time. The private key never enters the repository, and CI asserts the key still derives the allowlisted id.
|
|
40
40
|
- An icon, at 16, 32, 48 and 128, drawn on an 8 unit grid so every line lands on a pixel boundary at 16.
|
|
41
41
|
- Offline tests for the MCP surface and the redaction, driven without a browser. CI green on Node 22 and 24 across Linux, macOS and Windows.
|
|
42
|
+
- Since 0.1.3, unreleased: one endpoint per Chrome profile, named after an id the extension mints and keeps. `list_browsers` names each connected profile, `list_tabs` covers all of them, tab tools route by tab id, and `open_tab` takes `browser` when there is a choice. Closes the wrong-profile case in #6 and #8, where a single per-user endpoint was won by whichever profile connected first.
|
|
42
43
|
|
|
43
44
|
The acceptance test is one number, and it holds: `list_tabs` returns every tab in the browser rather than the handful inside a managed group. On the machine this was built against that is 41 tabs, all of them addressable.
|
|
44
45
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ArgsOf, OperationName, ResultOf, TabInfo } from './protocol.js';
|
|
2
|
+
export interface Browser {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
/** Random, minted by the extension, stable for the life of the profile. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** Typed into the popup by the person. Empty until they do. */
|
|
7
|
+
label: string;
|
|
8
|
+
windows: number;
|
|
9
|
+
tabs: number;
|
|
10
|
+
/** The extension predates `identify`, so id and label are the host's fallback. */
|
|
11
|
+
unidentified: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A tab with the browser it lives in. */
|
|
14
|
+
export interface PlacedTab extends TabInfo {
|
|
15
|
+
browser: Browser;
|
|
16
|
+
}
|
|
17
|
+
/** The name a person or a model would use for a browser in output. */
|
|
18
|
+
export declare const displayName: (browser: Browser) => string;
|
|
19
|
+
/** Refused because the tab is not, or is ambiguously, anywhere. */
|
|
20
|
+
export declare class NoSuchTab extends Error {
|
|
21
|
+
readonly name = "NoSuchTab";
|
|
22
|
+
}
|
|
23
|
+
/** Refused because the caller named no browser when it had to, or named one that is not there. */
|
|
24
|
+
export declare class NoSuchBrowser extends Error {
|
|
25
|
+
readonly name = "NoSuchBrowser";
|
|
26
|
+
}
|
|
27
|
+
/** Drops every cached fact. For tests, which change the endpoint directory. */
|
|
28
|
+
export declare function forget(): void;
|
|
29
|
+
/** Every browser that answers, in a stable order. */
|
|
30
|
+
export declare function connectedBrowsers(): Promise<Browser[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Every tab in every connected browser, each knowing where it lives.
|
|
33
|
+
*
|
|
34
|
+
* Also refreshes the routing table, so a list is what teaches the router about
|
|
35
|
+
* new tabs, and a tool called on a tab nobody has listed yet triggers one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function allTabs(): Promise<{
|
|
38
|
+
browsers: Browser[];
|
|
39
|
+
tabs: PlacedTab[];
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* The one browser a tabless call should go to.
|
|
43
|
+
*
|
|
44
|
+
* With one connected there is nothing to choose. With more, the caller has to
|
|
45
|
+
* say, by id or by label, because picking for them is how a tab ends up opened
|
|
46
|
+
* in a profile nobody is looking at.
|
|
47
|
+
*/
|
|
48
|
+
export declare function chooseBrowser(selector: unknown, tool: string): Promise<Browser>;
|
|
49
|
+
/**
|
|
50
|
+
* Forgets where a tab id was placed.
|
|
51
|
+
*
|
|
52
|
+
* For a tab that appeared without a listing, which only open_tab does. Two
|
|
53
|
+
* browsers number tabs independently, so the id it hands out can be one another
|
|
54
|
+
* browser already holds, and a cached placement for it would send the next
|
|
55
|
+
* call there. Forgetting forces a listing, which is where collisions are seen.
|
|
56
|
+
*/
|
|
57
|
+
export declare function unplace(tabId: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* The endpoint holding a tab.
|
|
60
|
+
*
|
|
61
|
+
* The routing table is filled by listing, so a tab the caller learnt about from
|
|
62
|
+
* list_tabs is already placed. One that is not gets a fresh listing before it is
|
|
63
|
+
* declared missing, because a tab the person opened a moment ago is a perfectly
|
|
64
|
+
* good thing to be asked about.
|
|
65
|
+
*/
|
|
66
|
+
export declare function endpointForTab(tabId: number): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* The one endpoint holding every one of these tabs.
|
|
69
|
+
*
|
|
70
|
+
* Refused when they are spread across browsers, with each tab's browser named,
|
|
71
|
+
* because an operation on several tabs goes to one browser and the others
|
|
72
|
+
* would fail there with a message about a tab that does exist, elsewhere.
|
|
73
|
+
*/
|
|
74
|
+
export declare function endpointForTabs(tabIds: number[]): Promise<string>;
|
|
75
|
+
/** Sends an operation to whichever browser holds the tab it names. */
|
|
76
|
+
export declare function askTab<K extends OperationName>(op: K, args: ArgsOf<K> & {
|
|
77
|
+
tabId: number;
|
|
78
|
+
}, options?: {
|
|
79
|
+
timeoutMs?: number;
|
|
80
|
+
}): Promise<ResultOf<K>>;
|
package/dist/browsers.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Which Chrome profile a call goes to.
|
|
2
|
+
//
|
|
3
|
+
// Chrome starts one native host per profile, so with the extension loaded in
|
|
4
|
+
// two profiles there are two endpoints, and a caller has to end up at the right
|
|
5
|
+
// one. The rule is the one the rest of the project already lives by: a tab is
|
|
6
|
+
// named by id, and the id decides. Chrome hands out tab ids from one counter for
|
|
7
|
+
// the whole browser process, so two profiles of the same Chrome never share one,
|
|
8
|
+
// and the server can route on it without a selected browser or a `browser`
|
|
9
|
+
// argument on every tool. Only a call that names no tab, open_tab, has to be
|
|
10
|
+
// told where.
|
|
11
|
+
//
|
|
12
|
+
// Two different browsers (Chrome and Brave, say) do each have their own counter,
|
|
13
|
+
// so a collision is possible there. It is detected and refused rather than
|
|
14
|
+
// guessed at.
|
|
15
|
+
import { LEGACY_ID, endpointIdOf, listEndpoints } from './socket-path.js';
|
|
16
|
+
import { ask, ExtensionUnavailable } from './socket-client.js';
|
|
17
|
+
/** The name a person or a model would use for a browser in output. */
|
|
18
|
+
export const displayName = (browser) => browser.label === '' ? browser.id : browser.label;
|
|
19
|
+
/** Refused because the tab is not, or is ambiguously, anywhere. */
|
|
20
|
+
export class NoSuchTab extends Error {
|
|
21
|
+
name = 'NoSuchTab';
|
|
22
|
+
}
|
|
23
|
+
/** Refused because the caller named no browser when it had to, or named one that is not there. */
|
|
24
|
+
export class NoSuchBrowser extends Error {
|
|
25
|
+
name = 'NoSuchBrowser';
|
|
26
|
+
}
|
|
27
|
+
/** Stands in for an endpoint when two browsers both claim the tab id. */
|
|
28
|
+
const AMBIGUOUS = '\0ambiguous';
|
|
29
|
+
/**
|
|
30
|
+
* Which endpoint holds each tab id, as of the last time anyone looked.
|
|
31
|
+
*
|
|
32
|
+
* The only thing remembered between calls. Identity is asked for afresh each
|
|
33
|
+
* time a browser is listed, because the label is typed into the popup while the
|
|
34
|
+
* server is running, and a cached one would keep answering with the old name.
|
|
35
|
+
*/
|
|
36
|
+
let placement = new Map();
|
|
37
|
+
/** The endpoints the placement was built from, so a change in them invalidates it. */
|
|
38
|
+
let placedFrom = '';
|
|
39
|
+
/** The browsers as of the last listing, for naming an endpoint in a refusal. */
|
|
40
|
+
let lastBrowsers = new Map();
|
|
41
|
+
const endpointSignature = () => listEndpoints().join('\n');
|
|
42
|
+
/** Drops every cached fact. For tests, which change the endpoint directory. */
|
|
43
|
+
export function forget() {
|
|
44
|
+
placement = new Map();
|
|
45
|
+
placedFrom = '';
|
|
46
|
+
lastBrowsers = new Map();
|
|
47
|
+
}
|
|
48
|
+
async function identify(endpoint) {
|
|
49
|
+
try {
|
|
50
|
+
const who = await ask(endpoint, 'identify', {}, { timeoutMs: 3_000 });
|
|
51
|
+
return { endpoint, ...who, unidentified: false };
|
|
52
|
+
}
|
|
53
|
+
catch (failure) {
|
|
54
|
+
// A dead socket is a corpse and is skipped. A live host whose extension
|
|
55
|
+
// does not know the op is a browser we can still drive, so it is kept under
|
|
56
|
+
// the id the host itself fell back to.
|
|
57
|
+
if (failure instanceof ExtensionUnavailable) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
endpoint,
|
|
62
|
+
id: endpointIdOf(endpoint) ?? LEGACY_ID,
|
|
63
|
+
label: '',
|
|
64
|
+
windows: 0,
|
|
65
|
+
tabs: 0,
|
|
66
|
+
unidentified: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Every browser that answers, in a stable order. */
|
|
71
|
+
export async function connectedBrowsers() {
|
|
72
|
+
const answered = await Promise.all(listEndpoints().map((endpoint) => identify(endpoint)));
|
|
73
|
+
return answered.filter((browser) => browser !== undefined);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Every tab in every connected browser, each knowing where it lives.
|
|
77
|
+
*
|
|
78
|
+
* Also refreshes the routing table, so a list is what teaches the router about
|
|
79
|
+
* new tabs, and a tool called on a tab nobody has listed yet triggers one.
|
|
80
|
+
*/
|
|
81
|
+
export async function allTabs() {
|
|
82
|
+
const browsers = await connectedBrowsers();
|
|
83
|
+
const listed = await Promise.all(browsers.map(async (browser) => {
|
|
84
|
+
try {
|
|
85
|
+
const { tabs } = await ask(browser.endpoint, 'listTabs', {}, { timeoutMs: 5_000 });
|
|
86
|
+
return tabs.map((tab) => ({ ...tab, browser }));
|
|
87
|
+
}
|
|
88
|
+
catch (failure) {
|
|
89
|
+
// Gone between identify and now. Not an error in the listing.
|
|
90
|
+
if (failure instanceof ExtensionUnavailable) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
throw failure;
|
|
94
|
+
}
|
|
95
|
+
}));
|
|
96
|
+
const tabs = listed.flat();
|
|
97
|
+
placement = new Map();
|
|
98
|
+
placedFrom = endpointSignature();
|
|
99
|
+
lastBrowsers = new Map(browsers.map((browser) => [browser.endpoint, browser]));
|
|
100
|
+
for (const tab of tabs) {
|
|
101
|
+
const already = placement.get(tab.id);
|
|
102
|
+
if (already === undefined) {
|
|
103
|
+
placement.set(tab.id, tab.browser.endpoint);
|
|
104
|
+
}
|
|
105
|
+
else if (already !== tab.browser.endpoint) {
|
|
106
|
+
// Two browsers with the same id: marked so nothing routes to either.
|
|
107
|
+
placement.set(tab.id, AMBIGUOUS);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { browsers, tabs };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The one browser a tabless call should go to.
|
|
114
|
+
*
|
|
115
|
+
* With one connected there is nothing to choose. With more, the caller has to
|
|
116
|
+
* say, by id or by label, because picking for them is how a tab ends up opened
|
|
117
|
+
* in a profile nobody is looking at.
|
|
118
|
+
*/
|
|
119
|
+
export async function chooseBrowser(selector, tool) {
|
|
120
|
+
const browsers = await connectedBrowsers();
|
|
121
|
+
if (browsers.length === 0) {
|
|
122
|
+
throw new ExtensionUnavailable('the extension is not connected. Run `yoke install`, then load it in Chrome.');
|
|
123
|
+
}
|
|
124
|
+
if (selector === undefined || selector === '') {
|
|
125
|
+
const only = browsers[0];
|
|
126
|
+
if (browsers.length === 1 && only !== undefined) {
|
|
127
|
+
return only;
|
|
128
|
+
}
|
|
129
|
+
const names = browsers.map((browser) => `${displayName(browser)} (${browser.tabs} tab(s))`).join(', ');
|
|
130
|
+
throw new NoSuchBrowser(`${browsers.length} browsers are connected: ${names}. ${tool} opens a tab in one of them, so pass `
|
|
131
|
+
+ 'browser with an id or label from list_browsers.');
|
|
132
|
+
}
|
|
133
|
+
const wanted = String(selector);
|
|
134
|
+
const byId = browsers.find((browser) => browser.id === wanted);
|
|
135
|
+
if (byId !== undefined) {
|
|
136
|
+
return byId;
|
|
137
|
+
}
|
|
138
|
+
// Labels are typed by a person and nothing stops two profiles getting the
|
|
139
|
+
// same one, so a label that fits more than one is refused with the ids,
|
|
140
|
+
// which cannot collide.
|
|
141
|
+
const byLabel = browsers.filter((browser) => browser.label.toLowerCase() === wanted.toLowerCase());
|
|
142
|
+
if (byLabel.length > 1) {
|
|
143
|
+
const ids = byLabel.map((browser) => `${browser.id} (${browser.tabs} tab(s))`).join(', ');
|
|
144
|
+
throw new NoSuchBrowser(`${byLabel.length} connected browsers are labelled ${JSON.stringify(wanted)}: ${ids}. Pass the id of the one you mean.`);
|
|
145
|
+
}
|
|
146
|
+
const match = byLabel[0];
|
|
147
|
+
if (match === undefined) {
|
|
148
|
+
const names = browsers.map((browser) => displayName(browser)).join(', ');
|
|
149
|
+
throw new NoSuchBrowser(`no connected browser is ${JSON.stringify(wanted)}. Connected: ${names}.`);
|
|
150
|
+
}
|
|
151
|
+
return match;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Forgets where a tab id was placed.
|
|
155
|
+
*
|
|
156
|
+
* For a tab that appeared without a listing, which only open_tab does. Two
|
|
157
|
+
* browsers number tabs independently, so the id it hands out can be one another
|
|
158
|
+
* browser already holds, and a cached placement for it would send the next
|
|
159
|
+
* call there. Forgetting forces a listing, which is where collisions are seen.
|
|
160
|
+
*/
|
|
161
|
+
export function unplace(tabId) {
|
|
162
|
+
placement.delete(tabId);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The endpoint holding a tab.
|
|
166
|
+
*
|
|
167
|
+
* The routing table is filled by listing, so a tab the caller learnt about from
|
|
168
|
+
* list_tabs is already placed. One that is not gets a fresh listing before it is
|
|
169
|
+
* declared missing, because a tab the person opened a moment ago is a perfectly
|
|
170
|
+
* good thing to be asked about.
|
|
171
|
+
*/
|
|
172
|
+
export async function endpointForTab(tabId) {
|
|
173
|
+
// A browser that connected or left since the placement was built can have
|
|
174
|
+
// introduced a collision the cache knows nothing about, so the cache is
|
|
175
|
+
// trusted only while the endpoints it was built from are the ones on disk.
|
|
176
|
+
if (placement.get(tabId) === undefined || endpointSignature() !== placedFrom) {
|
|
177
|
+
await allTabs();
|
|
178
|
+
}
|
|
179
|
+
const endpoint = placement.get(tabId);
|
|
180
|
+
if (endpoint === AMBIGUOUS) {
|
|
181
|
+
const { tabs } = await allTabs();
|
|
182
|
+
const owners = tabs.filter((tab) => tab.id === tabId).map((tab) => displayName(tab.browser));
|
|
183
|
+
throw new NoSuchTab(`tab ${tabId} exists in ${owners.length} connected browsers at once (${owners.join(', ')}), which `
|
|
184
|
+
+ 'happens when two different browsers both run the extension. Nothing is sent, because it '
|
|
185
|
+
+ 'cannot be sent to both. Disable the extension in one of them, or close that tab in one.');
|
|
186
|
+
}
|
|
187
|
+
if (endpoint !== undefined) {
|
|
188
|
+
return endpoint;
|
|
189
|
+
}
|
|
190
|
+
if ((await connectedBrowsers()).length === 0) {
|
|
191
|
+
throw new ExtensionUnavailable('the extension is not connected. Run `yoke install`, then load it in Chrome.');
|
|
192
|
+
}
|
|
193
|
+
throw new NoSuchTab(`tab ${tabId} is not open in any connected browser. Call list_tabs for ids that are.`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The one endpoint holding every one of these tabs.
|
|
197
|
+
*
|
|
198
|
+
* Refused when they are spread across browsers, with each tab's browser named,
|
|
199
|
+
* because an operation on several tabs goes to one browser and the others
|
|
200
|
+
* would fail there with a message about a tab that does exist, elsewhere.
|
|
201
|
+
*/
|
|
202
|
+
export async function endpointForTabs(tabIds) {
|
|
203
|
+
const endpoints = await Promise.all(tabIds.map((tabId) => endpointForTab(tabId)));
|
|
204
|
+
const distinct = new Set(endpoints);
|
|
205
|
+
const only = endpoints[0];
|
|
206
|
+
if (distinct.size === 1 && only !== undefined) {
|
|
207
|
+
return only;
|
|
208
|
+
}
|
|
209
|
+
const where = tabIds.map((tabId, index) => {
|
|
210
|
+
const browser = lastBrowsers.get(endpoints[index] ?? '');
|
|
211
|
+
return `tab ${tabId} is in ${browser === undefined ? 'an unknown browser' : displayName(browser)}`;
|
|
212
|
+
});
|
|
213
|
+
throw new NoSuchTab(`those ${tabIds.length} tabs are in ${distinct.size} different browsers: ${where.join(', ')}. `
|
|
214
|
+
+ 'An operation on several tabs goes to one browser, so name tabs from one at a time.');
|
|
215
|
+
}
|
|
216
|
+
/** Sends an operation to whichever browser holds the tab it names. */
|
|
217
|
+
export async function askTab(op, args, options = {}) {
|
|
218
|
+
const endpoint = await endpointForTab(args.tabId);
|
|
219
|
+
try {
|
|
220
|
+
return await ask(endpoint, op, args, options);
|
|
221
|
+
}
|
|
222
|
+
catch (failure) {
|
|
223
|
+
// The host may have gone since the tab was placed. Once more from a fresh
|
|
224
|
+
// listing, and after that the failure is the answer.
|
|
225
|
+
if (!(failure instanceof ExtensionUnavailable)) {
|
|
226
|
+
throw failure;
|
|
227
|
+
}
|
|
228
|
+
placement.delete(args.tabId);
|
|
229
|
+
return ask(await endpointForTab(args.tabId), op, args, options);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=browsers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browsers.js","sourceRoot":"","sources":["../src/browsers.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,8EAA8E;AAC9E,iFAAiF;AACjF,iFAAiF;AACjF,2EAA2E;AAC3E,6EAA6E;AAC7E,cAAc;AACd,EAAE;AACF,iFAAiF;AACjF,2EAA2E;AAC3E,cAAc;AACd,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,GAAG,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAoB/D,sEAAsE;AACtE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAAgB,EAAU,EAAE,CACtD,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;AAEpD,mEAAmE;AACnE,MAAM,OAAO,SAAU,SAAQ,KAAK;IAChB,IAAI,GAAG,WAAW,CAAC;CACtC;AAED,kGAAkG;AAClG,MAAM,OAAO,aAAc,SAAQ,KAAK;IACpB,IAAI,GAAG,eAAe,CAAC;CAC1C;AAED,yEAAyE;AACzE,MAAM,SAAS,GAAG,aAAa,CAAC;AAEhC;;;;;;GAMG;AACH,IAAI,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;AAC1C,sFAAsF;AACtF,IAAI,UAAU,GAAG,EAAE,CAAC;AACpB,gFAAgF;AAChF,IAAI,YAAY,GAAG,IAAI,GAAG,EAAmB,CAAC;AAE9C,MAAM,iBAAiB,GAAG,GAAW,EAAE,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEnE,+EAA+E;AAC/E,MAAM,UAAU,MAAM;IACpB,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;IACtB,UAAU,GAAG,EAAE,CAAC;IAChB,YAAY,GAAG,IAAI,GAAG,EAAE,CAAC;AAC3B,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,QAAgB;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,OAAO,EAAE,QAAQ,EAAE,GAAG,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,OAAO,EAAE,CAAC;QACjB,wEAAwE;QACxE,4EAA4E;QAC5E,uCAAuC;QACvC,IAAI,OAAO,YAAY,oBAAoB,EAAE,CAAC;YAAC,OAAO,SAAS,CAAC;QAAC,CAAC;QAClE,OAAO;YACL,QAAQ;YACR,EAAE,EAAE,YAAY,CAAC,QAAQ,CAAC,IAAI,SAAS;YACvC,KAAK,EAAE,EAAE;YACT,OAAO,EAAE,CAAC;YACV,IAAI,EAAE,CAAC;YACP,YAAY,EAAE,IAAI;SACnB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC1F,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAsB,EAAE,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC;AACjF,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,MAAM,QAAQ,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QAC9D,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;YACnF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAa,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QAC7D,CAAC;QAAC,OAAO,OAAO,EAAE,CAAC;YACjB,8DAA8D;YAC9D,IAAI,OAAO,YAAY,oBAAoB,EAAE,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;YAC3D,MAAM,OAAO,CAAC;QAChB,CAAC;IACH,CAAC,CAAC,CAAC,CAAC;IACJ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3B,SAAS,GAAG,IAAI,GAAG,EAAE,CAAC;IACtB,UAAU,GAAG,iBAAiB,EAAE,CAAC;IACjC,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IAC/E,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,OAAO,KAAK,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC5C,qEAAqE;YACrE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAiB,EAAE,IAAY;IACjE,MAAM,QAAQ,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC3C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,oBAAoB,CAC5B,6EAA6E,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QAC9C,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YAAC,OAAO,IAAI,CAAC;QAAC,CAAC;QACjE,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvG,MAAM,IAAI,aAAa,CACrB,GAAG,QAAQ,CAAC,MAAM,4BAA4B,KAAK,KAAK,IAAI,uCAAuC;cACjG,iDAAiD,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC;IAC/D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IACxC,0EAA0E;IAC1E,wEAAwE;IACxE,wBAAwB;IACxB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACnG,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1F,MAAM,IAAI,aAAa,CACrB,GAAG,OAAO,CAAC,MAAM,oCAAoC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,GAAG,oCAAoC,CAAC,CAAC;IAC7H,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzE,MAAM,IAAI,aAAa,CAAC,2BAA2B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,gBAAgB,KAAK,GAAG,CAAC,CAAC;IACrG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAa;IAChD,0EAA0E;IAC1E,wEAAwE;IACxE,2EAA2E;IAC3E,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,iBAAiB,EAAE,KAAK,UAAU,EAAE,CAAC;QAC7E,MAAM,OAAO,EAAE,CAAC;IAClB,CAAC;IACD,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,OAAO,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7F,MAAM,IAAI,SAAS,CACjB,OAAO,KAAK,cAAc,MAAM,CAAC,MAAM,gCAAgC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW;cACjG,0FAA0F;cAC1F,yFAAyF,CAAC,CAAC;IACjG,CAAC;IACD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAAC,OAAO,QAAQ,CAAC;IAAC,CAAC;IAChD,IAAI,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,oBAAoB,CAC5B,6EAA6E,CAAC,CAAC;IACnF,CAAC;IACD,MAAM,IAAI,SAAS,CAAC,OAAO,KAAK,yEAAyE,CAAC,CAAC;AAC7G,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAgB;IACpD,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QACxC,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACzD,OAAO,OAAO,KAAK,UAAU,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IACrG,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,SAAS,CACjB,SAAS,MAAM,CAAC,MAAM,gBAAgB,QAAQ,CAAC,IAAI,wBAAwB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;UAC7F,oFAAoF,CAAC,CAAC;AAC5F,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,EAAK,EACL,IAAmC,EACnC,UAAkC,EAAE;IAEpC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAAC,OAAO,OAAO,EAAE,CAAC;QACjB,0EAA0E;QAC1E,qDAAqD;QACrD,IAAI,CAAC,CAAC,OAAO,YAAY,oBAAoB,CAAC,EAAE,CAAC;YAAC,MAAM,OAAO,CAAC;QAAC,CAAC;QAClE,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,OAAO,GAAG,CAAC,MAAM,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAClE,CAAC;AACH,CAAC"}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { install, uninstall } from './install.js';
|
|
7
|
-
import {
|
|
7
|
+
import { endpointDir } from './socket-path.js';
|
|
8
8
|
const USAGE = `yoke - browser automation in the Chrome you are already signed in to.
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
@@ -87,15 +87,26 @@ async function main() {
|
|
|
87
87
|
return checks.every((check) => check.ok) ? EXIT.OK : EXIT.UNAVAILABLE;
|
|
88
88
|
}
|
|
89
89
|
if (action === 'status') {
|
|
90
|
-
const {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const { connectedBrowsers } = await import('./browsers.js');
|
|
91
|
+
const browsers = await connectedBrowsers();
|
|
92
|
+
if (browsers.length === 0) {
|
|
93
|
+
process.stdout.write('extension: not reachable\n');
|
|
94
|
+
process.stdout.write(`endpoints: ${endpointDir()}\n`);
|
|
95
95
|
process.stdout.write('\nIf install has run and the extension is loaded, open chrome://extensions and '
|
|
96
96
|
+ 'check its service worker is running. Chrome starts the host on demand.\n');
|
|
97
|
+
return EXIT.UNAVAILABLE;
|
|
98
|
+
}
|
|
99
|
+
// One line per profile, because with two connected "connected" alone is
|
|
100
|
+
// exactly the answer that hid the wrong-profile case.
|
|
101
|
+
process.stdout.write(`extension: connected, ${browsers.length} browser(s)\n`);
|
|
102
|
+
for (const browser of browsers) {
|
|
103
|
+
const label = browser.unidentified
|
|
104
|
+
? '(legacy endpoint; reload the extension so this profile gets its own)'
|
|
105
|
+
: browser.label === '' ? '(unnamed)' : browser.label;
|
|
106
|
+
const tabs = browser.unidentified ? 'tabs unknown' : `${browser.tabs} tab(s)`;
|
|
107
|
+
process.stdout.write(` ${browser.id}\t${label}\t${tabs}\t${browser.endpoint}\n`);
|
|
97
108
|
}
|
|
98
|
-
return
|
|
109
|
+
return EXIT.OK;
|
|
99
110
|
}
|
|
100
111
|
process.stdout.write(USAGE);
|
|
101
112
|
return action === undefined || action === '-h' || action === '--help' ? EXIT.OK : EXIT.USAGE;
|