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 +14 -2
- package/README.md +5 -6
- package/dist/setup.js +9 -11
- package/dist/tools/browser.js +62 -5
- package/dist/tools/capture.js +41 -0
- package/dist/tools/flow.js +13 -0
- package/package.json +1 -1
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.
|
|
28
|
+
**2. Install the Chrome extension** (one time):
|
|
29
29
|
|
|
30
|
-
The setup wizard opens
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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
|
-
|
|
186
|
+
const STORE_URL = "https://chromewebstore.google.com/detail/chromeflow/lkdchdgkbkodliefobkkhiegjdiidime";
|
|
187
|
+
function tryOpenStorePage() {
|
|
187
188
|
try {
|
|
188
|
-
execSync(
|
|
189
|
+
execSync(`open "${STORE_URL}"`, { stdio: "ignore" });
|
|
189
190
|
return true;
|
|
190
191
|
} catch {
|
|
191
192
|
try {
|
|
192
|
-
execSync(
|
|
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
|
|
245
|
-
const opened =
|
|
245
|
+
console.log("\nChrome extension (one-time step):");
|
|
246
|
+
const opened = tryOpenStorePage();
|
|
246
247
|
if (opened) {
|
|
247
|
-
console.log(" Opened
|
|
248
|
+
console.log(" Opened Chrome Web Store \u2014 click 'Add to Chrome' to install.");
|
|
248
249
|
} else {
|
|
249
|
-
console.log(
|
|
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");
|
package/dist/tools/browser.js
CHANGED
|
@@ -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
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
package/dist/tools/capture.js
CHANGED
|
@@ -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.",
|
package/dist/tools/flow.js
CHANGED
|
@@ -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