@browserbridge/bbx 1.0.0 → 1.1.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.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/package.json +53 -53
  3. package/packages/agent-client/src/cli-helpers.js +43 -5
  4. package/packages/agent-client/src/cli.js +176 -171
  5. package/packages/agent-client/src/client.js +66 -21
  6. package/packages/agent-client/src/command-registry.js +104 -69
  7. package/packages/agent-client/src/detect.js +162 -54
  8. package/packages/agent-client/src/install.js +34 -28
  9. package/packages/agent-client/src/mcp-config.js +40 -40
  10. package/packages/agent-client/src/runtime.js +41 -20
  11. package/packages/agent-client/src/setup-status.js +23 -30
  12. package/packages/mcp-server/src/bin.js +57 -5
  13. package/packages/mcp-server/src/handlers.js +573 -256
  14. package/packages/mcp-server/src/server.js +568 -257
  15. package/packages/native-host/bin/bridge-daemon.js +39 -6
  16. package/packages/native-host/bin/install-manifest.js +26 -4
  17. package/packages/native-host/bin/postinstall.js +4 -2
  18. package/packages/native-host/src/config.js +142 -13
  19. package/packages/native-host/src/daemon-process.js +396 -0
  20. package/packages/native-host/src/daemon.js +350 -150
  21. package/packages/native-host/src/framing.js +131 -11
  22. package/packages/native-host/src/install-manifest.js +194 -29
  23. package/packages/native-host/src/native-host.js +154 -102
  24. package/packages/protocol/src/budget.js +3 -7
  25. package/packages/protocol/src/capabilities.js +6 -3
  26. package/packages/protocol/src/defaults.js +1 -0
  27. package/packages/protocol/src/errors.js +15 -11
  28. package/packages/protocol/src/payload-cost.js +19 -6
  29. package/packages/protocol/src/protocol.js +242 -73
  30. package/packages/protocol/src/registry.js +311 -45
  31. package/packages/protocol/src/summary.js +260 -109
  32. package/packages/protocol/src/types.js +29 -4
  33. package/skills/browser-bridge/SKILL.md +3 -2
  34. package/skills/browser-bridge/agents/openai.yaml +3 -3
  35. package/skills/browser-bridge/references/interaction.md +34 -11
  36. package/skills/browser-bridge/references/patch-workflow.md +3 -0
  37. package/skills/browser-bridge/references/protocol.md +127 -71
  38. package/skills/browser-bridge/references/tailwind.md +12 -11
  39. package/skills/browser-bridge/references/token-efficiency.md +23 -22
  40. package/skills/browser-bridge/references/ui-workflows.md +8 -0
  41. package/CHANGELOG.md +0 -55
  42. package/assets/banner.jpg +0 -0
  43. package/assets/logo.png +0 -0
  44. package/assets/logo.svg +0 -65
  45. package/docs/api-reference.md +0 -157
  46. package/docs/cli-guide.md +0 -128
  47. package/docs/index.md +0 -25
  48. package/docs/manual-setup.md +0 -140
  49. package/docs/mcp-vs-cli.md +0 -258
  50. package/docs/publishing.md +0 -114
  51. package/docs/quickstart.md +0 -104
  52. package/docs/troubleshooting.md +0 -59
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -52
  55. package/packages/extension/assets/icon-128.png +0 -0
  56. package/packages/extension/assets/icon-16.png +0 -0
  57. package/packages/extension/assets/icon-32.png +0 -0
  58. package/packages/extension/assets/icon-48.png +0 -0
  59. package/packages/extension/src/background-helpers.js +0 -459
  60. package/packages/extension/src/background-routing.js +0 -91
  61. package/packages/extension/src/background.js +0 -3227
  62. package/packages/extension/src/content-script-helpers.js +0 -281
  63. package/packages/extension/src/content-script.js +0 -1977
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -102
  66. package/packages/extension/ui/offscreen.html +0 -6
  67. package/packages/extension/ui/offscreen.js +0 -61
  68. package/packages/extension/ui/popup.html +0 -35
  69. package/packages/extension/ui/popup.js +0 -279
  70. package/packages/extension/ui/sidepanel.html +0 -102
  71. package/packages/extension/ui/sidepanel.js +0 -1854
  72. package/packages/extension/ui/ui.css +0 -1159
@@ -3,7 +3,11 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
6
- import { APP_NAME, getManifestInstallDir, SUPPORTED_BROWSERS } from '../../native-host/src/config.js';
6
+ import {
7
+ APP_NAME,
8
+ getManifestInstallDir,
9
+ SUPPORTED_BROWSERS,
10
+ } from '../../native-host/src/config.js';
7
11
  import { resolveDefaultExtensionId } from '../../native-host/src/install-manifest.js';
