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 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` directly:
158
+ `execute_script` with the hidden combobox input approach (most reliable):
159
159
 
160
160
  ```js
161
- // 1. Open the menu click the control div (filter by pageY if multiple)
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");
@@ -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.",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromeflow",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "Browser guidance MCP server for Claude Code — highlights, clicks, fills, and captures from the web so you don't have to.",
5
5
  "type": "module",
6
6
  "bin": {