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
package/.env.example
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# VenomBrowser Bridge Server — Kimi-style configuration
|
|
2
|
+
# v4.0.0
|
|
3
|
+
|
|
4
|
+
# ─── Server Config ──────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
# Single port for the daemon (HTTP + WebSocket)
|
|
7
|
+
BRIDGE_PORT=10086
|
|
8
|
+
|
|
9
|
+
# WebSocket port for extension (BRIDGE_PORT + 1 by default)
|
|
10
|
+
WS_PORT=10087
|
|
11
|
+
|
|
12
|
+
# ─── LLM Provider Config ─────────────────────────────────────────────────────
|
|
13
|
+
# Used by bin/venombrowser-agent.js and bin/venombrowser-chat.js
|
|
14
|
+
|
|
15
|
+
# Which provider to use: "openrouter" or "ollama"
|
|
16
|
+
LLM_PROVIDER=openrouter
|
|
17
|
+
|
|
18
|
+
# OpenRouter (Default)
|
|
19
|
+
# Get a free API key at https://openrouter.ai/keys
|
|
20
|
+
OPENROUTER_API_KEY=your-sk-or-v1-key
|
|
21
|
+
OPENROUTER_MODEL=openrouter/free
|
|
22
|
+
|
|
23
|
+
# Ollama (Local)
|
|
24
|
+
# Requires Ollama running locally (ollama serve)
|
|
25
|
+
OLLAMA_HOST=http://localhost:11434
|
|
26
|
+
OLLAMA_MODEL=qwen2.5:7b
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser Agent — CLI entry point (Kimi-style)
|
|
4
|
+
*
|
|
5
|
+
* Standalone browser automation agent powered by any configured LLM provider.
|
|
6
|
+
* Connects to the VenomBrowser daemon and executes browser tasks.
|
|
7
|
+
*
|
|
8
|
+
* v4.0.0 — Kimi-style architecture:
|
|
9
|
+
* - Uses new bridge-client with Kimi-style protocol
|
|
10
|
+
* - Screenshot returns file path, NOT base64 (fixes cost leak)
|
|
11
|
+
* - Uses snapshot() with @e refs for element targeting
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/venombrowser-agent.js "search google for weather in tokyo"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
|
18
|
+
|
|
19
|
+
const { BrowserBridge } = require('../src/bridge-client');
|
|
20
|
+
const { createProvider } = require('../src/providers');
|
|
21
|
+
const { TOOL_DEFINITIONS, executeTool } = require('../src/tools/browser-tools');
|
|
22
|
+
|
|
23
|
+
const MAX_STEPS = parseInt(process.env.MAX_STEPS || '25', 10);
|
|
24
|
+
const session = `agent-${Date.now()}`;
|
|
25
|
+
|
|
26
|
+
// ─── Logging ─────────────────────────────────────────────────────────────────
|
|
27
|
+
function log(level, message, data = null) {
|
|
28
|
+
const timestamp = new Date().toISOString().slice(11, 19);
|
|
29
|
+
const emoji = {
|
|
30
|
+
info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌',
|
|
31
|
+
tool: '🔧', agent: '🤖', result: '📊'
|
|
32
|
+
}[level] || 'ℹ️';
|
|
33
|
+
|
|
34
|
+
console.log(`[${timestamp}] ${emoji} ${message}`);
|
|
35
|
+
if (data && typeof data === 'object') {
|
|
36
|
+
console.log(` ${JSON.stringify(data, null, 2).split('\n').join('\n ')}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
41
|
+
async function main() {
|
|
42
|
+
let provider;
|
|
43
|
+
try {
|
|
44
|
+
provider = createProvider();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`❌ Provider error: ${err.message}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const bridge = new BrowserBridge({ session });
|
|
51
|
+
|
|
52
|
+
console.log('🚀 VenomBrowser Agent v4.0.0 (Kimi-style)');
|
|
53
|
+
console.log('═'.repeat(55));
|
|
54
|
+
log('info', `Provider: ${provider}`);
|
|
55
|
+
log('info', `Session: ${session}`);
|
|
56
|
+
log('info', `Max steps: ${MAX_STEPS}`);
|
|
57
|
+
console.log('═'.repeat(55));
|
|
58
|
+
|
|
59
|
+
// System checks
|
|
60
|
+
log('info', 'Running system checks...');
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await provider.healthCheck();
|
|
64
|
+
log('success', `LLM provider ready: ${provider}`);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
log('error', `LLM check failed: ${err.message}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const connected = await bridge.isConnected();
|
|
72
|
+
if (!connected) throw new Error('VenomBrowser daemon not connected');
|
|
73
|
+
log('success', 'VenomBrowser daemon connected');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log('error', `Daemon error: ${err.message}`);
|
|
76
|
+
log('info', 'Make sure daemon is running: npm start');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log();
|
|
81
|
+
|
|
82
|
+
const task = process.argv.slice(2).join(' ');
|
|
83
|
+
if (!task) {
|
|
84
|
+
console.error('❌ Usage: node venombrowser-agent.js "<task description>"');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Agent loop ──────────────────────────────────────────────────────
|
|
89
|
+
const sessionStart = Date.now();
|
|
90
|
+
let totalToolCalls = 0;
|
|
91
|
+
|
|
92
|
+
const messages = [
|
|
93
|
+
{
|
|
94
|
+
role: 'system',
|
|
95
|
+
content: `You are VenomBrowser, a browser automation agent controlling a real Chrome browser.
|
|
96
|
+
|
|
97
|
+
CRITICAL RULES:
|
|
98
|
+
- ALWAYS call snapshot() after navigating to understand the page before clicking anything.
|
|
99
|
+
- Use @e refs from snapshot to click/fill elements (e.g. click("@e3"), fill("@e5", "text")).
|
|
100
|
+
- NEVER guess CSS selectors. Use @e refs from snapshot instead.
|
|
101
|
+
|
|
102
|
+
NAVIGATION STRATEGY:
|
|
103
|
+
1. navigate() to the target URL
|
|
104
|
+
2. snapshot() to see all interactive elements with @e refs
|
|
105
|
+
3. click("@eN") or fill("@eN", "text") to interact
|
|
106
|
+
|
|
107
|
+
FORM FILLING: Call snapshot() to see all form fields with their @e refs. Use fill() for inputs, click() for submit.
|
|
108
|
+
|
|
109
|
+
ERROR RECOVERY: If an action fails, re-run snapshot() to get fresh @e refs. Never repeat the same failed action.
|
|
110
|
+
|
|
111
|
+
When done, respond with plain text summarizing what you found or accomplished.`
|
|
112
|
+
},
|
|
113
|
+
{ role: 'user', content: task }
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (let step = 1; step <= MAX_STEPS; step++) {
|
|
117
|
+
log('info', `═══ Step ${step}/${MAX_STEPS} ═══`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const reply = await provider.chat(messages, TOOL_DEFINITIONS);
|
|
121
|
+
|
|
122
|
+
if (reply.content) {
|
|
123
|
+
log('agent', reply.content);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!reply.tool_calls || reply.tool_calls.length === 0) {
|
|
127
|
+
const duration = ((Date.now() - sessionStart) / 1000).toFixed(1);
|
|
128
|
+
log('success', 'Agent completed the task!');
|
|
129
|
+
log('info', `Duration: ${duration}s | Tools: ${totalToolCalls} | Steps: ${step}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const assistantMsg = { role: 'assistant', content: reply.content || '' };
|
|
134
|
+
if (reply.tool_calls) assistantMsg.tool_calls = reply.tool_calls;
|
|
135
|
+
messages.push(assistantMsg);
|
|
136
|
+
|
|
137
|
+
for (const call of reply.tool_calls) {
|
|
138
|
+
const name = call.function.name;
|
|
139
|
+
const args = call.function.arguments || {};
|
|
140
|
+
|
|
141
|
+
log('tool', `→ ${name}(${JSON.stringify(args)})`);
|
|
142
|
+
totalToolCalls++;
|
|
143
|
+
|
|
144
|
+
let resultPayload;
|
|
145
|
+
try {
|
|
146
|
+
const result = await executeTool(bridge, name, args);
|
|
147
|
+
resultPayload = { success: true, result };
|
|
148
|
+
|
|
149
|
+
const preview = JSON.stringify(result);
|
|
150
|
+
log('result', preview.length > 200 ? preview.slice(0, 200) + '...' : preview);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
resultPayload = { success: false, error: error.message };
|
|
153
|
+
log('error', `Tool error: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
messages.push({
|
|
157
|
+
role: 'tool',
|
|
158
|
+
tool_call_id: call.id,
|
|
159
|
+
name,
|
|
160
|
+
content: JSON.stringify(resultPayload)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log('error', `Step ${step} failed: ${error.message}`);
|
|
165
|
+
messages.push({
|
|
166
|
+
role: 'system',
|
|
167
|
+
content: `Error occurred: ${error.message}. Try a different approach.`
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const duration = ((Date.now() - sessionStart) / 1000).toFixed(1);
|
|
175
|
+
log('warning', `Hit MAX_STEPS (${MAX_STEPS}) without completion.`);
|
|
176
|
+
log('info', `Duration: ${duration}s | Tools: ${totalToolCalls}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
main().catch(err => {
|
|
180
|
+
log('error', `Agent failed: ${err.message}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser Interactive Chat Agent — CLI entry point (Kimi-style)
|
|
4
|
+
*
|
|
5
|
+
* Live chat interface with the configured LLM provider + VenomBrowser daemon.
|
|
6
|
+
* Talk to the agent naturally and give it browser automation tasks in real-time.
|
|
7
|
+
*
|
|
8
|
+
* v4.0.0 — Kimi-style architecture:
|
|
9
|
+
* - Uses new bridge-client with Kimi-style protocol
|
|
10
|
+
* - Screenshot returns file path, NOT base64 (fixes cost leak)
|
|
11
|
+
* - Uses snapshot() with @e refs for element targeting
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node bin/venombrowser-chat.js
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
|
|
18
|
+
const readline = require('readline');
|
|
19
|
+
const { BrowserBridge } = require('../src/bridge-client');
|
|
20
|
+
const { createProvider } = require('../src/providers');
|
|
21
|
+
const { TOOL_DEFINITIONS, executeTool } = require('../src/tools/browser-tools');
|
|
22
|
+
|
|
23
|
+
const MAX_STEPS = parseInt(process.env.MAX_STEPS || '25', 10);
|
|
24
|
+
const session = `chat-${Date.now()}`;
|
|
25
|
+
|
|
26
|
+
// ─── Logging ─────────────────────────────────────────────────────────────────
|
|
27
|
+
function log(level, message, skipTimestamp = false) {
|
|
28
|
+
const timestamp = new Date().toISOString().slice(11, 19);
|
|
29
|
+
const emoji = {
|
|
30
|
+
info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌',
|
|
31
|
+
tool: '🔧', agent: '🤖', user: '👤', system: '🔔'
|
|
32
|
+
}[level] || 'ℹ️';
|
|
33
|
+
|
|
34
|
+
if (skipTimestamp) {
|
|
35
|
+
console.log(`${emoji} ${message}`);
|
|
36
|
+
} else {
|
|
37
|
+
console.log(`[${timestamp}] ${emoji} ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── System Prompt (Kimi-style) ─────────────────────────────────────────────
|
|
42
|
+
const SYSTEM_PROMPT = `You are VenomBrowser, a browser automation assistant controlling a real Chrome browser.
|
|
43
|
+
|
|
44
|
+
## CRITICAL RULES — ALWAYS FOLLOW THESE
|
|
45
|
+
1. **ALWAYS use snapshot() after navigating** to understand what's on the page. This gives you an accessibility tree with @e refs for all interactive elements.
|
|
46
|
+
2. **Use click("@eN") and fill("@eN", "text")** to interact with elements. The @e refs from snapshot are MUCH more reliable than CSS selectors.
|
|
47
|
+
3. **NEVER guess CSS selectors.** The @e refs replace selector guessing entirely.
|
|
48
|
+
|
|
49
|
+
## WORKFLOW FOR ANY TASK
|
|
50
|
+
1. navigate() to the target URL
|
|
51
|
+
2. snapshot() to see all interactive elements with @e refs
|
|
52
|
+
3. click("@eN") or fill("@eN", "text") to interact
|
|
53
|
+
4. Repeat steps 2-3 as the page changes
|
|
54
|
+
|
|
55
|
+
## NAVIGATION STRATEGY
|
|
56
|
+
1. **Direct URL first**: For searches, use URL params: google.com/search?q=...
|
|
57
|
+
2. **@e ref click second**: Use click("@eN") after calling snapshot()
|
|
58
|
+
3. **Text-based click third**: Use click(text: "visible text") as fallback
|
|
59
|
+
|
|
60
|
+
## FILLING FORMS
|
|
61
|
+
1. Call snapshot() to see all form fields with their @e refs
|
|
62
|
+
2. Use fill("@eN", text) for each field
|
|
63
|
+
3. Use click("@eN") to submit
|
|
64
|
+
|
|
65
|
+
## EXTRACTING DATA
|
|
66
|
+
- Use evaluate() for structured data extraction
|
|
67
|
+
- Use snapshot() to understand page structure
|
|
68
|
+
|
|
69
|
+
## SCROLLING
|
|
70
|
+
- Use evaluate("window.scrollBy(0, 800)") to scroll
|
|
71
|
+
- Then snapshot() again to discover newly visible elements
|
|
72
|
+
|
|
73
|
+
## TAB MANAGEMENT
|
|
74
|
+
- Use list_tabs() to see all open tabs
|
|
75
|
+
- Use close_tab() / navigate(url, { newTab: true })
|
|
76
|
+
|
|
77
|
+
## ERROR RECOVERY
|
|
78
|
+
- If an element is stale, re-run snapshot() to get fresh @e refs
|
|
79
|
+
- If a click doesn't work, try a different @e ref
|
|
80
|
+
- Never repeat the same failed action
|
|
81
|
+
|
|
82
|
+
## RESPONSE STYLE
|
|
83
|
+
- Be concise. Summarize what you found/accomplished.
|
|
84
|
+
- When done, give the final answer directly.`;
|
|
85
|
+
|
|
86
|
+
// ─── Main Chat Interface ─────────────────────────────────────────────────────
|
|
87
|
+
class ChatAgent {
|
|
88
|
+
constructor() {
|
|
89
|
+
try {
|
|
90
|
+
this.provider = createProvider();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`❌ Provider error: ${err.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.bridge = new BrowserBridge({ session });
|
|
97
|
+
|
|
98
|
+
this.conversationHistory = [
|
|
99
|
+
{ role: 'system', content: SYSTEM_PROMPT }
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
this.rl = readline.createInterface({
|
|
103
|
+
input: process.stdin,
|
|
104
|
+
output: process.stdout,
|
|
105
|
+
prompt: '\n👤 You: '
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async start() {
|
|
110
|
+
console.log('🚀 VenomBrowser Interactive Chat Agent v4.0.0 (Kimi-style)');
|
|
111
|
+
console.log('═'.repeat(60));
|
|
112
|
+
log('info', `Provider: ${this.provider}`, true);
|
|
113
|
+
log('info', `Session: ${session}`, true);
|
|
114
|
+
log('info', `Max steps per task: ${MAX_STEPS}`, true);
|
|
115
|
+
console.log('═'.repeat(60));
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await this._systemCheck();
|
|
119
|
+
console.log();
|
|
120
|
+
|
|
121
|
+
log('agent', 'Hello! I can help you automate browser tasks.', true);
|
|
122
|
+
log('agent', 'Just tell me what you want me to do with the browser!', true);
|
|
123
|
+
console.log();
|
|
124
|
+
console.log('Examples:');
|
|
125
|
+
console.log(' • "go to google and search for python tutorials"');
|
|
126
|
+
console.log(' • "take a screenshot of this page"');
|
|
127
|
+
console.log(' • "find the top news stories on hacker news"');
|
|
128
|
+
console.log();
|
|
129
|
+
console.log('Commands:');
|
|
130
|
+
console.log(' • "clear" - clear conversation history');
|
|
131
|
+
console.log(' • "status" - show system status');
|
|
132
|
+
console.log(' • "quit" - exit the chat');
|
|
133
|
+
|
|
134
|
+
this.rl.prompt();
|
|
135
|
+
|
|
136
|
+
this.rl.on('line', async (input) => {
|
|
137
|
+
const message = input.trim();
|
|
138
|
+
|
|
139
|
+
if (message === '') {
|
|
140
|
+
this.rl.prompt();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const cmd = message.toLowerCase();
|
|
145
|
+
if (cmd === 'quit' || cmd === 'exit') {
|
|
146
|
+
log('system', 'Goodbye!', true);
|
|
147
|
+
this.rl.close();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (cmd === 'clear') {
|
|
152
|
+
this.conversationHistory = [this.conversationHistory[0]];
|
|
153
|
+
log('system', 'Conversation history cleared', true);
|
|
154
|
+
this.rl.prompt();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cmd === 'status') {
|
|
159
|
+
try {
|
|
160
|
+
const status = await this.bridge.getStatus();
|
|
161
|
+
log('info', `Bridge connected: ${status.connected}`, true);
|
|
162
|
+
log('info', `Commands executed: ${status.commands_executed}`, true);
|
|
163
|
+
log('info', `Uptime: ${status.uptime_seconds}s`, true);
|
|
164
|
+
log('info', `Provider: ${this.provider}`, true);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
log('error', `Status check failed: ${error.message}`, true);
|
|
167
|
+
}
|
|
168
|
+
this.rl.prompt();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log();
|
|
173
|
+
log('agent', 'Working on it...', true);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await this._processUserMessage(message);
|
|
177
|
+
|
|
178
|
+
if (!result.printedInline) {
|
|
179
|
+
console.log();
|
|
180
|
+
log('agent', result.response, true);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (result.toolCalls > 0) {
|
|
184
|
+
log('info', `Done — ${result.steps} step(s), ${result.toolCalls} tool call(s).`, true);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.log();
|
|
188
|
+
log('error', `Sorry, I encountered an error: ${error.message}`, true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.rl.prompt();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
this.rl.on('close', () => {
|
|
195
|
+
log('system', 'Goodbye!', true);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
} catch (err) {
|
|
200
|
+
log('error', `Startup failed: ${err.message}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async _systemCheck() {
|
|
206
|
+
log('system', 'Checking system status...');
|
|
207
|
+
|
|
208
|
+
await this.provider.healthCheck();
|
|
209
|
+
log('success', `LLM provider ready: ${this.provider}`);
|
|
210
|
+
|
|
211
|
+
const connected = await this.bridge.isConnected();
|
|
212
|
+
if (!connected) throw new Error('VenomBrowser daemon not connected. Is the daemon running?');
|
|
213
|
+
|
|
214
|
+
log('success', 'VenomBrowser daemon connected');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async _processUserMessage(userInput) {
|
|
218
|
+
this.conversationHistory.push({ role: 'user', content: userInput });
|
|
219
|
+
|
|
220
|
+
let stepCount = 0;
|
|
221
|
+
let toolCallCount = 0;
|
|
222
|
+
let lastContent = null;
|
|
223
|
+
let printedInline = false;
|
|
224
|
+
|
|
225
|
+
while (stepCount < MAX_STEPS) {
|
|
226
|
+
stepCount++;
|
|
227
|
+
|
|
228
|
+
const reply = await this.provider.chat(this.conversationHistory, TOOL_DEFINITIONS);
|
|
229
|
+
|
|
230
|
+
const assistantMsg = { role: 'assistant', content: reply.content || '' };
|
|
231
|
+
if (reply.tool_calls) assistantMsg.tool_calls = reply.tool_calls;
|
|
232
|
+
this.conversationHistory.push(assistantMsg);
|
|
233
|
+
|
|
234
|
+
lastContent = reply.content;
|
|
235
|
+
|
|
236
|
+
if (!reply.tool_calls || reply.tool_calls.length === 0) {
|
|
237
|
+
if (reply.content) {
|
|
238
|
+
console.log();
|
|
239
|
+
log('agent', reply.content, true);
|
|
240
|
+
printedInline = true;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
response: reply.content || 'Task completed.',
|
|
244
|
+
completed: true,
|
|
245
|
+
steps: stepCount,
|
|
246
|
+
toolCalls: toolCallCount,
|
|
247
|
+
printedInline
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (reply.content) {
|
|
252
|
+
log('agent', reply.content, true);
|
|
253
|
+
printedInline = true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const call of reply.tool_calls) {
|
|
257
|
+
toolCallCount++;
|
|
258
|
+
const name = call.function.name;
|
|
259
|
+
const args = call.function.arguments || {};
|
|
260
|
+
|
|
261
|
+
log('tool', `→ ${name}(${JSON.stringify(args)})`, true);
|
|
262
|
+
|
|
263
|
+
let resultPayload;
|
|
264
|
+
try {
|
|
265
|
+
const result = await executeTool(this.bridge, name, args);
|
|
266
|
+
resultPayload = { success: true, result };
|
|
267
|
+
|
|
268
|
+
if (name === 'navigate') log('success', `✓ Loaded: ${result.title || args.url}`, true);
|
|
269
|
+
else if (name === 'snapshot') log('success', `✓ Got ${result.tree?.children?.length || 0} elements`, true);
|
|
270
|
+
else if (name === 'click') log('success', '✓ Clicked successfully', true);
|
|
271
|
+
else if (name === 'fill') log('success', '✓ Filled successfully', true);
|
|
272
|
+
else if (name === 'screenshot') log('success', `✓ Screenshot saved: ${result.path}`, true);
|
|
273
|
+
else log('success', `✓ ${name} completed`, true);
|
|
274
|
+
|
|
275
|
+
} catch (error) {
|
|
276
|
+
resultPayload = { success: false, error: error.message };
|
|
277
|
+
log('error', `Failed: ${error.message}`, true);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
this.conversationHistory.push({
|
|
281
|
+
role: 'tool',
|
|
282
|
+
tool_call_id: call.id,
|
|
283
|
+
name,
|
|
284
|
+
content: JSON.stringify(resultPayload)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
response: `Hit the step limit (${MAX_STEPS}). The task might need to be broken into smaller parts.`,
|
|
291
|
+
completed: false,
|
|
292
|
+
steps: stepCount,
|
|
293
|
+
toolCalls: toolCallCount,
|
|
294
|
+
printedInline: false
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const chatAgent = new ChatAgent();
|
|
300
|
+
chatAgent.start().catch(console.error);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* VenomBrowser CLI — Unified command dispatcher
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* venombrowser setup — run the interactive setup wizard
|
|
7
|
+
* venombrowser start — start the bridge daemon
|
|
8
|
+
* venombrowser status — check daemon + extension status
|
|
9
|
+
* venombrowser connect — auto-register with known agents
|
|
10
|
+
* venombrowser agent "<task>" — run a one-shot browser task
|
|
11
|
+
* venombrowser uninstall — remove all MCP entries + stop daemon
|
|
12
|
+
* venombrowser chat — start interactive chat agent
|
|
13
|
+
* venombrowser help — show this help
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { fork, execSync } = require('child_process');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
|
|
20
|
+
const BIN_DIR = __dirname;
|
|
21
|
+
const ENV_PATH = path.join(BIN_DIR, '..', '.env');
|
|
22
|
+
|
|
23
|
+
// Load .env for BRIDGE_PORT
|
|
24
|
+
require('dotenv').config({ path: ENV_PATH });
|
|
25
|
+
|
|
26
|
+
const BRIDGE_PORT = parseInt(process.env.BRIDGE_PORT || '10086', 10);
|
|
27
|
+
|
|
28
|
+
// ─── Subcommand handlers ────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function showHelp() {
|
|
31
|
+
console.log(`
|
|
32
|
+
🕷️ VenomBrowser CLI — Browser Automation Bridge
|
|
33
|
+
|
|
34
|
+
Usage: venombrowser <command> [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
setup Run the interactive setup wizard
|
|
38
|
+
start Start the bridge daemon
|
|
39
|
+
status Check daemon & extension connection status
|
|
40
|
+
connect Auto-register MCP server with known agents
|
|
41
|
+
uninstall Remove MCP entries, stop daemon (does NOT npm uninstall)
|
|
42
|
+
agent "<task>" Run a one-shot browser automation task
|
|
43
|
+
chat Start the interactive chat agent
|
|
44
|
+
help Show this help message
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
venombrowser setup
|
|
48
|
+
venombrowser start
|
|
49
|
+
venombrowser status
|
|
50
|
+
venombrowser agent "search google for weather in tokyo"
|
|
51
|
+
venombrowser connect
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function runSetup() {
|
|
56
|
+
const child = fork(path.join(BIN_DIR, 'venombrowser-setup.js'), process.argv.slice(3), {
|
|
57
|
+
stdio: 'inherit'
|
|
58
|
+
});
|
|
59
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function runStart() {
|
|
63
|
+
// Load dotenv and require bridge-server directly (same as venombrowser-server.js)
|
|
64
|
+
require('dotenv').config({ path: ENV_PATH });
|
|
65
|
+
require('../src/bridge-server');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runStatus() {
|
|
69
|
+
const url = `http://127.0.0.1:${BRIDGE_PORT}/health`;
|
|
70
|
+
|
|
71
|
+
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
72
|
+
let body = '';
|
|
73
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
74
|
+
res.on('end', () => {
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(body);
|
|
77
|
+
const serverUp = data.server === 'ok';
|
|
78
|
+
const extConnected = data.extension === 'connected';
|
|
79
|
+
|
|
80
|
+
console.log('🕷️ VenomBrowser Status');
|
|
81
|
+
console.log('═'.repeat(40));
|
|
82
|
+
console.log(` Server: ${serverUp ? '✅ Running' : '❌ Not running'} (port ${data.port || BRIDGE_PORT})`);
|
|
83
|
+
console.log(` Extension: ${extConnected ? '✅ Connected' : '❌ Disconnected'}`);
|
|
84
|
+
console.log('═'.repeat(40));
|
|
85
|
+
|
|
86
|
+
if (serverUp && extConnected) {
|
|
87
|
+
console.log(' Status: Connected');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} else if (serverUp) {
|
|
90
|
+
console.log(' Status: Disconnected (extension not connected)');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(' Status: Disconnected');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
console.error('❌ Invalid response from daemon');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.on('error', () => {
|
|
104
|
+
console.log('🕷️ VenomBrowser Status');
|
|
105
|
+
console.log('═'.repeat(40));
|
|
106
|
+
console.log(` Server: ❌ Not running (port ${BRIDGE_PORT})`);
|
|
107
|
+
console.log(` Extension: ❌ Disconnected`);
|
|
108
|
+
console.log('═'.repeat(40));
|
|
109
|
+
console.log(' Status: Disconnected');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(' Start the daemon with: venombrowser start');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
req.on('timeout', () => {
|
|
116
|
+
req.destroy();
|
|
117
|
+
console.log('🕷️ VenomBrowser Status');
|
|
118
|
+
console.log('═'.repeat(40));
|
|
119
|
+
console.log(` Server: ❌ Not responding (port ${BRIDGE_PORT})`);
|
|
120
|
+
console.log(` Extension: ❌ Disconnected`);
|
|
121
|
+
console.log('═'.repeat(40));
|
|
122
|
+
console.log(' Status: Disconnected');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runConnect() {
|
|
128
|
+
const { connect } = require('../src/connect');
|
|
129
|
+
connect();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function runUninstall() {
|
|
133
|
+
const { uninstall } = require('../src/uninstall');
|
|
134
|
+
uninstall(process.argv.slice(3));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runAgent() {
|
|
138
|
+
const args = process.argv.slice(3);
|
|
139
|
+
if (args.length === 0) {
|
|
140
|
+
console.error('❌ Usage: venombrowser agent "<task description>"');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const child = fork(path.join(BIN_DIR, 'venombrowser-agent.js'), args, {
|
|
144
|
+
stdio: 'inherit'
|
|
145
|
+
});
|
|
146
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runChat() {
|
|
150
|
+
const child = fork(path.join(BIN_DIR, 'venombrowser-chat.js'), process.argv.slice(3), {
|
|
151
|
+
stdio: 'inherit'
|
|
152
|
+
});
|
|
153
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Dispatch ───────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const command = process.argv[2];
|
|
159
|
+
|
|
160
|
+
switch (command) {
|
|
161
|
+
case 'setup':
|
|
162
|
+
runSetup();
|
|
163
|
+
break;
|
|
164
|
+
case 'start':
|
|
165
|
+
runStart();
|
|
166
|
+
break;
|
|
167
|
+
case 'status':
|
|
168
|
+
runStatus();
|
|
169
|
+
break;
|
|
170
|
+
case 'connect':
|
|
171
|
+
runConnect();
|
|
172
|
+
break;
|
|
173
|
+
case 'uninstall':
|
|
174
|
+
runUninstall();
|
|
175
|
+
break;
|
|
176
|
+
case 'agent':
|
|
177
|
+
runAgent();
|
|
178
|
+
break;
|
|
179
|
+
case 'chat':
|
|
180
|
+
runChat();
|
|
181
|
+
break;
|
|
182
|
+
case 'help':
|
|
183
|
+
case '--help':
|
|
184
|
+
case '-h':
|
|
185
|
+
showHelp();
|
|
186
|
+
break;
|
|
187
|
+
case undefined:
|
|
188
|
+
showHelp();
|
|
189
|
+
break;
|
|
190
|
+
default:
|
|
191
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
192
|
+
console.error(' Run "venombrowser help" for available commands.');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|