8
12
  import { methodNeedsTab } from './cli-helpers.js';
9
13
  import { BridgeClient } from './client.js';
@@ -65,7 +69,7 @@ export async function requestBridge(client, method, params = {}, options = {}) {
65
69
  method,
66
70
  params,
67
71
  tabId: methodNeedsTab(method) ? (options.tabId ?? null) : null,
68
- meta: withRequestMeta(options.source, options.tokenBudget)
72
+ meta: withRequestMeta(options.source, options.tokenBudget),
69
73
  });
70
74
  }
71
75
 
@@ -81,9 +85,14 @@ export async function resolveRef(client, refOrSelector, tabId = null, source) {
81
85
  return refOrSelector;
82
86
  }
83
87
 
84
- const response = await requestBridge(client, 'dom.query', {
85
- selector: refOrSelector
86
- }, { tabId, source });
88
+ const response = await requestBridge(
89
+ client,
90
+ 'dom.query',
91
+ {
92
+ selector: refOrSelector,
93
+ },
94
+ { tabId, source }
95
+ );
87
96
 
88
97
  if (!response.ok) {
89
98
  throw new Error(response.error.message);
@@ -184,9 +193,7 @@ export async function checkBrowserManifests() {
184
193
  */
185
194
  export async function getDoctorReport(options = {}) {
186
195
  const manifest = await (options.loadManifest || loadInstalledManifest)();
187
- const allowedOrigins = Array.isArray(manifest?.allowed_origins)
188
- ? manifest.allowed_origins
189
- : [];
196
+ const allowedOrigins = Array.isArray(manifest?.allowed_origins) ? manifest.allowed_origins : [];
190
197
  const manifestInstalled = Boolean(manifest);
191
198
  const defaultExtensionId = options.defaultExtensionIdInfo || resolveDefaultExtensionId();
192
199
 
@@ -208,7 +215,7 @@ export async function getDoctorReport(options = {}) {
208
215
  routeReason: 'access_disabled',
209
216
  issues: [],
210
217
  nextSteps: [],
211
- browserManifests
218
+ browserManifests,
212
219
  };
213
220
 
214
221
  try {
@@ -217,7 +224,8 @@ export async function getDoctorReport(options = {}) {
217
224
  if (!response.ok) {
218
225
  throw new Error(response.error.message);
219
226
  }
220
- const result = /** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
227
+ const result =
228
+ /** @type {{ daemon?: string, extensionConnected?: boolean, access?: {
221
229
  enabled?: boolean,
222
230
  windowId?: number | null,
223
231
  routeTabId?: number | null,
@@ -227,10 +235,13 @@ export async function getDoctorReport(options = {}) {
227
235
  report.daemonReachable = result.daemon === 'ok';
228
236
  report.extensionConnected = result.extensionConnected === true;
229
237
  report.accessEnabled = result.access?.enabled === true;
230
- report.enabledWindowId = typeof result.access?.windowId === 'number' ? result.access.windowId : null;
231
- report.routeTabId = typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
238
+ report.enabledWindowId =
239
+ typeof result.access?.windowId === 'number' ? result.access.windowId : null;
240
+ report.routeTabId =
241
+ typeof result.access?.routeTabId === 'number' ? result.access.routeTabId : null;
232
242
  report.routeReady = result.access?.routeReady === true;
233
- report.routeReason = typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
243
+ report.routeReason =
244
+ typeof result.access?.reason === 'string' ? result.access.reason : 'access_disabled';
234
245
  });
235
246
  } catch {
236
247
  report.daemonReachable = false;
@@ -241,13 +252,17 @@ export async function getDoctorReport(options = {}) {
241
252
 
242
253
  if (!report.manifestInstalled) {
243
254
  report.issues.push('native_host_manifest_missing');
244
- report.nextSteps.push(defaultExtensionId.extensionId
245
- ? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
246
- : 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.');
255
+ report.nextSteps.push(
256
+ defaultExtensionId.extensionId
257
+ ? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
258
+ : 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
259
+ );
247
260
  } else if (browsersWithoutManifest.length > 0) {
248
261
  report.issues.push('native_host_manifest_partial');
249
262
  const missing = browsersWithoutManifest.map((b) => b.browser).join(', ');
250
- report.nextSteps.push(`Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`);
263
+ report.nextSteps.push(
264
+ `Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`
265
+ );
251
266
  }
252
267
  if (!report.daemonReachable) {
253
268
  report.issues.push('daemon_offline');
@@ -255,14 +270,20 @@ export async function getDoctorReport(options = {}) {
255
270
  }
256
271
  if (report.daemonReachable && !report.extensionConnected) {
257
272
  report.issues.push('extension_disconnected');
258
- report.nextSteps.push('Open Chrome and make sure the Browser Bridge extension is installed and active.');
273
+ report.nextSteps.push(
274
+ 'Open Chrome and make sure the Browser Bridge extension is installed and active.'
275
+ );
259
276
  }
260
277
  if (report.daemonReachable && report.extensionConnected && !report.accessEnabled) {
261
278
  report.issues.push('access_disabled');
262
- report.nextSteps.push('If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to click Enable for the needed window, then tell you when that window is ready.');
279
+ report.nextSteps.push(
280
+ 'If a Browser Bridge call returns ACCESS_DENIED, stop requesting access. Ask the user to click Enable for the needed window, then tell you when that window is ready.'
281
+ );
263
282
  } else if (report.daemonReachable && report.extensionConnected && !report.routeReady) {
264
283
  report.issues.push(report.routeReason || 'no_routable_active_tab');
265
- report.nextSteps.push('Switch to a supported page in the enabled window, or use an explicit tabId override.');
284
+ report.nextSteps.push(
285
+ 'Switch to a supported page in the enabled window, or use an explicit tabId override.'
286
+ );
266
287
  }
267
288
 
268
289
  return report;
@@ -57,8 +57,8 @@ const SKILL_TARGET_LABELS = {
57
57
  * global?: boolean,
58
58
  * cwd?: string,
59
59
  * projectPath?: string,
60
- * mcpDetectors?: Record<string, () => boolean>,
61
- * skillDetectors?: Record<string, () => boolean>,
60
+ * mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
61
+ * skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
62
62
  * access?: (targetPath: string) => Promise<void>,
63
63
  * readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
64
64
  * }} SetupStatusOptions
@@ -76,14 +76,18 @@ export async function collectSetupStatus(options = {}) {
76
76
  const projectPath = options.projectPath || cwd;
77
77
  const access = options.access || fs.promises.access.bind(fs.promises);
78
78
  const readFile = options.readFile || fs.promises.readFile.bind(fs.promises);
79
- const detectedMcpClients = new Set(detectMcpClients(options.mcpDetectors));
80
- const detectedSkillTargets = new Set(
79
+ const [detectedMcpClientResult, detectedSkillTargetResult] = await Promise.allSettled([
80
+ detectMcpClients(options.mcpDetectors),
81
81
  detectSkillTargets(options.skillDetectors),
82
- );
82
+ ]);
83
+ const detectedMcpClientNames =
84
+ detectedMcpClientResult.status === 'fulfilled' ? detectedMcpClientResult.value : [];
85
+ const detectedSkillTargetNames =
86
+ detectedSkillTargetResult.status === 'fulfilled' ? detectedSkillTargetResult.value : [];
87
+ const detectedMcpClients = new Set(detectedMcpClientNames);
88
+ const detectedSkillTargets = new Set(detectedSkillTargetNames);
83
89
  for (const clientName of detectedMcpClients) {
84
- if (
85
- SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))
86
- ) {
90
+ if (SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))) {
87
91
  detectedSkillTargets.add(/** @type {SupportedTarget} */ (clientName));
88
92
  }
89
93
  }
@@ -96,7 +100,7 @@ export async function collectSetupStatus(options = {}) {
96
100
  detected: detectedMcpClients.has(clientName),
97
101
  readFile,
98
102
  });
99
- }),
103
+ })
100
104
  );
