venombrowser 4.1.1 → 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-cli.js +37 -0
- package/bin/venombrowser-mcp.js +7 -2
- package/bin/venombrowser-setup.js +144 -182
- 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/bin/venombrowser-cli.js
CHANGED
|
@@ -37,6 +37,8 @@ Commands:
|
|
|
37
37
|
setup Run the interactive setup wizard
|
|
38
38
|
start Start the bridge daemon
|
|
39
39
|
status Check daemon & extension connection status
|
|
40
|
+
stop Stop the bridge daemon (releases port)
|
|
41
|
+
restart Restart the bridge daemon
|
|
40
42
|
connect Auto-register MCP server with known agents
|
|
41
43
|
uninstall Remove MCP entries, stop daemon (does NOT npm uninstall)
|
|
42
44
|
agent "<task>" Run a one-shot browser automation task
|
|
@@ -153,6 +155,35 @@ function runChat() {
|
|
|
153
155
|
child.on('exit', (code) => process.exit(code || 0));
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
function runStop() {
|
|
159
|
+
const { BrowserBridge } = require('../src/bridge-client');
|
|
160
|
+
const bridge = new BrowserBridge({ port: BRIDGE_PORT, session: 'cli', apiKey: process.env.REST_API_KEY });
|
|
161
|
+
bridge.stopServer()
|
|
162
|
+
.then(() => console.log('✅ Sent stop signal to bridge daemon. It will shut down gracefully.'))
|
|
163
|
+
.catch(err => {
|
|
164
|
+
if (err.message.includes('fetch failed') || err.message.includes('ECONNREFUSED')) {
|
|
165
|
+
console.log('✅ Daemon is already stopped.');
|
|
166
|
+
} else {
|
|
167
|
+
console.error('❌ Failed to stop daemon:', err.message);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runRestart() {
|
|
173
|
+
console.log('🔄 Restarting VenomBrowser daemon...');
|
|
174
|
+
const { BrowserBridge } = require('../src/bridge-client');
|
|
175
|
+
const bridge = new BrowserBridge({ port: BRIDGE_PORT, session: 'cli', apiKey: process.env.REST_API_KEY });
|
|
176
|
+
bridge.stopServer()
|
|
177
|
+
.then(() => {
|
|
178
|
+
console.log('✅ Daemon stopped.');
|
|
179
|
+
setTimeout(() => runStart(), 1000);
|
|
180
|
+
})
|
|
181
|
+
.catch(err => {
|
|
182
|
+
console.log('✅ Daemon was not running.');
|
|
183
|
+
runStart();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
156
187
|
// ─── Dispatch ───────────────────────────────────────────────────────────────
|
|
157
188
|
|
|
158
189
|
const command = process.argv[2];
|
|
@@ -167,6 +198,12 @@ switch (command) {
|
|
|
167
198
|
case 'status':
|
|
168
199
|
runStatus();
|
|
169
200
|
break;
|
|
201
|
+
case 'stop':
|
|
202
|
+
runStop();
|
|
203
|
+
break;
|
|
204
|
+
case 'restart':
|
|
205
|
+
runRestart();
|
|
206
|
+
break;
|
|
170
207
|
case 'connect':
|
|
171
208
|
runConnect();
|
|
172
209
|
break;
|
package/bin/venombrowser-mcp.js
CHANGED
|
@@ -136,9 +136,14 @@ async function main() {
|
|
|
136
136
|
// Step 1: Ensure the bridge daemon is running (Kimi-style auto-start)
|
|
137
137
|
await ensureBridgeRunning();
|
|
138
138
|
|
|
139
|
-
// Step 2: Start the MCP server (stdio transport for agent communication)
|
|
140
139
|
const VenomBrowserMcpServer = require('../src/mcp-server');
|
|
141
|
-
const server = new VenomBrowserMcpServer(
|
|
140
|
+
const server = new VenomBrowserMcpServer({
|
|
141
|
+
onBeforeCommand: async (toolName) => {
|
|
142
|
+
// Don't auto-start if the agent is explicitly trying to stop the server
|
|
143
|
+
if (toolName === 'stop_server') return;
|
|
144
|
+
await ensureBridgeRunning();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
142
147
|
await server.start();
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -1,183 +1,145 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* VenomBrowser Setup — Interactive setup wizard
|
|
4
|
-
*
|
|
5
|
-
* Helps users (and agents) configure VenomBrowser by:
|
|
6
|
-
* 1. Creating/updating .env from .env.example
|
|
7
|
-
* 2.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log('
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
// ──
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// ── Final summary ─────────────────────────────────────────────────────────
|
|
147
|
-
const final = { ...existing, ...updates };
|
|
148
|
-
console.log();
|
|
149
|
-
console.log('🚀 Next steps:');
|
|
150
|
-
if (!final.WS_AUTH_TOKEN || final.WS_AUTH_TOKEN.includes('paste')) {
|
|
151
|
-
console.log(' 1. Set your WS_AUTH_TOKEN (see instructions above)');
|
|
152
|
-
console.log(' 2. Start using your AI agent (the bridge daemon will auto-start)');
|
|
153
|
-
} else {
|
|
154
|
-
console.log(' 1. Start using your AI agent (the bridge daemon will auto-start)');
|
|
155
|
-
}
|
|
156
|
-
console.log(' 2. venombrowser chat — start interactive chat');
|
|
157
|
-
console.log(' 3. venombrowser agent "task" — run a one-shot task');
|
|
158
|
-
console.log();
|
|
159
|
-
|
|
160
|
-
// ── Auto-register with agents ────────────────────────────────────────────
|
|
161
|
-
try {
|
|
162
|
-
const { connect } = require('../src/connect');
|
|
163
|
-
connect();
|
|
164
|
-
} catch (err) {
|
|
165
|
-
console.log('ℹ️ Auto-registration skipped:', err.message);
|
|
166
|
-
// Still print manual instructions as fallback
|
|
167
|
-
const absPath = path.resolve(__dirname, 'venombrowser-mcp.js').replace(/\\/g, '/');
|
|
168
|
-
console.log('🔌 To connect Claude Code or other MCP agents, add this to your MCP config:');
|
|
169
|
-
console.log(JSON.stringify({
|
|
170
|
-
"venombrowser": {
|
|
171
|
-
command: "node",
|
|
172
|
-
args: [absPath],
|
|
173
|
-
env: {
|
|
174
|
-
REST_API_KEY: final.REST_API_KEY || updates.REST_API_KEY || 'your-key'
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}, null, 2));
|
|
178
|
-
console.log();
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
main().catch(console.error);
|
|
183
|
-
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser Setup — Interactive setup wizard
|
|
4
|
+
*
|
|
5
|
+
* Helps users (and agents) configure VenomBrowser by:
|
|
6
|
+
* 1. Creating/updating .env from .env.example
|
|
7
|
+
* 2. Configuring the LLM provider (optional)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node bin/venombrowser-setup.js
|
|
11
|
+
* node bin/venombrowser-setup.js --auto # Non-interactive: use defaults
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const readline = require('readline');
|
|
17
|
+
|
|
18
|
+
const ENV_PATH = path.join(__dirname, '..', '.env');
|
|
19
|
+
const EXAMPLE_PATH = path.join(__dirname, '..', '.env.example');
|
|
20
|
+
|
|
21
|
+
function parseEnv(content) {
|
|
22
|
+
const vars = {};
|
|
23
|
+
for (const line of content.split('\n')) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
26
|
+
const eqIndex = trimmed.indexOf('=');
|
|
27
|
+
if (eqIndex === -1) continue;
|
|
28
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
29
|
+
const val = trimmed.slice(eqIndex + 1).trim();
|
|
30
|
+
vars[key] = val;
|
|
31
|
+
}
|
|
32
|
+
return vars;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function updateEnvFile(updates) {
|
|
36
|
+
let content;
|
|
37
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
38
|
+
content = fs.readFileSync(ENV_PATH, 'utf8');
|
|
39
|
+
} else if (fs.existsSync(EXAMPLE_PATH)) {
|
|
40
|
+
content = fs.readFileSync(EXAMPLE_PATH, 'utf8');
|
|
41
|
+
} else {
|
|
42
|
+
// Build minimal .env
|
|
43
|
+
content = Object.entries(updates).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
|
|
44
|
+
fs.writeFileSync(ENV_PATH, content);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
49
|
+
// Replace existing key= lines, or append
|
|
50
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
51
|
+
if (regex.test(content)) {
|
|
52
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
53
|
+
} else {
|
|
54
|
+
content += `\n${key}=${value}\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(ENV_PATH, content);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ask(rl, question) {
|
|
62
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main() {
|
|
66
|
+
console.log('🕷️ VenomBrowser Bridge Setup');
|
|
67
|
+
console.log('═'.repeat(50));
|
|
68
|
+
|
|
69
|
+
// Load existing env
|
|
70
|
+
let existing = {};
|
|
71
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
72
|
+
existing = parseEnv(fs.readFileSync(ENV_PATH, 'utf8'));
|
|
73
|
+
console.log('📄 Found existing .env file');
|
|
74
|
+
} else {
|
|
75
|
+
console.log('📄 No .env file found — will create one');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const crypto = require('crypto');
|
|
79
|
+
const updates = {};
|
|
80
|
+
|
|
81
|
+
// ── Auto-generate security tokens if missing ──────────────────────────────
|
|
82
|
+
if (!existing.REST_API_KEY) {
|
|
83
|
+
updates.REST_API_KEY = crypto.randomBytes(32).toString('hex');
|
|
84
|
+
console.log('🔑 Generated new REST_API_KEY');
|
|
85
|
+
}
|
|
86
|
+
if (!existing.WS_AUTH_TOKEN) {
|
|
87
|
+
updates.WS_AUTH_TOKEN = crypto.randomUUID();
|
|
88
|
+
console.log('🔑 Generated new WS_AUTH_TOKEN');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const finalWsToken = updates.WS_AUTH_TOKEN || existing.WS_AUTH_TOKEN;
|
|
92
|
+
|
|
93
|
+
// ── Inject token into extension ───────────────────────────────────────────
|
|
94
|
+
const bgPath = path.join(__dirname, '..', '..', 'extension', 'background.js');
|
|
95
|
+
if (fs.existsSync(bgPath)) {
|
|
96
|
+
let bgContent = fs.readFileSync(bgPath, 'utf8');
|
|
97
|
+
// Look for INJECTED_TOKEN = '__WS_AUTH_TOKEN__' or an already injected token
|
|
98
|
+
bgContent = bgContent.replace(/const INJECTED_TOKEN = '.*?';/, `const INJECTED_TOKEN = '${finalWsToken}';`);
|
|
99
|
+
fs.writeFileSync(bgPath, bgContent);
|
|
100
|
+
console.log('💉 Injected WS_AUTH_TOKEN into extension/background.js');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Save ──────────────────────────────────────────────────────────────────
|
|
104
|
+
if (Object.keys(updates).length > 0) {
|
|
105
|
+
updateEnvFile(updates);
|
|
106
|
+
console.log();
|
|
107
|
+
console.log('💾 Configuration saved to server/.env');
|
|
108
|
+
} else {
|
|
109
|
+
console.log();
|
|
110
|
+
console.log('✅ Configuration is already up to date');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Final summary ─────────────────────────────────────────────────────────
|
|
114
|
+
console.log();
|
|
115
|
+
console.log('🚀 Next steps:');
|
|
116
|
+
console.log(' 1. IMPORTANT: Go to chrome://extensions and click the "Reload" icon on the VenomBrowser extension');
|
|
117
|
+
console.log(' (This is required to apply the newly generated security token)');
|
|
118
|
+
console.log(' 2. Start using your AI agent (the bridge daemon will auto-start)');
|
|
119
|
+
console.log(' 3. venombrowser chat — start interactive chat');
|
|
120
|
+
console.log(' 4. venombrowser agent "task" — run a one-shot task');
|
|
121
|
+
console.log();
|
|
122
|
+
|
|
123
|
+
// ── Auto-register with agents ────────────────────────────────────────────
|
|
124
|
+
try {
|
|
125
|
+
const { connect } = require('../src/connect');
|
|
126
|
+
connect();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.log('ℹ️ Auto-registration skipped:', err.message);
|
|
129
|
+
// Still print manual instructions as fallback
|
|
130
|
+
const absPath = path.resolve(__dirname, 'venombrowser-mcp.js').replace(/\\/g, '/');
|
|
131
|
+
console.log('🔌 To connect Claude Code or other MCP agents, add this to your MCP config:');
|
|
132
|
+
console.log(JSON.stringify({
|
|
133
|
+
"venombrowser": {
|
|
134
|
+
command: "node",
|
|
135
|
+
args: [absPath],
|
|
136
|
+
env: {
|
|
137
|
+
BRIDGE_PORT: "10086"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, null, 2));
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main().catch(console.error);
|
package/package.json
CHANGED
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
|
|
@@ -188,8 +188,9 @@ const TOOL_DEFINITIONS = [
|
|
|
188
188
|
parameters: {
|
|
189
189
|
type: 'object',
|
|
190
190
|
properties: {
|
|
191
|
-
selector: { type: 'string', description: '
|
|
192
|
-
files: { type: 'array', items: { type: 'string' }, description: '
|
|
191
|
+
selector: { type: 'string', description: '@e ref or CSS selector' },
|
|
192
|
+
files: { type: 'array', items: { type: 'string' }, description: 'Absolute paths to files' },
|
|
193
|
+
confirm: { type: 'boolean', description: 'Required: set to true to confirm this action' }
|
|
193
194
|
},
|
|
194
195
|
required: ['selector', 'files']
|
|
195
196
|
}
|
|
@@ -233,8 +234,188 @@ const TOOL_DEFINITIONS = [
|
|
|
233
234
|
function: {
|
|
234
235
|
name: 'close_session',
|
|
235
236
|
description: 'Close all tabs in the session.',
|
|
237
|
+
parameters: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
confirm: { type: 'boolean', description: 'Required: set to true to confirm closing all tabs' }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
// ── Phase 1: Cheap Wins ──
|
|
246
|
+
{
|
|
247
|
+
type: 'function',
|
|
248
|
+
function: {
|
|
249
|
+
name: 'scroll_to',
|
|
250
|
+
description: 'Scroll an element into view. Use before clicking elements that may be off-screen.',
|
|
251
|
+
parameters: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
selector: { type: 'string', description: '@e ref or CSS selector of element to scroll to' }
|
|
255
|
+
},
|
|
256
|
+
required: ['selector']
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: 'function',
|
|
262
|
+
function: {
|
|
263
|
+
name: 'check_visibility',
|
|
264
|
+
description: 'Check if an element is visible, in viewport, and not obscured by overlays (cookie banners, modals, etc).',
|
|
265
|
+
parameters: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
selector: { type: 'string', description: '@e ref or CSS selector' }
|
|
269
|
+
},
|
|
270
|
+
required: ['selector']
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
type: 'function',
|
|
276
|
+
function: {
|
|
277
|
+
name: 'check_stale',
|
|
278
|
+
description: 'Check if an @e ref from a previous snapshot is still valid (element still in DOM).',
|
|
279
|
+
parameters: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
selector: { type: 'string', description: '@e ref (e.g. "@e5")' }
|
|
283
|
+
},
|
|
284
|
+
required: ['selector']
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
type: 'function',
|
|
290
|
+
function: {
|
|
291
|
+
name: 'double_click',
|
|
292
|
+
description: 'Double-click an element. Useful for text selection or opening items.',
|
|
293
|
+
parameters: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
selector: { type: 'string', description: '@e ref or CSS selector' }
|
|
297
|
+
},
|
|
298
|
+
required: ['selector']
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
type: 'function',
|
|
304
|
+
function: {
|
|
305
|
+
name: 'right_click',
|
|
306
|
+
description: 'Right-click an element to trigger context menu.',
|
|
307
|
+
parameters: {
|
|
308
|
+
type: 'object',
|
|
309
|
+
properties: {
|
|
310
|
+
selector: { type: 'string', description: '@e ref or CSS selector' }
|
|
311
|
+
},
|
|
312
|
+
required: ['selector']
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
type: 'function',
|
|
318
|
+
function: {
|
|
319
|
+
name: 'keyboard',
|
|
320
|
+
description: 'Press a key or key combination. Uses real hardware-level key events via CDP. Examples: key="Enter", key="a" modifiers=["Control"] for Ctrl+A.',
|
|
321
|
+
parameters: {
|
|
322
|
+
type: 'object',
|
|
323
|
+
properties: {
|
|
324
|
+
key: { type: 'string', description: 'Key name: Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right, Space, F1-F12, or any letter/number.' },
|
|
325
|
+
modifiers: { type: 'array', items: { type: 'string', enum: ['Control', 'Alt', 'Shift', 'Meta'] }, description: 'Modifier keys to hold. Default none.' }
|
|
326
|
+
},
|
|
327
|
+
required: ['key']
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
type: 'function',
|
|
333
|
+
function: {
|
|
334
|
+
name: 'go_back',
|
|
335
|
+
description: 'Navigate back in browser history (like clicking the back button).',
|
|
236
336
|
parameters: { type: 'object', properties: {} }
|
|
237
337
|
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: 'function',
|
|
341
|
+
function: {
|
|
342
|
+
name: 'go_forward',
|
|
343
|
+
description: 'Navigate forward in browser history.',
|
|
344
|
+
parameters: { type: 'object', properties: {} }
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
type: 'function',
|
|
349
|
+
function: {
|
|
350
|
+
name: 'manage_cookies',
|
|
351
|
+
description: 'Read, set, or delete cookies for the current page domain.',
|
|
352
|
+
parameters: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
cmd: { type: 'string', enum: ['get', 'get_all', 'set', 'delete', 'clear'], description: 'Cookie operation' },
|
|
356
|
+
name: { type: 'string', description: 'Cookie name (required for get/set/delete)' },
|
|
357
|
+
value: { type: 'string', description: 'Cookie value (for set)' },
|
|
358
|
+
domain: { type: 'string', description: 'Cookie domain (optional, defaults to current domain)' },
|
|
359
|
+
confirm: { type: 'boolean', description: 'Required: true if cmd is delete or clear' }
|
|
360
|
+
},
|
|
361
|
+
required: ['cmd']
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
type: 'function',
|
|
367
|
+
function: {
|
|
368
|
+
name: 'manage_storage',
|
|
369
|
+
description: 'Read, set, or delete localStorage/sessionStorage entries for the current page.',
|
|
370
|
+
parameters: {
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
cmd: { type: 'string', enum: ['get', 'get_all', 'set', 'delete', 'clear', 'keys'], description: 'Storage operation' },
|
|
374
|
+
storage_type: { type: 'string', enum: ['local', 'session'], description: 'Which storage to use. Default "local".' },
|
|
375
|
+
key: { type: 'string', description: 'Storage key (required for get/set/delete)' },
|
|
376
|
+
value: { type: 'string', description: 'Value to store (for set). Objects will be JSON-stringified.' },
|
|
377
|
+
confirm: { type: 'boolean', description: 'Required: true if cmd is delete or clear' }
|
|
378
|
+
},
|
|
379
|
+
required: ['cmd']
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
// ── Lifecycle Management ──
|
|
384
|
+
{
|
|
385
|
+
type: 'function',
|
|
386
|
+
function: {
|
|
387
|
+
name: 'bridge_status',
|
|
388
|
+
description: 'Check the VenomBrowser bridge daemon status: whether the server is running, the extension is connected, uptime, and command count.',
|
|
389
|
+
parameters: { type: 'object', properties: {} }
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
type: 'function',
|
|
394
|
+
function: {
|
|
395
|
+
name: 'stop_server',
|
|
396
|
+
description: 'Stop the VenomBrowser bridge daemon completely. This releases the port and disconnects the extension. Use when you are completely done with browser automation and want to free resources.',
|
|
397
|
+
parameters: {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: {
|
|
400
|
+
confirm: { type: 'boolean', description: 'Required: set to true to confirm stopping the server' }
|
|
401
|
+
},
|
|
402
|
+
required: ['confirm']
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
type: 'function',
|
|
408
|
+
function: {
|
|
409
|
+
name: 'restart_server',
|
|
410
|
+
description: 'Restart the VenomBrowser bridge daemon. Use this to fix connection issues — the daemon will shut down and auto-restart on your next browser command.',
|
|
411
|
+
parameters: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
confirm: { type: 'boolean', description: 'Required: set to true to confirm restarting the server' }
|
|
415
|
+
},
|
|
416
|
+
required: ['confirm']
|
|
417
|
+
}
|
|
418
|
+
}
|
|
238
419
|
}
|
|
239
420
|
];
|
|
240
421
|
|
|
@@ -340,6 +521,88 @@ async function executeTool(bridge, name, args = {}) {
|
|
|
340
521
|
return { closed: r.closed };
|
|
341
522
|
}
|
|
342
523
|
|
|
524
|
+
// ── Phase 1: Cheap Wins ──
|
|
525
|
+
case 'scroll_to': {
|
|
526
|
+
const r = await bridge.scrollTo(args.selector);
|
|
527
|
+
return r;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
case 'check_visibility': {
|
|
531
|
+
const r = await bridge.checkVisibility(args.selector);
|
|
532
|
+
return r;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
case 'check_stale': {
|
|
536
|
+
const r = await bridge.checkStale(args.selector);
|
|
537
|
+
return r;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case 'double_click': {
|
|
541
|
+
const r = await bridge.doubleClick(args.selector);
|
|
542
|
+
return { success: true, tag: r.tag, text: r.text };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case 'right_click': {
|
|
546
|
+
const r = await bridge.rightClick(args.selector);
|
|
547
|
+
return { success: true, tag: r.tag, text: r.text };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
case 'keyboard': {
|
|
551
|
+
const r = await bridge.keyboard(args.key, { modifiers: args.modifiers });
|
|
552
|
+
return r;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
case 'go_back': {
|
|
556
|
+
const r = await bridge.goBack();
|
|
557
|
+
return r;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case 'go_forward': {
|
|
561
|
+
const r = await bridge.goForward();
|
|
562
|
+
return r;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
case 'manage_cookies': {
|
|
566
|
+
const r = await bridge.manageCookies(args.cmd, { name: args.name, value: args.value, domain: args.domain });
|
|
567
|
+
return r;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'manage_storage': {
|
|
571
|
+
const r = await bridge.manageStorage(args.cmd, { storageType: args.storage_type, key: args.key, value: args.value });
|
|
572
|
+
return r;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── Lifecycle Management ──
|
|
576
|
+
case 'bridge_status': {
|
|
577
|
+
const r = await bridge.bridgeStatus();
|
|
578
|
+
return r;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case 'stop_server': {
|
|
582
|
+
try {
|
|
583
|
+
const r = await bridge.stopServer();
|
|
584
|
+
return { success: true, message: r.message || 'Server is shutting down.' };
|
|
585
|
+
} catch (e) {
|
|
586
|
+
// Server may close connection before responding — that's expected
|
|
587
|
+
if (e.message?.includes('ECONNRESET') || e.message?.includes('ECONNREFUSED') || e.message?.includes('socket hang up')) {
|
|
588
|
+
return { success: true, message: 'Server has shut down.' };
|
|
589
|
+
}
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
case 'restart_server': {
|
|
595
|
+
try {
|
|
596
|
+
const r = await bridge.restartServer();
|
|
597
|
+
return { success: true, message: r.message || 'Server is restarting. Your next browser command will auto-spawn a new daemon.' };
|
|
598
|
+
} catch (e) {
|
|
599
|
+
if (e.message?.includes('ECONNRESET') || e.message?.includes('ECONNREFUSED') || e.message?.includes('socket hang up')) {
|
|
600
|
+
return { success: true, message: 'Server has shut down for restart. Your next browser command will auto-spawn a new daemon.' };
|
|
601
|
+
}
|
|
602
|
+
throw e;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
343
606
|
default:
|
|
344
607
|
throw new Error(`Unknown tool: ${name}`);
|
|
345
608
|
}
|