@browserbridge/bbx 1.4.0 → 1.5.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.
- package/package.json +1 -1
- package/packages/agent-client/src/cli.js +13 -1
- package/packages/agent-client/src/mcp-config.js +11 -25
- package/packages/agent-client/src/runtime.js +2 -10
- package/packages/agent-client/src/types.ts +2 -1
- package/packages/mcp-server/src/handlers-page.js +6 -0
- package/packages/native-host/src/daemon-process.js +26 -4
- package/packages/native-host/src/daemon.js +3 -1
- package/packages/native-host/src/framing.js +7 -2
- package/packages/protocol/src/protocol.js +3 -1
package/package.json
CHANGED
|
@@ -687,7 +687,7 @@ async function main() {
|
|
|
687
687
|
process.exitCode = 1;
|
|
688
688
|
} catch (error) {
|
|
689
689
|
const message = error instanceof Error ? error.message : String(error);
|
|
690
|
-
const raw =
|
|
690
|
+
const raw = getErrorCode(error);
|
|
691
691
|
let code = 'ERROR';
|
|
692
692
|
if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
|
|
693
693
|
code = 'DAEMON_OFFLINE';
|
|
@@ -709,6 +709,18 @@ async function main() {
|
|
|
709
709
|
}
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
/**
|
|
713
|
+
* @param {unknown} error
|
|
714
|
+
* @returns {string}
|
|
715
|
+
*/
|
|
716
|
+
function getErrorCode(error) {
|
|
717
|
+
if (!(error instanceof Error) || !('code' in error)) {
|
|
718
|
+
return '';
|
|
719
|
+
}
|
|
720
|
+
const code = /** @type {{ code?: unknown }} */ (error).code;
|
|
721
|
+
return typeof code === 'string' ? code : '';
|
|
722
|
+
}
|
|
723
|
+
|
|
712
724
|
/**
|
|
713
725
|
* Allow tests to shrink request timeouts without changing the shared default.
|
|
714
726
|
*
|
|
@@ -94,29 +94,19 @@ export function getMcpConfigShape(clientName) {
|
|
|
94
94
|
* }}
|
|
95
95
|
*/
|
|
96
96
|
function createBaseServerConfig(clientName) {
|
|
97
|
-
const
|
|
98
|
-
process.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
env: {},
|
|
103
|
-
}
|
|
104
|
-
: {
|
|
105
|
-
command: 'bbx',
|
|
106
|
-
args: ['mcp', 'serve'],
|
|
107
|
-
env: {},
|
|
108
|
-
};
|
|
97
|
+
const serverConfig = {
|
|
98
|
+
command: process.execPath,
|
|
99
|
+
args: [mcpServerBinPath],
|
|
100
|
+
env: {},
|
|
101
|
+
};
|
|
109
102
|
|
|
110
103
|
if (clientName === 'opencode') {
|
|
111
104
|
return {
|
|
112
105
|
type: 'local',
|
|
113
|
-
command:
|
|
114
|
-
process.platform === 'win32'
|
|
115
|
-
? [process.execPath, mcpServerBinPath]
|
|
116
|
-
: ['bbx', 'mcp', 'serve'],
|
|
106
|
+
command: [process.execPath, mcpServerBinPath],
|
|
117
107
|
};
|
|
118
108
|
}
|
|
119
|
-
return
|
|
109
|
+
return serverConfig;
|
|
120
110
|
}
|
|
121
111
|
|
|
122
112
|
/** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
|
|
@@ -142,13 +132,11 @@ const MCP_CONFIG_SHAPES = {
|
|
|
142
132
|
*/
|
|
143
133
|
export function buildMcpConfig(clientName) {
|
|
144
134
|
if (clientName === 'codex') {
|
|
145
|
-
const command = process.platform === 'win32' ? process.execPath : 'bbx';
|
|
146
|
-
const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
|
|
147
135
|
return {
|
|
148
136
|
mcp_servers: {
|
|
149
137
|
[BROWSER_BRIDGE_SERVER_NAME]: {
|
|
150
|
-
command,
|
|
151
|
-
args,
|
|
138
|
+
command: process.execPath,
|
|
139
|
+
args: [mcpServerBinPath],
|
|
152
140
|
},
|
|
153
141
|
},
|
|
154
142
|
};
|
|
@@ -277,12 +265,10 @@ export async function getMcpConfigPaths(clientName, options) {
|
|
|
277
265
|
* @returns {string}
|
|
278
266
|
*/
|
|
279
267
|
function formatCodexServerBlock() {
|
|
280
|
-
const command = process.platform === 'win32' ? process.execPath : 'bbx';
|
|
281
|
-
const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
|
|
282
268
|
return [
|
|
283
269
|
`[mcp_servers."${BROWSER_BRIDGE_SERVER_NAME}"]`,
|
|
284
|
-
`command = ${JSON.stringify(
|
|
285
|
-
`args = ${JSON.stringify(
|
|
270
|
+
`command = ${JSON.stringify(process.execPath)}`,
|
|
271
|
+
`args = ${JSON.stringify([mcpServerBinPath])}`,
|
|
286
272
|
'',
|
|
287
273
|
].join('\n');
|
|
288
274
|
}
|
|
@@ -160,10 +160,10 @@ export async function checkBrowserManifests() {
|
|
|
160
160
|
export async function getDoctorReport(options = {}) {
|
|
161
161
|
const manifest = await (options.loadManifest || loadInstalledManifest)();
|
|
162
162
|
const allowedOrigins = Array.isArray(manifest?.allowed_origins) ? manifest.allowed_origins : [];
|
|
163
|
-
const manifestInstalled = Boolean(manifest);
|
|
164
163
|
const defaultExtensionId = options.defaultExtensionIdInfo || resolveDefaultExtensionId();
|
|
165
164
|
|
|
166
|
-
const browserManifests = await checkBrowserManifests();
|
|
165
|
+
const browserManifests = await (options.checkBrowserManifests || checkBrowserManifests)();
|
|
166
|
+
const manifestInstalled = Boolean(manifest) || browserManifests.some((b) => b.installed);
|
|
167
167
|
|
|
168
168
|
/** @type {DoctorReport} */
|
|
169
169
|
const report = {
|
|
@@ -214,8 +214,6 @@ export async function getDoctorReport(options = {}) {
|
|
|
214
214
|
report.extensionConnected = false;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
const browsersWithoutManifest = browserManifests.filter((b) => !b.installed);
|
|
218
|
-
|
|
219
217
|
if (!report.manifestInstalled) {
|
|
220
218
|
report.issues.push('native_host_manifest_missing');
|
|
221
219
|
report.nextSteps.push(
|
|
@@ -223,12 +221,6 @@ export async function getDoctorReport(options = {}) {
|
|
|
223
221
|
? 'Run `bbx install` (or `bbx install --all` for all browsers) to install the native host manifest.'
|
|
224
222
|
: 'Run `bbx install <extension-id>` (or `bbx install --all`) to install the native host manifest.'
|
|
225
223
|
);
|
|
226
|
-
} else if (browsersWithoutManifest.length > 0) {
|
|
227
|
-
report.issues.push('native_host_manifest_partial');
|
|
228
|
-
const missing = browsersWithoutManifest.map((b) => b.browser).join(', ');
|
|
229
|
-
report.nextSteps.push(
|
|
230
|
-
`Manifests missing for: ${missing}. Run \`bbx install --all\` to install for all supported browsers.`
|
|
231
|
-
);
|
|
232
224
|
}
|
|
233
225
|
if (!report.daemonReachable) {
|
|
234
226
|
report.issues.push('daemon_offline');
|
|
@@ -67,7 +67,7 @@ export type ClientMessage =
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
export interface PendingRequest {
|
|
70
|
-
resolve: (value:
|
|
70
|
+
resolve: (value: unknown) => void;
|
|
71
71
|
reject: (error: Error) => void;
|
|
72
72
|
timeoutId: NodeJS.Timeout;
|
|
73
73
|
}
|
|
@@ -118,6 +118,7 @@ export interface DoctorReport {
|
|
|
118
118
|
|
|
119
119
|
export interface DoctorReportOptions {
|
|
120
120
|
loadManifest?: () => Promise<{ allowed_origins?: string[] } | null>;
|
|
121
|
+
checkBrowserManifests?: () => Promise<BrowserManifestStatus[]>;
|
|
121
122
|
manifestPath?: string;
|
|
122
123
|
defaultExtensionIdInfo?: { extensionId: string | null; source: string };
|
|
123
124
|
bridgeClientRunner?: <T>(
|
|
@@ -87,6 +87,12 @@ export async function handlePageTool(args) {
|
|
|
87
87
|
}
|
|
88
88
|
const entry = PAGE_ACTIONS[normalizedArgs.action];
|
|
89
89
|
if (!entry) return summarizeToolError(`Unsupported page action "${args.action}".`);
|
|
90
|
+
if (
|
|
91
|
+
normalizedArgs.action === 'evaluate' &&
|
|
92
|
+
(typeof normalizedArgs.expression !== 'string' || !normalizedArgs.expression.trim())
|
|
93
|
+
) {
|
|
94
|
+
return summarizeToolError('expression is required for page evaluate.');
|
|
95
|
+
}
|
|
90
96
|
return callBridgeTool(entry.method, entry.params(normalizedArgs), {
|
|
91
97
|
tabId: typeof normalizedArgs.tabId === 'number' ? normalizedArgs.tabId : null,
|
|
92
98
|
tokenBudget: getToolTokenBudget(normalizedArgs),
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
|
|
9
9
|
import { pingExistingDaemon } from './daemon.js';
|
|
10
10
|
import {
|
|
11
|
+
applyWindowsTcpTransportDefaults,
|
|
11
12
|
createSocketBridgeTransport,
|
|
12
13
|
formatBridgeTransport,
|
|
13
14
|
getBridgeTransport,
|
|
@@ -124,7 +125,7 @@ export async function clearDaemonPidFile(options = {}) {
|
|
|
124
125
|
*/
|
|
125
126
|
export async function stopBridgeDaemon(options = {}) {
|
|
126
127
|
const {
|
|
127
|
-
transport =
|
|
128
|
+
transport = undefined,
|
|
128
129
|
socketPath = undefined,
|
|
129
130
|
pidPath = getDaemonPidPath(),
|
|
130
131
|
timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
|
|
@@ -136,7 +137,7 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
136
137
|
rmFn = fs.promises.rm,
|
|
137
138
|
sleepFn = sleep,
|
|
138
139
|
} = options;
|
|
139
|
-
const resolvedTransport =
|
|
140
|
+
const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
|
|
140
141
|
const resolvedSocketPath =
|
|
141
142
|
resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
|
|
142
143
|
|
|
@@ -238,7 +239,7 @@ export async function restartBridgeDaemonIfRunning(options = {}) {
|
|
|
238
239
|
*/
|
|
239
240
|
async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
240
241
|
const {
|
|
241
|
-
transport =
|
|
242
|
+
transport = undefined,
|
|
242
243
|
socketPath = undefined,
|
|
243
244
|
pidPath = getDaemonPidPath(),
|
|
244
245
|
timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
|
|
@@ -248,7 +249,7 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
|
248
249
|
sleepFn = sleep,
|
|
249
250
|
spawnDaemonFn = spawnBridgeDaemonProcess,
|
|
250
251
|
} = options;
|
|
251
|
-
const resolvedTransport =
|
|
252
|
+
const resolvedTransport = resolveDaemonTransport({ transport, socketPath });
|
|
252
253
|
|
|
253
254
|
spawnDaemonFn();
|
|
254
255
|
|
|
@@ -271,6 +272,27 @@ async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
|
271
272
|
};
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Mirror the daemon entrypoint transport defaults so restart polling targets the
|
|
277
|
+
* same endpoint the spawned process listens on.
|
|
278
|
+
*
|
|
279
|
+
* @param {{ transport?: BridgeTransport, socketPath?: string }} options
|
|
280
|
+
* @returns {BridgeTransport}
|
|
281
|
+
*/
|
|
282
|
+
function resolveDaemonTransport(options) {
|
|
283
|
+
const { transport, socketPath } = options;
|
|
284
|
+
if (socketPath) {
|
|
285
|
+
return createSocketBridgeTransport(socketPath);
|
|
286
|
+
}
|
|
287
|
+
if (transport) {
|
|
288
|
+
return transport;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const env = { ...process.env };
|
|
292
|
+
applyWindowsTcpTransportDefaults(env);
|
|
293
|
+
return getBridgeTransport(env);
|
|
294
|
+
}
|
|
295
|
+
|
|
274
296
|
/**
|
|
275
297
|
* @param {BridgeTransport} transport
|
|
276
298
|
* @returns {Promise<number | null>}
|
|
@@ -53,7 +53,7 @@ const DAEMON_VERSION = loadDaemonVersion();
|
|
|
53
53
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
54
54
|
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
55
55
|
/** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number, __authenticated?: boolean }} ClientSocket */
|
|
56
|
-
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
56
|
+
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, protocolVersion?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
57
57
|
/**
|
|
58
58
|
* @typedef {{
|
|
59
59
|
* installAgentFiles: typeof import('../../agent-client/src/install.js').installAgentFiles,
|
|
@@ -762,6 +762,7 @@ export class BridgeDaemon {
|
|
|
762
762
|
this.trackPendingRequest(request.id, {
|
|
763
763
|
socket,
|
|
764
764
|
method: request.method,
|
|
765
|
+
protocolVersion: request.meta?.protocol_version,
|
|
765
766
|
source: typeof request.meta?.source === 'string' ? request.meta.source : '',
|
|
766
767
|
targets: new Set(targets),
|
|
767
768
|
timeoutId: setTimeout(() => {
|
|
@@ -925,6 +926,7 @@ export class BridgeDaemon {
|
|
|
925
926
|
socketPath: this.socketPath,
|
|
926
927
|
transport: formatBridgeTransport(this.transport),
|
|
927
928
|
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
929
|
+
...getVersionNegotiationPayload(pending.protocolVersion),
|
|
928
930
|
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
929
931
|
daemonVersion: DAEMON_VERSION,
|
|
930
932
|
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { once } from 'node:events';
|
|
4
4
|
|
|
5
|
-
import { MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
|
|
5
|
+
import { MAX_JSON_LINE_BYTES, MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @param {NodeJS.WritableStream} stream
|
|
@@ -193,7 +193,12 @@ export function createNativeMessageReader(stream, onMessage, onProtocolError) {
|
|
|
193
193
|
* @returns {Promise<void>}
|
|
194
194
|
*/
|
|
195
195
|
export async function writeJsonLine(socket, message) {
|
|
196
|
-
|
|
196
|
+
const line = `${JSON.stringify(message)}\n`;
|
|
197
|
+
const byteLength = Buffer.byteLength(line.slice(0, -1), 'utf8');
|
|
198
|
+
if (byteLength > MAX_JSON_LINE_BYTES) {
|
|
199
|
+
throw new Error(`JSON line exceeds ${MAX_JSON_LINE_BYTES} bytes: ${byteLength}`);
|
|
200
|
+
}
|
|
201
|
+
if (!socket.write(line)) {
|
|
197
202
|
await once(socket, 'drain');
|
|
198
203
|
}
|
|
199
204
|
}
|
|
@@ -97,7 +97,9 @@ export const SUPPORTED_VERSIONS = Object.freeze(['1.0']);
|
|
|
97
97
|
* @returns {number}
|
|
98
98
|
*/
|
|
99
99
|
function clampInt(value, min, max, fallback) {
|
|
100
|
-
|
|
100
|
+
const numeric = Number(value);
|
|
101
|
+
const integer = Number.isFinite(numeric) && numeric !== 0 ? Math.trunc(numeric) : fallback;
|
|
102
|
+
return Math.min(Math.max(integer, min), max);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** @type {ReadonlyArray<BridgeMethod>} */
|