@browserbridge/bbx 1.7.5 → 1.7.7

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/README.md CHANGED
@@ -114,7 +114,7 @@ Browser Bridge is optimized for the opposite starting point: **inspect the state
114
114
 
115
115
  On Ubuntu, Chromium is commonly installed as a strict snap, and Flatpak Chromium is similarly sandboxed. If native messaging stays disconnected there, use a non-sandboxed Chromium-based browser such as Google Chrome, Brave, or Edge.
116
116
 
117
- MCP mode is self-contained: the server exposes tools, startup instructions, and prompt templates, so a separate CLI skill is not required for MCP guidance.
117
+ MCP mode is self-contained: the server exposes tools and startup instructions, so a separate CLI skill is not required for MCP guidance.
118
118
 
119
119
  ## How it works
120
120
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbridge/bbx",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
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,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
+ }
@@ -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
+ }