chromeflow 0.8.0 → 0.9.8
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/LICENSE +21 -0
- package/README.md +43 -148
- package/bin/chromeflow.mjs +25916 -0
- package/package.json +35 -20
- package/CLAUDE.md +0 -370
- package/dist/index.js +0 -115
- package/dist/setup.js +0 -493
- package/dist/tools/browser.js +0 -404
- package/dist/tools/capture.js +0 -216
- package/dist/tools/flow.js +0 -436
- package/dist/tools/highlight.js +0 -70
- package/dist/types.js +0 -0
- package/dist/ws-bridge.js +0 -116
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
|
-
};
|