@browserbridge/bbx 1.7.4 → 1.7.6

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 (34) hide show
  1. package/package.json +6 -6
  2. package/packages/agent-client/src/atomic-write.js +99 -0
  3. package/packages/agent-client/src/cli-args.js +152 -0
  4. package/packages/agent-client/src/cli-batch.js +140 -0
  5. package/packages/agent-client/src/cli-helpers.js +20 -3
  6. package/packages/agent-client/src/cli-output.js +57 -0
  7. package/packages/agent-client/src/cli-proxy-remote.js +350 -0
  8. package/packages/agent-client/src/cli-setup-commands.js +404 -0
  9. package/packages/agent-client/src/cli.js +63 -906
  10. package/packages/agent-client/src/command-registry.js +12 -6
  11. package/packages/agent-client/src/install.js +39 -4
  12. package/packages/agent-client/src/mcp-config.js +28 -14
  13. package/packages/agent-client/src/remotes.js +93 -15
  14. package/packages/agent-client/src/runtime.js +6 -4
  15. package/packages/mcp-server/src/bin.js +10 -1
  16. package/packages/mcp-server/src/guidance.js +1 -1
  17. package/packages/mcp-server/src/handlers-capture.js +8 -5
  18. package/packages/mcp-server/src/handlers-dom.js +2 -2
  19. package/packages/mcp-server/src/handlers-navigation.js +20 -9
  20. package/packages/mcp-server/src/handlers-page.js +111 -111
  21. package/packages/mcp-server/src/handlers-utils.js +268 -8
  22. package/packages/mcp-server/src/handlers.js +31 -10
  23. package/packages/mcp-server/src/lifecycle.js +413 -0
  24. package/packages/mcp-server/src/server.js +27 -19
  25. package/packages/native-host/src/auth-token.js +1 -1
  26. package/packages/native-host/src/config.js +9 -4
  27. package/packages/native-host/src/daemon-process.js +8 -1
  28. package/packages/protocol/src/capabilities.js +33 -110
  29. package/packages/protocol/src/capability-values.js +45 -0
  30. package/packages/protocol/src/defaults.js +19 -2
  31. package/packages/protocol/src/protocol.js +106 -5
  32. package/packages/protocol/src/registry.js +299 -86
  33. package/packages/protocol/src/types.ts +19 -1
  34. package/skills/browser-bridge/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "agent-tools",
@@ -56,7 +56,7 @@
56
56
  "test": "c8 node --test",
57
57
  "coverage": "c8 report --reporter=text --reporter=text-summary",
58
58
  "coverage:review": "c8 report --reporter=json-summary && node scripts/review-coverage.mjs",
59
- "coverage:check": "c8 check-coverage --lines 80 --branches 75",
59
+ "coverage:check": "c8 check-coverage --lines 92 --branches 83",
60
60
  "typecheck": "tsc --noEmit",
61
61
  "postinstall": "node packages/native-host/bin/postinstall.js",
62
62
  "package:extension": "node scripts/package-extension.mjs",
@@ -75,13 +75,13 @@
75
75
  "devDependencies": {
76
76
  "@types/chrome": "^0.2.2",
77
77
  "@types/node": "^26.1.1",
78
- "c8": "^11.0.0",
78
+ "c8": "^12.0.0",
79
79
  "linkedom": "^0.18.13",
80
- "oxfmt": "^0.58.0",
81
- "oxlint": "^1.73.0",
80
+ "oxfmt": "^0.59.0",
81
+ "oxlint": "^1.74.0",
82
82
  "typescript": "^7.0.2"
83
83
  },
84
84
  "engines": {
85
- "node": ">=18"
85
+ "node": ">=20"
86
86
  }
87
87
  }
