rechrome 1.1.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 +7 -9
- package/package.json +12 -12
- package/rech.js +8 -40
- package/rech.ts +8 -40
- package/serve.js +8 -11
- package/serve.ts +8 -11
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** — optional Gemini-powered screenshot descriptions
|
|
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
|
|
|
@@ -76,14 +75,13 @@ Copy `.env.example` to `.env.local` and edit:
|
|
|
76
75
|
cp .env.example .env.local
|
|
77
76
|
```
|
|
78
77
|
|
|
79
|
-
| Variable
|
|
80
|
-
|
|
81
|
-
| `REMOTE_CHROME_URL`
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server) | — |
|
|
78
|
+
| Variable | Description | Default |
|
|
79
|
+
| -------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------- |
|
|
80
|
+
| `REMOTE_CHROME_URL` | Connection URL (auto-generated by `rechrome serve`) | — |
|
|
81
|
+
| `PLAYWRIGHT_CLI` | Path to playwright-cli binary (recommended: `playwright-cli-multi-tab` for full multi-tab support) | `playwright-cli` |
|
|
82
|
+
| `RECH_HOST` | Server bind address | `127.0.0.1` |
|
|
83
|
+
| `PLAYWRIGHT_MCP_EXTENSION_ID` | Chrome extension ID (client overrides server) | — |
|
|
84
|
+
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server) | — |
|
|
87
85
|
|
|
88
86
|
### Remote access
|
|
89
87
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rechrome",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "1.3.0",
|
|
5
4
|
"repository": {
|
|
6
5
|
"type": "git",
|
|
7
6
|
"url": "https://github.com/snomiao/rechrome.git"
|
|
@@ -10,6 +9,16 @@
|
|
|
10
9
|
"rech": "./rech.js",
|
|
11
10
|
"rechrome": "./rech.js"
|
|
12
11
|
},
|
|
12
|
+
"files": [
|
|
13
|
+
".env.example",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
16
|
+
"rech.js",
|
|
17
|
+
"rech.ts",
|
|
18
|
+
"serve.js",
|
|
19
|
+
"serve.ts"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
13
22
|
"scripts": {
|
|
14
23
|
"serve": "bun run rech.ts serve",
|
|
15
24
|
"test": "bun test",
|
|
@@ -22,14 +31,5 @@
|
|
|
22
31
|
"branches": [
|
|
23
32
|
"main"
|
|
24
33
|
]
|
|
25
|
-
}
|
|
26
|
-
"files": [
|
|
27
|
-
"rech.ts",
|
|
28
|
-
"rech.js",
|
|
29
|
-
"serve.ts",
|
|
30
|
-
"serve.js",
|
|
31
|
-
".env.example",
|
|
32
|
-
"README.md",
|
|
33
|
-
"LICENSE"
|
|
34
|
-
]
|
|
34
|
+
}
|
|
35
35
|
}
|
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",
|
|
@@ -141,7 +107,12 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
141
107
|
}
|
|
142
108
|
if (branch) gitUrl += `/tree/${branch}`;
|
|
143
109
|
// Strip any embedded credentials from the URL
|
|
144
|
-
try {
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(gitUrl);
|
|
112
|
+
u.username = "";
|
|
113
|
+
u.password = "";
|
|
114
|
+
gitUrl = u.toString();
|
|
115
|
+
} catch {}
|
|
145
116
|
return { gitUrl };
|
|
146
117
|
}
|
|
147
118
|
} catch {}
|
|
@@ -158,6 +129,7 @@ function getClientEnv(): Record<string, string> {
|
|
|
158
129
|
|
|
159
130
|
async function run(url: string, args: string[]) {
|
|
160
131
|
const { key, host, port } = parseUrl(url);
|
|
132
|
+
|
|
161
133
|
const identity = await getClientIdentity();
|
|
162
134
|
console.error(
|
|
163
135
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -177,12 +149,11 @@ async function run(url: string, args: string[]) {
|
|
|
177
149
|
process.exit(1);
|
|
178
150
|
}
|
|
179
151
|
|
|
180
|
-
const { status, stdout, stderr, files,
|
|
152
|
+
const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
|
|
181
153
|
status: number;
|
|
182
154
|
stdout: string;
|
|
183
155
|
stderr: string;
|
|
184
156
|
files?: string[];
|
|
185
|
-
descriptions?: Record<string, string>;
|
|
186
157
|
existingSession?: boolean;
|
|
187
158
|
};
|
|
188
159
|
|
|
@@ -207,9 +178,6 @@ async function run(url: string, args: string[]) {
|
|
|
207
178
|
const dest = join(dlDir, basename(name));
|
|
208
179
|
await Bun.write(dest, fileRes);
|
|
209
180
|
console.error(`[rech] downloaded: ${dest}`);
|
|
210
|
-
if (descriptions?.[name]) {
|
|
211
|
-
console.error(`[rech] vision: ${descriptions[name]}`);
|
|
212
|
-
}
|
|
213
181
|
}
|
|
214
182
|
}
|
|
215
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",
|
|
@@ -141,7 +107,12 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
141
107
|
}
|
|
142
108
|
if (branch) gitUrl += `/tree/${branch}`;
|
|
143
109
|
// Strip any embedded credentials from the URL
|
|
144
|
-
try {
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(gitUrl);
|
|
112
|
+
u.username = "";
|
|
113
|
+
u.password = "";
|
|
114
|
+
gitUrl = u.toString();
|
|
115
|
+
} catch {}
|
|
145
116
|
return { gitUrl };
|
|
146
117
|
}
|
|
147
118
|
} catch {}
|
|
@@ -158,6 +129,7 @@ function getClientEnv(): Record<string, string> {
|
|
|
158
129
|
|
|
159
130
|
async function run(url: string, args: string[]) {
|
|
160
131
|
const { key, host, port } = parseUrl(url);
|
|
132
|
+
|
|
161
133
|
const identity = await getClientIdentity();
|
|
162
134
|
console.error(
|
|
163
135
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -177,12 +149,11 @@ async function run(url: string, args: string[]) {
|
|
|
177
149
|
process.exit(1);
|
|
178
150
|
}
|
|
179
151
|
|
|
180
|
-
const { status, stdout, stderr, files,
|
|
152
|
+
const { status, stdout, stderr, files, existingSession } = (await res.json()) as {
|
|
181
153
|
status: number;
|
|
182
154
|
stdout: string;
|
|
183
155
|
stderr: string;
|
|
184
156
|
files?: string[];
|
|
185
|
-
descriptions?: Record<string, string>;
|
|
186
157
|
existingSession?: boolean;
|
|
187
158
|
};
|
|
188
159
|
|
|
@@ -207,9 +178,6 @@ async function run(url: string, args: string[]) {
|
|
|
207
178
|
const dest = join(dlDir, basename(name));
|
|
208
179
|
await Bun.write(dest, fileRes);
|
|
209
180
|
console.error(`[rech] downloaded: ${dest}`);
|
|
210
|
-
if (descriptions?.[name]) {
|
|
211
|
-
console.error(`[rech] vision: ${descriptions[name]}`);
|
|
212
|
-
}
|
|
213
181
|
}
|
|
214
182
|
}
|
|
215
183
|
|
package/serve.js
CHANGED
|
@@ -2,7 +2,14 @@ import { file } from "bun";
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { mkdirSync } from "fs";
|
|
4
4
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
log,
|
|
7
|
+
parseUrl,
|
|
8
|
+
getOrCreateUrl,
|
|
9
|
+
authCheck,
|
|
10
|
+
RECH_DIR,
|
|
11
|
+
PASSTHROUGH_ENV_KEYS,
|
|
12
|
+
} from "./rech.js";
|
|
6
13
|
|
|
7
14
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
15
|
const absBase = resolve(base) + "/";
|
|
@@ -192,22 +199,12 @@ export async function serve() {
|
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
// Auto-describe screenshot files with Gemini vision
|
|
196
|
-
const descriptions: Record<string, string> = {};
|
|
197
|
-
for (const f of outputFiles) {
|
|
198
|
-
if (/\.(?:png|jpe?g)$/i.test(f)) {
|
|
199
|
-
const desc = await describeImage(join(workDir, f));
|
|
200
|
-
if (desc) descriptions[f] = desc;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
202
|
const rebrand = (s: string) => s.replaceAll("npx playwright-cli", "rech");
|
|
205
203
|
return Response.json({
|
|
206
204
|
status,
|
|
207
205
|
stdout: rebrand(stdout),
|
|
208
206
|
stderr: rebrand(stderr),
|
|
209
207
|
files: outputFiles,
|
|
210
|
-
descriptions,
|
|
211
208
|
});
|
|
212
209
|
},
|
|
213
210
|
});
|
package/serve.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { file } from "bun";
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { mkdirSync } from "fs";
|
|
4
4
|
import { join, resolve, relative, isAbsolute } from "path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
log,
|
|
7
|
+
parseUrl,
|
|
8
|
+
getOrCreateUrl,
|
|
9
|
+
authCheck,
|
|
10
|
+
RECH_DIR,
|
|
11
|
+
PASSTHROUGH_ENV_KEYS,
|
|
12
|
+
} from "./rech.ts";
|
|
6
13
|
|
|
7
14
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
15
|
const absBase = resolve(base) + "/";
|
|
@@ -192,22 +199,12 @@ export async function serve() {
|
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
// Auto-describe screenshot files with Gemini vision
|
|
196
|
-
const descriptions: Record<string, string> = {};
|
|
197
|
-
for (const f of outputFiles) {
|
|
198
|
-
if (/\.(?:png|jpe?g)$/i.test(f)) {
|
|
199
|
-
const desc = await describeImage(join(workDir, f));
|
|
200
|
-
if (desc) descriptions[f] = desc;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
202
|
const rebrand = (s: string) => s.replaceAll("npx playwright-cli", "rech");
|
|
205
203
|
return Response.json({
|
|
206
204
|
status,
|
|
207
205
|
stdout: rebrand(stdout),
|
|
208
206
|
stderr: rebrand(stderr),
|
|
209
207
|
files: outputFiles,
|
|
210
|
-
descriptions,
|
|
211
208
|
});
|
|
212
209
|
},
|
|
213
210
|
});
|