101
105
  const skillTargets = await Promise.all(
102
106
  SUPPORTED_TARGETS.map(async (target) => {
@@ -107,7 +111,7 @@ export async function collectSetupStatus(options = {}) {
107
111
  access,
108
112
  readFile,
109
113
  });
110
- }),
114
+ })
111
115
  );
112
116
 
113
117
  return {
@@ -138,12 +142,8 @@ async function collectMcpClientStatus(clientName, options) {
138
142
  });
139
143
  const entries = await Promise.all(
140
144
  configPaths.map(async (configPath) => {
141
- return readBrowserBridgeMcpEntry(
142
- clientName,
143
- configPath,
144
- options.readFile,
145
- );
146
- }),
145
+ return readBrowserBridgeMcpEntry(clientName, configPath, options.readFile);
146
+ })
147
147
  );
148
148
  const preferredEntry =
149
149
  entries.find((entry) => entry.configured) ||
@@ -188,20 +188,17 @@ async function collectSkillTargetStatus(target, options) {
188
188
  skillName,
189
189
  sentinelFilename,
190
190
  options.access,
191
- options.readFile,
191
+ options.readFile
192
192
  );
193
- }),
193
+ })
194
194
  );
195
195
  const skillByName = new Map(skills.map((skill) => [skill.name, skill]));
