@browserbridge/bbx 1.4.0 → 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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
  *
@@ -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
  }
@@ -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),
@@ -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
  }
@@ -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>} */