@btraut/browser-bridge 0.4.0 → 0.4.2

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 CHANGED
@@ -14,6 +14,26 @@ _TBD_
14
14
 
15
15
  _TBD_
16
16
 
17
+ ## [0.4.2] - 2026-02-07
18
+
19
+ ### Fixed
20
+
21
+ - Fix the GitHub release workflow tag/version verification step so tag pushes reliably create a GitHub Release and upload the extension zip.
22
+
23
+ ## [0.4.1] - 2026-02-07
24
+
25
+ ### Added
26
+
27
+ - `health_check` MCP tool and core endpoint (`/health_check`) for uptime/memory/session/extension status.
28
+ - Full-page scrolling screenshots for `artifacts.screenshot` via `fullPage: true` (scroll + stitch, up to ~50K px tall).
29
+ - MCP Streamable HTTP server transport (in addition to stdio).
30
+ - Pre-built Chrome extension zip attached to GitHub releases.
31
+ - Element-targeted screenshots for `artifacts.screenshot` via `selector`.
32
+
33
+ ### Fixed
34
+
35
+ _TBD_
36
+
17
37
  ## [0.4.0] - 2026-02-06
18
38
 
19
39
  ### Added
package/README.md CHANGED
@@ -1,10 +1,47 @@
1
1
  <img src="docs/assets/readme-header.png" alt="Browser Bridge header graphic" width="720" />
