venombrowser 4.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 +26 -0
- package/bin/venombrowser-agent.js +182 -0
- package/bin/venombrowser-chat.js +300 -0
- package/bin/venombrowser-cli.js +194 -0
- package/bin/venombrowser-mcp.js +30 -0
- package/bin/venombrowser-server.js +11 -0
- package/bin/venombrowser-setup.js +183 -0
- package/package.json +65 -0
- package/src/bridge-client.js +207 -0
- package/src/bridge-server.js +476 -0
- package/src/connect.js +220 -0
- package/src/mcp-server.js +101 -0
- package/src/providers/index.js +47 -0
- package/src/providers/ollama.js +108 -0
- package/src/providers/openrouter.js +158 -0
- package/src/tools/browser-tools.js +364 -0
- package/src/uninstall.js +246 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser MCP Server — CLI entry point
|
|
4
|
+
* Starts the MCP server in stdio mode for agent integration.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node bin/venombrowser-mcp.js
|
|
7
|
+
* Or: npm run mcp
|
|
8
|
+
*
|
|
9
|
+
* Agent configuration example (Claude Code):
|
|
10
|
+
* {
|
|
11
|
+
* "mcpServers": {
|
|
12
|
+
* "venombrowser": {
|
|
13
|
+
* "command": "node",
|
|
14
|
+
* "args": ["C:/path/to/VenomBrowser/server/bin/venombrowser-mcp.js"],
|
|
15
|
+
* "env": { "REST_API_KEY": "your-bridge-api-key" }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Load .env for default config — agents can also pass env vars directly
|
|
22
|
+
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
|
23
|
+
|
|
24
|
+
const VenomBrowserMcpServer = require('../src/mcp-server');
|
|
25
|
+
|
|
26
|
+
const server = new VenomBrowserMcpServer();
|
|
27
|
+
server.start().catch(err => {
|
|
28
|
+
console.error('[VenomBrowser MCP] Failed to start:', err.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser Bridge Server — CLI entry point
|
|
4
|
+
* Starts the WebSocket + REST API bridge server.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node bin/venombrowser-server.js
|
|
7
|
+
* Or: npm start
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
|
11
|
+
require('../src/bridge-server');
|
|
@@ -0,0 +1,183 @@
|
|
|
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. Generating a REST_API_KEY if missing
|
|
8
|
+
* 3. Prompting for WS_AUTH_TOKEN (from extension popup)
|
|
9
|
+
* 4. Configuring the LLM provider
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node bin/venombrowser-setup.js
|
|
13
|
+
* node bin/venombrowser-setup.js --auto # Non-interactive: generate keys, use defaults
|
|
14
|
+
* node bin/venombrowser-setup.js --token=<tok> # Set the WS_AUTH_TOKEN directly
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const readline = require('readline');
|
|
21
|
+
|
|
22
|
+
const ENV_PATH = path.join(__dirname, '..', '.env');
|
|
23
|
+
const EXAMPLE_PATH = path.join(__dirname, '..', '.env.example');
|
|
24
|
+
|
|
25
|
+
function generateKey(length = 32) {
|
|
26
|
+
return crypto.randomBytes(length).toString('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseEnv(content) {
|
|
30
|
+
const vars = {};
|
|
31
|
+
for (const line of content.split('\n')) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
34
|
+
const eqIndex = trimmed.indexOf('=');
|
|
35
|
+
if (eqIndex === -1) continue;
|
|
36
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
37
|
+
const val = trimmed.slice(eqIndex + 1).trim();
|
|
38
|
+
vars[key] = val;
|
|
39
|
+
}
|
|
40
|
+
return vars;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateEnvFile(updates) {
|
|
44
|
+
let content;
|
|
45
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
46
|
+
content = fs.readFileSync(ENV_PATH, 'utf8');
|
|
47
|
+
} else if (fs.existsSync(EXAMPLE_PATH)) {
|
|
48
|
+
content = fs.readFileSync(EXAMPLE_PATH, 'utf8');
|
|
49
|
+
} else {
|
|
50
|
+
// Build minimal .env
|
|
51
|
+
content = Object.entries(updates).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
|
|
52
|
+
fs.writeFileSync(ENV_PATH, content);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
57
|
+
// Replace existing key= lines, or append
|
|
58
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
59
|
+
if (regex.test(content)) {
|
|
60
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
61
|
+
} else {
|
|
62
|
+
content += `\n${key}=${value}\n`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(ENV_PATH, content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ask(rl, question) {
|
|
70
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
const args = process.argv.slice(2);
|
|
75
|
+
const isAuto = args.includes('--auto');
|
|
76
|
+
const tokenArg = args.find(a => a.startsWith('--token='));
|
|
77
|
+
const tokenValue = tokenArg ? tokenArg.split('=').slice(1).join('=') : null;
|
|
78
|
+
|
|
79
|
+
console.log('🕷️ VenomBrowser Bridge Setup');
|
|
80
|
+
console.log('═'.repeat(50));
|
|
81
|
+
|
|
82
|
+
// Load existing env
|
|
83
|
+
let existing = {};
|
|
84
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
85
|
+
existing = parseEnv(fs.readFileSync(ENV_PATH, 'utf8'));
|
|
86
|
+
console.log('📄 Found existing .env file');
|
|
87
|
+
} else {
|
|
88
|
+
console.log('📄 No .env file found — will create one');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const updates = {};
|
|
92
|
+
|
|
93
|
+
// ── REST_API_KEY ─────────────────────────────────────────────────────────
|
|
94
|
+
if (!existing.REST_API_KEY || existing.REST_API_KEY.includes('pick-any') || existing.REST_API_KEY.includes('your')) {
|
|
95
|
+
const key = generateKey();
|
|
96
|
+
updates.REST_API_KEY = key;
|
|
97
|
+
console.log(`🔑 Generated REST_API_KEY: ${key}`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`🔑 REST_API_KEY: ${existing.REST_API_KEY.slice(0, 8)}...`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── WS_AUTH_TOKEN ────────────────────────────────────────────────────────
|
|
103
|
+
if (tokenValue) {
|
|
104
|
+
updates.WS_AUTH_TOKEN = tokenValue;
|
|
105
|
+
console.log(`🔐 WS_AUTH_TOKEN set from --token flag`);
|
|
106
|
+
} else if (isAuto) {
|
|
107
|
+
if (!existing.WS_AUTH_TOKEN || existing.WS_AUTH_TOKEN.includes('paste')) {
|
|
108
|
+
console.log('⚠️ WS_AUTH_TOKEN not set. After loading the extension, run:');
|
|
109
|
+
console.log(' node bin/venombrowser-setup.js --token=<paste-token-from-extension-popup>');
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Interactive
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
|
|
115
|
+
if (!existing.WS_AUTH_TOKEN || existing.WS_AUTH_TOKEN.includes('paste')) {
|
|
116
|
+
console.log();
|
|
117
|
+
console.log('📋 To get your WS_AUTH_TOKEN:');
|
|
118
|
+
console.log(' 1. Load the extension/ folder in chrome://extensions/');
|
|
119
|
+
console.log(' 2. Click the VenomBrowser Bridge icon in your toolbar');
|
|
120
|
+
console.log(' 3. Click the auth token to copy it');
|
|
121
|
+
console.log();
|
|
122
|
+
const token = await ask(rl, '🔐 Paste your Auth Token (or press Enter to skip): ');
|
|
123
|
+
if (token.trim()) {
|
|
124
|
+
updates.WS_AUTH_TOKEN = token.trim();
|
|
125
|
+
console.log('✅ WS_AUTH_TOKEN saved!');
|
|
126
|
+
} else {
|
|
127
|
+
console.log('⏭️ Skipped — you can set it later with: node bin/venombrowser-setup.js --token=<token>');
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`🔐 WS_AUTH_TOKEN: ${existing.WS_AUTH_TOKEN.slice(0, 8)}...`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
rl.close();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Save ──────────────────────────────────────────────────────────────────
|
|
137
|
+
if (Object.keys(updates).length > 0) {
|
|
138
|
+
updateEnvFile(updates);
|
|
139
|
+
console.log();
|
|
140
|
+
console.log('💾 Configuration saved to server/.env');
|
|
141
|
+
} else {
|
|
142
|
+
console.log();
|
|
143
|
+
console.log('✅ Configuration is already up to date');
|
|
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. venombrowser start — start the bridge server');
|
|
153
|
+
} else {
|
|
154
|
+
console.log(' 1. venombrowser start — start the bridge server');
|
|
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
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "venombrowser",
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Agent-agnostic browser automation platform — give any AI agent eyes and hands in your real Chrome browser",
|
|
5
|
+
"author": "TechVenom",
|
|
6
|
+
"main": "src/bridge-server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"venombrowser": "bin/venombrowser-cli.js",
|
|
9
|
+
"venombrowser-server": "bin/venombrowser-server.js",
|
|
10
|
+
"venombrowser-mcp": "bin/venombrowser-mcp.js",
|
|
11
|
+
"venombrowser-agent": "bin/venombrowser-agent.js",
|
|
12
|
+
"venombrowser-chat": "bin/venombrowser-chat.js",
|
|
13
|
+
"venombrowser-setup": "bin/venombrowser-setup.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"src/",
|
|
18
|
+
".env.example"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"setup": "node bin/venombrowser-setup.js",
|
|
22
|
+
"start": "node bin/venombrowser-server.js",
|
|
23
|
+
"mcp": "node bin/venombrowser-mcp.js",
|
|
24
|
+
"agent": "node bin/venombrowser-agent.js",
|
|
25
|
+
"chat": "node bin/venombrowser-chat.js",
|
|
26
|
+
"test": "node test/test-connection.js",
|
|
27
|
+
"prepublishOnly": "node -e \"const p=require('./package.json'); if(!p.bin||!p.bin.venombrowser){console.error('ERROR: missing bin entry');process.exit(1)} console.log('prepublishOnly: bin entry OK')\""
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"browser",
|
|
31
|
+
"automation",
|
|
32
|
+
"mcp",
|
|
33
|
+
"ai-agent",
|
|
34
|
+
"chrome-extension",
|
|
35
|
+
"playwright-alternative",
|
|
36
|
+
"llm",
|
|
37
|
+
"openai",
|
|
38
|
+
"anthropic",
|
|
39
|
+
"opencode",
|
|
40
|
+
"cursor"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/TechVenom/VenomBrowser.git",
|
|
45
|
+
"directory": "server"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/TechVenom/VenomBrowser#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/TechVenom/VenomBrowser/issues"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.6.0",
|
|
57
|
+
"axios": "^1.7.7",
|
|
58
|
+
"cors": "^2.8.5",
|
|
59
|
+
"dotenv": "^16.4.5",
|
|
60
|
+
"express": "^4.21.0",
|
|
61
|
+
"jsonc-parser": "^3.3.1",
|
|
62
|
+
"uuid": "^9.0.1",
|
|
63
|
+
"ws": "^8.18.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VenomBrowser Bridge Client — Kimi-style HTTP client
|
|
3
|
+
*
|
|
4
|
+
* Communicates with the daemon via single POST /command endpoint.
|
|
5
|
+
* Format: { action, args, session }
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const client = new BrowserBridge({ session: 'my-task' });
|
|
9
|
+
* await client.navigate('https://example.com');
|
|
10
|
+
* const snapshot = await client.snapshot();
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
|
|
15
|
+
class BridgeError extends Error {}
|
|
16
|
+
class BridgeNotConnectedError extends BridgeError {}
|
|
17
|
+
class BridgeTimeoutError extends BridgeError {}
|
|
18
|
+
class BridgeCommandError extends BridgeError {}
|
|
19
|
+
|
|
20
|
+
class BrowserBridge {
|
|
21
|
+
constructor({ host = '127.0.0.1', port = 10086, session = 'default' } = {}) {
|
|
22
|
+
this.baseUrl = `http://${host}:${port}`;
|
|
23
|
+
this.session = session;
|
|
24
|
+
this.http = axios.create({
|
|
25
|
+
baseURL: this.baseUrl,
|
|
26
|
+
timeout: 60000
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Core command method ─────────────────────────────────────────────────
|
|
31
|
+
async command(action, args = {}, timeout = 30000) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await this.http.post('/command', {
|
|
34
|
+
action,
|
|
35
|
+
args,
|
|
36
|
+
session: this.session
|
|
37
|
+
}, { timeout });
|
|
38
|
+
return response.data;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
this._handleError(e, action);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Connection ───────────────────────────────────────────────────────────
|
|
45
|
+
async isConnected() {
|
|
46
|
+
try {
|
|
47
|
+
const r = await this.http.get('/health');
|
|
48
|
+
return r.data.server === 'ok';
|
|
49
|
+
} catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getStatus() {
|
|
53
|
+
return this._get('/status');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Navigation (Kimi-style) ─────────────────────────────────────────────
|
|
57
|
+
async navigate(url, { newTab = false, group_title } = {}) {
|
|
58
|
+
return this.command('navigate', { url, newTab, group_title }, 45000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Tab management (Kimi-style) ─────────────────────────────────────────
|
|
62
|
+
async findTab(url, { active = false } = {}) {
|
|
63
|
+
return this.command('find_tab', { url, active });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async listTabs() {
|
|
67
|
+
const r = await this.command('list_tabs');
|
|
68
|
+
return r.tabs || [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async closeTab(tabId) {
|
|
72
|
+
return this.command('close_tab', { tabId });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async closeSession() {
|
|
76
|
+
return this.command('close_session');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Content interaction (Kimi-style) ────────────────────────────────────
|
|
80
|
+
async snapshot() {
|
|
81
|
+
return this.command('snapshot');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async click(selector) {
|
|
85
|
+
return this.command('click', { selector });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fill(selector, value) {
|
|
89
|
+
return this.command('fill', { selector, value });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async type(selector, text, { pressEnter = false } = {}) {
|
|
93
|
+
return this.command('type', { selector, text, pressEnter });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async submitForm(selector) {
|
|
97
|
+
return this.command('submit_form', { selector });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async evaluate(code) {
|
|
101
|
+
return this.command('evaluate', { code }, 30000);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async cdp(method, params = {}) {
|
|
105
|
+
return this.command('cdp', { method, params }, 30000);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Screenshots (Kimi-style — returns file path, not base64) ────────────
|
|
109
|
+
async screenshot({ format = 'png', quality, selector, path } = {}) {
|
|
110
|
+
return this.command('screenshot', { format, quality, selector, path });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Network monitoring (Kimi-style) ─────────────────────────────────────
|
|
114
|
+
async network(cmd, { filter, requestId } = {}) {
|
|
115
|
+
return this.command('network', { cmd, filter, requestId });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── File upload (Kimi-style) ────────────────────────────────────────────
|
|
119
|
+
async upload(selector, files) {
|
|
120
|
+
return this.command('upload', { selector, files });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── Save as PDF (Kimi-style) ────────────────────────────────────────────
|
|
124
|
+
async saveAsPdf({ paper_format, landscape, scale, print_background, path } = {}) {
|
|
125
|
+
return this.command('save_as_pdf', { paper_format, landscape, scale, print_background, path }, 60000);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Scroll ───────────────────────────────────────────────────────────────
|
|
129
|
+
async scroll(direction = 'down', amount = 500) {
|
|
130
|
+
return this.command('scroll', { direction, amount });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Legacy compatibility (maps old names to new) ─────────────────────────
|
|
134
|
+
async extract(selector = null) {
|
|
135
|
+
const r = await this.evaluate(`
|
|
136
|
+
(() => {
|
|
137
|
+
${selector
|
|
138
|
+
? `const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
|
|
139
|
+
return el ? { text: el.innerText || el.textContent || '' } : null;`
|
|
140
|
+
: `const clone = document.body.cloneNode(true);
|
|
141
|
+
['script','style','noscript','svg','iframe'].forEach(t =>
|
|
142
|
+
clone.querySelectorAll(t).forEach(e => e.remove()));
|
|
143
|
+
return { text: clone.innerText || clone.textContent || '' };`
|
|
144
|
+
}
|
|
145
|
+
})()
|
|
146
|
+
`);
|
|
147
|
+
return r.value?.text || '';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getLinks({ selector = 'a' } = {}) {
|
|
151
|
+
const r = await this.evaluate(`
|
|
152
|
+
(() => {
|
|
153
|
+
return Array.from(document.querySelectorAll('${selector.replace(/'/g, "\\'")}'))
|
|
154
|
+
.map(a => ({ text: (a.innerText||'').trim(), href: a.href }))
|
|
155
|
+
.filter(l => l.href && l.href !== location.href);
|
|
156
|
+
})()
|
|
157
|
+
`);
|
|
158
|
+
return r.value || [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async getForms() {
|
|
162
|
+
const r = await this.evaluate(`
|
|
163
|
+
(() => {
|
|
164
|
+
return Array.from(document.forms).map((f, i) => ({
|
|
165
|
+
index: i, action: f.action, method: f.method,
|
|
166
|
+
fields: Array.from(f.elements).map(e => ({
|
|
167
|
+
name: e.name, type: e.type, tag: e.tagName.toLowerCase(),
|
|
168
|
+
value: e.value, placeholder: e.placeholder || null
|
|
169
|
+
}))
|
|
170
|
+
}));
|
|
171
|
+
})()
|
|
172
|
+
`);
|
|
173
|
+
return r.value || [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async getCurrentUrl() {
|
|
177
|
+
const r = await this.evaluate('location.href');
|
|
178
|
+
return r.value || '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getPageTitle() {
|
|
182
|
+
const r = await this.evaluate('document.title');
|
|
183
|
+
return r.value || '';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────
|
|
187
|
+
async _get(path) {
|
|
188
|
+
try {
|
|
189
|
+
return (await this.http.get(path)).data;
|
|
190
|
+
} catch (e) { this._handleError(e, `GET ${path}`); }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_handleError(e, context) {
|
|
194
|
+
if (e.response?.status === 503) throw new BridgeNotConnectedError('Extension not connected');
|
|
195
|
+
if (e.response?.status === 408) throw new BridgeTimeoutError(`${context} timed out`);
|
|
196
|
+
if (e.code === 'ECONNREFUSED') throw new BridgeNotConnectedError(`Server not running on ${this.baseUrl}`);
|
|
197
|
+
throw new BridgeCommandError(`${context}: ${e.response?.data?.error ?? e.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
BrowserBridge,
|
|
203
|
+
BridgeError,
|
|
204
|
+
BridgeNotConnectedError,
|
|
205
|
+
BridgeTimeoutError,
|
|
206
|
+
BridgeCommandError
|
|
207
|
+
};
|