chromeflow 0.5.0 → 0.7.1
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/CLAUDE.md +28 -3
- package/README.md +1 -2
- package/dist/setup.js +6 -3
- package/dist/tools/browser.js +38 -56
- package/dist/tools/flow.js +191 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -25,7 +25,9 @@ Do NOT ask "should I open the browser?" — just do it. The user expects seamles
|
|
|
25
25
|
2. **Never use `take_screenshot` to read page content.** After `scroll_page`, after
|
|
26
26
|
`click_element`, after navigation — always call `get_page_text`, not `take_screenshot`.
|
|
27
27
|
`get_page_text` returns up to 10,000 characters; if truncated it tells you the next
|
|
28
|
-
`startIndex` to paginate.
|
|
28
|
+
`startIndex` to paginate. When you only need to confirm a specific phrase is present,
|
|
29
|
+
prefer `find_text("phrase")` — it returns matches with context and selectors instead of
|
|
30
|
+
dumping the whole page. Screenshots are only for locating an element's pixel position
|
|
29
31
|
when DOM queries have already failed. Never take more than 1–2 screenshots in a row.
|
|
30
32
|
|
|
31
33
|
3. **Use `wait_for_selector` to wait for async page changes** (build completion, modals,
|
|
@@ -98,13 +100,18 @@ After a secret key or API key is revealed:
|
|
|
98
100
|
Use the absolute path for `envPath` — it's the Claude Code working directory + `/.env`.
|
|
99
101
|
|
|
100
102
|
To capture and share a screenshot (e.g. for uploading to a form or pasting into a chat),
|
|
101
|
-
use `
|
|
103
|
+
use `take_screenshot(copy_to_clipboard=true, save_to="downloads")` — saves a PNG to ~/Downloads
|
|
104
|
+
and copies it to the clipboard. The defaults (`copy_to_clipboard=false, save_to="none"`) return
|
|
105
|
+
the image to Claude only.
|
|
102
106
|
|
|
103
107
|
## Working with complex forms
|
|
104
108
|
- Before filling a large or unfamiliar form, call `get_form_fields()` to get a full inventory
|
|
105
109
|
of every field (type, label, current value, vertical position, and section heading). Use
|
|
106
110
|
`get_elements()` when you need pixel coordinates of visible elements; use `get_form_fields()`
|
|
107
111
|
when you need to understand the full structure of a form including fields below the fold.
|
|
112
|
+
If you only need one or two specific fields, use `find_input("hint")` instead — targeted
|
|
113
|
+
lookup is much cheaper than the full inventory and returns labels you can pipe straight
|
|
114
|
+
into `fill_input`.
|
|
108
115
|
- `get_form_fields()` includes `[type=file]` fields even when they are visually hidden behind
|
|
109
116
|
custom drag-and-drop zones. Use `set_file_input(hint, filePath)` to upload a file — provide
|
|
110
117
|
the label/hint text and the absolute path to the file on disk.
|
|
@@ -147,6 +154,24 @@ use `take_and_copy_screenshot()` — it saves a PNG to ~/Downloads and copies it
|
|
|
147
154
|
- For multi-session tasks (long forms that may exceed context), call `save_page_state()` as a
|
|
148
155
|
checkpoint. A future session can call `restore_page_state()` to reload all field values.
|
|
149
156
|
|
|
157
|
+
## Discovery — find without dumping the whole page
|
|
158
|
+
|
|
159
|
+
Three lightweight tools save tokens vs `get_page_text` / `get_form_fields` when you don't need the full content:
|
|
160
|
+
|
|
161
|
+
- `find_text("Saved successfully")` — grep the DOM. Returns surrounding context, a CSS selector, and a `clickable` flag for each match. Use this instead of `get_page_text` when you're checking whether a specific phrase is present, or to locate a button by its visible text. If `clickable=true`, pipe the matched text straight into `click_element`.
|
|
162
|
+
- `find_input("Email")` — fuzzy form-field lookup, top-N. Returns labels you can pipe straight into `fill_input(label, value)` — both tools share the same match ranks (`aria-eq` → `placeholder-eq` → `label-text-eq` → `name-eq` → `id-eq` → `*-includes` → `fuzzy-text-walk`). Cheaper than `get_form_fields` when you just need a couple of specific fields. Pass `type_filter="email"` to restrict to a specific input type.
|
|
163
|
+
- `wait_for_text("Saved")` — wait for text to appear without knowing the selector ahead of time. Complements `wait_for_selector` for the case where you only know the post-action message.
|
|
164
|
+
|
|
165
|
+
All three pierce open shadow roots and accept `frame="iframe.selector"` for same-origin iframes. Pass `regex=true` on `find_text` / `wait_for_text` for case-insensitive regex matching. Pass `exact=true` on `find_input` to refuse fuzzy text-walk matches.
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
find_text("Build complete", scope_selector=".log-output") — only check the build log section
|
|
169
|
+
find_input("Card number", type_filter="text") — find Stripe's card-number field
|
|
170
|
+
wait_for_text("Deploy successful", timeout_ms=30000) — wait up to 30s after clicking Deploy
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Reach for these BEFORE `get_page_text` / `get_form_fields` when the goal is "is X here?" or "where is X?". Reserve `get_page_text` for reading actual content, and `get_form_fields` for understanding a whole form's structure.
|
|
174
|
+
|
|
150
175
|
## Working with multiple tabs
|
|
151
176
|
- Before opening a new tab, call `list_tabs()` to check if the target URL is already open —
|
|
152
177
|
use `switch_to_tab` to return to it instead of opening a duplicate.
|
|
@@ -277,7 +302,7 @@ instead of DOM text): `get_page_text()` returns nothing useful. Zoom out and scr
|
|
|
277
302
|
```js
|
|
278
303
|
// Shrink to fit wide content, then screenshot
|
|
279
304
|
document.body.style.zoom = '0.4';
|
|
280
|
-
// use
|
|
305
|
+
// use take_screenshot() to read it
|
|
281
306
|
// restore afterward:
|
|
282
307
|
document.body.style.zoom = '1';
|
|
283
308
|
```
|
package/README.md
CHANGED
|
@@ -86,8 +86,7 @@ Claude will navigate, highlight steps, click what it can, pause for anything sen
|
|
|
86
86
|
| Run arbitrary JS | `execute_script` |
|
|
87
87
|
| Read browser console output | `get_console_logs` |
|
|
88
88
|
| Capture credentials to `.env` | `read_element`, `write_to_env` |
|
|
89
|
-
| Screenshot (
|
|
90
|
-
| Screenshot + save + copy to clipboard | `take_and_copy_screenshot` |
|
|
89
|
+
| Screenshot (Claude-only by default; pass `copy_to_clipboard` / `save_to` to share) | `take_screenshot` |
|
|
91
90
|
| Screenshot the terminal window | `capture_terminal` |
|
|
92
91
|
| Save/restore form state across tabs | `save_page_state`, `restore_page_state` |
|
|
93
92
|
|
package/dist/setup.js
CHANGED
|
@@ -160,8 +160,7 @@ const CHROMEFLOW_TOOLS = [
|
|
|
160
160
|
"scroll_to_element",
|
|
161
161
|
"save_page_state",
|
|
162
162
|
"restore_page_state",
|
|
163
|
-
// v0.1.25+
|
|
164
|
-
"take_and_copy_screenshot",
|
|
163
|
+
// v0.1.25+ → merged into take_screenshot in v0.7.0
|
|
165
164
|
// v0.1.32+
|
|
166
165
|
"fill_form",
|
|
167
166
|
// v0.1.36+
|
|
@@ -177,7 +176,11 @@ const CHROMEFLOW_TOOLS = [
|
|
|
177
176
|
// v0.1.57+
|
|
178
177
|
"inspect_request_headers",
|
|
179
178
|
// v0.2.1+
|
|
180
|
-
"wait_for_change"
|
|
179
|
+
"wait_for_change",
|
|
180
|
+
// v0.6.0+
|
|
181
|
+
"find_text",
|
|
182
|
+
"find_input",
|
|
183
|
+
"wait_for_text"
|
|
181
184
|
].map((t) => `mcp__chromeflow__${t}`);
|
|
182
185
|
function patchSettingsLocalJson(cwd) {
|
|
183
186
|
const claudeDir = join(cwd, ".claude");
|
package/dist/tools/browser.js
CHANGED
|
@@ -53,58 +53,52 @@ ${lines.join("\n")}` }]
|
|
|
53
53
|
);
|
|
54
54
|
server.tool(
|
|
55
55
|
"take_screenshot",
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
`Capture a screenshot of the current page. By default returns the image to Claude only; pass copy_to_clipboard or save_to to also share the image outside Claude (paste into a chat, upload to a form, keep as a file).
|
|
57
|
+
|
|
58
|
+
IMPORTANT: Do NOT use this to read page content \u2014 call get_page_text instead, which is faster and returns searchable text. Screenshots are ONLY for locating an element's pixel coordinates when DOM queries have already failed. Never take a screenshot immediately after open_page, scroll_page, or click_element. Never take more than 1-2 screenshots in a row.`,
|
|
59
|
+
{
|
|
60
|
+
copy_to_clipboard: z.boolean().optional().describe("Copy the PNG to the system clipboard (macOS only). Default false."),
|
|
61
|
+
save_to: z.enum(["downloads", "cwd", "none"]).optional().describe(`Save the PNG to disk: "downloads" (~/Downloads), "cwd" (Claude's working directory), or "none" (default \u2014 image returned only to Claude).`)
|
|
62
|
+
},
|
|
63
|
+
async ({ copy_to_clipboard = false, save_to = "none" }) => {
|
|
64
|
+
const sharing = copy_to_clipboard || save_to !== "none";
|
|
65
|
+
const response = await bridge.request({ type: "screenshot", grid: !sharing });
|
|
60
66
|
if (response.type !== "screenshot_response") {
|
|
61
67
|
throw new Error("Unexpected response from extension");
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
type: "image",
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
]
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
);
|
|
78
|
-
server.tool(
|
|
79
|
-
"take_and_copy_screenshot",
|
|
80
|
-
`Take a screenshot, return it to Claude, copy it to the system clipboard, and save it as a PNG file.
|
|
81
|
-
Use this instead of take_screenshot when you need the image outside of Claude \u2014 to paste into a chat, upload to a form, or keep as a file.
|
|
82
|
-
Unlike take_screenshot (Claude-only), this also puts the image on the clipboard and saves it to disk.
|
|
83
|
-
save_to controls where the PNG is saved: "downloads" (default) saves to ~/Downloads, "cwd" saves to Claude's current working directory.`,
|
|
84
|
-
{
|
|
85
|
-
save_to: z.enum(["downloads", "cwd"]).optional().describe(`Where to save the PNG file: "downloads" (~/Downloads, default) or "cwd" (Claude's current working directory)`)
|
|
86
|
-
},
|
|
87
|
-
async ({ save_to = "downloads" }) => {
|
|
88
|
-
const response = await bridge.request({ type: "screenshot", grid: false });
|
|
89
|
-
if (response.type !== "screenshot_response") throw new Error("Unexpected response from extension");
|
|
69
|
+
if (!sharing) {
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "image", data: response.image, mimeType: "image/png" },
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: `Screenshot captured (${response.width}x${response.height}). Analyze the image to identify element positions for highlighting.`
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
90
80
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
91
81
|
const filename = `chromeflow-${timestamp}.png`;
|
|
92
82
|
const imageBuffer = Buffer.from(response.image, "base64");
|
|
93
83
|
const tmpPath = join(tmpdir(), filename);
|
|
94
84
|
writeFileSync(tmpPath, imageBuffer);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
85
|
+
const notes = [];
|
|
86
|
+
if (save_to !== "none") {
|
|
87
|
+
const savePath = save_to === "cwd" ? join(process.cwd(), filename) : join(homedir(), "Downloads", filename);
|
|
88
|
+
copyFileSync(tmpPath, savePath);
|
|
89
|
+
notes.push(`Saved to ${savePath}`);
|
|
90
|
+
}
|
|
91
|
+
if (copy_to_clipboard) {
|
|
92
|
+
try {
|
|
93
|
+
execSync(`osascript -e 'set the clipboard to (read (POSIX file "${tmpPath}") as \xABclass PNGf\xBB)'`);
|
|
94
|
+
notes.push("Copied to clipboard");
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
103
97
|
}
|
|
104
98
|
return {
|
|
105
99
|
content: [
|
|
106
100
|
{ type: "image", data: response.image, mimeType: "image/png" },
|
|
107
|
-
{ type: "text", text:
|
|
101
|
+
{ type: "text", text: notes.length ? notes.join(". ") + "." : `Screenshot captured (${response.width}x${response.height}).` }
|
|
108
102
|
]
|
|
109
103
|
};
|
|
110
104
|
}
|
|
@@ -285,15 +279,7 @@ For iframe contenteditables: pass \`frame\` (a CSS selector for the iframe). typ
|
|
|
285
279
|
);
|
|
286
280
|
server.tool(
|
|
287
281
|
"set_file_input",
|
|
288
|
-
|
|
289
|
-
Uses Chrome DevTools Protocol to set the file \u2014 the only way to bypass the browser's file-input script restriction.
|
|
290
|
-
|
|
291
|
-
Returns success=true ONLY if an observable change is detected within wait_ms: either the page-level file count goes up, or the file is consumed by the page's React handler (input is reset), or verify_selector matches a new element on the page. Otherwise success=false with a clear message \u2014 typically because the page rejected the file (size/type) or the React handler hasn't run yet.
|
|
292
|
-
|
|
293
|
-
For rapid batch uploads (multiple set_file_input calls in a row), this commit-wait prevents the second CDP call from overwriting the first before React reads it \u2014 no manual sleep needed between calls.
|
|
294
|
-
|
|
295
|
-
hint: label text, name, or CSS selector of the file input (or empty string to target the first file input on the page).
|
|
296
|
-
file_path: absolute path to the file on the local filesystem (e.g. /Users/you/Downloads/task.zip).`,
|
|
282
|
+
"Upload a file to a file input \u2014 works even when the input is hidden behind a custom drag-and-drop zone. Returns success=true only after an observable commit (file count goes up, input gets reset, or verify_selector appears within wait_ms). See CLAUDE.md for batch-upload guidance.",
|
|
297
283
|
{
|
|
298
284
|
hint: z.string().describe("Label text, name, or surrounding text of the file input. Use empty string to target the first file input on the page."),
|
|
299
285
|
file_path: z.string().describe("Absolute path to the file to upload (e.g. /Users/you/Downloads/task.zip)"),
|
|
@@ -339,13 +325,9 @@ Returns the matched element's tag/name/id/type so you can verify it was the righ
|
|
|
339
325
|
);
|
|
340
326
|
server.tool(
|
|
341
327
|
"execute_script",
|
|
342
|
-
`Execute JavaScript in the current page's context and return the result
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
Top-level return statements are supported (e.g. multi-statement scripts with \`return value;\`).
|
|
346
|
-
Top-level \`await\` is supported \u2014 write \`return await fetch(url).then(r => r.json())\` directly without the window.__variable + sleep + re-read pattern. Detected automatically when the code contains the \`await\` keyword.
|
|
347
|
-
If the page called alert()/confirm()/prompt() since the last check, the message will appear as PAGE ALERT in the result \u2014 read it and act on it.
|
|
348
|
-
NOTE: Pages with strict Content Security Policy (e.g. Stripe, GitHub) will fall through to a CDP path that bypasses CSP \u2014 but the script still runs, so retries usually aren't needed.`,
|
|
328
|
+
`Execute JavaScript in the current page's context and return the result. Use for reading framework state or DOM properties not visible in text \u2014 prefer get_page_text for visible content. Top-level \`return\` and \`await\` are supported.
|
|
329
|
+
|
|
330
|
+
CSP-strict pages (Stripe, GitHub) silently fall through to a CDP eval path. Page alerts (alert/confirm/prompt) fired since the last script appear as PAGE ALERT in the result.`,
|
|
349
331
|
{
|
|
350
332
|
code: z.string().describe(
|
|
351
333
|
"JavaScript expression or multi-statement script to evaluate in the page. Top-level `return` is supported."
|
package/dist/tools/flow.js
CHANGED
|
@@ -187,6 +187,197 @@ Examples: scroll_to_element("#submit-btn"), scroll_to_element("Billing address")
|
|
|
187
187
|
return { content: [{ type: "text", text: msg }] };
|
|
188
188
|
}
|
|
189
189
|
);
|
|
190
|
+
server.tool(
|
|
191
|
+
"find_text",
|
|
192
|
+
`Search the page for text and get back actionable matches without dumping the whole DOM. Use this instead of get_page_text when you only need to know "is X on the page?" or "where is the Save button?".
|
|
193
|
+
|
|
194
|
+
For each match, returns the surrounding context, the nearest meaningful element (button/link/heading/role/label/etc.), a best-effort CSS selector, and a clickable flag. If a match is clickable, pipe the matched text into click_element to act on it.
|
|
195
|
+
|
|
196
|
+
Use when:
|
|
197
|
+
- Checking whether a toast / error message / heading appeared after an action
|
|
198
|
+
- Locating one of multiple buttons by text
|
|
199
|
+
- Finding all instances of a phrase to count or inspect them
|
|
200
|
+
|
|
201
|
+
Do NOT use for: reading large blocks of body text \u2014 use get_page_text(selector=...) for that. find_text returns one short snippet per match, not the full content.
|
|
202
|
+
|
|
203
|
+
Pierces open shadow roots. Pass frame="iframe.selector" to search inside a same-origin iframe.`,
|
|
204
|
+
{
|
|
205
|
+
query: z.string().describe(
|
|
206
|
+
"Text to search for. Substring match by default; pass regex=true to interpret as a case-insensitive regex."
|
|
207
|
+
),
|
|
208
|
+
max: z.number().int().min(1).optional().describe("Maximum matches to return (default 10). total_matches is reported even when truncated."),
|
|
209
|
+
scope_selector: z.string().optional().describe('Limit search to descendants of this CSS selector (e.g. ".main-panel", "#dialog"). Default searches the whole body.'),
|
|
210
|
+
regex: z.boolean().optional().describe("Treat query as a regex (case-insensitive). Default false."),
|
|
211
|
+
visible_only: z.boolean().optional().describe("Skip matches inside display:none / visibility:hidden / aria-hidden=true ancestors. Default true."),
|
|
212
|
+
context_chars: z.number().int().min(0).optional().describe("Characters of surrounding context to include before/after each match. Default 60."),
|
|
213
|
+
frame: z.string().optional().describe('Same-origin iframe CSS selector (e.g. "iframe.editor") to search inside. Cross-origin iframes are not supported.')
|
|
214
|
+
},
|
|
215
|
+
async ({ query, max, scope_selector, regex, visible_only, context_chars, frame }) => {
|
|
216
|
+
const response = await bridge.request({
|
|
217
|
+
type: "find_text",
|
|
218
|
+
query,
|
|
219
|
+
max,
|
|
220
|
+
scope_selector,
|
|
221
|
+
regex,
|
|
222
|
+
visible_only,
|
|
223
|
+
context_chars,
|
|
224
|
+
frame
|
|
225
|
+
});
|
|
226
|
+
const r = response;
|
|
227
|
+
if (r.frame_error) {
|
|
228
|
+
return { content: [{ type: "text", text: r.frame_error }] };
|
|
229
|
+
}
|
|
230
|
+
if (r.scope_missed) {
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{ type: "text", text: `Scope selector "${scope_selector}" did not match any element \u2014 no search performed.` }
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (r.matches.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
content: [
|
|
240
|
+
{ type: "text", text: `No matches found for "${query}".` }
|
|
241
|
+
]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const lines = r.matches.map((m, i) => {
|
|
245
|
+
const role = m.role ? `, role=${m.role}` : "";
|
|
246
|
+
const click = m.clickable ? " \u2014 clickable" : "";
|
|
247
|
+
const pos = m.position ? ` at (${m.position.x}, ${m.position.y}, ${m.position.width}\xD7${m.position.height})` : "";
|
|
248
|
+
return ` ${i + 1}. [${m.tag}${role}]${click}${pos} \u2014 selector: ${m.selector}
|
|
249
|
+
"${m.text}"
|
|
250
|
+
context: ${m.context}`;
|
|
251
|
+
});
|
|
252
|
+
const header = r.truncated ? `Found ${r.matches.length} of ${r.total_matches} matches for "${query}":` : `Found ${r.matches.length} match${r.matches.length === 1 ? "" : "es"} for "${query}":`;
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: `${header}
|
|
255
|
+
${lines.join("\n")}` }]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
server.tool(
|
|
260
|
+
"find_input",
|
|
261
|
+
`Locate form inputs whose label / placeholder / aria-label / name / id matches a hint, returning the top N with their section heading. Use this instead of get_form_fields when you only need a couple of fields \u2014 it's the targeted lookup, not the full inventory.
|
|
262
|
+
|
|
263
|
+
Match strength is reported as match_kind: aria-eq / placeholder-eq / label-text-eq / name-eq / id-eq are exact matches; *-includes are partial matches; fuzzy-text-walk is the lowest-confidence fallback.
|
|
264
|
+
|
|
265
|
+
Returned labels are designed to be piped straight into fill_input(label, value), which uses the same fuzzy ranks to find the same field again. No CSS selector is returned \u2014 fill_input matches by label text, not by selector.
|
|
266
|
+
|
|
267
|
+
Use when:
|
|
268
|
+
- "Is the Email field on this page?"
|
|
269
|
+
- "Find the price input below the fold"
|
|
270
|
+
- "Which input has placeholder 'you@example.com'?"
|
|
271
|
+
|
|
272
|
+
Do NOT use for: filling fields (use fill_input / fill_form). For the full form inventory (every field including hidden ones), use get_form_fields.
|
|
273
|
+
|
|
274
|
+
Pierces open shadow roots. Pass frame="iframe.selector" to search inside a same-origin iframe. Pass exact=true to refuse fuzzy text-walk and *-includes matches when the hint is short and could collide with neighbours.`,
|
|
275
|
+
{
|
|
276
|
+
query: z.string().describe(
|
|
277
|
+
"Hint to match against the field's label, placeholder, aria-label, name, or id (e.g. 'Email', 'price', 'Card number')"
|
|
278
|
+
),
|
|
279
|
+
type_filter: z.string().optional().describe(
|
|
280
|
+
'Restrict to a specific input type \u2014 "email", "checkbox", "file", "textarea", "select", "number", etc. Default "any".'
|
|
281
|
+
),
|
|
282
|
+
max: z.number().int().min(1).optional().describe("Maximum fields to return (default 5). total_matches is reported even when truncated."),
|
|
283
|
+
exact: z.boolean().optional().describe(
|
|
284
|
+
"If true, return only exact equality matches (aria-eq / placeholder-eq / label-text-eq / name-eq / id-eq). Skips fuzzy text-walk and *-includes. Default false."
|
|
285
|
+
),
|
|
286
|
+
frame: z.string().optional().describe("Same-origin iframe CSS selector to search inside. Cross-origin iframes are not supported.")
|
|
287
|
+
},
|
|
288
|
+
async ({ query, type_filter, max, exact, frame }) => {
|
|
289
|
+
const response = await bridge.request({
|
|
290
|
+
type: "find_input",
|
|
291
|
+
query,
|
|
292
|
+
type_filter,
|
|
293
|
+
max,
|
|
294
|
+
exact,
|
|
295
|
+
frame
|
|
296
|
+
});
|
|
297
|
+
const r = response;
|
|
298
|
+
if (r.frame_error) {
|
|
299
|
+
return { content: [{ type: "text", text: r.frame_error }] };
|
|
300
|
+
}
|
|
301
|
+
if (r.fields.length === 0) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: `No input fields found matching "${query}".` }]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const lines = r.fields.map((f, i) => {
|
|
307
|
+
const placeholderPart = f.placeholder ? ` placeholder="${f.placeholder}"` : "";
|
|
308
|
+
const valuePart = f.value ? ` value="${f.value}"` : "";
|
|
309
|
+
const underPart = f.under ? ` [under: "${f.under}"]` : "";
|
|
310
|
+
const posPart = f.position ? ` at y=${f.position.y}` : "";
|
|
311
|
+
return ` ${i + 1}. "${f.label}" type=${f.type}${placeholderPart}${valuePart}${underPart} \u2014 match: ${f.match_kind}${posPart}`;
|
|
312
|
+
});
|
|
313
|
+
const header = r.truncated ? `Found ${r.fields.length} of ${r.total_matches} input(s) for "${query}":` : `Found ${r.fields.length} input${r.fields.length === 1 ? "" : "s"} for "${query}":`;
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: "text", text: `${header}
|
|
316
|
+
${lines.join("\n")}
|
|
317
|
+
|
|
318
|
+
To fill: fill_input("${r.fields[0].label}", "<value>")` }]
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
server.tool(
|
|
323
|
+
"wait_for_text",
|
|
324
|
+
`Wait for text to appear in the DOM. Complement to wait_for_selector for the case where you only know the message text \u2014 no selector required. Uses a MutationObserver under the hood, no polling.
|
|
325
|
+
|
|
326
|
+
Resolves on the first match (or if the text is already present). Returns the elapsed time, the matched text, and the surrounding context.
|
|
327
|
+
|
|
328
|
+
Use when:
|
|
329
|
+
- "Click Save, then wait for 'Saved successfully' to show"
|
|
330
|
+
- "Wait until the deploy log says 'Build complete'"
|
|
331
|
+
- Any case where the post-action signal is a phrase, not a known selector
|
|
332
|
+
|
|
333
|
+
Do NOT use for: waiting on a known CSS selector (use wait_for_selector \u2014 slightly cheaper).
|
|
334
|
+
|
|
335
|
+
Pierces open shadow roots. Pass frame="iframe.selector" to wait for text inside a same-origin iframe.`,
|
|
336
|
+
{
|
|
337
|
+
query: z.string().describe("Text to wait for (substring by default; pass regex=true for a regex)"),
|
|
338
|
+
timeout_ms: z.number().int().min(100).optional().describe("Maximum milliseconds to wait (default 10000)"),
|
|
339
|
+
scope_selector: z.string().optional().describe("Limit the observation to a CSS selector's subtree (e.g. '.toast-region')"),
|
|
340
|
+
regex: z.boolean().optional().describe("Treat query as a regex (case-insensitive). Default false."),
|
|
341
|
+
frame: z.string().optional().describe("Same-origin iframe CSS selector to wait inside. Cross-origin iframes are not supported.")
|
|
342
|
+
},
|
|
343
|
+
async ({ query, timeout_ms, scope_selector, regex, frame }) => {
|
|
344
|
+
const wsTimeout = Math.max(15e3, (timeout_ms ?? 1e4) + 5e3);
|
|
345
|
+
const response = await bridge.request(
|
|
346
|
+
{
|
|
347
|
+
type: "wait_for_text",
|
|
348
|
+
query,
|
|
349
|
+
timeout_ms,
|
|
350
|
+
scope_selector,
|
|
351
|
+
regex,
|
|
352
|
+
frame
|
|
353
|
+
},
|
|
354
|
+
wsTimeout
|
|
355
|
+
);
|
|
356
|
+
const r = response;
|
|
357
|
+
if (r.frame_error) {
|
|
358
|
+
return { content: [{ type: "text", text: r.frame_error }] };
|
|
359
|
+
}
|
|
360
|
+
if (!r.found) {
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{ type: "text", text: `Timed out after ${r.elapsed_ms}ms waiting for "${query}".` }
|
|
364
|
+
]
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const ctx = r.context ? `
|
|
368
|
+
context: ${r.context}` : "";
|
|
369
|
+
const sel = r.selector ? `
|
|
370
|
+
selector: ${r.selector}` : "";
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: `Found "${r.text}" after ${r.elapsed_ms}ms.${sel}${ctx}`
|
|
376
|
+
}
|
|
377
|
+
]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
);
|
|
190
381
|
server.tool(
|
|
191
382
|
"fill_form",
|
|
192
383
|
`Fill multiple form fields in a single call by targeting each field by its label text.
|
package/package.json
CHANGED