rechrome 1.2.0 → 1.4.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.
package/.env.example CHANGED
@@ -1,11 +1,9 @@
1
1
  # Remote Chrome connection URL (auto-generated by `rech serve` if not set)
2
2
  # REMOTE_CHROME_URL=remote-chrome://key@host:13775
3
3
 
4
- # Gemini API key for screenshot vision descriptions (optional)
5
- # GEMINI_API_KEY=your-gemini-api-key
6
-
7
- # Custom playwright-cli binary path (default: playwright-cli)
8
- # For full multi-tab & multi-session support, use playwright-cli-multi-tab:
4
+ # Custom playwright-cli binary (default: playwright-cli). Supports space-separated runtime prefix.
5
+ # For global install: PLAYWRIGHT_CLI=playwright-cli-multi-tab
6
+ # For local submodule (with bun): PLAYWRIGHT_CLI=bun ./lib/playwright-cli/playwright-cli.js
9
7
  # PLAYWRIGHT_CLI=playwright-cli-multi-tab
10
8
 
11
9
  # Bind address for the server (default: 127.0.0.1, use 0.0.0.0 for remote access)
@@ -14,3 +12,10 @@
14
12
  # Playwright MCP extension settings (can be set on server or client; client overrides server)
15
13
  # PLAYWRIGHT_MCP_EXTENSION_ID=your-extension-id
16
14
  # PLAYWRIGHT_MCP_EXTENSION_TOKEN=your-extension-token
15
+
16
+ # Chrome profile to use when connecting via --extension (macOS/Windows multi-profile setups)
17
+ # Set USER_DATA_DIR to Chrome's user data directory, and PROFILE_DIRECTORY to the sub-profile name.
18
+ # macOS default: ~/Library/Application Support/Google/Chrome
19
+ # Profile names: Default, "Profile 1", "Profile 2", etc. (check chrome://version for yours)
20
+ # PLAYWRIGHT_MCP_USER_DATA_DIR=/Users/yourname/Library/Application Support/Google/Chrome
21
+ # PLAYWRIGHT_MCP_PROFILE_DIRECTORY=Profile 2
package/README.md CHANGED
@@ -10,7 +10,6 @@ Built on top of [playwright-multi-tab](https://github.com/snomiao/playwright-mul
10
10
 
11
11
  - **Session isolation** — clients are automatically namespaced by git repo or hostname
12
12
  - **File transfer** — screenshots and PDFs are automatically downloaded to the client
13
- - **Vision descriptions** — opt-in Gemini-powered screenshot descriptions (`--gemini-vision`)
14
13
  - **Hot-reload config** — `.env.local` changes are picked up without restart
15
14
  - **Security** — bearer auth, path traversal protection, env allowlisting for child processes
16
15
 
@@ -61,9 +60,6 @@ rechrome open https://example.com
61
60
  # Take a screenshot
62
61
  rechrome screenshot
63
62
 
64
- # Take a screenshot with Gemini vision description
65
- rechrome screenshot --gemini-vision
66
-
67
63
  # List open tabs
68
64
  rechrome tab-list
69
65
 
@@ -82,11 +78,19 @@ cp .env.example .env.local
82
78
  | Variable | Description | Default |
83
79
  | -------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------- |
84
80
  | `REMOTE_CHROME_URL` | Connection URL (auto-generated by `rechrome serve`) | — |
85
- | `GEMINI_API_KEY` | Gemini API key for `--gemini-vision` screenshot descriptions | — |
86
81
  | `PLAYWRIGHT_CLI` | Path to playwright-cli binary (recommended: `playwright-cli-multi-tab` for full multi-tab support) | `playwright-cli` |
87
82
  | `RECH_HOST` | Server bind address | `127.0.0.1` |
88
- | `PLAYWRIGHT_MCP_EXTENSION_ID` | Chrome extension ID (client overrides server) | — |
89
- | `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server) | — |
83
+ | `PLAYWRIGHT_MCP_EXTENSION_ID` | Chrome extension ID (client overrides server) | — |
84
+ | `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server) | — |
85
+ | `PLAYWRIGHT_MCP_USER_DATA_DIR` | Chrome user data directory — use to pin connections to a specific Chrome install (client overrides server) | — |
86
+ | `PLAYWRIGHT_MCP_PROFILE_DIRECTORY` | Chrome profile sub-directory (e.g. `Profile 2`) — use when multiple profiles share the same user data dir (client overrides server) | — |
87
+
88
+ > **Multi-profile tip:** If you have multiple Chrome profiles open and only one has the Playwright MCP Bridge extension, set both vars so the extension connection always targets the correct profile:
89
+ > ```
90
+ > PLAYWRIGHT_MCP_USER_DATA_DIR=/Users/yourname/Library/Application Support/Google/Chrome
91
+ > PLAYWRIGHT_MCP_PROFILE_DIRECTORY=Profile 2
92
+ > ```
93
+ > To find your profile directory name, open `chrome://version` in the target Chrome profile and look for **Profile Path**.
90
94
 
