@browserbridge/bbx 1.0.1 → 1.2.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 (70) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +122 -45
  5. package/packages/agent-client/src/client.js +134 -8
  6. package/packages/agent-client/src/command-registry.js +4 -1
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers-capture.js +279 -0
  13. package/packages/mcp-server/src/handlers-dom.js +196 -0
  14. package/packages/mcp-server/src/handlers-navigation.js +79 -0
  15. package/packages/mcp-server/src/handlers-page.js +365 -0
  16. package/packages/mcp-server/src/handlers-utils.js +296 -0
  17. package/packages/mcp-server/src/handlers.js +63 -1159
  18. package/packages/mcp-server/src/server.js +13 -3
  19. package/packages/native-host/bin/bridge-daemon.js +34 -4
  20. package/packages/native-host/bin/install-manifest.js +32 -2
  21. package/packages/native-host/bin/postinstall.js +16 -0
  22. package/packages/native-host/src/config.js +131 -6
  23. package/packages/native-host/src/daemon-logger.js +157 -0
  24. package/packages/native-host/src/daemon-process.js +422 -0
  25. package/packages/native-host/src/daemon.js +322 -77
  26. package/packages/native-host/src/framing.js +131 -11
  27. package/packages/native-host/src/install-manifest.js +121 -7
  28. package/packages/native-host/src/native-host.js +110 -73
  29. package/packages/protocol/src/capabilities.js +4 -0
  30. package/packages/protocol/src/defaults.js +1 -0
  31. package/packages/protocol/src/errors.js +4 -0
  32. package/packages/protocol/src/payload-cost.js +19 -6
  33. package/packages/protocol/src/protocol.js +143 -7
  34. package/packages/protocol/src/registry.js +13 -0
  35. package/packages/protocol/src/summary.js +18 -10
  36. package/packages/protocol/src/types.js +28 -3
  37. package/skills/browser-bridge/SKILL.md +2 -1
  38. package/skills/browser-bridge/references/interaction.md +1 -0
  39. package/skills/browser-bridge/references/protocol.md +2 -1
  40. package/CHANGELOG.md +0 -55
  41. package/assets/banner.jpg +0 -0
  42. package/assets/logo.png +0 -0
  43. package/assets/logo.svg +0 -65
  44. package/docs/api-reference.md +0 -157
  45. package/docs/cli-guide.md +0 -128
  46. package/docs/index.md +0 -25
  47. package/docs/manual-setup.md +0 -140
  48. package/docs/mcp-vs-cli.md +0 -258
  49. package/docs/publishing.md +0 -112
  50. package/docs/quickstart.md +0 -104
  51. package/docs/troubleshooting.md +0 -59
  52. package/docs/unpacked-extension.md +0 -72
  53. package/docs/usage-scenarios.md +0 -136
  54. package/manifest.json +0 -38
  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 -474
  60. package/packages/extension/src/background-routing.js +0 -89
  61. package/packages/extension/src/background.js +0 -3490
  62. package/packages/extension/src/content-script-helpers.js +0 -282
  63. package/packages/extension/src/content-script.js +0 -2043
  64. package/packages/extension/src/debugger-coordinator.js +0 -188
  65. package/packages/extension/src/sidepanel-helpers.js +0 -104
  66. package/packages/extension/ui/popup.html +0 -35
  67. package/packages/extension/ui/popup.js +0 -298
  68. package/packages/extension/ui/sidepanel.html +0 -102
  69. package/packages/extension/ui/sidepanel.js +0 -1771
  70. package/packages/extension/ui/ui.css +0 -1160
