@btraut/browser-bridge 0.1.1 → 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on "Keep a Changelog", and this project adheres to Semantic Versioning.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] - 2026-02-06
10
+
11
+ ### Added
12
+
13
+ - `browser-bridge install` interactive installer for skills and MCP.
14
+ - `browser-bridge skill install` and `browser-bridge skill status`.
15
+ - `browser-bridge mcp install` for Codex, Claude, and Cursor.
16
+ - Skill version manifest (`skill.json`) to detect out-of-date installs.
17
+ - `browser-bridge mcp serve` (while keeping `browser-bridge mcp` working).
18
+
19
+ ## [0.2.0] - 2026-02-05
20
+
21
+ ### Added
22
+
23
+ - `browser-bridge inspect dom-snapshot --max-nodes <n>` (AX format only) to bound snapshot size for agent/LLM consumption.
24
+
25
+ ## [0.1.1] - 2026-02-05
26
+
27
+ ### Added
28
+
29
+ - Initial release.
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
+ <img src="docs/assets/readme-header.png" alt="Browser Bridge header graphic" width="720" />
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@btraut/browser-bridge.svg)](https://www.npmjs.com/package/@btraut/browser-bridge) [![CI](https://github.com/btraut/browser-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/btraut/browser-bridge/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/btraut/browser-bridge.svg)](LICENSE)
4
+
1
5
  # Browser Bridge
2
6
 
3
- Local Chrome control for coding agents. Browser Bridge provides a CLI and an
4
- optional MCP server that drive a real Chrome instance and read page state
5
- through a Chrome extension.
7
+ Local Chrome control for coding agents. Browser Bridge provides a CLI and an optional MCP server that drive your real, local Chrome (not headless) and read page state through a Chrome extension. This keeps you in the loop, with your existing tabs and login state.
6
8
 
7
9
  ## Requirements
8
10
 
9
11
  - Node.js 20+
10
12
  - Chrome (stable)
11
- - Browser Bridge extension (Chrome Web Store; listing coming soon)
13
+ - Browser Bridge extension (Chrome Web Store listing pending; see manual install below)
12
14
  - Local-only usage (all services bind to 127.0.0.1)
13
15
 
14
16
  ## Install
@@ -18,12 +20,24 @@ npm i -g @btraut/browser-bridge
18
20
  browser-bridge --help
19
21
  ```
20
22
 
23
+ ## Chrome Extension (Manual Install)
24
+
25
+ Chrome Web Store listing is pending. For now, install the extension manually:
26
+
27
+ 1. Clone this repo.
28
+ 2. Install deps and build:
29
+
30
+ ```bash
31
+ npm install
32
+ npm run build
33
+ ```
34
+
35
+ 3. Open Chrome and navigate to `chrome://extensions`.
36
+ 4. Enable **Developer mode**, click **Load unpacked**, and select `packages/extension` (the folder with `manifest.json`).
37
+
21
38
  ## Quickstart
22
39
 
23
- 1. Install the Chrome Web Store extension (listing coming soon). For local
24
- testing without the store, you can load unpacked from:
25
- - this repo: `packages/extension`
26
- - npm install: `$(npm root -g)/@btraut/browser-bridge/extension`
40
+ 1. Install the extension (see "Chrome Extension (Manual Install)" above).
27
41
  2. Install the Browser Bridge skill (see below).
28
42
  3. (Optional) Add Browser Bridge to your MCP client (Codex or Claude Code below).
29
43
  4. Run a quick CLI check:
@@ -32,13 +46,23 @@ browser-bridge --help
32
46
  browser-bridge session create
33
47
  # Use the session_id from the output for the next commands.
34
48
  browser-bridge drive navigate --session-id <id> --url https://example.com
35
- browser-bridge inspect dom-snapshot --session-id <id>
49
+ browser-bridge inspect dom-snapshot --session-id <id> --max-nodes 2000
36
50
  browser-bridge session close --session-id <id>
37
51
  ```
38
52
 
53
+ Notes:
54
+
55
+ - `inspect dom-snapshot` defaults to `--format ax`; `--max-nodes` is only supported for AX snapshots.
56
+
39
57
  ## Skills (Codex + Claude Code)
40
58
 
41
- Copy the Browser Bridge skill into your agent skills directory:
59
+ Easiest option (recommended):
60
+
61
+ ```bash
62
+ browser-bridge install
63
+ ```
64
+
65
+ Or copy the Browser Bridge skill into your agent skills directory:
42
66
 
43
67
  ```bash
44
68
  # From this repo:
@@ -54,14 +78,11 @@ Restart your agent app if it does not pick up the new skill automatically.
54
78
 
55
79
  ## MCP Server (Optional)
56
80
 
57
- The MCP server runs over stdio and forwards tool calls to Core. It is optional,
58
- since you can use the CLI directly. MCP clients launch it automatically when
59
- needed, so you typically do not run it yourself.
81
+ The MCP server runs over stdio and forwards tool calls to Core. It is optional, since you can use the CLI directly. MCP clients launch it automatically when needed, so you typically do not run it yourself.
60
82
 
61
83
  - Manual start (debugging): `browser-bridge mcp`
62
84
  - Use your MCP client to call `tools/list`, then `session.create`
63
- - Override Core host/port with `--host`, `--port`, or `BROWSER_BRIDGE_CORE_HOST` /
64
- `BROWSER_BRIDGE_CORE_PORT`.
85
+ - Override Core host/port with `--host`, `--port`, or `BROWSER_BRIDGE_CORE_HOST` / `BROWSER_BRIDGE_CORE_PORT`.
65
86
 
66
87
  ## Add MCP (Codex CLI)
67
88
 
@@ -98,6 +119,14 @@ claude mcp add --transport stdio browser-bridge \
98
119
  - CLI: `browser-bridge diagnostics doctor --session-id <id>`
99
120
  - Reports extension and debugger status alongside session state.
100
121
 
122
+ ## Changelog
123
+
124
+ See `CHANGELOG.md`.
125
+
126
+ ## Releasing
127
+
128
+ See `docs/releasing.md`.
129
+
101
130
  ## Security Model (v1)
102
131
 
103
132
  - Extension <-> Core WebSocket has no authentication; trust local machine only.
@@ -111,7 +140,6 @@ If you are contributing locally, load the extension unpacked:
111
140
  2. Enable **Developer mode**.
112
141
  3. Click **Load unpacked** and select `packages/extension` (repo).
113
142
  4. Confirm the extension's background service worker is running.
114
- 5. Start the Core daemon (or run `browser-bridge session create`) so the
115
- extension can connect to `127.0.0.1`.
143
+ 5. Start the Core daemon (or run `browser-bridge session create`) so the extension can connect to `127.0.0.1`.
116
144
 
117
145
  Additional manual test flows live in `docs/manual-test.md`.
package/dist/api.js CHANGED
@@ -1237,6 +1237,9 @@ var InspectService = class {
1237
1237
  if (input.compact) {
1238
1238
  warnings.push("Compact filter is only supported for AX snapshots.");
1239
1239
  }
1240
+ if (input.maxNodes !== void 0) {
1241
+ warnings.push("max_nodes is only supported for AX snapshots.");
1242
+ }
1240
1243
  if (input.selector && html === "") {
1241
1244
  warnings.push(`Selector not found: ${input.selector}`);
1242
1245
  }
@@ -1289,10 +1292,25 @@ var InspectService = class {
1289
1292
  {}
1290
1293
  );
1291
1294
  }
1292
- const snapshot = input.interactive || input.compact ? this.applyAxSnapshotFilters(result2, {
1295
+ let snapshot = input.interactive || input.compact ? this.applyAxSnapshotFilters(result2, {
1293
1296
  interactiveOnly: input.interactive,
1294
1297
  compact: input.compact
1295
1298
  }) : result2;
1299
+ let truncated = false;
1300
+ const truncationWarnings = [];
1301
+ if (input.maxNodes !== void 0) {
1302
+ const truncatedResult = this.truncateAxSnapshot(
1303
+ snapshot,
1304
+ input.maxNodes
1305
+ );
1306
+ snapshot = truncatedResult.snapshot;
1307
+ truncated = truncatedResult.truncated;
1308
+ if (truncated) {
1309
+ truncationWarnings.push(
1310
+ `AX snapshot truncated to ${input.maxNodes} nodes.`
1311
+ );
1312
+ }
1313
+ }
1296
1314
  const refMap = this.assignRefsToAxSnapshot(snapshot);
1297
1315
  const refWarnings = await this.applySnapshotRefs(
1298
1316
  selection.tabId,
@@ -1301,11 +1319,13 @@ var InspectService = class {
1301
1319
  const warnings = [
1302
1320
  ...selection.warnings ?? [],
1303
1321
  ...selectorWarnings,
1322
+ ...truncationWarnings,
1304
1323
  ...refWarnings ?? []
1305
1324
  ];
1306
1325
  return {
1307
1326
  format: "ax",
1308
1327
  snapshot,
1328
+ ...truncated ? { truncated: true } : {},
1309
1329
  ...warnings.length > 0 ? { warnings } : {}
1310
1330
  };
1311
1331
  } catch (error) {
@@ -1322,6 +1342,7 @@ var InspectService = class {
1322
1342
  const warnings = [
1323
1343
  ...selection.warnings ?? [],
1324
1344
  "AX snapshot failed; returned HTML instead.",
1345
+ ...input.maxNodes !== void 0 ? ["max_nodes is only supported for AX snapshots."] : [],
1325
1346
  ...input.interactive ? ["Interactive filter is only supported for AX snapshots."] : [],
1326
1347
  ...input.compact ? ["Compact filter is only supported for AX snapshots."] : [],
1327
1348
  ...input.selector && html === "" ? [`Selector not found: ${input.selector}`] : []
@@ -1824,6 +1845,106 @@ var InspectService = class {
1824
1845
  }
1825
1846
  return filtered;
1826
1847
  }
1848
+ truncateAxSnapshot(snapshot, maxNodes) {
1849
+ const nodes = this.getAxNodes(snapshot);
1850
+ if (!Number.isFinite(maxNodes) || maxNodes <= 0) {
1851
+ return { snapshot, truncated: false };
1852
+ }
1853
+ if (nodes.length === 0 || nodes.length <= maxNodes) {
1854
+ return { snapshot, truncated: false };
1855
+ }
1856
+ const nodeById = /* @__PURE__ */ new Map();
1857
+ const parentCount = /* @__PURE__ */ new Map();
1858
+ for (const node of nodes) {
1859
+ if (!node || typeof node !== "object" || typeof node.nodeId !== "string") {
1860
+ continue;
1861
+ }
1862
+ nodeById.set(node.nodeId, node);
1863
+ parentCount.set(node.nodeId, parentCount.get(node.nodeId) ?? 0);
1864
+ }
1865
+ if (nodeById.size === 0) {
1866
+ const sliced = nodes.slice(0, maxNodes);
1867
+ for (const node of sliced) {
1868
+ if (node && typeof node === "object" && Array.isArray(node.childIds)) {
1869
+ node.childIds = [];
1870
+ }
1871
+ }
1872
+ return {
1873
+ snapshot: this.replaceAxNodes(snapshot, sliced),
1874
+ truncated: true
1875
+ };
1876
+ }
1877
+ for (const node of nodes) {
1878
+ if (!node || typeof node !== "object" || !Array.isArray(node.childIds)) {
1879
+ continue;
1880
+ }
1881
+ for (const childId of node.childIds) {
1882
+ if (typeof childId !== "string") {
1883
+ continue;
1884
+ }
1885
+ parentCount.set(childId, (parentCount.get(childId) ?? 0) + 1);
1886
+ }
1887
+ }
1888
+ let roots = Array.from(nodeById.keys()).filter(
1889
+ (id) => (parentCount.get(id) ?? 0) === 0
1890
+ );
1891
+ if (roots.length === 0) {
1892
+ const first = nodes.find(
1893
+ (node) => node && typeof node.nodeId === "string"
1894
+ )?.nodeId;
1895
+ if (first) {
1896
+ roots = [first];
1897
+ }
1898
+ }
1899
+ const kept = /* @__PURE__ */ new Set();
1900
+ const visited = /* @__PURE__ */ new Set();
1901
+ const queue = [...roots];
1902
+ while (queue.length > 0 && kept.size < maxNodes) {
1903
+ const id = queue.shift();
1904
+ if (!id || visited.has(id)) {
1905
+ continue;
1906
+ }
1907
+ visited.add(id);
1908
+ const node = nodeById.get(id);
1909
+ if (!node) {
1910
+ continue;
1911
+ }
1912
+ kept.add(id);
1913
+ if (Array.isArray(node.childIds)) {
1914
+ for (const childId of node.childIds) {
1915
+ if (typeof childId === "string" && !visited.has(childId)) {
1916
+ queue.push(childId);
1917
+ }
1918
+ }
1919
+ }
1920
+ }
1921
+ if (kept.size === 0) {
1922
+ const fallback = [];
1923
+ for (const node of nodes) {
1924
+ if (fallback.length >= maxNodes) {
1925
+ break;
1926
+ }
1927
+ if (node && typeof node.nodeId === "string") {
1928
+ fallback.push(node.nodeId);
1929
+ }
1930
+ }
1931
+ fallback.forEach((id) => kept.add(id));
1932
+ }
1933
+ const filtered = nodes.filter(
1934
+ (node) => node && typeof node.nodeId === "string" && kept.has(node.nodeId)
1935
+ );
1936
+ for (const node of filtered) {
1937
+ if (Array.isArray(node.childIds)) {
1938
+ node.childIds = node.childIds.filter(
1939
+ (id) => typeof id === "string" && kept.has(id)
1940
+ );
1941
+ }
1942
+ }
1943
+ return {
1944
+ snapshot: this.replaceAxNodes(snapshot, filtered),
1945
+ truncated: true
1946
+ };
1947
+ }
1827
1948
  filterAxSnapshot(snapshot, predicate) {
1828
1949
  const nodes = this.getAxNodes(snapshot);
1829
1950
  if (nodes.length === 0) {
@@ -2945,6 +3066,9 @@ var InspectDomSnapshotInputSchema = import_zod2.z.object({
2945
3066
  consistency: InspectConsistencySchema.default("best_effort"),
2946
3067
  interactive: import_zod2.z.boolean().default(false),
2947
3068
  compact: import_zod2.z.boolean().default(false),
3069
+ // Used primarily to bound large AX trees for LLM/agent consumption.
3070
+ // CLI passes this as a string, so coerce for convenience.
3071
+ max_nodes: import_zod2.z.coerce.number().int().positive().max(5e4).optional(),
2948
3072
  selector: import_zod2.z.string().min(1).optional(),
2949
3073
  target: TargetHintSchema.optional()
2950
3074
  });
@@ -3642,6 +3766,7 @@ var registerInspectRoutes = (router, options) => {
3642
3766
  consistency: body.consistency,
3643
3767
  interactive: body.interactive,
3644
3768
  compact: body.compact,
3769
+ maxNodes: body.max_nodes,
3645
3770
  selector: body.selector,
3646
3771
  targetHint: resolveTargetHint(body.target, options)
3647
3772
  });