qz-browser-agent 0.1.5 → 0.1.6

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
@@ -15,34 +15,11 @@ and step in from the side panel. Everything runs locally.
15
15
 
16
16
  ## Quick start
17
17
 
18
- Requires Node.js 22 or later.
19
-
20
- 1. **Install the extension.** Click **Add to Chrome** on the
21
- [Chrome Web Store](https://chromewebstore.google.com/detail/dknbdgkgoinlgjpfebapkklbmjjgdblm).
22
- 2. **Install the CLI.** Or use `npx qz-browser-agent@latest` in place of `qz`.
23
-
24
- ```bash
25
- npm install --global qz-browser-agent@latest
26
- ```
27
- 3. **Drive the browser.** No model setup is needed in qz.
28
-
29
- ```bash
30
- qz session create --title "Read example page"
31
- qz script --session SESSION_ID - <<'JS'
32
- const page = qz.tab(await qz.openTab({ url: "https://example.com" }));
33
- return await page.read({ limit: 2000 });
34
- JS
35
- ```
36
-
37
- The first command connects the browser automatically. To teach your agent the full workflow,
38
- install the qz Skill with `npx skills add alibaba/qz-browser-agent -g`, or have it read `qz skill`.
39
- 4. **Or hand qz the whole task.** Choose a provider and model in the side panel's **Settings**, then:
40
-
41
- ```bash
42
- qz create "Find this year's Nobel Prize winners in every category and put them in a table with source links." --stream
43
- ```
44
-
45
- `--stream` shows progress, any questions qz needs you to answer, and the final result.
18
+ Use Node.js 22+ on the computer running your browser. Reuse `qz` if it is already on PATH; otherwise, install it once for ongoing use with `npm install -g qz-browser-agent`. Use `npx -y qz-browser-agent@latest` in place of `qz` only as a temporary fallback. Run `qz skill` with that same CLI and read its matching-version guide rather than copying instructions from another version.
19
+
20
+ Start with `qz session create --title "Read a page" --json`. qz prepares its local service and reuses a responding browser connection, or opens a connection page in your default browser. Ask the user to review and accept the first-use data notice in the qz side panel and approve the connection when prompted. If the extension is missing, `qz install` opens the Chrome Web Store; the user adds the extension, completes setup, and you retry the session command if needed. If qz is installed in another browser profile, open the connection page’s copyable link there. This direct-drive path needs no manual port, service, or qz model setup.
21
+
22
+ Use the returned `result.sessionId` in every `qz script --session ID task.js` call, following the guide and binding pages with `qz.tab(tabRef)`. You plan and drive the task; finish with `qz session cancel ID`. Use `qz create "TASK" --stream` only when the user wants qz’s built-in agent to plan and execute the task, including human–agent collaboration; that path uses the provider and model configured in qz.
46
23
 
47
24
  ## Drive the browser
48
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qz-browser-agent",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Local browser tools and MCP access for the qz extension",
5
5
  "type": "module",
6
6
  "engines": { "node": ">=22" },
package/src/catalog.json CHANGED
@@ -283,7 +283,7 @@
283
283
  },