@@ -0,0 +1,422 @@
1
+ // @ts-check
2
+
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { pingExistingDaemon } from './daemon.js';
10
+ import {
11
+ createSocketBridgeTransport,
12
+ formatBridgeTransport,
13
+ getBridgeDir,
14
+ getBridgeTransport,
15
+ getDaemonPidPath,
16
+ } from './config.js';
17
+
18
+ /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
19
+
20
+ const execFileAsync = promisify(execFile);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const daemonEntryPath = path.resolve(__dirname, '../bin/bridge-daemon.js');
23
+ const DEFAULT_DAEMON_RESTART_TIMEOUT_MS = 5_000;
24
+ const DEFAULT_DAEMON_POLL_INTERVAL_MS = 100;
25
+
26
+ /**
27
+ * @typedef {{
28
+ * transport?: BridgeTransport,
29
+ * socketPath?: string,
30
+ * pidPath?: string,
31
+ * timeoutMs?: number,
32
+ * pollIntervalMs?: number,
33
+ * pingDaemonFn?: (transport: BridgeTransport) => Promise<boolean>,
34
+ * readPidFn?: (pidPath?: string) => Promise<number | null>,
35
+ * findPidByTransportFn?: (transport: BridgeTransport) => Promise<number | null>,
36
+ * killFn?: typeof process.kill,
37
+ * rmFn?: typeof fs.promises.rm,
38
+ * sleepFn?: (ms: number) => Promise<void>,
39
+ * }} StopBridgeDaemonOptions
40
+ */
41
+
42
+ /**
43
+ * @typedef {{
44
+ * transport?: BridgeTransport,
45
+ * socketPath?: string,
46
+ * pidPath?: string,
47
+ * timeoutMs?: number,
48
+ * pollIntervalMs?: number,
49
+ * pingDaemonFn?: (transport: BridgeTransport) => Promise<boolean>,
50
+ * readPidFn?: (pidPath?: string) => Promise<number | null>,
51
+ * findPidByTransportFn?: (transport: BridgeTransport) => Promise<number | null>,
52
+ * killFn?: typeof process.kill,
53
+ * rmFn?: typeof fs.promises.rm,
54
+ * sleepFn?: (ms: number) => Promise<void>,
55
+ * spawnDaemonFn?: typeof spawnBridgeDaemonProcess,
56
+ * }} RestartBridgeDaemonOptions
57
+ */
58
+
59
+ /**
60
+ * @returns {import('node:child_process').ChildProcess}
61
+ */
62
+ export function spawnBridgeDaemonProcess() {
63
+ const child = spawn(process.execPath, [daemonEntryPath], {
64
+ detached: true,
65
+ stdio: 'ignore',
66
+ });
67
+ child.unref();
68
+ return child;
69
+ }
70
+
71
+ /**
72
+ * @param {number} [pid=process.pid]
73
+ * @param {string} [pidPath=getDaemonPidPath()]
74
+ * @returns {Promise<void>}
75
+ */
76
+ export async function writeDaemonPidFile(pid = process.pid, pidPath = getDaemonPidPath()) {
77
+ await fs.promises.mkdir(getBridgeDir(), { recursive: true });
78
+ await fs.promises.writeFile(pidPath, `${pid}\n`, 'utf8');
79
+ }
80
+
81
+ /**
82
+ * @param {string} [pidPath=getDaemonPidPath()]
83
+ * @returns {Promise<number | null>}
84
+ */
85
+ export async function readDaemonPidFile(pidPath = getDaemonPidPath()) {
86
+ try {
87
+ const raw = await fs.promises.readFile(pidPath, 'utf8');
88
+ const pid = Number.parseInt(raw.trim(), 10);
89
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
90
+ } catch (error) {
91
+ if (isMissingFileError(error)) {
92
+ return null;
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @param {{ pid?: number | null, pidPath?: string, rmFn?: typeof fs.promises.rm }} [options={}]
100
+ * @returns {Promise<void>}
101
+ */
102
+ export async function clearDaemonPidFile(options = {}) {
103
+ const { pid = null, pidPath = getDaemonPidPath(), rmFn = fs.promises.rm } = options;
104
+
105
+ if (pid !== null) {
106
+ const currentPid = await readDaemonPidFile(pidPath);
107
+ if (currentPid !== pid) {
108
+ return;
109
+ }
110
+ }
111
+
112
+ try {
113
+ await rmFn(pidPath, { force: true });
114
+ } catch (error) {
115
+ if (isMissingFileError(error)) {
116
+ return;
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * @param {StopBridgeDaemonOptions} [options={}]
124
+ * @returns {Promise<{ transport: string, socketPath: string, previouslyRunning: boolean, previousPid: number | null, removedStaleSocket: boolean }>}
125
+ */
126
+ export async function stopBridgeDaemon(options = {}) {
127
+ const {
128
+ transport = getBridgeTransport(),
129
+ socketPath = undefined,
130
+ pidPath = getDaemonPidPath(),
131
+ timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
132
+ pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
133
+ pingDaemonFn = pingExistingDaemon,
134
+ readPidFn = readDaemonPidFile,
135
+ findPidByTransportFn = findDaemonPidByTransport,
136
+ killFn = process.kill.bind(process),
137
+ rmFn = fs.promises.rm,
138
+ sleepFn = sleep,
139
+ } = options;
140
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
141
+ const resolvedSocketPath =
142
+ resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
143
+
144
+ let previousPid = await readPidFn(pidPath);
145
+ let previouslyRunning = previousPid !== null;
146
+
147
+ if (previousPid === null && (await safePingDaemon(resolvedTransport, pingDaemonFn))) {
148
+ previousPid = await findPidByTransportFn(resolvedTransport);
149
+ previouslyRunning = true;
150
+ }
151
+
152
+ if (previousPid !== null) {
153
+ try {
154
+ killFn(previousPid, 'SIGTERM');
155
+ } catch (error) {
156
+ if (!isMissingProcessError(error)) {
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ const stopped = await waitForDaemonReachability({
162
+ transport: resolvedTransport,
163
+ reachable: false,
164
+ timeoutMs,
165
+ pollIntervalMs,
166
+ pingDaemonFn,
167
+ sleepFn,
168
+ });
169
+ if (!stopped) {
170
+ throw new Error(`Timed out waiting for Browser Bridge daemon (pid ${previousPid}) to stop.`);
171
+ }
172
+ }
173
+
174
+ await clearDaemonPidFile({ pid: previousPid, pidPath, rmFn });
175
+
176
+ const removedStaleSocket = await removeStaleSocket(resolvedTransport, rmFn, pingDaemonFn);
177
+ return {
178
+ transport: formatBridgeTransport(resolvedTransport),
179
+ socketPath: resolvedSocketPath,
180
+ previouslyRunning,
181
+ previousPid,
182
+ removedStaleSocket,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * @param {RestartBridgeDaemonOptions} [options={}]
188
+ * @returns {Promise<{
189
+ * transport: string,
190
+ * socketPath: string,
191
+ * pidPath: string,
192
+ * pid: number | null,
193
+ * previouslyRunning: boolean,
194
+ * previousPid: number | null,
195
+ * removedStaleSocket: boolean,
196
+ * }>}
197
+ */
198
+ export async function restartBridgeDaemon(options = {}) {
199
+ const stopResult = await stopBridgeDaemon(options);
200
+ return restartBridgeDaemonAfterStop(stopResult, options);
201
+ }
202
+
203
+ /**
204
+ * Restart the daemon only when one is already running. This is useful during
205
+ * package upgrades where the launcher changed and the in-memory daemon should
206
+ * pick up the new install, without eagerly starting a fresh background process.
207
+ *
208
+ * @param {RestartBridgeDaemonOptions} [options={}]
209
+ * @returns {Promise<{
210
+ * transport: string,
211
+ * socketPath: string,
212
+ * pidPath: string,
213
+ * pid: number | null,
214
+ * previouslyRunning: boolean,
215
+ * previousPid: number | null,
216
+ * removedStaleSocket: boolean,
217
+ * } | null>}
218
+ */
219
+ export async function restartBridgeDaemonIfRunning(options = {}) {
220
+ const stopResult = await stopBridgeDaemon(options);
221
+ if (!stopResult.previouslyRunning) {
222
+ return null;
223
+ }
224
+ return restartBridgeDaemonAfterStop(stopResult, options);
225
+ }
226
+
227
+ /**
228
+ * @param {Awaited<ReturnType<typeof stopBridgeDaemon>>} stopResult
229
+ * @param {RestartBridgeDaemonOptions} [options={}]
230
+ * @returns {Promise<{
231
+ * transport: string,
232
+ * socketPath: string,
233
+ * pidPath: string,
234
+ * pid: number | null,
235
+ * previouslyRunning: boolean,
236
+ * previousPid: number | null,
237
+ * removedStaleSocket: boolean,
238
+ * }>}
239
+ */
240
+ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
241
+ const {
242
+ transport = getBridgeTransport(),
243
+ socketPath = undefined,
244
+ pidPath = getDaemonPidPath(),
245
+ timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
246
+ pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
247
+ pingDaemonFn = pingExistingDaemon,
248
+ readPidFn = readDaemonPidFile,
249
+ sleepFn = sleep,
250
+ spawnDaemonFn = spawnBridgeDaemonProcess,
251
+ } = options;
252
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
253
+
254
+ spawnDaemonFn();
255
+
256
+ const started = await waitForDaemonReachability({
257
+ transport: resolvedTransport,
258
+ reachable: true,
259
+ timeoutMs,
260
+ pollIntervalMs,
261
+ pingDaemonFn,
262
+ sleepFn,
263
+ });
264
+ if (!started) {
265
+ throw new Error('Timed out waiting for Browser Bridge daemon to start.');
266
+ }
267
+
268
+ return {
269
+ ...stopResult,
270
+ pidPath,
271
+ pid: await readPidFn(pidPath),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * @param {BridgeTransport} transport
277
+ * @returns {Promise<number | null>}
278
+ */
279
+ export async function findDaemonPidByTransport(transport) {
280
+ if (transport.type !== 'socket') {
281
+ return null;
282
+ }
283
+ return findDaemonPidBySocket(transport.socketPath);
284
+ }
285
+
286
+ /**
287
+ * @param {string} socketPath
288
+ * @returns {Promise<number | null>}
289
+ */
290
+ export async function findDaemonPidBySocket(socketPath) {
291
+ if (process.platform === 'win32') {
292
+ return null;
293
+ }
294
+
295
+ try {
296
+ const { stdout } = await execFileAsync('lsof', ['-t', '--', socketPath]);
297
+ const pid = Number.parseInt(
298
+ stdout
299
+ .split(/\r?\n/u)
300
+ .map((line) => line.trim())
301
+ .find(Boolean) ?? '',
302
+ 10
303
+ );
304
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
305
+ } catch (error) {
306
+ if (isCommandNotFoundError(error) || isLsofNoResultsError(error)) {
307
+ return null;
308
+ }
309
+ throw error;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * @param {{
315
+ * transport: BridgeTransport,
316
+ * reachable: boolean,
317
+ * timeoutMs: number,
318
+ * pollIntervalMs: number,
319
+ * pingDaemonFn: (transport: BridgeTransport) => Promise<boolean>,
320
+ * sleepFn: (ms: number) => Promise<void>,
321
+ * }} options
322
+ * @returns {Promise<boolean>}
323
+ */
324
+ async function waitForDaemonReachability(options) {
325
+ const { transport, reachable, timeoutMs, pollIntervalMs, pingDaemonFn, sleepFn } = options;
326
+ const deadline = Date.now() + timeoutMs;
327
+ while (Date.now() <= deadline) {
328
+ if ((await safePingDaemon(transport, pingDaemonFn)) === reachable) {
329
+ return true;
330
+ }
331
+ await sleepFn(pollIntervalMs);
332
+ }
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * @param {BridgeTransport} transport
338
+ * @param {typeof fs.promises.rm} rmFn
339
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
340
+ * @returns {Promise<boolean>}
341
+ */
342
+ async function removeStaleSocket(transport, rmFn, pingDaemonFn) {
343
+ if (transport.type !== 'socket') {
344
+ return false;
345
+ }
346
+
347
+ if (await safePingDaemon(transport, pingDaemonFn)) {
348
+ return false;
349
+ }
350
+
351
+ try {
352
+ await fs.promises.access(transport.socketPath);
353
+ } catch (error) {
354
+ if (isMissingFileError(error)) {
355
+ return false;
356
+ }
357
+ throw error;
358
+ }
359
+
360
+ try {
361
+ await rmFn(transport.socketPath, { force: true });
362
+ return true;
363
+ } catch (error) {
364
+ if (isMissingFileError(error)) {
365
+ return false;
366
+ }
367
+ throw error;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * @param {BridgeTransport} transport
373
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
374
+ * @returns {Promise<boolean>}
375
+ */
376
+ async function safePingDaemon(transport, pingDaemonFn) {
377
+ try {
378
+ return await pingDaemonFn(transport);
379
+ } catch {
380
+ return false;
381
+ }
382
+ }
383
+
384
+ /**
385
+ * @param {number} ms
386
+ * @returns {Promise<void>}
387
+ */
388
+ function sleep(ms) {
389
+ return new Promise((resolve) => setTimeout(resolve, ms));
390
+ }
391
+
392
+ /**
393
+ * @param {unknown} error
394
+ * @returns {boolean}
395
+ */
396
+ function isMissingFileError(error) {
397
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
398
+ }
399
+
400
+ /**
401
+ * @param {unknown} error
402
+ * @returns {boolean}
403
+ */
404
+ function isMissingProcessError(error) {
405
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH');
406
+ }
407
+
408
+ /**
409
+ * @param {unknown} error
410
+ * @returns {boolean}
411
+ */
412
+ function isCommandNotFoundError(error) {
413
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
414
+ }
415
+
416
+ /**
417
+ * @param {unknown} error
418
+ * @returns {boolean}
419
+ */
420
+ function isLsofNoResultsError(error) {
421
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 1);
422
+ }