chromeflow 0.8.0 → 0.9.9

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/dist/setup.js DELETED
@@ -1,493 +0,0 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
2
- import { homedir } from "os";
3
- import { join, resolve, dirname } from "path";
4
- import { fileURLToPath } from "url";
5
- import { execSync } from "child_process";
6
- const HOME = homedir();
7
- const CLAUDE_JSON_PATH = join(HOME, ".claude.json");
8
- function getClaudeMdContent() {
9
- const packageDir = dirname(dirname(fileURLToPath(import.meta.url)));
10
- const claudeMdPath = join(packageDir, "CLAUDE.md");
11
- if (existsSync(claudeMdPath)) {
12
- return readFileSync(claudeMdPath, "utf8");
13
- }
14
- const devPath = join(dirname(fileURLToPath(import.meta.url)), "..", "CLAUDE.md");
15
- if (existsSync(devPath)) {
16
- return readFileSync(devPath, "utf8");
17
- }
18
- throw new Error("CLAUDE.md not found in package. Run `npm run build` first.");
19
- }
20
- const PROJECT_CLAUDE_MD = `# Chromeflow \u2014 Claude Instructions
21
-
22
- ## What chromeflow is
23
- Chromeflow is a browser guidance tool. When a task requires the user to interact with a
24
- website (create accounts, set up billing, retrieve API keys, configure third-party services),
25
- use chromeflow to guide them through it visually instead of giving text instructions.
26
-
27
- ## When to use chromeflow (be proactive)
28
- Use chromeflow automatically whenever a task requires:
29
- - Creating or configuring a third-party account (Stripe, SendGrid, Supabase, Vercel, etc.)
30
- - Retrieving API keys, secrets, or credentials to place in \`.env\`
31
- - Setting up pricing tiers, webhooks, or service configuration in a web UI
32
- - Any browser-based step that is blocking code work
33
-
34
- Do NOT ask "should I open the browser?" \u2014 just do it. The user expects seamless handoff.
35
-
36
- ## HARD RULES \u2014 never break these
37
-
38
- 1. **Never use Bash as a fallback for browser tasks.** If \`click_element\` fails, use
39
- \`scroll_page\` then retry, or use \`highlight_region\` to show the user. Never use
40
- \`osascript\`, \`applescript\`, or any shell command to control the browser.
41
-
42
- 2. **Take a screenshot only when you need to decide what to do next.** Do not take
43
- a screenshot after every action as a reflex. Take one after navigation, or when
44
- \`click_element\`/\`find_and_highlight\` fails and you need to locate something visually.
45
-
46
- 3. **\`open_page\` already waits for navigation.** Never call \`wait_for_navigation\`
47
- immediately after \`open_page\` \u2014 it will time out.
48
-
49
- 4. **When \`click_element\` fails:** first try \`scroll_page(down)\` then retry
50
- \`click_element\`. If it still fails, \`take_screenshot\` and use \`highlight_region\`
51
- with pixel coordinates from the image.
52
-
53
- ## Guided flow pattern
54
-
55
- \`\`\`
56
- 1. open_page(url) \u2014 navigate to the right page
57
- 2. For each step:
58
- a. [if needed] take_screenshot() \u2014 only when you need to locate something
59
- b. Claude acts directly:
60
- click_element("Save") \u2014 press buttons/links Claude can press
61
- fill_input("Product name", "Pro") \u2014 fill fields Claude knows the answer to
62
- scroll_page("down") \u2014 reveal off-screen content then retry
63
- Or pause for the user:
64
- find_and_highlight(text, msg) \u2014 show the user what to do
65
- wait_for_click() \u2014 wait for user interaction
66
- 3. clear_overlays() \u2014 clean up when done
67
- \`\`\`
68
-
69
- **Default to automation.** Only pause for human input when the step genuinely requires
70
- personal data or a human decision.
71
-
72
- ## What to do automatically vs pause for the user
73
-
74
- **Claude acts directly** (\`click_element\` / \`fill_input\`):
75
- - Any button: Save, Continue, Create, Add, Confirm, Next, Submit, Update
76
- - Product names, descriptions, feature lists
77
- - Prices and amounts specified in the task
78
- - URLs, redirect URIs, webhook endpoints
79
- - Selecting billing period, currency, or other known options
80
- - Dismissing cookie banners, cookie dialogs, "not now" prompts
81
-
82
- **Pause for the user** (\`find_and_highlight\` + \`wait_for_click\`):
83
- - Email address / username / login
84
- - Password or passphrase
85
- - Payment method / billing / card details
86
- - Phone number / 2FA / OTP codes
87
- - Any legal consent the user must personally accept
88
- - Choices that depend on user preference Claude wasn't told
89
-
90
- ## Capturing credentials
91
- After a secret key or API key is revealed:
92
- 1. \`read_element(hint)\` \u2014 capture the value
93
- 2. \`write_to_env(KEY_NAME, value, envPath)\` \u2014 write to \`.env\`
94
- 3. Tell the user what was written
95
-
96
- Use the absolute path for \`envPath\` \u2014 it's the Claude Code working directory + \`/.env\`.
97
-
98
- ## Error handling
99
- - \`click_element\` not found \u2192 \`scroll_page("down")\` then retry
100
- - Still not found \u2192 \`take_screenshot()\` then \`highlight_region(x,y,w,h,msg)\`
101
- - Page still loading \u2192 \`take_screenshot()\` to confirm, proceed when content is visible
102
- - Never use Bash to work around a stuck browser interaction
103
- `;
104
- function isRunningViaNpx() {
105
- return process.env.npm_execpath?.includes("npx") === true || process.argv[1]?.includes("_npx") === true || process.env.npm_config_user_agent?.includes("npm") === true;
106
- }
107
- function patchClaudeJson(serverScriptPath) {
108
- let config = {};
109
- if (existsSync(CLAUDE_JSON_PATH)) {
110
- try {
111
- config = JSON.parse(readFileSync(CLAUDE_JSON_PATH, "utf8"));
112
- } catch {
113
- config = {};
114
- }
115
- }
116
- if (!config.mcpServers || typeof config.mcpServers !== "object") {
117
- config.mcpServers = {};
118
- }
119
- const entry = isRunningViaNpx() ? { command: "npx", args: ["-y", "chromeflow"] } : { command: "node", args: [serverScriptPath] };
120
- config.mcpServers.chromeflow = entry;
121
- writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(config, null, 2) + "\n");
122
- }
123
- function patchProjectClaudeMd(cwd, force = false) {
124
- const claudeMdPath = join(cwd, "CLAUDE.md");
125
- const content = getClaudeMdContent();
126
- if (existsSync(claudeMdPath)) {
127
- const existing = readFileSync(claudeMdPath, "utf8");
128
- if (existing.includes("chromeflow")) {
129
- if (!force) return "already-present";
130
- const before = existing.slice(0, existing.indexOf("# Chromeflow")).trimEnd();
131
- writeFileSync(claudeMdPath, (before ? before + "\n\n" : "") + content);
132
- return "updated";
133
- }
134
- writeFileSync(claudeMdPath, existing.trimEnd() + "\n\n" + content);
135
- return "appended";
136
- }
137
- writeFileSync(claudeMdPath, content);
138
- return "created";
139
- }
140
- const CHROMEFLOW_TOOLS = [
141
- "open_page",
142
- "take_screenshot",
143
- "clear_overlays",
144
- "get_elements",
145
- "execute_script",
146
- "fill_input",
147
- "read_element",
148
- "get_page_text",
149
- "write_to_env",
150
- "scroll_page",
151
- "click_element",
152
- "wait_for_click",
153
- "wait_for_selector",
154
- "find_and_highlight",
155
- "highlight_region",
156
- // v0.1.23+
157
- "switch_to_tab",
158
- "list_tabs",
159
- "get_form_fields",
160
- "scroll_to_element",
161
- "save_page_state",
162
- "restore_page_state",
163
- // v0.1.25+ → merged into take_screenshot in v0.7.0
164
- // v0.1.32+
165
- "fill_form",
166
- // v0.1.36+
167
- "set_file_input",
168
- // v0.1.39+
169
- "get_console_logs",
170
- // v0.1.40+
171
- "capture_terminal",
172
- // v0.1.42+
173
- "set_dialog_response",
174
- // v0.1.46+
175
- "type_text",
176
- // v0.1.57+
177
- "inspect_request_headers",
178
- // v0.2.1+
179
- "wait_for_change",
180
- // v0.6.0+
181
- "find_text",
182
- "find_input",
183
- "wait_for_text",
184
- // v0.6.5+
185
- "react_set_input",
186
- // v0.8.0+
187
- "react_call_prop"
188
- ].map((t) => `mcp__chromeflow__${t}`);
189
- function patchSettingsLocalJson(cwd) {
190
- const claudeDir = join(cwd, ".claude");
191
- const settingsPath = join(claudeDir, "settings.local.json");
192
- let settings = {};
193
- if (existsSync(settingsPath)) {
194
- try {
195
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
196
- } catch {
197
- settings = {};
198
- }
199
- }
200
- if (!settings.permissions || typeof settings.permissions !== "object") {
201
- settings.permissions = {};
202
- }
203
- const perms = settings.permissions;
204
- const existing = Array.isArray(perms.allow) ? perms.allow : [];
205
- const withoutChromeflow = existing.filter((t) => !t.startsWith("mcp__chromeflow__"));
206
- const merged = [...withoutChromeflow, ...CHROMEFLOW_TOOLS];
207
- const changed = merged.length !== existing.length || CHROMEFLOW_TOOLS.some((t) => !existing.includes(t));
208
- if (!changed) return "already-present";
209
- perms.allow = merged;
210
- if (!existsSync(claudeDir)) mkdirSync(claudeDir, { recursive: true });
211
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
212
- return "updated";
213
- }
214
- const STORE_URL = "https://chromewebstore.google.com/detail/chromeflow/lkdchdgkbkodliefobkkhiegjdiidime";
215
- function tryOpenStorePage() {
216
- try {
217
- execSync(`open "${STORE_URL}"`, { stdio: "ignore" });
218
- return true;
219
- } catch {
220
- try {
221
- execSync(`xdg-open "${STORE_URL}"`, { stdio: "ignore" });
222
- return true;
223
- } catch {
224
- return false;
225
- }
226
- }
227
- }
228
- function patchGlobalClaudeMd() {
229
- const globalClaudeMdPath = join(HOME, ".claude", "CLAUDE.md");
230
- const hint = `## Chromeflow
231
-
232
- chromeflow is installed globally as an MCP server.
233
-
234
- If you are working in a project and the project's CLAUDE.md does not contain chromeflow
235
- instructions, tell the user: "Run \`npx chromeflow setup\` in this project directory to
236
- configure chromeflow for it."
237
- `;
238
- if (existsSync(globalClaudeMdPath)) {
239
- const existing = readFileSync(globalClaudeMdPath, "utf8");
240
- if (existing.includes("chromeflow")) return "already-present";
241
- writeFileSync(globalClaudeMdPath, existing.trimEnd() + "\n\n" + hint);
242
- return "appended";
243
- }
244
- const dir = join(HOME, ".claude");
245
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
246
- writeFileSync(globalClaudeMdPath, hint);
247
- return "created";
248
- }
249
- async function runSetup() {
250
- const scriptPath = fileURLToPath(import.meta.url);
251
- const distDir = dirname(scriptPath);
252
- const serverScriptPath = resolve(distDir, "index.js");
253
- console.log("\nChromeflow Setup\n" + "\u2500".repeat(40));
254
- patchClaudeJson(serverScriptPath);
255
- const viaNpx = isRunningViaNpx();
256
- console.log(`\u2713 Registered MCP server in ~/.claude.json`);
257
- console.log(viaNpx ? ` \u2192 npx -y chromeflow (auto-updates)` : ` \u2192 node ${serverScriptPath}`);
258
- const cwd = process.cwd();
259
- const mdResult = patchProjectClaudeMd(cwd);
260
- const settingsResult = patchSettingsLocalJson(cwd);
261
- if (mdResult === "already-present") {
262
- console.log("\u2713 CLAUDE.md already has chromeflow instructions (run `npx chromeflow update` to refresh)");
263
- } else if (mdResult === "appended") {
264
- console.log(`\u2713 Appended chromeflow instructions to ${join(cwd, "CLAUDE.md")}`);
265
- } else {
266
- console.log(`\u2713 Created ${join(cwd, "CLAUDE.md")}`);
267
- }
268
- if (settingsResult === "already-present") {
269
- console.log("\u2713 .claude/settings.local.json already allows chromeflow tools");
270
- } else {
271
- console.log("\u2713 Added chromeflow tools to .claude/settings.local.json (no approval prompts)");
272
- }
273
- console.log("\nChrome extension (one-time step):");
274
- const opened = tryOpenStorePage();
275
- if (opened) {
276
- console.log(" Opened Chrome Web Store \u2014 click 'Add to Chrome' to install.");
277
- } else {
278
- console.log(` Install from the Chrome Web Store:
279
- ${STORE_URL}`);
280
- }
281
- const globalResult = patchGlobalClaudeMd();
282
- if (globalResult === "already-present") {
283
- console.log("\u2713 ~/.claude/CLAUDE.md already has chromeflow hint");
284
- } else if (globalResult === "appended") {
285
- console.log("\u2713 Appended chromeflow hint to ~/.claude/CLAUDE.md");
286
- } else {
287
- console.log("\u2713 Created ~/.claude/CLAUDE.md with chromeflow hint");
288
- }
289
- console.log("\nDone. Restart Claude Code to activate chromeflow.\n");
290
- }
291
- async function runUninstall() {
292
- const cwd = process.cwd();
293
- console.log("\nChromeflow Uninstall\n" + "\u2500".repeat(40));
294
- if (existsSync(CLAUDE_JSON_PATH)) {
295
- try {
296
- const config = JSON.parse(readFileSync(CLAUDE_JSON_PATH, "utf8"));
297
- if (config.mcpServers && typeof config.mcpServers === "object") {
298
- delete config.mcpServers.chromeflow;
299
- }
300
- writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(config, null, 2) + "\n");
301
- console.log("\u2713 Removed chromeflow MCP server from ~/.claude.json");
302
- } catch {
303
- console.log(" Could not update ~/.claude.json (skipping)");
304
- }
305
- }
306
- const claudeMdPath = join(cwd, "CLAUDE.md");
307
- if (existsSync(claudeMdPath)) {
308
- const existing = readFileSync(claudeMdPath, "utf8");
309
- if (existing.includes("# Chromeflow")) {
310
- const idx = existing.indexOf("# Chromeflow");
311
- const before = existing.slice(0, idx).trimEnd();
312
- writeFileSync(claudeMdPath, before ? before + "\n" : "");
313
- console.log(`\u2713 Removed chromeflow section from ${claudeMdPath}`);
314
- }
315
- }
316
- const settingsPath = join(cwd, ".claude", "settings.local.json");
317
- if (existsSync(settingsPath)) {
318
- try {
319
- const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
320
- const perms = settings.permissions;
321
- if (perms && Array.isArray(perms.allow)) {
322
- perms.allow = perms.allow.filter((t) => !t.startsWith("mcp__chromeflow__"));
323
- }
324
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
325
- console.log("\u2713 Removed chromeflow tools from .claude/settings.local.json");
326
- } catch {
327
- console.log(" Could not update .claude/settings.local.json (skipping)");
328
- }
329
- }
330
- const globalClaudeMdPath = join(HOME, ".claude", "CLAUDE.md");
331
- if (existsSync(globalClaudeMdPath)) {
332
- const existing = readFileSync(globalClaudeMdPath, "utf8");
333
- if (existing.includes("## Chromeflow")) {
334
- const idx = existing.indexOf("## Chromeflow");
335
- const before = existing.slice(0, idx).trimEnd();
336
- writeFileSync(globalClaudeMdPath, before ? before + "\n" : "");
337
- console.log("\u2713 Removed chromeflow hint from ~/.claude/CLAUDE.md");
338
- }
339
- }
340
- console.log("\nDone. Restart Claude Code to complete removal.\n");
341
- }
342
- async function fetchLatestClaudeMd() {
343
- try {
344
- const res = await fetch("https://unpkg.com/chromeflow@latest/CLAUDE.md");
345
- if (res.ok) return await res.text();
346
- } catch {
347
- }
348
- return getClaudeMdContent();
349
- }
350
- async function runUpdate() {
351
- const cwd = process.cwd();
352
- console.log("\nChromeflow Update\n" + "\u2500".repeat(40));
353
- const freshContent = await fetchLatestClaudeMd();
354
- const claudeMdPath = join(cwd, "CLAUDE.md");
355
- let mdResult;
356
- if (existsSync(claudeMdPath)) {
357
- const existing = readFileSync(claudeMdPath, "utf8");
358
- if (existing.includes("# Chromeflow")) {
359
- const before = existing.slice(0, existing.indexOf("# Chromeflow")).trimEnd();
360
- const newContent = (before ? before + "\n\n" : "") + freshContent;
361
- if (newContent === existing) {
362
- mdResult = "unchanged";
363
- } else {
364
- writeFileSync(claudeMdPath, newContent);
365
- mdResult = "updated";
366
- }
367
- } else {
368
- writeFileSync(claudeMdPath, existing.trimEnd() + "\n\n" + freshContent);
369
- mdResult = "appended";
370
- }
371
- } else {
372
- writeFileSync(claudeMdPath, freshContent);
373
- mdResult = "created";
374
- }
375
- if (mdResult === "unchanged") {
376
- console.log(`\u2713 ${claudeMdPath} already up to date`);
377
- } else if (mdResult === "updated") {
378
- console.log(`\u2713 Updated chromeflow instructions in ${claudeMdPath}`);
379
- } else if (mdResult === "appended") {
380
- console.log(`\u2713 Appended chromeflow instructions to ${claudeMdPath}`);
381
- } else {
382
- console.log(`\u2713 Created ${claudeMdPath}`);
383
- }
384
- const settingsResult = patchSettingsLocalJson(cwd);
385
- if (settingsResult === "already-present") {
386
- console.log("\u2713 .claude/settings.local.json already up to date");
387
- } else {
388
- console.log("\u2713 Updated chromeflow tools in .claude/settings.local.json");
389
- }
390
- console.log("Done.\n");
391
- }
392
- function readGlobalChromeflowVersion() {
393
- try {
394
- const root = execSync("npm root -g", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
395
- const pkgPath = join(root, "chromeflow", "package.json");
396
- if (!existsSync(pkgPath)) return null;
397
- return JSON.parse(readFileSync(pkgPath, "utf8")).version;
398
- } catch {
399
- return null;
400
- }
401
- }
402
- function listNpxCachedChromeflowVersions() {
403
- const cacheDir = join(HOME, ".npm", "_npx");
404
- if (!existsSync(cacheDir)) return [];
405
- const out = [];
406
- let entries;
407
- try {
408
- entries = readdirSync(cacheDir);
409
- } catch {
410
- return [];
411
- }
412
- for (const hash of entries) {
413
- const pkgPath = join(cacheDir, hash, "node_modules", "chromeflow", "package.json");
414
- if (!existsSync(pkgPath)) continue;
415
- try {
416
- const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
417
- out.push({ hash, version: v });
418
- } catch {
419
- }
420
- }
421
- return out;
422
- }
423
- async function fetchLatestPublishedVersion() {
424
- try {
425
- const res = await fetch("https://registry.npmjs.org/chromeflow/latest");
426
- if (!res.ok) return null;
427
- const json = await res.json();
428
- return json.version ?? null;
429
- } catch {
430
- return null;
431
- }
432
- }
433
- function compareSemver(a, b) {
434
- const pa = a.split(".").map(Number);
435
- const pb = b.split(".").map(Number);
436
- for (let i = 0; i < 3; i++) {
437
- const da = pa[i] ?? 0;
438
- const db = pb[i] ?? 0;
439
- if (da !== db) return da - db;
440
- }
441
- return 0;
442
- }
443
- async function runDoctor(runningVersion) {
444
- console.log("\nChromeflow Doctor\n" + "\u2500".repeat(40));
445
- const installScript = fileURLToPath(import.meta.url);
446
- console.log(`Running version: ${runningVersion}`);
447
- console.log(`Running from: ${installScript}`);
448
- const globalVersion = readGlobalChromeflowVersion();
449
- console.log(`Global install: ${globalVersion ?? "(none \u2014 not installed via `npm install -g chromeflow`)"}`);
450
- const npxCaches = listNpxCachedChromeflowVersions();
451
- if (npxCaches.length === 0) {
452
- console.log(`npx cache: (empty)`);
453
- } else {
454
- console.log(`npx cache (${npxCaches.length} ${npxCaches.length === 1 ? "entry" : "entries"}):`);
455
- for (const c of npxCaches) console.log(` ${c.hash}: ${c.version}`);
456
- }
457
- const latest = await fetchLatestPublishedVersion();
458
- console.log(`Latest on npm: ${latest ?? "(unable to reach https://registry.npmjs.org)"}`);
459
- const allKnown = [runningVersion];
460
- if (globalVersion) allKnown.push(globalVersion);
461
- for (const c of npxCaches) allKnown.push(c.version);
462
- const stale = latest ? allKnown.some((v) => v !== "unknown" && compareSemver(v, latest) < 0) : false;
463
- if (stale) {
464
- console.log("\n\u26A0 Stale chromeflow install detected.");
465
- console.log(" Reinstall recipe:");
466
- console.log(" npm uninstall -g chromeflow");
467
- console.log(" rm -rf ~/.npm/_npx");
468
- console.log(" npm install -g chromeflow@latest");
469
- console.log(" Then restart Claude Code so the MCP server picks up the new version.");
470
- } else if (latest && compareSemver(runningVersion, latest) === 0) {
471
- console.log("\n\u2713 chromeflow is up to date.");
472
- } else if (!latest) {
473
- console.log("\n? Could not check the registry; skipping freshness check.");
474
- } else {
475
- console.log("\n\u2713 Running version is current.");
476
- }
477
- console.log("\nTools shipped in this build (from setup manifest):");
478
- const toolNames = CHROMEFLOW_TOOLS.map((t) => t.replace("mcp__chromeflow__", ""));
479
- console.log(" " + toolNames.join(", "));
480
- console.log(` (${toolNames.length} tools)`);
481
- console.log("\nIf chromeflow tools are missing in Claude Code:");
482
- console.log(" 1. Restart Claude Code (the MCP server is started at session start).");
483
- console.log(" 2. If still missing, run the reinstall recipe above.");
484
- console.log(" 3. Confirm the MCP startup log shows the version banner");
485
- console.log(` ([chromeflow] v${latest ?? "X.Y.Z"} \u2014 registered N tools).`);
486
- console.log("");
487
- }
488
- export {
489
- runDoctor,
490
- runSetup,
491
- runUninstall,
492
- runUpdate
493
- };