@@ -0,0 +1,99 @@
1
+ // @ts-check
2
+
3
+ import { randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ /**
8
+ * Replace a file through a sibling temporary file so readers never observe a
9
+ * partial write. Existing permissions are preserved unless an explicit mode is
10
+ * requested.
11
+ *
12
+ * @param {string} targetPath
13
+ * @param {string | NodeJS.ArrayBufferView} data
14
+ * @param {{ encoding?: BufferEncoding, mode?: number, preserveMode?: boolean }} [options={ }]
15
+ * @returns {Promise<void>}
16
+ */
17
+ export async function atomicWriteFile(targetPath, data, options = {}) {
18
+ const directory = path.dirname(targetPath);
19
+ const temporaryPath = path.join(
20
+ directory,
21
+ `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`
22
+ );
23
+ await fs.promises.mkdir(directory, { recursive: true });
24
+
25
+ let existingMode;
26
+ if (options.preserveMode !== false && options.mode === undefined) {
27
+ try {
28
+ existingMode = (await fs.promises.stat(targetPath)).mode & 0o777;
29
+ } catch (error) {
30
+ if (!isMissingFileError(error)) {
31
+ throw error;
32
+ }
33
+ }
34
+ }
35
+
36
+ const mode = options.mode ?? existingMode;
37
+ try {
38
+ await fs.promises.writeFile(temporaryPath, data, {
39
+ ...(options.encoding ? { encoding: options.encoding } : {}),
40
+ ...(mode === undefined ? {} : { mode }),
41
+ flag: 'wx',
42
+ });
43
+ if (process.platform !== 'win32' && mode !== undefined) {
44
+ await fs.promises.chmod(temporaryPath, mode);
45
+ }
46
+ try {
47
+ await fs.promises.rename(temporaryPath, targetPath);
48
+ } catch (error) {
49
+ if (process.platform !== 'win32' || !isReplaceError(error)) {
50
+ throw error;
51
+ }
52
+ try {
53
+ await fs.promises.stat(targetPath);
54
+ } catch {
55
+ throw error;
56
+ }
57
+
58
+ const backupPath = `${temporaryPath}.backup`;
59
+ await fs.promises.rename(targetPath, backupPath);
60
+ try {
61
+ await fs.promises.rename(temporaryPath, targetPath);
62
+ } catch (replaceError) {
63
+ await fs.promises.rename(backupPath, targetPath).catch(() => {});
64
+ throw replaceError;
65
+ }
66
+ await fs.promises.rm(backupPath, { force: true }).catch(() => {});
67
+ }
68
+ if (process.platform !== 'win32' && options.mode !== undefined) {
69
+ await fs.promises.chmod(targetPath, options.mode);
70
+ }
71
+ } catch (error) {
72
+ await fs.promises.rm(temporaryPath, { force: true }).catch(() => {});
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Windows does not replace an existing file with rename().
79
+ *
80
+ * @param {unknown} error
81
+ * @returns {boolean}
82
+ */
83
+ function isReplaceError(error) {
84
+ const code =
85
+ error && typeof error === 'object' ? /** @type {{ code?: unknown }} */ (error).code : null;
86
+ return code === 'EEXIST' || code === 'EPERM' || code === 'EACCES';
87
+ }
88
+
89
+ /**
90
+ * @param {unknown} error
91
+ * @returns {boolean}
92
+ */
93
+ function isMissingFileError(error) {
94
+ return Boolean(
95
+ error &&
96
+ typeof error === 'object' &&
97
+ /** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
98
+ );
99
+ }
@@ -0,0 +1,152 @@
1
+ // @ts-check
2
+
3
+ import { METHODS } from '../../protocol/src/index.js';
4
+ import { methodNeedsTab, parseIntArg, parseJsonObject } from './cli-helpers.js';
5
+
6
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
7
+
8
+ /**
9
+ * Read all of stdin as UTF-8 text. Resolves once stdin closes.
10
+ *
11
+ * @returns {Promise<string>}
12
+ */
13
+ export function readStdin() {
14
+ return new Promise((resolve, reject) => {
15
+ const chunks = /** @type {Buffer[]} */ ([]);
16
+ process.stdin.setEncoding('utf8');
17
+ process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
18
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
19
+ process.stdin.on('error', reject);
20
+ if (process.stdin.isTTY) {
21
+ resolve('');
22
+ }
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Extract the global --remote flag from anywhere in the argument list.
28
+ * Exits with a usage error when the flag is present without a value.
29
+ *
30
+ * @param {string[]} args
31
+ * @returns {{ remoteId: string | null, explicit: boolean, rest: string[] }}
32
+ */
33
+ export function extractRemoteFlag(args) {
34
+ const rest = [...args];
35
+ const index = rest.indexOf('--remote');
36
+ if (index === -1) {
37
+ return { remoteId: null, explicit: false, rest };
38
+ }
39
+ const remoteId = rest[index + 1];
40
+ if (!remoteId || remoteId.startsWith('--')) {
41
+ process.stderr.write('--remote requires a destination name (see `bbx remote list`).\n');
42
+ process.exit(1);
43
+ }
44
+ rest.splice(index, 2);
45
+ return { remoteId, explicit: true, rest };
46
+ }
47
+
48
+ /**
49
+ * @param {string[]} args
50
+ * @returns {{ tabId: number | null, rest: string[] }}
51
+ */
52
+ export function extractTabFlag(args) {
53
+ const rest = [...args];
54
+ let tabId = null;
55
+ const tabIndex = rest.indexOf('--tab');
56
+ if (tabIndex !== -1) {
57
+ tabId = parseIntArg(rest[tabIndex + 1], 'tabId');
58
+ rest.splice(tabIndex, 2);
59
+ }
60
+ return { tabId, rest };
61
+ }
62
+
63
+ /**
64
+ * @param {string[]} args
65
+ * @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
66
+ */
67
+ export async function parseCallCommand(args) {
68
+ const parsed = extractTabFlag(args);
69
+ const [first, second, ...extra] = parsed.rest;
70
+ if (!first) {
71
+ throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
72
+ }
73
+ if (extra.length > 0) {
74
+ throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
75
+ }
76
+
77
+ if (first.includes('.')) {
78
+ const method = /** @type {BridgeMethod} */ (first);
79
+ if (!METHODS.includes(method)) {
80
+ throw new Error(`Unknown method "${first}". Run bbx skill to see available methods.`);
81
+ }
82
+ let rawParams = second;
83
+ // Support piped stdin: `echo '{"key":"val"}' | bbx call method -`
84
+ if (rawParams === '-') {
85
+ rawParams = await readStdin();
86
+ }
87
+ return {
88
+ method,
89
+ tabId: methodNeedsTab(method) ? parsed.tabId : null,
90
+ params: parseJsonObject(rawParams),
91
+ };
92
+ }
93
+
94
+ throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
95
+ }
96
+
97
+ /**
98
+ * @param {string[]} args
99
+ * @returns {{ pattern: string, isBlock: boolean, statusCode: number, body: string | undefined }}
100
+ */
101
+ export function parseInterceptAddArgs(args) {
102
+ const [pattern, ...optionArgs] = args;
103
+ if (!pattern || pattern.startsWith('--')) {
104
+ throw new Error(
105
+ 'Usage: intercept add <urlPattern> [--respond <body>] [--status <code>] [--block]'
106
+ );
107
+ }
108
+
109
+ let isBlock = false;
110
+ let statusCode = 200;
111
+ let hasStatus = false;
112
+ let body;
113
+ for (let index = 0; index < optionArgs.length; index += 1) {
114
+ const option = optionArgs[index];
115
+ if (option === '--block') {
116
+ if (isBlock) {
117
+ throw new Error('The --block option may only be specified once.');
118
+ }
119
+ isBlock = true;
120
+ continue;
121
+ }
122
+ if (option === '--respond') {
123
+ const value = optionArgs[++index];
124
+ if (body !== undefined) {
125
+ throw new Error('The --respond option may only be specified once.');
126
+ }
127
+ if (value === undefined || value.startsWith('--')) {
128
+ throw new Error('--respond requires a body value.');
129
+ }
130
+ body = value;
131
+ continue;
132
+ }
133
+ if (option === '--status') {
134
+ if (hasStatus) {
135
+ throw new Error('The --status option may only be specified once.');
136
+ }
137
+ const value = optionArgs[++index];
138
+ statusCode = parseIntArg(value, 'status');
139
+ if (statusCode < 100 || statusCode > 599) {
140
+ throw new Error('status must be an integer between 100 and 599.');
141
+ }
142
+ hasStatus = true;
143
+ continue;
144
+ }
145
+ throw new Error(`Unknown or extra intercept add option "${option}".`);
146
+ }
147
+
148
+ if (isBlock && body !== undefined) {
149
+ throw new Error('Use either --block or --respond, not both.');
150
+ }
151
+ return { pattern, isBlock, statusCode, body };
152
+ }
@@ -0,0 +1,140 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ getProtocolVersion,
5
+ METHODS,
6
+ summarizeBatchErrorItem,
7
+ summarizeBatchResponseItem,
8
+ } from '../../protocol/src/index.js';
9
+ import { methodNeedsTab } from './cli-helpers.js';
10
+ import { requestBridge } from './runtime.js';
11
+
12
+ /** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
13
+ /** @typedef {import('./types.js').BridgeRequestSource} BridgeRequestSource */
14
+
15
+ /**
16
+ * @typedef {{
17
+ * method: string,
18
+ * tabId: null,
19
+ * ok: false,
20
+ * summary: string,
21
+ * evidence: null,
22
+ * durationMs: 0,
23
+ * approxTokens: 0,
24
+ * meta: { protocol_version: string },
25
+ * error: { code: 'INVALID_REQUEST', message: string },
26
+ * response: null,
27
+ * }} InvalidBatchItem
28
+ */
29
+
30
+ /**
31
+ * @typedef {ReturnType<typeof summarizeBatchResponseItem>
32
+ * | ReturnType<typeof summarizeBatchErrorItem>
33
+ * | InvalidBatchItem} BatchResultItem
34
+ */
35
+
36
+ /**
37
+ * Build the standard invalid-request batch item.
38
+ *
39
+ * @param {string} method
40
+ * @param {string} message
41
+ * @returns {InvalidBatchItem}
42
+ */
43
+ function invalidBatchItem(method, message) {
44
+ return {
45
+ method,
46
+ tabId: null,
47
+ ok: false,
48
+ summary: `INVALID_REQUEST: ${message}`,
49
+ evidence: null,
50
+ durationMs: 0,
51
+ approxTokens: 0,
52
+ meta: { protocol_version: getProtocolVersion() },
53
+ error: {
54
+ code: 'INVALID_REQUEST',
55
+ message,
56
+ },
57
+ response: null,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Parse and execute `bbx batch '[...]'`: validate each call, run them in
63
+ * parallel against the bridge, and return per-call summary items.
64
+ *
65
+ * @param {import('./client.js').BridgeClient} client
66
+ * @param {string | undefined} input - Raw JSON array argument
67
+ * @param {BridgeRequestSource} source - Request source tag (e.g. 'cli')
68
+ * @returns {Promise<BatchResultItem[]>}
69
+ */
70
+ export async function runBatchCalls(client, input, source) {
71
+ if (!input) {
72
+ throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
73
+ }
74
+ let calls;
75
+ try {
76
+ calls = JSON.parse(input);
77
+ } catch {
78
+ throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
79
+ }
80
+ if (!Array.isArray(calls)) {
81
+ throw new Error('Batch input must be a JSON array.');
82
+ }
83
+ if (!client.connected) {
84
+ await client.connect();
85
+ }
86
+ return Promise.all(
87
+ calls.map(async (call) => {
88
+ if (!call || typeof call !== 'object' || Array.isArray(call)) {
89
+ return invalidBatchItem('', 'Each batch call needs a method.');
90
+ }
91
+ const batchCall = /** @type {Record<string, unknown>} */ (call);
92
+ if (typeof batchCall.method !== 'string') {
93
+ return invalidBatchItem('', 'Each batch call needs a method.');
94
+ }
95
+ if (!METHODS.includes(/** @type {BridgeMethod} */ (batchCall.method))) {
96
+ return invalidBatchItem(batchCall.method, `Unknown bridge method "${batchCall.method}".`);
97
+ }
98
+ const method = /** @type {BridgeMethod} */ (batchCall.method);
99
+ if (
100
+ batchCall.params !== undefined &&
101
+ (!batchCall.params ||
102
+ typeof batchCall.params !== 'object' ||
103
+ Array.isArray(batchCall.params))
104
+ ) {
105
+ return invalidBatchItem(method, 'Batch call params must be a JSON object.');
106
+ }
107
+ const params =
108
+ batchCall.params === undefined
109
+ ? {}
110
+ : /** @type {Record<string, unknown>} */ (batchCall.params);
111
+ const tabId =
112
+ methodNeedsTab(method) &&
113
+ typeof batchCall.tabId === 'number' &&
114
+ Number.isInteger(batchCall.tabId) &&
115
+ batchCall.tabId > 0
116
+ ? batchCall.tabId
117
+ : null;
118
+ const startTime = Date.now();
119
+ try {
120
+ const response = await requestBridge(client, method, params, {
121
+ tabId,
122
+ source,
123
+ });
124
+ return summarizeBatchResponseItem({
125
+ method,
126
+ tabId,
127
+ response,
128
+ durationMs: Date.now() - startTime,
129
+ });
130
+ } catch (err) {
131
+ return summarizeBatchErrorItem({
132
+ method,
133
+ tabId,
134
+ error: err,
135
+ durationMs: Date.now() - startTime,
136
+ });
137
+ }
138
+ })
139
+ );
140
+ }
@@ -96,7 +96,7 @@ export function parseJsonObject(value) {
96
96
 
97
97
  /**
98
98
  * Parse a CLI argument as a positive integer, throwing a user-friendly error
99
- * if the value is missing or not a finite number.
99
+ * if the value is missing or not a positive integer.
100
100
  *
101
101
  * @param {string | undefined} value
102
102
  * @param {string} argName - The argument name shown in the error message
@@ -104,12 +104,29 @@ export function parseJsonObject(value) {
104
104
  */
105
105
  export function parseIntArg(value, argName) {
106
106
  const n = Number(value);
107
- if (!value || !Number.isFinite(n)) {
108
- throw new Error(`${argName} must be a number (got ${JSON.stringify(value)}).`);
107
+ if (!value || !Number.isInteger(n) || n <= 0) {
108
+ throw new Error(`${argName} must be a positive integer (got ${JSON.stringify(value)}).`);
109
109
  }
110
110
  return n;
111
111
  }
112
112
 
113
+ /**
114
+ * Parse a CLI argument as a finite number. This is intentionally separate from
115
+ * positive integer parsing because viewport coordinates may be fractional or
116
+ * negative.
117
+ *
118
+ * @param {string | undefined} value
119
+ * @param {string} argName
120
+ * @returns {number}
121
+ */
122
+ export function parseNumberArg(value, argName) {
123
+ const number = Number(value);
124
+ if (value === undefined || value === '' || !Number.isFinite(number)) {
125
+ throw new Error(`${argName} must be a number (got ${JSON.stringify(value)}).`);
126
+ }
127
+ return number;
128
+ }
129
+
113
130
  /**
114
131
  * @param {string} method
115
132
  * @returns {boolean}
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+
3
+ import { sanitizeOutput } from './cli-helpers.js';
4
+ import { CLI_HELP_SECTIONS } from './command-registry.js';
5
+ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
6
+
7
+ /**
8
+ * @param {unknown} value
9
+ * @returns {void}
10
+ */
11
+ export function printJson(value) {
12
+ process.stdout.write(
13
+ `${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`
14
+ );
15
+ }
16
+
17
+ /**
18
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
19
+ * @param {string} [method] - Optional method name for disambiguation
20
+ * @returns {Promise<void>}
21
+ */
22
+ export async function printSummary(response, method) {
23
+ if (!response.ok) {
24
+ process.exitCode = 1;
25
+ }
26
+ printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
27
+ }
28
+
29
+ /**
30
+ * @param {import('../../protocol/src/types.js').BridgeResponse} response
31
+ * @returns {void}
32
+ */
33
+ export function printCallResponse(response) {
34
+ if (response.ok) {
35
+ printJson(response.result);
36
+ return;
37
+ }
38
+
39
+ process.exitCode = 1;
40
+ const errorText = `${response.error.code}: ${response.error.message}`;
41
+ process.stderr.write(
42
+ `${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
43
+ );
44
+ printJson(response);
45
+ }
46
+
47
+ /**
48
+ * @returns {void}
49
+ */
50
+ export function printUsage() {
51
+ const blocks = ['Usage: bbx [--remote <name>] <command> [args]'];
52
+ for (const section of CLI_HELP_SECTIONS) {
53
+ blocks.push('', `${section.title}:`);
54
+ blocks.push(...section.lines.map((line) => ` ${line}`));
55
+ }
56
+ process.stdout.write(`${blocks.join('\n')}\n`);
57
+ }