venombrowser 4.1.0 → 4.1.2
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/bin/venombrowser-agent.js +1 -1
- package/bin/venombrowser-chat.js +1 -1
- package/bin/venombrowser-cli.js +38 -1
- package/bin/venombrowser-mcp.js +153 -30
- package/bin/venombrowser-server.js +1 -1
- package/bin/venombrowser-setup.js +145 -183
- package/package.json +1 -1
- package/src/bridge-client.js +65 -5
- package/src/bridge-server.js +216 -13
- package/src/connect.js +10 -2
- package/src/mcp-server.js +9 -2
- package/src/tools/browser-tools.js +265 -2
package/src/bridge-client.js
CHANGED
|
@@ -18,22 +18,29 @@ class BridgeTimeoutError extends BridgeError {}
|
|
|
18
18
|
class BridgeCommandError extends BridgeError {}
|
|
19
19
|
|
|
20
20
|
class BrowserBridge {
|
|
21
|
-
constructor({ host = '127.0.0.1', port = 10086, session = 'default' } = {}) {
|
|
21
|
+
constructor({ host = '127.0.0.1', port = 10086, session = 'default', apiKey } = {}) {
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error('apiKey is required to connect to the VenomBrowser bridge.');
|
|
24
|
+
}
|
|
22
25
|
this.baseUrl = `http://${host}:${port}`;
|
|
23
26
|
this.session = session;
|
|
24
27
|
this.http = axios.create({
|
|
25
28
|
baseURL: this.baseUrl,
|
|
26
|
-
timeout: 60000
|
|
29
|
+
timeout: 60000,
|
|
30
|
+
headers: {
|
|
31
|
+
'x-bridge-key': apiKey
|
|
32
|
+
}
|
|
27
33
|
});
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// ─── Core command method ─────────────────────────────────────────────────
|
|
31
|
-
async command(action, args = {}, timeout = 30000) {
|
|
37
|
+
async command(action, args = {}, timeout = 30000, extra = {}) {
|
|
32
38
|
try {
|
|
33
39
|
const response = await this.http.post('/command', {
|
|
34
40
|
action,
|
|
35
41
|
args,
|
|
36
|
-
session: this.session
|
|
42
|
+
session: this.session,
|
|
43
|
+
...extra
|
|
37
44
|
}, { timeout });
|
|
38
45
|
return response.data;
|
|
39
46
|
} catch (e) {
|
|
@@ -73,7 +80,19 @@ class BrowserBridge {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
async closeSession() {
|
|
76
|
-
return this.command('close_session');
|
|
83
|
+
return this.command('close_session', {}, 30000, { confirm: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async stopServer() {
|
|
87
|
+
return this.command('stop_server', {}, 5000, { confirm: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async restartServer() {
|
|
91
|
+
return this.command('restart_server', {}, 5000, { confirm: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async bridgeStatus() {
|
|
95
|
+
return this.command('status');
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
// ─── Content interaction (Kimi-style) ────────────────────────────────────
|
|
@@ -130,6 +149,47 @@ class BrowserBridge {
|
|
|
130
149
|
return this.command('scroll', { direction, amount });
|
|
131
150
|
}
|
|
132
151
|
|
|
152
|
+
// ─── Phase 1: Cheap Wins ─────────────────────────────────────────────────
|
|
153
|
+
async scrollTo(selector) {
|
|
154
|
+
return this.command('scroll_to', { selector });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async checkVisibility(selector) {
|
|
158
|
+
return this.command('check_visibility', { selector });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async checkStale(selector) {
|
|
162
|
+
return this.command('check_stale', { selector });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async doubleClick(selector) {
|
|
166
|
+
return this.command('double_click', { selector });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async rightClick(selector) {
|
|
170
|
+
return this.command('right_click', { selector });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async keyboard(key, { modifiers = [] } = {}) {
|
|
174
|
+
return this.command('keyboard', { key, modifiers });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async goBack() {
|
|
178
|
+
return this.command('go_back');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async goForward() {
|
|
182
|
+
return this.command('go_forward');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async manageCookies(cmd, { name, value, domain } = {}) {
|
|
186
|
+
return this.command('manage_cookies', { cmd, name, value, domain });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async manageStorage(cmd, { storageType = 'local', key, value } = {}) {
|
|
190
|
+
return this.command('manage_storage', { cmd, storage_type: storageType, key, value });
|
|
191
|
+
}
|
|
192
|
+
|
|
133
193
|
// ─── Legacy compatibility (maps old names to new) ─────────────────────────
|
|
134
194
|
async extract(selector = null) {
|
|
135
195
|
const r = await this.evaluate(`
|
package/src/bridge-server.js
CHANGED
|
@@ -16,13 +16,37 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const WebSocket = require('ws');
|
|
18
18
|
const { v4: uuidv4 } = require('uuid');
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
|
|
20
21
|
const PORT = parseInt(process.env.BRIDGE_PORT || '10086', 10);
|
|
21
22
|
const WS_PORT = parseInt(process.env.WS_PORT || String(PORT + 1), 10);
|
|
23
|
+
const WS_AUTH_TOKEN = process.env.WS_AUTH_TOKEN;
|
|
24
|
+
const REST_API_KEY = process.env.REST_API_KEY;
|
|
25
|
+
const CORS_ORIGIN = process.env.CORS_ORIGIN;
|
|
26
|
+
|
|
27
|
+
if (!WS_AUTH_TOKEN || !REST_API_KEY) {
|
|
28
|
+
console.error('[Bridge] FATAL: WS_AUTH_TOKEN and REST_API_KEY must be set in the environment.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
const WS_PING_INTERVAL = 15000;
|
|
23
33
|
const WS_PING_TIMEOUT = 10000;
|
|
24
34
|
const CMD_TIMEOUT = 30000;
|
|
35
|
+
const IDLE_TIMEOUT = 15 * 60 * 1000; // 15 minutes of inactivity shuts down the daemon
|
|
25
36
|
const SCREENSHOT_DIR = path.join(__dirname, '..', '..', 'screenshots');
|
|
37
|
+
const LOG_DIR = path.join(__dirname, '..', 'logs');
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
40
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function safeCompare(a, b) {
|
|
44
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
45
|
+
const aBuf = Buffer.from(a);
|
|
46
|
+
const bBuf = Buffer.from(b);
|
|
47
|
+
if (aBuf.length !== bBuf.length) return false;
|
|
48
|
+
return crypto.timingSafeEqual(aBuf, bBuf);
|
|
49
|
+
}
|
|
26
50
|
|
|
27
51
|
class VenomBrowserBridgeServer {
|
|
28
52
|
constructor() {
|
|
@@ -37,13 +61,57 @@ class VenomBrowserBridgeServer {
|
|
|
37
61
|
this.currentTab = null;
|
|
38
62
|
this.pingTimer = null;
|
|
39
63
|
this.lastPong = Date.now();
|
|
64
|
+
this.idleTimer = null;
|
|
65
|
+
|
|
66
|
+
this.lastRestActivity = Date.now();
|
|
67
|
+
this.EXTENSION_IDLE_TIMEOUT = parseInt(process.env.EXTENSION_IDLE_TIMEOUT || String(120 * 1000), 10);
|
|
68
|
+
this.DAEMON_IDLE_TIMEOUT = parseInt(process.env.DAEMON_IDLE_TIMEOUT || String(30 * 60 * 1000), 10);
|
|
69
|
+
|
|
70
|
+
// Setup uncaught exception handlers
|
|
71
|
+
process.on('uncaughtException', (err) => {
|
|
72
|
+
console.error('[Bridge] UNCAUGHT EXCEPTION:', err);
|
|
73
|
+
});
|
|
40
74
|
|
|
75
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
76
|
+
console.error('[Bridge] UNHANDLED REJECTION:', reason);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
start() {
|
|
81
|
+
console.log(`\n🚀 Starting VenomBrowser Bridge Server v4.1.0...`);
|
|
41
82
|
this.startHTTP();
|
|
42
83
|
this.startWS();
|
|
43
84
|
this.startPingLoop();
|
|
85
|
+
this.startIdleMonitor();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
startIdleMonitor() {
|
|
89
|
+
setInterval(() => {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const idleTime = now - this.lastRestActivity;
|
|
92
|
+
|
|
93
|
+
// 1. If idle longer than DAEMON_IDLE_TIMEOUT, shut down completely
|
|
94
|
+
if (idleTime > this.DAEMON_IDLE_TIMEOUT) {
|
|
95
|
+
console.log(`[Bridge] Daemon idle for > ${this.DAEMON_IDLE_TIMEOUT}ms. Shutting down.`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. If idle longer than EXTENSION_IDLE_TIMEOUT, disconnect extension
|
|
100
|
+
if (idleTime > this.EXTENSION_IDLE_TIMEOUT && this.extensionWs && this.extensionWs.readyState === WebSocket.OPEN) {
|
|
101
|
+
console.log(`[Bridge] No REST activity for > ${this.EXTENSION_IDLE_TIMEOUT}ms. Disconnecting extension.`);
|
|
102
|
+
this.safeSend(this.extensionWs, { type: 'idle_disconnect' });
|
|
103
|
+
|
|
104
|
+
// Wait a tiny bit for message to send before closing
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
if (this.extensionWs) {
|
|
107
|
+
this.extensionWs.close(1000, 'Idle Disconnect');
|
|
108
|
+
}
|
|
109
|
+
}, 100);
|
|
110
|
+
}
|
|
111
|
+
}, 10000); // Check every 10 seconds
|
|
44
112
|
}
|
|
45
113
|
|
|
46
|
-
// ───
|
|
114
|
+
// ─── HTTP Server ─────────────────────────────────────────────────────────
|
|
47
115
|
safeSend(ws, data) {
|
|
48
116
|
try {
|
|
49
117
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -59,9 +127,16 @@ class VenomBrowserBridgeServer {
|
|
|
59
127
|
// ─── HTTP Server ─────────────────────────────────────────────────────────
|
|
60
128
|
startHTTP() {
|
|
61
129
|
this.httpServer = http.createServer((req, res) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
130
|
+
// CORS handling
|
|
131
|
+
if (CORS_ORIGIN && CORS_ORIGIN !== '*') {
|
|
132
|
+
if (req.headers.origin === CORS_ORIGIN) {
|
|
133
|
+
res.setHeader('Access-Control-Allow-Origin', CORS_ORIGIN);
|
|
134
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
135
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, x-bridge-key');
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// No CORS by default. Only allow localhost processes.
|
|
139
|
+
}
|
|
65
140
|
|
|
66
141
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
67
142
|
|
|
@@ -80,6 +155,12 @@ class VenomBrowserBridgeServer {
|
|
|
80
155
|
});
|
|
81
156
|
}
|
|
82
157
|
|
|
158
|
+
// API Key Auth for all other endpoints
|
|
159
|
+
const apiKey = req.headers['x-bridge-key'];
|
|
160
|
+
if (!safeCompare(apiKey, REST_API_KEY)) {
|
|
161
|
+
return send(401, { success: false, error: 'Unauthorized: Invalid x-bridge-key' });
|
|
162
|
+
}
|
|
163
|
+
|
|
83
164
|
if (req.method === 'GET' && req.url === '/status') {
|
|
84
165
|
return send(200, {
|
|
85
166
|
success: true,
|
|
@@ -139,10 +220,19 @@ class VenomBrowserBridgeServer {
|
|
|
139
220
|
|
|
140
221
|
if (!action) throw new Error('action is required');
|
|
141
222
|
|
|
223
|
+
const destructiveActions = ['upload', 'close_session', 'stop_server', 'restart_server'];
|
|
224
|
+
const destructiveArgs = ['delete', 'clear'];
|
|
225
|
+
if (destructiveActions.includes(action) || (['manage_cookies', 'manage_storage'].includes(action) && destructiveArgs.includes(args.cmd))) {
|
|
226
|
+
if (request.confirm !== true) {
|
|
227
|
+
throw new Error(`Confirmation required: The action '${action}' is destructive. You must include "confirm": true in your request to proceed.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
142
231
|
if (!this.sessions.has(session)) {
|
|
143
232
|
this.sessions.set(session, { tabs: new Set(), startTime: Date.now(), commandCount: 0 });
|
|
144
233
|
}
|
|
145
234
|
this.sessions.get(session).commandCount++;
|
|
235
|
+
this.lastRestActivity = Date.now();
|
|
146
236
|
|
|
147
237
|
if (action === 'health') return { service: 'venombrowser-bridge' };
|
|
148
238
|
|
|
@@ -156,8 +246,6 @@ class VenomBrowserBridgeServer {
|
|
|
156
246
|
};
|
|
157
247
|
}
|
|
158
248
|
|
|
159
|
-
if (!this.isConnected()) throw new Error('Extension not connected');
|
|
160
|
-
|
|
161
249
|
return this.executeAction(action, args, session, tabId);
|
|
162
250
|
}
|
|
163
251
|
|
|
@@ -218,6 +306,19 @@ class VenomBrowserBridgeServer {
|
|
|
218
306
|
return { closed };
|
|
219
307
|
}
|
|
220
308
|
|
|
309
|
+
case 'stop_server': {
|
|
310
|
+
console.log('[Bridge] Received stop_server command from agent/CLI. Shutting down gracefully.');
|
|
311
|
+
setTimeout(() => this.shutdown(), 100);
|
|
312
|
+
return { success: true, message: 'Server is shutting down.' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'restart_server': {
|
|
316
|
+
console.log('[Bridge] Received restart_server command. Shutting down for restart...');
|
|
317
|
+
// The MCP server / agent will auto-spawn a new daemon on next command
|
|
318
|
+
setTimeout(() => this.shutdown(), 100);
|
|
319
|
+
return { success: true, message: 'Server is restarting. Next command will auto-spawn a new daemon.' };
|
|
320
|
+
}
|
|
321
|
+
|
|
221
322
|
case 'snapshot': {
|
|
222
323
|
const r = await send('SNAPSHOT', {}, tabId || this.currentTab);
|
|
223
324
|
const res = r.result || {};
|
|
@@ -245,12 +346,18 @@ class VenomBrowserBridgeServer {
|
|
|
245
346
|
}
|
|
246
347
|
|
|
247
348
|
case 'evaluate': {
|
|
248
|
-
const
|
|
349
|
+
const code = args.code;
|
|
350
|
+
fs.appendFileSync(path.join(LOG_DIR, 'audit.log'), `[${new Date().toISOString()}] evaluate: ${code}\n`);
|
|
351
|
+
const r = await send('EVALUATE', { code }, tabId || this.currentTab, 30000);
|
|
249
352
|
return { type: r.result?.type, value: r.result?.result };
|
|
250
353
|
}
|
|
251
354
|
|
|
252
355
|
case 'cdp': {
|
|
253
|
-
const
|
|
356
|
+
const { method, params = {} } = args;
|
|
357
|
+
if (method === 'Runtime.evaluate' || method === 'Runtime.evaluateOnNewDocument') {
|
|
358
|
+
fs.appendFileSync(path.join(LOG_DIR, 'audit.log'), `[${new Date().toISOString()}] CDP ${method}: ${JSON.stringify(params)}\n`);
|
|
359
|
+
}
|
|
360
|
+
const r = await send('CDP', { method, params }, tabId || this.currentTab, 30000);
|
|
254
361
|
return r.result;
|
|
255
362
|
}
|
|
256
363
|
|
|
@@ -294,6 +401,57 @@ class VenomBrowserBridgeServer {
|
|
|
294
401
|
return r.result;
|
|
295
402
|
}
|
|
296
403
|
|
|
404
|
+
// ── Phase 1: Cheap Wins ──
|
|
405
|
+
case 'scroll_to': {
|
|
406
|
+
const r = await send('SCROLL_TO', { selector: args.selector }, tabId || this.currentTab);
|
|
407
|
+
return r.result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'check_visibility': {
|
|
411
|
+
const r = await send('CHECK_VISIBILITY', { selector: args.selector }, tabId || this.currentTab);
|
|
412
|
+
return r.result;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
case 'check_stale': {
|
|
416
|
+
const r = await send('CHECK_STALE', { selector: args.selector }, tabId || this.currentTab);
|
|
417
|
+
return r.result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
case 'double_click': {
|
|
421
|
+
const r = await send('DOUBLE_CLICK', { selector: args.selector }, tabId || this.currentTab);
|
|
422
|
+
return { success: true, tag: r.result?.tag, text: r.result?.text };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
case 'right_click': {
|
|
426
|
+
const r = await send('RIGHT_CLICK', { selector: args.selector }, tabId || this.currentTab);
|
|
427
|
+
return { success: true, tag: r.result?.tag, text: r.result?.text };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
case 'keyboard': {
|
|
431
|
+
const r = await send('KEYBOARD', { key: args.key, modifiers: args.modifiers || [] }, tabId || this.currentTab);
|
|
432
|
+
return r.result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'go_back': {
|
|
436
|
+
const r = await send('GO_BACK', {}, tabId || this.currentTab);
|
|
437
|
+
return r.result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
case 'go_forward': {
|
|
441
|
+
const r = await send('GO_FORWARD', {}, tabId || this.currentTab);
|
|
442
|
+
return r.result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
case 'manage_cookies': {
|
|
446
|
+
const r = await send('COOKIES', { cmd: args.cmd, name: args.name, value: args.value, domain: args.domain }, tabId || this.currentTab);
|
|
447
|
+
return r.result;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case 'manage_storage': {
|
|
451
|
+
const r = await send('STORAGE', { cmd: args.cmd, storageType: args.storage_type || args.storageType || 'local', key: args.key, value: args.value }, tabId || this.currentTab);
|
|
452
|
+
return r.result;
|
|
453
|
+
}
|
|
454
|
+
|
|
297
455
|
default:
|
|
298
456
|
throw new Error(`Unknown action: ${action}`);
|
|
299
457
|
}
|
|
@@ -301,8 +459,8 @@ class VenomBrowserBridgeServer {
|
|
|
301
459
|
|
|
302
460
|
// ─── WebSocket server ────────────────────────────────────────────────────
|
|
303
461
|
startWS() {
|
|
304
|
-
this.wss = new WebSocket.Server({ port: WS_PORT }, () => {
|
|
305
|
-
console.log(`[Bridge] WebSocket on ws://
|
|
462
|
+
this.wss = new WebSocket.Server({ host: '127.0.0.1', port: WS_PORT }, () => {
|
|
463
|
+
console.log(`[Bridge] WebSocket on ws://127.0.0.1:${WS_PORT}`);
|
|
306
464
|
});
|
|
307
465
|
|
|
308
466
|
this.wss.on('error', (e) => {
|
|
@@ -316,6 +474,16 @@ class VenomBrowserBridgeServer {
|
|
|
316
474
|
const addr = req.socket.remoteAddress || 'unknown';
|
|
317
475
|
console.log(`[Bridge] Extension connected from ${addr}`);
|
|
318
476
|
|
|
477
|
+
// Handshake timeout
|
|
478
|
+
ws.authTimeout = setTimeout(() => {
|
|
479
|
+
if (!ws.authenticated) {
|
|
480
|
+
console.log('[Bridge] Extension handshake timed out.');
|
|
481
|
+
ws.close(4001, 'Auth timeout');
|
|
482
|
+
}
|
|
483
|
+
}, 5000);
|
|
484
|
+
|
|
485
|
+
ws.authenticated = false;
|
|
486
|
+
|
|
319
487
|
// Replace previous connection
|
|
320
488
|
if (this.extensionWs && this.extensionWs.readyState === WebSocket.OPEN) {
|
|
321
489
|
console.log('[Bridge] Replacing previous extension');
|
|
@@ -364,9 +532,19 @@ class VenomBrowserBridgeServer {
|
|
|
364
532
|
}
|
|
365
533
|
if (msg.type === 'hello') {
|
|
366
534
|
console.log(`[Bridge] Extension handshake: ${msg.client} v${msg.version}`);
|
|
535
|
+
if (safeCompare(msg.token, WS_AUTH_TOKEN)) {
|
|
536
|
+
ws.authenticated = true;
|
|
537
|
+
clearTimeout(ws.authTimeout);
|
|
538
|
+
this.safeSend(ws, { type: 'auth_ok' });
|
|
539
|
+
} else {
|
|
540
|
+
this.safeSend(ws, { type: 'auth_fail' });
|
|
541
|
+
setTimeout(() => ws.close(4001, 'Unauthorized'), 100);
|
|
542
|
+
}
|
|
367
543
|
return;
|
|
368
544
|
}
|
|
369
545
|
|
|
546
|
+
if (!ws.authenticated) return; // Drop messages from unauthenticated sockets
|
|
547
|
+
|
|
370
548
|
// Command response
|
|
371
549
|
if (msg.id && this.pending.has(msg.id)) {
|
|
372
550
|
const entry = this.pending.get(msg.id);
|
|
@@ -400,9 +578,14 @@ class VenomBrowserBridgeServer {
|
|
|
400
578
|
}
|
|
401
579
|
|
|
402
580
|
// ─── Send command to extension ───────────────────────────────────────────
|
|
403
|
-
sendToExtension(command, params = {}, tabId = null, timeoutMs = CMD_TIMEOUT) {
|
|
404
|
-
if (!this.isConnected()) {
|
|
405
|
-
|
|
581
|
+
async sendToExtension(command, params = {}, tabId = null, timeoutMs = CMD_TIMEOUT) {
|
|
582
|
+
if (!this.isConnected() || !this.extensionWs.authenticated) {
|
|
583
|
+
console.log(`[Bridge] Extension not ready. Waiting for reconnection (max 10s)...`);
|
|
584
|
+
try {
|
|
585
|
+
await this.waitForConnection(10000);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
406
589
|
}
|
|
407
590
|
|
|
408
591
|
const id = uuidv4();
|
|
@@ -425,6 +608,25 @@ class VenomBrowserBridgeServer {
|
|
|
425
608
|
});
|
|
426
609
|
}
|
|
427
610
|
|
|
611
|
+
waitForConnection(timeoutMs) {
|
|
612
|
+
return new Promise((resolve, reject) => {
|
|
613
|
+
if (this.isConnected() && this.extensionWs.authenticated) return resolve();
|
|
614
|
+
|
|
615
|
+
const checkInterval = setInterval(() => {
|
|
616
|
+
if (this.isConnected() && this.extensionWs.authenticated) {
|
|
617
|
+
clearInterval(checkInterval);
|
|
618
|
+
clearTimeout(timeoutTimer);
|
|
619
|
+
resolve();
|
|
620
|
+
}
|
|
621
|
+
}, 500);
|
|
622
|
+
|
|
623
|
+
const timeoutTimer = setTimeout(() => {
|
|
624
|
+
clearInterval(checkInterval);
|
|
625
|
+
reject(new Error('Timed out waiting for extension to connect'));
|
|
626
|
+
}, timeoutMs);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
428
630
|
rejectAll(reason) {
|
|
429
631
|
for (const { reject, timer } of this.pending.values()) {
|
|
430
632
|
clearTimeout(timer);
|
|
@@ -466,6 +668,7 @@ process.on('unhandledRejection', (reason) => {
|
|
|
466
668
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
467
669
|
console.log('🚀 Starting VenomBrowser Bridge Server v4.1.0...');
|
|
468
670
|
const server = new VenomBrowserBridgeServer();
|
|
671
|
+
server.start();
|
|
469
672
|
console.log(`📡 WebSocket : ws://localhost:${WS_PORT}`);
|
|
470
673
|
console.log(`🌐 HTTP API : http://127.0.0.1:${PORT}/command`);
|
|
471
674
|
console.log(`📋 Status : http://127.0.0.1:${PORT}/status`);
|
package/src/connect.js
CHANGED
|
@@ -185,9 +185,17 @@ function connect() {
|
|
|
185
185
|
const registered = [];
|
|
186
186
|
|
|
187
187
|
for (const { name, path: configPath, format } of configs) {
|
|
188
|
+
// If the file doesn't exist, create it so we can auto-configure
|
|
188
189
|
if (!fs.existsSync(configPath)) {
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
try {
|
|
191
|
+
const dir = path.dirname(configPath);
|
|
192
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
193
|
+
fs.writeFileSync(configPath, '{}', 'utf8');
|
|
194
|
+
console.log(` ✨ ${name}: created missing config file`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(` ⏭️ ${name}: could not create config (${err.message})`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
const result = format === 'mcp'
|
package/src/mcp-server.js
CHANGED
|
@@ -20,12 +20,15 @@ const { BrowserBridge } = require('./bridge-client');
|
|
|
20
20
|
const { getMcpToolDefinitions, executeTool } = require('./tools/browser-tools');
|
|
21
21
|
|
|
22
22
|
class VenomBrowserMcpServer {
|
|
23
|
-
constructor() {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.onBeforeCommand = options.onBeforeCommand || null;
|
|
24
25
|
const host = process.env.BRIDGE_HOST || '127.0.0.1';
|
|
25
26
|
const port = parseInt(process.env.BRIDGE_PORT || '10086', 10);
|
|
26
27
|
const session = process.env.BRIDGE_SESSION || 'mcp-session';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const apiKey = process.env.REST_API_KEY;
|
|
30
|
+
|
|
31
|
+
this.bridge = new BrowserBridge({ host, port, session, apiKey });
|
|
29
32
|
|
|
30
33
|
this.server = new Server(
|
|
31
34
|
{ name: 'venombrowser', version: '4.0.0' },
|
|
@@ -45,6 +48,10 @@ class VenomBrowserMcpServer {
|
|
|
45
48
|
const { name, arguments: args } = request.params;
|
|
46
49
|
|
|
47
50
|
try {
|
|
51
|
+
if (this.onBeforeCommand) {
|
|
52
|
+
await this.onBeforeCommand(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
const result = await executeTool(this.bridge, name, args || {});
|
|
49
56
|
|
|
50
57
|
// Special handling for screenshots — return as MCP image content
|