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 CHANGED
@@ -1,353 +1,126 @@
1
- # qz command line and MCP
1
+ # qz
2
2
 
3
- Run browser tasks, manage results, and connect external agents to qz through CLI or MCP.
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
- ## Quick start
7
-
8
- Run the published package with npx:
9
-
10
- ```bash
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
- ```sh
28
- npm install --global qz-browser-agent@latest
29
- qz --version
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
- `connect` reuses the healthy service already running for this installation and protocol. It prefers port
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
+ ![A coding agent handing a GitHub research task to qz](https://raw.githubusercontent.com/alibaba/qz-browser-agent/main/docs/images/examples/en/pi-delegate-en.gif)
63
17
 
64
- ## Connect an MCP client
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
- ### Task and resource operations
20
+ Requires Node.js 22 or later.
79
21
 
80
- CLI and MCP use the same task observer and browser operations. The task and resource commands map to these MCP operations:
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
- | CLI | MCP tool |
83
- | --- | --- |
84
- | `create`, `send`, `cancel` | `qingzhou_create_session`, `qingzhou_send_message`, `qingzhou_stop_session` |
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
- ```json
98
- {"firstMessage":"Summarize the current page.","stream":true}
99
- ```
31
+ 3. **Run your first task.**
100
32
 
101
- Supply `_meta.progressToken` on the MCP request to receive progress. Each notification includes a
102
- readable `message` and its structured event in `params._meta["qz/task-event"]`. Events include
103
- `created`, `status`, `text`, `replace`, and `input`. Input events include the current form, exact
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
- MCP `timeoutSeconds` bounds each browser request (180 seconds by default).
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
- ## Run a task
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
- qz create "Compare these two laptops."
119
- qz create "Summarize the current page." --title "Research" --wait
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
- `create` starts a task and returns its ID. The title is optional. `--wait` waits for the result;
129
- `--stream` also displays progress as it arrives. Both stay connected while the task waits for user
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
- ```bash
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
- `show` and pending-input notices include the current question, labeled field IDs,
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 data
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
- Use the exact names printed by each list; quote names containing spaces. Data exports default to
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
- ## Settings and automations
60
+ **Codex** (`~/.codex/config.toml`):
236
61
 
237
- ```bash
238
- qz settings
239
- qz settings get model
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
- `settings` returns current values, editable fields, provider IDs, and protocol IDs. Credentials are
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
- npm ci
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
- Keep the `--` separator when forwarding options through npm. For a local `qz` executable,
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
- ```bash
314
- npm test -- services --match node
315
- node tests/release/npm-package.mjs
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "qz": { "url": "http://127.0.0.1:18765/mcp" }
79
+ }
80
+ }
316
81
  ```
317
82
 
318
- The package check packs the service and installs that tarball in two clean temporary directories
319
- using the public npm registry. It checks the installed command, versions, MCP tools, service reuse,
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
- Choose the package version in `services/mcp-bridge/package.json`, complete the repository checks,
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
- The **npm package** GitHub Actions workflow tests the CLI and the installed package on Linux,
334
- macOS, and Windows. On a manual run, provide the expected version. With **publish** left unchecked,
335
- it only validates the package and retains the tested tarball. Publishing requires a manual run
336
- from `main` with **publish** checked; all three platforms must pass first.
337
-
338
- Configure the npm package's trusted publisher for the `alibaba/qz-browser-agent` repository,
339
- workflow `npm-package.yml`, and GitHub environment `npm`. Enable direct publishing in that
340
- trusted publisher configuration. The publish job uses OIDC and uploads the exact Linux-tested
341
- tarball. The npm package must be created under an account with publishing rights before this
342
- trusted publisher can be configured. See [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/).
343
-
344
- For the first manual release, log in with npm, verify the saved tarball, and publish it:
345
-
346
- ```bash
347
- npm login --registry=https://registry.npmjs.org
348
- node tests/release/npm-package.mjs --output tests/results/npm/qz-browser-agent-0.1.0.tgz
349
- npm publish tests/results/npm/qz-browser-agent-0.1.0.tgz --access public --registry=https://registry.npmjs.org
350
- ```
351
-
352
- The root package stays private. Publishing the service does not publish the Chrome extension.
353
- After publication, check `npx qz-browser-agent@0.1.0 --version` from a directory outside the repository.
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.0",
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": ">=24" },
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 === "send") {
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]) < 24) {
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 24 or later. Current version: ${process.versions.node}.`,
14
- hint: "Install Node.js 24 or later, then run qz again.",
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 { main } = await import("./cli.mjs");
21
- const controller = new AbortController();
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
+ }