bwb-browser 2.0.4 → 3.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/lib/setup.mjs ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bwb-browser --setup
4
+ * One-command self-configuration for any AI CLI agent.
5
+ *
6
+ * Detects installed agents, writes MCP config entries, verifies Chrome.
7
+ * Run: bwb --setup
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { execSync } from 'child_process';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const HOME = process.env.HOME || '/root';
17
+ const SERVER_PATH = path.resolve(__dirname, '..', 'server.mjs');
18
+
19
+ // ─── Detect Chrome/Chromium ───────────────────────────────────────
20
+ function detectChrome() {
21
+ if (process.env.BWB_CHROME_PATH) {
22
+ const p = process.env.BWB_CHROME_PATH;
23
+ if (fs.existsSync(p)) return { path: p, source: 'BWB_CHROME_PATH env' };
24
+ }
25
+
26
+ const candidates = [
27
+ 'chromium-browser', 'chromium', 'google-chrome',
28
+ 'google-chrome-stable', 'chrome', 'brave-browser',
29
+ '/usr/bin/chromium-browser', '/usr/bin/chromium',
30
+ '/usr/bin/google-chrome', '/snap/bin/chromium',
31
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
32
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
33
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
34
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
35
+ ];
36
+
37
+ for (const bin of candidates) {
38
+ try {
39
+ const out = execSync(`which "${bin}" 2>/dev/null || command -v "${bin}" 2>/dev/null || echo ""`, {
40
+ encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
41
+ }).trim();
42
+ if (out) return { path: out, source: 'PATH' };
43
+ } catch { /* not found */ }
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ // ─── Detect AI CLI tools ──────────────────────────────────────────
50
+ const AGENT_CONFIGS = [
51
+ {
52
+ name: 'OpenCode',
53
+ file: path.join(HOME, '.config/opencode/opencode.json'),
54
+ detect: (cfg) => !!(cfg.mcp?.bwb || cfg.mcpServers?.bwb),
55
+ add(cfg) {
56
+ const entry = {
57
+ type: 'local',
58
+ command: ['node', SERVER_PATH]
59
+ };
60
+ if (cfg.mcp && typeof cfg.mcp === 'object') cfg.mcp.bwb = entry;
61
+ else if (cfg.mcpServers && typeof cfg.mcpServers === 'object') cfg.mcpServers.bwb = entry;
62
+ else cfg.mcp = { bwb: entry };
63
+ return cfg;
64
+ }
65
+ },
66
+ {
67
+ name: 'Antigravity (CLI)',
68
+ file: path.join(HOME, '.gemini/config/mcp_config.json'),
69
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
70
+ add(cfg) {
71
+ if (!cfg.mcpServers) cfg.mcpServers = {};
72
+ cfg.mcpServers.bwb = {
73
+ command: 'node',
74
+ args: [SERVER_PATH]
75
+ };
76
+ return cfg;
77
+ }
78
+ },
79
+ {
80
+ name: 'Antigravity (CLI - secondary)',
81
+ file: path.join(HOME, '.gemini/antigravity-cli/mcp_config.json'),
82
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
83
+ add(cfg) {
84
+ if (!cfg.mcpServers) cfg.mcpServers = {};
85
+ cfg.mcpServers.bwb = {
86
+ command: 'node',
87
+ args: [SERVER_PATH]
88
+ };
89
+ return cfg;
90
+ }
91
+ },
92
+ {
93
+ name: 'Claude Code',
94
+ file: path.join(HOME, '.claude/settings.json'),
95
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
96
+ add(cfg) {
97
+ if (!cfg.mcpServers) cfg.mcpServers = {};
98
+ cfg.mcpServers.bwb = {
99
+ command: 'node',
100
+ args: [SERVER_PATH]
101
+ };
102
+ return cfg;
103
+ }
104
+ },
105
+ {
106
+ name: 'Hermes',
107
+ file: path.join(HOME, '.hermes/mcp.json'),
108
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
109
+ add(cfg) {
110
+ if (!cfg.mcpServers) cfg.mcpServers = {};
111
+ cfg.mcpServers.bwb = {
112
+ command: 'node',
113
+ args: [SERVER_PATH]
114
+ };
115
+ return cfg;
116
+ }
117
+ },
118
+ {
119
+ name: 'Cline (VS Code)',
120
+ file: path.join(HOME, '.cline/mcp.json'),
121
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
122
+ add(cfg) {
123
+ if (!cfg.mcpServers) cfg.mcpServers = {};
124
+ cfg.mcpServers.bwb = {
125
+ command: 'node',
126
+ args: [SERVER_PATH]
127
+ };
128
+ return cfg;
129
+ }
130
+ },
131
+ {
132
+ name: 'Continue.dev',
133
+ file: path.join(HOME, '.continue/config.json'),
134
+ detect: (cfg) => {
135
+ const servers = cfg.experimental?.mcpServers || cfg.mcpServers;
136
+ return !!(servers?.bwb);
137
+ },
138
+ add(cfg) {
139
+ if (!cfg.experimental) cfg.experimental = {};
140
+ if (!cfg.experimental.mcpServers) cfg.experimental.mcpServers = {};
141
+ cfg.experimental.mcpServers.bwb = {
142
+ command: 'node',
143
+ args: [SERVER_PATH]
144
+ };
145
+ return cfg;
146
+ }
147
+ },
148
+ {
149
+ name: 'Codex CLI',
150
+ file: path.join(HOME, '.codex/mcp.json'),
151
+ detect: (cfg) => !!(cfg.mcpServers?.bwb),
152
+ add(cfg) {
153
+ if (!cfg.mcpServers) cfg.mcpServers = {};
154
+ cfg.mcpServers.bwb = {
155
+ command: 'node',
156
+ args: [SERVER_PATH]
157
+ };
158
+ return cfg;
159
+ }
160
+ },
161
+ ];
162
+
163
+ // ─── Core logic ────────────────────────────────────────────────────
164
+ function backup(file) {
165
+ const bak = file + '.bak';
166
+ try {
167
+ fs.copyFileSync(file, bak);
168
+ return bak;
169
+ } catch { return null; }
170
+ }
171
+
172
+ function writeConfigSafely(file, data) {
173
+ const tmp = file + '.tmp';
174
+ try {
175
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
176
+ fs.renameSync(tmp, file);
177
+ return true;
178
+ } catch { return false; }
179
+ }
180
+
181
+ function configureAgent(agent) {
182
+ const { name, file, detect, add } = agent;
183
+ const fileExists = fs.existsSync(file);
184
+
185
+ if (!fileExists) return { name, status: 'skipped', reason: 'not installed' };
186
+
187
+ try {
188
+ const raw = fs.readFileSync(file, 'utf8');
189
+ let config;
190
+ try { config = JSON.parse(raw); }
191
+ catch { return { name, status: 'error', reason: 'invalid JSON' }; }
192
+
193
+ if (detect(config)) {
194
+ return { name, status: 'already configured', file };
195
+ }
196
+
197
+ const bak = backup(file);
198
+ config = add(config);
199
+ if (!writeConfigSafely(file, config)) {
200
+ return { name, status: 'error', reason: 'write failed' };
201
+ }
202
+
203
+ return { name, status: 'configured', file, backup: bak };
204
+ } catch (err) {
205
+ return { name, status: 'error', reason: err.message };
206
+ }
207
+ }
208
+
209
+ // ─── Permissions check ─────────────────────────────────────────────
210
+ function checkPermissions() {
211
+ // On Termux, the HOME may be accessible. Check if we can read/write config dirs.
212
+ const checkPaths = [
213
+ path.join(HOME, '.config/opencode'),
214
+ path.join(HOME, '.gemini/config'),
215
+ path.join(HOME, '.claude'),
216
+ ];
217
+ const issues = [];
218
+ for (const p of checkPaths) {
219
+ if (fs.existsSync(p)) {
220
+ try { fs.accessSync(p, fs.constants.R_OK | fs.constants.W_OK); }
221
+ catch { issues.push(`${p}: no write permission`); }
222
+ }
223
+ }
224
+ return issues;
225
+ }
226
+
227
+ // ─── ────────────────────────────────────────────────────────────────
228
+ export function runSetup() {
229
+ console.log('\n ╔══════════════════════════════════════════════╗');
230
+ console.log(' ║ bwb-browser — Auto Setup ║');
231
+ console.log(' ╚══════════════════════════════════════════════╝\n');
232
+
233
+ // 1. Self-check
234
+ if (!fs.existsSync(SERVER_PATH)) {
235
+ console.error(` ❌ server.mjs not found at:\n ${SERVER_PATH}`);
236
+ console.error(' Is bwb-browser installed correctly?\n');
237
+ process.exit(1);
238
+ }
239
+ console.log(` 📦 bwb-browser: ${SERVER_PATH}`);
240
+ console.log(` v${getVersion()}\n`);
241
+
242
+ // 2. Check permissions
243
+ const permIssues = checkPermissions();
244
+ if (permIssues.length > 0) {
245
+ console.log(' ⚠️ Permission notes:');
246
+ for (const i of permIssues) console.log(` • ${i}`);
247
+ console.log();
248
+ }
249
+
250
+ // 3. Detect Chrome
251
+ const chrome = detectChrome();
252
+ if (chrome) {
253
+ console.log(` ✅ Chrome/Chromium detected:`);
254
+ console.log(` ${chrome.path} (${chrome.source})`);
255
+ } else {
256
+ console.log(` ⚠️ Chrome/Chromium not detected.`);
257
+ console.log(` Install it: pkg install chromium (Termux)`);
258
+ console.log(` or set BWB_CHROME_PATH env var.\n`);
259
+ }
260
+
261
+ // 4. Configure all agents
262
+ console.log(' 🔧 Configuring agents...\n');
263
+ const results = AGENT_CONFIGS.map(configureAgent);
264
+
265
+ const configured = results.filter(r => r.status === 'configured');
266
+ const alreadyDone = results.filter(r => r.status === 'already configured');
267
+ const skipped = results.filter(r => r.status === 'skipped');
268
+ const errors = results.filter(r => r.status === 'error');
269
+
270
+ for (const r of results) {
271
+ switch (r.status) {
272
+ case 'configured':
273
+ console.log(` ✅ ${r.name}: configured`);
274
+ if (r.backup) console.log(` backup: ${r.backup}`);
275
+ break;
276
+ case 'already configured':
277
+ console.log(` ✅ ${r.name}: already set up`);
278
+ break;
279
+ case 'skipped':
280
+ break; // silent
281
+ case 'error':
282
+ console.log(` ❌ ${r.name}: ${r.reason}`);
283
+ break;
284
+ }
285
+ }
286
+
287
+ // 5. Summary
288
+ console.log('\n ─── Summary ───────────────────────────────────────');
289
+ console.log(` ✅ Agents configured: ${configured.length}`);
290
+ console.log(` ✅ Already set up: ${alreadyDone.length}`);
291
+ console.log(` ⏭️ Not installed: ${skipped.length}`);
292
+ if (errors.length) console.log(` ❌ Errors: ${errors.length}`);
293
+ console.log();
294
+
295
+ if (configured.length > 0) {
296
+ console.log(' 🔄 RESTART REQUIRED: Close and reopen your AI agent');
297
+ console.log(' for the new MCP tools to take effect.\n');
298
+ }
299
+
300
+ if (!chrome) {
301
+ console.log(' ⚠️ Chrome not found — install it first:');
302
+ console.log(' Termux: pkg install chromium');
303
+ console.log(' macOS: brew install --cask google-chrome');
304
+ console.log(' Linux: apt install chromium-browser\n');
305
+ }
306
+
307
+ console.log(' 🚀 Ready to go! Try: browser_status\n');
308
+ return { configured: configured.length, alreadyDone: alreadyDone.length, errors: errors.length };
309
+ }
310
+
311
+ function getVersion() {
312
+ try {
313
+ const pkg = path.resolve(__dirname, '..', 'package.json');
314
+ const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
315
+ return json.version || 'unknown';
316
+ } catch {
317
+ return 'unknown';
318
+ }
319
+ }
320
+
321
+ // ─── Direct execution ──────────────────────────────────────────────
322
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
323
+ runSetup();
324
+ }
package/lib/tabs.mjs ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * bwb-browser — Multi-tab Management
3
+ *
4
+ * Manages multiple page targets (tabs) within a single Chrome browser instance.
5
+ * Uses CDP's target system to create, close, switch, and list tabs.
6
+ * Each tab gets its own CDP protocol connection.
7
+ */
8
+
9
+ import CDP from "chrome-remote-interface";
10
+ import { ensureBrowser, protocol, actualCdpPort, cfg } from "./browser.mjs";
11
+
12
+ // ─── Tab State ───────────────────────────────────────────────────────────────
13
+
14
+ /** @type {Map<string, {protocol: import('chrome-remote-interface').Protocol|null, title: string, url: string}>} */
15
+ const tabs = new Map();
16
+ let activeTabId = null;
17
+
18
+ // ─── Ensure Default Tab Registered ──────────────────────────────────────────
19
+
20
+ async function ensureDefaultTab() {
21
+ if (tabs.size > 0) return;
22
+
23
+ // Browser must be started first
24
+ await ensureBrowser();
25
+ if (!protocol) throw new Error("Browser not started");
26
+
27
+ const port = actualCdpPort || cfg.port;
28
+ const targets = await CDP.List({ port });
29
+ const page = targets.find(t => t.type === "page");
30
+ if (page) {
31
+ tabs.set(page.id, { protocol, title: page.title, url: page.url });
32
+ activeTabId = page.id;
33
+ }
34
+ }
35
+
36
+ // ─── getActiveProtocol ───────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Returns the CDP protocol for the active tab.
40
+ * If no tabs are managed, ensures browser is started and returns default protocol.
41
+ * All tool handlers should use this instead of ensureBrowser() directly.
42
+ */
43
+ export async function getActiveProtocol() {
44
+ if (tabs.size === 0) {
45
+ await ensureDefaultTab();
46
+ }
47
+
48
+ if (activeTabId && tabs.has(activeTabId)) {
49
+ const tab = tabs.get(activeTabId);
50
+ if (tab.protocol) return tab.protocol;
51
+ }
52
+
53
+ // Fallback to default protocol
54
+ return protocol;
55
+ }
56
+
57
+ // ─── Create Tab ──────────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Creates a new browser tab and navigates to url (or about:blank).
61
+ * Switches to the new tab automatically.
62
+ */
63
+ export async function createTab(url) {
64
+ await ensureDefaultTab();
65
+
66
+ const port = actualCdpPort || cfg.port;
67
+ const info = await CDP.New({ port, url: url || "about:blank" });
68
+
69
+ // Create a CDP connection for this specific target
70
+ const newProtocol = await CDP({ target: info.id, port });
71
+ tabs.set(info.id, { protocol: newProtocol, title: info.title || "", url: info.url || url || "" });
72
+ activeTabId = info.id;
73
+
74
+ return { id: info.id, title: info.title || "", url: info.url || "" };
75
+ }
76
+
77
+ // ─── Close Tab ───────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Closes a tab by targetId. If no targetId provided, closes the active tab.
81
+ * Cannot close the last remaining tab.
82
+ */
83
+ export async function closeTab(targetId) {
84
+ await ensureDefaultTab();
85
+
86
+ const id = targetId || activeTabId;
87
+ if (!id) throw new Error("No tab to close");
88
+
89
+ if (tabs.size <= 1) {
90
+ throw new Error("Cannot close the only remaining tab. Use browser_restart instead.");
91
+ }
92
+
93
+ const port = actualCdpPort || cfg.port;
94
+ await CDP.Close({ id, port });
95
+
96
+ const tab = tabs.get(id);
97
+ if (tab && tab.protocol && tab.protocol !== protocol) {
98
+ try { await tab.protocol.close(); } catch {}
99
+ }
100
+ tabs.delete(id);
101
+
102
+ // Switch to another tab
103
+ if (activeTabId === id) {
104
+ activeTabId = tabs.keys().next().value;
105
+ }
106
+
107
+ return { closed: id, activeTab: activeTabId };
108
+ }
109
+
110
+ // ─── Switch Tab ──────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Switches to a different tab by targetId.
114
+ */
115
+ export function switchTab(targetId) {
116
+ if (!targetId) throw new Error("No targetId provided");
117
+ if (!tabs.has(targetId)) {
118
+ throw new Error(`Tab not found: ${targetId}`);
119
+ }
120
+
121
+ activeTabId = targetId;
122
+ const tab = tabs.get(targetId);
123
+ return { id: targetId, title: tab.title, url: tab.url };
124
+ }
125
+
126
+ // ─── List Tabs ───────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Lists all open tabs with their IDs, titles, URLs, and active status.
130
+ */
131
+ export function listTabs() {
132
+ const result = [];
133
+ for (const [id, tab] of tabs) {
134
+ result.push({
135
+ id,
136
+ title: tab.title,
137
+ url: tab.url,
138
+ active: id === activeTabId,
139
+ });
140
+ }
141
+ return result;
142
+ }
143
+
144
+ // ─── Clear Tabs (for restart) ───────────────────────────────────────────────
145
+
146
+ /**
147
+ * Clears all tab state. Called during browser restart to prevent stale connections.
148
+ */
149
+ export function clearTabs() {
150
+ tabs.clear();
151
+ activeTabId = null;
152
+ }
153
+
154
+ // ─── Tab Sync (update title/url after navigation) ───────────────────────────
155
+
156
+ /**
157
+ * Call after navigation to keep tab metadata current.
158
+ */
159
+ export function syncActiveTab(title, url) {
160
+ if (activeTabId && tabs.has(activeTabId)) {
161
+ const tab = tabs.get(activeTabId);
162
+ if (title) tab.title = title;
163
+ if (url) tab.url = url;
164
+ }
165
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "bwb-browser",
3
- "version": "2.0.4",
4
- "description": "Browser Without Bloat — 30KB MCP browser automation server. Raw CDP, zero bloat, works on any platform.",
3
+ "version": "3.1.0",
4
+ "description": "Browser Without Bloat — 76KB MCP server that gives AI agents browser superpowers. Natural language interaction, live event watching, persistent sessions, multi-tab. Runs on Termux/Android, desktop, and CI. Zero heavy dependencies.",
5
5
  "bin": {
6
6
  "bwb": "bin/bwb"
7
7
  },
8
8
  "files": [
9
9
  "server.mjs",
10
+ "lib/",
10
11
  "bin/bwb",
11
12
  "AGENTS.md",
12
13
  "LICENSE",
@@ -14,7 +15,7 @@
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "node server.mjs",
17
- "test": "node --check server.mjs"
18
+ "test": "node --check server.mjs && node --check lib/*.mjs"
18
19
  },
19
20
  "keywords": [
20
21
  "mcp",
@@ -26,7 +27,24 @@
26
27
  "headless",
27
28
  "bwb",
28
29
  "browser-without-bloat",
29
- "ai-agent"
30
+ "ai-agent",
31
+ "termux",
32
+ "android",
33
+ "natural-language",
34
+ "llm",
35
+ "ai-tools",
36
+ "claude",
37
+ "opencode",
38
+ "agent",
39
+ "web-scraping",
40
+ "browser-testing",
41
+ "e2e-testing",
42
+ "fingerprint",
43
+ "session",
44
+ "lightweight",
45
+ "zero-dependencies",
46
+ "mobile",
47
+ "devtools"
30
48
  ],
31
49
  "author": "Krish Tiwari (@krshforever)",
32
50
  "license": "MIT",