@browserbridge/bbx 1.7.2 → 1.7.4

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.7.2",
3
+ "version": "1.7.4",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -61,8 +61,9 @@
61
61
  "postinstall": "node packages/native-host/bin/postinstall.js",
62
62
  "package:extension": "node scripts/package-extension.mjs",
63
63
  "check:extension-zip": "node scripts/check-extension-zip.mjs",
64
- "release:check": "npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm run check:extension-zip && npm pack --dry-run",
65
- "prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run coverage:check",
64
+ "check:release-version": "node scripts/check-release-version.mjs",
65
+ "release:check": "npm run check:release-version && npm run lint && npm run typecheck && npm test && npm run coverage:check && npm run package:extension && npm run check:extension-zip && npm pack --dry-run",
66
+ "prepublishOnly": "npm run check:release-version && npm run lint && npm run typecheck && npm test && npm run coverage:check",
66
67
  "status": "node packages/agent-client/src/cli.js status",
67
68
  "daemon": "node packages/native-host/bin/bridge-daemon.js",
68
69
  "install-manifest": "node packages/native-host/bin/install-manifest.js"
@@ -72,13 +73,13 @@
72
73
  "zod": "^4.4.3"
73
74
  },
74
75
  "devDependencies": {
75
- "@types/chrome": "^0.1.43",
76
- "@types/node": "^25.9.2",
76
+ "@types/chrome": "^0.2.2",
77
+ "@types/node": "^26.1.1",
77
78
  "c8": "^11.0.0",
78
- "linkedom": "^0.18.12",
79
- "oxfmt": "^0.54.0",
80
- "oxlint": "^1.69.0",
81
- "typescript": "^6.0.3"
79
+ "linkedom": "^0.18.13",
80
+ "oxfmt": "^0.58.0",
81
+ "oxlint": "^1.73.0",
82
+ "typescript": "^7.0.2"
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=18"
@@ -512,6 +512,9 @@ async function main() {
512
512
 
513
513
  if (command === 'doctor') {
514
514
  const report = await getDoctorReport();
515
+ if (report.issues.length > 0) {
516
+ process.exitCode = 1;
517
+ }
515
518
  printJson({
516
519
  ok: report.issues.length === 0,
517
520
  summary:
@@ -963,9 +966,9 @@ async function handleProxyCommand(args) {
963
966
  const exampleHost = getProxyExampleHost(bindHost);
964
967
  const tokenNote =
965
968
  tokenSource === 'existing'
966
- ? ' (unchanged — already-configured clients keep working; pass --rotate-token to generate a new secret)'
969
+ ? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
967
970
  : tokenSource === 'generated' && existing
968
- ? ' (rotated — update every configured client with `bbx remote add`)'
971
+ ? ' (rotated - update every configured client with `bbx remote add`)'
969
972
  : '';
970
973
  const lines = [
971
974
  `Browser Bridge proxy enabled on ${bindHost}:${port}.`,
@@ -1298,6 +1301,9 @@ async function ensureClientConnection() {
1298
1301
  * @returns {Promise<void>}
1299
1302
  */
1300
1303
  async function printSummary(response, method) {
1304
+ if (!response.ok) {
1305
+ process.exitCode = 1;
1306
+ }
1301
1307
  printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
1302
1308
  }
1303
1309
 
@@ -75,7 +75,7 @@ function getLegacyCopilotVsCodeConfigPath() {
75
75
 
76
76
  /**
77
77
  * @param {McpClientName} clientName
78
- * @returns {{ key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }}
78
+ * @returns {{ key: string, includeType: boolean, keepEmptyBlock?: boolean }}
79
79
  */
80
80
  export function getMcpConfigShape(clientName) {
81
81
  return MCP_CONFIG_SHAPES[clientName] ?? { key: 'mcpServers', includeType: false };
@@ -108,7 +108,7 @@ function createBaseServerConfig(clientName) {
108
108
  return serverConfig;
109
109
  }
110
110
 
111
- /** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
111
+ /** @type {Record<McpClientName, { key: string, includeType: boolean, keepEmptyBlock?: boolean }>} */
112
112
  const MCP_CONFIG_SHAPES = {
113
113
  antigravity: { key: 'mcpServers', includeType: false },
114
114
  agents: { key: 'mcpServers', includeType: true },
@@ -116,7 +116,6 @@ const MCP_CONFIG_SHAPES = {
116
116
  copilot: {
117
117
  key: 'mcpServers',
118
118
  includeType: true,
119
- legacyKeys: ['servers'],
120
119
  keepEmptyBlock: true,
121
120
  },
122
121
  cursor: { key: 'mcpServers', includeType: false },
@@ -125,6 +124,24 @@ const MCP_CONFIG_SHAPES = {
125
124
  opencode: { key: 'mcp', includeType: false },
126
125
  };
127
126
 
127
+ /**
128
+ * Copilot CLI and VS Code use different top-level keys. Select the schema from
129
+ * the destination file instead of migrating one valid schema into the other.
130
+ *
131
+ * @param {McpClientName} clientName
132
+ * @param {string} configPath
133
+ * @returns {{ key: string, includeType: boolean, keepEmptyBlock?: boolean }}
134
+ */
135
+ export function getMcpConfigShapeForPath(clientName, configPath) {
136
+ if (
137
+ clientName === 'copilot' &&
138
+ path.resolve(configPath) !== path.resolve(getCopilotUserConfigPath())
139
+ ) {
140
+ return { key: 'servers', includeType: true, keepEmptyBlock: true };
141
+ }
142
+ return getMcpConfigShape(clientName);
143
+ }
144
+
128
145
  /**
129
146
  * @param {McpClientName} clientName
130
147
  * @returns {Record<string, unknown>}
@@ -376,37 +393,6 @@ function removeCodexServerBlock(raw) {
376
393
  return trimmed ? `${trimmed}\n` : '';
377
394
  }
378
395
 
379
- /**
380
- * @param {McpClientName} clientName
381
- * @returns {string[]}
382
- */
383
- function getJsonMcpBlockKeys(clientName) {
384
- const shape = getMcpConfigShape(clientName);
385
- return [...(shape.legacyKeys ?? []), shape.key];
386
- }
387
-
388
- /**
389
- * Merge MCP entries from the current config key plus any legacy aliases.
390
- *
391
- * @param {Record<string, unknown>} config
392
- * @param {McpClientName} clientName
393
- * @returns {Record<string, unknown>}
394
- */
395
- function getMergedJsonMcpBlock(config, clientName) {
396
- /** @type {Record<string, unknown>} */
397
- const merged = {};
398
-
399
- for (const key of getJsonMcpBlockKeys(clientName)) {
400
- const block = config[key];
401
- if (!block || typeof block !== 'object' || Array.isArray(block)) {
402
- continue;
403
- }
404
- Object.assign(merged, /** @type {Record<string, unknown>} */ (block));
405
- }
406
-
407
- return merged;
408
- }
409
-
410
396
  /**
411
397
  * Merge the browser-bridge entry into an existing client config file, or
412
398
  * create it if it does not exist. Existing unrelated entries are preserved.
@@ -445,7 +431,7 @@ export async function findConfiguredMcpClients(options) {
445
431
  for (const configPath of configPaths) {
446
432
  try {
447
433
  const raw = await fs.promises.readFile(configPath, 'utf8');
448
- if (parseInstalledMcpConfig(clientName, raw).configured) {
434
+ if (parseInstalledMcpConfig(clientName, raw, configPath).configured) {
449
435
  configured = true;
450
436
  break;
451
437
  }
@@ -498,8 +484,6 @@ export async function removeMcpConfig(clientName, options) {
498
484
  * @returns {Promise<void>}
499
485
  */
500
486
  async function installJsonMcpConfig(clientName, configPath, stdout) {
501
- const newEntry = buildMcpConfig(clientName);
502
-
503
487
  /** @type {Record<string, unknown>} */
504
488
  let existing = {};
505
489
  try {
@@ -517,21 +501,23 @@ async function installJsonMcpConfig(clientName, configPath, stdout) {
517
501
  // File missing - start fresh.
518
502
  }
519
503
 
520
- const shape = getMcpConfigShape(clientName);
504
+ const shape = getMcpConfigShapeForPath(clientName, configPath);
521
505
  const topKey = shape.key;
522
- const entryBlock = /** @type {Record<string, unknown>} */ (newEntry[topKey]);
523
- const existingBlock = getMergedJsonMcpBlock(existing, clientName);
506
+ const serverConfig = createBaseServerConfig(clientName);
507
+ const entry = shape.includeType ? { type: 'stdio', ...serverConfig } : serverConfig;
508
+ const currentBlock = existing[topKey];
509
+ const existingBlock =
510
+ currentBlock && typeof currentBlock === 'object' && !Array.isArray(currentBlock)
511
+ ? /** @type {Record<string, unknown>} */ (currentBlock)
512
+ : {};
524
513
 
525
514
  const merged = {
526
515
  ...existing,
527
516
  [topKey]: {
528
517
  ...existingBlock,
529
- ...entryBlock,
518
+ [BROWSER_BRIDGE_SERVER_NAME]: entry,
530
519
  },
531
520
  };
532
- for (const key of shape.legacyKeys ?? []) {
533
- delete merged[key];
534
- }
535
521
 
536
522
  await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
537
523
  await fs.promises.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
@@ -578,9 +564,13 @@ async function removeJsonMcpConfig(clientName, configPath, stdout) {
578
564
  return false;
579
565
  }
580
566
 
581
- const shape = getMcpConfigShape(clientName);
567
+ const shape = getMcpConfigShapeForPath(clientName, configPath);
582
568
  const topKey = shape.key;
583
- const block = getMergedJsonMcpBlock(existing, clientName);
569
+ const currentBlock = existing[topKey];
570
+ const block =
571
+ currentBlock && typeof currentBlock === 'object' && !Array.isArray(currentBlock)
572
+ ? /** @type {Record<string, unknown>} */ (currentBlock)
573
+ : {};
584
574
  if (!Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME)) {
585
575
  return false;
586
576
  }
@@ -589,9 +579,7 @@ async function removeJsonMcpConfig(clientName, configPath, stdout) {
589
579
  delete updatedBlock[BROWSER_BRIDGE_SERVER_NAME];
590
580
 
591
581
  const merged = { ...existing };
592
- for (const key of getJsonMcpBlockKeys(clientName)) {
593
- delete merged[key];
594
- }
582
+ delete merged[topKey];
595
583
  if (Object.keys(updatedBlock).length > 0 || shape.keepEmptyBlock) {
596
584
  merged[topKey] = updatedBlock;
597
585
  }
@@ -629,23 +617,35 @@ async function removeCodexMcpConfig(configPath, stdout) {
629
617
  /**
630
618
  * @param {McpClientName} clientName
631
619
  * @param {string} raw
620
+ * @param {string} [configPath]
632
621
  * @returns {{ configured: boolean }}
633
622
  */
634
- export function parseInstalledMcpConfig(clientName, raw) {
623
+ export function parseInstalledMcpConfig(clientName, raw, configPath) {
635
624
  if (clientName === 'codex') {
636
625
  return { configured: hasCodexServerBlock(raw) };
637
626
  }
638
627
 
639
628
  try {
640
629
  const parsed = JSON.parse(raw);
641
- const block =
630
+ const config =
642
631
  parsed && typeof parsed === 'object' && !Array.isArray(parsed)
643
- ? getMergedJsonMcpBlock(/** @type {Record<string, unknown>} */ (parsed), clientName)
632
+ ? /** @type {Record<string, unknown>} */ (parsed)
644
633
  : {};
634
+ const keys = configPath
635
+ ? [getMcpConfigShapeForPath(clientName, configPath).key]
636
+ : clientName === 'copilot'
637
+ ? ['mcpServers', 'servers']
638
+ : [getMcpConfigShape(clientName).key];
645
639
  return {
646
- configured: Boolean(
647
- block && typeof block === 'object' && Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME)
648
- ),
640
+ configured: keys.some((key) => {
641
+ const block = config[key];
642
+ return Boolean(
643
+ block &&
644
+ typeof block === 'object' &&
645
+ !Array.isArray(block) &&
646
+ Object.hasOwn(block, BROWSER_BRIDGE_SERVER_NAME)
647
+ );
648
+ }),
649
649
  };
650
650
  } catch {
651
651
  return { configured: false };
@@ -18,6 +18,12 @@ import {
18
18
  summarizeDaemonRestarts,
19
19
  } from '../../native-host/src/daemon-process.js';
20
20
  import { resolveDefaultExtensionId } from '../../native-host/src/install-manifest.js';
21
+ import {
22
+ CLIENT_REQUEST_TIMEOUT_MARGIN_MS,
23
+ DEFAULT_CLIENT_REQUEST_TIMEOUT_MS,
24
+ getBridgeOperationTimeoutMs,
25
+ MAX_CLIENT_REQUEST_TIMEOUT_MS,
26
+ } from '../../protocol/src/index.js';
21
27
  import { methodNeedsTab } from './cli-helpers.js';
22
28
  import { BridgeClient } from './client.js';
23
29
 
@@ -53,11 +59,24 @@ export async function ensureClientConnected(client) {
53
59
  */
54
60
  export async function requestBridge(client, method, params = {}, options = {}) {
55
61
  await ensureClientConnected(client);
62
+ const operationTimeoutMs = getBridgeOperationTimeoutMs(method, params);
63
+ const clientDefaultTimeoutMs =
64
+ typeof client.defaultTimeoutMs === 'number'
65
+ ? client.defaultTimeoutMs
66
+ : DEFAULT_CLIENT_REQUEST_TIMEOUT_MS;
67
+ const timeoutMs =
68
+ operationTimeoutMs === null
69
+ ? undefined
70
+ : Math.min(
71
+ MAX_CLIENT_REQUEST_TIMEOUT_MS,
72
+ Math.max(clientDefaultTimeoutMs, operationTimeoutMs + CLIENT_REQUEST_TIMEOUT_MARGIN_MS)
73
+ );
56
74
  return client.request({
57
75
  method,
58
76
  params,
59
77
  tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
60
78
  meta: withRequestMeta(options.source, options.tokenBudget),
79
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
61
80
  });
62
81
  }
63
82
 
@@ -331,7 +350,7 @@ export async function checkNativeHostManifestHealth(browserManifests) {
331
350
  */
332
351
  export async function checkUnwritableBridgePaths() {
333
352
  // The root-owned-files scenario comes from sudo installs, which do not exist
334
- // on Windows — and fs.access(W_OK) there only reflects the read-only file
353
+ // on Windows - and fs.access(W_OK) there only reflects the read-only file
335
354
  // attribute, so it would produce false positives.
336
355
  if (os.platform() === 'win32') {
337
356
  return [];
@@ -10,9 +10,9 @@ export const MCP_SERVER_INSTRUCTIONS = [
10
10
  'In permission-ask hosts, use browser_call as the default Browser Bridge MCP tool so the user can approve one BBX tool instead of separate browser_status, browser_page, browser_dom, browser_input, and other tools.',
11
11
  'Prefer Browser Bridge MCP tools over shelling out to bbx. Use bbx only for explicit CLI setup, doctor, logs, or raw debugging requests.',
12
12
  'Start with browser_call method health.ping. If window access is disabled, call browser_call method access.request once, ask the user to click Enable in the Browser Bridge popup or side panel, then retry once.',
13
- 'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, layout.get_box_model, or batch. Keep budgets quick or normal before widening.',
13
+ 'Use structured reads first through browser_call: page.get_state, dom.query, page.get_text, styles.get_computed, or layout.get_box_model. Use browser_batch only for parallel reads; run mutations sequentially and keep limits tight before widening.',
14
14
  'Reuse elementRef values returned by DOM tools. Use attribute allowlists for focused DOM reads.',
15
- 'Escalate to screenshot.capture_element, screenshot.capture_region, accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
15
+ 'Escalate to screenshot.capture_element, screenshot.capture_region, dom.get_accessibility_tree, page.evaluate, viewport.resize, or CDP only when structured reads cannot answer the question.',
16
16
  'Use patch.apply_styles or patch.apply_dom for temporary experiments, and rollback patches before finishing unless the user asks to keep them.',
17
17
  'Only use the specialized Browser Bridge MCP tools directly when the host has already allowed them or the user explicitly wants typed tool calls.',
18
18
  ].join('\n');
@@ -102,8 +102,8 @@ function createGuidePrompt() {
102
102
  '1. Prefer MCP over `bbx`; do not shell out unless setup, doctor, logs, or raw CLI debugging is explicitly needed.',
103
103
  '2. In permission-ask hosts, use `browser_call` as the default tool so the user can approve one BBX MCP tool instead of separate tools for status, page, DOM, input, and patches.',
104
104
  '3. Start with `browser_call` method `health.ping`. If access is disabled, call `browser_call` method `access.request` once, ask the user to click Enable, then retry once.',
105
- '4. Start with structured reads via `browser_call`: `page.get_state`, `dom.query`, `dom.find_by_text`, `dom.find_by_role`, `styles.get_computed`, and `batch`.',
106
- '5. Keep budgets tight with `budgetPreset: "quick"` or `"normal"`; widen only when results are truncated.',
105
+ '4. Start with structured reads via `browser_call`: `page.get_state`, `dom.query`, `dom.find_by_text`, `dom.find_by_role`, and `styles.get_computed`. Use `browser_batch` for parallel reads.',
106
+ '5. `browser_call` accepts explicit method limits, not `budgetPreset`; keep limits tight and widen only when results are truncated.',
107
107
  '6. Reuse `elementRef` values returned by DOM tools instead of rescanning.',
108
108
  '7. Escalate to screenshots, accessibility tree, `page.evaluate`, viewport resize, or CDP only when structured reads cannot answer.',
109
109
  '8. Use `patch.apply_styles` or `patch.apply_dom` for temporary experiments and rollback before finishing unless the user asks to keep patches.',