bwb-browser 4.0.0 → 4.0.1

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/AGENTS.md CHANGED
@@ -226,6 +226,18 @@ BWB_CHROME_PATH=/path/to/chrome bwb
226
226
  bwb --browser-path /path/to/chrome
227
227
  ```
228
228
 
229
+ ### Attach Mode (drive the window you're looking at)
230
+ ```bash
231
+ # Launch your browser with remote debugging first, e.g.:
232
+ # brave --remote-debugging-port=9222
233
+ BWB_ATTACH_PORT=9222 bwb
234
+ # or
235
+ bwb --attach-port 9222
236
+ ```
237
+ Guest mode: no spawn, no kill, no journal restore. `browser_newTab` opens a
238
+ VISIBLE tab in your window. Auto-shed is disabled (those are your real tabs) —
239
+ pressure is reported, you close them. `browser_status` shows `attached: true`.
240
+
229
241
  ---
230
242
 
231
243
  ## License
package/README.md CHANGED
@@ -90,7 +90,7 @@ Create tabs, close them, switch between them, save cookies, load them back. Like
90
90
 
91
91
  Normalizes `navigator.webdriver`, plugins, languages, and user-agent for testing environments. Not "stealth mode" — just honest fingerprint normalization so your tests actually match real user conditions.
92
92
 
93
- ### 7. Element Screenshots (new in 3.2.0)
93
+ ### 7. Element Screenshots
94
94
 
95
95
  Capture just one element — a login form, a chart, a product card — not the whole page:
96
96
 
@@ -105,20 +105,24 @@ Every screenshot is saved to disk (Android: `/storage/emulated/0/Download/bwb-sc
105
105
 
106
106
  ---
107
107
 
108
- ## What's New in 3.2.0
108
+ ## What's New in 4.0.0 — "Lightweight Like Air"
109
109
 
110
- ### New
111
- - **`browser_screenshot({ selector })`** — element-level capture. Grab just the login form, the chart, the product card — not the whole page.
112
- - **Screenshot directory auto-detect** — Termux/Android → `/storage/emulated/0/Download/bwb-screenshots/`, desktop → `~/bwb-screenshots/`. Override with `BWB_SCREENSHOTS_DIR`.
110
+ ### Static-first fetch ladder
111
+ - **`browser_goto` no longer spawns Chromium for plain pages** — fetch + extract in milliseconds (`mode: "static"`). JS pages escalate automatically (`mode: "browser"` + reason). Dead URLs error without spawning anything.
112
+ - **On-demand capabilities** — `browser_download` / `browser_export` ship as verbs, not weight. Missing backends prompt for consent install. Nothing heavy is ever bundled.
113
113
 
