@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 +29 -0
- package/README.md +45 -17
- package/dist/api.js +126 -1
- package/dist/api.js.map +2 -2
- package/dist/index.js +635 -22
- package/dist/index.js.map +4 -4
- package/package.json +4 -2
- package/skills/browser-bridge/SKILL.md +9 -10
- package/skills/browser-bridge/skill.json +4 -0
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
|
+
[](https://www.npmjs.com/package/@btraut/browser-bridge) [](https://github.com/btraut/browser-bridge/actions/workflows/ci.yml) [](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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|