chromeflow 0.1.37 → 0.1.39
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 +30 -6
- package/dist/setup.js +3 -1
- package/dist/tools/browser.js +3 -2
- package/dist/tools/capture.js +34 -3
- package/dist/tools/flow.js +6 -4
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -140,6 +140,9 @@ screenshot to check what happened.
|
|
|
140
140
|
2. `get_elements()` to get exact coords → `highlight_region(x,y,w,h,msg)`
|
|
141
141
|
3. `take_screenshot()` only if you still can't identify the element from DOM queries
|
|
142
142
|
|
|
143
|
+
**Multiple elements with the same label** (e.g. many "Remove" buttons):
|
|
144
|
+
`click_element("Remove", nth=3)` — use `nth` (1-based) to target the specific one by order top-to-bottom. Check `get_form_fields` or `get_page_text` first to determine which index corresponds to the right section.
|
|
145
|
+
|
|
143
146
|
**`fill_input` not found:**
|
|
144
147
|
1. `click_element(hint)` to focus the field, then retry `fill_input`
|
|
145
148
|
2. `find_and_highlight(hint, "Click here — I'll fill it in")` (no `valueToType`) then
|
|
@@ -152,14 +155,31 @@ screenshot to check what happened.
|
|
|
152
155
|
|
|
153
156
|
**React Select / custom styled dropdowns** (e.g. "Select..." components on DataAnnotation):
|
|
154
157
|
`click_element` and `fill_input` do NOT work on these — they intercept native events. Use
|
|
155
|
-
`execute_script`
|
|
158
|
+
`execute_script` with the hidden combobox input approach (most reliable):
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
// 1. Find the hidden combobox input (each React Select has one: input[id*="react-select-N-input"])
|
|
162
|
+
var input = document.querySelector('input[id*="react-select-3-input"]');
|
|
163
|
+
input.focus();
|
|
164
|
+
|
|
165
|
+
// 2. Set value via native setter to trigger React's onChange
|
|
166
|
+
var setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set;
|
|
167
|
+
setter.call(input, 'Target Option');
|
|
168
|
+
input.dispatchEvent(new Event('input', {bubbles: true}));
|
|
169
|
+
|
|
170
|
+
// 3. Wait 300ms for the dropdown to filter, then click the first matching option
|
|
171
|
+
// (run this as a separate execute_script call after a brief pause)
|
|
172
|
+
var option = document.querySelector('[id*="react-select-3-option-0"]');
|
|
173
|
+
if (option) option.click();
|
|
174
|
+
|
|
175
|
+
// 4. Verify — the control div should show the selected value
|
|
176
|
+
document.querySelector('[class*="singleValue"]').textContent.trim();
|
|
177
|
+
```
|
|
156
178
|
|
|
179
|
+
Fallback if the combobox approach doesn't work (older React Select versions):
|
|
157
180
|
```js
|
|
158
|
-
// 1. Open the menu — click the control div (filter by pageY if multiple)
|
|
159
181
|
var controls = document.querySelectorAll('[class*="control"]');
|
|
160
182
|
controls[N].click();
|
|
161
|
-
|
|
162
|
-
// 2. Pick an option by exact text
|
|
163
183
|
var allEls = document.querySelectorAll('*');
|
|
164
184
|
for (var i = 0; i < allEls.length; i++) {
|
|
165
185
|
if (allEls[i].textContent.trim() === 'Target Option' && allEls[i].children.length === 0) {
|
|
@@ -168,10 +188,14 @@ for (var i = 0; i < allEls.length; i++) {
|
|
|
168
188
|
break;
|
|
169
189
|
}
|
|
170
190
|
}
|
|
191
|
+
```
|
|
171
192
|
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
**Page text with large embedded content** (e.g. uploaded log files previewed inline): full-page `get_page_text()` pagination becomes unwieldy. Scope to a specific section instead:
|
|
194
|
+
```
|
|
195
|
+
get_page_text(selector=".section-3") — scope to a CSS selector
|
|
196
|
+
get_page_text(selector="#upload-form") — scope to an id
|
|
174
197
|
```
|
|
198
|
+
Use `execute_script("document.querySelectorAll('section').length")` to find structural selectors first.
|
|
175
199
|
|
|
176
200
|
**Page content rendered as images** (e.g. qualification "Examples" tabs that show PNG screenshots
|
|
177
201
|
instead of DOM text): `get_page_text()` returns nothing useful. Zoom out and screenshot instead:
|
package/dist/setup.js
CHANGED
|
@@ -169,7 +169,9 @@ const CHROMEFLOW_TOOLS = [
|
|
|
169
169
|
// v0.1.32+
|
|
170
170
|
"fill_form",
|
|
171
171
|
// v0.1.36+
|
|
172
|
-
"set_file_input"
|
|
172
|
+
"set_file_input",
|
|
173
|
+
// v0.1.39+
|
|
174
|
+
"get_console_logs"
|
|
173
175
|
].map((t) => `mcp__chromeflow__${t}`);
|
|
174
176
|
function patchSettingsLocalJson(cwd) {
|
|
175
177
|
const claudeDir = join(cwd, ".claude");
|
package/dist/tools/browser.js
CHANGED
|
@@ -122,7 +122,8 @@ save_to controls where the PNG is saved: "downloads" (default) saves to ~/Downlo
|
|
|
122
122
|
`Get the exact pixel positions of all visible interactive elements on the page (inputs, buttons, links, selects).
|
|
123
123
|
Use this INSTEAD OF take_screenshot when you need coordinates for highlight_region \u2014 the coordinates are exact DOM values, not estimates.
|
|
124
124
|
Returns a numbered list with element type, label, and precise x/y/width/height in CSS pixels.
|
|
125
|
-
|
|
125
|
+
IMPORTANT: x/y are VIEWPORT-relative (0,0 = top-left of the visible area). Use these exact values directly in highlight_region \u2014 do not add window.scrollY.
|
|
126
|
+
Use get_form_fields instead if you need document y positions or fields below the fold.`,
|
|
126
127
|
{},
|
|
127
128
|
async () => {
|
|
128
129
|
const response = await bridge.request({ type: "get_elements" });
|
|
@@ -175,7 +176,7 @@ ${lines.join("\n")}${r.warning ?? ""}` }]
|
|
|
175
176
|
Uses Chrome DevTools Protocol to set the file \u2014 the only way to bypass the browser's file-input script restriction.
|
|
176
177
|
hint: label text or name of the file input (or empty string to target the first file input on the page).
|
|
177
178
|
file_path: absolute path to the file on the local filesystem (e.g. /Users/you/Downloads/task.zip).
|
|
178
|
-
After calling this,
|
|
179
|
+
After calling this, verify the upload was accepted: use execute_script to check that the input's files.length > 0, or use get_page_text to look for a success indicator (e.g. a Remove button appearing). If not accepted, call set_file_input again \u2014 occasional React timing issues may require a retry.`,
|
|
179
180
|
{
|
|
180
181
|
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."),
|
|
181
182
|
file_path: z.string().describe("Absolute path to the file to upload (e.g. /Users/you/Downloads/task.zip)")
|
package/dist/tools/capture.js
CHANGED
|
@@ -12,10 +12,11 @@ DO NOT use for: email address, password, payment/billing info, phone number \u20
|
|
|
12
12
|
After filling, call wait_for_click only if the user needs to review/confirm; otherwise proceed directly to the next step.`,
|
|
13
13
|
{
|
|
14
14
|
textHint: z.string().describe("The label, placeholder, or nearby text identifying the input (e.g. 'Product name', 'Amount', 'Description')"),
|
|
15
|
-
value: z.string().describe("The value to fill in")
|
|
15
|
+
value: z.string().describe("The value to fill in"),
|
|
16
|
+
nth: z.number().int().min(1).optional().describe("Which match to fill when multiple inputs share the same label (1 = first/topmost, default 1)")
|
|
16
17
|
},
|
|
17
|
-
async ({ textHint, value }) => {
|
|
18
|
-
const response = await bridge.request({ type: "fill_input", textHint, value });
|
|
18
|
+
async ({ textHint, value, nth }) => {
|
|
19
|
+
const response = await bridge.request({ type: "fill_input", textHint, value, nth });
|
|
19
20
|
if (response.type !== "fill_response") throw new Error("Unexpected response");
|
|
20
21
|
const r = response;
|
|
21
22
|
return {
|
|
@@ -117,6 +118,36 @@ The snapshot is read from the local temp file written by save_page_state.`,
|
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
);
|
|
121
|
+
server.tool(
|
|
122
|
+
"get_console_logs",
|
|
123
|
+
`Read the browser console output (log, warn, error, info) captured since the page loaded.
|
|
124
|
+
Returns the last 200 messages with their level and timestamp.
|
|
125
|
+
Use this to check for JavaScript errors, debug React issues, or verify that an action produced the expected console output.
|
|
126
|
+
Pass level="error" to see only errors, or omit to see all levels.`,
|
|
127
|
+
{
|
|
128
|
+
level: z.enum(["log", "warn", "error", "info"]).optional().describe('Filter by log level (e.g. "error" to see only errors). Omit for all levels.')
|
|
129
|
+
},
|
|
130
|
+
async ({ level }) => {
|
|
131
|
+
const response = await bridge.request({ type: "execute_script", code: `JSON.stringify(window._consoleLogs || [])` });
|
|
132
|
+
if (response.type !== "script_response") throw new Error("Unexpected response");
|
|
133
|
+
let logs;
|
|
134
|
+
try {
|
|
135
|
+
logs = JSON.parse(response.result);
|
|
136
|
+
} catch {
|
|
137
|
+
return { content: [{ type: "text", text: "No console logs captured (console capture may not be injected on this page yet \u2014 navigate first)." }] };
|
|
138
|
+
}
|
|
139
|
+
if (level) logs = logs.filter((l) => l.level === level);
|
|
140
|
+
if (logs.length === 0) {
|
|
141
|
+
return { content: [{ type: "text", text: level ? `No ${level}-level console messages.` : "No console messages captured." }] };
|
|
142
|
+
}
|
|
143
|
+
const lines = logs.map((l) => {
|
|
144
|
+
const time = new Date(l.time).toISOString().slice(11, 23);
|
|
145
|
+
return `[${time}] ${l.level.toUpperCase()}: ${l.message.slice(0, 500)}`;
|
|
146
|
+
});
|
|
147
|
+
return { content: [{ type: "text", text: `Console logs (${logs.length} entries):
|
|
148
|
+
${lines.join("\n")}` }] };
|
|
149
|
+
}
|
|
150
|
+
);
|
|
120
151
|
server.tool(
|
|
121
152
|
"write_to_env",
|
|
122
153
|
"Write a key=value pair to a .env file. Use this after capturing an API key or ID from the page.",
|
package/dist/tools/flow.js
CHANGED
|
@@ -17,14 +17,16 @@ function registerFlowTools(server, bridge) {
|
|
|
17
17
|
`Click a button, link, or interactive element on the page by its visible text or aria-label.
|
|
18
18
|
Use this whenever Claude can press a button without needing user input \u2014 e.g. "Save", "Continue", "Create product", "Add pricing", "Confirm", "Next".
|
|
19
19
|
After clicking, use get_page_text to check the result \u2014 only use take_screenshot if you need pixel positions.
|
|
20
|
-
Do NOT use for: elements that require the user to make a personal choice, consent to terms, or enter sensitive data
|
|
20
|
+
Do NOT use for: elements that require the user to make a personal choice, consent to terms, or enter sensitive data.
|
|
21
|
+
When multiple elements share the same label (e.g. many "Remove" buttons), use nth to target a specific one (1 = first/topmost, 2 = second, etc.).`,
|
|
21
22
|
{
|
|
22
23
|
textHint: z.string().describe(
|
|
23
24
|
"The visible label of the button or link (e.g. 'Save product', 'Continue', 'Add a product', 'Create')"
|
|
24
|
-
)
|
|
25
|
+
),
|
|
26
|
+
nth: z.number().int().min(1).optional().describe("Which match to click when multiple elements share the same label (1 = first/topmost, default 1)")
|
|
25
27
|
},
|
|
26
|
-
async ({ textHint }) => {
|
|
27
|
-
const response = await bridge.request({ type: "click_element", textHint });
|
|
28
|
+
async ({ textHint, nth }) => {
|
|
29
|
+
const response = await bridge.request({ type: "click_element", textHint, nth });
|
|
28
30
|
const r = response;
|
|
29
31
|
if (!r.success) {
|
|
30
32
|
return {
|
package/package.json
CHANGED