@browserbridge/bbx 1.7.5 → 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/cli-args.js +152 -0
- package/packages/agent-client/src/cli-batch.js +140 -0
- 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 +22 -1062
- package/packages/agent-client/src/runtime.js +1 -2
- 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 +17 -2
- package/packages/protocol/src/registry.js +299 -86
|
@@ -4,82 +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 { getBridgeAuthTokenPath } 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 {
|
|
21
|
-
import {
|
|
22
|
-
createRuntimeContext,
|
|
23
|
-
getProtocolVersion,
|
|
24
|
-
METHODS,
|
|
25
|
-
summarizeBatchErrorItem,
|
|
26
|
-
summarizeBatchResponseItem,
|
|
27
|
-
} from '../../protocol/src/index.js';
|
|
11
|
+
import { createRuntimeContext, METHODS } from '../../protocol/src/index.js';
|
|
28
12
|
import {
|
|
29
13
|
restartRegisteredMcpProcesses,
|
|
30
14
|
tryStartMcpProcessControl,
|
|
31
15
|
} from '../../mcp-server/src/lifecycle.js';
|
|
32
16
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
33
|
-
import { CLI_HELP_SECTIONS, SHORTCUT_COMMANDS } from './command-registry.js';
|
|
34
|
-
import { atomicWriteFile } from './atomic-write.js';
|
|
35
|
-
import {
|
|
36
|
-
interactiveCheckbox,
|
|
37
|
-
interactiveConfirm,
|
|
38
|
-
methodNeedsTab,
|
|
39
|
-
parseIntArg,
|
|
40
|
-
parseJsonObject,
|
|
41
|
-
sanitizeOutput,
|
|
42
|
-
} from './cli-helpers.js';
|
|
43
17
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} 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';
|
|
52
28
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
} 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';
|
|
60
35
|
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
61
|
-
import {
|
|
62
|
-
addRemoteDestination,
|
|
63
|
-
assertProxyBindSafety,
|
|
64
|
-
createBridgeClientForDestination,
|
|
65
|
-
listBridgeDestinations,
|
|
66
|
-
normalizeDestinationId,
|
|
67
|
-
parseRemoteEndpoint,
|
|
68
|
-
readRemoteConfig,
|
|
69
|
-
removeRemoteDestination,
|
|
70
|
-
resolveProxyEnableSettings,
|
|
71
|
-
isLoopbackHost,
|
|
72
|
-
} from './remotes.js';
|
|
73
|
-
import { collectSetupStatus } from './setup-status.js';
|
|
74
|
-
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
36
|
+
import { createBridgeClientForDestination } from './remotes.js';
|
|
75
37
|
|
|
76
38
|
/** @typedef {import('./types.js').BridgeMethod} BridgeMethod */
|
|
77
39
|
/** @typedef {import('./types.js').ScreenshotResult} ScreenshotResult */
|
|
78
40
|
|
|
79
41
|
const REQUEST_SOURCE = 'cli';
|
|
80
42
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
81
|
-
const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
|
|
82
|
-
const TEST_DETECTED_SKILL_TARGETS_ENV = 'BBX_TEST_DETECTED_SKILL_TARGETS';
|
|
83
43
|
const REMOTE_ENV = 'BBX_REMOTE';
|
|
84
44
|
|
|
85
45
|
/**
|
|
@@ -107,25 +67,6 @@ const LOCAL_ONLY_COMMANDS = new Set([
|
|
|
107
67
|
'restart',
|
|
108
68
|
]);
|
|
109
69
|
|
|
110
|
-
/**
|
|
111
|
-
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
112
|
-
*
|
|
113
|
-
* @returns {Promise<string>}
|
|
114
|
-
*/
|
|
115
|
-
function readStdin() {
|
|
116
|
-
return new Promise((resolve, reject) => {
|
|
117
|
-
const chunks = /** @type {Buffer[]} */ ([]);
|
|
118
|
-
process.stdin.setEncoding('utf8');
|
|
119
|
-
process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
120
|
-
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
|
|
121
|
-
process.stdin.on('error', reject);
|
|
122
|
-
// If stdin is a TTY and nothing is piped, read nothing
|
|
123
|
-
if (process.stdin.isTTY) {
|
|
124
|
-
resolve('');
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
70
|
const remoteFlag = extractRemoteFlag(process.argv.slice(2));
|
|
130
71
|
const [command, ...rest] = remoteFlag.rest;
|
|
131
72
|
|
|
@@ -163,7 +104,6 @@ if (command === 'skill') {
|
|
|
163
104
|
|
|
164
105
|
if (command === 'install') {
|
|
165
106
|
const { execFileSync } = await import('node:child_process');
|
|
166
|
-
const { fileURLToPath } = await import('node:url');
|
|
167
107
|
const installScript = path.resolve(
|
|
168
108
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
169
109
|
'../../native-host/bin/install-manifest.js'
|
|
@@ -185,242 +125,11 @@ if (command === 'uninstall') {
|
|
|
185
125
|
}
|
|
186
126
|
|
|
187
127
|
if (command === 'install-skill') {
|
|
188
|
-
|
|
189
|
-
if (!hasExplicitInstallSkillTarget(rest)) {
|
|
190
|
-
let scopeOptions;
|
|
191
|
-
try {
|
|
192
|
-
scopeOptions = parseInstallAgentArgs(rest);
|
|
193
|
-
} catch (error) {
|
|
194
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const isGlobal = scopeOptions.global !== false;
|
|
199
|
-
const projectPath = isGlobal ? os.homedir() : scopeOptions.projectPath;
|
|
200
|
-
|
|
201
|
-
const setupStatus = await collectSetupStatus({
|
|
202
|
-
global: isGlobal,
|
|
203
|
-
cwd: process.cwd(),
|
|
204
|
-
projectPath,
|
|
205
|
-
...getSetupStatusTestOverrides(),
|
|
206
|
-
});
|
|
207
|
-
/** @type {import('./types.js').SupportedTarget[]} */
|
|
208
|
-
const detected = /** @type {import('./types.js').SupportedTarget[]} */ (
|
|
209
|
-
setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
210
|
-
);
|
|
211
|
-
const installedManagedTargets = new Set(
|
|
212
|
-
setupStatus.skillTargets
|
|
213
|
-
.filter((entry) => entry.installed && entry.managed)
|
|
214
|
-
.map((entry) => entry.key)
|
|
215
|
-
);
|
|
216
|
-
const installedManagedTargetList =
|
|
217
|
-
/** @type {import('./types.js').SupportedTarget[]} */ ([...installedManagedTargets]);
|
|
218
|
-
|
|
219
|
-
// Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
|
|
220
|
-
const items = SUPPORTED_TARGETS.map((t) => ({
|
|
221
|
-
value: t,
|
|
222
|
-
label: `${t.padEnd(10)} ${TARGET_LABELS[t]}`,
|
|
223
|
-
hint: formatSelectionHint({
|
|
224
|
-
detected: detected.includes(t),
|
|
225
|
-
installed: installedManagedTargets.has(t),
|
|
226
|
-
}),
|
|
227
|
-
checked: installedManagedTargets.has(t),
|
|
228
|
-
}));
|
|
229
|
-
|
|
230
|
-
const selected = await interactiveCheckbox(
|
|
231
|
-
'Select agents to install skill for (↑↓ move · space toggle · a all · enter confirm)',
|
|
232
|
-
items
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
/** @type {import('./types.js').SupportedTarget[]} */
|
|
236
|
-
let targets;
|
|
237
|
-
if (selected === null) {
|
|
238
|
-
// Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
|
|
239
|
-
targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
|
|
240
|
-
} else {
|
|
241
|
-
targets = /** @type {import('./types.js').SupportedTarget[]} */ (selected);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (selected !== null) {
|
|
245
|
-
const deselectedTargets =
|
|
246
|
-
/** @type {import('./types.js').SupportedTarget[]} */ (
|
|
247
|
-
installedManagedTargetList.filter((target) => !targets.includes(target))
|
|
248
|
-
);
|
|
249
|
-
const removableTargets = await findInstalledManagedTargets({
|
|
250
|
-
targets: deselectedTargets,
|
|
251
|
-
projectPath,
|
|
252
|
-
global: isGlobal,
|
|
253
|
-
});
|
|
254
|
-
if (removableTargets.length > 0) {
|
|
255
|
-
const confirmed = await interactiveConfirm(
|
|
256
|
-
`Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}?`
|
|
257
|
-
);
|
|
258
|
-
if (confirmed) {
|
|
259
|
-
const removedPaths = await removeAgentFiles({
|
|
260
|
-
targets: removableTargets,
|
|
261
|
-
projectPath,
|
|
262
|
-
global: isGlobal,
|
|
263
|
-
});
|
|
264
|
-
for (const p of removedPaths) process.stdout.write(`Removed ${p}\n`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (targets.length === 0) {
|
|
270
|
-
process.stdout.write('No targets selected.\n');
|
|
271
|
-
process.exit(0);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const installedPaths = await installAgentFiles({
|
|
275
|
-
targets,
|
|
276
|
-
projectPath,
|
|
277
|
-
global: isGlobal,
|
|
278
|
-
});
|
|
279
|
-
for (const p of installedPaths) process.stdout.write(`Installed ${p}\n`);
|
|
280
|
-
process.exit(0);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Explicit targets or 'all' provided - use existing arg-parsing logic.
|
|
284
|
-
const options = parseInstallAgentArgs(rest);
|
|
285
|
-
const installedPaths = await installAgentFiles(options);
|
|
286
|
-
for (const installedPath of installedPaths) {
|
|
287
|
-
process.stdout.write(`Installed ${installedPath}\n`);
|
|
288
|
-
}
|
|
289
|
-
process.exit(0);
|
|
128
|
+
await runInstallSkillCommand(rest);
|
|
290
129
|
}
|
|
291
130
|
|
|
292
131
|
if (command === 'install-mcp') {
|
|
293
|
-
|
|
294
|
-
/** @type {string[]} */
|
|
295
|
-
const positionals = [];
|
|
296
|
-
|
|
297
|
-
for (const arg of rest) {
|
|
298
|
-
if (arg === '--local') {
|
|
299
|
-
isGlobal = false;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (arg === '--global') {
|
|
303
|
-
isGlobal = true;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (arg.startsWith('--')) {
|
|
307
|
-
process.stderr.write(`Unknown install-mcp option "${arg}".\n`);
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
positionals.push(arg);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (positionals.length > 1) {
|
|
314
|
-
process.stderr.write(`Unexpected extra argument "${positionals[1]}".\n`);
|
|
315
|
-
process.exit(1);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const clientArg = positionals[0];
|
|
319
|
-
|
|
320
|
-
/** @type {import('./types.js').McpClientName[]} */
|
|
321
|
-
let clients;
|
|
322
|
-
|
|
323
|
-
if (!clientArg) {
|
|
324
|
-
// No client specified: inspect current MCP config and prompt interactively.
|
|
325
|
-
const setupStatus = await collectSetupStatus({
|
|
326
|
-
global: isGlobal,
|
|
327
|
-
cwd: process.cwd(),
|
|
328
|
-
projectPath: process.cwd(),
|
|
329
|
-
...getSetupStatusTestOverrides(),
|
|
330
|
-
});
|
|
331
|
-
const detected = /** @type {import('./types.js').McpClientName[]} */ (
|
|
332
|
-
setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
333
|
-
);
|
|
334
|
-
const configuredClients = new Set(
|
|
335
|
-
setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
|
|
336
|
-
);
|
|
337
|
-
const configuredClientList =
|
|
338
|
-
/** @type {import('./types.js').McpClientName[]} */ ([...configuredClients]);
|
|
339
|
-
const items = MCP_CLIENT_NAMES.map((c) => ({
|
|
340
|
-
value: c,
|
|
341
|
-
label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
|
|
342
|
-
hint: formatSelectionHint({
|
|
343
|
-
detected: detected.includes(c),
|
|
344
|
-
installed: configuredClients.has(c),
|
|
345
|
-
}),
|
|
346
|
-
checked: configuredClients.has(c),
|
|
347
|
-
}));
|
|
348
|
-
|
|
349
|
-
const selected = await interactiveCheckbox(
|
|
350
|
-
'Select clients to configure (↑↓ move · space toggle · a all · enter confirm)',
|
|
351
|
-
items
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
if (selected === null) {
|
|
355
|
-
// Non-TTY: prefer configured clients, then detected clients, then all.
|
|
356
|
-
clients =
|
|
357
|
-
configuredClients.size > 0
|
|
358
|
-
? configuredClientList
|
|
359
|
-
: detected.length > 0
|
|
360
|
-
? detected
|
|
361
|
-
: [...MCP_CLIENT_NAMES];
|
|
362
|
-
} else {
|
|
363
|
-
clients = /** @type {import('./types.js').McpClientName[]} */ (selected);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (selected !== null) {
|
|
367
|
-
const deselectedClients =
|
|
368
|
-
/** @type {import('./types.js').McpClientName[]} */ (
|
|
369
|
-
configuredClientList.filter((clientName) => !clients.includes(clientName))
|
|
370
|
-
);
|
|
371
|
-
const removableClients = await findConfiguredMcpClients({
|
|
372
|
-
clients: deselectedClients,
|
|
373
|
-
global: isGlobal,
|
|
374
|
-
cwd: process.cwd(),
|
|
375
|
-
});
|
|
376
|
-
if (removableClients.length > 0) {
|
|
377
|
-
const confirmed = await interactiveConfirm(
|
|
378
|
-
`Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}?`
|
|
379
|
-
);
|
|
380
|
-
if (confirmed) {
|
|
381
|
-
for (const clientName of removableClients) {
|
|
382
|
-
await removeMcpConfig(clientName, {
|
|
383
|
-
global: isGlobal,
|
|
384
|
-
cwd: process.cwd(),
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (clients.length === 0) {
|
|
392
|
-
process.stdout.write('No clients selected.\n');
|
|
393
|
-
process.exit(0);
|
|
394
|
-
}
|
|
395
|
-
} else if (clientArg === 'all') {
|
|
396
|
-
clients = [...MCP_CLIENT_NAMES];
|
|
397
|
-
} else {
|
|
398
|
-
const parts = clientArg
|
|
399
|
-
.split(',')
|
|
400
|
-
.map((s) => s.trim().toLowerCase())
|
|
401
|
-
.filter(Boolean);
|
|
402
|
-
if (parts.includes('all')) {
|
|
403
|
-
clients = [...MCP_CLIENT_NAMES];
|
|
404
|
-
} else {
|
|
405
|
-
clients = [];
|
|
406
|
-
for (const part of parts) {
|
|
407
|
-
if (!isMcpClientName(part)) {
|
|
408
|
-
process.stderr.write(
|
|
409
|
-
`Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n`
|
|
410
|
-
);
|
|
411
|
-
process.exit(1);
|
|
412
|
-
}
|
|
413
|
-
clients.push(part);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
await installMcpClientSetup(clients, {
|
|
419
|
-
global: isGlobal,
|
|
420
|
-
projectPath: process.cwd(),
|
|
421
|
-
stdout: process.stdout,
|
|
422
|
-
});
|
|
423
|
-
process.exit(0);
|
|
132
|
+
await runInstallMcpCommand(rest);
|
|
424
133
|
}
|
|
425
134
|
|
|
426
135
|
if (command === 'mcp') {
|
|
@@ -633,130 +342,7 @@ async function main() {
|
|
|
633
342
|
}
|
|
634
343
|
|
|
635
344
|
if (command === 'batch') {
|
|
636
|
-
const
|
|
637
|
-
if (!input) {
|
|
638
|
-
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
639
|
-
}
|
|
640
|
-
let calls;
|
|
641
|
-
try {
|
|
642
|
-
calls = JSON.parse(input);
|
|
643
|
-
} catch {
|
|
644
|
-
throw new Error('Invalid JSON syntax. Expected a JSON array of bridge calls.');
|
|
645
|
-
}
|
|
646
|
-
if (!Array.isArray(calls)) {
|
|
647
|
-
throw new Error('Batch input must be a JSON array.');
|
|
648
|
-
}
|
|
649
|
-
await ensureClientConnection();
|
|
650
|
-
const results = await Promise.all(
|
|
651
|
-
calls.map(async (call) => {
|
|
652
|
-
if (!call || typeof call !== 'object' || Array.isArray(call)) {
|
|
653
|
-
return {
|
|
654
|
-
method: '',
|
|
655
|
-
tabId: null,
|
|
656
|
-
ok: false,
|
|
657
|
-
summary: 'INVALID_REQUEST: Each batch call needs a method.',
|
|
658
|
-
evidence: null,
|
|
659
|
-
durationMs: 0,
|
|
660
|
-
approxTokens: 0,
|
|
661
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
662
|
-
error: {
|
|
663
|
-
code: 'INVALID_REQUEST',
|
|
664
|
-
message: 'Each batch call needs a method.',
|
|
665
|
-
},
|
|
666
|
-
response: null,
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
const batchCall = /** @type {Record<string, unknown>} */ (call);
|
|
670
|
-
if (typeof batchCall.method !== 'string') {
|
|
671
|
-
return {
|
|
672
|
-
method: '',
|
|
673
|
-
tabId: null,
|
|
674
|
-
ok: false,
|
|
675
|
-
summary: 'INVALID_REQUEST: Each batch call needs a method.',
|
|
676
|
-
evidence: null,
|
|
677
|
-
durationMs: 0,
|
|
678
|
-
approxTokens: 0,
|
|
679
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
680
|
-
error: {
|
|
681
|
-
code: 'INVALID_REQUEST',
|
|
682
|
-
message: 'Each batch call needs a method.',
|
|
683
|
-
},
|
|
684
|
-
response: null,
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
if (!METHODS.includes(/** @type {BridgeMethod} */ (batchCall.method))) {
|
|
688
|
-
return {
|
|
689
|
-
method: batchCall.method,
|
|
690
|
-
tabId: null,
|
|
691
|
-
ok: false,
|
|
692
|
-
summary: `INVALID_REQUEST: Unknown bridge method "${batchCall.method}".`,
|
|
693
|
-
evidence: null,
|
|
694
|
-
durationMs: 0,
|
|
695
|
-
approxTokens: 0,
|
|
696
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
697
|
-
error: {
|
|
698
|
-
code: 'INVALID_REQUEST',
|
|
699
|
-
message: `Unknown bridge method "${batchCall.method}".`,
|
|
700
|
-
},
|
|
701
|
-
response: null,
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
const method = /** @type {BridgeMethod} */ (batchCall.method);
|
|
705
|
-
if (
|
|
706
|
-
batchCall.params !== undefined &&
|
|
707
|
-
(!batchCall.params ||
|
|
708
|
-
typeof batchCall.params !== 'object' ||
|
|
709
|
-
Array.isArray(batchCall.params))
|
|
710
|
-
) {
|
|
711
|
-
return {
|
|
712
|
-
method,
|
|
713
|
-
tabId: null,
|
|
714
|
-
ok: false,
|
|
715
|
-
summary: 'INVALID_REQUEST: Batch call params must be a JSON object.',
|
|
716
|
-
evidence: null,
|
|
717
|
-
durationMs: 0,
|
|
718
|
-
approxTokens: 0,
|
|
719
|
-
meta: { protocol_version: getProtocolVersion() },
|
|
720
|
-
error: {
|
|
721
|
-
code: 'INVALID_REQUEST',
|
|
722
|
-
message: 'Batch call params must be a JSON object.',
|
|
723
|
-
},
|
|
724
|
-
response: null,
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
const params =
|
|
728
|
-
batchCall.params === undefined
|
|
729
|
-
? {}
|
|
730
|
-
: /** @type {Record<string, unknown>} */ (batchCall.params);
|
|
731
|
-
const tabId =
|
|
732
|
-
methodNeedsTab(method) &&
|
|
733
|
-
typeof batchCall.tabId === 'number' &&
|
|
734
|
-
Number.isInteger(batchCall.tabId) &&
|
|
735
|
-
batchCall.tabId > 0
|
|
736
|
-
? batchCall.tabId
|
|
737
|
-
: null;
|
|
738
|
-
const startTime = Date.now();
|
|
739
|
-
try {
|
|
740
|
-
const response = await requestBridge(client, method, params, {
|
|
741
|
-
tabId,
|
|
742
|
-
source: REQUEST_SOURCE,
|
|
743
|
-
});
|
|
744
|
-
return summarizeBatchResponseItem({
|
|
745
|
-
method,
|
|
746
|
-
tabId,
|
|
747
|
-
response,
|
|
748
|
-
durationMs: Date.now() - startTime,
|
|
749
|
-
});
|
|
750
|
-
} catch (err) {
|
|
751
|
-
return summarizeBatchErrorItem({
|
|
752
|
-
method,
|
|
753
|
-
tabId,
|
|
754
|
-
error: err,
|
|
755
|
-
durationMs: Date.now() - startTime,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
})
|
|
759
|
-
);
|
|
345
|
+
const results = await runBatchCalls(client, rest[0], REQUEST_SOURCE);
|
|
760
346
|
if (results.some((result) => !result.ok)) {
|
|
761
347
|
process.exitCode = 1;
|
|
762
348
|
}
|
|
@@ -1000,408 +586,6 @@ async function main() {
|
|
|
1000
586
|
}
|
|
1001
587
|
}
|
|
1002
588
|
|
|
1003
|
-
/**
|
|
1004
|
-
* @param {string[]} args
|
|
1005
|
-
* @returns {Promise<void>}
|
|
1006
|
-
*/
|
|
1007
|
-
async function handleProxyCommand(args) {
|
|
1008
|
-
const [subcommand, ...restArgs] = args;
|
|
1009
|
-
if (subcommand === 'enable') {
|
|
1010
|
-
const options = parseProxyEnableArgs(restArgs);
|
|
1011
|
-
const existing = readProxyConfig();
|
|
1012
|
-
const { port, bindHost, token, tokenSource } = resolveProxyEnableSettings(
|
|
1013
|
-
existing,
|
|
1014
|
-
options,
|
|
1015
|
-
randomUUID
|
|
1016
|
-
);
|
|
1017
|
-
assertProxyBindSafety(existing, options, bindHost);
|
|
1018
|
-
await atomicWriteFile(getBridgeAuthTokenPath(), `${token}\n`, {
|
|
1019
|
-
encoding: 'utf8',
|
|
1020
|
-
mode: 0o600,
|
|
1021
|
-
});
|
|
1022
|
-
const configPath = getProxyConfigPath();
|
|
1023
|
-
await atomicWriteFile(
|
|
1024
|
-
configPath,
|
|
1025
|
-
`${JSON.stringify(
|
|
1026
|
-
{
|
|
1027
|
-
enabled: true,
|
|
1028
|
-
port,
|
|
1029
|
-
bindHost,
|
|
1030
|
-
token,
|
|
1031
|
-
},
|
|
1032
|
-
null,
|
|
1033
|
-
2
|
|
1034
|
-
)}\n`,
|
|
1035
|
-
{ encoding: 'utf8', mode: 0o600 }
|
|
1036
|
-
);
|
|
1037
|
-
const result = await restartBridgeDaemon();
|
|
1038
|
-
const tokenNote =
|
|
1039
|
-
tokenSource === 'existing'
|
|
1040
|
-
? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
|
|
1041
|
-
: tokenSource === 'generated' && existing
|
|
1042
|
-
? ' (rotated - update every configured client with `bbx remote add`)'
|
|
1043
|
-
: '';
|
|
1044
|
-
const lines = [
|
|
1045
|
-
`Browser Bridge proxy enabled on ${bindHost}:${port}.`,
|
|
1046
|
-
'',
|
|
1047
|
-
`Token: ${token}${tokenNote}`,
|
|
1048
|
-
`Config: ${configPath}`,
|
|
1049
|
-
`Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
1050
|
-
];
|
|
1051
|
-
if (tokenSource !== 'existing') {
|
|
1052
|
-
if (isLoopbackHost(bindHost)) {
|
|
1053
|
-
lines.push(
|
|
1054
|
-
'',
|
|
1055
|
-
'On your dev machine, open an SSH local-forward:',
|
|
1056
|
-
'',
|
|
1057
|
-
`ssh -N -L ${port}:127.0.0.1:${port} <user>@<browser-host>`,
|
|
1058
|
-
'',
|
|
1059
|
-
'Save the token to a private file, then add the tunneled remote:',
|
|
1060
|
-
'',
|
|
1061
|
-
`bbx remote add remote-bbx 127.0.0.1:${port} --token-file <token-file>`
|
|
1062
|
-
);
|
|
1063
|
-
} else {
|
|
1064
|
-
lines.push(
|
|
1065
|
-
'',
|
|
1066
|
-
'WARNING: raw TCP is exposed without transport encryption.',
|
|
1067
|
-
'On your dev machine, add this direct remote:',
|
|
1068
|
-
'',
|
|
1069
|
-
`bbx remote add remote-bbx ${getProxyExampleHost(bindHost)}:${port} --token-file <token-file>`
|
|
1070
|
-
);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
lines.push('');
|
|
1074
|
-
process.stdout.write(lines.join('\n'));
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (subcommand === 'disable') {
|
|
1079
|
-
const configPath = getProxyConfigPath();
|
|
1080
|
-
await fs.promises.rm(configPath, { force: true });
|
|
1081
|
-
const result = await restartBridgeDaemon();
|
|
1082
|
-
process.stdout.write(
|
|
1083
|
-
`Browser Bridge proxy disabled. Daemon ${result.previouslyRunning ? 'restarted' : 'started'}.\n`
|
|
1084
|
-
);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
if (subcommand === 'status') {
|
|
1089
|
-
const config = readProxyConfig();
|
|
1090
|
-
if (!config) {
|
|
1091
|
-
process.stdout.write('Browser Bridge proxy is disabled.\n');
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
const reachable = await pingExistingDaemon(createTcpBridgeTransport(config.port, '127.0.0.1'));
|
|
1095
|
-
process.stdout.write(
|
|
1096
|
-
[
|
|
1097
|
-
`Browser Bridge proxy is enabled on ${config.bindHost}:${config.port}.`,
|
|
1098
|
-
`Config: ${getProxyConfigPath()}`,
|
|
1099
|
-
`Daemon: ${reachable ? 'reachable' : 'not reachable'} on 127.0.0.1:${config.port}`,
|
|
1100
|
-
'',
|
|
1101
|
-
].join('\n')
|
|
1102
|
-
);
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
throw new Error('Usage: bbx proxy <enable|disable|status>');
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
/**
|
|
1110
|
-
* Parse `bbx proxy enable` flags. Values stay undefined when not passed so an
|
|
1111
|
-
* existing proxy config can be reused instead of silently reset to defaults.
|
|
1112
|
-
*
|
|
1113
|
-
* @param {string[]} args
|
|
1114
|
-
* @returns {import('./remotes.js').ProxyEnableOptions}
|
|
1115
|
-
*/
|
|
1116
|
-
function parseProxyEnableArgs(args) {
|
|
1117
|
-
/** @type {import('./remotes.js').ProxyEnableOptions} */
|
|
1118
|
-
const options = {};
|
|
1119
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1120
|
-
const arg = args[index];
|
|
1121
|
-
if (arg === '--port') {
|
|
1122
|
-
options.port = parseIntArg(args[++index] || '', 'port');
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
if (arg === '--bind-host') {
|
|
1126
|
-
options.bindHost = args[++index] || '';
|
|
1127
|
-
if (!options.bindHost.trim()) {
|
|
1128
|
-
throw new Error('--bind-host requires a value.');
|
|
1129
|
-
}
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
if (arg === '--token') {
|
|
1133
|
-
options.token = args[++index] || '';
|
|
1134
|
-
if (!options.token.trim()) {
|
|
1135
|
-
throw new Error('--token requires a value.');
|
|
1136
|
-
}
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
if (arg === '--rotate-token') {
|
|
1140
|
-
options.rotateToken = true;
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
|
-
if (arg === '--unsafe-plaintext') {
|
|
1144
|
-
options.unsafePlaintext = true;
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
throw new Error(`Unknown proxy enable option "${arg}".`);
|
|
1148
|
-
}
|
|
1149
|
-
if (options.token && options.rotateToken) {
|
|
1150
|
-
throw new Error('Use either --token or --rotate-token, not both.');
|
|
1151
|
-
}
|
|
1152
|
-
if (options.port !== undefined && (options.port < 1 || options.port > 65535)) {
|
|
1153
|
-
throw new Error('port must be an integer between 1 and 65535.');
|
|
1154
|
-
}
|
|
1155
|
-
return options;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
/**
|
|
1159
|
-
* @param {string} bindHost
|
|
1160
|
-
* @returns {string}
|
|
1161
|
-
*/
|
|
1162
|
-
function getProxyExampleHost(bindHost) {
|
|
1163
|
-
if (bindHost !== '0.0.0.0' && bindHost !== '::') {
|
|
1164
|
-
return bindHost;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
/** @type {string | null} */
|
|
1168
|
-
let firstIpv4 = null;
|
|
1169
|
-
for (const [name, entries] of Object.entries(os.networkInterfaces())) {
|
|
1170
|
-
for (const entry of entries ?? []) {
|
|
1171
|
-
if (!entry.internal && entry.family === 'IPv4') {
|
|
1172
|
-
if (name.toLowerCase() === 'ethernet 0') {
|
|
1173
|
-
return entry.address;
|
|
1174
|
-
}
|
|
1175
|
-
firstIpv4 ??= entry.address;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
return firstIpv4 ?? '<host>';
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
/**
|
|
1184
|
-
* @param {string[]} args
|
|
1185
|
-
* @returns {Promise<void>}
|
|
1186
|
-
*/
|
|
1187
|
-
async function handleRemoteCommand(args) {
|
|
1188
|
-
const [subcommand, ...restArgs] = args;
|
|
1189
|
-
if (subcommand === 'add') {
|
|
1190
|
-
const [name, endpoint, ...optionArgs] = restArgs;
|
|
1191
|
-
if (!name || !endpoint) {
|
|
1192
|
-
throw new Error(
|
|
1193
|
-
'Usage: bbx remote add <name> <host:port> (--token <token>|--token-file <path>)'
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
const token = await parseRemoteTokenOption(optionArgs);
|
|
1197
|
-
const { host, port } = parseRemoteEndpoint(endpoint);
|
|
1198
|
-
const remote = await addRemoteDestination({
|
|
1199
|
-
id: normalizeDestinationId(name),
|
|
1200
|
-
host,
|
|
1201
|
-
port,
|
|
1202
|
-
token,
|
|
1203
|
-
});
|
|
1204
|
-
process.stdout.write(
|
|
1205
|
-
`Remote destination "${remote.id}" saved (${remote.host}:${remote.port}).\n`
|
|
1206
|
-
);
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (subcommand === 'remove') {
|
|
1211
|
-
const [name] = restArgs;
|
|
1212
|
-
if (!name) {
|
|
1213
|
-
throw new Error('Usage: bbx remote remove <name>');
|
|
1214
|
-
}
|
|
1215
|
-
const removed = await removeRemoteDestination(name);
|
|
1216
|
-
process.stdout.write(
|
|
1217
|
-
removed
|
|
1218
|
-
? `Remote destination "${name}" removed.\n`
|
|
1219
|
-
: `Remote destination "${name}" was not configured.\n`
|
|
1220
|
-
);
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
if (subcommand === 'list') {
|
|
1225
|
-
const config = await readRemoteConfig();
|
|
1226
|
-
if (config.remotes.length === 0) {
|
|
1227
|
-
process.stdout.write('No remote destinations configured.\n');
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
process.stdout.write(
|
|
1231
|
-
`${config.remotes.map((remote) => `${remote.id}\t${remote.host}:${remote.port}`).join('\n')}\n`
|
|
1232
|
-
);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (subcommand === 'test') {
|
|
1237
|
-
const [name] = restArgs;
|
|
1238
|
-
if (!name) {
|
|
1239
|
-
throw new Error('Usage: bbx remote test <name>');
|
|
1240
|
-
}
|
|
1241
|
-
const remoteClient = await createBridgeClientForDestination(name);
|
|
1242
|
-
try {
|
|
1243
|
-
await remoteClient.connect();
|
|
1244
|
-
const response = await remoteClient.request({ method: 'health.ping' });
|
|
1245
|
-
if (response.ok) {
|
|
1246
|
-
const health = /** @type {{ extensionConnected?: boolean }} */ (response.result);
|
|
1247
|
-
process.stdout.write(
|
|
1248
|
-
`Remote destination "${name}" is reachable (extension ${
|
|
1249
|
-
health.extensionConnected === true ? 'connected' : 'not connected'
|
|
1250
|
-
}).\n`
|
|
1251
|
-
);
|
|
1252
|
-
} else {
|
|
1253
|
-
process.exitCode = 1;
|
|
1254
|
-
process.stdout.write(`Remote destination "${name}" failed: ${response.error.message}\n`);
|
|
1255
|
-
}
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
process.exitCode = 1;
|
|
1258
|
-
process.stdout.write(
|
|
1259
|
-
`Remote destination "${name}" is not reachable: ${
|
|
1260
|
-
error instanceof Error ? error.message : String(error)
|
|
1261
|
-
}\n`
|
|
1262
|
-
);
|
|
1263
|
-
} finally {
|
|
1264
|
-
await remoteClient.close();
|
|
1265
|
-
}
|
|
1266
|
-
return;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (subcommand === 'destinations') {
|
|
1270
|
-
const destinations = await listBridgeDestinations();
|
|
1271
|
-
process.stdout.write(
|
|
1272
|
-
`${destinations
|
|
1273
|
-
.map((destination) =>
|
|
1274
|
-
destination.local
|
|
1275
|
-
? `${destination.id}\tlocal`
|
|
1276
|
-
: `${destination.id}\t${destination.host}:${destination.port}`
|
|
1277
|
-
)
|
|
1278
|
-
.join('\n')}\n`
|
|
1279
|
-
);
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
throw new Error('Usage: bbx remote <add|remove|list|test|destinations>');
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
/**
|
|
1287
|
-
* @param {string[]} args
|
|
1288
|
-
* @returns {Promise<string>}
|
|
1289
|
-
*/
|
|
1290
|
-
async function parseRemoteTokenOption(args) {
|
|
1291
|
-
let token = '';
|
|
1292
|
-
let tokenFile = '';
|
|
1293
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1294
|
-
const arg = args[index];
|
|
1295
|
-
if (arg === '--token') {
|
|
1296
|
-
const value = args[++index];
|
|
1297
|
-
if (!value || value.startsWith('--')) {
|
|
1298
|
-
throw new Error('--token requires a value.');
|
|
1299
|
-
}
|
|
1300
|
-
token = value;
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
if (arg === '--token-file') {
|
|
1304
|
-
const value = args[++index];
|
|
1305
|
-
if (!value || value.startsWith('--')) {
|
|
1306
|
-
throw new Error('--token-file requires a path.');
|
|
1307
|
-
}
|
|
1308
|
-
tokenFile = value;
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
throw new Error(`Unknown remote add option "${arg}".`);
|
|
1312
|
-
}
|
|
1313
|
-
if ((token && tokenFile) || (!token.trim() && !tokenFile.trim())) {
|
|
1314
|
-
throw new Error(
|
|
1315
|
-
'Usage: bbx remote add <name> <host:port> (--token <token>|--token-file <path>)'
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
return tokenFile ? (await fs.promises.readFile(tokenFile, 'utf8')).trim() : token.trim();
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Determine whether install-skill includes an explicit target without treating
|
|
1323
|
-
* option values such as `--project <path>` as positional targets.
|
|
1324
|
-
*
|
|
1325
|
-
* @param {string[]} args
|
|
1326
|
-
* @returns {boolean}
|
|
1327
|
-
*/
|
|
1328
|
-
function hasExplicitInstallSkillTarget(args) {
|
|
1329
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
1330
|
-
const arg = args[index];
|
|
1331
|
-
if (arg === '--project') {
|
|
1332
|
-
index += 1;
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
if (arg === '--agents' || arg === '--agent') {
|
|
1336
|
-
return true;
|
|
1337
|
-
}
|
|
1338
|
-
if (arg.startsWith('--agents=') || arg.startsWith('--agent=')) {
|
|
1339
|
-
return true;
|
|
1340
|
-
}
|
|
1341
|
-
if (!arg.startsWith('--')) {
|
|
1342
|
-
return true;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return false;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* @param {string[]} args
|
|
1350
|
-
* @returns {{ pattern: string, isBlock: boolean, statusCode: number, body: string | undefined }}
|
|
1351
|
-
*/
|
|
1352
|
-
function parseInterceptAddArgs(args) {
|
|
1353
|
-
const [pattern, ...optionArgs] = args;
|
|
1354
|
-
if (!pattern || pattern.startsWith('--')) {
|
|
1355
|
-
throw new Error(
|
|
1356
|
-
'Usage: intercept add <urlPattern> [--respond <body>] [--status <code>] [--block]'
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
let isBlock = false;
|
|
1361
|
-
let statusCode = 200;
|
|
1362
|
-
let hasStatus = false;
|
|
1363
|
-
let body;
|
|
1364
|
-
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
1365
|
-
const option = optionArgs[index];
|
|
1366
|
-
if (option === '--block') {
|
|
1367
|
-
if (isBlock) {
|
|
1368
|
-
throw new Error('The --block option may only be specified once.');
|
|
1369
|
-
}
|
|
1370
|
-
isBlock = true;
|
|
1371
|
-
continue;
|
|
1372
|
-
}
|
|
1373
|
-
if (option === '--respond') {
|
|
1374
|
-
const value = optionArgs[++index];
|
|
1375
|
-
if (body !== undefined) {
|
|
1376
|
-
throw new Error('The --respond option may only be specified once.');
|
|
1377
|
-
}
|
|
1378
|
-
if (value === undefined || value.startsWith('--')) {
|
|
1379
|
-
throw new Error('--respond requires a body value.');
|
|
1380
|
-
}
|
|
1381
|
-
body = value;
|
|
1382
|
-
continue;
|
|
1383
|
-
}
|
|
1384
|
-
if (option === '--status') {
|
|
1385
|
-
if (hasStatus) {
|
|
1386
|
-
throw new Error('The --status option may only be specified once.');
|
|
1387
|
-
}
|
|
1388
|
-
const value = optionArgs[++index];
|
|
1389
|
-
statusCode = parseIntArg(value, 'status');
|
|
1390
|
-
if (statusCode < 100 || statusCode > 599) {
|
|
1391
|
-
throw new Error('status must be an integer between 100 and 599.');
|
|
1392
|
-
}
|
|
1393
|
-
hasStatus = true;
|
|
1394
|
-
continue;
|
|
1395
|
-
}
|
|
1396
|
-
throw new Error(`Unknown or extra intercept add option "${option}".`);
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
if (isBlock && body !== undefined) {
|
|
1400
|
-
throw new Error('Use either --block or --respond, not both.');
|
|
1401
|
-
}
|
|
1402
|
-
return { pattern, isBlock, statusCode, body };
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
589
|
/**
|
|
1406
590
|
* @param {unknown} error
|
|
1407
591
|
* @returns {string}
|
|
@@ -1428,227 +612,3 @@ function getClientTimeoutOverride() {
|
|
|
1428
612
|
const value = Number.parseInt(raw, 10);
|
|
1429
613
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1430
614
|
}
|
|
1431
|
-
|
|
1432
|
-
/**
|
|
1433
|
-
* Allow CLI tests to provide deterministic MCP client detection without relying
|
|
1434
|
-
* on whatever tools happen to be installed on the host machine.
|
|
1435
|
-
*
|
|
1436
|
-
* @returns {{
|
|
1437
|
-
* mcpDetectors?: Record<string, () => boolean>,
|
|
1438
|
-
* skillDetectors?: Record<string, () => boolean>,
|
|
1439
|
-
* }}
|
|
1440
|
-
*/
|
|
1441
|
-
function getSetupStatusTestOverrides() {
|
|
1442
|
-
/** @type {{ mcpDetectors?: Record<string, () => boolean>, skillDetectors?: Record<string, () => boolean> }} */
|
|
1443
|
-
const overrides = {};
|
|
1444
|
-
if (TEST_DETECTED_MCP_CLIENTS_ENV in process.env) {
|
|
1445
|
-
const detectedClients = new Set(
|
|
1446
|
-
(process.env[TEST_DETECTED_MCP_CLIENTS_ENV] || '')
|
|
1447
|
-
.split(',')
|
|
1448
|
-
.map((value) => value.trim().toLowerCase())
|
|
1449
|
-
.filter(Boolean)
|
|
1450
|
-
);
|
|
1451
|
-
overrides.mcpDetectors = Object.fromEntries(
|
|
1452
|
-
MCP_CLIENT_NAMES.map((clientName) => [clientName, () => detectedClients.has(clientName)])
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
if (TEST_DETECTED_SKILL_TARGETS_ENV in process.env) {
|
|
1457
|
-
const detectedTargets = new Set(
|
|
1458
|
-
(process.env[TEST_DETECTED_SKILL_TARGETS_ENV] || '')
|
|
1459
|
-
.split(',')
|
|
1460
|
-
.map((value) => value.trim().toLowerCase())
|
|
1461
|
-
.filter(Boolean)
|
|
1462
|
-
);
|
|
1463
|
-
overrides.skillDetectors = Object.fromEntries(
|
|
1464
|
-
SUPPORTED_TARGETS.map((target) => [target, () => detectedTargets.has(target)])
|
|
1465
|
-
);
|
|
1466
|
-
}
|
|
1467
|
-
return overrides;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
/**
|
|
1471
|
-
* @param {{ detected: boolean, installed: boolean }} options
|
|
1472
|
-
* @returns {string | undefined}
|
|
1473
|
-
*/
|
|
1474
|
-
function formatSelectionHint(options) {
|
|
1475
|
-
/** @type {string[]} */
|
|
1476
|
-
const parts = [];
|
|
1477
|
-
if (options.detected) {
|
|
1478
|
-
parts.push('● detected');
|
|
1479
|
-
}
|
|
1480
|
-
if (options.installed) {
|
|
1481
|
-
parts.push('installed');
|
|
1482
|
-
}
|
|
1483
|
-
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
/**
|
|
1487
|
-
* @returns {Promise<void>}
|
|
1488
|
-
*/
|
|
1489
|
-
async function ensureClientConnection() {
|
|
1490
|
-
if (!client.connected) {
|
|
1491
|
-
await client.connect();
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
/**
|
|
1496
|
-
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
1497
|
-
* @param {string} [method] - Optional method name for disambiguation
|
|
1498
|
-
* @returns {Promise<void>}
|
|
1499
|
-
*/
|
|
1500
|
-
async function printSummary(response, method) {
|
|
1501
|
-
if (!response.ok) {
|
|
1502
|
-
process.exitCode = 1;
|
|
1503
|
-
}
|
|
1504
|
-
printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
/**
|
|
1508
|
-
* @param {unknown} value
|
|
1509
|
-
* @returns {void}
|
|
1510
|
-
*/
|
|
1511
|
-
function printJson(value) {
|
|
1512
|
-
process.stdout.write(
|
|
1513
|
-
`${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
/**
|
|
1518
|
-
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
1519
|
-
* @returns {void}
|
|
1520
|
-
*/
|
|
1521
|
-
function printCallResponse(response) {
|
|
1522
|
-
if (response.ok) {
|
|
1523
|
-
printJson(response.result);
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
process.exitCode = 1;
|
|
1528
|
-
const errorText = `${response.error.code}: ${response.error.message}`;
|
|
1529
|
-
process.stderr.write(
|
|
1530
|
-
`${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
|
|
1531
|
-
);
|
|
1532
|
-
printJson(response);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function printUsage() {
|
|
1536
|
-
const blocks = ['Usage: bbx [--remote <name>] <command> [args]'];
|
|
1537
|
-
for (const section of CLI_HELP_SECTIONS) {
|
|
1538
|
-
blocks.push('', `${section.title}:`);
|
|
1539
|
-
blocks.push(...section.lines.map((line) => ` ${line}`));
|
|
1540
|
-
}
|
|
1541
|
-
process.stdout.write(`${blocks.join('\n')}\n`);
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
/**
|
|
1545
|
-
* @returns {Promise<void>}
|
|
1546
|
-
*/
|
|
1547
|
-
async function uninstallBrowserBridge() {
|
|
1548
|
-
const cwd = process.cwd();
|
|
1549
|
-
const globalProjectPath = os.homedir();
|
|
1550
|
-
|
|
1551
|
-
for (const clientName of MCP_CLIENT_NAMES) {
|
|
1552
|
-
await removeMcpConfig(clientName, { global: true, cwd });
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
const removedGlobalSkillPaths = await removeAgentFiles({
|
|
1556
|
-
targets: SUPPORTED_TARGETS,
|
|
1557
|
-
projectPath: globalProjectPath,
|
|
1558
|
-
global: true,
|
|
1559
|
-
});
|
|
1560
|
-
for (const removedPath of removedGlobalSkillPaths) {
|
|
1561
|
-
process.stdout.write(`Removed ${removedPath}\n`);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
for (const clientName of MCP_CLIENT_NAMES) {
|
|
1565
|
-
await removeMcpConfig(clientName, { global: false, cwd });
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
const removedLocalSkillPaths = await removeAgentFiles({
|
|
1569
|
-
targets: SUPPORTED_TARGETS,
|
|
1570
|
-
projectPath: cwd,
|
|
1571
|
-
global: false,
|
|
1572
|
-
});
|
|
1573
|
-
for (const removedPath of removedLocalSkillPaths) {
|
|
1574
|
-
process.stdout.write(`Removed ${removedPath}\n`);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
for (const [index, browser] of SUPPORTED_BROWSERS.entries()) {
|
|
1578
|
-
await uninstallNativeManifest({
|
|
1579
|
-
browser,
|
|
1580
|
-
removeBridgeDir: index === SUPPORTED_BROWSERS.length - 1,
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
/**
|
|
1586
|
-
* @param {string[]} args
|
|
1587
|
-
* @returns {Promise<{ tabId: number | null, method: BridgeMethod, params: Record<string, unknown> }>}
|
|
1588
|
-
*/
|
|
1589
|
-
async function parseCallCommand(args) {
|
|
1590
|
-
const parsed = extractTabFlag(args);
|
|
1591
|
-
const [first, second, ...extra] = parsed.rest;
|
|
1592
|
-
if (!first) {
|
|
1593
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1594
|
-
}
|
|
1595
|
-
if (extra.length > 0) {
|
|
1596
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
if (first.includes('.')) {
|
|
1600
|
-
const method = /** @type {BridgeMethod} */ (first);
|
|
1601
|
-
if (!METHODS.includes(method)) {
|
|
1602
|
-
throw new Error(`Unknown method "${first}". Run bbx skill to see available methods.`);
|
|
1603
|
-
}
|
|
1604
|
-
let rawParams = second;
|
|
1605
|
-
// Support piped stdin: `echo '{"key":"val"}' | bbx call method -`
|
|
1606
|
-
if (rawParams === '-') {
|
|
1607
|
-
rawParams = await readStdin();
|
|
1608
|
-
}
|
|
1609
|
-
return {
|
|
1610
|
-
method,
|
|
1611
|
-
tabId: methodNeedsTab(method) ? parsed.tabId : null,
|
|
1612
|
-
params: parseJsonObject(rawParams),
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
throw new Error('Usage: call [--tab <tabId>] <method> [paramsJson]');
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
/**
|
|
1620
|
-
* Extract the global --remote flag from anywhere in the argument list.
|
|
1621
|
-
* Exits with a usage error when the flag is present without a value.
|
|
1622
|
-
*
|
|
1623
|
-
* @param {string[]} args
|
|
1624
|
-
* @returns {{ remoteId: string | null, explicit: boolean, rest: string[] }}
|
|
1625
|
-
*/
|
|
1626
|
-
function extractRemoteFlag(args) {
|
|
1627
|
-
const rest = [...args];
|
|
1628
|
-
const index = rest.indexOf('--remote');
|
|
1629
|
-
if (index === -1) {
|
|
1630
|
-
return { remoteId: null, explicit: false, rest };
|
|
1631
|
-
}
|
|
1632
|
-
const remoteId = rest[index + 1];
|
|
1633
|
-
if (!remoteId || remoteId.startsWith('--')) {
|
|
1634
|
-
process.stderr.write('--remote requires a destination name (see `bbx remote list`).\n');
|
|
1635
|
-
process.exit(1);
|
|
1636
|
-
}
|
|
1637
|
-
rest.splice(index, 2);
|
|
1638
|
-
return { remoteId, explicit: true, rest };
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
/**
|
|
1642
|
-
* @param {string[]} args
|
|
1643
|
-
* @returns {{ tabId: number | null, rest: string[] }}
|
|
1644
|
-
*/
|
|
1645
|
-
function extractTabFlag(args) {
|
|
1646
|
-
const rest = [...args];
|
|
1647
|
-
let tabId = null;
|
|
1648
|
-
const tabIndex = rest.indexOf('--tab');
|
|
1649
|
-
if (tabIndex !== -1) {
|
|
1650
|
-
tabId = parseIntArg(rest[tabIndex + 1], 'tabId');
|
|
1651
|
-
rest.splice(tabIndex, 2);
|
|
1652
|
-
}
|
|
1653
|
-
return { tabId, rest };
|
|
1654
|
-
}
|