114
- ### Bug Fixes
115
- - **`browser_back` rewritten** — native CDP history navigation (the `Page.goBack` call doesn't exist in bundled CDP 1.3; the old `history.back()` JS hack is gone). Verified with real two-step back navigation.
116
- - **`browser_act` precision fixes** — navigation regex no longer swallows compound instructions ("go to X and read the title" works), "fill X with Y" vs "type Y in X" no longer swap target/text, and `search` can't hijack "fill search with X"
117
- - **`killOrphanedChrome` safety** — graceful SIGTERM→SIGKILL, now scoped to bwb's own profile so it never kills another agent's browser
118
- - **`browser_restart` hygiene** — watch listeners can't outlive the dying protocol
119
- - **`browser_status` accuracy** — uses the real bound port, no more hardcoded 9222 poke
114
+ ### Vigilance system
115
+ - **Every tool response carries a `[bwb resources]` footer** — MCP + Chromium MB, tabs, ok/watch/critical. `browser_watch` streams memory samples on its existing poll rhythm.
116
+ - **Thresholds act, then report** — critical pressure hibernates tabs, tears down at one tab, journals everything. The agent reads about the save, never discovers the OOM.
120
117
 
121
- *Fixes from the PR #1 code review by @netzro (Hermes Agent) are incorporated and credited in the [changelog](./CHANGELOG.md).*
118
+ ### Survival profile
119
+ - **`--lean` auto-enables on Termux** — capped renderers, silenced background services, 3-tab cap, 5-minute mayfly teardown, 256MB JS heap. `--nuclear` opts into `--single-process`.
120
+ - **Tab journal + lazy restore** — kills become resume points, not disasters. `bwb --setup` prints a survival guide.
121
+
122
+ ### Breaking
123
+ - `browser_title` + `browser_url` folded into `browser_status.targets`. Still 26 tools — that's now a release gate.
124
+
125
+ *Full story in the [changelog](./CHANGELOG.md). Older releases documented there too.*
122
126
 
123
127
  ---
124
128
 
@@ -127,7 +131,7 @@ Every screenshot is saved to disk (Android: `/storage/emulated/0/Download/bwb-sc
127
131
  ```bash
128
132
  npm install -g bwb-browser
129
133
  bwb --version
130
- # → bwb-browser 3.2.0
134
+ # → bwb-browser 4.0.0
131
135
  ```
132
136
 
133
137
  Done. If you have Chrome/Chromium anywhere on your system, bwb finds it. No config files. No environment variables. Just works.
@@ -151,12 +155,10 @@ bwb
151
155
  | **`browser_watch`** | 🔥 Live event capture — console, network, errors, navigation |
152
156
  | **`browser_diagnose`** | 🔥 Full page health check — perf, errors, broken images, score |
153
157
  | **`browser_fingerprint`** | 🔥 Realistic browser profile for testing |
154
- | `browser_goto` | Navigate to a URL |
158
+ | `browser_goto` | Navigate — static-first, escalates to browser with reason |
155
159
  | `browser_screenshot` | Take a screenshot — whole page, viewport, or a single element via `selector` |
156
160
  | `browser_html` | Get page/selector HTML |
157
161
  | `browser_text` | Get page/selector text |
158
- | `browser_title` | Get page title |
159
- | `browser_url` | Get current URL |
160
162
  | `browser_back` | Go back in history |
161
163
  | `browser_click` | Click an element (native CDP) |
162
164
  | `browser_fill` | Fill an input field (native CDP) |
@@ -171,7 +173,9 @@ bwb
171
173
  | `browser_saveCookies` | Save session to disk |
172
174
  | `browser_loadCookies` | Load session from disk |
173
175
  | `browser_listSessions` | List saved sessions |
174
- | `browser_status` | Browser connection info |
176
+ | `browser_download` | Download media (needs system yt-dlp, consent-gated) |
177
+ | `browser_export` | Export md/txt/html (pdf/docx/pptx need pip libs, consent-gated) |
178
+ | `browser_status` | Status + live resources + active profile |
175
179
  | `browser_restart` | Restart the browser |
176
180
 
177
181
  ---
@@ -212,13 +216,13 @@ See [AGENTS.md](./AGENTS.md) for copy-paste configs for each one.
212
216
 
213
217
  ## The Backstory
214
218
 
215
- I built this because I was tired of every browser automation tool assuming you have 400MB to spare and a desktop-class machine. I work from my phone sometimes. Termux exists. Why shouldn't browser automation work there too?
219
+ Every browser automation tool assumes you have 400MB to spare and a desktop-class machine. That assumption excludes phones, cheap VPS boxes, Raspberry Pis, and CI runners — most of the world's computers.
216
220
 
217
- So I did what any reasonable person would do: I ignored all the existing solutions and wrote my own, using nothing but raw CDP — the protocol Chrome speaks natively. No wrappers. No abstractions. Just JSON messages over WebSocket.
221
+ bwb is engineered against the hardest constraint first: **a memory-pressured device where every megabyte is contested.** No bundled browser. No wrapper frameworks. Just raw CDP — the protocol Chrome speaks natively — plus a static-fetch ladder so Chromium only starts when JavaScript demands it. Mobile-first isn't a feature here. It's the design spec everything else has to survive.
218
222
 
219
- The result is 76KB of source code that does what 400MB of dependencies do. It's not _better_ code — it's _less_ code. And sometimes less is all you need.
223
+ The result is ~136KB of source that does what 400MB of dependencies do. Not better code — less code, held to budgets: 26 tools max, 60 kB tarball max, zero native modules. Constraints are features.
220
224
 
221
- *— Krish Tiwari ([@krshforever](https://github.com/krshforever)), somewhere on an Indian train, writing code on a phone*
225
+ *— Krish Tiwari ([@krshforever](https://github.com/krshforever))*
222
226
 
223
227
  ---
224
228
 
package/lib/browser.mjs CHANGED
@@ -35,8 +35,15 @@ export const cfg = {
35
35
  nuclear: false, // --single-process: max RAM saving, min stability. Opt-in only.
36
36
  idleMs: null, // null = auto (5min lean, off desktop); 0 = never teardown
37
37
  tabMax: null, // null = auto (3 lean, unlimited desktop); 0 = unlimited
38
+ attachPort: 0, // 0 = off (spawn own browser). N = attach to existing
39
+ // browser's CDP port (e.g. 9222) — never spawns, never kills.
38
40
  };
39
41
 
42
+ // True when connected to a foreign (user-owned) browser via --attach-port.
43
+ // Attached browsers are never spawned, killed, or journal-restored —
44
+ // bwb is a guest there. Tab hibernation still works but closes REAL tabs.
45
+ export let attached = false;
46
+
40
47
  // ─── Environment ─────────────────────────────────────────────────────────────
41
48
 
42
49
  export function isTermux() {
@@ -119,8 +126,11 @@ export function findBrowserPath(cliPath) {
119
126
  "chromium-browser",
120
127
  "chromium",
121
128
  "google-chrome-stable",
129
+ "brave-browser",
130
+ "brave",
122
131
  "/usr/bin/google-chrome",
123
132
  "/usr/bin/chromium-browser",
133
+ "/usr/bin/brave-browser",
124
134
  "/snap/bin/chromium",
125
135
  ],
126
136
  darwin: [
@@ -179,6 +189,34 @@ export async function ensureBrowser() {
179
189
  browserStartup = new Promise((res, rej) => { startResolve = res; startReject = rej; });
180
190
  browserStartup.catch(() => { browserStartup = null; });
181
191
 
192
+ // ─── Attach mode: guest on someone else's browser ──────────────────────
193
+ // No spawn, no orphan-kill, no journal restore (never navigate a user's
194
+ // tabs on connect). ensureDefaultTab() registers whatever is already open.
195
+ if (cfg.attachPort) {
196
+ (async () => {
197
+ try {
198
+ const port = cfg.attachPort;
199
+ const cdp = await CDP({ port });
200
+ protocol = cdp;
201
+ browser = null;
202
+ attached = true;
203
+ actualCdpPort = port;
204
+ startResolve(cdp);
205
+ pokeActivity();
206
+ } catch (err) {
207
+ browserStartup = null;
208
+ browserExited = true;
209
+ attached = false;
210
+ startReject(new Error(
211
+ `Attach failed: no browser listening on CDP port ${cfg.attachPort} (${err.message}). ` +
212
+ `Launch one with --remote-debugging-port=${cfg.attachPort} first.`
213
+ ));
214
+ }
215
+ })();
216
+ return browserStartup;
217
+ }
218
+ attached = false;
219
+
182
220
  (async () => {
183
221
  try {
184
222
  const browserPath = assertBrowserExists(cfg.browserPath);
@@ -297,7 +335,8 @@ export async function stopBrowser(reason = "manual") {
297
335
  try { await protocol.close(); } catch {}
298
336
  protocol = null;
299
337
  }
300
- if (browser) {
338
+ // Attached browsers are foreign — disconnect only, never kill.
339
+ if (browser && !attached) {
301
340
  try { browser.kill("SIGTERM"); } catch {}
302
341
  await new Promise(r => setTimeout(r, 1500));
303
342
  try { browser.kill("SIGKILL"); } catch {}
@@ -306,6 +345,7 @@ export async function stopBrowser(reason = "manual") {
306
345
  } catch {}
307
346
  browserExited = true; // next ensureBrowser() respawns fresh + restores journal
308
347
  actualCdpPort = null;
348
+ attached = false;
309
349
  browserStartup = null;
310
350
  cleaningUp = false;
311
351
  try {
@@ -326,7 +366,9 @@ export async function restartBrowser() {
326
366
  try { await protocol.close(); } catch {}
327
367
  protocol = null;
328
368
  }
329
- if (browser) {
369
+ // Attached browsers are foreign — disconnect only, never kill.
370
+ // Next ensureBrowser() re-attaches (no journal restore in attach mode).
371
+ if (browser && !attached) {
330
372
  browser.kill("SIGTERM");
331
373
  await new Promise(r => setTimeout(r, 2000));
332
374
  try { browser.kill("SIGKILL"); } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bwb-browser",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Browser Without Bloat — lightweight MCP server that gives AI agents browser superpowers. Static-first fetch ladder (Chromium only when JS demands it), lean profiles for Termux/1GB VPS, resource vigilance, tab journaling. Runs anywhere. Stays lean like air.",
5
5
  "bin": {
6
6
  "bwb": "bin/bwb"
package/server.mjs CHANGED
@@ -21,6 +21,10 @@
21
21
  * (default: 5min lean, off desktop)
22
22
  * --tab-max / BWB_TAB_MAX — Live-tab cap, oldest hibernated
23
23
  * (default: 3 lean, unlimited desktop)
24
+ * --attach-port / BWB_ATTACH_PORT — Attach to an already-running
25
+ * browser's CDP port (e.g. 9222)
26
+ * instead of spawning. Guest mode:
27
+ * never spawns, kills, or restores.
24
28
  */
25
29
 
26
30
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -35,7 +39,7 @@ import { fileURLToPath } from "url";
35
39
 
36
40
  import {
37
41
  ensureBrowser, restartBrowser, stopBrowser, saveScreenshot,
38
- cfg, browser, browserExited, actualCdpPort,
42
+ cfg, browser, browserExited, actualCdpPort, attached,
39
43
  isTermux, pokeActivity, setIdleSuppressed,
40
44
  } from "./lib/browser.mjs";
41
45
 
@@ -87,6 +91,7 @@ function parseArgs() {
87
91
  case "--nuclear": cliCfg.nuclear = args[++i] !== "false"; break;
88
92
  case "--idle": cliCfg.idleMs = parseInt(args[++i], 10); break;
89
93
  case "--tab-max": cliCfg.tabMax = parseInt(args[++i], 10); break;
94
+ case "--attach-port": cliCfg.attachPort = parseInt(args[++i], 10); break;
90
95
  case "--version": console.log(`bwb-browser ${BWB_VERSION}`); process.exit(0);
91
96
  case "--help": printHelp(); process.exit(0);
92
97
  }
@@ -117,6 +122,8 @@ OPTIONS:
117
122
  --nuclear Add --single-process (max saving, min stability)
118
123
  --idle <ms> Mayfly teardown after N ms idle (default: 5min lean)
119
124
  --tab-max <n> Live-tab cap, oldest hibernated (default: 3 lean)
125
+ --attach-port <n> Attach to a running browser's CDP port (e.g. 9222).
126
+ Guest mode: no spawn, no kill, visible window.
120
127
  --version Print version
121
128
  --help Show this help
122
129
 
@@ -198,6 +205,7 @@ async function ensureDeps() {
198
205
 
199
206
  Object.assign(cfg, parseArgs());
200
207
  cfg.port = cfg.port || parseInt(process.env.BWB_CDP_PORT || "0", 10);
208
+ cfg.attachPort = cfg.attachPort || parseInt(process.env.BWB_ATTACH_PORT || "0", 10);
201
209
  cfg.headless = cfg.headless !== undefined ? cfg.headless : (process.env.BWB_HEADLESS !== "false");
202
210
  cfg.userDataDir = cfg.userDataDir || process.env.BWB_USER_DATA_DIR || join(homedir(), ".cache", "bwb-browser");
203
211
  cfg.screenshotsDir = cfg.screenshotsDir || process.env.BWB_SCREENSHOTS_DIR || (() => {
@@ -733,9 +741,11 @@ const tools = {
733
741
  schema: {},
734
742
  handler: async () => {
735
743
  const status = { connected: false, port: cfg.port, actualPort: null, running: false, pid: null, tabs: [] };
736
- if (browser && !browserExited) {
744
+ // Attach mode: no child process (browser === null) — liveness is the CDP link.
745
+ if ((browser && !browserExited) || (attached && !browserExited)) {
737
746
  status.running = true;
738
- status.pid = browser.pid;
747
+ status.pid = browser ? browser.pid : null;
748
+ if (attached) status.attached = true;
739
749
  status.tabs = listTabs();
740
750
  // actualCdpPort is the real bound port; cfg.port may be 0 (random).
741
751
  // Never fall back to a hardcoded 9222 — that could be another tool's browser.
@@ -753,6 +763,7 @@ const tools = {
753
763
  status.resources = sampleResources(browser?.pid, status.tabs.filter((t) => !t.hibernated).length);
754
764
  status.resources.state = assess(status.resources);
755
765
  status.profile = { lean: cfg.lean, nuclear: cfg.nuclear, idleMs: cfg.idleMs, tabMax: cfg.tabMax };
766
+ if (attached) status.profile.attached = actualCdpPort || cfg.attachPort;
756
767
  } catch {}
757
768
  return { content: [{ type: "text", text: JSON.stringify(status) }] };
758
769
  },
@@ -788,7 +799,9 @@ for (const [name, tool] of Object.entries(tools)) {
788
799
  const liveTabs = listTabs().filter((t) => !t.hibernated).length;
789
800
  const sample = sampleResources(browser?.pid, liveTabs);
790
801
  let note = "";
791
- if (assess(sample) === "critical" && browser && !browserExited) {
802
+ // Never auto-shed in attach mode: those are the user's REAL tabs.
803
+ // Report pressure in the footer; the human closes their own tabs.
804
+ if (!attached && assess(sample) === "critical" && browser && !browserExited) {
792
805
  // Evidence first: keep the peak numbers that triggered the shed.
793
806
  const peak = `${sample.mcpMb}+${sample.chromiumMb ?? "?"}MB`;
794
807
  // Shed oldest non-active tabs first; teardown at one tab. Journal keeps all.