rechrome 1.0.0 → 1.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.
- package/.env.example +4 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/rech.js +14 -1
- package/rech.ts +14 -1
- package/serve.js +16 -1
- package/serve.ts +16 -1
package/.env.example
CHANGED
|
@@ -10,3 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
# Bind address for the server (default: 127.0.0.1, use 0.0.0.0 for remote access)
|
|
12
12
|
# RECH_HOST=127.0.0.1
|
|
13
|
+
|
|
14
|
+
# Playwright MCP extension settings (can be set on server or client; client overrides server)
|
|
15
|
+
# PLAYWRIGHT_MCP_EXTENSION_ID=your-extension-id
|
|
16
|
+
# PLAYWRIGHT_MCP_EXTENSION_TOKEN=your-extension-token
|
package/README.md
CHANGED
|
@@ -82,6 +82,8 @@ cp .env.example .env.local
|
|
|
82
82
|
| `GEMINI_API_KEY` | Gemini API key for screenshot vision descriptions | — |
|
|
83
83
|
| `PLAYWRIGHT_CLI` | Path to playwright-cli binary (recommended: `playwright-cli-multi-tab` for full multi-tab support) | `playwright-cli` |
|
|
84
84
|
| `RECH_HOST` | Server bind address | `127.0.0.1` |
|
|
85
|
+
| `PLAYWRIGHT_MCP_EXTENSION_ID` | Chrome extension ID (client overrides server) | — |
|
|
86
|
+
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | Chrome extension token (client overrides server) | — |
|
|
85
87
|
|
|
86
88
|
### Remote access
|
|
87
89
|
|
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -70,6 +70,11 @@ export async function describeImage(imagePath: string): Promise<string | null> {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export const PASSTHROUGH_ENV_KEYS = [
|
|
74
|
+
"PLAYWRIGHT_MCP_EXTENSION_ID",
|
|
75
|
+
"PLAYWRIGHT_MCP_EXTENSION_TOKEN",
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
73
78
|
export function log(msg: string) {
|
|
74
79
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
75
80
|
const ts = new Date().toISOString();
|
|
@@ -143,6 +148,14 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
143
148
|
return { hostname: hostname(), cwd };
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
function getClientEnv(): Record<string, string> {
|
|
152
|
+
const env: Record<string, string> = {};
|
|
153
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
154
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
155
|
+
}
|
|
156
|
+
return env;
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
async function run(url: string, args: string[]) {
|
|
147
160
|
const { key, host, port } = parseUrl(url);
|
|
148
161
|
const identity = await getClientIdentity();
|
|
@@ -152,7 +165,7 @@ async function run(url: string, args: string[]) {
|
|
|
152
165
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
153
166
|
method: "POST",
|
|
154
167
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
155
|
-
body: JSON.stringify({ args, identity }),
|
|
168
|
+
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
156
169
|
signal: AbortSignal.timeout(70_000),
|
|
157
170
|
}).catch((e) => {
|
|
158
171
|
console.error(`[rech] ${e.message}`);
|
package/rech.ts
CHANGED
|
@@ -70,6 +70,11 @@ export async function describeImage(imagePath: string): Promise<string | null> {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export const PASSTHROUGH_ENV_KEYS = [
|
|
74
|
+
"PLAYWRIGHT_MCP_EXTENSION_ID",
|
|
75
|
+
"PLAYWRIGHT_MCP_EXTENSION_TOKEN",
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
73
78
|
export function log(msg: string) {
|
|
74
79
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
75
80
|
const ts = new Date().toISOString();
|
|
@@ -143,6 +148,14 @@ async function getClientIdentity(): Promise<{ gitUrl?: string; hostname?: string
|
|
|
143
148
|
return { hostname: hostname(), cwd };
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
function getClientEnv(): Record<string, string> {
|
|
152
|
+
const env: Record<string, string> = {};
|
|
153
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
154
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
155
|
+
}
|
|
156
|
+
return env;
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
async function run(url: string, args: string[]) {
|
|
147
160
|
const { key, host, port } = parseUrl(url);
|
|
148
161
|
const identity = await getClientIdentity();
|
|
@@ -152,7 +165,7 @@ async function run(url: string, args: string[]) {
|
|
|
152
165
|
const res = await fetch(`http://${host}:${port}/run`, {
|
|
153
166
|
method: "POST",
|
|
154
167
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
155
|
-
body: JSON.stringify({ args, identity }),
|
|
168
|
+
body: JSON.stringify({ args, identity, env: getClientEnv() }),
|
|
156
169
|
signal: AbortSignal.timeout(70_000),
|
|
157
170
|
}).catch((e) => {
|
|
158
171
|
console.error(`[rech] ${e.message}`);
|
package/serve.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 { log, parseUrl, getOrCreateUrl, authCheck, describeImage, RECH_DIR } from "./rech.js";
|
|
5
|
+
import { log, parseUrl, getOrCreateUrl, authCheck, describeImage, RECH_DIR, PASSTHROUGH_ENV_KEYS } from "./rech.js";
|
|
6
6
|
|
|
7
7
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
8
|
const absBase = resolve(base) + "/";
|
|
@@ -44,6 +44,7 @@ export async function serve() {
|
|
|
44
44
|
let args: string[];
|
|
45
45
|
let sessionId: string;
|
|
46
46
|
let clientName = "";
|
|
47
|
+
let clientEnv: Record<string, string> = {};
|
|
47
48
|
if (Array.isArray(body)) {
|
|
48
49
|
args = body;
|
|
49
50
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
@@ -66,6 +67,12 @@ export async function serve() {
|
|
|
66
67
|
clientName = clientAddr;
|
|
67
68
|
log(`session from client IP fallback: ${clientAddr} -> ${sessionId}`);
|
|
68
69
|
}
|
|
70
|
+
// Extract allowlisted env vars from client (client overrides server)
|
|
71
|
+
if (body.env && typeof body.env === "object") {
|
|
72
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
73
|
+
if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
let clientSession = "";
|
|
@@ -117,6 +124,13 @@ export async function serve() {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
128
|
+
const passthroughEnv: Record<string, string | undefined> = {};
|
|
129
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
130
|
+
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
131
|
+
}
|
|
132
|
+
Object.assign(passthroughEnv, clientEnv);
|
|
133
|
+
|
|
120
134
|
const childEnv: Record<string, string | undefined> = {
|
|
121
135
|
PATH: process.env.PATH,
|
|
122
136
|
HOME: process.env.HOME,
|
|
@@ -124,6 +138,7 @@ export async function serve() {
|
|
|
124
138
|
DISPLAY: process.env.DISPLAY,
|
|
125
139
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
126
140
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
141
|
+
...passthroughEnv,
|
|
127
142
|
};
|
|
128
143
|
const proc = Bun.spawn([bin, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
|
|
129
144
|
cwd: workDir,
|
package/serve.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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 { log, parseUrl, getOrCreateUrl, authCheck, describeImage, RECH_DIR } from "./rech.ts";
|
|
5
|
+
import { log, parseUrl, getOrCreateUrl, authCheck, describeImage, RECH_DIR, PASSTHROUGH_ENV_KEYS } from "./rech.ts";
|
|
6
6
|
|
|
7
7
|
export function isUnderDir(base: string, candidate: string): boolean {
|
|
8
8
|
const absBase = resolve(base) + "/";
|
|
@@ -44,6 +44,7 @@ export async function serve() {
|
|
|
44
44
|
let args: string[];
|
|
45
45
|
let sessionId: string;
|
|
46
46
|
let clientName = "";
|
|
47
|
+
let clientEnv: Record<string, string> = {};
|
|
47
48
|
if (Array.isArray(body)) {
|
|
48
49
|
args = body;
|
|
49
50
|
const clientAddr = `${req.headers.get("x-forwarded-for") || server.requestIP(req)?.address || "unknown"}`;
|
|
@@ -66,6 +67,12 @@ export async function serve() {
|
|
|
66
67
|
clientName = clientAddr;
|
|
67
68
|
log(`session from client IP fallback: ${clientAddr} -> ${sessionId}`);
|
|
68
69
|
}
|
|
70
|
+
// Extract allowlisted env vars from client (client overrides server)
|
|
71
|
+
if (body.env && typeof body.env === "object") {
|
|
72
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
73
|
+
if (typeof body.env[key] === "string") clientEnv[key] = body.env[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
let clientSession = "";
|
|
@@ -117,6 +124,13 @@ export async function serve() {
|
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
// Merge passthrough env: server .env.local defaults, then client overrides
|
|
128
|
+
const passthroughEnv: Record<string, string | undefined> = {};
|
|
129
|
+
for (const key of PASSTHROUGH_ENV_KEYS) {
|
|
130
|
+
if (process.env[key]) passthroughEnv[key] = process.env[key];
|
|
131
|
+
}
|
|
132
|
+
Object.assign(passthroughEnv, clientEnv);
|
|
133
|
+
|
|
120
134
|
const childEnv: Record<string, string | undefined> = {
|
|
121
135
|
PATH: process.env.PATH,
|
|
122
136
|
HOME: process.env.HOME,
|
|
@@ -124,6 +138,7 @@ export async function serve() {
|
|
|
124
138
|
DISPLAY: process.env.DISPLAY,
|
|
125
139
|
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
126
140
|
...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: clientName } : {}),
|
|
141
|
+
...passthroughEnv,
|
|
127
142
|
};
|
|
128
143
|
const proc = Bun.spawn([bin, ...filteredArgs, "--extension", `-s=${namespacedSession}`], {
|
|
129
144
|
cwd: workDir,
|