2
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)
3
+ [![npm version](https://img.shields.io/npm/v/@btraut/browser-bridge.svg)](https://www.npmjs.com/package/@btraut/browser-bridge) [![npm downloads](https://img.shields.io/npm/dm/@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
4
 
5
5
  # Browser Bridge
6
6
 
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.
7
+ **Reliable local Chrome control for coding agents.**
8
+
9
+ Browser Bridge drives your real, local Chrome (not headless) and inspects page state through a Chrome extension plus a local daemon. You stay in the loop with your existing tabs and login state.
10
+
11
+ What makes it different:
12
+
13
+ - **Real browser state**: operate on your actual Chrome profile (tabs, cookies, logins, extensions).
14
+ - **Two-plane architecture**: a **drive** plane that does what a user does (click, type, navigate), plus an **inspect** plane that reads state (DOM, console, screenshots). This separation makes runs less flaky and lets inspection happen in parallel.
15
+ - **Token-efficient inspection**: stable element refs like `@e1` (find once, reuse everywhere) plus knobs to bound output (`--max-nodes`, `--compact`, `--interactive`, `--selector`).
16
+ - **Structured errors for agents**: stable error codes with a `retryable` flag (no more guessing whether to retry).
17
+ - **Recovery-first**: sessions have an explicit state machine with `session.recover()` and `diagnostics doctor`.
18
+ - **Inspect beyond screenshots**: DOM snapshots (AX + HTML) and `inspect dom-diff` to detect page changes.
19
+
20
+ ## Why Browser Bridge
21
+
22
+ Browser Bridge is built for agent reliability and "stay logged in" workflows in your real Chrome, not for headless test automation.
23
+
24
+ If you're coming from Playwright/Puppeteer-style tooling:
25
+
26
+ - Browser Bridge targets the user's existing, interactive Chrome session by default (typical Playwright/Puppeteer flows spin up a separate browser/context).
27
+ - Browser Bridge surfaces retry guidance in the API (`retryable`) instead of forcing the agent to infer it from exceptions and timing.
28
+ - Browser Bridge ships a first-class inspect plane (DOM snapshots, diffs, diagnostics) designed for LLM consumption, with output-bounding options to keep agent context small.
29
+
30
+ If you're coming from an extension-only MCP tool:
31
+
32
+ - Browser Bridge puts a stateful local Core daemon behind the tools (sessions, recovery, diagnostics, artifacts).
33
+ - Drive actions are serialized for determinism; inspect is a separate plane that can keep producing structured state.
34
+ - CLI works everywhere; MCP is optional.
35
+
36
+ ## How It Works
37
+
38
+ Core keeps a session state machine and exposes a small set of stable tools:
39
+
40
+ - `session.*` - lifecycle + recovery
41
+ - `drive.*` - navigation + input (single-flight)
42
+ - `inspect.*` - DOM snapshots/diffs + evaluation
43
+ - `diagnostics.*` - health checks
44
+ - `artifacts.*` - screenshots
8
45
 
9
46
  ## Requirements
10
47
 
@@ -13,7 +50,7 @@ Local Chrome control for coding agents. Browser Bridge provides a CLI and an opt
13
50
  - Browser Bridge extension (Chrome Web Store listing pending; see manual install below)
14
51
  - Local-only usage (all services bind to 127.0.0.1)
15
52
 
16
- ## Install
53
+ ## Install (CLI)
17
54
 
18
55
  ```bash
19
56
  npm i -g @btraut/browser-bridge
@@ -24,6 +61,10 @@ browser-bridge --help
24
61
 
25
62
  Chrome Web Store listing is pending. For now, install the extension manually:
26
63
 
64
+ 1. Download the latest pre-built extension zip from [GitHub Releases](https://github.com/btraut/browser-bridge/releases) (Assets), unzip it, and use the unzipped folder for step 3.
65
+
66
+ Alternative (build from source):
67
+
27
68
  1. Clone this repo.
28
69
  2. Install deps and build:
29
70
 
@@ -33,13 +74,13 @@ npm run build
33
74
  ```
34
75
 
35
76
  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`).
77
+ 4. Enable **Developer mode**, click **Load unpacked**, and select the extension folder (the folder with `manifest.json`).
37
78
 
38
79
  ## Quickstart
39
80
 
40
81
  1. Install the extension.
41
- 2. Run `browser-bridge install` (skill + optional MCP).
42
- 3. Run a quick CLI check:
82
+ 2. (Optional) Run `browser-bridge install` (skill + optional MCP).
83
+ 3. Run a quick CLI check (Core auto-starts by default):
43
84
 
44
85
  ```bash
45
86
  browser-bridge session create
@@ -53,7 +94,9 @@ Notes:
53
94
 
54
95
  - `inspect dom-snapshot` defaults to `--format ax`; `--max-nodes` is only supported for AX snapshots.
55
96
 
56
- ## Skills (Codex + Claude Code)
97
+ ## Skills (Agent Clients)
98
+
99
+ Browser Bridge skills work across many agent clients, including Codex and Claude Code.
57
100
 
58
101
  Easiest option (recommended):
59
102
 
@@ -128,34 +171,16 @@ claude mcp add --transport stdio browser-bridge \
128
171
  - CLI: `browser-bridge diagnostics doctor --session-id <id>`
129
172
  - Reports extension and debugger status alongside session state.
130
173
 
174
+ ## Recovery
175
+
176
+ If drive or inspect gets into a bad state, recovery is explicit:
177
+
178
+ - `browser-bridge session recover --session-id <id>`
179
+ - Then retry the failed operation once (tools report whether failures are `retryable`).
180
+
131
181
  ## Session TTL (Core Daemon)
132
182
 
133
183
  The Core daemon keeps sessions in memory. By default, it automatically cleans up idle sessions after 1 hour.
134
184
 
135
185
  - `BROWSER_BRIDGE_SESSION_TTL_MS`: Idle session TTL in milliseconds. Set to `0` to disable cleanup.
136
186
  - `BROWSER_BRIDGE_SESSION_CLEANUP_INTERVAL_MS`: Cleanup interval in milliseconds. Defaults to a small value relative to the TTL.
137
-
138
- ## Changelog
139
-
140
- See `CHANGELOG.md`.
141
-
142
- ## Releasing
143
-
144
- See `docs/releasing.md`.
145
-
146
- ## Security Model (v1)
147
-
148
- - Extension <-> Core WebSocket has no authentication; trust local machine only.
149
- - Do not expose the port or run the Core daemon on shared hosts.
150
-
151
- ## Development Notes
152
-
153
- If you are contributing locally, load the extension unpacked:
154
-
155
- 1. Open Chrome and navigate to `chrome://extensions`.
156
- 2. Enable **Developer mode**.
157
- 3. Click **Load unpacked** and select `packages/extension` (repo).
158
- 4. Confirm the extension's background service worker is running.
159
- 5. Start the Core daemon (or run `browser-bridge session create`) so the extension can connect to `127.0.0.1`.
160
-
161
- Additional manual test flows live in `docs/manual-test.md`.
package/dist/api.js CHANGED
@@ -2521,8 +2521,125 @@ var InspectService = class {
2521
2521
  async screenshot(input) {
2522
2522
  this.requireSession(input.sessionId);
2523
2523
  const selection = await this.resolveTab(input.targetHint);
2524
- await this.debuggerCommand(selection.tabId, "Page.enable", {});
2525
2524
  const format = input.format ?? "png";
2525
+ const writeArtifact = async (data2) => {
2526
+ try {
2527
+ const rootDir = await ensureArtifactRootDir(input.sessionId);
2528
+ const artifactId = (0, import_crypto3.randomUUID)();
2529
+ const extension = format === "jpeg" ? "jpg" : format;
2530
+ const filePath = import_node_path2.default.join(
2531
+ rootDir,
2532
+ `screenshot-${artifactId}.${extension}`
2533
+ );
2534
+ await (0, import_promises2.writeFile)(filePath, Buffer.from(data2, "base64"));
2535
+ const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
2536
+ const output = {
2537
+ artifact_id: artifactId,
2538
+ path: filePath,
2539
+ mime
2540
+ };
2541
+ this.markInspectConnected(input.sessionId);
2542
+ return output;
2543
+ } catch {
2544
+ const error = new InspectError(
2545
+ "ARTIFACT_IO_ERROR",
2546
+ "Failed to write screenshot file."
2547
+ );
2548
+ this.recordError(error);
2549
+ throw error;
2550
+ }
2551
+ };
2552
+ if (input.selector) {
2553
+ if (!this.extensionBridge?.request) {
2554
+ const error = new InspectError(
2555
+ "NOT_SUPPORTED",
2556
+ "Element screenshots require an extension that supports drive.screenshot."
2557
+ );
2558
+ this.recordError(error);
2559
+ throw error;
2560
+ }
2561
+ const response = await this.extensionBridge.request(
2562
+ "drive.screenshot",
2563
+ {
2564
+ tab_id: selection.tabId,
2565
+ mode: "element",
2566
+ selector: input.selector,
2567
+ format,
2568
+ ...typeof input.quality === "number" ? { quality: input.quality } : {}
2569
+ },
2570
+ 12e4
2571
+ );
2572
+ if (response.status === "error") {
2573
+ const error = new InspectError(
2574
+ response.error?.code ?? "INSPECT_UNAVAILABLE",
2575
+ response.error?.message ?? "Failed to capture element screenshot.",
2576
+ {
2577
+ retryable: response.error?.retryable ?? false,
2578
+ ...response.error?.details ? { details: response.error.details } : {}
2579
+ }
2580
+ );
2581
+ this.recordError(error);
2582
+ throw error;
2583
+ }
2584
+ const result2 = response.result;
2585
+ if (!result2?.data_base64 || typeof result2.data_base64 !== "string") {
2586
+ const error = new InspectError(
2587
+ "INSPECT_UNAVAILABLE",
2588
+ "Failed to capture element screenshot."
2589
+ );
2590
+ this.recordError(error);
2591
+ throw error;
2592
+ }
2593
+ return await writeArtifact(result2.data_base64);
2594
+ }
2595
+ if (input.target === "full" && this.extensionBridge?.request) {
2596
+ try {
2597
+ const response = await this.extensionBridge.request(
2598
+ "drive.screenshot",
2599
+ {
2600
+ tab_id: selection.tabId,
2601
+ mode: "full_page",
2602
+ format,
2603
+ ...typeof input.quality === "number" ? { quality: input.quality } : {}
2604
+ },
2605
+ 12e4
2606
+ );
2607
+ if (response.status === "error") {
2608
+ const error = new InspectError(
2609
+ response.error?.code ?? "INSPECT_UNAVAILABLE",
2610
+ response.error?.message ?? "Failed to capture full page screenshot.",
2611
+ {
2612
+ retryable: response.error?.retryable ?? false,
2613
+ ...response.error?.details ? { details: response.error.details } : {}
2614
+ }
2615
+ );
2616
+ this.recordError(error);
2617
+ throw error;
2618
+ }
2619
+ const result2 = response.result;
2620
+ if (!result2?.data_base64 || typeof result2.data_base64 !== "string") {
2621
+ const error = new InspectError(
2622
+ "INSPECT_UNAVAILABLE",
2623
+ "Failed to capture full page screenshot."
2624
+ );
2625
+ this.recordError(error);
2626
+ throw error;
2627
+ }
2628
+ return await writeArtifact(result2.data_base64);
2629
+ } catch (error) {
2630
+ if (error instanceof InspectError) {
2631
+ const code = String(error.code);
2632
+ if (![
2633
+ "NOT_SUPPORTED",
2634
+ "NOT_IMPLEMENTED",
2635
+ "INSPECT_UNAVAILABLE"
2636
+ ].includes(code)) {
2637
+ throw error;
2638
+ }
2639
+ }
2640
+ }
2641
+ }
2642
+ await this.debuggerCommand(selection.tabId, "Page.enable", {});
2526
2643
  let captureParams = {
2527
2644
  format,
2528
2645
  fromSurface: true
@@ -2567,31 +2684,7 @@ var InspectService = class {
2567
2684
  this.recordError(error);
2568
2685
  throw error;
2569
2686
  }
2570
- try {
2571
- const rootDir = await ensureArtifactRootDir(input.sessionId);
2572
- const artifactId = (0, import_crypto3.randomUUID)();
2573
- const extension = format === "jpeg" ? "jpg" : format;
2574
- const filePath = import_node_path2.default.join(
2575
- rootDir,
2576
- `screenshot-${artifactId}.${extension}`
2577
- );
2578
- await (0, import_promises2.writeFile)(filePath, Buffer.from(data, "base64"));
2579
- const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
2580
- const output = {
2581
- artifact_id: artifactId,
2582
- path: filePath,
2583
- mime
2584
- };
2585
- this.markInspectConnected(input.sessionId);
2586
- return output;
2587
- } catch {
2588
- const error = new InspectError(
2589
- "ARTIFACT_IO_ERROR",
2590
- "Failed to write screenshot file."
2591
- );
2592
- this.recordError(error);
2593
- throw error;
2594
- }
2687
+ return await writeArtifact(data);
2595
2688
  }
2596
2689
  ensureDebugger() {
2597
2690
  if (!this.debugger) {
@@ -3266,10 +3359,30 @@ var ArtifactsScreenshotInputSchema = import_zod2.z.object({
3266
3359
  session_id: import_zod2.z.string().min(1),
3267
3360
  target: import_zod2.z.enum(["viewport", "full"]).default("viewport"),
3268
3361
  fullPage: import_zod2.z.boolean().default(false),
3362
+ selector: import_zod2.z.string().min(1).optional(),
3269
3363
  format: import_zod2.z.enum(["png", "jpeg", "webp"]).default("png"),
3270
3364
  quality: import_zod2.z.number().min(0).max(100).optional()
3271
3365
  });
3272
3366
  var ArtifactsScreenshotOutputSchema = ArtifactInfoSchema;
3367
+ var HealthCheckInputSchema = import_zod2.z.object({});
3368
+ var HealthCheckOutputSchema = import_zod2.z.object({
3369
+ started_at: import_zod2.z.string().min(1),
3370
+ uptime_ms: import_zod2.z.number().finite().nonnegative(),
3371
+ memory: import_zod2.z.object({
3372
+ rss: import_zod2.z.number().finite().nonnegative(),
3373
+ heapTotal: import_zod2.z.number().finite().nonnegative(),
3374
+ heapUsed: import_zod2.z.number().finite().nonnegative(),
3375
+ external: import_zod2.z.number().finite().nonnegative(),
3376
+ arrayBuffers: import_zod2.z.number().finite().nonnegative().optional()
3377
+ }).passthrough(),
3378
+ sessions: import_zod2.z.object({
3379
+ active: import_zod2.z.number().finite().nonnegative()
3380
+ }).passthrough(),
3381
+ extension: import_zod2.z.object({
3382
+ connected: import_zod2.z.boolean(),
3383
+ last_seen_at: import_zod2.z.string().min(1).optional()
3384
+ }).passthrough()
3385
+ }).passthrough();
3273
3386
  var DiagnosticsDoctorInputSchema = import_zod2.z.object({
3274
3387
  session_id: import_zod2.z.string().min(1).optional()
3275
3388
  });
@@ -3325,6 +3438,7 @@ var registerArtifactsRoutes = (router, options = {}) => {
3325
3438
  const result = await inspect.screenshot({
3326
3439
  sessionId: input.session_id,
3327
3440
  target,
3441
+ selector: input.selector,
3328
3442
  format: input.format,
3329
3443
  quality: input.quality,
3330
3444
  targetHint: hint
@@ -3461,7 +3575,44 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
3461
3575
  };
3462
3576
 
3463
3577
  // packages/core/src/routes/diagnostics.ts
3578
+ var PROCESS_STARTED_AT = new Date(
3579
+ Date.now() - Math.floor(process.uptime() * 1e3)
3580
+ ).toISOString();
3464
3581
  var registerDiagnosticsRoutes = (router, options = {}) => {
3582
+ router.post("/health_check", (req, res) => {
3583
+ const body = req.body ?? {};
3584
+ if (!isRecord(body)) {
3585
+ sendError(res, 400, {
3586
+ code: "INVALID_ARGUMENT",
3587
+ message: "Request body must be an object.",
3588
+ retryable: false
3589
+ });
3590
+ return;
3591
+ }
3592
+ const parsed = HealthCheckInputSchema.safeParse(body);
3593
+ if (!parsed.success) {
3594
+ const issue = parsed.error.issues[0];
3595
+ sendError(res, 400, {
3596
+ code: "INVALID_ARGUMENT",
3597
+ message: issue?.message ?? "Invalid health check request.",
3598
+ retryable: false,
3599
+ details: issue?.path.length ? { field: issue.path.map((part) => String(part)).join(".") } : void 0
3600
+ });
3601
+ return;
3602
+ }
3603
+ const sessionsActive = options.registry ? options.registry.list().length : 0;
3604
+ const extensionStatus = options.extensionBridge?.getStatus();
3605
+ sendResult(res, {
3606
+ started_at: PROCESS_STARTED_AT,
3607
+ uptime_ms: Math.floor(process.uptime() * 1e3),
3608
+ memory: process.memoryUsage(),
3609
+ sessions: { active: sessionsActive },
3610
+ extension: {
3611
+ connected: extensionStatus?.connected ?? false,
3612
+ ...extensionStatus?.lastSeenAt ? { last_seen_at: extensionStatus.lastSeenAt } : {}
3613
+ }
3614
+ });
3615
+ });
3465
3616
  router.post("/diagnostics/doctor", (req, res) => {
3466
3617
  let sessionId;
3467
3618
  if (req.body !== void 0) {
@@ -4826,6 +4977,16 @@ var TOOL_DEFINITIONS = [
4826
4977
  corePath: "/artifacts/screenshot"
4827
4978
  }
4828
4979
  },
4980
+ {
4981
+ name: "health_check",
4982
+ config: {
4983
+ title: "Health Check",
4984
+ description: "Check server health including uptime, memory usage, active session count, and extension connection status.",
4985
+ inputSchema: HealthCheckInputSchema,
4986
+ outputSchema: envelope(HealthCheckOutputSchema),
4987
+ corePath: "/health_check"
4988
+ }
4989
+ },
4829
4990
  {
4830
4991
  name: "diagnostics.doctor",
4831
4992
  config: {
@@ -4870,6 +5031,8 @@ var registerBrowserBridgeTools = (server, client) => {
4870
5031
  // packages/mcp-adapter/src/server.ts
4871
5032
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
4872
5033
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
5034
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
5035
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
4873
5036
  var DEFAULT_SERVER_NAME = "browser-bridge";
4874
5037
  var DEFAULT_SERVER_VERSION = "0.0.0";
4875
5038
  var createMcpServer = (options = {}) => {