196
196
  const coreSkill = skillByName.get(coreSkillName) || null;
197
197
  const coreInstalled = Boolean(coreSkill?.exists);
198
198
  const coreManaged = Boolean(coreSkill?.exists && coreSkill.managed);
199
- const installedVersion = getInstalledSkillBundleVersion(
200
- coreSkill ? [coreSkill] : [],
201
- );
199
+ const installedVersion = getInstalledSkillBundleVersion(coreSkill ? [coreSkill] : []);
202
200
  const updateAvailable =
203
- coreManaged &&
204
- isManagedVersionOutdated(coreSkill?.version || null, currentVersion);
201
+ coreManaged && isManagedVersionOutdated(coreSkill?.version || null, currentVersion);
205
202
 
206
203
  return {
207
204
  key: target,
@@ -230,15 +227,13 @@ async function collectInstalledSkillStatus(
230
227
  skillName,
231
228
  sentinelFilename,
232
229
  access,
233
- readFile,
230
+ readFile
234
231
  ) {
235
232
  const skillPath = path.join(basePath, skillName);
236
233
  const exists = await pathExists(skillPath, access);
237
234
  const sentinelPath = path.join(skillPath, sentinelFilename);
238
235
  const managed = exists && (await pathExists(sentinelPath, access));
239
- const version = managed
240
- ? await readManagedSkillVersion(sentinelPath, readFile)
241
- : null;
236
+ const version = managed ? await readManagedSkillVersion(sentinelPath, readFile) : null;
242
237
 
243
238
  return {
244
239
  name: skillName,
@@ -261,9 +256,7 @@ function getInstalledSkillBundleVersion(skills) {
261
256
  if (!first || typeof first.version !== 'string') {
262
257
  return null;
263
258
  }
264
- return skills.every((skill) => skill.version === first.version)
265
- ? first.version
266
- : null;
259
+ return skills.every((skill) => skill.version === first.version) ? first.version : null;
267
260
  }
268
261
 
269
262
  /**
@@ -1,10 +1,62 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
4
7
  import { startBridgeMcpServer } from './server.js';
5
8
 
6
- startBridgeMcpServer().catch((error) => {
7
- const message = error instanceof Error ? error.stack || error.message : String(error);
8
- process.stderr.write(`${message}\n`);
9
- process.exit(1);
10
- });
9
+ /**
10
+ * @typedef {{
11
+ * start?: () => Promise<void>,
12
+ * argv?: string[],
13
+ * stdout?: { write: (chunk: string) => unknown },
14
+ * stderr?: { write: (chunk: string) => unknown },
15
+ * exit?: (code: number) => unknown,
16
+ * }} BridgeMcpCliOptions
17
+ */
18
+
19
+ const HELP_FLAGS = new Set(['help', '--help', '-h']);
20
+
21
+ const MCP_USAGE = [
22
+ 'Usage: bbx-mcp [--help]',
23
+ '',
24
+ 'Start the Browser Bridge MCP stdio server.',
25
+ ].join('\n');
26
+
27
+ /**
28
+ * Start the MCP server CLI and report startup failures to stderr.
29
+ *
30
+ * @param {BridgeMcpCliOptions} [options]
31
+ * @returns {Promise<number>}
32
+ */
33
+ export async function runBridgeMcpCli(options = {}) {
34
+ const {
35
+ start = startBridgeMcpServer,
36
+ argv = process.argv.slice(2),
37
+ stdout = process.stdout,
38
+ stderr = process.stderr,
39
+ exit = process.exit,
40
+ } = options;
41
+
42
+ if (argv.some((arg) => HELP_FLAGS.has(arg))) {
43
+ stdout.write(`${MCP_USAGE}\n`);
44
+ return 0;
45
+ }
46
+
47
+ try {
48
+ await start();
49
+ return 0;
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.stack || error.message : String(error);
52
+ stderr.write(`${message}\n`);
53
+ exit(1);
54
+ return 1;
55
+ }
56
+ }
57
+
58
+ const entryPointUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
59
+
60
+ if (entryPointUrl === import.meta.url) {
61
+ void runBridgeMcpCli();
62
+ }