@browserbridge/bbx 1.4.0 → 1.5.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/README.md CHANGED
@@ -107,7 +107,7 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
107
107
 
108
108
  1. Install [Browser Bridge from the Chrome Web Store](https://chromewebstore.google.com/detail/browser-bridge/jjjkmmcdkpcgamlopogicbnnhdgebhie) in Chrome or another Chromium-based browser
109
109
  2. `npm install -g @browserbridge/bbx` - installs the CLI and native host
110
- 3. Run `bbx install`, or target a specific browser with `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
110
+ 3. Run `bbx install` (Chromium on Linux, Chrome elsewhere), or target a specific browser with `bbx install --browser chrome`, `bbx install --browser edge`, `bbx install --browser brave`, `bbx install --browser chromium`, or `bbx install --browser arc`
111
111
  4. In the extension side panel, install MCP or CLI (skill) for your agent of choice
112
112
  5. Enable Browser Bridge for the browser window you want to inspect/control with the AI agent
113
113
  6. Ask your agent to use Browser Bridge via MCP (`BB MCP` or `Browser Bridge MCP`), or invoke the installed Browser Bridge skill in CLI mode (`/browser-bridge`, `browser-bridge`, or the client-specific skill trigger)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -687,7 +687,7 @@ async function main() {
687
687
  process.exitCode = 1;
688
688
  } catch (error) {
689
689
  const message = error instanceof Error ? error.message : String(error);
690
- const raw = error instanceof Error && 'code' in error ? /** @type {any} */ (error).code : '';
690
+ const raw = getErrorCode(error);
691
691
  let code = 'ERROR';
692
692
  if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
693
693
  code = 'DAEMON_OFFLINE';
@@ -709,6 +709,18 @@ async function main() {
709
709
  }
710
710
  }
711
711
 
712
+ /**
713
+ * @param {unknown} error
714
+ * @returns {string}
715
+ */
716
+ function getErrorCode(error) {
717
+ if (!(error instanceof Error) || !('code' in error)) {
718
+ return '';
719
+ }
720
+ const code = /** @type {{ code?: unknown }} */ (error).code;
721
+ return typeof code === 'string' ? code : '';
722
+ }
723
+
712
724
  /**
713
725
  * Allow tests to shrink request timeouts without changing the shared default.
714
726
  *
@@ -237,7 +237,7 @@ export const CLI_HELP_SECTIONS = Object.freeze([
237
237
  {
238
238
  title: 'Setup',
239
239
  lines: [
240
- 'bbx install [extension-id] [--browser chrome|edge|brave|chromium|arc] [--all] Install native messaging manifest',
240
+ 'bbx install [extension-id] [--browser chrome|edge|brave|chromium|arc] [--all] Install native messaging manifest (Linux defaults to Chromium)',
241
241
  'bbx uninstall Remove native host manifests, Browser Bridge runtime files, and managed MCP/skill installs',
242
242
  'bbx install-skill [targets|all] [--global] [--project <path>] Install/update the managed Browser Bridge CLI skill',
243
243
  'bbx install-mcp [client|all] [--local] Write MCP config for codex|claude|cursor|copilot|opencode|antigravity|windsurf|agents',
@@ -94,29 +94,19 @@ export function getMcpConfigShape(clientName) {
94
94
  * }}
95
95
  */
96
96
  function createBaseServerConfig(clientName) {
97
- const windowsCommand =
98
- process.platform === 'win32'
99
- ? {
100
- command: process.execPath,
101
- args: [mcpServerBinPath],
102
- env: {},
103
- }
104
- : {
105
- command: 'bbx',
106
- args: ['mcp', 'serve'],
107
- env: {},
108
- };
97
+ const serverConfig = {
98
+ command: process.execPath,
99
+ args: [mcpServerBinPath],
100
+ env: {},
101
+ };
109
102
 
110
103
  if (clientName === 'opencode') {
111
104
  return {
112
105
  type: 'local',
113
- command:
114
- process.platform === 'win32'
115
- ? [process.execPath, mcpServerBinPath]
116
- : ['bbx', 'mcp', 'serve'],
106
+ command: [process.execPath, mcpServerBinPath],
117
107
  };
118
108
  }
119
- return windowsCommand;
109
+ return serverConfig;
120
110
  }
121
111
 
122
112
  /** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
@@ -142,13 +132,11 @@ const MCP_CONFIG_SHAPES = {
142
132
  */
143
133
  export function buildMcpConfig(clientName) {
144
134
  if (clientName === 'codex') {
145
- const command = process.platform === 'win32' ? process.execPath : 'bbx';
146
- const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
147
135
  return {
148
136
  mcp_servers: {
149
137
  [BROWSER_BRIDGE_SERVER_NAME]: {
150
- command,
151
- args,
138
+ command: process.execPath,
139
+ args: [mcpServerBinPath],
152
140
  },
153
141
  },
154
142
  };
@@ -277,12 +265,10 @@ export async function getMcpConfigPaths(clientName, options) {
277
265
  * @returns {string}
278
266
  */
279
267
  function formatCodexServerBlock() {
280
- const command = process.platform === 'win32' ? process.execPath : 'bbx';
281
- const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
282
268
  return [
283
269
  `[mcp_servers."${BROWSER_BRIDGE_SERVER_NAME}"]`,
284
- `command = ${JSON.stringify(command)}`,
285
- `args = ${JSON.stringify(args)}`,
270
+ `command = ${JSON.stringify(process.execPath)}`,
271
+ `args = ${JSON.stringify([mcpServerBinPath])}`,
286
272
  '',
287
273
  ].join('\n');
288
274
  }
@@ -113,7 +113,7 @@ export async function withBridgeClient(callback) {
113
113
  }
114
114
 
115
115
  /**
116
- * @param {SupportedBrowser} [browser='chrome']
116
+ * @param {SupportedBrowser} [browser]
117
117
  * @returns {string}
118
118
  */
119
119
  export function getManifestPath(browser) {
@@ -121,7 +121,7 @@ export function getManifestPath(browser) {
121
121
  }
122
122
 
123
123
  /**
124
- * @param {SupportedBrowser} [browser='chrome']
124
+ * @param {SupportedBrowser} [browser]
125
125
  * @returns {Promise<{allowed_origins?: string[]} | null>}
126
126
  */
127
127
  export async function loadInstalledManifest(browser) {
@@ -160,10 +160,10 @@ export async function checkBrowserManifests() {
160
160
  export async function getDoctorReport(options = {}) {
161
161
  const manifest = await (options.loadManifest || loadInstalledManifest)();
162
162
  const allowedOrigins = Array.isArray(manifest?.allowed_origins) ? manifest.allowed_origins : [];
163
- const manifestInstalled = Boolean(manifest);
164
163
  const defaultExtensionId = options.defaultExtensionIdInfo || resolveDefaultExtensionId();
165
164
 
166
- const browserManifests = await checkBrowserManifests();
165
+ const browserManifests = await (options.checkBrowserManifests || checkBrowserManifests)();
166
+ const manifestInstalled = Boolean(manifest) || browserManifests.some((b) => b.installed);
167
167
 
168
168
  /** @type {DoctorReport} */
169
169
  const report = {
@@ -214,8 +214,6 @@ export async function getDoctorReport(options = {}) {
214
214
  report.extensionConnected = false;
215
215
  }
216
216
 
217
- const browsersWithoutManifest = browserManifests.filter((b) => !b.installed);
218
-
219
217
  if (!report.manifestInstalled) {
220
218
  report.issues.push('native_host_manifest_missing');
221
219
  report.nextSteps.push(
@@ -223,12 +221,6 @@ export async function getDoctorReport(options = {}) {
223
221
  ? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
224
222
  : 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
225
223
  );
226
- } else if (browsersWithoutManifest.length > 0) {
227
- report.issues.push('native_host_manifest_partial');
228
- const missing = browsersWithoutManifest.map((b) => b.browser).join(', ');
229
- report.nextSteps.push(
230
- `Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`
231
- );
232
224
  }
233
225
  if (!report.daemonReachable) {
234
226
  report.issues.push('daemon_offline');
@@ -67,7 +67,7 @@ export type ClientMessage =
67
67
  };
68
68
 
69
69
  export interface PendingRequest {
70
- resolve: (value: any) => void;
70
+ resolve: (value: unknown) => void;
71
71
  reject: (error: Error) => void;
72
72
  timeoutId: NodeJS.Timeout;
73
73
  }
@@ -118,6 +118,7 @@ export interface DoctorReport {
118
118
 
119
119
  export interface DoctorReportOptions {
120
120
  loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
121
+ checkBrowserManifests?: () => Promise<BrowserManifestStatus[]>;
121
122
  manifestPath?: string;
122
123
  defaultExtensionIdInfo?: { extensionId: string | null; source: string };
123
124
  bridgeClientRunner?: <T>(
@@ -87,6 +87,12 @@ export async function handlePageTool(args) {
87
87
  }
88
88
  const entry = PAGE_ACTIONS[normalizedArgs.action];
89
89
  if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
90
+ if (
91
+ normalizedArgs.action === 'evaluate' &&
92
+ (typeof normalizedArgs.expression !== 'string' || !normalizedArgs.expression.trim())
93
+ ) {
94
+ return summarizeToolError('expression is required for page evaluate.');
95
+ }
90
96
  return callBridgeTool(entry.method, entry.params(normalizedArgs), {
91
97
  tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
92
98
  tokenBudget: getToolTokenBudget(normalizedArgs),
@@ -9,7 +9,7 @@ import {
9
9
  parseExtensionId,
10
10
  uninstallNativeManifest,
11
11
  } from '../src/install-manifest.js';
12
- import { SUPPORTED_BROWSERS } from '../src/config.js';
12
+ import { getDefaultBrowser, SUPPORTED_BROWSERS } from '../src/config.js';
13
13
 
14
14
  /** @typedef {import('../src/config.js').SupportedBrowser} SupportedBrowser */
15
15
 
@@ -61,7 +61,7 @@ const targets = installAll
61
61
  ? [...SUPPORTED_BROWSERS]
62
62
  : browsers.length > 0
63
63
  ? browsers
64
- : [/** @type {SupportedBrowser} */ ('chrome')];
64
+ : [getDefaultBrowser()];
65
65
 
66
66
  for (const [index, target] of targets.entries()) {
67
67
  if (uninstall) {
@@ -1,5 +1,6 @@
1
1
  // @ts-check
2
2
 
3
+ import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
5
6
 
@@ -189,14 +190,37 @@ export function getLauncherFilename() {
189
190
  */
190
191
  export const SUPPORTED_BROWSERS = ['chrome', 'edge', 'brave', 'chromium', 'arc'];
191
192
 
193
+ /**
194
+ * @returns {SupportedBrowser}
195
+ */
196
+ export function getDefaultBrowser() {
197
+ return os.platform() === 'linux' ? 'chromium' : 'chrome';
198
+ }
199
+
200
+ /**
201
+ * @param {NodeJS.ProcessEnv} env
202
+ * @param {string} home
203
+ * @returns {string}
204
+ */
205
+ function getLinuxConfigHome(env, home) {
206
+ if (env.CHROME_CONFIG_HOME) {
207
+ return env.CHROME_CONFIG_HOME;
208
+ }
209
+ if (env.XDG_CONFIG_HOME) {
210
+ return env.XDG_CONFIG_HOME;
211
+ }
212
+ return path.join(home, '.config');
213
+ }
214
+
192
215
  /**
193
216
  * Return the native messaging host manifest install directory for the given
194
217
  * browser on the current platform.
195
218
  *
196
- * @param {SupportedBrowser} [browser='chrome']
219
+ * @param {SupportedBrowser} [browser=getDefaultBrowser()]
220
+ * @param {NodeJS.ProcessEnv} [env=process.env]
197
221
  * @returns {string}
198
222
  */
199
- export function getManifestInstallDir(browser = 'chrome') {
223
+ export function getManifestInstallDir(browser = getDefaultBrowser(), env = process.env) {
200
224
  const platform = os.platform();
201
225
  const home = os.homedir();
202
226
 
@@ -213,7 +237,7 @@ export function getManifestInstallDir(browser = 'chrome') {
213
237
  }
214
238
 
215
239
  if (platform === 'win32') {
216
- const winBase = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
240
+ const winBase = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
217
241
  const winPaths = {
218
242
  chrome: path.join(winBase, 'Google', 'Chrome', 'User Data', 'NativeMessagingHosts'),
219
243
  edge: path.join(winBase, 'Microsoft', 'Edge', 'User Data', 'NativeMessagingHosts'),
@@ -231,12 +255,18 @@ export function getManifestInstallDir(browser = 'chrome') {
231
255
  }
232
256
 
233
257
  // Linux / others
258
+ const linuxConfigHome = getLinuxConfigHome(env, home);
259
+ const chromiumSnapProfile = path.join(home, 'snap', 'chromium', 'common', 'chromium');
260
+ const useChromiumSnapProfile =
261
+ !env.CHROME_CONFIG_HOME && !env.XDG_CONFIG_HOME && fs.existsSync(chromiumSnapProfile);
234
262
  const linuxPaths = {
235
- chrome: path.join(home, '.config', 'google-chrome', 'NativeMessagingHosts'),
236
- edge: path.join(home, '.config', 'microsoft-edge', 'NativeMessagingHosts'),
237
- brave: path.join(home, '.config', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts'),
238
- chromium: path.join(home, '.config', 'chromium', 'NativeMessagingHosts'),
239
- arc: path.join(home, '.config', 'Arc', 'User Data', 'NativeMessagingHosts'),
263
+ chrome: path.join(linuxConfigHome, 'google-chrome', 'NativeMessagingHosts'),
264
+ edge: path.join(linuxConfigHome, 'microsoft-edge', 'NativeMessagingHosts'),
265
+ brave: path.join(linuxConfigHome, 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts'),
266
+ chromium: useChromiumSnapProfile
267
+ ? path.join(chromiumSnapProfile, 'NativeMessagingHosts')
268
+ : path.join(linuxConfigHome, 'chromium', 'NativeMessagingHosts'),
269
+ arc: path.join(linuxConfigHome, 'Arc', 'User Data', 'NativeMessagingHosts'),
240
270
  };
241
271
  return linuxPaths[browser] ?? linuxPaths.chrome;
242
272
  }
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { pingExistingDaemon } from './daemon.js';
10
10
  import {
11
+ applyWindowsTcpTransportDefaults,
11
12
  createSocketBridgeTransport,
12
13
  formatBridgeTransport,
13
14
  getBridgeTransport,
@@ -124,7 +125,7 @@ export async function clearDaemonPidFile(options = {}) {
124
125
  */
125
126
  export async function stopBridgeDaemon(options = {}) {
126
127
  const {
127
- transport = getBridgeTransport(),
128
+ transport = undefined,
128
129
  socketPath = undefined,
129
130
  pidPath = getDaemonPidPath(),
130
131
  timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
@@ -136,7 +137,7 @@ export async function stopBridgeDaemon(options = {}) {
136
137
  rmFn = fs.promises.rm,
137
138
  sleepFn = sleep,
138
139
  } = options;
139
- const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
140
+ const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
140
141
  const resolvedSocketPath =
141
142
  resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
142
143
 
@@ -238,7 +239,7 @@ export async function restartBridgeDaemonIfRunning(options = {}) {
238
239
  */
239
240
  async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
240
241
  const {
241
- transport = getBridgeTransport(),
242
+ transport = undefined,
242
243
  socketPath = undefined,
243
244
  pidPath = getDaemonPidPath(),
244
245
  timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
@@ -248,7 +249,7 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
248
249
  sleepFn = sleep,
249
250
  spawnDaemonFn = spawnBridgeDaemonProcess,
250
251
  } = options;
251
- const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
252
+ const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
252
253
 
253
254
  spawnDaemonFn();
254
255
 
@@ -271,6 +272,27 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
271
272
  };
272
273
  }
273
274
 
275
+ /**
276
+ * Mirror the daemon entrypoint transport defaults so restart polling targets the
277
+ * same endpoint the spawned process listens on.
278
+ *
279
+ * @param {{ transport?: BridgeTransport, socketPath?: string }} options
280
+ * @returns {BridgeTransport}
281
+ */
282
+ function resolveDaemonTransport(options) {
283
+ const { transport, socketPath } = options;
284
+ if (socketPath) {
285
+ return createSocketBridgeTransport(socketPath);
286
+ }
287
+ if (transport) {
288
+ return transport;
289
+ }
290
+
291
+ const env = { ...process.env };
292
+ applyWindowsTcpTransportDefaults(env);
293
+ return getBridgeTransport(env);
294
+ }
295
+
274
296
  /**
275
297
  * @param {BridgeTransport} transport
276
298
  * @returns {Promise<number | null>}
@@ -53,7 +53,7 @@ const DAEMON_VERSION = loadDaemonVersion();
53
53
  /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
54
54
  /** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
55
55
  /** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number, __authenticated?: boolean }} ClientSocket */
56
- /** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
56
+ /** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
57
57
  /**
58
58
  * @typedef {{
59
59
  * installAgentFiles: typeof import('../../agent-client/src/install.js').installAgentFiles,
@@ -762,6 +762,7 @@ export class BridgeDaemon {
762
762
  this.trackPendingRequest(request.id, {
763
763
  socket,
764
764
  method: request.method,
765
+ protocolVersion: request.meta?.protocol_version,
765
766
  source: typeof request.meta?.source === 'string' ? request.meta.source : '',
766
767
  targets: new Set(targets),
767
768
  timeoutId: setTimeout(() => {
@@ -925,6 +926,7 @@ export class BridgeDaemon {
925
926
  socketPath: this.socketPath,
926
927
  transport: formatBridgeTransport(this.transport),
927
928
  connectedExtensions: this.getConnectedExtensionsSnapshot(),
929
+ ...getVersionNegotiationPayload(pending.protocolVersion),
928
930
  .../** @type {Record<string, unknown>} */ (responseMessage.result),
929
931
  daemonVersion: DAEMON_VERSION,
930
932
  daemon_supported_versions: SUPPORTED_VERSIONS,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { once } from 'node:events';
4
4
 
5
- import { MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
5
+ import { MAX_JSON_LINE_BYTES, MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
6
6
 
7
7
  /**
8
8
  * @param {NodeJS.WritableStream} stream
@@ -193,7 +193,12 @@ export function createNativeMessageReader(stream, onMessage, onProtocolError) {
193
193
  * @returns {Promise<void>}
194
194
  */
195
195
  export async function writeJsonLine(socket, message) {
196
- if (!socket.write(`${JSON.stringify(message)}\n`)) {
196
+ const line = `${JSON.stringify(message)}\n`;
197
+ const byteLength = Buffer.byteLength(line.slice(0, -1), 'utf8');
198
+ if (byteLength > MAX_JSON_LINE_BYTES) {
199
+ throw new Error(`JSON line exceeds ${MAX_JSON_LINE_BYTES} bytes: ${byteLength}`);
200
+ }
201
+ if (!socket.write(line)) {
197
202
  await once(socket, 'drain');
198
203
  }
199
204
  }
@@ -259,13 +259,13 @@ export async function installNativeManifest(options) {
259
259
  extensionIdArg,
260
260
  browser,
261
261
  nodePath = process.execPath,
262
- installDir = getManifestInstallDir(browser),
263
- bridgeDir = getBridgeDir(),
262
+ env = process.env,
263
+ installDir = getManifestInstallDir(browser, env),
264
+ bridgeDir = getBridgeDir(env),
264
265
  stdout = process.stdout,
265
266
  stderr = process.stderr,
266
267
  preserveCustomExtensionId = false,
267
268
  writeRegistryValue: writeRegistryValueFn = writeRegistryValue,
268
- env = process.env,
269
269
  } = options;
270
270
 
271
271
  const parsedExtensionId = parseExtensionId(extensionIdArg);
@@ -97,7 +97,9 @@ export const SUPPORTED_VERSIONS = Object.freeze(['1.0']);
97
97
  * @returns {number}
98
98
  */
99
99
  function clampInt(value, min, max, fallback) {
100
- return Math.min(Math.max(Number(value) || fallback, min), max);
100
+ const numeric = Number(value);
101
+ const integer = Number.isFinite(numeric) && numeric !== 0 ? Math.trunc(numeric) : fallback;
102
+ return Math.min(Math.max(integer, min), max);
101
103
  }
102
104
 
103
105
  /** @type {ReadonlyArray<BridgeMethod>} */