@browserbridge/bbx 1.0.0 → 1.1.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/README.md +6 -4
- package/package.json +53 -53
- package/packages/agent-client/src/cli-helpers.js +43 -5
- package/packages/agent-client/src/cli.js +176 -171
- package/packages/agent-client/src/client.js +66 -21
- package/packages/agent-client/src/command-registry.js +104 -69
- package/packages/agent-client/src/detect.js +162 -54
- package/packages/agent-client/src/install.js +34 -28
- package/packages/agent-client/src/mcp-config.js +40 -40
- package/packages/agent-client/src/runtime.js +41 -20
- package/packages/agent-client/src/setup-status.js +23 -30
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +573 -256
- package/packages/mcp-server/src/server.js +568 -257
- package/packages/native-host/bin/bridge-daemon.js +39 -6
- package/packages/native-host/bin/install-manifest.js +26 -4
- package/packages/native-host/bin/postinstall.js +4 -2
- package/packages/native-host/src/config.js +142 -13
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +350 -150
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +194 -29
- package/packages/native-host/src/native-host.js +154 -102
- package/packages/protocol/src/budget.js +3 -7
- package/packages/protocol/src/capabilities.js +6 -3
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +15 -11
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +242 -73
- package/packages/protocol/src/registry.js +311 -45
- package/packages/protocol/src/summary.js +260 -109
- package/packages/protocol/src/types.js +29 -4
- package/skills/browser-bridge/SKILL.md +3 -2
- package/skills/browser-bridge/agents/openai.yaml +3 -3
- package/skills/browser-bridge/references/interaction.md +34 -11
- package/skills/browser-bridge/references/patch-workflow.md +3 -0
- package/skills/browser-bridge/references/protocol.md +127 -71
- package/skills/browser-bridge/references/tailwind.md +12 -11
- package/skills/browser-bridge/references/token-efficiency.md +23 -22
- package/skills/browser-bridge/references/ui-workflows.md +8 -0
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -114
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -52
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -459
- package/packages/extension/src/background-routing.js +0 -91
- package/packages/extension/src/background.js +0 -3227
- package/packages/extension/src/content-script-helpers.js +0 -281
- package/packages/extension/src/content-script.js +0 -1977
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -102
- package/packages/extension/ui/offscreen.html +0 -6
- package/packages/extension/ui/offscreen.js +0 -61
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -279
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1854
- package/packages/extension/ui/ui.css +0 -1159
|
@@ -6,7 +6,11 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
applyWindowsTcpTransportDefaults,
|
|
11
|
+
SUPPORTED_BROWSERS,
|
|
12
|
+
} from '../../native-host/src/config.js';
|
|
13
|
+
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
10
14
|
import { uninstallNativeManifest } from '../../native-host/src/install-manifest.js';
|
|
11
15
|
import {
|
|
12
16
|
createRuntimeContext,
|
|
@@ -23,8 +27,8 @@ import {
|
|
|
23
27
|
methodNeedsTab,
|
|
24
28
|
parseIntArg,
|
|
25
29
|
parseJsonObject,
|
|
30
|
+
sanitizeOutput,
|
|
26
31
|
} from './cli-helpers.js';
|
|
27
|
-
import { detectMcpClients, detectSkillTargets } from './detect.js';
|
|
28
32
|
import {
|
|
29
33
|
findInstalledManagedTargets,
|
|
30
34
|
installAgentFiles,
|
|
@@ -42,51 +46,16 @@ import {
|
|
|
42
46
|
MCP_CLIENT_NAMES,
|
|
43
47
|
removeMcpConfig,
|
|
44
48
|
} from './mcp-config.js';
|
|
45
|
-
import {
|
|
46
|
-
getDoctorReport,
|
|
47
|
-
requestBridge,
|
|
48
|
-
resolveRef,
|
|
49
|
-
} from './runtime.js';
|
|
49
|
+
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
50
50
|
import { collectSetupStatus } from './setup-status.js';
|
|
51
|
-
import {
|
|
52
|
-
annotateBridgeSummary,
|
|
53
|
-
summarizeBridgeResponse,
|
|
54
|
-
} from './subagent.js';
|
|
51
|
+
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
55
52
|
|
|
56
53
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
57
54
|
/** @typedef {{ image: string, rect: Record<string, unknown> }} ScreenshotResult */
|
|
58
55
|
|
|
59
56
|
const REQUEST_SOURCE = 'cli';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* Strip ANSI escape sequences from a string to prevent terminal injection
|
|
63
|
-
* from malicious page content (e.g. DOM text, console output, eval results).
|
|
64
|
-
*
|
|
65
|
-
* @param {string} str
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
function stripAnsi(str) {
|
|
69
|
-
// eslint-disable-next-line no-control-regex
|
|
70
|
-
return str.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').replace(/\x1b[^[]/g, '');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Recursively sanitize all string values in a value tree by stripping ANSI
|
|
75
|
-
* escape sequences. Only strings are touched; structure is preserved.
|
|
76
|
-
*
|
|
77
|
-
* @param {unknown} value
|
|
78
|
-
* @returns {unknown}
|
|
79
|
-
*/
|
|
80
|
-
function sanitizeOutput(value) {
|
|
81
|
-
if (typeof value === 'string') return stripAnsi(value);
|
|
82
|
-
if (Array.isArray(value)) return value.map(sanitizeOutput);
|
|
83
|
-
if (value !== null && typeof value === 'object') {
|
|
84
|
-
return Object.fromEntries(
|
|
85
|
-
Object.entries(/** @type {Record<string, unknown>} */ (value)).map(([k, v]) => [k, sanitizeOutput(v)])
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
57
|
+
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
58
|
+
const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
|
|
90
59
|
|
|
91
60
|
/**
|
|
92
61
|
* Read all of stdin as UTF-8 text. Resolves once stdin closes.
|
|
@@ -98,9 +67,7 @@ function readStdin() {
|
|
|
98
67
|
const chunks = /** @type {Buffer[]} */ ([]);
|
|
99
68
|
process.stdin.setEncoding('utf8');
|
|
100
69
|
process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
101
|
-
process.stdin.on('end', () =>
|
|
102
|
-
resolve(Buffer.concat(chunks).toString('utf8').trim()),
|
|
103
|
-
);
|
|
70
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8').trim()));
|
|
104
71
|
process.stdin.on('error', reject);
|
|
105
72
|
// If stdin is a TTY and nothing is piped, read nothing
|
|
106
73
|
if (process.stdin.isTTY) {
|
|
@@ -117,7 +84,10 @@ if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
|
117
84
|
}
|
|
118
85
|
|
|
119
86
|
if (['--version', '-v'].includes(command)) {
|
|
120
|
-
const pkgPath = path.resolve(
|
|
87
|
+
const pkgPath = path.resolve(
|
|
88
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
89
|
+
'../../../package.json'
|
|
90
|
+
);
|
|
121
91
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
122
92
|
process.stdout.write(`${pkg.version}\n`);
|
|
123
93
|
process.exit(0);
|
|
@@ -125,7 +95,7 @@ if (['--version', '-v'].includes(command)) {
|
|
|
125
95
|
|
|
126
96
|
if (command === 'skill') {
|
|
127
97
|
process.stdout.write(
|
|
128
|
-
`${JSON.stringify(createRuntimeContext(), null, process.stdout.isTTY ? 2 : undefined)}\n
|
|
98
|
+
`${JSON.stringify(createRuntimeContext(), null, process.stdout.isTTY ? 2 : undefined)}\n`
|
|
129
99
|
);
|
|
130
100
|
process.exit(0);
|
|
131
101
|
}
|
|
@@ -135,7 +105,7 @@ if (command === 'install') {
|
|
|
135
105
|
const { fileURLToPath } = await import('node:url');
|
|
136
106
|
const installScript = path.resolve(
|
|
137
107
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
138
|
-
'../../native-host/bin/install-manifest.js'
|
|
108
|
+
'../../native-host/bin/install-manifest.js'
|
|
139
109
|
);
|
|
140
110
|
execFileSync(process.execPath, [installScript, ...rest], {
|
|
141
111
|
stdio: 'inherit',
|
|
@@ -167,18 +137,19 @@ if (command === 'install-skill') {
|
|
|
167
137
|
global: isGlobal,
|
|
168
138
|
cwd: process.cwd(),
|
|
169
139
|
projectPath: isGlobal ? os.homedir() : process.cwd(),
|
|
140
|
+
...getSetupStatusTestOverrides(),
|
|
170
141
|
});
|
|
171
142
|
/** @type {import('./install.js').SupportedTarget[]} */
|
|
172
|
-
const detected =
|
|
143
|
+
const detected = /** @type {import('./install.js').SupportedTarget[]} */ (
|
|
144
|
+
setupStatus.skillTargets.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
145
|
+
);
|
|
173
146
|
const installedManagedTargets = new Set(
|
|
174
147
|
setupStatus.skillTargets
|
|
175
148
|
.filter((entry) => entry.installed && entry.managed)
|
|
176
|
-
.map((entry) => entry.key)
|
|
149
|
+
.map((entry) => entry.key)
|
|
177
150
|
);
|
|
178
151
|
const installedManagedTargetList =
|
|
179
|
-
/** @type {import('./install.js').SupportedTarget[]} */ ([
|
|
180
|
-
...installedManagedTargets,
|
|
181
|
-
]);
|
|
152
|
+
/** @type {import('./install.js').SupportedTarget[]} */ ([...installedManagedTargets]);
|
|
182
153
|
|
|
183
154
|
// Aliases like 'openai' and 'google' map to canonical targets and stay omitted.
|
|
184
155
|
const items = SUPPORTED_TARGETS.map((t) => ({
|
|
@@ -193,30 +164,23 @@ if (command === 'install-skill') {
|
|
|
193
164
|
|
|
194
165
|
const selected = await interactiveCheckbox(
|
|
195
166
|
'Select agents to install skill for (↑↓ move · space toggle · a all · enter confirm)',
|
|
196
|
-
items
|
|
167
|
+
items
|
|
197
168
|
);
|
|
198
169
|
|
|
199
170
|
/** @type {import('./install.js').SupportedTarget[]} */
|
|
200
171
|
let targets;
|
|
201
172
|
if (selected === null) {
|
|
202
173
|
// Non-TTY: prefer managed installs, then detected targets (always includes 'agents').
|
|
203
|
-
targets =
|
|
204
|
-
installedManagedTargets.size > 0
|
|
205
|
-
? installedManagedTargetList
|
|
206
|
-
: detected;
|
|
174
|
+
targets = installedManagedTargets.size > 0 ? installedManagedTargetList : detected;
|
|
207
175
|
} else {
|
|
208
|
-
targets = /** @type {import('./install.js').SupportedTarget[]} */ (
|
|
209
|
-
selected
|
|
210
|
-
);
|
|
176
|
+
targets = /** @type {import('./install.js').SupportedTarget[]} */ (selected);
|
|
211
177
|
}
|
|
212
178
|
|
|
213
179
|
const projectPath = isGlobal ? os.homedir() : process.cwd();
|
|
214
180
|
if (selected !== null) {
|
|
215
181
|
const deselectedTargets =
|
|
216
182
|
/** @type {import('./install.js').SupportedTarget[]} */ (
|
|
217
|
-
installedManagedTargetList.filter(
|
|
218
|
-
(target) => !targets.includes(target),
|
|
219
|
-
)
|
|
183
|
+
installedManagedTargetList.filter((target) => !targets.includes(target))
|
|
220
184
|
);
|
|
221
185
|
const removableTargets = await findInstalledManagedTargets({
|
|
222
186
|
targets: deselectedTargets,
|
|
@@ -225,7 +189,7 @@ if (command === 'install-skill') {
|
|
|
225
189
|
});
|
|
226
190
|
if (removableTargets.length > 0) {
|
|
227
191
|
const confirmed = await interactiveConfirm(
|
|
228
|
-
`Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}
|
|
192
|
+
`Remove Browser Bridge skill from deselected targets: ${removableTargets.join(', ')}?`
|
|
229
193
|
);
|
|
230
194
|
if (confirmed) {
|
|
231
195
|
const removedPaths = await removeAgentFiles({
|
|
@@ -286,17 +250,16 @@ if (command === 'install-mcp') {
|
|
|
286
250
|
global: isGlobal,
|
|
287
251
|
cwd: process.cwd(),
|
|
288
252
|
projectPath: process.cwd(),
|
|
253
|
+
...getSetupStatusTestOverrides(),
|
|
289
254
|
});
|
|
290
|
-
const detected =
|
|
255
|
+
const detected = /** @type {import('./mcp-config.js').McpClientName[]} */ (
|
|
256
|
+
setupStatus.mcpClients.filter((entry) => entry.detected).map((entry) => entry.key)
|
|
257
|
+
);
|
|
291
258
|
const configuredClients = new Set(
|
|
292
|
-
setupStatus.mcpClients
|
|
293
|
-
.filter((entry) => entry.configured)
|
|
294
|
-
.map((entry) => entry.key),
|
|
259
|
+
setupStatus.mcpClients.filter((entry) => entry.configured).map((entry) => entry.key)
|
|
295
260
|
);
|
|
296
261
|
const configuredClientList =
|
|
297
|
-
/** @type {import('./mcp-config.js').McpClientName[]} */ ([
|
|
298
|
-
...configuredClients,
|
|
299
|
-
]);
|
|
262
|
+
/** @type {import('./mcp-config.js').McpClientName[]} */ ([...configuredClients]);
|
|
300
263
|
const items = MCP_CLIENT_NAMES.map((c) => ({
|
|
301
264
|
value: c,
|
|
302
265
|
label: `${c.padEnd(10)} ${MCP_CLIENT_LABELS[c]}`,
|
|
@@ -309,7 +272,7 @@ if (command === 'install-mcp') {
|
|
|
309
272
|
|
|
310
273
|
const selected = await interactiveCheckbox(
|
|
311
274
|
'Select clients to configure (↑↓ move · space toggle · a all · enter confirm)',
|
|
312
|
-
items
|
|
275
|
+
items
|
|
313
276
|
);
|
|
314
277
|
|
|
315
278
|
if (selected === null) {
|
|
@@ -321,17 +284,13 @@ if (command === 'install-mcp') {
|
|
|
321
284
|
? detected
|
|
322
285
|
: [...MCP_CLIENT_NAMES];
|
|
323
286
|
} else {
|
|
324
|
-
clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (
|
|
325
|
-
selected
|
|
326
|
-
);
|
|
287
|
+
clients = /** @type {import('./mcp-config.js').McpClientName[]} */ (selected);
|
|
327
288
|
}
|
|
328
289
|
|
|
329
290
|
if (selected !== null) {
|
|
330
291
|
const deselectedClients =
|
|
331
292
|
/** @type {import('./mcp-config.js').McpClientName[]} */ (
|
|
332
|
-
configuredClientList.filter(
|
|
333
|
-
(clientName) => !clients.includes(clientName),
|
|
334
|
-
)
|
|
293
|
+
configuredClientList.filter((clientName) => !clients.includes(clientName))
|
|
335
294
|
);
|
|
336
295
|
const removableClients = await findConfiguredMcpClients({
|
|
337
296
|
clients: deselectedClients,
|
|
@@ -340,7 +299,7 @@ if (command === 'install-mcp') {
|
|
|
340
299
|
});
|
|
341
300
|
if (removableClients.length > 0) {
|
|
342
301
|
const confirmed = await interactiveConfirm(
|
|
343
|
-
`Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}
|
|
302
|
+
`Remove Browser Bridge MCP config from deselected clients: ${removableClients.join(', ')}?`
|
|
344
303
|
);
|
|
345
304
|
if (confirmed) {
|
|
346
305
|
for (const clientName of removableClients) {
|
|
@@ -371,7 +330,7 @@ if (command === 'install-mcp') {
|
|
|
371
330
|
for (const part of parts) {
|
|
372
331
|
if (!isMcpClientName(part)) {
|
|
373
332
|
process.stderr.write(
|
|
374
|
-
`Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n
|
|
333
|
+
`Unknown client "${part}". Supported: ${MCP_CLIENT_NAMES.join(', ')}, all\n`
|
|
375
334
|
);
|
|
376
335
|
process.exit(1);
|
|
377
336
|
}
|
|
@@ -396,9 +355,7 @@ if (command === 'mcp') {
|
|
|
396
355
|
}
|
|
397
356
|
if (subcommand === 'config') {
|
|
398
357
|
if (!clientName || !isMcpClientName(clientName)) {
|
|
399
|
-
process.stderr.write(
|
|
400
|
-
`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`,
|
|
401
|
-
);
|
|
358
|
+
process.stderr.write(`Usage: bbx mcp config <${MCP_CLIENT_NAMES.join('|')}>\n`);
|
|
402
359
|
process.exit(1);
|
|
403
360
|
}
|
|
404
361
|
process.stdout.write(formatMcpConfig(clientName));
|
|
@@ -408,7 +365,11 @@ if (command === 'mcp') {
|
|
|
408
365
|
process.exit(1);
|
|
409
366
|
}
|
|
410
367
|
|
|
411
|
-
const
|
|
368
|
+
const clientTimeoutMs = getClientTimeoutOverride();
|
|
369
|
+
applyWindowsTcpTransportDefaults();
|
|
370
|
+
const client = new BridgeClient(
|
|
371
|
+
clientTimeoutMs ? { defaultTimeoutMs: clientTimeoutMs } : undefined
|
|
372
|
+
);
|
|
412
373
|
|
|
413
374
|
await main();
|
|
414
375
|
|
|
@@ -419,7 +380,7 @@ async function main() {
|
|
|
419
380
|
client,
|
|
420
381
|
'health.ping',
|
|
421
382
|
{},
|
|
422
|
-
{ source: REQUEST_SOURCE }
|
|
383
|
+
{ source: REQUEST_SOURCE }
|
|
423
384
|
);
|
|
424
385
|
await printSummary(healthResponse);
|
|
425
386
|
return;
|
|
@@ -427,12 +388,7 @@ async function main() {
|
|
|
427
388
|
|
|
428
389
|
if (command === 'access-request') {
|
|
429
390
|
await printSummary(
|
|
430
|
-
await requestBridge(
|
|
431
|
-
client,
|
|
432
|
-
'access.request',
|
|
433
|
-
{},
|
|
434
|
-
{ source: REQUEST_SOURCE },
|
|
435
|
-
),
|
|
391
|
+
await requestBridge(client, 'access.request', {}, { source: REQUEST_SOURCE })
|
|
436
392
|
);
|
|
437
393
|
return;
|
|
438
394
|
}
|
|
@@ -444,28 +400,31 @@ async function main() {
|
|
|
444
400
|
summary:
|
|
445
401
|
report.issues.length === 0
|
|
446
402
|
? 'Browser Bridge is ready.'
|
|
447
|
-
: `Browser Bridge has ${report.issues.length}
|
|
403
|
+
: `Browser Bridge has ${report.issues.length} readiness issue(s).`,
|
|
448
404
|
evidence: report,
|
|
449
405
|
});
|
|
450
406
|
return;
|
|
451
407
|
}
|
|
452
408
|
|
|
409
|
+
if (command === 'restart') {
|
|
410
|
+
const result = await restartBridgeDaemon();
|
|
411
|
+
printJson({
|
|
412
|
+
ok: true,
|
|
413
|
+
summary: result.previouslyRunning
|
|
414
|
+
? 'Browser Bridge daemon restarted.'
|
|
415
|
+
: 'Browser Bridge daemon started.',
|
|
416
|
+
evidence: result,
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
453
421
|
if (command === 'logs') {
|
|
454
|
-
await printSummary(
|
|
455
|
-
await requestBridge(client, 'log.tail', {}, { source: REQUEST_SOURCE }),
|
|
456
|
-
);
|
|
422
|
+
await printSummary(await requestBridge(client, 'log.tail', {}, { source: REQUEST_SOURCE }));
|
|
457
423
|
return;
|
|
458
424
|
}
|
|
459
425
|
|
|
460
426
|
if (command === 'tabs') {
|
|
461
|
-
await printSummary(
|
|
462
|
-
await requestBridge(
|
|
463
|
-
client,
|
|
464
|
-
'tabs.list',
|
|
465
|
-
{},
|
|
466
|
-
{ source: REQUEST_SOURCE },
|
|
467
|
-
),
|
|
468
|
-
);
|
|
427
|
+
await printSummary(await requestBridge(client, 'tabs.list', {}, { source: REQUEST_SOURCE }));
|
|
469
428
|
return;
|
|
470
429
|
}
|
|
471
430
|
|
|
@@ -477,7 +436,7 @@ async function main() {
|
|
|
477
436
|
{
|
|
478
437
|
url: url || undefined,
|
|
479
438
|
},
|
|
480
|
-
{ source: REQUEST_SOURCE }
|
|
439
|
+
{ source: REQUEST_SOURCE }
|
|
481
440
|
);
|
|
482
441
|
await printSummary(response);
|
|
483
442
|
return;
|
|
@@ -494,7 +453,7 @@ async function main() {
|
|
|
494
453
|
{
|
|
495
454
|
tabId: parseIntArg(tabId, 'tabId'),
|
|
496
455
|
},
|
|
497
|
-
{ source: REQUEST_SOURCE }
|
|
456
|
+
{ source: REQUEST_SOURCE }
|
|
498
457
|
);
|
|
499
458
|
await printSummary(response);
|
|
500
459
|
return;
|
|
@@ -506,7 +465,7 @@ async function main() {
|
|
|
506
465
|
tabId,
|
|
507
466
|
source: REQUEST_SOURCE,
|
|
508
467
|
});
|
|
509
|
-
|
|
468
|
+
printCallResponse(response);
|
|
510
469
|
return;
|
|
511
470
|
}
|
|
512
471
|
|
|
@@ -514,9 +473,7 @@ async function main() {
|
|
|
514
473
|
await ensureClientConnection();
|
|
515
474
|
const input = rest[0];
|
|
516
475
|
if (!input) {
|
|
517
|
-
throw new Error(
|
|
518
|
-
'Usage: batch \'[{"method":"...","params":{...}}, ...]\'',
|
|
519
|
-
);
|
|
476
|
+
throw new Error('Usage: batch \'[{"method":"...","params":{...}}, ...]\'');
|
|
520
477
|
}
|
|
521
478
|
const calls = JSON.parse(input);
|
|
522
479
|
if (!Array.isArray(calls)) {
|
|
@@ -534,7 +491,10 @@ async function main() {
|
|
|
534
491
|
durationMs: 0,
|
|
535
492
|
approxTokens: 0,
|
|
536
493
|
meta: { protocol_version: '1.0' },
|
|
537
|
-
error: {
|
|
494
|
+
error: {
|
|
495
|
+
code: 'INVALID_REQUEST',
|
|
496
|
+
message: 'Each batch call needs a method.',
|
|
497
|
+
},
|
|
538
498
|
response: null,
|
|
539
499
|
};
|
|
540
500
|
}
|
|
@@ -548,14 +508,16 @@ async function main() {
|
|
|
548
508
|
durationMs: 0,
|
|
549
509
|
approxTokens: 0,
|
|
550
510
|
meta: { protocol_version: '1.0' },
|
|
551
|
-
error: {
|
|
511
|
+
error: {
|
|
512
|
+
code: 'INVALID_REQUEST',
|
|
513
|
+
message: `Unknown bridge method "${call.method}".`,
|
|
514
|
+
},
|
|
552
515
|
response: null,
|
|
553
516
|
};
|
|
554
517
|
}
|
|
555
518
|
const method = /** @type {BridgeMethod} */ (call.method);
|
|
556
|
-
const tabId =
|
|
557
|
-
? call.tabId
|
|
558
|
-
: null;
|
|
519
|
+
const tabId =
|
|
520
|
+
methodNeedsTab(call.method) && typeof call.tabId === 'number' ? call.tabId : null;
|
|
559
521
|
const startTime = Date.now();
|
|
560
522
|
try {
|
|
561
523
|
const response = await client.request({
|
|
@@ -578,25 +540,19 @@ async function main() {
|
|
|
578
540
|
durationMs: Date.now() - startTime,
|
|
579
541
|
});
|
|
580
542
|
}
|
|
581
|
-
})
|
|
543
|
+
})
|
|
582
544
|
);
|
|
583
545
|
printJson(results);
|
|
584
546
|
return;
|
|
585
547
|
}
|
|
586
548
|
|
|
587
|
-
if (
|
|
588
|
-
command
|
|
589
|
-
&& METHODS.includes(/** @type {BridgeMethod} */ (command))
|
|
590
|
-
) {
|
|
591
|
-
const { tabId, method, params } = await parseCallCommand([
|
|
592
|
-
command,
|
|
593
|
-
...rest,
|
|
594
|
-
]);
|
|
549
|
+
if (command.includes('.') && METHODS.includes(/** @type {BridgeMethod} */ (command))) {
|
|
550
|
+
const { tabId, method, params } = await parseCallCommand([command, ...rest]);
|
|
595
551
|
const response = await requestBridge(client, method, params, {
|
|
596
552
|
tabId,
|
|
597
553
|
source: REQUEST_SOURCE,
|
|
598
554
|
});
|
|
599
|
-
|
|
555
|
+
printCallResponse(response);
|
|
600
556
|
return;
|
|
601
557
|
}
|
|
602
558
|
|
|
@@ -605,18 +561,13 @@ async function main() {
|
|
|
605
561
|
let elementRef;
|
|
606
562
|
if (shortcutCmd.resolve) {
|
|
607
563
|
if (!rest[0]) throw new Error(`Usage: ${command} <ref|selector>`);
|
|
608
|
-
elementRef = await resolveRef(
|
|
609
|
-
client,
|
|
610
|
-
rest[0],
|
|
611
|
-
null,
|
|
612
|
-
REQUEST_SOURCE,
|
|
613
|
-
);
|
|
564
|
+
elementRef = await resolveRef(client, rest[0], null, REQUEST_SOURCE);
|
|
614
565
|
}
|
|
615
566
|
const response = await requestBridge(
|
|
616
567
|
client,
|
|
617
568
|
shortcutCmd.method,
|
|
618
569
|
shortcutCmd.build(rest, elementRef),
|
|
619
|
-
{ source: REQUEST_SOURCE }
|
|
570
|
+
{ source: REQUEST_SOURCE }
|
|
620
571
|
);
|
|
621
572
|
await printSummary(response, shortcutCmd.printMethod);
|
|
622
573
|
return;
|
|
@@ -626,12 +577,7 @@ async function main() {
|
|
|
626
577
|
const [key, refOrSelector] = rest;
|
|
627
578
|
if (!key) throw new Error('Usage: press-key <key> [ref|selector]');
|
|
628
579
|
const elementRef = refOrSelector
|
|
629
|
-
? await resolveRef(
|
|
630
|
-
client,
|
|
631
|
-
refOrSelector,
|
|
632
|
-
null,
|
|
633
|
-
REQUEST_SOURCE,
|
|
634
|
-
)
|
|
580
|
+
? await resolveRef(client, refOrSelector, null, REQUEST_SOURCE)
|
|
635
581
|
: undefined;
|
|
636
582
|
const response = await requestBridge(
|
|
637
583
|
client,
|
|
@@ -640,42 +586,51 @@ async function main() {
|
|
|
640
586
|
key,
|
|
641
587
|
target: elementRef ? { elementRef } : undefined,
|
|
642
588
|
},
|
|
643
|
-
{ source: REQUEST_SOURCE }
|
|
589
|
+
{ source: REQUEST_SOURCE }
|
|
644
590
|
);
|
|
645
591
|
await printSummary(response);
|
|
646
592
|
return;
|
|
647
593
|
}
|
|
648
594
|
|
|
595
|
+
if (command === 'cdp-press-key') {
|
|
596
|
+
const parsed = extractTabFlag(rest);
|
|
597
|
+
const [key, code] = parsed.rest;
|
|
598
|
+
if (!key) throw new Error('Usage: cdp-press-key [--tab <tabId>] <key> [code]');
|
|
599
|
+
const response = await requestBridge(
|
|
600
|
+
client,
|
|
601
|
+
'cdp.dispatch_key_event',
|
|
602
|
+
{
|
|
603
|
+
key,
|
|
604
|
+
code,
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
tabId: parsed.tabId,
|
|
608
|
+
source: REQUEST_SOURCE,
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
await printSummary(response, 'cdp.dispatch_key_event');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
649
615
|
if (command === 'screenshot') {
|
|
650
616
|
const [refOrSelector, outputPath] = rest;
|
|
651
617
|
if (!refOrSelector) throw new Error('Usage: screenshot <ref|selector> [path]');
|
|
652
|
-
const elementRef = await resolveRef(
|
|
653
|
-
client,
|
|
654
|
-
refOrSelector,
|
|
655
|
-
null,
|
|
656
|
-
REQUEST_SOURCE,
|
|
657
|
-
);
|
|
618
|
+
const elementRef = await resolveRef(client, refOrSelector, null, REQUEST_SOURCE);
|
|
658
619
|
const response = await requestBridge(
|
|
659
620
|
client,
|
|
660
621
|
'screenshot.capture_element',
|
|
661
622
|
{
|
|
662
623
|
elementRef,
|
|
663
624
|
},
|
|
664
|
-
{ source: REQUEST_SOURCE }
|
|
625
|
+
{ source: REQUEST_SOURCE }
|
|
665
626
|
);
|
|
666
627
|
if (!response.ok) {
|
|
667
628
|
await printSummary(response);
|
|
668
629
|
return;
|
|
669
630
|
}
|
|
670
|
-
const screenshotResult = /** @type {ScreenshotResult} */ (
|
|
671
|
-
|
|
672
|
-
);
|
|
673
|
-
const filePath =
|
|
674
|
-
outputPath || path.join(os.tmpdir(), `bbx-${Date.now()}.png`);
|
|
675
|
-
const data = screenshotResult.image.replace(
|
|
676
|
-
/^data:image\/png;base64,/,
|
|
677
|
-
'',
|
|
678
|
-
);
|
|
631
|
+
const screenshotResult = /** @type {ScreenshotResult} */ (response.result);
|
|
632
|
+
const filePath = outputPath || path.join(os.tmpdir(), `bbx-${Date.now()}.png`);
|
|
633
|
+
const data = screenshotResult.image.replace(/^data:image\/png;base64,/, '');
|
|
679
634
|
await fs.promises.writeFile(filePath, Buffer.from(data, 'base64'));
|
|
680
635
|
printJson({
|
|
681
636
|
ok: true,
|
|
@@ -689,9 +644,7 @@ async function main() {
|
|
|
689
644
|
let expression = rest.join(' ');
|
|
690
645
|
if (!expression || expression === '-') expression = await readStdin();
|
|
691
646
|
if (!expression)
|
|
692
|
-
throw new Error(
|
|
693
|
-
'Usage: eval <expression> (or pipe via stdin: echo "expr" | bbx eval -)',
|
|
694
|
-
);
|
|
647
|
+
throw new Error('Usage: eval <expression> (or pipe via stdin: echo "expr" | bbx eval -)');
|
|
695
648
|
const response = await requestBridge(
|
|
696
649
|
client,
|
|
697
650
|
'page.evaluate',
|
|
@@ -699,7 +652,7 @@ async function main() {
|
|
|
699
652
|
expression,
|
|
700
653
|
returnByValue: true,
|
|
701
654
|
},
|
|
702
|
-
{ source: REQUEST_SOURCE }
|
|
655
|
+
{ source: REQUEST_SOURCE }
|
|
703
656
|
);
|
|
704
657
|
await printSummary(response);
|
|
705
658
|
return;
|
|
@@ -710,12 +663,9 @@ async function main() {
|
|
|
710
663
|
process.exitCode = 1;
|
|
711
664
|
} catch (error) {
|
|
712
665
|
const message = error instanceof Error ? error.message : String(error);
|
|
713
|
-
const raw =
|
|
714
|
-
error instanceof Error && 'code' in error
|
|
715
|
-
? /** @type {any} */ (error).code
|
|
716
|
-
: '';
|
|
666
|
+
const raw = error instanceof Error && 'code' in error ? /** @type {any} */ (error).code : '';
|
|
717
667
|
let code = 'ERROR';
|
|
718
|
-
if (raw === 'ENOENT' || raw === 'ECONNREFUSED') {
|
|
668
|
+
if (raw === 'ENOENT' || raw === 'ECONNREFUSED' || raw === 'EINVAL') {
|
|
719
669
|
code = 'DAEMON_OFFLINE';
|
|
720
670
|
} else if (raw === 'BRIDGE_TIMEOUT') {
|
|
721
671
|
code = 'BRIDGE_TIMEOUT';
|
|
@@ -735,6 +685,48 @@ async function main() {
|
|
|
735
685
|
}
|
|
736
686
|
}
|
|
737
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Allow tests to shrink request timeouts without changing the shared default.
|
|
690
|
+
*
|
|
691
|
+
* @returns {number | undefined}
|
|
692
|
+
*/
|
|
693
|
+
function getClientTimeoutOverride() {
|
|
694
|
+
const raw = process.env[TEST_TIMEOUT_ENV];
|
|
695
|
+
if (!raw) {
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const value = Number.parseInt(raw, 10);
|
|
700
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Allow CLI tests to provide deterministic MCP client detection without relying
|
|
705
|
+
* on whatever tools happen to be installed on the host machine.
|
|
706
|
+
*
|
|
707
|
+
* @returns {{
|
|
708
|
+
* mcpDetectors?: Record<string, () => boolean>,
|
|
709
|
+
* }}
|
|
710
|
+
*/
|
|
711
|
+
function getSetupStatusTestOverrides() {
|
|
712
|
+
if (!(TEST_DETECTED_MCP_CLIENTS_ENV in process.env)) {
|
|
713
|
+
return {};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const detectedClients = new Set(
|
|
717
|
+
(process.env[TEST_DETECTED_MCP_CLIENTS_ENV] || '')
|
|
718
|
+
.split(',')
|
|
719
|
+
.map((value) => value.trim().toLowerCase())
|
|
720
|
+
.filter(Boolean)
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
mcpDetectors: Object.fromEntries(
|
|
725
|
+
MCP_CLIENT_NAMES.map((clientName) => [clientName, () => detectedClients.has(clientName)])
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
738
730
|
/**
|
|
739
731
|
* @param {{ detected: boolean, installed: boolean }} options
|
|
740
732
|
* @returns {string | undefined}
|
|
@@ -766,10 +758,7 @@ async function ensureClientConnection() {
|
|
|
766
758
|
* @returns {Promise<void>}
|
|
767
759
|
*/
|
|
768
760
|
async function printSummary(response, method) {
|
|
769
|
-
printJson(annotateBridgeSummary(
|
|
770
|
-
summarizeBridgeResponse(response, method),
|
|
771
|
-
response,
|
|
772
|
-
));
|
|
761
|
+
printJson(annotateBridgeSummary(summarizeBridgeResponse(response, method), response));
|
|
773
762
|
}
|
|
774
763
|
|
|
775
764
|
/**
|
|
@@ -778,10 +767,28 @@ async function printSummary(response, method) {
|
|
|
778
767
|
*/
|
|
779
768
|
function printJson(value) {
|
|
780
769
|
process.stdout.write(
|
|
781
|
-
`${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n
|
|
770
|
+
`${JSON.stringify(sanitizeOutput(value), null, process.stdout.isTTY ? 2 : undefined)}\n`
|
|
782
771
|
);
|
|
783
772
|
}
|
|
784
773
|
|
|
774
|
+
/**
|
|
775
|
+
* @param {import('../../protocol/src/types.js').BridgeResponse} response
|
|
776
|
+
* @returns {void}
|
|
777
|
+
*/
|
|
778
|
+
function printCallResponse(response) {
|
|
779
|
+
if (response.ok) {
|
|
780
|
+
printJson(response.result);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
process.exitCode = 1;
|
|
785
|
+
const errorText = `${response.error.code}: ${response.error.message}`;
|
|
786
|
+
process.stderr.write(
|
|
787
|
+
`${process.stderr.isTTY ? `\u001b[31m${sanitizeOutput(errorText)}\u001b[0m` : sanitizeOutput(errorText)}\n`
|
|
788
|
+
);
|
|
789
|
+
printJson(response);
|
|
790
|
+
}
|
|
791
|
+
|
|
785
792
|
function printUsage() {
|
|
786
793
|
const blocks = ['Usage: bbx <command> [args]'];
|
|
787
794
|
for (const section of CLI_HELP_SECTIONS) {
|
|
@@ -846,9 +853,7 @@ async function parseCallCommand(args) {
|
|
|
846
853
|
if (first.includes('.')) {
|
|
847
854
|
const method = /** @type {BridgeMethod} */ (first);
|
|
848
855
|
if (!METHODS.includes(method)) {
|
|
849
|
-
throw new Error(
|
|
850
|
-
`Unknown method "${first}". Run bbx skill to see available methods.`,
|
|
851
|
-
);
|
|
856
|
+
throw new Error(`Unknown method "${first}". Run bbx skill to see available methods.`);
|
|
852
857
|
}
|
|
853
858
|
let rawParams = second;
|
|
854
859
|
// Support piped stdin: `echo '{"key":"val"}' | bbx call method -`
|