chromeflow 0.1.0

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.
Files changed (2) hide show
  1. package/dist/index.js +652 -0
  2. package/package.json +41 -0
package/dist/index.js ADDED
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/ws-bridge.ts
8
+ import { execSync } from "child_process";
9
+ import { WebSocketServer, WebSocket } from "ws";
10
+ var WS_PORT = 7878;
11
+ var REQUEST_TIMEOUT_MS = 3e4;
12
+ var WsBridge = class {
13
+ wss;
14
+ client = null;
15
+ pending = /* @__PURE__ */ new Map();
16
+ constructor() {
17
+ this.bind();
18
+ }
19
+ bind() {
20
+ const wss = new WebSocketServer({ port: WS_PORT });
21
+ wss.on("error", (err) => {
22
+ if (err.code === "EADDRINUSE") {
23
+ console.error(
24
+ `[chromeflow] Port ${WS_PORT} in use \u2014 killing stale process and retrying...`
25
+ );
26
+ try {
27
+ execSync(`lsof -ti:${WS_PORT} | xargs kill -9`, { stdio: "ignore" });
28
+ } catch {
29
+ }
30
+ setTimeout(() => this.bind(), 800);
31
+ } else {
32
+ console.error("[chromeflow] WS server error:", err);
33
+ }
34
+ });
35
+ wss.on("listening", () => {
36
+ this.wss = wss;
37
+ console.error(`[chromeflow] WS bridge listening on ws://localhost:${WS_PORT}`);
38
+ });
39
+ wss.on("connection", (ws) => {
40
+ if (this.client) {
41
+ this.client.terminate();
42
+ }
43
+ this.client = ws;
44
+ console.error("[chromeflow] Extension connected");
45
+ ws.on("message", (data) => {
46
+ let msg;
47
+ try {
48
+ msg = JSON.parse(data.toString());
49
+ } catch {
50
+ return;
51
+ }
52
+ if (msg.type === "ready") {
53
+ console.error("[chromeflow] Extension ready");
54
+ return;
55
+ }
56
+ const pending = this.pending.get(msg.requestId);
57
+ if (pending) {
58
+ clearTimeout(pending.timer);
59
+ this.pending.delete(msg.requestId);
60
+ if (msg.type === "error") {
61
+ pending.reject(new Error(msg.message));
62
+ } else {
63
+ pending.resolve(msg);
64
+ }
65
+ }
66
+ });
67
+ ws.on("close", () => {
68
+ console.error("[chromeflow] Extension disconnected");
69
+ this.client = null;
70
+ for (const [id, pending] of this.pending) {
71
+ clearTimeout(pending.timer);
72
+ pending.reject(new Error("Extension disconnected"));
73
+ this.pending.delete(id);
74
+ }
75
+ });
76
+ });
77
+ }
78
+ isConnected() {
79
+ return this.client !== null && this.client.readyState === WebSocket.OPEN;
80
+ }
81
+ /** Send a message and wait for a response from the extension. */
82
+ request(message) {
83
+ if (!this.isConnected()) {
84
+ return Promise.reject(
85
+ new Error(
86
+ "Chromeflow extension is not connected. Open Chrome and ensure the extension is installed."
87
+ )
88
+ );
89
+ }
90
+ const requestId = crypto.randomUUID();
91
+ return new Promise((resolve2, reject) => {
92
+ const timer = setTimeout(() => {
93
+ this.pending.delete(requestId);
94
+ reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
95
+ }, REQUEST_TIMEOUT_MS);
96
+ this.pending.set(requestId, { resolve: resolve2, reject, timer });
97
+ this.client.send(JSON.stringify({ ...message, requestId }));
98
+ });
99
+ }
100
+ /** Send a fire-and-forget message (no response expected). */
101
+ send(message) {
102
+ if (!this.isConnected()) {
103
+ throw new Error("Chromeflow extension is not connected.");
104
+ }
105
+ const requestId = crypto.randomUUID();
106
+ this.client.send(JSON.stringify({ ...message, requestId }));
107
+ }
108
+ };
109
+
110
+ // src/tools/browser.ts
111
+ import { z } from "zod";
112
+ function registerBrowserTools(server, bridge) {
113
+ server.tool(
114
+ "open_page",
115
+ "Navigate the user's active Chrome tab to a URL",
116
+ { url: z.string().url().describe("The URL to navigate to") },
117
+ async ({ url }) => {
118
+ await bridge.request({ type: "navigate", url });
119
+ return {
120
+ content: [{ type: "text", text: `Navigated to ${url}` }]
121
+ };
122
+ }
123
+ );
124
+ server.tool(
125
+ "take_screenshot",
126
+ "Capture a screenshot of the current browser tab so you can see what is on the page. Use this to identify element positions before highlighting.",
127
+ {},
128
+ async () => {
129
+ const response = await bridge.request({ type: "screenshot" });
130
+ if (response.type !== "screenshot_response") {
131
+ throw new Error("Unexpected response from extension");
132
+ }
133
+ return {
134
+ content: [
135
+ {
136
+ type: "image",
137
+ data: response.image,
138
+ mimeType: "image/png"
139
+ },
140
+ {
141
+ type: "text",
142
+ text: `Screenshot captured (${response.width}x${response.height}). Analyze the image to identify element positions for highlighting.`
143
+ }
144
+ ]
145
+ };
146
+ }
147
+ );
148
+ server.tool(
149
+ "clear_overlays",
150
+ "Remove all Keyclaw highlights, callouts, and guide panels from the current page",
151
+ {},
152
+ async () => {
153
+ await bridge.request({ type: "clear" });
154
+ return {
155
+ content: [{ type: "text", text: "All overlays cleared." }]
156
+ };
157
+ }
158
+ );
159
+ }
160
+
161
+ // src/tools/highlight.ts
162
+ import { z as z2 } from "zod";
163
+ function registerHighlightTools(server, bridge) {
164
+ server.tool(
165
+ "find_and_highlight",
166
+ "Find an element on the page by its visible text and highlight it with an instructional callout. Try this before using highlight_region. Returns whether the element was found.",
167
+ {
168
+ text: z2.string().describe(
169
+ "Visible text of the element or text near it (e.g. 'API Keys', 'Create account')"
170
+ ),
171
+ message: z2.string().describe(
172
+ "Instruction to show the user in the callout (e.g. 'Click here to create your API key')"
173
+ )
174
+ },
175
+ async ({ text, message }) => {
176
+ const response = await bridge.request({
177
+ type: "find_highlight",
178
+ text,
179
+ message
180
+ });
181
+ if (response.type !== "find_highlight_response") {
182
+ throw new Error("Unexpected response from extension");
183
+ }
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: response.found ? `Element containing "${text}" highlighted.` : `Element containing "${text}" not found. Try take_screenshot to identify the element visually.`
189
+ }
190
+ ]
191
+ };
192
+ }
193
+ );
194
+ server.tool(
195
+ "highlight_region",
196
+ "Highlight a specific pixel region on the page with an instructional callout. Use this after take_screenshot when you can see the element's position.",
197
+ {
198
+ x: z2.number().describe("Left edge of the region in CSS pixels"),
199
+ y: z2.number().describe("Top edge of the region in CSS pixels"),
200
+ width: z2.number().describe("Width of the region in CSS pixels"),
201
+ height: z2.number().describe("Height of the region in CSS pixels"),
202
+ message: z2.string().describe(
203
+ "Instruction to show the user in the callout (e.g. 'Click here to reveal your API key')"
204
+ )
205
+ },
206
+ async ({ x, y, width, height, message }) => {
207
+ await bridge.request({ type: "highlight_region", x, y, width, height, message });
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: `Region highlighted at (${x}, ${y}) ${width}\xD7${height}.`
213
+ }
214
+ ]
215
+ };
216
+ }
217
+ );
218
+ server.tool(
219
+ "show_guide_panel",
220
+ "Show a floating step-by-step guide panel on the page to help the user understand what they need to do",
221
+ {
222
+ title: z2.string().describe("Title of the guide (e.g. 'Set up Stripe API keys')"),
223
+ steps: z2.array(
224
+ z2.object({
225
+ text: z2.string().describe("Step instruction text"),
226
+ done: z2.boolean().optional().describe("Whether this step is already completed")
227
+ })
228
+ ).describe("Ordered list of steps")
229
+ },
230
+ async ({ title, steps }) => {
231
+ await bridge.request({ type: "show_panel", title, steps });
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: `Guide panel shown: "${title}" with ${steps.length} steps.`
237
+ }
238
+ ]
239
+ };
240
+ }
241
+ );
242
+ }
243
+
244
+ // src/tools/capture.ts
245
+ import { z as z3 } from "zod";
246
+ import { appendFileSync, readFileSync, writeFileSync } from "fs";
247
+ function registerCaptureTools(server, bridge) {
248
+ server.tool(
249
+ "fill_input",
250
+ `Fill a form input field with a value automatically.
251
+ Use this for fields Claude knows the answer to (product name, price, description, tier name, URLs, etc.).
252
+ DO NOT use for: email address, password, payment/billing info, phone number \u2014 highlight those instead and tell the user what to enter.
253
+ After filling, call wait_for_click only if the user needs to review/confirm; otherwise proceed directly to the next step.`,
254
+ {
255
+ textHint: z3.string().describe("The label, placeholder, or nearby text identifying the input (e.g. 'Product name', 'Amount', 'Description')"),
256
+ value: z3.string().describe("The value to fill in")
257
+ },
258
+ async ({ textHint, value }) => {
259
+ const response = await bridge.request({ type: "fill_input", textHint, value });
260
+ if (response.type !== "fill_response") throw new Error("Unexpected response");
261
+ const r = response;
262
+ return {
263
+ content: [{ type: "text", text: r.success ? `Filled "${textHint}": ${r.message}` : `Could not fill "${textHint}": ${r.message}` }]
264
+ };
265
+ }
266
+ );
267
+ server.tool(
268
+ "read_element",
269
+ "Read the text value of an element on the page, identified by nearby visible text. Use this to capture API keys, IDs, or other values shown on the page.",
270
+ {
271
+ textHint: z3.string().describe(
272
+ "Visible text near or within the element whose value you want to read (e.g. 'Publishable key', 'sk-live')"
273
+ )
274
+ },
275
+ async ({ textHint }) => {
276
+ const response = await bridge.request({ type: "read_element", textHint });
277
+ if (response.type !== "read_response") {
278
+ throw new Error("Unexpected response from extension");
279
+ }
280
+ if (response.value === null) {
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: `Could not find a value near "${textHint}". Try take_screenshot to locate it.`
286
+ }
287
+ ]
288
+ };
289
+ }
290
+ return {
291
+ content: [
292
+ {
293
+ type: "text",
294
+ text: `Value captured: ${response.value}`
295
+ }
296
+ ]
297
+ };
298
+ }
299
+ );
300
+ server.tool(
301
+ "write_to_env",
302
+ "Write a key=value pair to a .env file. Use this after capturing an API key or ID from the page.",
303
+ {
304
+ key: z3.string().describe("Environment variable name (e.g. STRIPE_SECRET_KEY)"),
305
+ value: z3.string().describe("The value to write"),
306
+ envPath: z3.string().describe(
307
+ "Absolute path to the .env file (e.g. /Users/me/myproject/.env)"
308
+ )
309
+ },
310
+ async ({ key, value, envPath }) => {
311
+ try {
312
+ let existing = "";
313
+ try {
314
+ existing = readFileSync(envPath, "utf-8");
315
+ } catch {
316
+ }
317
+ const lines = existing.split("\n");
318
+ const keyPattern = new RegExp(`^${key}=`);
319
+ const existingIndex = lines.findIndex((l) => keyPattern.test(l));
320
+ if (existingIndex !== -1) {
321
+ lines[existingIndex] = `${key}=${value}`;
322
+ writeFileSync(envPath, lines.join("\n"), "utf-8");
323
+ } else {
324
+ const toAppend = (existing && !existing.endsWith("\n") ? "\n" : "") + `${key}=${value}
325
+ `;
326
+ appendFileSync(envPath, toAppend, "utf-8");
327
+ }
328
+ return {
329
+ content: [
330
+ {
331
+ type: "text",
332
+ text: `Written ${key}=<value> to ${envPath}`
333
+ }
334
+ ]
335
+ };
336
+ } catch (err) {
337
+ throw new Error(`Failed to write to .env: ${err.message}`);
338
+ }
339
+ }
340
+ );
341
+ }
342
+
343
+ // src/tools/flow.ts
344
+ import { z as z4 } from "zod";
345
+ function registerFlowTools(server, bridge) {
346
+ server.tool(
347
+ "scroll_page",
348
+ "Scroll the page or the focused panel up or down. Use this when a button (e.g. Save) is below the visible area of a panel or page. After scrolling, retry click_element.",
349
+ {
350
+ direction: z4.enum(["down", "up"]).describe("Scroll direction"),
351
+ amount: z4.number().optional().describe("Pixels to scroll (default 400)")
352
+ },
353
+ async ({ direction, amount = 400 }) => {
354
+ await bridge.request({ type: "scroll_page", direction, amount });
355
+ return { content: [{ type: "text", text: `Scrolled ${direction} ${amount}px.` }] };
356
+ }
357
+ );
358
+ server.tool(
359
+ "click_element",
360
+ `Click a button, link, or interactive element on the page by its visible text or aria-label.
361
+ Use this whenever Claude can press a button without needing user input \u2014 e.g. "Save", "Continue", "Create product", "Add pricing", "Confirm", "Next".
362
+ After clicking, call take_screenshot() to see the result before proceeding.
363
+ Do NOT use for: elements that require the user to make a personal choice, consent to terms, or enter sensitive data.`,
364
+ {
365
+ textHint: z4.string().describe(
366
+ "The visible label of the button or link (e.g. 'Save product', 'Continue', 'Add a product', 'Create')"
367
+ )
368
+ },
369
+ async ({ textHint }) => {
370
+ const response = await bridge.request({ type: "click_element", textHint });
371
+ const r = response;
372
+ if (!r.success) {
373
+ return {
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: `Could not click "${textHint}": ${r.message}. Call take_screenshot() to locate the element visually.`
378
+ }
379
+ ]
380
+ };
381
+ }
382
+ return {
383
+ content: [{ type: "text", text: r.message }]
384
+ };
385
+ }
386
+ );
387
+ server.tool(
388
+ "wait_for_click",
389
+ `Wait for the user to click (or interact with) the currently highlighted element, then return.
390
+ Use this after highlighting a step so the flow advances automatically without the user returning to the chat.
391
+ After this resolves, highlight the next step immediately.
392
+ If the click causes page navigation, this resolves when the new page finishes loading.`,
393
+ {
394
+ timeout: z4.number().optional().describe("Max seconds to wait for the click (default 120)")
395
+ },
396
+ async ({ timeout = 120 }) => {
397
+ const response = await bridge.request({
398
+ type: "start_click_watch",
399
+ timeout: timeout * 1e3
400
+ });
401
+ if (response.type === "navigation_complete") {
402
+ return {
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: `User clicked. Page navigated to: ${response.url}`
407
+ }
408
+ ]
409
+ };
410
+ }
411
+ return {
412
+ content: [{ type: "text", text: "User clicked the highlighted element." }]
413
+ };
414
+ }
415
+ );
416
+ server.tool(
417
+ "wait_for_navigation",
418
+ "Wait for the browser to navigate to a new page (without requiring a prior click watch). Useful after calling open_page or after a form submission.",
419
+ {
420
+ urlPattern: z4.string().optional().describe("Substring to match in the new URL \u2014 waits for any navigation if omitted"),
421
+ timeout: z4.number().optional().describe("Max seconds to wait (default 30)")
422
+ },
423
+ async ({ urlPattern, timeout = 30 }) => {
424
+ const response = await bridge.request({
425
+ type: "start_click_watch",
426
+ timeout: timeout * 1e3
427
+ });
428
+ const url = response.url ?? "";
429
+ if (urlPattern && !url.includes(urlPattern)) {
430
+ return {
431
+ content: [
432
+ { type: "text", text: `Navigation detected to ${url} (pattern "${urlPattern}" not matched \u2014 proceeding anyway).` }
433
+ ]
434
+ };
435
+ }
436
+ return {
437
+ content: [{ type: "text", text: `Page navigated to: ${url}` }]
438
+ };
439
+ }
440
+ );
441
+ server.tool(
442
+ "mark_step_done",
443
+ "Mark a step in the guide panel as completed (shows a green check). Call this after wait_for_click resolves.",
444
+ {
445
+ stepIndex: z4.number().int().describe("0-based index of the step to mark done")
446
+ },
447
+ async ({ stepIndex }) => {
448
+ await bridge.request({ type: "mark_step_done", stepIndex });
449
+ return {
450
+ content: [{ type: "text", text: `Step ${stepIndex + 1} marked as done.` }]
451
+ };
452
+ }
453
+ );
454
+ }
455
+
456
+ // src/setup.ts
457
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync } from "fs";
458
+ import { homedir } from "os";
459
+ import { join, resolve, dirname } from "path";
460
+ import { fileURLToPath } from "url";
461
+ import { execSync as execSync2 } from "child_process";
462
+ var HOME = homedir();
463
+ var CLAUDE_JSON_PATH = join(HOME, ".claude.json");
464
+ var PROJECT_CLAUDE_MD = `# Chromeflow \u2014 Claude Instructions
465
+
466
+ ## What chromeflow is
467
+ Chromeflow is a browser guidance tool. When a task requires the user to interact with a
468
+ website (create accounts, set up billing, retrieve API keys, configure third-party services),
469
+ use chromeflow to guide them through it visually instead of giving text instructions.
470
+
471
+ ## When to use chromeflow (be proactive)
472
+ Use chromeflow automatically whenever a task requires:
473
+ - Creating or configuring a third-party account (Stripe, SendGrid, Supabase, Vercel, etc.)
474
+ - Retrieving API keys, secrets, or credentials to place in \`.env\`
475
+ - Setting up pricing tiers, webhooks, or service configuration in a web UI
476
+ - Any browser-based step that is blocking code work
477
+
478
+ Do NOT ask "should I open the browser?" \u2014 just do it. The user expects seamless handoff.
479
+
480
+ ## HARD RULES \u2014 never break these
481
+
482
+ 1. **Never use Bash as a fallback for browser tasks.** If \`click_element\` fails, use
483
+ \`scroll_page\` then retry, or use \`highlight_region\` to show the user. Never use
484
+ \`osascript\`, \`applescript\`, or any shell command to control the browser.
485
+
486
+ 2. **Take a screenshot only when you need to decide what to do next.** Do not take
487
+ a screenshot after every action as a reflex. Take one after navigation, or when
488
+ \`click_element\`/\`find_and_highlight\` fails and you need to locate something visually.
489
+
490
+ 3. **\`open_page\` already waits for navigation.** Never call \`wait_for_navigation\`
491
+ immediately after \`open_page\` \u2014 it will time out.
492
+
493
+ 4. **When \`click_element\` fails:** first try \`scroll_page(down)\` then retry
494
+ \`click_element\`. If it still fails, \`take_screenshot\` and use \`highlight_region\`
495
+ with pixel coordinates from the image.
496
+
497
+ ## Guided flow pattern
498
+
499
+ \`\`\`
500
+ 1. show_guide_panel(title, steps[]) \u2014 show the full plan upfront
501
+ 2. open_page(url) \u2014 navigate to the right page
502
+ 3. For each step:
503
+ a. [if needed] take_screenshot() \u2014 only when you need to locate something
504
+ b. Claude acts directly:
505
+ click_element("Save") \u2014 press buttons/links Claude can press
506
+ fill_input("Product name", "Pro") \u2014 fill fields Claude knows the answer to
507
+ scroll_page("down") \u2014 reveal off-screen content then retry
508
+ Or pause for the user:
509
+ find_and_highlight(text, msg) \u2014 show the user what to do
510
+ wait_for_click() \u2014 wait for user interaction
511
+ c. mark_step_done(i) \u2014 check off the step
512
+ 4. clear_overlays() \u2014 clean up when done
513
+ \`\`\`
514
+
515
+ **Default to automation.** Only pause for human input when the step genuinely requires
516
+ personal data or a human decision.
517
+
518
+ ## What to do automatically vs pause for the user
519
+
520
+ **Claude acts directly** (\`click_element\` / \`fill_input\`):
521
+ - Any button: Save, Continue, Create, Add, Confirm, Next, Submit, Update
522
+ - Product names, descriptions, feature lists
523
+ - Prices and amounts specified in the task
524
+ - URLs, redirect URIs, webhook endpoints
525
+ - Selecting billing period, currency, or other known options
526
+ - Dismissing cookie banners, cookie dialogs, "not now" prompts
527
+
528
+ **Pause for the user** (\`find_and_highlight\` + \`wait_for_click\`):
529
+ - Email address / username / login
530
+ - Password or passphrase
531
+ - Payment method / billing / card details
532
+ - Phone number / 2FA / OTP codes
533
+ - Any legal consent the user must personally accept
534
+ - Choices that depend on user preference Claude wasn't told
535
+
536
+ ## Capturing credentials
537
+ After a secret key or API key is revealed:
538
+ 1. \`read_element(hint)\` \u2014 capture the value
539
+ 2. \`write_to_env(KEY_NAME, value, envPath)\` \u2014 write to \`.env\`
540
+ 3. Tell the user what was written
541
+
542
+ Use the absolute path for \`envPath\` \u2014 it's the Claude Code working directory + \`/.env\`.
543
+
544
+ ## Error handling
545
+ - \`click_element\` not found \u2192 \`scroll_page("down")\` then retry
546
+ - Still not found \u2192 \`take_screenshot()\` then \`highlight_region(x,y,w,h,msg)\`
547
+ - Page still loading \u2192 \`take_screenshot()\` to confirm, proceed when content is visible
548
+ - Never use Bash to work around a stuck browser interaction
549
+ `;
550
+ function isRunningViaNpx() {
551
+ return process.env.npm_execpath?.includes("npx") === true || process.argv[1]?.includes("_npx") === true || process.env.npm_config_user_agent?.includes("npm") === true;
552
+ }
553
+ function patchClaudeJson(serverScriptPath) {
554
+ let config = {};
555
+ if (existsSync(CLAUDE_JSON_PATH)) {
556
+ try {
557
+ config = JSON.parse(readFileSync2(CLAUDE_JSON_PATH, "utf8"));
558
+ } catch {
559
+ config = {};
560
+ }
561
+ }
562
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
563
+ config.mcpServers = {};
564
+ }
565
+ const entry = isRunningViaNpx() ? { command: "npx", args: ["-y", "chromeflow"] } : { command: "node", args: [serverScriptPath] };
566
+ config.mcpServers.chromeflow = entry;
567
+ writeFileSync2(CLAUDE_JSON_PATH, JSON.stringify(config, null, 2) + "\n");
568
+ }
569
+ function patchProjectClaudeMd(cwd) {
570
+ const claudeMdPath = join(cwd, "CLAUDE.md");
571
+ if (existsSync(claudeMdPath)) {
572
+ const existing = readFileSync2(claudeMdPath, "utf8");
573
+ if (existing.includes("chromeflow")) {
574
+ return "already-present";
575
+ }
576
+ writeFileSync2(claudeMdPath, existing.trimEnd() + "\n\n" + PROJECT_CLAUDE_MD);
577
+ return "appended";
578
+ }
579
+ writeFileSync2(claudeMdPath, PROJECT_CLAUDE_MD);
580
+ return "created";
581
+ }
582
+ function tryOpenExtensionsPage() {
583
+ try {
584
+ execSync2('open -a "Google Chrome" "chrome://extensions"', { stdio: "ignore" });
585
+ return true;
586
+ } catch {
587
+ try {
588
+ execSync2('xdg-open "chrome://extensions"', { stdio: "ignore" });
589
+ return true;
590
+ } catch {
591
+ return false;
592
+ }
593
+ }
594
+ }
595
+ async function runSetup() {
596
+ const scriptPath = fileURLToPath(import.meta.url);
597
+ const distDir = dirname(scriptPath);
598
+ const serverScriptPath = resolve(distDir, "index.js");
599
+ const extensionDistPath = resolve(distDir, "..", "..", "extension", "dist");
600
+ console.log("\nChromeflow Setup\n" + "\u2500".repeat(40));
601
+ patchClaudeJson(serverScriptPath);
602
+ const viaNpx = isRunningViaNpx();
603
+ console.log(`\u2713 Registered MCP server in ~/.claude.json`);
604
+ console.log(viaNpx ? ` \u2192 npx -y chromeflow (auto-updates)` : ` \u2192 node ${serverScriptPath}`);
605
+ const cwd = process.cwd();
606
+ const mdResult = patchProjectClaudeMd(cwd);
607
+ if (mdResult === "already-present") {
608
+ console.log("\u2713 CLAUDE.md already has chromeflow instructions");
609
+ } else if (mdResult === "appended") {
610
+ console.log(`\u2713 Appended chromeflow instructions to ${join(cwd, "CLAUDE.md")}`);
611
+ } else {
612
+ console.log(`\u2713 Created ${join(cwd, "CLAUDE.md")}`);
613
+ }
614
+ console.log("\nChrome extension (one-time manual step):");
615
+ const opened = tryOpenExtensionsPage();
616
+ if (opened) {
617
+ console.log(" Opened chrome://extensions in Chrome.");
618
+ } else {
619
+ console.log(" Open chrome://extensions in Chrome.");
620
+ }
621
+ console.log(" 1. Enable Developer mode (top-right toggle)");
622
+ console.log(" 2. Click 'Load unpacked'");
623
+ console.log(` 3. Select: ${extensionDistPath}`);
624
+ console.log("\nDone. Restart Claude Code to activate chromeflow.\n");
625
+ }
626
+
627
+ // src/index.ts
628
+ if (process.argv[2] === "setup") {
629
+ runSetup().catch((err) => {
630
+ console.error(err);
631
+ process.exit(1);
632
+ });
633
+ } else {
634
+ main().catch((err) => {
635
+ console.error("[chromeflow] Fatal error:", err);
636
+ process.exit(1);
637
+ });
638
+ }
639
+ async function main() {
640
+ const bridge = new WsBridge();
641
+ const server = new McpServer({
642
+ name: "chromeflow",
643
+ version: "0.1.0"
644
+ });
645
+ registerBrowserTools(server, bridge);
646
+ registerHighlightTools(server, bridge);
647
+ registerCaptureTools(server, bridge);
648
+ registerFlowTools(server, bridge);
649
+ const transport = new StdioServerTransport();
650
+ await server.connect(transport);
651
+ console.error("[chromeflow] MCP server running. Waiting for Claude...");
652
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "chromeflow",
3
+ "version": "0.1.0",
4
+ "description": "Browser guidance MCP server for Claude Code — highlights, clicks, fills, and captures from the web so you don't have to.",
5
+ "type": "module",
6
+ "bin": {
7
+ "chromeflow": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/NeoDrew/chromeflow.git"
15
+ },
16
+ "homepage": "https://github.com/NeoDrew/chromeflow",
17
+ "license": "MIT",
18
+ "keywords": [
19
+ "mcp",
20
+ "claude",
21
+ "claude-code",
22
+ "browser",
23
+ "automation",
24
+ "chrome"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "ws": "^8.18.0",
33
+ "zod": "^3.22.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "@types/ws": "^8.5.0",
38
+ "tsup": "^8.0.0",
39
+ "typescript": "^5.5.0"
40
+ }
41
+ }