rechrome 1.1.0 → 1.2.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/README.md +12 -9
- package/package.json +12 -12
- package/rech.js +12 -2
- package/rech.ts +12 -2
- package/serve.js +18 -6
- package/serve.ts +18 -6
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ 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** —
|
|
13
|
+
- **Vision descriptions** — opt-in Gemini-powered screenshot descriptions (`--gemini-vision`)
|
|
14
14
|
- **Hot-reload config** — `.env.local` changes are picked up without restart
|
|
15
15
|
- **Security** — bearer auth, path traversal protection, env allowlisting for child processes
|
|
16
16
|
|
|
@@ -61,6 +61,9 @@ rechrome open https://example.com
|
|
|
61
61
|
# Take a screenshot
|
|
62
62
|
rechrome screenshot
|
|
63
63
|
|
|
64
|
+
# Take a screenshot with Gemini vision description
|
|
65
|
+
rechrome screenshot --gemini-vision
|
|
66
|
+
|
|
64
67
|
# List open tabs
|
|
65
68
|
rechrome tab-list
|
|
66
69
|
|
|
@@ -76,14 +79,14 @@ Copy `.env.example` to `.env.local` and edit:
|
|
|
76
79
|
cp .env.example .env.local
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
| Variable
|
|
80
|
-
|
|
81
|
-
| `REMOTE_CHROME_URL`
|
|
82
|
-
| `GEMINI_API_KEY`
|
|
83
|
-
| `PLAYWRIGHT_CLI`
|
|
84
|
-
| `RECH_HOST`
|
|
85
|
-
| `PLAYWRIGHT_MCP_EXTENSION_ID`
|
|
86
|
-
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server)
|
|
82
|
+
| Variable | Description | Default |
|
|
83
|
+
| -------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------- |
|
|
84
|
+
| `REMOTE_CHROME_URL` | Connection URL (auto-generated by `rechrome serve`) | — |
|
|
85
|
+
| `GEMINI_API_KEY` | Gemini API key for `--gemini-vision` screenshot descriptions | — |
|
|
86
|
+
| `PLAYWRIGHT_CLI` | Path to playwright-cli binary (recommended: `playwright-cli-multi-tab` for full multi-tab support) | `playwright-cli` |
|
|
87
|
+
| `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) | — |
|
|
87
90
|
|
|
88
91
|
### Remote access
|
|
89
92
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rechrome",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "1.2.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
|
@@ -141,7 +141,12 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
141
141
|
}
|
|
142
142
|
if (branch) gitUrl += `/tree/${branch}`;
|
|
143
143
|
// Strip any embedded credentials from the URL
|
|
144
|
-
try {
|
|
144
|
+
try {
|
|
145
|
+
const u = new URL(gitUrl);
|
|
146
|
+
u.username = "";
|
|
147
|
+
u.password = "";
|
|
148
|
+
gitUrl = u.toString();
|
|
149
|
+
} catch {}
|
|
145
150
|
return { gitUrl };
|
|
146
151
|
}
|
|
147
152
|
} catch {}
|
|
@@ -158,6 +163,11 @@ function getClientEnv(): Record<string, string> {
|
|
|
158
163
|
|
|
159
164
|
async function run(url: string, args: string[]) {
|
|
160
165
|
const { key, host, port } = parseUrl(url);
|
|
166
|
+
|
|
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
|
+
|
|
161
171
|
const identity = await getClientIdentity();
|
|
162
172
|
console.error(
|
|
163
173
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -165,7 +175,7 @@ async function run(url: string, args: string[]) {
|
|
|
165
175
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
166
176
|
method: "POST",
|
|
167
177
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
168
|
-
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
178
|
+
body: JSON.stringify({ args: filteredArgs, identity, env: getClientEnv(), geminiVision }),
|
|
169
179
|
signal: AbortSignal.timeout(70_000),
|
|
170
180
|
}).catch((e) => {
|
|
171
181
|
console.error(`[rech] ${e.message}`);
|
package/rech.ts
CHANGED
|
@@ -141,7 +141,12 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
141
141
|
}
|
|
142
142
|
if (branch) gitUrl += `/tree/${branch}`;
|
|
143
143
|
// Strip any embedded credentials from the URL
|
|
144
|
-
try {
|
|
144
|
+
try {
|
|
145
|
+
const u = new URL(gitUrl);
|
|
146
|
+
u.username = "";
|
|
147
|
+
u.password = "";
|
|
148
|
+
gitUrl = u.toString();
|
|
149
|
+
} catch {}
|
|
145
150
|
return { gitUrl };
|
|
146
151
|
}
|
|
147
152
|
} catch {}
|
|
@@ -158,6 +163,11 @@ function getClientEnv(): Record<string, string> {
|
|
|
158
163
|
|
|
159
164
|
async function run(url: string, args: string[]) {
|
|
160
165
|
const { key, host, port } = parseUrl(url);
|
|
166
|
+
|
|
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
|
+
|
|
161
171
|
const identity = await getClientIdentity();
|
|
162
172
|
console.error(
|
|
163
173
|
`[rech] connecting to ${host}:${port} (identity: ${identity.gitUrl || `${identity.hostname}:${identity.cwd}`})`,
|
|
@@ -165,7 +175,7 @@ async function run(url: string, args: string[]) {
|
|
|
165
175
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
166
176
|
method: "POST",
|
|
167
177
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
168
|
-
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
178
|
+
body: JSON.stringify({ args: filteredArgs, identity, env: getClientEnv(), geminiVision }),
|
|
169
179
|
signal: AbortSignal.timeout(70_000),
|
|
170
180
|
}).catch((e) => {
|
|
171
181
|
console.error(`[rech] ${e.message}`);
|
package/serve.js
CHANGED
|
@@ -2,7 +2,15 @@ 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
|
+
describeImage,
|
|
11
|
+
RECH_DIR,
|
|
12
|
+
PASSTHROUGH_ENV_KEYS,
|
|
13
|
+
} from "./rech.js";
|
|
6
14
|
|
|
7
15
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
16
|
const absBase = resolve(base) + "/";
|
|
@@ -45,6 +53,7 @@ export async function serve() {
|
|
|
45
53
|
let sessionId: string;
|
|
46
54
|
let clientName = "";
|
|
47
55
|
let clientEnv: Record<string, string> = {};
|
|
56
|
+
let geminiVision = false;
|
|
48
57
|
if (Array.isArray(body)) {
|
|
49
58
|
args = body;
|
|
50
59
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
@@ -73,6 +82,7 @@ export async function serve() {
|
|
|
73
82
|
if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
|
|
74
83
|
}
|
|
75
84
|
}
|
|
85
|
+
if (body.geminiVision) geminiVision = true;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
let clientSession = "";
|
|
@@ -192,12 +202,14 @@ export async function serve() {
|
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
// Auto-describe screenshot files with Gemini vision
|
|
205
|
+
// Auto-describe screenshot files with Gemini vision (opt-in via --gemini-vision)
|
|
196
206
|
const descriptions: Record<string, string> = {};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
|
package/serve.ts
CHANGED
|
@@ -2,7 +2,15 @@ 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
|
+
describeImage,
|
|
11
|
+
RECH_DIR,
|
|
12
|
+
PASSTHROUGH_ENV_KEYS,
|
|
13
|
+
} from "./rech.ts";
|
|
6
14
|
|
|
7
15
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
16
|
const absBase = resolve(base) + "/";
|
|
@@ -45,6 +53,7 @@ export async function serve() {
|
|
|
45
53
|
let sessionId: string;
|
|
46
54
|
let clientName = "";
|
|
47
55
|
let clientEnv: Record<string, string> = {};
|
|
56
|
+
let geminiVision = false;
|
|
48
57
|
if (Array.isArray(body)) {
|
|
49
58
|
args = body;
|
|
50
59
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
@@ -73,6 +82,7 @@ export async function serve() {
|
|
|
73
82
|
if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
|
|
74
83
|
}
|
|
75
84
|
}
|
|
85
|
+
if (body.geminiVision) geminiVision = true;
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
let clientSession = "";
|
|
@@ -192,12 +202,14 @@ export async function serve() {
|
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
// Auto-describe screenshot files with Gemini vision
|
|
205
|
+
// Auto-describe screenshot files with Gemini vision (opt-in via --gemini-vision)
|
|
196
206
|
const descriptions: Record<string, string> = {};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
}
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
|