@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
@@ -0,0 +1,396 @@
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 {
200
+ transport = getBridgeTransport(),
201
+ socketPath = undefined,
202
+ pidPath = getDaemonPidPath(),
203
+ timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
204
+ pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
205
+ pingDaemonFn = pingExistingDaemon,
206
+ readPidFn = readDaemonPidFile,
207
+ findPidByTransportFn = findDaemonPidByTransport,
208
+ killFn = process.kill.bind(process),
209
+ rmFn = fs.promises.rm,
210
+ sleepFn = sleep,
211
+ spawnDaemonFn = spawnBridgeDaemonProcess,
212
+ } = options;
213
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
214
+
215
+ const stopResult = await stopBridgeDaemon({
216
+ transport: resolvedTransport,
217
+ pidPath,
218
+ timeoutMs,
219
+ pollIntervalMs,
220
+ pingDaemonFn,
221
+ readPidFn,
222
+ findPidByTransportFn,
223
+ killFn,
224
+ rmFn,
225
+ sleepFn,
226
+ });
227
+
228
+ spawnDaemonFn();
229
+
230
+ const started = await waitForDaemonReachability({
231
+ transport: resolvedTransport,
232
+ reachable: true,
233
+ timeoutMs,
234
+ pollIntervalMs,
235
+ pingDaemonFn,
236
+ sleepFn,
237
+ });
238
+ if (!started) {
239
+ throw new Error('Timed out waiting for Browser Bridge daemon to start.');
240
+ }
241
+
242
+ return {
243
+ ...stopResult,
244
+ pidPath,
245
+ pid: await readPidFn(pidPath),
246
+ };
247
+ }
248
+
249
+ /**
250
+ * @param {BridgeTransport} transport
251
+ * @returns {Promise<number | null>}
252
+ */
253
+ export async function findDaemonPidByTransport(transport) {
254
+ if (transport.type !== 'socket') {
255
+ return null;
256
+ }
257
+ return findDaemonPidBySocket(transport.socketPath);
258
+ }
259
+
260
+ /**
261
+ * @param {string} socketPath
262
+ * @returns {Promise<number | null>}
263
+ */
264
+ export async function findDaemonPidBySocket(socketPath) {
265
+ if (process.platform === 'win32') {
266
+ return null;
267
+ }
268
+
269
+ try {
270
+ const { stdout } = await execFileAsync('lsof', ['-t', '--', socketPath]);
271
+ const pid = Number.parseInt(
272
+ stdout
273
+ .split(/\r?\n/u)
274
+ .map((line) => line.trim())
275
+ .find(Boolean) ?? '',
276
+ 10
277
+ );
278
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
279
+ } catch (error) {
280
+ if (isCommandNotFoundError(error) || isLsofNoResultsError(error)) {
281
+ return null;
282
+ }
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @param {{
289
+ * transport: BridgeTransport,
290
+ * reachable: boolean,
291
+ * timeoutMs: number,
292
+ * pollIntervalMs: number,
293
+ * pingDaemonFn: (transport: BridgeTransport) => Promise<boolean>,
294
+ * sleepFn: (ms: number) => Promise<void>,
295
+ * }} options
296
+ * @returns {Promise<boolean>}
297
+ */
298
+ async function waitForDaemonReachability(options) {
299
+ const { transport, reachable, timeoutMs, pollIntervalMs, pingDaemonFn, sleepFn } = options;
300
+ const deadline = Date.now() + timeoutMs;
301
+ while (Date.now() <= deadline) {
302
+ if ((await safePingDaemon(transport, pingDaemonFn)) === reachable) {
303
+ return true;
304
+ }
305
+ await sleepFn(pollIntervalMs);
306
+ }
307
+ return false;
308
+ }
309
+
310
+ /**
311
+ * @param {BridgeTransport} transport
312
+ * @param {typeof fs.promises.rm} rmFn
313
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
314
+ * @returns {Promise<boolean>}
315
+ */
316
+ async function removeStaleSocket(transport, rmFn, pingDaemonFn) {
317
+ if (transport.type !== 'socket') {
318
+ return false;
319
+ }
320
+
321
+ if (await safePingDaemon(transport, pingDaemonFn)) {
322
+ return false;
323
+ }
324
+
325
+ try {
326
+ await fs.promises.access(transport.socketPath);
327
+ } catch (error) {
328
+ if (isMissingFileError(error)) {
329
+ return false;
330
+ }
331
+ throw error;
332
+ }
333
+
334
+ try {
335
+ await rmFn(transport.socketPath, { force: true });
336
+ return true;
337
+ } catch (error) {
338
+ if (isMissingFileError(error)) {
339
+ return false;
340
+ }
341
+ throw error;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * @param {BridgeTransport} transport
347
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
348
+ * @returns {Promise<boolean>}
349
+ */
350
+ async function safePingDaemon(transport, pingDaemonFn) {
351
+ try {
352
+ return await pingDaemonFn(transport);
353
+ } catch {
354
+ return false;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * @param {number} ms
360
+ * @returns {Promise<void>}
361
+ */
362
+ function sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+
366
+ /**
367
+ * @param {unknown} error
368
+ * @returns {boolean}
369
+ */
370
+ function isMissingFileError(error) {
371
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
372
+ }
373
+
374
+ /**
375
+ * @param {unknown} error
376
+ * @returns {boolean}
377
+ */
378
+ function isMissingProcessError(error) {
379
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH');
380
+ }
381
+
382
+ /**
383
+ * @param {unknown} error
384
+ * @returns {boolean}
385
+ */
386
+ function isCommandNotFoundError(error) {
387
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
388
+ }
389
+
390
+ /**
391
+ * @param {unknown} error
392
+ * @returns {boolean}
393
+ */
394
+ function isLsofNoResultsError(error) {
395
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 1);
396
+ }