qz-browser-agent 0.1.0 → 0.1.2
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/README.md +91 -318
- package/package.json +2 -2
- package/src/args.mjs +18 -2
- package/src/bin.mjs +5 -7
- package/src/browser-registry.mjs +101 -0
- package/src/catalog.json +29 -31
- package/src/cli.mjs +97 -19
- package/src/client.mjs +15 -3
- package/src/commands.mjs +8 -5
- package/src/daemon.mjs +18 -3
- package/src/package-info.mjs +1 -0
- package/src/server.mjs +18 -8
- package/src/state.mjs +136 -32
- package/src/task-stream.mjs +5 -3
- package/src/terminal.mjs +38 -16
- package/src/tools.mjs +20 -29
package/README.md
CHANGED
|
@@ -1,353 +1,126 @@
|
|
|
1
|
-
# qz
|
|
1
|
+
# qz
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Requires Node.js 24 or later and Chrome. CLI and MCP share the same browser connection.
|
|
3
|
+
An open-source browser agent for humans and coding agents.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
npx qz-browser-agent@latest install
|
|
12
|
-
npx qz-browser-agent@latest connect
|
|
13
|
-
npx qz-browser-agent@latest create "Summarize the current page." --stream
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
`install` opens the [latest extension release](https://github.com/alibaba/qz-browser-agent/releases/latest).
|
|
17
|
-
Extract its ZIP, open `chrome://extensions`, enable **Developer mode**, and choose **Load unpacked**.
|
|
18
|
-
Select the extracted extension folder. Open qz and accept its first-use data notice.
|
|
19
|
-
|
|
20
|
-
`connect` starts the local MCP service and opens a connection page if a browser has not connected.
|
|
21
|
-
Open that page in the Chrome profile where qz is installed. The connection page identifies the
|
|
22
|
-
local service to the extension; no token or DevTools configuration is required.
|
|
23
|
-
|
|
24
|
-
The examples and suggested next commands use `qz`. Replace it with
|
|
25
|
-
`npx qz-browser-agent@latest`, or install the command globally:
|
|
5
|
+
qz is a local-first extension for Chrome and Edge.
|
|
6
|
+
People use it from the side panel; agents such as Claude Code, Codex, and pi hand it web tasks
|
|
7
|
+
through this CLI or MCP. qz works in your own browser, with your existing logins, and returns
|
|
8
|
+
tables, files, or reports with sources. Everything runs locally, and your data goes only to the
|
|
9
|
+
model you choose.
|
|
26
10
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
On Windows, use PowerShell 7.3 or newer with its default native argument passing. After a global
|
|
33
|
-
install, use `qz.ps1` for commands containing JSON or quoted names; Windows command suggestions
|
|
34
|
-
use that wrapper. For automation, pin an exact npm version such as `qz-browser-agent@0.1.0`.
|
|
35
|
-
|
|
36
|
-
## Connect and stop
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
qz connect
|
|
40
|
-
qz status
|
|
41
|
-
qz mcp
|
|
42
|
-
qz stop
|
|
43
|
-
```
|
|
11
|
+
[Website](https://alibaba.github.io/qz-browser-agent/) ·
|
|
12
|
+
[GitHub](https://github.com/alibaba/qz-browser-agent) ·
|
|
13
|
+
[Chrome Web Store](https://chromewebstore.google.com/detail/dknbdgkgoinlgjpfebapkklbmjjgdblm) ·
|
|
14
|
+
[Full CLI and MCP reference](https://github.com/alibaba/qz-browser-agent/blob/main/docs/cli.md)
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
18765; if another program owns that port, it uses a free port chosen by the operating system.
|
|
47
|
-
It confirms that a browser responds before reporting success, and opens the connection page to
|
|
48
|
-
resume polling if a previously connected browser no longer responds.
|
|
49
|
-
`mcp` prints the actual MCP URL. `status` and `mcp` never start a service or open a browser.
|
|
50
|
-
`stop` shuts down the shared service and disconnects the extension and MCP clients.
|
|
51
|
-
|
|
52
|
-
After first-use consent, an unavailable local service gets up to three connection attempts.
|
|
53
|
-
Once a connection succeeds, later interruptions switch to one recovery attempt per minute after
|
|
54
|
-
three consecutive failed polls, using that same endpoint. Recovery continues across worker restarts;
|
|
55
|
-
disabling the bridge stops it. Run `qz connect` to retry immediately. Connected browsers use long polls
|
|
56
|
-
capped at 20 seconds and reload saved settings before each poll. The side panel and connection page
|
|
57
|
-
can be closed.
|
|
58
|
-
|
|
59
|
-
`status` distinguishes an unavailable local service from a running service with no connected
|
|
60
|
-
browser. A failed health check is retried once; an unresponsive live service is reported without
|
|
61
|
-
starting another instance. After updating the extension, reload it at `chrome://extensions` and
|
|
62
|
-
run `qz connect` to reconnect.
|
|
16
|
+

|
|
63
17
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
Run `qz connect`, then `qz mcp`. Copy the returned URL into your MCP client's HTTP configuration:
|
|
67
|
-
|
|
68
|
-
```toml
|
|
69
|
-
[mcp_servers.qz]
|
|
70
|
-
url = "http://127.0.0.1:18765/mcp"
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Use the URL printed by your command if its port differs. `qz mcp --json` returns the URL as JSON
|
|
74
|
-
for scripts. The `qingzhou_*` MCP tool names and extension protocol remain available. Responses
|
|
75
|
-
to pending input bind to an explicit `waitId`. MCP initialization and streaming are handled by your
|
|
76
|
-
client's MCP implementation.
|
|
18
|
+
## Quick start
|
|
77
19
|
|
|
78
|
-
|
|
20
|
+
Requires Node.js 22 or later.
|
|
79
21
|
|
|
80
|
-
|
|
22
|
+
1. **Install the extension.** Click **Add to Chrome** on the
|
|
23
|
+
[Chrome Web Store](https://chromewebstore.google.com/detail/dknbdgkgoinlgjpfebapkklbmjjgdblm),
|
|
24
|
+
open the qz side panel, and choose a provider and model in **Settings**.
|
|
25
|
+
2. **Connect.** This starts the local service and pairs automatically with your default browser.
|
|
81
26
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| `tasks`, `show`, `watch` | `qingzhou_list_sessions`, `qingzhou_get_session_snapshot`, `qingzhou_watch_task` |
|
|
86
|
-
| `show`, `submit` / `approve` / `reject` | `qingzhou_get_session_snapshot`, `qingzhou_respond_to_user_input` |
|
|
87
|
-
| `data list` / `delete` / `export` | `qingzhou_list_data_tables`, `qingzhou_delete_data_table`, `qingzhou_export_data_table` |
|
|
88
|
-
| `images list` / `delete` / `export` | `qingzhou_list_image_collections`, `qingzhou_delete_image_collection`, `qingzhou_export_image_collection` |
|
|
89
|
-
| `skills list` / `show` / `read` / `tools` / `export` | `qingzhou_list_skills`, `qingzhou_get_skill`, `qingzhou_get_skill_resource`, `qingzhou_list_plugins`, `qingzhou_export_skill` |
|
|
90
|
-
| `settings get` / `set` | `qingzhou_get_settings`, `qingzhou_update_settings` |
|
|
91
|
-
| `automations list` / `run` | `qingzhou_list_automations`, `qingzhou_run_automation` |
|
|
92
|
-
|
|
93
|
-
MCP creation, message submission, and input responses accept `wait: true` to wait for completion;
|
|
94
|
-
`stream: true` also sends assistant text through standard MCP progress notifications. A watch call
|
|
95
|
-
always waits. For example, call `qingzhou_create_session` with:
|
|
27
|
+
```bash
|
|
28
|
+
npx qz-browser-agent@latest connect
|
|
29
|
+
```
|
|
96
30
|
|
|
97
|
-
|
|
98
|
-
{"firstMessage":"Summarize the current page.","stream":true}
|
|
99
|
-
```
|
|
31
|
+
3. **Run your first task.**
|
|
100
32
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
field IDs, option values, and `responseTemplate`. Fill that template and pass it to
|
|
105
|
-
`qingzhou_respond_to_user_input`; another CLI or MCP connection can answer while the original call
|
|
106
|
-
keeps waiting. Native Chrome permissions still require the user's click in the side panel.
|
|
33
|
+
```bash
|
|
34
|
+
npx qz-browser-agent@latest create "Find this year's Nobel Prize winners in every category and put them in a table with source links." --stream
|
|
35
|
+
```
|
|
107
36
|
|
|
108
|
-
|
|
109
|
-
`waitTimeoutSeconds` bounds the whole observation; omit it for no overall deadline. Configure your
|
|
110
|
-
MCP client's own request deadline for long tasks. On timeout or cancellation, the browser task
|
|
111
|
-
continues; error details include the task ID and a recovery tool call. `send_message` supports
|
|
112
|
-
`steer: true` and follows its returned `clientMessageId`, including when steering stays in the same
|
|
113
|
-
run. A long observation stays bound to the browser selected when it started.
|
|
37
|
+
`--stream` shows progress, any questions qz needs you to answer, and the final result.
|
|
114
38
|
|
|
115
|
-
|
|
39
|
+
For regular use, install the `qz` command globally and use it in place of
|
|
40
|
+
`npx qz-browser-agent@latest`:
|
|
116
41
|
|
|
117
42
|
```bash
|
|
118
|
-
|
|
119
|
-
qz
|
|
120
|
-
qz create "Find the main differences between these pages." --stream
|
|
121
|
-
qz send TASK_ID "Add the source links." --stream
|
|
122
|
-
qz send TASK_ID "Focus on open-source options." --steer --stream
|
|
123
|
-
qz tasks
|
|
124
|
-
qz show TASK_ID
|
|
125
|
-
qz cancel TASK_ID
|
|
43
|
+
npm install --global qz-browser-agent@latest
|
|
44
|
+
qz --version
|
|
126
45
|
```
|
|
127
46
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
input and continue automatically after you respond. Task observation has no default time limit.
|
|
131
|
-
Add `--timeout SECONDS` to set a total deadline, including time spent waiting for input. Each
|
|
132
|
-
individual connection request remains bounded (180 seconds by default). A timeout or Ctrl-C ends
|
|
133
|
-
watching; `qz watch TASK_ID --stream` resumes observation and `qz cancel TASK_ID` stops the task.
|
|
134
|
-
Each CLI command initializes one MCP connection on its first tool call and reuses it for subsequent
|
|
135
|
-
reads and responses. Completion, errors, and Ctrl-C close that connection. Transport failures are
|
|
136
|
-
reported without automatically repeating browser actions.
|
|
137
|
-
|
|
138
|
-
Provide the task text directly after `create`, or read it from stdin with `qz create -`.
|
|
139
|
-
Use `--parent TASK_ID` to create a child task, and `qz tasks --limit N` to limit the task list.
|
|
140
|
-
`send TASK_ID MESSAGE` sends a follow-up to the specified task; `qz send TASK_ID -` reads the message
|
|
141
|
-
from stdin. A busy task queues a follow-up for its next run. Add `--steer` to deliver the message
|
|
142
|
-
to the current run at its next safe boundary. Both start a new run when the task is idle. Pending
|
|
143
|
-
questions and permissions still require a response.
|
|
144
|
-
|
|
145
|
-
`--wait` and `--stream` track the submitted message. Streaming starts after that message, including
|
|
146
|
-
when steering continues the same run. Pending questions from the preceding run remain visible so
|
|
147
|
-
you can answer them and let a queued follow-up proceed. A connection failure never resends a message
|
|
148
|
-
automatically.
|
|
149
|
-
|
|
150
|
-
## Respond to a task
|
|
47
|
+
To let your agent learn when and how to use qz, install the Skill with
|
|
48
|
+
`npx skills add alibaba/qz-browser-agent -g`.
|
|
151
49
|
|
|
152
|
-
|
|
153
|
-
qz show TASK_ID
|
|
154
|
-
qz submit TASK_ID '{"selectedIds":["option-a"]}' --stream
|
|
155
|
-
qz approve TASK_ID --wait
|
|
156
|
-
qz reject TASK_ID
|
|
157
|
-
qz watch TASK_ID --stream
|
|
158
|
-
```
|
|
50
|
+
## Connect an MCP client
|
|
159
51
|
|
|
160
|
-
`
|
|
161
|
-
option values, descriptions, image URLs, and a submission template with the actual field IDs.
|
|
162
|
-
Edit its placeholder values and run the displayed command to respond. `submit` takes a JSON object
|
|
163
|
-
keyed by field IDs; use option values for choices and arrays for multiple choices. Use
|
|
164
|
-
`qz submit TASK_ID -` to read answers from stdin. Invalid or stale submissions return the current
|
|
165
|
-
input and its template so you can correct the response directly.
|
|
166
|
-
`approve` continues a confirmation after you finish the requested action. `reject` declines the
|
|
167
|
-
current request; `cancel` stops the whole task.
|
|
168
|
-
|
|
169
|
-
Each response reads the current wait ID before sending. To answer a specific request you already
|
|
170
|
-
read, pass `--wait-id WAIT_ID` to `submit`, `approve`, or `reject`. A stale or invalid response
|
|
171
|
-
leaves the request pending. Review the application consent notice included in the pending input,
|
|
172
|
-
such as location sharing, before approving. Native Chrome permissions require a click in the qz side panel; CLI
|
|
173
|
-
approval reports that action is needed and preserves the wait without opening a browser page.
|
|
174
|
-
With `--wait` or `--stream`, the command stays connected while you grant permission in Chrome.
|
|
175
|
-
Location approval reads the current disclosure version and includes it in the response. Human
|
|
176
|
-
terminal output shows the notice before submitting the explicit approval; JSON output remains a
|
|
177
|
-
single final result. MCP callers should show the notice to the user before sending approval with
|
|
178
|
-
the returned `consentVersion`.
|
|
179
|
-
|
|
180
|
-
`submit`, `approve`, and `reject` accept `--wait` and `--stream` to follow the task until it finishes, including
|
|
181
|
-
further questions and permission requests. Each new request is shown once. Respond in the side
|
|
182
|
-
panel or another terminal; the original command continues automatically. `watch` follows an
|
|
183
|
-
existing task without resubmitting anything. Timeouts and Ctrl-C stop watching, while the task
|
|
184
|
-
continues in the browser.
|
|
185
|
-
|
|
186
|
-
## Browser selection and MCP tools
|
|
187
|
-
|
|
188
|
-
Use `--client ID` or `-c ID` to select a browser when several are connected. Find IDs in `qz status`.
|
|
189
|
-
Use `-t` for `--title`. Run `qz help COMMAND` or `qz COMMAND --help` for command-specific options.
|
|
190
|
-
|
|
191
|
-
MCP clients can inspect browser tool schemas with `qingzhou_list_tools` and `qingzhou_get_tool`,
|
|
192
|
-
then call `qingzhou_execute_tool` in a task. Set `wait: false` to receive a job ID and read the result
|
|
193
|
-
with `qingzhou_get_tool_job`. Pass `runId` to bind execution to a particular task run. MCP also
|
|
194
|
-
provides `qingzhou_get_trace` and `qingzhou_clear_trace` for diagnostics.
|
|
195
|
-
|
|
196
|
-
If a request times out or its connection is interrupted, an operation already started in the
|
|
197
|
-
browser may still be running. Check the task before repeating it. Disconnected MCP requests are
|
|
198
|
-
cancelled in the service, and reconnecting does not replay their browser actions.
|
|
199
|
-
|
|
200
|
-
## Data, images, and Skills
|
|
52
|
+
After `connect`, print the MCP URL:
|
|
201
53
|
|
|
202
54
|
```bash
|
|
203
|
-
qz
|
|
204
|
-
qz data export TABLE
|
|
205
|
-
qz data export TABLE --format csv
|
|
206
|
-
qz data delete TABLE
|
|
207
|
-
qz images
|
|
208
|
-
qz images export COLLECTION
|
|
209
|
-
qz images delete COLLECTION
|
|
210
|
-
qz skills --query research
|
|
211
|
-
qz skills show NAME
|
|
212
|
-
qz skills read NAME scripts/run.js --limit 20000
|
|
213
|
-
qz skills tools --session TASK_ID
|
|
214
|
-
qz skills export NAME
|
|
55
|
+
npx qz-browser-agent@latest mcp
|
|
215
56
|
```
|
|
216
57
|
|
|
217
|
-
|
|
218
|
-
Excel (`xlsx`); `--format csv` selects CSV. Image collections export as a ZIP containing available
|
|
219
|
-
image files and metadata. Missing local images are reported in the result. Skill exports contain
|
|
220
|
-
one user-installed Skill and its resources; built-in Skills require the extension and cannot be
|
|
221
|
-
exported independently.
|
|
222
|
-
|
|
223
|
-
`skills show` includes the resource paths. `skills read NAME PATH` reads one resource, with a
|
|
224
|
-
default limit of 20,000 characters. Use `--start` and `--limit` to select a chunk; truncated text
|
|
225
|
-
includes the command for the next chunk. `skills tools` lists contributed tools and whether they
|
|
226
|
-
are active in the selected task's context.
|
|
227
|
-
`skills --scope all|global|site|resident` filters by activation scope. Exports accept `--filename NAME`.
|
|
228
|
-
`automations --limit N` limits the automation list.
|
|
229
|
-
|
|
230
|
-
Exports use Chrome downloads. The result includes the download status, ID, filename, and path when
|
|
231
|
-
Chrome provides it. Check pending downloads in `chrome://downloads`. `delete` removes the named table
|
|
232
|
-
or image collection and its rows. Image blobs shared by other collections or task history remain
|
|
233
|
-
in the asset store.
|
|
58
|
+
The default is `http://127.0.0.1:18765/mcp`; use the URL your command prints if the port differs.
|
|
234
59
|
|
|
235
|
-
|
|
60
|
+
**Codex** (`~/.codex/config.toml`):
|
|
236
61
|
|
|
237
|
-
```
|
|
238
|
-
qz
|
|
239
|
-
|
|
240
|
-
qz settings set model MODEL_ID
|
|
241
|
-
qz settings set vision true
|
|
242
|
-
qz settings set '{"modelParameters":{"temperature":0.5}}'
|
|
243
|
-
qz settings set - < settings.json
|
|
244
|
-
qz automations
|
|
245
|
-
qz automations run AUTOMATION_ID
|
|
62
|
+
```toml
|
|
63
|
+
[mcp_servers.qz]
|
|
64
|
+
url = "http://127.0.0.1:18765/mcp"
|
|
246
65
|
```
|
|
247
66
|
|
|
248
|
-
|
|
249
|
-
redacted; `qz settings get apiKey` reports whether a key is configured. `set KEY VALUE` parses JSON
|
|
250
|
-
values such as booleans, numbers, objects, and arrays; other values are strings. `set JSON` applies
|
|
251
|
-
a patch containing the supplied top-level fields. Follow the field descriptions for values that
|
|
252
|
-
replace an entire object, such as `modelParameters`.
|
|
253
|
-
|
|
254
|
-
Read credential patches from stdin to keep them out of shell history. JSON is validated before
|
|
255
|
-
connecting. The extension validates field names and values before saving. All commands accept
|
|
256
|
-
`--client ID` to select a browser profile. `automations run` starts an existing automation and
|
|
257
|
-
returns its task ID.
|
|
258
|
-
|
|
259
|
-
The CLI uses the same browser operations as MCP. Its commands correspond to tasks, data,
|
|
260
|
-
images, Skills, settings, and automations. `qz mcp` only returns the connection URL for MCP
|
|
261
|
-
clients.
|
|
262
|
-
|
|
263
|
-
## Terminal output
|
|
264
|
-
|
|
265
|
-
Interactive terminals show readable text; pipes and redirected output receive JSON. Use `--json`
|
|
266
|
-
to request JSON explicitly or `--plain` to keep text when redirecting output. `--no-color` or the
|
|
267
|
-
`NO_COLOR` environment variable disables color. `qz mcp` prints only the MCP URL by default;
|
|
268
|
-
`qz mcp --json` returns a JSON result.
|
|
269
|
-
|
|
270
|
-
In JSON mode, `--wait` and `--stream` emit one final JSON result on stdout. Complete pending-input
|
|
271
|
-
notices and submission templates appear on stderr so you can respond while preserving
|
|
272
|
-
machine-readable output. MCP snapshots and rejected responses include `input`, with a
|
|
273
|
-
`responseTemplate` that binds answers to the current session and wait. Chrome-native permission
|
|
274
|
-
requests provide user instructions instead of an approval template.
|
|
275
|
-
Exit codes are `0` for success (including reading pending input), `1` for an operation or
|
|
276
|
-
task failure or a rejected submission, `2` for invalid arguments, and `130` for Ctrl-C.
|
|
277
|
-
Connection errors include the GitHub setup guide and installation link.
|
|
278
|
-
|
|
279
|
-
## Local access
|
|
280
|
-
|
|
281
|
-
The service listens on loopback. It validates Host and Origin, rejects redirects, and accepts
|
|
282
|
-
extension polling only from extension origins or local clients. Ordinary websites cannot send
|
|
283
|
-
requests through the MCP endpoint. Local programs can control connected qz tasks while the
|
|
284
|
-
service is running. Run `qz stop` to disconnect.
|
|
285
|
-
|
|
286
|
-
Connection metadata is stored in `~/.qz-browser-agent/run/`; `QZ_STATE_DIR` selects another local
|
|
287
|
-
directory for isolated tests. Atomic startup locks prevent duplicate services. Development copies
|
|
288
|
-
and published installations use separate identities. The client verifies the service identity,
|
|
289
|
-
protocol version, instance, and process before reusing or stopping it. Editing source files or
|
|
290
|
-
running a newer package keeps the healthy service and its connections alive. `status` reports
|
|
291
|
-
when a different build is available. To load the new service code, finish active work, run
|
|
292
|
-
`qz stop`, then `qz connect`.
|
|
293
|
-
|
|
294
|
-
## Package development
|
|
295
|
-
|
|
296
|
-
The npm package name is `qz-browser-agent`; the executable name is `qz`. Local development uses
|
|
297
|
-
this same package name. The repository root stays private, and this service directory is the
|
|
298
|
-
publishable package. Publishing requires separate authorization and ownership of the npm name.
|
|
299
|
-
|
|
300
|
-
From the repository root, install the locked dependencies and run the local source:
|
|
67
|
+
**Claude Code**:
|
|
301
68
|
|
|
302
69
|
```bash
|
|
303
|
-
|
|
304
|
-
npm run qz -- --version
|
|
305
|
-
npm run qz -- connect
|
|
306
|
-
npm run qz -- create "Summarize the current page." --stream
|
|
70
|
+
claude mcp add --transport http qz http://127.0.0.1:18765/mcp
|
|
307
71
|
```
|
|
308
72
|
|
|
309
|
-
|
|
310
|
-
run `npm link` in `services/mcp-bridge`. JavaScript runs directly on Node.js. The published
|
|
311
|
-
package contains the runtime files, tool catalog, README, and license.
|
|
73
|
+
**Clients with an `mcpServers` JSON config**, such as Cursor:
|
|
312
74
|
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"qz": { "url": "http://127.0.0.1:18765/mcp" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
316
81
|
```
|
|
317
82
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
and shutdown on an isolated port. The test owns its npm configuration, cache, connection state,
|
|
321
|
-
and service process. It requires network access to download dependencies.
|
|
322
|
-
|
|
323
|
-
Use `--output tests/results/npm/qz-browser-agent-0.1.0.tgz` to retain the verified tarball.
|
|
324
|
-
Use `--tarball PATH` to check an existing tarball. The check also runs through
|
|
325
|
-
`npm test -- release --match npm-package`.
|
|
326
|
-
|
|
327
|
-
## Publish to npm
|
|
83
|
+
CLI and MCP share the same browser connection. The MCP tool list is in the
|
|
84
|
+
[reference](https://github.com/alibaba/qz-browser-agent/blob/main/docs/cli.md#task-and-resource-operations).
|
|
328
85
|
|
|
329
|
-
|
|
330
|
-
and commit the intended source before publishing. CLI and MCP report that same package version.
|
|
331
|
-
Each published version identifies immutable package contents; use a new version for updates.
|
|
86
|
+
## Common commands
|
|
332
87
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
88
|
+
| Command | What it does |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `qz connect` | Start the local service and pair a browser |
|
|
91
|
+
| `qz status` | Check the connection and service address |
|
|
92
|
+
| `qz browsers` | List paired browsers, or `forget` an offline one |
|
|
93
|
+
| `qz create "PROMPT"` | Start a task; add `--stream` or `--wait` to follow it |
|
|
94
|
+
| `qz send TASK_ID "MESSAGE"` | Send a follow-up, or `--steer` the current run |
|
|
95
|
+
| `qz tasks` | List tasks |
|
|
96
|
+
| `qz show TASK_ID` | Read a task's status, response, or pending question |
|
|
97
|
+
| `qz watch TASK_ID` | Follow a running task until it finishes |
|
|
98
|
+
| `qz submit` / `approve` / `reject` | Answer a question or permission request |
|
|
99
|
+
| `qz cancel TASK_ID` | Stop a task |
|
|
100
|
+
| `qz data` | List, export, or delete data tables |
|
|
101
|
+
| `qz images` | List, export, or delete image collections |
|
|
102
|
+
| `qz skills` | Find, read, or export Skills |
|
|
103
|
+
| `qz settings` | Read or update settings |
|
|
104
|
+
| `qz automations` | List or run automations |
|
|
105
|
+
| `qz stop` | Stop the local service and disconnect all browsers |
|
|
106
|
+
|
|
107
|
+
With several browser profiles connected, choose one with `--client NAME|UUID` (or `-c`).
|
|
108
|
+
Run `qz COMMAND --help` for options. Interactive terminals show readable text; pipes receive JSON.
|
|
109
|
+
|
|
110
|
+
## Privacy and local access
|
|
111
|
+
|
|
112
|
+
The service accepts connections only on your computer, and ordinary websites cannot use its MCP
|
|
113
|
+
endpoint. Local programs can control connected qz tasks while the service is running. `qz stop`
|
|
114
|
+
disconnects all browsers and MCP clients; tasks already running in the browser keep going.
|
|
115
|
+
Connection settings are stored in `~/.qz-browser-agent/run/`, and pairing credentials never
|
|
116
|
+
leave your computer.
|
|
117
|
+
|
|
118
|
+
## Reference
|
|
119
|
+
|
|
120
|
+
See the [qz CLI and MCP reference](https://github.com/alibaba/qz-browser-agent/blob/main/docs/cli.md)
|
|
121
|
+
for pairing a specific browser profile, Windows PowerShell notes, updates, task responses,
|
|
122
|
+
exports, output formats, exit codes, and the MCP tool mapping.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[Apache-2.0](https://github.com/alibaba/qz-browser-agent/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qz-browser-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Local browser tools and MCP access for the qz extension",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"engines": { "node": ">=
|
|
6
|
+
"engines": { "node": ">=22" },
|
|
7
7
|
"bin": { "qz": "src/bin.mjs" },
|
|
8
8
|
"files": ["src/*.mjs", "src/catalog.json", "README.md", "LICENSE"],
|
|
9
9
|
"license": "Apache-2.0",
|
package/src/args.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { QzError } from "./client.mjs";
|
|
3
|
+
import { browserName } from "./browser-registry.mjs";
|
|
3
4
|
import { COMMANDS, commandOptions } from "./commands.mjs";
|
|
4
5
|
|
|
5
6
|
function invalid(message) {
|
|
@@ -22,7 +23,7 @@ export function parseCommand(argv) {
|
|
|
22
23
|
title: { type: "string", short: "t" }, client: { type: "string", short: "c" },
|
|
23
24
|
"wait-id": { type: "string" }, format: { type: "string" }, query: { type: "string" },
|
|
24
25
|
start: { type: "string" }, limit: { type: "string" }, scope: { type: "string" }, filename: { type: "string" },
|
|
25
|
-
parent: { type: "string" },
|
|
26
|
+
parent: { type: "string" }, port: { type: "string" }, name: { type: "string" }, "no-open": { type: "boolean" },
|
|
26
27
|
wait: { type: "boolean" }, stream: { type: "boolean" }, steer: { type: "boolean" },
|
|
27
28
|
json: { type: "boolean" }, plain: { type: "boolean" }, "no-color": { type: "boolean" },
|
|
28
29
|
help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" },
|
|
@@ -56,6 +57,17 @@ export function parseCommand(argv) {
|
|
|
56
57
|
for (const name of ["session", "client", "wait-id", "parent"]) {
|
|
57
58
|
if (options[name] !== undefined && !options[name].trim()) invalid(`--${name} requires a non-empty ID.`);
|
|
58
59
|
}
|
|
60
|
+
if (options.name !== undefined) {
|
|
61
|
+
try { options.name = browserName(options.name); } catch (error) { invalid(error.message); }
|
|
62
|
+
}
|
|
63
|
+
if (options.client !== undefined && (options.name !== undefined || options["no-open"])) {
|
|
64
|
+
invalid("Use --client to select an existing browser, or --name/--no-open to prepare a binding URL.");
|
|
65
|
+
}
|
|
66
|
+
if (options.port !== undefined) {
|
|
67
|
+
const port = Number(options.port);
|
|
68
|
+
if (!options.port.trim() || !Number.isInteger(port) || port < 1 || port > 65535) invalid("--port must be an integer from 1 to 65535.");
|
|
69
|
+
options.port = port;
|
|
70
|
+
}
|
|
59
71
|
if (options.filename !== undefined && !options.filename.trim()) invalid("--filename requires a non-empty name.");
|
|
60
72
|
if (options.scope !== undefined && !["all", "global", "site", "resident"].includes(options.scope)) invalid("--scope must be all, global, site, or resident.");
|
|
61
73
|
if (options.client !== undefined) {
|
|
@@ -78,7 +90,11 @@ export function parseCommand(argv) {
|
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
let command = originalCommand, action, id;
|
|
81
|
-
if (command === "
|
|
93
|
+
if (command === "browsers") {
|
|
94
|
+
action = words[0] || "list";
|
|
95
|
+
if (action === "list" ? words.length > 1 : action !== "forget" || words.length !== 2 || !words[1].trim()) invalid("Use qz browsers [list], or qz browsers forget NAME|UUID.");
|
|
96
|
+
id = words[1];
|
|
97
|
+
} else if (command === "send") {
|
|
82
98
|
id = words[0]; options.prompt = words.slice(1).join(" ");
|
|
83
99
|
if (!id?.trim() || !options.prompt.trim()) invalid('Use qz send TASK_ID "MESSAGE".');
|
|
84
100
|
} else if (command === "data" || command === "images" || command === "skills") {
|
package/src/bin.mjs
CHANGED
|
@@ -7,19 +7,17 @@ let isEntry = false;
|
|
|
7
7
|
try { isEntry = realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); } catch {}
|
|
8
8
|
if (isEntry) {
|
|
9
9
|
const argv = process.argv.slice(2);
|
|
10
|
-
if (Number(process.versions.node.split(".")[0]) <
|
|
10
|
+
if (Number(process.versions.node.split(".")[0]) < 22) {
|
|
11
11
|
const error = {
|
|
12
12
|
code: "unsupported_node_version",
|
|
13
|
-
message: `qz requires Node.js
|
|
14
|
-
hint: "Install Node.js
|
|
13
|
+
message: `qz requires Node.js 22 or later. Current version: ${process.versions.node}.`,
|
|
14
|
+
hint: "Install Node.js 22 or later, then run qz again.",
|
|
15
15
|
};
|
|
16
16
|
if (argv.includes("--json")) process.stdout.write(`${JSON.stringify({ ok: false, error })}\n`);
|
|
17
17
|
else process.stderr.write(`${error.message}\n${error.hint}\n`);
|
|
18
18
|
process.exitCode = 1;
|
|
19
19
|
} else {
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
process.once("SIGINT", () => controller.abort());
|
|
23
|
-
process.exitCode = await main(argv, { signal: controller.signal });
|
|
20
|
+
const { runMain } = await import("./cli.mjs");
|
|
21
|
+
process.exitCode = await runMain(argv);
|
|
24
22
|
}
|
|
25
23
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
|
|
5
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
export const isBrowserId = value => typeof value === "string" && uuid.test(value);
|
|
7
|
+
export function pairingError(code, message) {
|
|
8
|
+
return Object.assign(new Error(`${code}: ${message}`), { code });
|
|
9
|
+
}
|
|
10
|
+
export function browserName(value) {
|
|
11
|
+
const name = typeof value === "string" ? value.trim() : "";
|
|
12
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name) || uuid.test(name)) {
|
|
13
|
+
throw pairingError("BRIDGE_NAME_INVALID", "Use 1–64 letters, digits, underscores, or hyphens, starting with a letter or digit. A UUID cannot be used as a name.");
|
|
14
|
+
}
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
export function sameToken(actual, expected) {
|
|
18
|
+
if (typeof actual !== "string" || typeof expected !== "string") return false;
|
|
19
|
+
const a = Buffer.from(actual), b = Buffer.from(expected);
|
|
20
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function missingRegistry(path) {
|
|
24
|
+
// On Windows, ENOENT can also mean that a parent component is a file.
|
|
25
|
+
for (let candidate = path; ; candidate = dirname(candidate)) {
|
|
26
|
+
try { await lstat(candidate); }
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== "ENOENT" || dirname(candidate) === candidate) return false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
return candidate !== path && await stat(candidate).then(entry => entry.isDirectory(), () => false);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The daemon owns this file; its scope lock prevents competing writers. */
|
|
36
|
+
export class BrowserRegistry {
|
|
37
|
+
constructor({ path, entries = [], persist } = {}) {
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.entries = new Map(entries.map(entry => [entry.clientId, entry]));
|
|
40
|
+
this.persist = persist || (async entries => {
|
|
41
|
+
if (!path) return;
|
|
42
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
43
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
await writeFile(temporary, `${JSON.stringify({ version: 1, browsers: entries })}\n`, { mode: 0o600 });
|
|
46
|
+
await rename(temporary, path);
|
|
47
|
+
} finally { await rm(temporary, { force: true }).catch(() => {}); }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static async open(path) {
|
|
52
|
+
if (!path) return new BrowserRegistry();
|
|
53
|
+
let document;
|
|
54
|
+
try { document = JSON.parse(await readFile(path, "utf8")); }
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT" && await missingRegistry(path)) return new BrowserRegistry({ path });
|
|
57
|
+
throw pairingError("BRIDGE_REGISTRY_READ_FAILED", "Cannot read the browser registry. Restore its valid contents before restarting qz.");
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
if (document.version !== 1 || !Array.isArray(document.browsers)) throw new Error();
|
|
61
|
+
const ids = new Set(), names = new Set();
|
|
62
|
+
for (const entry of document.browsers) {
|
|
63
|
+
if (!isBrowserId(entry.clientId) || browserName(entry.name) !== entry.name
|
|
64
|
+
|| !/^[A-Za-z0-9_-]{43}$/.test(entry.pairingToken) || ids.has(entry.clientId) || names.has(entry.name)) throw new Error();
|
|
65
|
+
ids.add(entry.clientId); names.add(entry.name);
|
|
66
|
+
}
|
|
67
|
+
return new BrowserRegistry({ path, entries: document.browsers });
|
|
68
|
+
} catch { throw pairingError("BRIDGE_REGISTRY_READ_FAILED", "The browser registry is invalid. Restore its valid contents before restarting qz."); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
list() { return [...this.entries.values()].map(({ clientId, name }) => ({ clientId, name })); }
|
|
72
|
+
get(selector) { return this.entries.get(selector) || [...this.entries.values()].find(entry => entry.name === selector); }
|
|
73
|
+
async save(entries) {
|
|
74
|
+
try { await this.persist([...entries.values()]); }
|
|
75
|
+
catch { throw pairingError("BRIDGE_REGISTRY_WRITE_FAILED", "Could not save the browser registry. Check its directory permissions and retry."); }
|
|
76
|
+
this.entries = entries;
|
|
77
|
+
}
|
|
78
|
+
async pair({ clientId, name, pairingToken }) {
|
|
79
|
+
const existing = this.entries.get(clientId);
|
|
80
|
+
if (existing && !sameToken(pairingToken, existing.pairingToken)) {
|
|
81
|
+
throw pairingError("BRIDGE_PAIRING_REQUIRED", "This browser identity is already paired. Use its original profile, or forget the offline browser before pairing again.");
|
|
82
|
+
}
|
|
83
|
+
if (!name && !existing) {
|
|
84
|
+
let index = this.entries.size ? 2 : 1;
|
|
85
|
+
do { name = index === 1 ? "default" : `browser-${index}`; index++; } while (this.get(name));
|
|
86
|
+
}
|
|
87
|
+
name = browserName(name || existing?.name);
|
|
88
|
+
const occupied = this.get(name);
|
|
89
|
+
if (occupied && occupied.clientId !== clientId) throw pairingError("BRIDGE_NAME_IN_USE", `Browser name ${name} is already registered. Choose another name or forget its offline registration.`);
|
|
90
|
+
if (existing?.name === name) return existing;
|
|
91
|
+
if (pairingToken !== undefined && !/^[A-Za-z0-9_-]{43}$/.test(pairingToken)) throw pairingError("BRIDGE_PAIRING_REQUIRED", "The browser pairing proof is invalid. Reconnect from the extension settings.");
|
|
92
|
+
const entry = { clientId, name, pairingToken: existing?.pairingToken || pairingToken || randomBytes(32).toString("base64url") };
|
|
93
|
+
await this.save(new Map(this.entries).set(clientId, entry));
|
|
94
|
+
return entry;
|
|
95
|
+
}
|
|
96
|
+
async forget(clientId) {
|
|
97
|
+
const entries = new Map(this.entries);
|
|
98
|
+
entries.delete(clientId);
|
|
99
|
+
await this.save(entries);
|
|
100
|
+
}
|
|
101
|
+
}
|