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.
@@ -0,0 +1,364 @@
1
+ /**
2
+ * VenomBrowser Browser Tools — Kimi-style tool definitions & execution
3
+ *
4
+ * Single source of truth for:
5
+ * 1. Tool schemas (Kimi-style actions)
6
+ * 2. Tool execution logic (routes to bridge-client)
7
+ *
8
+ * v4.0.0 — Kimi-style architecture:
9
+ * - Actions match Kimi WebBridge: navigate, snapshot, click, fill, evaluate, etc.
10
+ * - Screenshot returns file path, NOT base64 (fixes cost leak)
11
+ * - Added: upload, save_as_pdf, network, cdp
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const SCREENSHOT_DIR = path.join(__dirname, '..', '..', 'screenshots');
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // Tool Definitions — Kimi-style actions
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ const TOOL_DEFINITIONS = [
24
+ {
25
+ type: 'function',
26
+ function: {
27
+ name: 'navigate',
28
+ description: 'Navigate the browser to a URL. First call opens a tab.',
29
+ parameters: {
30
+ type: 'object',
31
+ properties: {
32
+ url: { type: 'string', description: 'URL to navigate to' },
33
+ newTab: { type: 'boolean', description: 'Open in a new tab. Default false.' },
34
+ group_title: { type: 'string', description: 'Tab group label (first navigate only)' }
35
+ },
36
+ required: ['url']
37
+ }
38
+ }
39
+ },
40
+ {
41
+ type: 'function',
42
+ function: {
43
+ name: 'find_tab',
44
+ description: 'Re-select a tab opened this session, or borrow the user\'s active tab.',
45
+ parameters: {
46
+ type: 'object',
47
+ properties: {
48
+ url: { type: 'string', description: 'URL to find' },
49
+ active: { type: 'boolean', description: 'Borrow the user\'s currently viewed tab. Default false.' }
50
+ },
51
+ required: ['url']
52
+ }
53
+ }
54
+ },
55
+ {
56
+ type: 'function',
57
+ function: {
58
+ name: 'snapshot',
59
+ description: 'Get the page accessibility tree with @e refs for interactive elements. Use this to read page content and locate elements for click/fill.',
60
+ parameters: { type: 'object', properties: {} }
61
+ }
62
+ },
63
+ {
64
+ type: 'function',
65
+ function: {
66
+ name: 'click',
67
+ description: 'Click an element by @e ref (from snapshot) or CSS selector.',
68
+ parameters: {
69
+ type: 'object',
70
+ properties: {
71
+ selector: { type: 'string', description: '@e ref (e.g. "@e3") or CSS selector' }
72
+ },
73
+ required: ['selector']
74
+ }
75
+ }
76
+ },
77
+ {
78
+ type: 'function',
79
+ function: {
80
+ name: 'fill',
81
+ description: 'Fill an input/textarea/contenteditable with text. Works on React/Vue controlled inputs.',
82
+ parameters: {
83
+ type: 'object',
84
+ properties: {
85
+ selector: { type: 'string', description: '@e ref or CSS selector' },
86
+ value: { type: 'string', description: 'Text to fill' }
87
+ },
88
+ required: ['selector', 'value']
89
+ }
90
+ }
91
+ },
92
+ {
93
+ type: 'function',
94
+ function: {
95
+ name: 'type',
96
+ description: 'Type text into an input and optionally press Enter. Use for search boxes.',
97
+ parameters: {
98
+ type: 'object',
99
+ properties: {
100
+ selector: { type: 'string', description: '@e ref or CSS selector' },
101
+ text: { type: 'string', description: 'Text to type' },
102
+ pressEnter: { type: 'boolean', description: 'Press Enter after typing' }
103
+ },
104
+ required: ['selector', 'text']
105
+ }
106
+ }
107
+ },
108
+ {
109
+ type: 'function',
110
+ function: {
111
+ name: 'submit_form',
112
+ description: 'Submit the form containing the given element. More reliable than pressing Enter.',
113
+ parameters: {
114
+ type: 'object',
115
+ properties: {
116
+ selector: { type: 'string', description: '@e ref or CSS selector of element inside the form' }
117
+ },
118
+ required: ['selector']
119
+ }
120
+ }
121
+ },
122
+ {
123
+ type: 'function',
124
+ function: {
125
+ name: 'evaluate',
126
+ description: 'Execute JavaScript in the page context. Returns the result.',
127
+ parameters: {
128
+ type: 'object',
129
+ properties: {
130
+ code: { type: 'string', description: 'JavaScript code to execute' }
131
+ },
132
+ required: ['code']
133
+ }
134
+ }
135
+ },
136
+ {
137
+ type: 'function',
138
+ function: {
139
+ name: 'cdp',
140
+ description: 'Raw Chrome DevTools Protocol command. Low-level escape hatch for cases other tools don\'t cover.',
141
+ parameters: {
142
+ type: 'object',
143
+ properties: {
144
+ method: { type: 'string', description: 'CDP method (e.g. "Runtime.evaluate")' },
145
+ params: { type: 'object', description: 'CDP method parameters' }
146
+ },
147
+ required: ['method']
148
+ }
149
+ }
150
+ },
151
+ {
152
+ type: 'function',
153
+ function: {
154
+ name: 'screenshot',
155
+ description: 'Capture a screenshot. Returns file path, not base64.',
156
+ parameters: {
157
+ type: 'object',
158
+ properties: {
159
+ format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format. Default "png".' },
160
+ quality: { type: 'number', description: 'JPEG quality (0-100). Only for jpeg format.' },
161
+ selector: { type: 'string', description: '@e ref or CSS selector to screenshot a specific element.' },
162
+ path: { type: 'string', description: 'Custom output path.' }
163
+ }
164
+ }
165
+ }
166
+ },
167
+ {
168
+ type: 'function',
169
+ function: {
170
+ name: 'network',
171
+ description: 'Monitor network requests: start/stop capture, list requests, get details.',
172
+ parameters: {
173
+ type: 'object',
174
+ properties: {
175
+ cmd: { type: 'string', enum: ['start', 'stop', 'list', 'detail'], description: 'Network command' },
176
+ filter: { type: 'string', description: 'URL filter for list command' },
177
+ requestId: { type: 'string', description: 'Request ID for detail command' }
178
+ },
179
+ required: ['cmd']
180
+ }
181
+ }
182
+ },
183
+ {
184
+ type: 'function',
185
+ function: {
186
+ name: 'upload',
187
+ description: 'Upload files to a file input element.',
188
+ parameters: {
189
+ type: 'object',
190
+ properties: {
191
+ selector: { type: 'string', description: 'CSS selector of the file input' },
192
+ files: { type: 'array', items: { type: 'string' }, description: 'File paths to upload' }
193
+ },
194
+ required: ['selector', 'files']
195
+ }
196
+ }
197
+ },
198
+ {
199
+ type: 'function',
200
+ function: {
201
+ name: 'save_as_pdf',
202
+ description: 'Save the current page as a PDF file.',
203
+ parameters: {
204
+ type: 'object',
205
+ properties: {
206
+ paper_format: { type: 'string', enum: ['letter', 'a4', 'legal', 'a3', 'tabloid'], description: 'Paper size. Default "letter".' },
207
+ landscape: { type: 'boolean', description: 'Landscape orientation. Default false.' },
208
+ scale: { type: 'number', description: 'Scale (0.1-2.0). Default 1.0.' },
209
+ print_background: { type: 'boolean', description: 'Print background colors. Default true.' },
210
+ path: { type: 'string', description: 'Custom output path.' }
211
+ }
212
+ }
213
+ }
214
+ },
215
+ {
216
+ type: 'function',
217
+ function: {
218
+ name: 'list_tabs',
219
+ description: 'List all open browser tabs.',
220
+ parameters: { type: 'object', properties: {} }
221
+ }
222
+ },
223
+ {
224
+ type: 'function',
225
+ function: {
226
+ name: 'close_tab',
227
+ description: 'Close the current tab in the session.',
228
+ parameters: { type: 'object', properties: {} }
229
+ }
230
+ },
231
+ {
232
+ type: 'function',
233
+ function: {
234
+ name: 'close_session',
235
+ description: 'Close all tabs in the session.',
236
+ parameters: { type: 'object', properties: {} }
237
+ }
238
+ }
239
+ ];
240
+
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+ // Tool Execution — routes to bridge-client (Kimi-style)
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+
245
+ function ensureScreenshotDir() {
246
+ if (!fs.existsSync(SCREENSHOT_DIR)) {
247
+ fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
248
+ }
249
+ }
250
+
251
+ async function executeTool(bridge, name, args = {}) {
252
+ switch (name) {
253
+ case 'navigate': {
254
+ const r = await bridge.navigate(args.url, { newTab: args.newTab, group_title: args.group_title });
255
+ return { url: r.url || args.url, title: r.title || '', tabId: r.tabId };
256
+ }
257
+
258
+ case 'find_tab': {
259
+ const r = await bridge.findTab(args.url, { active: args.active });
260
+ return { url: r.url, tabId: r.tabId, borrowed: r.borrowed };
261
+ }
262
+
263
+ case 'snapshot': {
264
+ const r = await bridge.snapshot();
265
+ return { url: r.url, title: r.title, refCount: r.refCount, elements: r.elements };
266
+ }
267
+
268
+ case 'click': {
269
+ const r = await bridge.click(args.selector);
270
+ return { success: true, tag: r.tag, text: r.text };
271
+ }
272
+
273
+ case 'fill': {
274
+ const r = await bridge.fill(args.selector, args.value);
275
+ return { success: true, typed: r.typed, length: r.length };
276
+ }
277
+
278
+ case 'type': {
279
+ const r = await bridge.type(args.selector, args.text, { pressEnter: args.pressEnter });
280
+ return { success: true, typed: r.typed, length: r.length };
281
+ }
282
+
283
+ case 'submit_form': {
284
+ const r = await bridge.submitForm(args.selector);
285
+ return { success: true, submitted: r.submitted, action: r.action, method: r.method };
286
+ }
287
+
288
+ case 'evaluate': {
289
+ const r = await bridge.evaluate(args.code);
290
+ return { type: r.type, value: r.value };
291
+ }
292
+
293
+ case 'cdp': {
294
+ const r = await bridge.cdp(args.method, args.params);
295
+ return r;
296
+ }
297
+
298
+ case 'screenshot': {
299
+ const r = await bridge.screenshot({ format: args.format, quality: args.quality, selector: args.selector });
300
+ return {
301
+ format: r.format || args.format || 'png',
302
+ path: r.path,
303
+ sizeBytes: r.sizeBytes,
304
+ mimeType: r.mimeType || `image/${args.format || 'png'}`
305
+ };
306
+ }
307
+
308
+ case 'network': {
309
+ const r = await bridge.network(args.cmd, { filter: args.filter, requestId: args.requestId });
310
+ return r;
311
+ }
312
+
313
+ case 'upload': {
314
+ const r = await bridge.upload(args.selector, args.files);
315
+ return { success: true, fileCount: r.fileCount };
316
+ }
317
+
318
+ case 'save_as_pdf': {
319
+ const r = await bridge.saveAsPdf({
320
+ paper_format: args.paper_format,
321
+ landscape: args.landscape,
322
+ scale: args.scale,
323
+ print_background: args.print_background
324
+ });
325
+ return { path: r.path, sizeBytes: r.sizeBytes, mimeType: r.mimeType, pageTitle: r.pageTitle };
326
+ }
327
+
328
+ case 'list_tabs': {
329
+ const tabs = await bridge.listTabs();
330
+ return { tabs };
331
+ }
332
+
333
+ case 'close_tab': {
334
+ const r = await bridge.closeTab();
335
+ return { closed: true };
336
+ }
337
+
338
+ case 'close_session': {
339
+ const r = await bridge.closeSession();
340
+ return { closed: r.closed };
341
+ }
342
+
343
+ default:
344
+ throw new Error(`Unknown tool: ${name}`);
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Get tool definitions in MCP format (inputSchema instead of parameters).
350
+ */
351
+ function getMcpToolDefinitions() {
352
+ return TOOL_DEFINITIONS.map(t => ({
353
+ name: t.function.name,
354
+ description: t.function.description,
355
+ inputSchema: t.function.parameters
356
+ }));
357
+ }
358
+
359
+ module.exports = {
360
+ TOOL_DEFINITIONS,
361
+ executeTool,
362
+ getMcpToolDefinitions,
363
+ SCREENSHOT_DIR
364
+ };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * VenomBrowser Uninstall — Fully reverse everything install/setup/connect did
3
+ *
4
+ * 1. Stop daemon (by port, not by matching all node.exe)
5
+ * 2. Remove "venombrowser" entries from agent configs (jsonc-parser, safe)
6
+ * 3. Leave everything else untouched
7
+ * 4. Optionally purge user data (screenshots/)
8
+ * 5. Print npm uninstall instructions
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { execSync } = require('child_process');
15
+ const jsonc = require('jsonc-parser');
16
+
17
+ const PURGE_DATA_FLAG = '--purge-data';
18
+
19
+ /**
20
+ * Stop the daemon process listening on the given port.
21
+ * Uses netstat to find the PID, then kills only that process.
22
+ */
23
+ function stopDaemon(port) {
24
+ try {
25
+ if (process.platform === 'win32') {
26
+ const output = execSync(
27
+ `netstat -ano | findstr :${port} | findstr LISTENING`,
28
+ { encoding: 'utf8', timeout: 5000 }
29
+ );
30
+ const lines = output.trim().split('\n').filter(Boolean);
31
+ const pids = new Set();
32
+ for (const line of lines) {
33
+ const parts = line.trim().split(/\s+/);
34
+ const pid = parts[parts.length - 1];
35
+ if (pid && pid !== '0') pids.add(pid);
36
+ }
37
+ for (const pid of pids) {
38
+ try {
39
+ execSync(`taskkill /F /PID ${pid}`, { encoding: 'utf8', timeout: 5000 });
40
+ console.log(` ✅ Stopped daemon (PID ${pid}) on port ${port}`);
41
+ } catch {
42
+ console.log(` ⚠️ Could not stop PID ${pid} (may have already exited)`);
43
+ }
44
+ }
45
+ if (pids.size === 0) {
46
+ console.log(` ℹ️ No daemon found on port ${port}`);
47
+ }
48
+ } else {
49
+ // Unix: lsof or fuser
50
+ try {
51
+ const output = execSync(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 });
52
+ const pids = output.trim().split('\n').filter(Boolean);
53
+ for (const pid of pids) {
54
+ try {
55
+ execSync(`kill -9 ${pid}`, { encoding: 'utf8', timeout: 5000 });
56
+ console.log(` ✅ Stopped daemon (PID ${pid}) on port ${port}`);
57
+ } catch {
58
+ console.log(` ⚠️ Could not stop PID ${pid}`);
59
+ }
60
+ }
61
+ if (pids.length === 0) {
62
+ console.log(` ℹ️ No daemon found on port ${port}`);
63
+ }
64
+ } catch {
65
+ console.log(` ℹ️ No daemon found on port ${port}`);
66
+ }
67
+ }
68
+ } catch {
69
+ console.log(` ℹ️ No daemon found on port ${port}`);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Remove "venombrowser" key from the "mcpServers" object in a Claude Desktop / Cursor config.
75
+ * Uses jsonc.modify to preserve comments and formatting.
76
+ * Returns { removed: boolean, path: string }
77
+ */
78
+ function removeFromMcpServersConfig(filePath) {
79
+ let content;
80
+ try {
81
+ content = fs.readFileSync(filePath, 'utf8');
82
+ } catch {
83
+ return { removed: false, path: filePath, reason: 'not found' };
84
+ }
85
+
86
+ let config;
87
+ try {
88
+ config = jsonc.parse(content);
89
+ } catch {
90
+ return { removed: false, path: filePath, reason: 'invalid JSON' };
91
+ }
92
+
93
+ if (!config.mcpServers || !config.mcpServers['venombrowser']) {
94
+ return { removed: false, path: filePath, reason: 'entry not present' };
95
+ }
96
+
97
+ const edits = jsonc.modify(content, ['mcpServers', 'venombrowser'], undefined, {
98
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
99
+ });
100
+ const updated = jsonc.applyEdits(content, edits);
101
+ fs.writeFileSync(filePath, updated, 'utf8');
102
+ return { removed: true, path: filePath };
103
+ }
104
+
105
+ /**
106
+ * Remove "venombrowser" key from the "mcp" object in an OpenCode config.
107
+ * Uses jsonc.modify to preserve comments and formatting.
108
+ * Returns { removed: boolean, path: string }
109
+ */
110
+ function removeFromOpenCodeConfig(filePath) {
111
+ let content;
112
+ try {
113
+ content = fs.readFileSync(filePath, 'utf8');
114
+ } catch {
115
+ return { removed: false, path: filePath, reason: 'not found' };
116
+ }
117
+
118
+ let config;
119
+ try {
120
+ config = jsonc.parse(content);
121
+ } catch {
122
+ return { removed: false, path: filePath, reason: 'invalid JSON' };
123
+ }
124
+
125
+ if (!config.mcp || !config.mcp['venombrowser']) {
126
+ return { removed: false, path: filePath, reason: 'entry not present' };
127
+ }
128
+
129
+ const edits = jsonc.modify(content, ['mcp', 'venombrowser'], undefined, {
130
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
131
+ });
132
+ const updated = jsonc.applyEdits(content, edits);
133
+ fs.writeFileSync(filePath, updated, 'utf8');
134
+ return { removed: true, path: filePath };
135
+ }
136
+
137
+ /**
138
+ * Main uninstall routine.
139
+ * @param {string[]} args - CLI arguments (may include --purge-data)
140
+ */
141
+ function uninstall(args = []) {
142
+ const purgeData = args.includes(PURGE_DATA_FLAG);
143
+ const home = os.homedir();
144
+ const port = parseInt(process.env.BRIDGE_PORT || '10086', 10);
145
+
146
+ console.log('🧹 VenomBrowser Uninstall');
147
+ console.log('═'.repeat(55));
148
+ console.log();
149
+
150
+ // ── Step 1: Stop daemon ──────────────────────────────────────────────
151
+ console.log('Step 1: Stopping daemon...');
152
+ stopDaemon(port);
153
+ console.log();
154
+
155
+ // ── Step 2: Remove agent config entries ──────────────────────────────
156
+ console.log('Step 2: Removing MCP entries from agent configs...');
157
+ const homeDir = os.homedir();
158
+ const agentConfigs = [];
159
+
160
+ // Claude Desktop
161
+ if (process.platform === 'win32') {
162
+ const appdata = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
163
+ agentConfigs.push({
164
+ name: 'Claude Desktop',
165
+ path: path.join(appdata, 'Claude', 'claude_desktop_config.json'),
166
+ type: 'mcpServers'
167
+ });
168
+ } else {
169
+ agentConfigs.push({
170
+ name: 'Claude Desktop',
171
+ path: path.join(homeDir, '.config', 'claude', 'claude_desktop_config.json'),
172
+ type: 'mcpServers'
173
+ });
174
+ }
175
+
176
+ // Cursor
177
+ agentConfigs.push({
178
+ name: 'Cursor',
179
+ path: path.join(homeDir, '.cursor', 'mcp.json'),
180
+ type: 'mcpServers'
181
+ });
182
+
183
+ // OpenCode
184
+ agentConfigs.push({
185
+ name: 'OpenCode',
186
+ path: path.join(homeDir, '.config', 'opencode', 'opencode.jsonc'),
187
+ type: 'mcp'
188
+ });
189
+
190
+ let anyRemoved = false;
191
+ for (const { name, path: configPath, type } of agentConfigs) {
192
+ if (!fs.existsSync(configPath)) {
193
+ continue;
194
+ }
195
+
196
+ const result = type === 'mcp'
197
+ ? removeFromOpenCodeConfig(configPath)
198
+ : removeFromMcpServersConfig(configPath);
199
+
200
+ if (result.removed) {
201
+ console.log(` ✅ Removed venombrowser from ${name} config (${configPath})`);
202
+ anyRemoved = true;
203
+ } else if (result.reason === 'entry not present') {
204
+ // silently skip — we didn't touch this file
205
+ } else {
206
+ console.log(` ⚠️ ${name}: ${result.reason}`);
207
+ }
208
+ }
209
+
210
+ if (!anyRemoved) {
211
+ console.log(' ℹ️ No venombrowser entries found in any agent config');
212
+ }
213
+ console.log();
214
+
215
+ // ── Step 3: Purge user data (optional) ──────────────────────────────
216
+ const screenshotsDir = path.join(__dirname, '..', '..', 'screenshots');
217
+ if (purgeData) {
218
+ console.log('Step 3: Purging user data...');
219
+ if (fs.existsSync(screenshotsDir)) {
220
+ const count = fs.readdirSync(screenshotsDir).length;
221
+ fs.rmSync(screenshotsDir, { recursive: true, force: true });
222
+ console.log(` ✅ Deleted screenshots/ (${count} files)`);
223
+ } else {
224
+ console.log(' ℹ️ No screenshots/ folder found');
225
+ }
226
+ } else {
227
+ console.log('Step 3: User data preserved (screenshots/ kept)');
228
+ if (fs.existsSync(screenshotsDir)) {
229
+ console.log(` 📁 ${screenshotsDir}`);
230
+ }
231
+ console.log(' To delete screenshots too, re-run: venombrowser uninstall --purge-data');
232
+ }
233
+ console.log();
234
+
235
+ // ── Step 4: npm uninstall instructions ──────────────────────────────
236
+ console.log('Step 4: Remove the global npm package:');
237
+ console.log('═'.repeat(55));
238
+ console.log(' Run this command to complete the uninstall:');
239
+ console.log();
240
+ console.log(' npm uninstall -g venombrowser');
241
+ console.log();
242
+ console.log(' After that, "venombrowser" will no longer be on your PATH.');
243
+ console.log('═'.repeat(55));
244
+ }
245
+
246
+ module.exports = { uninstall };