91
95
  ### Remote access
92
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rech.js CHANGED
@@ -35,44 +35,12 @@ if (existsSync(envFile)) {
35
35
  });
36
36
  }
37
37
 
38
- /** Describe an image using Gemini vision API. Returns description or null on failure. */
39
- export async function describeImage(imagePath: string): Promise<string | null> {
40
- const apiKey = process.env.GEMINI_API_KEY;
41
- if (!apiKey) return null;
42
- try {
43
- const imageData = await file(imagePath).arrayBuffer();
44
- const base64 = Buffer.from(imageData).toString("base64");
45
- const mimeType = imagePath.endsWith(".png") ? "image/png" : "image/jpeg";
46
- const res = await fetch(
47
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:generateContent?key=${apiKey}`,
48
- {
49
- method: "POST",
50
- headers: { "Content-Type": "application/json" },
51
- body: JSON.stringify({
52
- contents: [
53
- {
54
- parts: [
55
- {
56
- text: "Describe this browser screenshot concisely in 2-3 sentences. Focus on what's visible: page layout, content, any errors or issues.",
57
- },
58
- { inline_data: { mime_type: mimeType, data: base64 } },
59
- ],
60
- },
61
- ],
62
- }),
63
- },
64
- );
65
- if (!res.ok) return null;
66
- const json = await res.json();
67
- return json.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null;
68
- } catch {
69
- return null;
70
- }
71
- }
72
38
 
73
39
  export const PASSTHROUGH_ENV_KEYS = [
74
40
  "PLAYWRIGHT_MCP_EXTENSION_ID",
75
41
  "PLAYWRIGHT_MCP_EXTENSION_TOKEN",
42
+ "PLAYWRIGHT_MCP_USER_DATA_DIR",
43
+ "PLAYWRIGHT_MCP_PROFILE_DIRECTORY",
76
44
  ] as const;
77
45
 
78
46
  export function log(msg: string) {
@@ -164,10 +132,6 @@ function getClientEnv(): Record<string, string> {
164
132
  async function run(url: string, args: string[]) {
165
133
  const { key, host, port } = parseUrl(url);
166
134
 
167
- // Extract --gemini-vision flag (not forwarded to server args)
168
- const geminiVision = args.includes("--gemini-vision");
169
- const filteredArgs = args.filter((a) => a !== "--gemini-vision");
170
-
171
135
  const identity = await getClientIdentity();
172
136
  console.error(
173
137
  `[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
@@ -175,7 +139,7 @@ async function run(url: string, args: string[]) {
175
139
  const res = await fetch(`http://${host}:${port}/run`, {
176
140
  method: "POST",
177
141
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
178
- body: JSON.stringify({ args: filteredArgs, identity, env: getClientEnv(), geminiVision }),
142
+ body: JSON.stringify({ args, identity, env: getClientEnv() }),
179
143
  signal: AbortSignal.timeout(70_000),
180
144
  }).catch((e) => {
181
145
  console.error(`[rech] ${e.message}`);
@@ -187,12 +151,11 @@ async function run(url: string, args: string[]) {
187
151
  process.exit(1);
188
152
  }
189
153
 
190
- const { status, stdout, stderr, files, descriptions, existingSession } = (await res.json()) as {
154
+ const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
191
155
  status: number;
192
156
  stdout: string;
193
157
  stderr: string;
194
158
  files?: string[];
195
- descriptions?: Record<string, string>;
196
159
  existingSession?: boolean;
197
160
  };
198
161
 
@@ -217,9 +180,6 @@ async function run(url: string, args: string[]) {
217
180
  const dest = join(dlDir, basename(name));
218
181
  await Bun.write(dest, fileRes);
219
182
  console.error(`[rech] downloaded: ${dest}`);
220
- if (descriptions?.[name]) {
221
- console.error(`[rech] vision: ${descriptions[name]}`);
222
- }
223
183
  }
224
184
  }
225
185
 
package/rech.ts CHANGED
@@ -35,44 +35,12 @@ if (existsSync(envFile)) {
35
35
  });
36
36
  }
37
37
 
38
- /** Describe an image using Gemini vision API. Returns description or null on failure. */
39
- export async function describeImage(imagePath: string): Promise<string | null> {
40
- const apiKey = process.env.GEMINI_API_KEY;
41
- if (!apiKey) return null;
42
- try {
43
- const imageData = await file(imagePath).arrayBuffer();
44
- const base64 = Buffer.from(imageData).toString("base64");
45
- const mimeType = imagePath.endsWith(".png") ? "image/png" : "image/jpeg";
46
- const res = await fetch(
47
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:generateContent?key=${apiKey}`,
48
- {
49
- method: "POST",
50
- headers: { "Content-Type": "application/json" },
51
- body: JSON.stringify({
52
- contents: [
53
- {
54
- parts: [
55
- {
56
- text: "Describe this browser screenshot concisely in 2-3 sentences. Focus on what's visible: page layout, content, any errors or issues.",
57
- },
58
- { inline_data: { mime_type: mimeType, data: base64 } },
59
- ],
60
- },
61
- ],
62
- }),
63
- },
64
- );
65
- if (!res.ok) return null;
66
- const json = await res.json();
67
- return json.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null;
68
- } catch {
69
- return null;
70
- }
71
- }
72
38
 
73
39
  export const PASSTHROUGH_ENV_KEYS = [
74
40
  "PLAYWRIGHT_MCP_EXTENSION_ID",
75
41
  "PLAYWRIGHT_MCP_EXTENSION_TOKEN",
42
+ "PLAYWRIGHT_MCP_USER_DATA_DIR",
43
+ "PLAYWRIGHT_MCP_PROFILE_DIRECTORY",
76
44
  ] as const;
77
45
 
78
46
  export function log(msg: string) {
@@ -164,10 +132,6 @@ function getClientEnv(): Record<string, string> {
164
132
  async function run(url: string, args: string[]) {
165
133
  const { key, host, port } = parseUrl(url);
166
134
 
167
- // Extract --gemini-vision flag (not forwarded to server args)
168
- const geminiVision = args.includes("--gemini-vision");
169
- const filteredArgs = args.filter((a) => a !== "--gemini-vision");
170
-
171
135
  const identity = await getClientIdentity();
172
136
  console.error(
173
137
  `[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
@@ -175,7 +139,7 @@ async function run(url: string, args: string[]) {
175
139
  const res = await fetch(`http://${host}:${port}/run`, {
176
140
  method: "POST",
177
141
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
178
- body: JSON.stringify({ args: filteredArgs, identity, env: getClientEnv(), geminiVision }),
142
+ body: JSON.stringify({ args, identity, env: getClientEnv() }),
179
143
  signal: AbortSignal.timeout(70_000),
180
144
  }).catch((e) => {
181
145
  console.error(`[rech] ${e.message}`);
@@ -187,12 +151,11 @@ async function run(url: string, args: string[]) {
187
151
  process.exit(1);
188
152
  }
189
153
 
190
- const { status, stdout, stderr, files, descriptions, existingSession } = (await res.json()) as {
154
+ const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
191
155
  status: number;
192
156
  stdout: string;
193
157
  stderr: string;
194
158
  files?: string[];
195
- descriptions?: Record<string, string>;
196
159
  existingSession?: boolean;
197
160
  };
198
161
 
@@ -217,9 +180,6 @@ async function run(url: string, args: string[]) {
217
180
  const dest = join(dlDir, basename(name));
218
181
  await Bun.write(dest, fileRes);
219
182
  console.error(`[rech] downloaded: ${dest}`);
220
- if (descriptions?.[name]) {
221
- console.error(`[rech] vision: ${descriptions[name]}`);
222
- }
223
183
  }
224
184
  }
225
185
 
package/serve.js CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  parseUrl,
8
8
  getOrCreateUrl,
9
9
  authCheck,
10
- describeImage,
11
10
  RECH_DIR,
12
11
  PASSTHROUGH_ENV_KEYS,
13
12
  } from "./rech.js";
@@ -53,7 +52,6 @@ export async function serve() {
53
52
  let sessionId: string;
54
53
  let clientName = "";
55
54
  let clientEnv: Record<string, string> = {};
56
- let geminiVision = false;
57
55
  if (Array.isArray(body)) {
58
56
  args = body;
59
57
  const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
@@ -82,7 +80,6 @@ export async function serve() {
82
80
  if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
83
81
  }
84
82
  }
85
- if (body.geminiVision) geminiVision = true;
86
83
  }
87
84
 
88
85
  let clientSession = "";
@@ -96,7 +93,7 @@ export async function serve() {
96
93
  });
97
94
  const namespacedSession = clientSession ? `${sessionId}-${clientSession}` : sessionId;
98
95
 
99
- const bin = process.env.PLAYWRIGHT_CLI || "playwright-cli";
96
+ const [bin, ...binArgs] = (process.env.PLAYWRIGHT_CLI || "playwright-cli").split(" ");
100
97
 
101
98
  if (filteredArgs.length === 0) {
102
99
  filteredArgs.push("--help");
@@ -108,7 +105,7 @@ export async function serve() {
108
105
  const isOpenCmd = filteredArgs[0] === "open";
109
106
  if (isOpenCmd) {
110
107
  try {
111
- const listProc = Bun.spawn([bin, "tab-list", "--extension", `-s=${namespacedSession}`], {
108
+ const listProc = Bun.spawn([bin, ...binArgs, "tab-list", "--extension", `-s=${namespacedSession}`], {
112
109
  cwd: workDir,
113
110
  stdin: "ignore",
114
111
  stdout: "pipe",
@@ -150,7 +147,7 @@ export async function serve() {
150
147
  ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
151
148
  ...passthroughEnv,
152
149
  };
153
- const proc = Bun.spawn([bin, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
150
+ const proc = Bun.spawn([bin, ...binArgs, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
154
151
  cwd: workDir,
155
152
  stdin: "ignore",
156
153
  stdout: "pipe",
@@ -202,24 +199,12 @@ export async function serve() {
202
199
  }
203
200
  }
204
201
 
205
- // Auto-describe screenshot files with Gemini vision (opt-in via --gemini-vision)
206
- const descriptions: Record<string, string> = {};
207
- if (geminiVision) {
208
- for (const f of outputFiles) {
209
- if (/\.(?:png|jpe?g)$/i.test(f)) {
210
- const desc = await describeImage(join(workDir, f));
211
- if (desc) descriptions[f] = desc;
212
- }
213
- }
214
- }
215
-
216
202
  const rebrand = (s: string) => s.replaceAll("npx playwright-cli", "rech");
217
203
  return Response.json({
218
204
  status,
219
205
  stdout: rebrand(stdout),
220
206
  stderr: rebrand(stderr),
221
207
  files: outputFiles,
222
- descriptions,
223
208
  });
224
209
  },
225
210
  });
package/serve.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  parseUrl,
8
8
  getOrCreateUrl,
9
9
  authCheck,
10
- describeImage,
11
10
  RECH_DIR,
12
11
  PASSTHROUGH_ENV_KEYS,
13
12
  } from "./rech.ts";
@@ -53,7 +52,6 @@ export async function serve() {
53
52
  let sessionId: string;
54
53
  let clientName = "";
55
54
  let clientEnv: Record<string, string> = {};
56
- let geminiVision = false;
57
55
  if (Array.isArray(body)) {
58
56
  args = body;
59
57
  const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
@@ -82,7 +80,6 @@ export async function serve() {
82
80
  if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
83
81
  }
84
82
  }
85
- if (body.geminiVision) geminiVision = true;
86
83
  }
87
84
 
88
85
  let clientSession = "";
@@ -96,7 +93,7 @@ export async function serve() {
96
93
  });
97
94
  const namespacedSession = clientSession ? `${sessionId}-${clientSession}` : sessionId;
98
95
 
99
- const bin = process.env.PLAYWRIGHT_CLI || "playwright-cli";
96
+ const [bin, ...binArgs] = (process.env.PLAYWRIGHT_CLI || "playwright-cli").split(" ");
100
97
 
101
98
  if (filteredArgs.length === 0) {
102
99
  filteredArgs.push("--help");
@@ -108,7 +105,7 @@ export async function serve() {
108
105
  const isOpenCmd = filteredArgs[0] === "open";
109
106
  if (isOpenCmd) {
110
107
  try {
111
- const listProc = Bun.spawn([bin, "tab-list", "--extension", `-s=${namespacedSession}`], {
108
+ const listProc = Bun.spawn([bin, ...binArgs, "tab-list", "--extension", `-s=${namespacedSession}`], {
112
109
  cwd: workDir,
113
110
  stdin: "ignore",
114
111
  stdout: "pipe",
@@ -150,7 +147,7 @@ export async function serve() {
150
147
  ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
151
148
  ...passthroughEnv,
152
149
  };
153
- const proc = Bun.spawn([bin, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
150
+ const proc = Bun.spawn([bin, ...binArgs, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
154
151
  cwd: workDir,
155
152
  stdin: "ignore",
156
153
  stdout: "pipe",
@@ -202,24 +199,12 @@ export async function serve() {
202
199
  }
203
200
  }
204
201
 
205
- // Auto-describe screenshot files with Gemini vision (opt-in via --gemini-vision)
206
- const descriptions: Record<string, string> = {};
207
- if (geminiVision) {
208
- for (const f of outputFiles) {
209
- if (/\.(?:png|jpe?g)$/i.test(f)) {
210
- const desc = await describeImage(join(workDir, f));
211
- if (desc) descriptions[f] = desc;
212
- }
213
- }
214
- }
215
-
216
202
  const rebrand = (s: string) => s.replaceAll("npx playwright-cli", "rech");
217
203
  return Response.json({
218
204
  status,
219
205
  stdout: rebrand(stdout),
220
206
  stderr: rebrand(stderr),
221
207
  files: outputFiles,
222
- descriptions,
223
208
  });
224
209
  },
225
210
  });