@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.
- package/package.json +6 -6
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-args.js +152 -0
- package/packages/agent-client/src/cli-batch.js +140 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli-output.js +57 -0
- package/packages/agent-client/src/cli-proxy-remote.js +350 -0
- package/packages/agent-client/src/cli-setup-commands.js +404 -0
- package/packages/agent-client/src/cli.js +63 -906
- package/packages/agent-client/src/command-registry.js +12 -6
- package/packages/agent-client/src/install.js +39 -4
- package/packages/agent-client/src/mcp-config.js +28 -14
- package/packages/agent-client/src/remotes.js +93 -15
- package/packages/agent-client/src/runtime.js +6 -4
- package/packages/mcp-server/src/bin.js +10 -1
- package/packages/mcp-server/src/guidance.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +8 -5
- package/packages/mcp-server/src/handlers-dom.js +2 -2
- package/packages/mcp-server/src/handlers-navigation.js +20 -9
- package/packages/mcp-server/src/handlers-page.js +111 -111
- package/packages/mcp-server/src/handlers-utils.js +268 -8
- package/packages/mcp-server/src/handlers.js +31 -10
- package/packages/mcp-server/src/lifecycle.js +413 -0
- package/packages/mcp-server/src/server.js +27 -19
- package/packages/native-host/src/auth-token.js +1 -1
- package/packages/native-host/src/config.js +9 -4
- package/packages/native-host/src/daemon-process.js +8 -1
- package/packages/protocol/src/capabilities.js +33 -110
- package/packages/protocol/src/capability-values.js +45 -0
- package/packages/protocol/src/defaults.js +19 -2
- package/packages/protocol/src/protocol.js +106 -5
- package/packages/protocol/src/registry.js +299 -86
- package/packages/protocol/src/types.ts +19 -1
- package/skills/browser-bridge/SKILL.md +1 -1
|
@@ -4,74 +4,42 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { randomUUID } from 'node:crypto';
|
|
8
7
|
import { fileURLToPath } from 'node:url';
|
|
9
8
|
|
|
10
|
-
import {
|
|
11
|
-
applyWindowsTcpTransportDefaults,
|
|
12
|
-
createTcpBridgeTransport,
|
|
13
|
-
getProxyConfigPath,
|
|
14
|
-
readProxyConfig,
|
|
15
|
-
SUPPORTED_BROWSERS,
|
|
16
|
-
} from '../../native-host/src/config.js';
|
|
17
|
-
import { writeBridgeAuthToken } from '../../native-host/src/auth-token.js';
|
|
18
|
-
import { pingExistingDaemon } from '../../native-host/src/daemon.js';
|
|
9
|
+
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
19
10
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
20
|
-
import {
|
|
11
|
+
import { createRuntimeContext, METHODS } from '../../protocol/src/index.js';
|
|
21
12
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
summarizeBatchErrorItem,
|
|
26
|
-
summarizeBatchResponseItem,
|
|
27
|
-
} from '../../protocol/src/index.js';
|
|
13
|
+
restartRegisteredMcpProcesses,
|
|
14
|
+
tryStartMcpProcessControl,
|
|
15
|
+
} from '../../mcp-server/src/lifecycle.js';
|
|
28
16
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
29
|
-
import { CLI_HELP_SECTIONS, SHORTCUT_COMMANDS } from './command-registry.js';
|
|
30
|
-
import {
|
|
31
|
-
interactiveCheckbox,
|
|
32
|
-
interactiveConfirm,
|
|
33
|
-
methodNeedsTab,
|
|
34
|
-
parseIntArg,
|
|
35
|
-
parseJsonObject,
|
|
36
|
-
sanitizeOutput,
|
|
37
|
-
} from './cli-helpers.js';
|
|
38
17
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} from './
|
|
18
|
+
extractRemoteFlag,
|
|
19
|
+
extractTabFlag,
|
|
20
|
+
parseCallCommand,
|
|
21
|
+
parseInterceptAddArgs,
|
|
22
|
+
readStdin,
|
|
23
|
+
} from './cli-args.js';
|
|
24
|
+
import { runBatchCalls } from './cli-batch.js';
|
|
25
|
+
import { parseIntArg } from './cli-helpers.js';
|
|
26
|
+
import { printCallResponse, printJson, printSummary, printUsage } from './cli-output.js';
|
|
27
|
+
import { handleProxyCommand, handleRemoteCommand } from './cli-proxy-remote.js';
|
|
47
28
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} from './mcp-config.js';
|
|
29
|
+
runInstallMcpCommand,
|
|
30
|
+
runInstallSkillCommand,
|
|
31
|
+
uninstallBrowserBridge,
|
|
32
|
+
} from './cli-setup-commands.js';
|
|
33
|
+
import { SHORTCUT_COMMANDS } from './command-registry.js';
|
|
34
|
+
import { formatMcpConfig, isMcpClientName, MCP_CLIENT_NAMES } from './mcp-config.js';
|
|
55
35
|
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
56
|
-
import {
|
|
57
|
-
addRemoteDestination,
|
|
58
|
-
createBridgeClientForDestination,
|
|
59
|
-
listBridgeDestinations,
|
|
60
|
-
normalizeDestinationId,
|
|
61
|
-
parseRemoteEndpoint,
|
|
62
|
-
readRemoteConfig,
|
|
63
|
-
removeRemoteDestination,
|
|
64
|
-
resolveProxyEnableSettings,
|
|
65
|
-
} from './remotes.js';
|
|
66
|
-
import { collectSetupStatus } from './setup-status.js';
|
|
67
|
-
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
36
|
+
import { createBridgeClientForDestination } from './remotes.js';
|
|
68
37
|
|
|
69
38
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
70
39
|
/** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
|
|
71
40
|
|
|
72
41
|
const REQUEST_SOURCE = 'cli';
|
|
73
42
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
74
|
-
const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
|
|
75
43
|
const REMOTE_ENV = 'BBX_REMOTE';
|
|
76
44
|
|
|
77
45
|
/**
|
|
@@ -99,25 +67,6 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
99
67
|
'restart',
|
|
100
68
|
]);
|
|
101
69
|
|
|
102
|
-
/**
|
|
103
|
-
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
104
|
-
*
|
|
105
|
-
* @returns {Promise<string>}
|
|
106
|
-
*/
|
|
107
|
-
function readStdin() {
|
|
108
|
-
return new Promise((resolve, reject) => {
|
|
109
|
-
const chunks = /** @type {Buffer[]} */ ([]);
|
|
110
|
-
process.stdin.setEncoding('utf8');
|
|
111
|
-
process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
112
|
-
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
|
|
113
|
-
process.stdin.on('error', reject);
|
|
114
|
-
// If stdin is a TTY and nothing is piped, read nothing
|
|
115
|
-
if (process.stdin.isTTY) {
|
|
116
|
-
resolve('');
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
70
|
const remoteFlag = extractRemoteFlag(process.argv.slice(2));
|
|
122
71
|
const [command, ...rest] = remoteFlag.rest;
|
|
123
72
|
|
|
@@ -155,7 +104,6 @@ if (command === 'skill') {
|
|
|
155
104
|
|
|
156
105
|
if (command === 'install') {
|
|
157
106
|
const { execFileSync } = await import('node:child_process');
|
|
158
|
-
const { fileURLToPath } = await import('node:url');
|
|
159
107
|
const installScript = path.resolve(
|
|
160
108
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
161
109
|
'../../native-host/bin/install-manifest.js'
|
|
@@ -177,250 +125,23 @@ if (command === 'uninstall') {
|
|
|
177
125
|
}
|
|
178
126
|
|
|
179
127
|
if (command === 'install-skill') {
|
|
180
|
-
|
|
181
|
-
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
182
|
-
|
|
183
|
-
if (positional.length === 0) {
|
|
184
|
-
let scopeOptions;
|
|
185
|
-
try {
|
|
186
|
-
scopeOptions = parseInstallAgentArgs(rest);
|
|
187
|
-
} catch (error) {
|
|
188
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const isGlobal = scopeOptions.global !== false;
|
|
193
|
-
const projectPath = isGlobal ? os.homedir() : scopeOptions.projectPath;
|
|
194
|
-
|
|
195
|
-
const setupStatus = await collectSetupStatus({
|
|
196
|
-
global: isGlobal,
|
|
197
|
-
cwd: process.cwd(),
|
|
198
|
-
projectPath,
|
|
199
|
-
...getSetupStatusTestOverrides(),
|
|
200
|
-
});
|
|
201
|
-
/** @type {import('./types.js').SupportedTarget[]} */
|
|
202
|
-
const detected = /** @type {import('./types.js').SupportedTarget[]} */ (
|
|
203
|
-
setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
204
|
-
);
|
|
205
|
-
const installedManagedTargets = new Set(
|
|
206
|
-
setupStatus.skillTargets
|
|
207
|
-
.filter((entry) => entry.installed && entry.managed)
|
|
208
|
-
.map((entry) => entry.key)
|
|
209
|
-
);
|
|
210
|
-
const installedManagedTargetList =
|
|
211
|
-
/** @type {import('./types.js').SupportedTarget[]} */ ([...installedManagedTargets]);
|
|
212
|
-
|
|
213
|
-
// Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
|
|
214
|
-
const items = SUPPORTED_TARGETS.map((t) => ({
|
|
215
|
-
value: t,
|
|
216
|
-
label: `${t.padEnd(10)} ${TARGET_LABELS[t]}`,
|
|
217
|
-
hint: formatSelectionHint({
|
|
218
|
-
detected: detected.includes(t),
|
|
219
|
-
installed: installedManagedTargets.has(t),
|
|
220
|
-
}),
|
|
221
|
-
checked: installedManagedTargets.has(t),
|
|
222
|
-
}));
|
|
223
|
-
|
|
224
|
-
const selected = await interactiveCheckbox(
|
|
225
|
-
'Select agents to install skill for (↑↓ move · space toggle · a all · enter confirm)',
|
|
226
|
-
items
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
/** @type {import('./types.js').SupportedTarget[]} */
|
|
230
|
-
let targets;
|
|
231
|
-
if (selected === null) {
|
|
232
|
-
// Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
|
|
233
|
-
targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
|
|
234
|
-
} else {
|
|
235
|
-
targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (selected !== null) {
|
|
239
|
-
const deselectedTargets =
|
|
240
|
-
/** @type {import('./types.js').SupportedTarget[]} */ (
|
|
241
|
-
installedManagedTargetList.filter((target) => !targets.includes(target))
|
|
242
|
-
);
|
|
243
|
-
const removableTargets = await findInstalledManagedTargets({
|
|
244
|
-
targets: deselectedTargets,
|
|
245
|
-
projectPath,
|
|
246
|
-
global: isGlobal,
|
|
247
|
-
});
|
|
248
|
-
if (removableTargets.length > 0) {
|
|
249
|
-
const confirmed = await interactiveConfirm(
|
|
250
|
-
`Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}?`
|
|
251
|
-
);
|
|
252
|
-
if (confirmed) {
|
|
253
|
-
const removedPaths = await removeAgentFiles({
|
|
254
|
-
targets: removableTargets,
|
|
255
|
-
projectPath,
|
|
256
|
-
global: isGlobal,
|
|
257
|
-
});
|
|
258
|
-
for (const p of removedPaths) process.stdout.write(`Removed ${p}\n`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (targets.length === 0) {
|
|
264
|
-
process.stdout.write('No targets selected.\n');
|
|
265
|
-
process.exit(0);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const installedPaths = await installAgentFiles({
|
|
269
|
-
targets,
|
|
270
|
-
projectPath,
|
|
271
|
-
global: isGlobal,
|
|
272
|
-
});
|
|
273
|
-
for (const p of installedPaths) process.stdout.write(`Installed ${p}\n`);
|
|
274
|
-
process.exit(0);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Explicit targets or 'all' provided - use existing arg-parsing logic.
|
|
278
|
-
const options = parseInstallAgentArgs(rest);
|
|
279
|
-
const installedPaths = await installAgentFiles(options);
|
|
280
|
-
for (const installedPath of installedPaths) {
|
|
281
|
-
process.stdout.write(`Installed ${installedPath}\n`);
|
|
282
|
-
}
|
|
283
|
-
process.exit(0);
|
|
128
|
+
await runInstallSkillCommand(rest);
|
|
284
129
|
}
|
|
285
130
|
|
|
286
131
|
if (command === 'install-mcp') {
|
|
287
|
-
|
|
288
|
-
/** @type {string[]} */
|
|
289
|
-
const positionals = [];
|
|
290
|
-
|
|
291
|
-
for (const arg of rest) {
|
|
292
|
-
if (arg === '--local') {
|
|
293
|
-
isGlobal = false;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (arg === '--global') {
|
|
297
|
-
isGlobal = true;
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
if (arg.startsWith('--')) {
|
|
301
|
-
process.stderr.write(`Unknown install-mcp option "${arg}".\n`);
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
|
-
positionals.push(arg);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (positionals.length > 1) {
|
|
308
|
-
process.stderr.write(`Unexpected extra argument "${positionals[1]}".\n`);
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const clientArg = positionals[0];
|
|
313
|
-
|
|
314
|
-
/** @type {import('./types.js').McpClientName[]} */
|
|
315
|
-
let clients;
|
|
316
|
-
|
|
317
|
-
if (!clientArg) {
|
|
318
|
-
// No client specified: inspect current MCP config and prompt interactively.
|
|
319
|
-
const setupStatus = await collectSetupStatus({
|
|
320
|
-
global: isGlobal,
|
|
321
|
-
cwd: process.cwd(),
|
|
322
|
-
projectPath: process.cwd(),
|
|
323
|
-
...getSetupStatusTestOverrides(),
|
|
324
|
-
});
|
|
325
|
-
const detected = /** @type {import('./types.js').McpClientName[]} */ (
|
|
326
|
-
setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
327
|
-
);
|
|
328
|
-
const configuredClients = new Set(
|
|
329
|
-
setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
|
|
330
|
-
);
|
|
331
|
-
const configuredClientList =
|
|
332
|
-
/** @type {import('./types.js').McpClientName[]} */ ([...configuredClients]);
|
|
333
|
-
const items = MCP_CLIENT_NAMES.map((c) => ({
|
|
334
|
-
value: c,
|
|
335
|
-
label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
|
|
336
|
-
hint: formatSelectionHint({
|
|
337
|
-
detected: detected.includes(c),
|
|
338
|
-
installed: configuredClients.has(c),
|
|
339
|
-
}),
|
|
340
|
-
checked: configuredClients.has(c),
|
|
341
|
-
}));
|
|
342
|
-
|
|
343
|
-
const selected = await interactiveCheckbox(
|
|
344
|
-
'Select clients to configure (↑↓ move · space toggle · a all · enter confirm)',
|
|
345
|
-
items
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
if (selected === null) {
|
|
349
|
-
// Non-TTY: prefer configured clients, then detected clients, then all.
|
|
350
|
-
clients =
|
|
351
|
-
configuredClients.size > 0
|
|
352
|
-
? configuredClientList
|
|
353
|
-
: detected.length > 0
|
|
354
|
-
? detected
|
|
355
|
-
: [...MCP_CLIENT_NAMES];
|
|
356
|
-
} else {
|
|
357
|
-
clients = /** @type {import('./types.js').McpClientName[]} */ (selected);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (selected !== null) {
|
|
361
|
-
const deselectedClients =
|
|
362
|
-
/** @type {import('./types.js').McpClientName[]} */ (
|
|
363
|
-
configuredClientList.filter((clientName) => !clients.includes(clientName))
|
|
364
|
-
);
|
|
365
|
-
const removableClients = await findConfiguredMcpClients({
|
|
366
|
-
clients: deselectedClients,
|
|
367
|
-
global: isGlobal,
|
|
368
|
-
cwd: process.cwd(),
|
|
369
|
-
});
|
|
370
|
-
if (removableClients.length > 0) {
|
|
371
|
-
const confirmed = await interactiveConfirm(
|
|
372
|
-
`Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}?`
|
|
373
|
-
);
|
|
374
|
-
if (confirmed) {
|
|
375
|
-
for (const clientName of removableClients) {
|
|
376
|
-
await removeMcpConfig(clientName, {
|
|
377
|
-
global: isGlobal,
|
|
378
|
-
cwd: process.cwd(),
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (clients.length === 0) {
|
|
386
|
-
process.stdout.write('No clients selected.\n');
|
|
387
|
-
process.exit(0);
|
|
388
|
-
}
|
|
389
|
-
} else if (clientArg === 'all') {
|
|
390
|
-
clients = [...MCP_CLIENT_NAMES];
|
|
391
|
-
} else {
|
|
392
|
-
const parts = clientArg
|
|
393
|
-
.split(',')
|
|
394
|
-
.map((s) => s.trim().toLowerCase())
|
|
395
|
-
.filter(Boolean);
|
|
396
|
-
if (parts.includes('all')) {
|
|
397
|
-
clients = [...MCP_CLIENT_NAMES];
|
|
398
|
-
} else {
|
|
399
|
-
clients = [];
|
|
400
|
-
for (const part of parts) {
|
|
401
|
-
if (!isMcpClientName(part)) {
|
|
402
|
-
process.stderr.write(
|
|
403
|
-
`Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n`
|
|
404
|
-
);
|
|
405
|
-
process.exit(1);
|
|
406
|
-
}
|
|
407
|
-
clients.push(part);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
await installMcpClientSetup(clients, {
|
|
413
|
-
global: isGlobal,
|
|
414
|
-
projectPath: process.cwd(),
|
|
415
|
-
stdout: process.stdout,
|
|
416
|
-
});
|
|
417
|
-
process.exit(0);
|
|
132
|
+
await runInstallMcpCommand(rest);
|
|
418
133
|
}
|
|
419
134
|
|
|
420
135
|
if (command === 'mcp') {
|
|
421
136
|
const [subcommand, clientName] = rest;
|
|
422
137
|
if (subcommand === 'serve') {
|
|
423
|
-
await
|
|
138
|
+
const control = await tryStartMcpProcessControl();
|
|
139
|
+
try {
|
|
140
|
+
await startBridgeMcpServer();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await control?.dispose();
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
424
145
|
await new Promise(() => {});
|
|
425
146
|
}
|
|
426
147
|
if (subcommand === 'config') {
|
|
@@ -528,12 +249,26 @@ async function main() {
|
|
|
528
249
|
|
|
529
250
|
if (command === 'restart') {
|
|
530
251
|
const result = await restartBridgeDaemon();
|
|
252
|
+
const mcpProcesses = await restartRegisteredMcpProcesses();
|
|
253
|
+
if (mcpProcesses.restartFailed > 0) {
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
}
|
|
531
256
|
printJson({
|
|
532
|
-
ok:
|
|
533
|
-
summary:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
257
|
+
ok: mcpProcesses.restartFailed === 0,
|
|
258
|
+
summary: `${
|
|
259
|
+
result.previouslyRunning
|
|
260
|
+
? 'Browser Bridge daemon restarted.'
|
|
261
|
+
: 'Browser Bridge daemon started.'
|
|
262
|
+
}${
|
|
263
|
+
mcpProcesses.restartRequested > 0
|
|
264
|
+
? ` Restart requested for ${mcpProcesses.restartRequested} MCP server(s).`
|
|
265
|
+
: ''
|
|
266
|
+
}${
|
|
267
|
+
mcpProcesses.restartFailed > 0
|
|
268
|
+
? ` Could not contact ${mcpProcesses.restartFailed} MCP server(s).`
|
|
269
|
+
: ''
|
|
270
|
+
}`,
|
|
271
|
+
evidence: { ...result, mcpProcesses },
|
|
537
272
|
});
|
|
538
273
|
return;
|
|
539
274
|
}
|
|
@@ -607,83 +342,10 @@ async function main() {
|
|
|
607
342
|
}
|
|
608
343
|
|
|
609
344
|
if (command === 'batch') {
|
|
610
|
-
const
|
|
611
|
-
if (!
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
let calls;
|
|
615
|
-
try {
|
|
616
|
-
calls = JSON.parse(input);
|
|
617
|
-
} catch {
|
|
618
|
-
throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
|
|
619
|
-
}
|
|
620
|
-
if (!Array.isArray(calls)) {
|
|
621
|
-
throw new Error('Batch input must be a JSON array.');
|
|
345
|
+
const results = await runBatchCalls(client, rest[0], REQUEST_SOURCE);
|
|
346
|
+
if (results.some((result) => !result.ok)) {
|
|
347
|
+
process.exitCode = 1;
|
|
622
348
|
}
|
|
623
|
-
await ensureClientConnection();
|
|
624
|
-
const results = await Promise.all(
|
|
625
|
-
calls.map(async (call) => {
|
|
626
|
-
if (!call || typeof call !== 'object' || typeof call.method !== 'string') {
|
|
627
|
-
return {
|
|
628
|
-
method: '',
|
|
629
|
-
tabId: null,
|
|
630
|
-
ok: false,
|
|
631
|
-
summary: 'INVALID_REQUEST: Each batch call needs a method.',
|
|
632
|
-
evidence: null,
|
|
633
|
-
durationMs: 0,
|
|
634
|
-
approxTokens: 0,
|
|
635
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
636
|
-
error: {
|
|
637
|
-
code: 'INVALID_REQUEST',
|
|
638
|
-
message: 'Each batch call needs a method.',
|
|
639
|
-
},
|
|
640
|
-
response: null,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
if (!METHODS.includes(/** @type {BridgeMethod} */ (call.method))) {
|
|
644
|
-
return {
|
|
645
|
-
method: call.method,
|
|
646
|
-
tabId: null,
|
|
647
|
-
ok: false,
|
|
648
|
-
summary: `INVALID_REQUEST: Unknown bridge method "${call.method}".`,
|
|
649
|
-
evidence: null,
|
|
650
|
-
durationMs: 0,
|
|
651
|
-
approxTokens: 0,
|
|
652
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
653
|
-
error: {
|
|
654
|
-
code: 'INVALID_REQUEST',
|
|
655
|
-
message: `Unknown bridge method "${call.method}".`,
|
|
656
|
-
},
|
|
657
|
-
response: null,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
const method = /** @type {BridgeMethod} */ (call.method);
|
|
661
|
-
const tabId =
|
|
662
|
-
methodNeedsTab(call.method) && typeof call.tabId === 'number' ? call.tabId : null;
|
|
663
|
-
const startTime = Date.now();
|
|
664
|
-
try {
|
|
665
|
-
const response = await client.request({
|
|
666
|
-
method,
|
|
667
|
-
tabId,
|
|
668
|
-
params: call.params || {},
|
|
669
|
-
meta: { source: REQUEST_SOURCE },
|
|
670
|
-
});
|
|
671
|
-
return summarizeBatchResponseItem({
|
|
672
|
-
method,
|
|
673
|
-
tabId,
|
|
674
|
-
response,
|
|
675
|
-
durationMs: Date.now() - startTime,
|
|
676
|
-
});
|
|
677
|
-
} catch (err) {
|
|
678
|
-
return summarizeBatchErrorItem({
|
|
679
|
-
method,
|
|
680
|
-
tabId,
|
|
681
|
-
error: err,
|
|
682
|
-
durationMs: Date.now() - startTime,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
})
|
|
686
|
-
);
|
|
687
349
|
printJson(results);
|
|
688
350
|
return;
|
|
689
351
|
}
|
|
@@ -817,22 +479,7 @@ async function main() {
|
|
|
817
479
|
const { tabId, rest: iArgs } = extractTabFlag(rest);
|
|
818
480
|
const sub = iArgs[0];
|
|
819
481
|
if (sub === 'add') {
|
|
820
|
-
const pattern = iArgs
|
|
821
|
-
if (!pattern)
|
|
822
|
-
throw new Error(
|
|
823
|
-
'Usage: intercept add <urlPattern> [--respond <body>] [--status <code>] [--block]'
|
|
824
|
-
);
|
|
825
|
-
const isBlock = iArgs.includes('--block');
|
|
826
|
-
const statusIdx = iArgs.indexOf('--status');
|
|
827
|
-
const statusCode = statusIdx !== -1 ? parseIntArg(iArgs[statusIdx + 1], 'status') : 200;
|
|
828
|
-
const respondIdx = iArgs.indexOf('--respond');
|
|
829
|
-
const body =
|
|
830
|
-
respondIdx !== -1
|
|
831
|
-
? iArgs
|
|
832
|
-
.slice(respondIdx + 1)
|
|
833
|
-
.filter((a) => !a.startsWith('--'))
|
|
834
|
-
.join(' ')
|
|
835
|
-
: undefined;
|
|
482
|
+
const { pattern, isBlock, statusCode, body } = parseInterceptAddArgs(iArgs.slice(1));
|
|
836
483
|
const response = await requestBridge(
|
|
837
484
|
client,
|
|
838
485
|
'network.intercept.add',
|
|
@@ -847,7 +494,9 @@ async function main() {
|
|
|
847
494
|
await printSummary(response);
|
|
848
495
|
} else if (sub === 'remove') {
|
|
849
496
|
const ruleId = iArgs[1];
|
|
850
|
-
if (!ruleId
|
|
497
|
+
if (!ruleId || iArgs.length !== 2) {
|
|
498
|
+
throw new Error('Usage: intercept remove <ruleId>');
|
|
499
|
+
}
|
|
851
500
|
const response = await requestBridge(
|
|
852
501
|
client,
|
|
853
502
|
'network.intercept.remove',
|
|
@@ -856,6 +505,9 @@ async function main() {
|
|
|
856
505
|
);
|
|
857
506
|
await printSummary(response);
|
|
858
507
|
} else if (sub === 'list') {
|
|
508
|
+
if (iArgs.length !== 1) {
|
|
509
|
+
throw new Error('Usage: intercept list');
|
|
510
|
+
}
|
|
859
511
|
const response = await requestBridge(
|
|
860
512
|
client,
|
|
861
513
|
'network.intercept.list',
|
|
@@ -864,6 +516,9 @@ async function main() {
|
|
|
864
516
|
);
|
|
865
517
|
await printSummary(response);
|
|
866
518
|
} else if (sub === 'clear') {
|
|
519
|
+
if (iArgs.length !== 1) {
|
|
520
|
+
throw new Error('Usage: intercept clear');
|
|
521
|
+
}
|
|
867
522
|
const response = await requestBridge(
|
|
868
523
|
client,
|
|
869
524
|
'network.intercept.clear',
|
|
@@ -931,291 +586,6 @@ async function main() {
|
|
|
931
586
|
}
|
|
932
587
|
}
|
|
933
588
|
|
|
934
|
-
/**
|
|
935
|
-
* @param {string[]} args
|
|
936
|
-
* @returns {Promise<void>}
|
|
937
|
-
*/
|
|
938
|
-
async function handleProxyCommand(args) {
|
|
939
|
-
const [subcommand, ...restArgs] = args;
|
|
940
|
-
if (subcommand === 'enable') {
|
|
941
|
-
const options = parseProxyEnableArgs(restArgs);
|
|
942
|
-
const existing = readProxyConfig();
|
|
943
|
-
const { port, bindHost, token, tokenSource } = resolveProxyEnableSettings(
|
|
944
|
-
existing,
|
|
945
|
-
options,
|
|
946
|
-
randomUUID
|
|
947
|
-
);
|
|
948
|
-
await writeBridgeAuthToken(token);
|
|
949
|
-
const configPath = getProxyConfigPath();
|
|
950
|
-
await fs.promises.mkdir(path.dirname(configPath), { recursive: true });
|
|
951
|
-
await fs.promises.writeFile(
|
|
952
|
-
configPath,
|
|
953
|
-
`${JSON.stringify(
|
|
954
|
-
{
|
|
955
|
-
enabled: true,
|
|
956
|
-
port,
|
|
957
|
-
bindHost,
|
|
958
|
-
token,
|
|
959
|
-
},
|
|
960
|
-
null,
|
|
961
|
-
2
|
|
962
|
-
)}\n`,
|
|
963
|
-
{ encoding: 'utf8', mode: 0o600 }
|
|
964
|
-
);
|
|
965
|
-
const result = await restartBridgeDaemon();
|
|
966
|
-
const exampleHost = getProxyExampleHost(bindHost);
|
|
967
|
-
const tokenNote =
|
|
968
|
-
tokenSource === 'existing'
|
|
969
|
-
? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
|
|
970
|
-
: tokenSource === 'generated' && existing
|
|
971
|
-
? ' (rotated - update every configured client with `bbx remote add`)'
|
|
972
|
-
: '';
|
|
973
|
-
const lines = [
|
|
974
|
-
`Browser Bridge proxy enabled on ${bindHost}:${port}.`,
|
|
975
|
-
'',
|
|
976
|
-
`Token: ${token}${tokenNote}`,
|
|
977
|
-
`Config: ${configPath}`,
|
|
978
|
-
`Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
979
|
-
];
|
|
980
|
-
if (tokenSource !== 'existing') {
|
|
981
|
-
lines.push(
|
|
982
|
-
'',
|
|
983
|
-
'On your dev machine, add this remote:',
|
|
984
|
-
'',
|
|
985
|
-
`bbx remote add <name> <host:port> --token ${token}`,
|
|
986
|
-
'',
|
|
987
|
-
'Example:',
|
|
988
|
-
'',
|
|
989
|
-
`bbx remote add remote-bbx ${exampleHost}:${port} --token ${token}`
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
lines.push('');
|
|
993
|
-
process.stdout.write(lines.join('\n'));
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
if (subcommand === 'disable') {
|
|
998
|
-
const configPath = getProxyConfigPath();
|
|
999
|
-
await fs.promises.rm(configPath, { force: true });
|
|
1000
|
-
const result = await restartBridgeDaemon();
|
|
1001
|
-
process.stdout.write(
|
|
1002
|
-
`Browser Bridge proxy disabled. Daemon ${result.previouslyRunning ? 'restarted' : 'started'}.\n`
|
|
1003
|
-
);
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (subcommand === 'status') {
|
|
1008
|
-
const config = readProxyConfig();
|
|
1009
|
-
if (!config) {
|
|
1010
|
-
process.stdout.write('Browser Bridge proxy is disabled.\n');
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
const reachable = await pingExistingDaemon(createTcpBridgeTransport(config.port, '127.0.0.1'));
|
|
1014
|
-
process.stdout.write(
|
|
1015
|
-
[
|
|
1016
|
-
`Browser Bridge proxy is enabled on ${config.bindHost}:${config.port}.`,
|
|
1017
|
-
`Config: ${getProxyConfigPath()}`,
|
|
1018
|
-
`Daemon: ${reachable ? 'reachable' : 'not reachable'} on 127.0.0.1:${config.port}`,
|
|
1019
|
-
'',
|
|
1020
|
-
].join('\n')
|
|
1021
|
-
);
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
throw new Error('Usage: bbx proxy <enable|disable|status>');
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* Parse `bbx proxy enable` flags. Values stay undefined when not passed so an
|
|
1030
|
-
* existing proxy config can be reused instead of silently reset to defaults.
|
|
1031
|
-
*
|
|
1032
|
-
* @param {string[]} args
|
|
1033
|
-
* @returns {import('./remotes.js').ProxyEnableOptions}
|
|
1034
|
-
*/
|
|
1035
|
-
function parseProxyEnableArgs(args) {
|
|
1036
|
-
/** @type {import('./remotes.js').ProxyEnableOptions} */
|
|
1037
|
-
const options = {};
|
|
1038
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1039
|
-
const arg = args[index];
|
|
1040
|
-
if (arg === '--port') {
|
|
1041
|
-
options.port = parseIntArg(args[++index] || '', 'port');
|
|
1042
|
-
continue;
|
|
1043
|
-
}
|
|
1044
|
-
if (arg === '--bind-host') {
|
|
1045
|
-
options.bindHost = args[++index] || '';
|
|
1046
|
-
if (!options.bindHost.trim()) {
|
|
1047
|
-
throw new Error('--bind-host requires a value.');
|
|
1048
|
-
}
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
if (arg === '--token') {
|
|
1052
|
-
options.token = args[++index] || '';
|
|
1053
|
-
if (!options.token.trim()) {
|
|
1054
|
-
throw new Error('--token requires a value.');
|
|
1055
|
-
}
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
if (arg === '--rotate-token') {
|
|
1059
|
-
options.rotateToken = true;
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
throw new Error(`Unknown proxy enable option "${arg}".`);
|
|
1063
|
-
}
|
|
1064
|
-
if (options.token && options.rotateToken) {
|
|
1065
|
-
throw new Error('Use either --token or --rotate-token, not both.');
|
|
1066
|
-
}
|
|
1067
|
-
if (options.port !== undefined && (options.port < 1 || options.port > 65535)) {
|
|
1068
|
-
throw new Error('port must be an integer between 1 and 65535.');
|
|
1069
|
-
}
|
|
1070
|
-
return options;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
/**
|
|
1074
|
-
* @param {string} bindHost
|
|
1075
|
-
* @returns {string}
|
|
1076
|
-
*/
|
|
1077
|
-
function getProxyExampleHost(bindHost) {
|
|
1078
|
-
if (bindHost !== '0.0.0.0' && bindHost !== '::') {
|
|
1079
|
-
return bindHost;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
/** @type {string | null} */
|
|
1083
|
-
let firstIpv4 = null;
|
|
1084
|
-
for (const [name, entries] of Object.entries(os.networkInterfaces())) {
|
|
1085
|
-
for (const entry of entries ?? []) {
|
|
1086
|
-
if (!entry.internal && entry.family === 'IPv4') {
|
|
1087
|
-
if (name.toLowerCase() === 'ethernet 0') {
|
|
1088
|
-
return entry.address;
|
|
1089
|
-
}
|
|
1090
|
-
firstIpv4 ??= entry.address;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
return firstIpv4 ?? '<host>';
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/**
|
|
1099
|
-
* @param {string[]} args
|
|
1100
|
-
* @returns {Promise<void>}
|
|
1101
|
-
*/
|
|
1102
|
-
async function handleRemoteCommand(args) {
|
|
1103
|
-
const [subcommand, ...restArgs] = args;
|
|
1104
|
-
if (subcommand === 'add') {
|
|
1105
|
-
const [name, endpoint, ...optionArgs] = restArgs;
|
|
1106
|
-
if (!name || !endpoint) {
|
|
1107
|
-
throw new Error('Usage: bbx remote add <name> <host:port> --token <token>');
|
|
1108
|
-
}
|
|
1109
|
-
const token = parseRemoteTokenOption(optionArgs);
|
|
1110
|
-
const { host, port } = parseRemoteEndpoint(endpoint);
|
|
1111
|
-
const remote = await addRemoteDestination({
|
|
1112
|
-
id: normalizeDestinationId(name),
|
|
1113
|
-
host,
|
|
1114
|
-
port,
|
|
1115
|
-
token,
|
|
1116
|
-
});
|
|
1117
|
-
process.stdout.write(
|
|
1118
|
-
`Remote destination "${remote.id}" saved (${remote.host}:${remote.port}).\n`
|
|
1119
|
-
);
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
if (subcommand === 'remove') {
|
|
1124
|
-
const [name] = restArgs;
|
|
1125
|
-
if (!name) {
|
|
1126
|
-
throw new Error('Usage: bbx remote remove <name>');
|
|
1127
|
-
}
|
|
1128
|
-
const removed = await removeRemoteDestination(name);
|
|
1129
|
-
process.stdout.write(
|
|
1130
|
-
removed
|
|
1131
|
-
? `Remote destination "${name}" removed.\n`
|
|
1132
|
-
: `Remote destination "${name}" was not configured.\n`
|
|
1133
|
-
);
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
if (subcommand === 'list') {
|
|
1138
|
-
const config = await readRemoteConfig();
|
|
1139
|
-
if (config.remotes.length === 0) {
|
|
1140
|
-
process.stdout.write('No remote destinations configured.\n');
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
process.stdout.write(
|
|
1144
|
-
`${config.remotes.map((remote) => `${remote.id}\t${remote.host}:${remote.port}`).join('\n')}\n`
|
|
1145
|
-
);
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
if (subcommand === 'test') {
|
|
1150
|
-
const [name] = restArgs;
|
|
1151
|
-
if (!name) {
|
|
1152
|
-
throw new Error('Usage: bbx remote test <name>');
|
|
1153
|
-
}
|
|
1154
|
-
const remoteClient = await createBridgeClientForDestination(name);
|
|
1155
|
-
try {
|
|
1156
|
-
await remoteClient.connect();
|
|
1157
|
-
const response = await remoteClient.request({ method: 'health.ping' });
|
|
1158
|
-
if (response.ok) {
|
|
1159
|
-
const health = /** @type {{ extensionConnected?: boolean }} */ (response.result);
|
|
1160
|
-
process.stdout.write(
|
|
1161
|
-
`Remote destination "${name}" is reachable (extension ${
|
|
1162
|
-
health.extensionConnected === true ? 'connected' : 'not connected'
|
|
1163
|
-
}).\n`
|
|
1164
|
-
);
|
|
1165
|
-
} else {
|
|
1166
|
-
process.exitCode = 1;
|
|
1167
|
-
process.stdout.write(`Remote destination "${name}" failed: ${response.error.message}\n`);
|
|
1168
|
-
}
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
process.exitCode = 1;
|
|
1171
|
-
process.stdout.write(
|
|
1172
|
-
`Remote destination "${name}" is not reachable: ${
|
|
1173
|
-
error instanceof Error ? error.message : String(error)
|
|
1174
|
-
}\n`
|
|
1175
|
-
);
|
|
1176
|
-
} finally {
|
|
1177
|
-
await remoteClient.close();
|
|
1178
|
-
}
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
if (subcommand === 'destinations') {
|
|
1183
|
-
const destinations = await listBridgeDestinations();
|
|
1184
|
-
process.stdout.write(
|
|
1185
|
-
`${destinations
|
|
1186
|
-
.map((destination) =>
|
|
1187
|
-
destination.local
|
|
1188
|
-
? `${destination.id}\tlocal`
|
|
1189
|
-
: `${destination.id}\t${destination.host}:${destination.port}`
|
|
1190
|
-
)
|
|
1191
|
-
.join('\n')}\n`
|
|
1192
|
-
);
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
throw new Error('Usage: bbx remote <add|remove|list|test|destinations>');
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
/**
|
|
1200
|
-
* @param {string[]} args
|
|
1201
|
-
* @returns {string}
|
|
1202
|
-
*/
|
|
1203
|
-
function parseRemoteTokenOption(args) {
|
|
1204
|
-
let token = '';
|
|
1205
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1206
|
-
const arg = args[index];
|
|
1207
|
-
if (arg === '--token') {
|
|
1208
|
-
token = args[++index] || '';
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
throw new Error(`Unknown remote add option "${arg}".`);
|
|
1212
|
-
}
|
|
1213
|
-
if (!token.trim()) {
|
|
1214
|
-
throw new Error('Usage: bbx remote add <name> <host:port> --token <token>');
|
|
1215
|
-
}
|
|
1216
|
-
return token.trim();
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
589
|
/**
|
|
1220
590
|
* @param {unknown} error
|
|
1221
591
|
* @returns {string}
|
|
@@ -1242,216 +612,3 @@ function getClientTimeoutOverride() {
|
|
|
1242
612
|
const value = Number.parseInt(raw, 10);
|
|
1243
613
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1244
614
|
}
|
|
1245
|
-
|
|
1246
|
-
/**
|
|
1247
|
-
* Allow CLI tests to provide deterministic MCP client detection without relying
|
|
1248
|
-
* on whatever tools happen to be installed on the host machine.
|
|
1249
|
-
*
|
|
1250
|
-
* @returns {{
|
|
1251
|
-
* mcpDetectors?: Record<string, () => boolean>,
|
|
1252
|
-
* }}
|
|
1253
|
-
*/
|
|
1254
|
-
function getSetupStatusTestOverrides() {
|
|
1255
|
-
if (!(TEST_DETECTED_MCP_CLIENTS_ENV in process.env)) {
|
|
1256
|
-
return {};
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
const detectedClients = new Set(
|
|
1260
|
-
(process.env[TEST_DETECTED_MCP_CLIENTS_ENV] || '')
|
|
1261
|
-
.split(',')
|
|
1262
|
-
.map((value) => value.trim().toLowerCase())
|
|
1263
|
-
.filter(Boolean)
|
|
1264
|
-
);
|
|
1265
|
-
|
|
1266
|
-
return {
|
|
1267
|
-
mcpDetectors: Object.fromEntries(
|
|
1268
|
-
MCP_CLIENT_NAMES.map((clientName) => [clientName, () => detectedClients.has(clientName)])
|
|
1269
|
-
),
|
|
1270
|
-
};
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
/**
|
|
1274
|
-
* @param {{ detected: boolean, installed: boolean }} options
|
|
1275
|
-
* @returns {string | undefined}
|
|
1276
|
-
*/
|
|
1277
|
-
function formatSelectionHint(options) {
|
|
1278
|
-
/** @type {string[]} */
|
|
1279
|
-
const parts = [];
|
|
1280
|
-
if (options.detected) {
|
|
1281
|
-
parts.push('● detected');
|
|
1282
|
-
}
|
|
1283
|
-
if (options.installed) {
|
|
1284
|
-
parts.push('installed');
|
|
1285
|
-
}
|
|
1286
|
-
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* @returns {Promise<void>}
|
|
1291
|
-
*/
|
|
1292
|
-
async function ensureClientConnection() {
|
|
1293
|
-
if (!client.connected) {
|
|
1294
|
-
await client.connect();
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
/**
|
|
1299
|
-
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
1300
|
-
* @param {string} [method] - Optional method name for disambiguation
|
|
1301
|
-
* @returns {Promise<void>}
|
|
1302
|
-
*/
|
|
1303
|
-
async function printSummary(response, method) {
|
|
1304
|
-
if (!response.ok) {
|
|
1305
|
-
process.exitCode = 1;
|
|
1306
|
-
}
|
|
1307
|
-
printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
/**
|
|
1311
|
-
* @param {unknown} value
|
|
1312
|
-
* @returns {void}
|
|
1313
|
-
*/
|
|
1314
|
-
function printJson(value) {
|
|
1315
|
-
process.stdout.write(
|
|
1316
|
-
`${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
/**
|
|
1321
|
-
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
1322
|
-
* @returns {void}
|
|
1323
|
-
*/
|
|
1324
|
-
function printCallResponse(response) {
|
|
1325
|
-
if (response.ok) {
|
|
1326
|
-
printJson(response.result);
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
process.exitCode = 1;
|
|
1331
|
-
const errorText = `${response.error.code}: ${response.error.message}`;
|
|
1332
|
-
process.stderr.write(
|
|
1333
|
-
`${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
|
|
1334
|
-
);
|
|
1335
|
-
printJson(response);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
function printUsage() {
|
|
1339
|
-
const blocks = ['Usage: bbx [--remote <name>] <command> [args]'];
|
|
1340
|
-
for (const section of CLI_HELP_SECTIONS) {
|
|
1341
|
-
blocks.push('', `${section.title}:`);
|
|
1342
|
-
blocks.push(...section.lines.map((line) => ` ${line}`));
|
|
1343
|
-
}
|
|
1344
|
-
process.stdout.write(`${blocks.join('\n')}\n`);
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
/**
|
|
1348
|
-
* @returns {Promise<void>}
|
|
1349
|
-
*/
|
|
1350
|
-
async function uninstallBrowserBridge() {
|
|
1351
|
-
const cwd = process.cwd();
|
|
1352
|
-
const globalProjectPath = os.homedir();
|
|
1353
|
-
|
|
1354
|
-
for (const clientName of MCP_CLIENT_NAMES) {
|
|
1355
|
-
await removeMcpConfig(clientName, { global: true, cwd });
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
const removedGlobalSkillPaths = await removeAgentFiles({
|
|
1359
|
-
targets: SUPPORTED_TARGETS,
|
|
1360
|
-
projectPath: globalProjectPath,
|
|
1361
|
-
global: true,
|
|
1362
|
-
});
|
|
1363
|
-
for (const removedPath of removedGlobalSkillPaths) {
|
|
1364
|
-
process.stdout.write(`Removed ${removedPath}\n`);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
for (const clientName of MCP_CLIENT_NAMES) {
|
|
1368
|
-
await removeMcpConfig(clientName, { global: false, cwd });
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
const removedLocalSkillPaths = await removeAgentFiles({
|
|
1372
|
-
targets: SUPPORTED_TARGETS,
|
|
1373
|
-
projectPath: cwd,
|
|
1374
|
-
global: false,
|
|
1375
|
-
});
|
|
1376
|
-
for (const removedPath of removedLocalSkillPaths) {
|
|
1377
|
-
process.stdout.write(`Removed ${removedPath}\n`);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
for (const [index, browser] of SUPPORTED_BROWSERS.entries()) {
|
|
1381
|
-
await uninstallNativeManifest({
|
|
1382
|
-
browser,
|
|
1383
|
-
removeBridgeDir: index === SUPPORTED_BROWSERS.length - 1,
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
/**
|
|
1389
|
-
* @param {string[]} args
|
|
1390
|
-
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
1391
|
-
*/
|
|
1392
|
-
async function parseCallCommand(args) {
|
|
1393
|
-
const parsed = extractTabFlag(args);
|
|
1394
|
-
const [first, second, ...extra] = parsed.rest;
|
|
1395
|
-
if (!first) {
|
|
1396
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1397
|
-
}
|
|
1398
|
-
if (extra.length > 0) {
|
|
1399
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
if (first.includes('.')) {
|
|
1403
|
-
const method = /** @type {BridgeMethod} */ (first);
|
|
1404
|
-
if (!METHODS.includes(method)) {
|
|
1405
|
-
throw new Error(`Unknown method "${first}". Run bbx skill to see available methods.`);
|
|
1406
|
-
}
|
|
1407
|
-
let rawParams = second;
|
|
1408
|
-
// Support piped stdin: `echo '{"key":"val"}' | bbx call method -`
|
|
1409
|
-
if (rawParams === '-') {
|
|
1410
|
-
rawParams = await readStdin();
|
|
1411
|
-
}
|
|
1412
|
-
return {
|
|
1413
|
-
method,
|
|
1414
|
-
tabId: methodNeedsTab(method) ? parsed.tabId : null,
|
|
1415
|
-
params: parseJsonObject(rawParams),
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
/**
|
|
1423
|
-
* Extract the global --remote flag from anywhere in the argument list.
|
|
1424
|
-
* Exits with a usage error when the flag is present without a value.
|
|
1425
|
-
*
|
|
1426
|
-
* @param {string[]} args
|
|
1427
|
-
* @returns {{ remoteId: string | null, explicit: boolean, rest: string[] }}
|
|
1428
|
-
*/
|
|
1429
|
-
function extractRemoteFlag(args) {
|
|
1430
|
-
const rest = [...args];
|
|
1431
|
-
const index = rest.indexOf('--remote');
|
|
1432
|
-
if (index === -1) {
|
|
1433
|
-
return { remoteId: null, explicit: false, rest };
|
|
1434
|
-
}
|
|
1435
|
-
const remoteId = rest[index + 1];
|
|
1436
|
-
if (!remoteId || remoteId.startsWith('--')) {
|
|
1437
|
-
process.stderr.write('--remote requires a destination name (see `bbx remote list`).\n');
|
|
1438
|
-
process.exit(1);
|
|
1439
|
-
}
|
|
1440
|
-
rest.splice(index, 2);
|
|
1441
|
-
return { remoteId, explicit: true, rest };
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
/**
|
|
1445
|
-
* @param {string[]} args
|
|
1446
|
-
* @returns {{ tabId: number | null, rest: string[] }}
|
|
1447
|
-
*/
|
|
1448
|
-
function extractTabFlag(args) {
|
|
1449
|
-
const rest = [...args];
|
|
1450
|
-
let tabId = null;
|
|
1451
|
-
const tabIndex = rest.indexOf('--tab');
|
|
1452
|
-
if (tabIndex !== -1) {
|
|
1453
|
-
tabId = parseIntArg(rest[tabIndex + 1], 'tabId');
|
|
1454
|
-
rest.splice(tabIndex, 2);
|
|
1455
|
-
}
|
|
1456
|
-
return { tabId, rest };
|
|
1457
|
-
}
|