bwb-browser 4.0.1 → 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/lib/vigil.mjs CHANGED
@@ -7,7 +7,11 @@
7
7
  * watcher stream carries memory samples. That IS the "instant" channel.
8
8
  */
9
9
 
10
- import { execSync } from "child_process";
10
+ import { execFile } from "child_process";
11
+ import { promisify } from "util";
12
+ import { platform } from "os";
13
+
14
+ const execFileAsync = promisify(execFile);
11
15
 
12
16
  // 1GB-box budgets (MB) when lean; roomier desktop defaults otherwise.
13
17
  // Override any via BWB_WARN_MB / BWB_CRIT_MB. resolveBudgets() is called
@@ -24,33 +28,50 @@ export function resolveBudgets(isLean) {
24
28
 
25
29
  const MB = 1024 * 1024;
26
30
 
31
+ // The process table is sampled at most every SAMPLE_TTL_MS. Reading it with a
32
+ // synchronous `ps` on EVERY tool call blocked the event loop (including
33
+ // browser_watch's event handlers) and listed every process on the box.
34
+ const SAMPLE_TTL_MS = 5000;
35
+ let sampleCache = { at: 0, procs: null };
36
+
37
+ function readProcessTable() {
38
+ if (Date.now() - sampleCache.at < SAMPLE_TTL_MS) return sampleCache.procs;
39
+ sampleCache = { at: Date.now(), procs: null };
40
+ if (platform() === "win32") return null; // no ps: resource readings degrade to self
41
+ execFileAsync("ps", ["-eo", "pid=,ppid=,rss="], { timeout: 3000, maxBuffer: 4 * 1024 * 1024 })
42
+ .then(({ stdout }) => {
43
+ const procs = stdout.trim().split("\n").map((l) => {
44
+ const [pid, ppid, rss] = l.trim().split(/\s+/).map(Number);
45
+ return { pid, ppid, rss };
46
+ }).filter((p) => Number.isFinite(p.pid));
47
+ // Only publish if no newer sample landed while we were reading.
48
+ if (Date.now() - sampleCache.at >= SAMPLE_TTL_MS - 100) sampleCache = { at: Date.now(), procs };
49
+ })
50
+ .catch(() => {});
51
+ return sampleCache.procs;
52
+ }
53
+
27
54
  function selfMb() {
28
55
  return Math.round(process.memoryUsage().rss / MB);
29
56
  }
30
57
 
31
- // Sum RSS of a pid + its descendants via ps. Null when unmeasurable.
58
+ // Sum RSS of a pid + its descendants via the cached process table.
59
+ // Async warm-up: the first call returns null, later ones are accurate.
32
60
  function treeMb(rootPid) {
33
61
  if (!rootPid) return null;
34
- try {
35
- const out = execSync("ps -o pid=,ppid=,rss= -e", { encoding: "utf8", timeout: 3000 });
36
- const procs = out.trim().split("\n").map((l) => {
37
- const [pid, ppid, rss] = l.trim().split(/\s+/).map(Number);
38
- return { pid, ppid, rss };
39
- });
40
- const kids = new Set([Number(rootPid)]);
41
- let grew = true;
42
- while (grew) {
43
- grew = false;
44
- for (const p of procs) {
45
- if (kids.has(p.ppid) && !kids.has(p.pid)) { kids.add(p.pid); grew = true; }
46
- }
62
+ const procs = readProcessTable();
63
+ if (!procs) return null;
64
+ const kids = new Set([Number(rootPid)]);
65
+ let grew = true;
66
+ while (grew) {
67
+ grew = false;
68
+ for (const p of procs) {
69
+ if (kids.has(p.ppid) && !kids.has(p.pid)) { kids.add(p.pid); grew = true; }
47
70
  }
48
- let kb = 0;
49
- for (const p of procs) if (kids.has(p.pid) && Number.isFinite(p.rss)) kb += p.rss;
50
- return Math.round(kb / 1024);
51
- } catch {
52
- return null;
53
71
  }
72
+ let kb = 0;
73
+ for (const p of procs) if (kids.has(p.pid) && Number.isFinite(p.rss)) kb += p.rss;
74
+ return Math.round(kb / 1024);
54
75
  }
55
76
 
56
77
  /** Snapshot: {mcpMb, chromiumMb|null, tabCount} */
@@ -61,6 +82,8 @@ export function sampleResources(browserPid, tabCount = 0) {
61
82
  /**
62
83
  * Policy verdict: 'ok' | 'watch' | 'critical'.
63
84
  * critical = shed load NOW (hibernate tabs → teardown). watch = warn only.
85
+ * Budgets are tuned for a 1GB box (lean). Desktop defaults are deliberately
86
+ * high, so 'critical' rarely fires there — that is intentional, not a bug.
64
87
  */
65
88
  export function assess(sample) {
66
89
  const total = (sample.chromiumMb || 0) + sample.mcpMb;
@@ -73,5 +96,5 @@ export function assess(sample) {
73
96
  export function resourceFooter(sample, note = "") {
74
97
  const c = sample.chromiumMb === null ? "browser:off" : `browser:${sample.chromiumMb}MB`;
75
98
  const trend = assess(sample);
76
- return `[bwb resources] mcp:${sample.mcpMb}MB ${c} tabs:${sample.tabCount} state:${trend}${note ? " " + note : ""}]`;
99
+ return `[bwb resources] mcp:${sample.mcpMb}MB ${c} tabs:${sample.tabCount} state:${trend}${note ? " " + note : ""}`;
77
100
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bwb-browser",
3
- "version": "4.0.1",
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.",
3
+ "version": "4.1.0",
4
+ "description": "Browser Without Bloat \u2014 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"
7
7
  },
@@ -15,7 +15,12 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "start": "node server.mjs",
18
- "test": "node --check server.mjs && node --check lib/*.mjs"
18
+ "test": "node --test test/*.test.mjs",
19
+ "lint": "for f in server.mjs lib/*.mjs; do node --check \"$f\" || exit 1; done",
20
+ "smoke": "node test/smoke.mjs",
21
+ "test:browser": "node --test test/integration.mjs",
22
+ "test:all": "npm run lint && npm test && npm run verify:docs && npm run smoke && npm run test:browser",
23
+ "verify:docs": "node scripts/verify-docs.mjs"
19
24
  },
20
25
  "keywords": [
21
26
  "mcp",
@@ -42,7 +47,7 @@
42
47
  "fingerprint",
43
48
  "session",
44
49
  "lightweight",
45
- "zero-dependencies",
50
+ "low-dependency",
46
51
  "mobile",
47
52
  "devtools"
48
53
  ],
@@ -61,9 +66,12 @@
61
66
  },
62
67
  "dependencies": {
63
68
  "@modelcontextprotocol/sdk": "^1.0.0",
64
- "chrome-remote-interface": "^0.34.0",
65
- "zod": "^3.22.0",
66
69
  "@mozilla/readability": "^0.6.0",
67
- "linkedom": "^0.18.13"
70
+ "chrome-remote-interface": "^0.34.0",
71
+ "linkedom": "^0.18.13",
72
+ "zod": "^3.22.0"
73
+ },
74
+ "devDependencies": {
75
+ "jsdom": "^26.1.0"
68
76
  }
69
77
  }