chromeflow 0.1.21 → 0.1.23

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
@@ -44,15 +44,16 @@ Do NOT ask "should I open the browser?" — just do it. The user expects seamles
44
44
 
45
45
  ```
46
46
  1. show_guide_panel(title, steps[]) — show the full plan upfront
47
- 2. open_page(url) — navigate to the right page
47
+ 2. open_page(url) — navigate to the right page (add new_tab=true to keep current tab open)
48
48
  mark_step_done(0) — ALWAYS mark step 0 done right after open_page succeeds
49
49
  3. For each step:
50
50
  a. Claude acts directly:
51
51
  click_element("Save") — press buttons/links Claude can press
52
52
  wait_for_selector(".success") or get_page_text() — ALWAYS confirm after click; click_element returns after 600ms regardless of outcome
53
- fill_input("Product name", "Pro") — fill fields Claude knows the answer to
53
+ fill_input("Product name", "Pro") — fill fields Claude knows the answer to (works on React, CodeMirror, and contenteditable)
54
54
  clear_overlays() — call this immediately after fill_input succeeds
55
55
  scroll_page("down") — reveal off-screen content then retry
56
+ scroll_to_element("label text") — jump directly to a known field instead of guessing pixel scroll amount
56
57
  b. Check results with text, not vision:
57
58
  get_page_text() — read errors/status after actions
58
59
  wait_for_selector(".success") — wait for async changes (builds, modals)
@@ -100,6 +101,17 @@ After a secret key or API key is revealed:
100
101
 
101
102
  Use the absolute path for `envPath` — it's the Claude Code working directory + `/.env`.
102
103
 
104
+ ## Working with complex forms
105
+ - Before filling a large or unfamiliar form, call `get_form_fields()` to get a full inventory of every field (type, label, current value, vertical position). This prevents missing fields and avoids positional guesswork.
106
+ - `fill_input` works on React-controlled inputs, contenteditable (Stripe, Notion), and **CodeMirror 6 editors** — it auto-detects all three. No `execute_script` workaround needed.
107
+ - `scroll_to_element("label text or #selector")` scrolls a specific field into view without guessing pixel offsets.
108
+ - For multi-session tasks (long forms that may exceed context), call `save_page_state()` as a checkpoint. A future session can call `restore_page_state()` to reload all field values from the saved snapshot.
109
+
110
+ ## Working with multiple tabs
111
+ - `open_page(url, new_tab=true)` opens a URL without losing the current tab.
112
+ - `list_tabs()` shows all open tabs with their index, title, and URL.
113
+ - `switch_to_tab("1")` switches by tab number; `switch_to_tab("form")` matches by URL or title substring.
114
+
103
115
  ## Error handling
104
116
  - After any action → `get_page_text()` to check for errors (not `take_screenshot`)
105
117
  - After `click_element("Save")` / form submission → use `get_page_text()` or `wait_for_selector` to confirm. Never use `wait_for_navigation` — most form saves don't navigate.
package/README.md CHANGED
@@ -25,12 +25,11 @@ This:
25
25
  - Adds a hint to `~/.claude/CLAUDE.md` so Claude will suggest `npx chromeflow setup` in any project that isn't yet configured
26
26
  - Pre-approves Chromeflow tools in `.claude/settings.local.json` (no per-action prompts)
27
27
 
28
- **2. Load the Chrome extension** (one time):
28
+ **2. Install the Chrome extension** (one time):
29
29
 
30
- The setup wizard opens `chrome://extensions` for you. Then:
31
- 1. Enable **Developer mode** (top-right toggle)
32
- 2. Click **Load unpacked**
33
- 3. Select the path printed by the setup wizard
30
+ The setup wizard opens the Chrome Web Store for you — click **Add to Chrome**.
31
+
32
+ Or install directly: [chromewebstore.google.com/detail/chromeflow/lkdchdgkbkodliefobkkhiegjdiidime](https://chromewebstore.google.com/detail/chromeflow/lkdchdgkbkodliefobkkhiegjdiidime)
34
33
 
35
34
  The extension persists across Chrome restarts. You only do this once.
36
35
 
@@ -88,7 +87,7 @@ npm run dev:mcp # watches mcp-server
88
87
  npm run dev:ext # watches extension
89
88
  ```
90
89
 
91
- After rebuilding the extension, click **Update** on `chrome://extensions`.
90
+ After rebuilding the extension, reload it from `chrome://extensions`.
92
91
 
93
92
  ## Requirements
94
93
 
package/dist/setup.js CHANGED
@@ -183,13 +183,14 @@ function patchSettingsLocalJson(cwd) {
183
183
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
184
184
  return "updated";
185
185
  }
186
- function tryOpenExtensionsPage() {
186
+ const STORE_URL = "https://chromewebstore.google.com/detail/chromeflow/lkdchdgkbkodliefobkkhiegjdiidime";
187
+ function tryOpenStorePage() {
187
188
  try {
188
- execSync('open -a "Google Chrome" "chrome://extensions"', { stdio: "ignore" });
189
+ execSync(`open "${STORE_URL}"`, { stdio: "ignore" });
189
190
  return true;
190
191
  } catch {
191
192
  try {
192
- execSync('xdg-open "chrome://extensions"', { stdio: "ignore" });
193
+ execSync(`xdg-open "${STORE_URL}"`, { stdio: "ignore" });
193
194
  return true;
194
195
  } catch {
195
196
  return false;
@@ -241,17 +242,14 @@ async function runSetup() {
241
242
  } else {
242
243
  console.log("\u2713 Added chromeflow tools to .claude/settings.local.json (no approval prompts)");
243
244
  }
244
- console.log("\nChrome extension (one-time manual step):");
245
- const opened = tryOpenExtensionsPage();
245
+ console.log("\nChrome extension (one-time step):");
246
+ const opened = tryOpenStorePage();
246
247
  if (opened) {
247
- console.log(" Opened chrome://extensions in Chrome.");
248
+ console.log(" Opened Chrome Web Store \u2014 click 'Add to Chrome' to install.");
248
249
  } else {
249
- console.log(" Open chrome://extensions in Chrome.");
250
+ console.log(` Install from the Chrome Web Store:
251
+ ${STORE_URL}`);
250
252
  }
251
- const extensionDistPath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "dist");
252
- console.log(" 1. Enable Developer mode (top-right toggle)");
253
- console.log(" 2. Click 'Load unpacked'");
254
- console.log(` 3. Select: ${extensionDistPath}`);
255
253
  const globalResult = patchGlobalClaudeMd();
256
254
  if (globalResult === "already-present") {
257
255
  console.log("\u2713 ~/.claude/CLAUDE.md already has chromeflow hint");
@@ -2,12 +2,45 @@ import { z } from "zod";
2
2
  function registerBrowserTools(server, bridge) {
3
3
  server.tool(
4
4
  "open_page",
5
- "Navigate the user's active Chrome tab to a URL",
6
- { url: z.string().url().describe("The URL to navigate to") },
7
- async ({ url }) => {
8
- await bridge.request({ type: "navigate", url });
5
+ "Navigate to a URL. By default reuses the active tab. Set new_tab=true to open alongside the current tab without losing it.",
6
+ {
7
+ url: z.string().url().describe("The URL to navigate to"),
8
+ new_tab: z.boolean().optional().describe("Open in a new tab instead of replacing the current one (default false)")
9
+ },
10
+ async ({ url, new_tab }) => {
11
+ await bridge.request({ type: "navigate", url, newTab: new_tab ?? false });
9
12
  return {
10
- content: [{ type: "text", text: `Navigated to ${url}` }]
13
+ content: [{ type: "text", text: `Navigated to ${url}${new_tab ? " (new tab)" : ""}` }]
14
+ };
15
+ }
16
+ );
17
+ server.tool(
18
+ "switch_to_tab",
19
+ `Switch the active tab to a different open tab. Use this after open_page(new_tab=true) to switch back to the original tab, or to jump between tabs.
20
+ Accepts: a tab number (1-based), a URL substring, or a title substring.
21
+ Example: switch_to_tab("1") to go to the first tab, switch_to_tab("form") to find a tab whose URL or title contains "form".`,
22
+ {
23
+ query: z.string().describe("Tab number (1-based), URL substring, or title substring to match")
24
+ },
25
+ async ({ query }) => {
26
+ await bridge.request({ type: "switch_to_tab", query });
27
+ return {
28
+ content: [{ type: "text", text: `Switched to tab matching "${query}"` }]
29
+ };
30
+ }
31
+ );
32
+ server.tool(
33
+ "list_tabs",
34
+ "List all open tabs in the current window with their index, title, and URL. Use this before switch_to_tab if you're not sure which tab to switch to.",
35
+ {},
36
+ async () => {
37
+ const response = await bridge.request({ type: "list_tabs" });
38
+ if (response.type !== "tabs_response") throw new Error("Unexpected response");
39
+ const tabs = response.tabs;
40
+ const lines = tabs.map((t) => `${t.index}. ${t.active ? "[active] " : ""}${t.title} \u2014 ${t.url}`);
41
+ return {
42
+ content: [{ type: "text", text: `Open tabs:
43
+ ${lines.join("\n")}` }]
11
44
  };
12
45
  }
13
46
  );
@@ -72,6 +105,30 @@ Use these exact x/y values in highlight_region.` }]
72
105
  };
73
106
  }
74
107
  );
108
+ server.tool(
109
+ "get_form_fields",
110
+ `Get a full inventory of all form fields on the page: inputs, textareas, selects, and CodeMirror editors.
111
+ Run this once at the start of a complex form to understand what fields exist, their labels, current values, and vertical positions.
112
+ Returns fields sorted by their y-position on the page (top to bottom).
113
+ Unlike get_elements, this includes ALL fields (even far below the fold) and is not limited to 60 items.`,
114
+ {},
115
+ async () => {
116
+ const response = await bridge.request({ type: "get_form_fields" });
117
+ if (response.type !== "form_fields_response") throw new Error("Unexpected response");
118
+ const fields = response.fields;
119
+ if (fields.length === 0) {
120
+ return { content: [{ type: "text", text: "No form fields found on page." }] };
121
+ }
122
+ const lines = fields.map((f) => {
123
+ const val = f.value ? ` [currently: "${f.value}"]` : "";
124
+ return `${f.index}. [${f.type}] "${f.label}"${val} \u2014 y:${f.y}`;
125
+ });
126
+ return {
127
+ content: [{ type: "text", text: `Form fields (${fields.length} total, sorted top-to-bottom):
128
+ ${lines.join("\n")}` }]
129
+ };
130
+ }
131
+ );
75
132
  server.tool(
76
133
  "execute_script",
77
134
  `Execute JavaScript in the current page's context and return the result as a string.
@@ -1,5 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { appendFileSync, readFileSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ const PAGE_STATE_FILE = join(tmpdir(), "chromeflow_page_state.json");
3
6
  function registerCaptureTools(server, bridge) {
4
7
  server.tool(
5
8
  "fill_input",
@@ -72,6 +75,44 @@ Only use take_screenshot when you need to locate an element's pixel position for
72
75
  };
73
76
  }
74
77
  );
78
+ server.tool(
79
+ "save_page_state",
80
+ `Snapshot the current values of all form fields (inputs, textareas, checkboxes, selects, CodeMirror editors) to a local file.
81
+ Use this before a context window runs out or any time you want a checkpoint mid-form.
82
+ A future session can call restore_page_state to pick up exactly where you left off.`,
83
+ {},
84
+ async () => {
85
+ const response = await bridge.request({ type: "save_page_state" });
86
+ if (response.type !== "save_state_response") throw new Error("Unexpected response");
87
+ const state = response.state;
88
+ writeFileSync(PAGE_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
89
+ return {
90
+ content: [{ type: "text", text: `Saved ${state.length} field values to ${PAGE_STATE_FILE}. Call restore_page_state in a future session to reload them.` }]
91
+ };
92
+ }
93
+ );
94
+ server.tool(
95
+ "restore_page_state",
96
+ `Restore form field values from a previously saved snapshot (created by save_page_state).
97
+ Use this at the start of a new session when resuming a long form-filling task.
98
+ The snapshot is read from the local temp file written by save_page_state.`,
99
+ {},
100
+ async () => {
101
+ let state;
102
+ try {
103
+ state = JSON.parse(readFileSync(PAGE_STATE_FILE, "utf-8"));
104
+ } catch {
105
+ return {
106
+ content: [{ type: "text", text: `No saved page state found at ${PAGE_STATE_FILE}. Call save_page_state first.` }]
107
+ };
108
+ }
109
+ const response = await bridge.request({ type: "restore_page_state", state });
110
+ const msg = response.message ?? "Done";
111
+ return {
112
+ content: [{ type: "text", text: msg }]
113
+ };
114
+ }
115
+ );
75
116
  server.tool(
76
117
  "write_to_env",
77
118
  "Write a key=value pair to a .env file. Use this after capturing an API key or ID from the page.",
@@ -95,6 +95,19 @@ to 15 seconds so the page is checked gently rather than hammered every 500ms.`,
95
95
  };
96
96
  }
97
97
  );
98
+ server.tool(
99
+ "scroll_to_element",
100
+ `Scroll an element into view by CSS selector or label/text match.
101
+ Use this instead of guessing scroll amounts when you know which field or section you need to reach.
102
+ Examples: scroll_to_element("#submit-btn"), scroll_to_element("Billing address"), scroll_to_element(".cm-editor")`,
103
+ {
104
+ query: z.string().describe("CSS selector (e.g. '#my-input', '.section-header') or visible text / label to search for")
105
+ },
106
+ async ({ query }) => {
107
+ await bridge.request({ type: "scroll_to_element", query });
108
+ return { content: [{ type: "text", text: `Scrolled to element matching "${query}".` }] };
109
+ }
110
+ );
98
111
  server.tool(
99
112
  "mark_step_done",
100
113
  "Mark a step in the guide panel as completed (shows a green check). Call this after wait_for_click resolves.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromeflow",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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": {