chromeflow 0.1.38 → 0.1.40
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 +21 -7
- package/dist/setup.js +5 -1
- package/dist/tools/browser.js +64 -1
- package/dist/tools/capture.js +34 -3
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -155,14 +155,31 @@ screenshot to check what happened.
|
|
|
155
155
|
|
|
156
156
|
**React Select / custom styled dropdowns** (e.g. "Select..." components on DataAnnotation):
|
|
157
157
|
`click_element` and `fill_input` do NOT work on these — they intercept native events. Use
|
|
158
|
-
`execute_script`
|
|
158
|
+
`execute_script` with the hidden combobox input approach (most reliable):
|
|
159
159
|
|
|
160
160
|
```js
|
|
161
|
-
// 1.
|
|
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
|
+
```
|
|
178
|
+
|
|
179
|
+
Fallback if the combobox approach doesn't work (older React Select versions):
|
|
180
|
+
```js
|
|
162
181
|
var controls = document.querySelectorAll('[class*="control"]');
|
|
163
182
|
controls[N].click();
|
|
164
|
-
|
|
165
|
-
// 2. Pick an option by exact text
|
|
166
183
|
var allEls = document.querySelectorAll('*');
|
|
167
184
|
for (var i = 0; i < allEls.length; i++) {
|
|
168
185
|
if (allEls[i].textContent.trim() === 'Target Option' && allEls[i].children.length === 0) {
|
|
@@ -171,9 +188,6 @@ for (var i = 0; i < allEls.length; i++) {
|
|
|
171
188
|
break;
|
|
172
189
|
}
|
|
173
190
|
}
|
|
174
|
-
|
|
175
|
-
// 3. Verify
|
|
176
|
-
controls[N].textContent.trim(); // should show selected value
|
|
177
191
|
```
|
|
178
192
|
|
|
179
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:
|
package/dist/setup.js
CHANGED
|
@@ -169,7 +169,11 @@ 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",
|
|
175
|
+
// v0.1.40+
|
|
176
|
+
"capture_terminal"
|
|
173
177
|
].map((t) => `mcp__chromeflow__${t}`);
|
|
174
178
|
function patchSettingsLocalJson(cwd) {
|
|
175
179
|
const claudeDir = join(cwd, ".claude");
|
package/dist/tools/browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { writeFileSync, copyFileSync } from "fs";
|
|
2
|
+
import { writeFileSync, copyFileSync, readFileSync } from "fs";
|
|
3
3
|
import { tmpdir, homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { execSync } from "child_process";
|
|
@@ -106,6 +106,69 @@ save_to controls where the PNG is saved: "downloads" (default) saves to ~/Downlo
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
);
|
|
109
|
+
server.tool(
|
|
110
|
+
"capture_terminal",
|
|
111
|
+
`Capture a screenshot of the terminal window (Terminal, iTerm2, Warp, VS Code, Ghostty, etc.) and save it as a PNG.
|
|
112
|
+
Use this when you need a screenshot of terminal output \u2014 e.g. test results, build logs, or command output \u2014 to upload to a form via set_file_input.
|
|
113
|
+
Auto-detects the terminal app. Returns the image to Claude AND saves the PNG file.
|
|
114
|
+
The saved file path can be passed directly to set_file_input(hint, file_path) to upload it.`,
|
|
115
|
+
{
|
|
116
|
+
save_to: z.enum(["downloads", "cwd"]).optional().describe('Where to save the PNG: "downloads" (~/Downloads, default) or "cwd" (working directory)')
|
|
117
|
+
},
|
|
118
|
+
async ({ save_to = "downloads" }) => {
|
|
119
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
120
|
+
const filename = `terminal-${timestamp}.png`;
|
|
121
|
+
const savePath = save_to === "cwd" ? join(process.cwd(), filename) : join(homedir(), "Downloads", filename);
|
|
122
|
+
let captured = false;
|
|
123
|
+
try {
|
|
124
|
+
const bounds = execSync(`osascript -e '
|
|
125
|
+
tell application "System Events"
|
|
126
|
+
set termApps to {"Terminal", "iTerm2", "Warp", "kitty", "Alacritty", "Ghostty", "Code", "Cursor", "Windsurf"}
|
|
127
|
+
repeat with appName in termApps
|
|
128
|
+
if exists process (contents of appName) then
|
|
129
|
+
tell process (contents of appName)
|
|
130
|
+
if (count of windows) > 0 then
|
|
131
|
+
set win to window 1
|
|
132
|
+
set pos to position of win
|
|
133
|
+
set sz to size of win
|
|
134
|
+
return (item 1 of pos as text) & "," & (item 2 of pos as text) & "," & (item 1 of sz as text) & "," & (item 2 of sz as text)
|
|
135
|
+
end if
|
|
136
|
+
end tell
|
|
137
|
+
end if
|
|
138
|
+
end repeat
|
|
139
|
+
error "No terminal window found"
|
|
140
|
+
end tell
|
|
141
|
+
'`, { timeout: 5e3 }).toString().trim();
|
|
142
|
+
execSync(`screencapture -x -R${bounds} "${savePath}"`, { timeout: 5e3 });
|
|
143
|
+
captured = true;
|
|
144
|
+
} catch {
|
|
145
|
+
try {
|
|
146
|
+
execSync(`screencapture -x "${savePath}"`, { timeout: 5e3 });
|
|
147
|
+
captured = true;
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!captured) {
|
|
152
|
+
return {
|
|
153
|
+
content: [{ type: "text", text: "Failed to capture terminal. Ensure Screen Recording permission is granted to your terminal app in System Settings > Privacy & Security > Screen Recording." }]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const imageBuffer = readFileSync(savePath);
|
|
157
|
+
const base64 = imageBuffer.toString("base64");
|
|
158
|
+
let clipboardNote = "";
|
|
159
|
+
try {
|
|
160
|
+
execSync(`osascript -e 'set the clipboard to (read (POSIX file "${savePath}") as \xABclass PNGf\xBB)'`);
|
|
161
|
+
clipboardNote = "Copied to clipboard. ";
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
content: [
|
|
166
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
167
|
+
{ type: "text", text: `${clipboardNote}Saved to ${savePath}` }
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
);
|
|
109
172
|
server.tool(
|
|
110
173
|
"clear_overlays",
|
|
111
174
|
"Remove all highlights and callout annotations from the current page. Does NOT remove the guide panel \u2014 the guide panel persists until the next flow starts.",
|
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/package.json
CHANGED