284
284
  {
285
285
  "name": "qingzhou_list_tools",
286
- "description": "List default direct-drive tool signatures; sessionId is optional. Query non-default or loaded Skill tools by exact name with qingzhou_get_tool (pass sessionId for loaded Skills). For full schemas, call tools.list_tools({names:[...]}) inside run_script. Never creates a session.",
286
+ "description": "List default direct-drive tool signatures; sessionId is optional. Query non-default or loaded Skill tools by exact name with qingzhou_get_tool (pass sessionId for loaded Skills). For full schemas, call qz.listTools({names:[...]}) inside run_script. Never creates a session.",
287
287
  "inputSchema": {
288
288
  "properties": {
289
289
  "sessionId": {"title": "Sessionid", "type": "string"},
@@ -381,7 +381,7 @@
381
381
  },
382
382
  {
383
383
  "name": "qingzhou_execute_tool",
384
- "description": "在已连接的浏览器里执行现有工具。先调用 qingzhou_create_drive_session 创建会话,再传入该 sessionId(不会隐式创建会话)。单次调用传 name 与 args;已知参数的并发或顺序批次传 calls(每项含 id、name、args,id 仅用于关联结果,不支持跨项插值);多步逻辑与条件编排可调用 name:\"run_script\"(页面外执行,支持顶层 await/return 与 tools.*,页面内 DOM 函数用 evaluate)。页面工具显式传 tabRef。逐项检查 isError,批次与脚本均非事务;run_script 的 result 为 {ok,result,trace} 回执,trace 按序记录报错与显式 trace()/console 输出。Execute browser tools in an explicitly created session (pass sessionId from qingzhou_create_drive_session). Supply name/args for one call, calls ([{id,name,args}]) for a static batch, or name:\"run_script\" for outer JS orchestration (use evaluate for in-page DOM functions). Page tools require an explicit tabRef. Check isError on each call; batches and scripts are not transactions. run_script returns {ok,result,trace}, where trace lists errors and explicit trace()/console diagnostics in order.",
384
+ "description": "在已连接的浏览器里执行现有工具。先调用 qingzhou_create_drive_session 创建会话,再传入该 sessionId(不会隐式创建会话)。单次调用传 name 与 args;已知参数的并发或顺序批次传 calls(每项含 id、name、args,id 仅用于关联结果,不支持跨项插值);多步逻辑与条件编排可调用 name:\"run_script\"(页面外执行,支持顶层 await/return 与 qz SDK,原始工具用 qz.call(name,args),页面内 DOM 函数用 page.evaluate)。页面工具显式传 tabRef。逐项检查 isError,批次与脚本均非事务;run_script 的 result 为 {ok,result,trace} 回执,trace 按序记录报错与显式 trace()/console 输出。Execute browser tools in an explicitly created session (pass sessionId from qingzhou_create_drive_session). Supply name/args for one call, calls ([{id,name,args}]) for a static batch, or name:\"run_script\" for outer JS orchestration with the qz SDK (qz.call(name,args) for raw tools; page.evaluate for in-page DOM functions). Page tools require an explicit tabRef. Check isError on each call; batches and scripts are not transactions. run_script returns {ok,result,trace}, where trace lists errors and explicit trace()/console diagnostics in order.",
385
385
  "inputSchema": {
386
386
  "properties": {
387
387
  "scriptApiVersion": {"type": "integer", "minimum": 1, "description": "Script API protocol version; must match the extension before executing the request."},
package/src/cli.mjs CHANGED
@@ -22,10 +22,11 @@ export const HELP = renderHelp();
22
22
  async function stdinText({ signal } = {}) {
23
23
  signal?.throwIfAborted();
24
24
  const input = process.stdin;
25
- if (input.readableEnded) return "";
26
25
  return new Promise((resolve, reject) => {
27
26
  const chunks = [];
27
+ let eof;
28
28
  const cleanup = () => {
29
+ clearImmediate(eof);
29
30
  input.removeListener("data", data);
30
31
  input.removeListener("end", end);
31
32
  input.removeListener("error", fail);
@@ -33,14 +34,21 @@ async function stdinText({ signal } = {}) {
33
34
  input.pause();
34
35
  };
35
36
  const data = chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
36
- const end = () => { cleanup(); resolve(Buffer.concat(chunks).toString("utf8")); };
37
+ // npm may close this pipe before its forwarded signal callback runs. Keep
38
+ // cancellation live through that I/O turn instead of reporting empty input.
39
+ const end = () => { eof = setImmediate(() => {
40
+ cleanup();
41
+ if (signal?.aborted) reject(signal.reason);
42
+ else resolve(Buffer.concat(chunks).toString("utf8"));
43
+ }); };
37
44
  const fail = error => { cleanup(); reject(error); };
38
45
  const abort = () => fail(signal.reason);
39
46
  input.on("data", data);
40
47
  input.once("end", end);
41
48
  input.once("error", fail);
42
49
  signal?.addEventListener("abort", abort, { once: true });
43
- input.resume();
50
+ if (input.readableEnded) end();
51
+ else input.resume();
44
52
  });
45
53
  }
46
54
 
package/src/commands.mjs CHANGED
@@ -136,8 +136,8 @@ export const COMMANDS = {
136
136
  help: ["qz script --session ID [FILE|-] [--args JSON]", "Run JavaScript orchestration outside the page. stdout is {ok,result,trace}: execution status, the returned business value (or error), and ordered trace messages.",
137
137
  "--session ID is required. Create one session per task with qz session create --title TITLE and pass its sessionId.",
138
138
  "Read a local file or use - for stdin. Write top-level await and return; --args passes a JSON value as args (aliased as input).",
139
- "Call browser tools via tools.<name>(...) with explicit tabRef values; awaited calls return business values directly and throw on failure. Use tools.evaluate({tabRef,code,args}) for in-page DOM/window access.",
140
- "Discover compact signatures with tools.list_tools() and full schemas with tools.list_tools({names:[...]}).",
139
+ "Use the qz SDK: bind a page with qz.tab(tabRef), act through page.locator(targetOrQuery), and run in-page DOM/window code with page.evaluate(code,args). Awaited calls return business values directly and throw on failure.",
140
+ "Discover compact signatures with qz.listTools() and full schemas with qz.listTools({names:[...]}); call any listed tool with qz.call(name,args). Run qz skill for the full guide.",
141
141
  "--timeout sets the script timeout in seconds (0 < seconds <= 3600) and defaults to 120. The request waits 60 seconds longer.",
142
142
  "stdout is always {ok,result,trace}: ok reports execution success, result holds the returned value (null for undefined) or {code,message,hint?,details?}, and trace lists errors and explicit trace()/console output in order without levels. Exit codes: 0 success, 1 execution or connection failure, 2 invalid arguments.",
143
143
  "qz script --session ID task.js --args '{\"query\":\"example\"}'", "qz script --session ID - <<'JS'"],
@@ -1,22 +1,14 @@
1
1
  ---
2
2
  name: qz-browser-agent
3
- description: Drive the connected browser through qz (轻舟) scripts. Create one titled session per task, pass its sessionId to every script, and bind each page with qz.tab(tabRef). Read qz skill for the installed CLI's guide. Read references/create-task.md only when the built-in agent should plan the task.
3
+ description: Drive the connected browser through qz (轻舟) scripts. Create one titled session per task, pass its sessionId to every script, and bind each page with qz.tab(tabRef). Read references/create-task.md only when the built-in agent should plan the task.
4
4
  ---
5
5
 
6
- Use the installed `qz` command. If unavailable, use `npx -y qz-browser-agent@latest` in place of `qz` in the commands below. Run `qz skill` once at the start of a task for the matching guide. `qz --version` prints its version.
6
+ Use Node.js 22+ on the computer running your browser. Reuse `qz` if it is already on PATH; otherwise, install it once for ongoing use with `npm install -g qz-browser-agent`. Use `npx -y qz-browser-agent@latest` in place of `qz` only as a temporary fallback.
7
7
 
8
8
  <!-- qz-drive-guide:en -->
9
- The external agent plans the work and drives the connected browser through qz (轻舟). Do not configure a qz model or call `qz create`. Read references/create-task.md only when the built-in agent should plan and execute the task.
9
+ Start with `qz session create --title "Read a page" --json`. qz prepares its local service and reuses a responding browser connection, or opens a connection page in your default browser. Ask the user to review and accept the first-use data notice in the qz side panel and approve the connection when prompted. If the extension is missing, `qz install` opens the Chrome Web Store; the user adds the extension, completes setup, and you retry the session command if needed. If qz is installed in another browser profile, open the connection page’s copyable link there. This direct-drive path needs no manual port or service setup. Do not configure a qz model for direct-drive.
10
10
 
11
- ## Connect
12
-
13
- Run commands with Node.js 22+ on the machine running the browser extension:
14
-
15
- ```sh
16
- qz status --json
17
- ```
18
-
19
- Session and script commands prepare the connection automatically: they reuse a responding online browser, or open the connection page once in the system default browser and wait for its extension to respond. Complete the extension's first-use notice and connection prompt when shown. If the extension is missing, `qz install` opens the store; ask the user to install it. When qz does not respond in that browser, the page offers the store and a copyable connection link, and a pending connection error carries the same link in `details.url`: ask the user which browser has qz installed, open the link there, and retry. Do not search browser profiles for the extension.
11
+ Use the returned `result.sessionId` in every `qz script --session ID task.js` call, following the guide and binding pages with `qz.tab(tabRef)`. You plan and drive the task; finish with `qz session cancel ID`. Use `qz create "TASK" --stream` and read `references/create-task.md` only when the user wants qz’s built-in agent to plan and execute the task, including human–agent collaboration; that path uses the provider and model configured in qz.
20
12
 
21
13
  When multiple browsers are registered, run `qz browsers --json` and pass `--client CLIENT_UUID` to select a profile explicitly. Existing sessions are located via read-only queries before execution; ambiguous or unavailable ownership requires explicit selection. To pair a specific profile without launching a browser, run `qz connect uri --name work --json` and open `result.url` in that profile. An incompatible local service is restarted once before browser operations; an incompatible extension requires updating the CLI and extension together.
22
14
 
@@ -36,25 +28,26 @@ return await qz.tab(opened).read({ limit: 3000 });
36
28
  JS
37
29
  ```
38
30
 
39
- `qz.openTab({url,tabAlias?})` creates a page and returns a Tab `{tabRef,url,title,active}`; `qz.listTabs()` returns a list in `tabs`. `qz.tab(...)` binds an existing page using that Tab, its `tabRef`, or its optional alias. Aliases use 1–64 letters, digits, underscores, or hyphens (not all digits), and expire when the tab closes. Returned positive integers or numeric strings are valid references; never guess one. `active` marks the tab visible to the user and never selects an operation's target. Use `page.goto(url)` to navigate an existing task tab. `page.setMode("mobile")` switches to mobile mode (`"desktop"` restores it); a change reloads the page, so re-observe it afterward.
31
+ `qz.openTab({url,tabAlias?})` creates a page and returns a Tab `{tabRef,url,title,active}`; `qz.listTabs()` returns `{tabs,count,foreign}`, where each Tab also has `ownership` and `foreign` counts tabs held by other sessions. `qz.tab(...)` binds an existing page using that Tab, its `tabRef`, or its optional alias. Aliases use 1–64 letters, digits, underscores, or hyphens (not all digits), and expire when the tab closes. Returned positive integers or numeric strings are valid references; never guess one. `active` marks the tab visible to the user and never selects an operation's target. Use `page.goto(url)` to navigate an existing task tab. `page.setMode("mobile")` switches to mobile mode (`"desktop"` restores it); a change reloads the page, so re-observe it afterward.
40
32
 
41
- One tab belongs to one session at a time. Use `qz.listTabs({scope:"all"})` for read-only inspection of other tabs, and a session-owned page for actions or evaluation. Binding a Page neither changes ownership nor selects the active tab.
33
+ One tab belongs to one session at a time. `qz.listTabs()` lists this session's tabs (`ownership:"owned"`) and tabs no session holds (`"unclaimed"`), including the page the user is viewing when it is unclaimed. To work on a page the user already has open, bind its `tabRef` from this list instead of opening a duplicate. Listing never claims a tab: the first action, navigation, or `page.evaluate` on an unclaimed tab claims it for this session, and reads leave ownership unchanged. `qz.listTabs({scope:"owned"})` lists only this session's tabs. `qz.listTabs({scope:"all"})` adds tabs held by other sessions (`"foreign"`), which allow read-only inspection only. Binding a Page neither changes ownership nor selects the active tab.
42
34
 
43
35
  ## Scripts and the qz SDK
44
36
 
45
- `qz script --session ID [FILE|-] [--args JSON]` runs JavaScript orchestration outside web pages. Read a local file or `-` for stdin; `--args` passes a JSON value exposed as `args`. Use top-level `await` and `return`, starting with a short comment stating the script's purpose. The outer script survives page navigation and provides `qz`, `args`, `trace()`, and `console`, without `document`/`window`, Node.js, `fs`, `process`, `require`, or `import`. Awaited SDK calls return business values directly and throw on failure.
37
+ `qz script --session ID [FILE|-] [--args JSON]` runs JavaScript orchestration outside web pages. Read a local file or `-` for stdin; `--args` passes a JSON value exposed as `args`. Use top-level `await` and `return`, starting with a short comment stating the script's purpose. Every `timeout` option is in seconds; a value outside a call's range fails instead of being clamped. The outer script survives page navigation and provides `qz`, `args`, `trace()`, and `console`, without `document`/`window`, Node.js, `fs`, `process`, `require`, or `import`. Awaited SDK calls return business values directly and throw on failure.
46
38
 
47
39
  Bind `const page = qz.tab(tabRef)` and use:
48
40
 
49
41
  | Purpose | SDK methods |
50
42
  | --- | --- |
51
43
  | Navigation and page mode | `page.goto(url, options?)`, `page.close()`, `page.setMode(mode)` |
52
- | Focused observations | `page.read({limit,...})`, `page.find({role:"link",...})`, `page.axTree(options)`, `page.screenshot(options)` |
44
+ | Focused observations | `page.read({limit,...})`, `page.find(query)`, `page.axTree(options)`, `page.screenshot(options)` |
45
+ | Page scrolling | `page.scroll({direction,pixels})` with direction `up`, `down`, `top`, or `bottom` |
53
46
  | Page JavaScript | `page.evaluate(code, args?, options?)` |
54
47
  | State waits | `page.waitForURL(pattern, options?)`, `page.waitForLoad(options?)`, `page.waitForFunction(code, args?, options?)` |
55
48
  | Element operations | `page.locator(targetOrQuery)` with the methods below |
56
49
 
57
- Use `qz.listTools()` for compact signatures of additional capabilities and `qz.listTools({names:["list_images"]})` for selected full schemas, including declared `outputSchema` fields and examples. Invoke those capabilities through `qz.call(name, args)`, using their catalog names and explicit `tabRef` for page calls. Read unfamiliar schemas before calling and use declared return fields.
50
+ Use `qz.listTools()` for compact signatures of additional capabilities and `qz.listTools({names:["list_images"]})` for selected full schemas, including declared `outputSchema` fields and examples. Invoke those capabilities through `qz.call(name, args)`, using their catalog names and explicit `tabRef` for page calls. Read unfamiliar schemas before calling and use declared return fields; unknown fields are rejected. Raw tools that take an element accept `target` or `locator` (the query object below), and `within` limits a search to a container.
58
51
 
59
52
  `qz_skills` are browser workflows installed in the qz extension. `qz session create --json` returns their summaries in `result.qz_skills`, and each script receipt includes them in `context.qz_skills`: enabled user Global and Resident workflows, plus Site workflows matching sites visited by this task. Start a new site with a short observation script and check this catalog before further work. Global and Resident workflow tools are callable from session creation and appear in `qz.listTools()`; Site workflow tools become callable when the task visits a matching site. Load a relevant workflow with `return await qz.call("load_skill", {name:"skill-name"})`. Read its `content` and `tools`: each tool includes its exact calling `name`, description, input `parameters`, and declared `outputSchema` with examples. Loading activates those tools for the session immediately, so later calls in the same script can use them. Follow the workflow’s page prerequisites, then invoke `qz.call(name, args)` with those parameters and an explicit `tabRef` for page tools. After conversation compaction, reload the workflow if its instructions are missing. The catalog repeats on every receipt; full instructions are loaded on demand through qz, independently of the external agent's own Skill loader. Use `qz.call("list_skills", {url:"https://example.com"})` to query explicitly; omit `url` for all user workflows. Built-in Skills are hidden from discovery but remain loadable by name.
60
53
 
@@ -80,9 +73,9 @@ Observe the relevant controls and choose the result needed by the next step. Use
80
73
 
81
74
  Use search results as leads; verify key facts on the original pages. Include source URLs for key findings and make clear when evidence or coverage is incomplete.
82
75
 
83
- `page.locator(query)` stores a query and resolves it when an operation runs. Queries accept `target`, `role`, `name`, `text`, `exact`, `scope`, and `nth`; at least one of `target`, `role`, `name`, or `text` is required. The SDK accepts `{css:"#input"}` as shorthand for `{target:"css:#input"}`; use one form. CSS lists have one prefix for the whole expression (`"css:input, button"`). `name` matches the accessible name reported by `page.find` or `page.axTree`; `text` matches element text. Both normalize whitespace and match exactly, case-sensitively, by default; `exact:false` matches a substring. `scope` is an observed `uid:` or `css:` root, including a frame document reference. A pinned UID already identifies its scope; do not combine it with `scope`. Element operations require one match: narrow an ambiguous query or explicitly choose `locator.nth(index)` using a zero-based index.
76
+ `page.locator(query)` stores a query and resolves it when an operation runs. Queries accept `target`, `role`, `name`, `text`, `exact`, `within`, `nth`, and `includeHidden`; at least one of `target`, `role`, `name`, or `text` is required. The SDK accepts `{css:"#input"}` as shorthand for `{target:"css:#input"}`; use one form. CSS lists have one prefix for the whole expression (`"css:input, button"`). `name` matches the accessible name reported by `page.find` or `page.axTree`; `text` matches element text. Both normalize whitespace and match exactly, case-sensitively, by default; `exact:false` matches a substring. `within` is an observed `uid:` or `css:` container, including a frame document reference. A pinned UID already identifies its container; do not combine it with `within`. Hidden elements match `role`/`name`/`text` queries, and count toward their `nth`, only with `includeHidden:true`, while a `target` always resolves, hidden or not. Element operations require one match: narrow an ambiguous query or explicitly choose `locator.nth(n)` using a zero-based index.
84
77
 
85
- Locator methods are `click(options?)`, `doubleClick(options?)`, `hover(options?)`, `fill(value, options?)`, `press(key, options?)`, `select(value, options?)`, `check(options?)`, `uncheck(options?)`, `scroll(options?)`, and `dragTo(destination, options?)` on the same tab. Actions locate and wait for a usable element within their shared `timeout` budget (seconds, default 20, maximum 120), then dispatch once. Call the next locator operation directly; it handles readiness. Dispatched actions are never automatically replayed. Ordinary success returns `{}`, with `navigated`/`url` or `navigationPending` when navigation is detected, and `popups` for observed new Tabs. If `confirmationRequired` is true, verify the field locally before continuing.
78
+ Locator methods are `click(options?)`, `doubleClick(options?)`, `hover(options?)`, `fill(value, options?)`, `press(key, options?)`, `select(value, options?)`, `check(options?)`, `uncheck(options?)`, `scrollIntoView(options?)`, and `dragTo(destination, options?)` on the same tab. Action options are `timeout`, `expect`, `tabAlias` (with `expect:"popup"`), and `submit` (fill only). Actions locate and wait for a usable element within their shared `timeout` budget (default 20, maximum 120), then dispatch once. Call the next locator operation directly; it handles readiness. Dispatched actions are never automatically replayed. Ordinary success returns `{}`, with `navigated`/`url` or `navigationPending` when navigation is detected, and `popups` for observed new Tabs. If `confirmationRequired` is true, verify the field locally before continuing.
86
79
 
87
80
  ```js
88
81
  // Fill an observed form and choose its asynchronously appearing suggestion.
@@ -92,14 +85,14 @@ await page.locator({ role: "option", name: args.expectedOption }).click({ timeou
92
85
  return await page.locator(args.fieldTarget).inspect();
93
86
  ```
94
87
 
95
- Use `locator.inspect()` for current element state and `locator.read({limit})` for its text. `locator.wait({state:"visible",timeout:10})` waits for `attached`, `visible` (default), `hidden`, or `detached` state. Hidden includes removal or invisibility; detached requires removal. `locator.count()` reads the current attached match count, including hidden elements; `locator.wait({count:3,timeout:10})` waits for exactly that count, including zero. Use either `state` or `count` per wait. A match or count describes the current moment; it does not prove a dynamic list has finished updating.
88
+ Use `locator.inspect()` for current element state, `locator.read({limit})` for its text, and `locator.axTree()` or `locator.screenshot()` for its subtree or pixels. `locator.wait({state:"visible",timeout:10})` waits for `attached`, `visible` (default), `hidden`, or `detached` state. Hidden includes removal or invisibility; detached requires removal. `locator.count()` reads the current match count; `locator.wait({count:3,timeout:10})` waits for exactly that count, including zero. Use either `state` or `count` per wait. A match or count describes the current moment; it does not prove a dynamic list has finished updating.
96
89
 
97
- When an action must cause navigation or a popup, pass `waitFor:"navigation"` or `waitFor:"popup"` in its options. This installs the listener before dispatch. Navigation returns its observation; popup returns a Tab `{tabRef,url,title,active}` directly. A failed wait never replays the action.
90
+ When an action must cause navigation or a popup, pass `expect:"navigation"` or `expect:"popup"` in its options. This installs the listener before dispatch. Navigation returns its observation; popup returns a Tab `{tabRef,url,title,active}` directly. A failed wait never replays the action.
98
91
 
99
92
  ```js
100
93
  // Open a detail page and read it.
101
94
  const popup = await qz.tab("page").locator({ role: "link", name: "Details" })
102
- .click({ waitFor: "popup", timeout: 10 });
95
+ .click({ expect: "popup", timeout: 10 });
103
96
  return await qz.tab(popup).read({ limit: 2000 });
104
97
  ```
105
98
 
@@ -119,7 +112,7 @@ return await page.waitForFunction(name => {
119
112
 
120
113
  Use selectors and fields observed on the page. If it retains previous values while loading, also wait for a completion signal that distinguishes the selected item's result.
121
114
 
122
- `page.find` returns an array of current element observations (up to `limit`), or `[]` when none match. Use normal array operations, e.g. `(await page.find({role:"link"})).slice(0,5)`; `waitFor:"match",timeout:10` waits for a candidate. Use `role`, `tag`, and `type` for single filters, or `roles`, `tags`, and `types` for multiple alternatives; use one form per filter. For example, `page.find({tag:"input",type:"search"})` finds search inputs. Its `pattern` accepts a regex string (case-insensitive by default) or a `RegExp`, as in `page.find({role:"link",pattern:/download/i})`; a `RegExp` supplies its own flags. Locator `name` and `text` match their specific fields. `page.axTree` uses `scope:"viewport"` or `"full"`; narrow `target` or `limit` when inspecting one control. Reserve screenshots for visual checks.
115
+ `page.find(query)` accepts the same `role`, `name`, `text`, `exact`, and `within` fields as `page.locator(query)` and returns an array of every matching current element observation (`[]` when none match). Results are in the same order as `nth` for the same query: document order, except that `exact:false` or `pattern` lists whole `name`/`text` matches first, then prefix matches, then the rest. Use array operations on the result, e.g. `(await page.find({text:"Tomorrow",exact:false})).slice(0,5)`; `waitForMatch:true,timeout:10` waits for a candidate. Hidden elements are skipped unless `includeHidden:true`. Use `role`, `tag`, and `type` for single filters, or `roles`, `tags`, and `types` for multiple alternatives; use one form per filter. For example, `page.find({tag:"input",type:"search"})` finds search inputs. For a regular expression, pass `pattern` instead of `name`/`text`/`exact`: a regex string (case-insensitive by default) or a `RegExp`, as in `page.find({role:"link",pattern:/download/i})`; a `RegExp` supplies its own flags. `page.axTree` reads `scope:"viewport"` or `"full"`; use `locator.axTree()` or a smaller `limit` for one control. Reserve screenshots for visual checks.
123
116
 
124
117
  Run one `qz script` at a time per session. Within it, use `await` for dependencies and `Promise.all` for independent page workflows. Same-page actions serialize; the caller sees results when the script returns. `Promise.all` preserves input order and rejects on first failure without cancelling siblings; use `Promise.allSettled` to retain every outcome. Await all calls before returning: exit cancels unfinished calls.
125
118
 
package/src/tools.mjs CHANGED
@@ -264,7 +264,7 @@ const GUIDE_TOOL = {
264
264
  properties: {},
265
265
  },
266
266
  };
267
- const DRIVE_INSTRUCTIONS = "先用 qingzhou_create_drive_session 或 qz session create --title TITLE 创建带可读标题的会话,后续每次执行工具或脚本都传入该 sessionId(CLI 必传 --session,缺失退出码为 2,不会自动建会话);list_tools/get_tool 查询基础目录无需会话。页面工具必传 tabRef(open_tab 设置的可选 tabAlias,或 open_tab / list_tabs 返回的 tabRef)。通过 qingzhou_execute_tool(单工具、calls 批次或 run_script 外层编排)执行操作,页面内 DOM 求值使用 evaluate。Create a session with a readable title using qingzhou_create_drive_session or qz session create --title TITLE, then pass that sessionId on every tool or script execution (CLI calls require --session; omitting it exits 2 and does not create a session). list_tools/get_tool can inspect the base catalog without a session. Page tools require tabRef (an optional tabAlias from open_tab, or the tabRef returned by open_tab or list_tabs). Orchestrate with qingzhou_execute_tool (single tool, calls batch, or outer run_script) and use evaluate for in-page DOM execution.";
267
+ const DRIVE_INSTRUCTIONS = "先用 qingzhou_create_drive_session 或 qz session create --title TITLE 创建带可读标题的会话,后续每次执行工具或脚本都传入该 sessionId(CLI 必传 --session,缺失退出码为 2,不会自动建会话);list_tools/get_tool 查询基础目录无需会话。页面工具必传 tabRef(open_tab 设置的可选 tabAlias,或 open_tab / list_tabs 返回的 tabRef)。list_tabs 默认列出本会话的页面和未被占用的页面(active=true 是用户正在看的页面);对未占用页面首次执行操作、导航或 evaluate 时由本会话占用。通过 qingzhou_execute_tool(单工具、calls 批次或 run_script 外层编排)执行操作,页面内 DOM 求值使用 evaluate。Create a session with a readable title using qingzhou_create_drive_session or qz session create --title TITLE, then pass that sessionId on every tool or script execution (CLI calls require --session; omitting it exits 2 and does not create a session). list_tools/get_tool can inspect the base catalog without a session. Page tools require tabRef (an optional tabAlias from open_tab, or the tabRef returned by open_tab or list_tabs). By default list_tabs lists this session's tabs and unclaimed tabs (active:true is the user's page); the first action, navigation, or evaluate on an unclaimed tab claims it for this session. Orchestrate with qingzhou_execute_tool (single tool, calls batch, or outer run_script) and use evaluate for in-page DOM execution.";
268
268
 
269
269
  export function mcpCatalogProfile({ clientName = "", profile = "" } = {}) {
270
270
  const explicit = String(profile || "").trim().toLowerCase();