rechrome 1.2.0 → 1.3.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 +0 -3
- package/README.md +0 -5
- package/package.json +1 -1
- package/rech.js +2 -44
- package/rech.ts +2 -44
- package/serve.js +0 -15
- package/serve.ts +0 -15
package/.env.example
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
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
4
|
# Custom playwright-cli binary path (default: playwright-cli)
|
|
8
5
|
# For full multi-tab & multi-session support, use playwright-cli-multi-tab:
|
|
9
6
|
# PLAYWRIGHT_CLI=playwright-cli-multi-tab
|
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,7 +78,6 @@ 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
83
|
| `PLAYWRIGHT_MCP_EXTENSION_ID` | Chrome extension ID (client overrides server) | — |
|
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -35,40 +35,6 @@ 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",
|
|
@@ -164,10 +130,6 @@ function getClientEnv(): Record<string, string> {
|
|
|
164
130
|
async function run(url: string, args: string[]) {
|
|
165
131
|
const { key, host, port } = parseUrl(url);
|
|
166
132
|
|
|
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
133
|
const identity = await getClientIdentity();
|
|
172
134
|
console.error(
|
|
173
135
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -175,7 +137,7 @@ async function run(url: string, args: string[]) {
|
|
|
175
137
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
176
138
|
method: "POST",
|
|
177
139
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
178
|
-
body: JSON.stringify({ args
|
|
140
|
+
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
179
141
|
signal: AbortSignal.timeout(70_000),
|
|
180
142
|
}).catch((e) => {
|
|
181
143
|
console.error(`[rech] ${e.message}`);
|
|
@@ -187,12 +149,11 @@ async function run(url: string, args: string[]) {
|
|
|
187
149
|
process.exit(1);
|
|
188
150
|
}
|
|
189
151
|
|
|
190
|
-
const { status, stdout, stderr, files,
|
|
152
|
+
const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
|
|
191
153
|
status: number;
|
|
192
154
|
stdout: string;
|
|
193
155
|
stderr: string;
|
|
194
156
|
files?: string[];
|
|
195
|
-
descriptions?: Record<string, string>;
|
|
196
157
|
existingSession?: boolean;
|
|
197
158
|
};
|
|
198
159
|
|
|
@@ -217,9 +178,6 @@ async function run(url: string, args: string[]) {
|
|
|
217
178
|
const dest = join(dlDir, basename(name));
|
|
218
179
|
await Bun.write(dest, fileRes);
|
|
219
180
|
console.error(`[rech] downloaded: ${dest}`);
|
|
220
|
-
if (descriptions?.[name]) {
|
|
221
|
-
console.error(`[rech] vision: ${descriptions[name]}`);
|
|
222
|
-
}
|
|
223
181
|
}
|
|
224
182
|
}
|
|
225
183
|
|
package/rech.ts
CHANGED
|
@@ -35,40 +35,6 @@ 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",
|
|
@@ -164,10 +130,6 @@ function getClientEnv(): Record<string, string> {
|
|
|
164
130
|
async function run(url: string, args: string[]) {
|
|
165
131
|
const { key, host, port } = parseUrl(url);
|
|
166
132
|
|
|
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
133
|
const identity = await getClientIdentity();
|
|
172
134
|
console.error(
|
|
173
135
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -175,7 +137,7 @@ async function run(url: string, args: string[]) {
|
|
|
175
137
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
176
138
|
method: "POST",
|
|
177
139
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
178
|
-
body: JSON.stringify({ args
|
|
140
|
+
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
179
141
|
signal: AbortSignal.timeout(70_000),
|
|
180
142
|
}).catch((e) => {
|
|
181
143
|
console.error(`[rech] ${e.message}`);
|
|
@@ -187,12 +149,11 @@ async function run(url: string, args: string[]) {
|
|
|
187
149
|
process.exit(1);
|
|
188
150
|
}
|
|
189
151
|
|
|
190
|
-
const { status, stdout, stderr, files,
|
|
152
|
+
const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
|
|
191
153
|
status: number;
|
|
192
154
|
stdout: string;
|
|
193
155
|
stderr: string;
|
|
194
156
|
files?: string[];
|
|
195
|
-
descriptions?: Record<string, string>;
|
|
196
157
|
existingSession?: boolean;
|
|
197
158
|
};
|
|
198
159
|
|
|
@@ -217,9 +178,6 @@ async function run(url: string, args: string[]) {
|
|
|
217
178
|
const dest = join(dlDir, basename(name));
|
|
218
179
|
await Bun.write(dest, fileRes);
|
|
219
180
|
console.error(`[rech] downloaded: ${dest}`);
|
|
220
|
-
if (descriptions?.[name]) {
|
|
221
|
-
console.error(`[rech] vision: ${descriptions[name]}`);
|
|
222
|
-
}
|
|
223
181
|
}
|
|
224
182
|
}
|
|
225
183
|
|
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 = "";
|
|
@@ -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 = "";
|
|
@@ -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
|
});
|