@browserbridge/bbx 1.7.4 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/agent-client/src/atomic-write.js +99 -0
- package/packages/agent-client/src/cli-helpers.js +20 -3
- package/packages/agent-client/src/cli.js +271 -74
- 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 +5 -2
- 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/protocol/src/defaults.js +2 -0
- package/packages/protocol/src/protocol.js +106 -5
- package/packages/protocol/src/types.ts +19 -1
- package/skills/browser-bridge/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replace a file through a sibling temporary file so readers never observe a
|
|
9
|
+
* partial write. Existing permissions are preserved unless an explicit mode is
|
|
10
|
+
* requested.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} targetPath
|
|
13
|
+
* @param {string | NodeJS.ArrayBufferView} data
|
|
14
|
+
* @param {{ encoding?: BufferEncoding, mode?: number, preserveMode?: boolean }} [options={ }]
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
export async function atomicWriteFile(targetPath, data, options = {}) {
|
|
18
|
+
const directory = path.dirname(targetPath);
|
|
19
|
+
const temporaryPath = path.join(
|
|
20
|
+
directory,
|
|
21
|
+
`.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`
|
|
22
|
+
);
|
|
23
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
24
|
+
|
|
25
|
+
let existingMode;
|
|
26
|
+
if (options.preserveMode !== false && options.mode === undefined) {
|
|
27
|
+
try {
|
|
28
|
+
existingMode = (await fs.promises.stat(targetPath)).mode & 0o777;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!isMissingFileError(error)) {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const mode = options.mode ?? existingMode;
|
|
37
|
+
try {
|
|
38
|
+
await fs.promises.writeFile(temporaryPath, data, {
|
|
39
|
+
...(options.encoding ? { encoding: options.encoding } : {}),
|
|
40
|
+
...(mode === undefined ? {} : { mode }),
|
|
41
|
+
flag: 'wx',
|
|
42
|
+
});
|
|
43
|
+
if (process.platform !== 'win32' && mode !== undefined) {
|
|
44
|
+
await fs.promises.chmod(temporaryPath, mode);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await fs.promises.rename(temporaryPath, targetPath);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (process.platform !== 'win32' || !isReplaceError(error)) {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await fs.promises.stat(targetPath);
|
|
54
|
+
} catch {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const backupPath = `${temporaryPath}.backup`;
|
|
59
|
+
await fs.promises.rename(targetPath, backupPath);
|
|
60
|
+
try {
|
|
61
|
+
await fs.promises.rename(temporaryPath, targetPath);
|
|
62
|
+
} catch (replaceError) {
|
|
63
|
+
await fs.promises.rename(backupPath, targetPath).catch(() => {});
|
|
64
|
+
throw replaceError;
|
|
65
|
+
}
|
|
66
|
+
await fs.promises.rm(backupPath, { force: true }).catch(() => {});
|
|
67
|
+
}
|
|
68
|
+
if (process.platform !== 'win32' && options.mode !== undefined) {
|
|
69
|
+
await fs.promises.chmod(targetPath, options.mode);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
await fs.promises.rm(temporaryPath, { force: true }).catch(() => {});
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Windows does not replace an existing file with rename().
|
|
79
|
+
*
|
|
80
|
+
* @param {unknown} error
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isReplaceError(error) {
|
|
84
|
+
const code =
|
|
85
|
+
error && typeof error === 'object' ? /** @type {{ code?: unknown }} */ (error).code : null;
|
|
86
|
+
return code === 'EEXIST' || code === 'EPERM' || code === 'EACCES';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {unknown} error
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function isMissingFileError(error) {
|
|
94
|
+
return Boolean(
|
|
95
|
+
error &&
|
|
96
|
+
typeof error === 'object' &&
|
|
97
|
+
/** @type {{ code?: unknown }} */ (error).code === 'ENOENT'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -96,7 +96,7 @@ export function parseJsonObject(value) {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Parse a CLI argument as a positive integer, throwing a user-friendly error
|
|
99
|
-
* if the value is missing or not a
|
|
99
|
+
* if the value is missing or not a positive integer.
|
|
100
100
|
*
|
|
101
101
|
* @param {string | undefined} value
|
|
102
102
|
* @param {string} argName - The argument name shown in the error message
|
|
@@ -104,12 +104,29 @@ export function parseJsonObject(value) {
|
|
|
104
104
|
*/
|
|
105
105
|
export function parseIntArg(value, argName) {
|
|
106
106
|
const n = Number(value);
|
|
107
|
-
if (!value || !Number.
|
|
108
|
-
throw new Error(`${argName} must be a
|
|
107
|
+
if (!value || !Number.isInteger(n) || n <= 0) {
|
|
108
|
+
throw new Error(`${argName} must be a positive integer (got ${JSON.stringify(value)}).`);
|
|
109
109
|
}
|
|
110
110
|
return n;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Parse a CLI argument as a finite number. This is intentionally separate from
|
|
115
|
+
* positive integer parsing because viewport coordinates may be fractional or
|
|
116
|
+
* negative.
|
|
117
|
+
*
|
|
118
|
+
* @param {string | undefined} value
|
|
119
|
+
* @param {string} argName
|
|
120
|
+
* @returns {number}
|
|
121
|
+
*/
|
|
122
|
+
export function parseNumberArg(value, argName) {
|
|
123
|
+
const number = Number(value);
|
|
124
|
+
if (value === undefined || value === '' || !Number.isFinite(number)) {
|
|
125
|
+
throw new Error(`${argName} must be a number (got ${JSON.stringify(value)}).`);
|
|
126
|
+
}
|
|
127
|
+
return number;
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
/**
|
|
114
131
|
* @param {string} method
|
|
115
132
|
* @returns {boolean}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
readProxyConfig,
|
|
15
15
|
SUPPORTED_BROWSERS,
|
|
16
16
|
} from '../../native-host/src/config.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getBridgeAuthTokenPath } from '../../native-host/src/auth-token.js';
|
|
18
18
|
import { pingExistingDaemon } from '../../native-host/src/daemon.js';
|
|
19
19
|
import { restartBridgeDaemon } from '../../native-host/src/daemon-process.js';
|
|
20
20
|
import { uninstallNativeManifest } from '../../native-host/src/install-manifest.js';
|
|
@@ -25,8 +25,13 @@ import {
|
|
|
25
25
|
summarizeBatchErrorItem,
|
|
26
26
|
summarizeBatchResponseItem,
|
|
27
27
|
} from '../../protocol/src/index.js';
|
|
28
|
+
import {
|
|
29
|
+
restartRegisteredMcpProcesses,
|
|
30
|
+
tryStartMcpProcessControl,
|
|
31
|
+
} from '../../mcp-server/src/lifecycle.js';
|
|
28
32
|
import { startBridgeMcpServer } from '../../mcp-server/src/server.js';
|
|
29
33
|
import { CLI_HELP_SECTIONS, SHORTCUT_COMMANDS } from './command-registry.js';
|
|
34
|
+
import { atomicWriteFile } from './atomic-write.js';
|
|
30
35
|
import {
|
|
31
36
|
interactiveCheckbox,
|
|
32
37
|
interactiveConfirm,
|
|
@@ -55,6 +60,7 @@ import {
|
|
|
55
60
|
import { getDoctorReport, requestBridge, resolveRef } from './runtime.js';
|
|
56
61
|
import {
|
|
57
62
|
addRemoteDestination,
|
|
63
|
+
assertProxyBindSafety,
|
|
58
64
|
createBridgeClientForDestination,
|
|
59
65
|
listBridgeDestinations,
|
|
60
66
|
normalizeDestinationId,
|
|
@@ -62,6 +68,7 @@ import {
|
|
|
62
68
|
readRemoteConfig,
|
|
63
69
|
removeRemoteDestination,
|
|
64
70
|
resolveProxyEnableSettings,
|
|
71
|
+
isLoopbackHost,
|
|
65
72
|
} from './remotes.js';
|
|
66
73
|
import { collectSetupStatus } from './setup-status.js';
|
|
67
74
|
import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
@@ -72,6 +79,7 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from './subagent.js';
|
|
|
72
79
|
const REQUEST_SOURCE = 'cli';
|
|
73
80
|
const TEST_TIMEOUT_ENV = 'BBX_CLIENT_REQUEST_TIMEOUT_MS';
|
|
74
81
|
const TEST_DETECTED_MCP_CLIENTS_ENV = 'BBX_TEST_DETECTED_MCP_CLIENTS';
|
|
82
|
+
const TEST_DETECTED_SKILL_TARGETS_ENV = 'BBX_TEST_DETECTED_SKILL_TARGETS';
|
|
75
83
|
const REMOTE_ENV = 'BBX_REMOTE';
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -178,9 +186,7 @@ if (command === 'uninstall') {
|
|
|
178
186
|
|
|
179
187
|
if (command === 'install-skill') {
|
|
180
188
|
// When no positional target is given, detect installed agents and prompt.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (positional.length === 0) {
|
|
189
|
+
if (!hasExplicitInstallSkillTarget(rest)) {
|
|
184
190
|
let scopeOptions;
|
|
185
191
|
try {
|
|
186
192
|
scopeOptions = parseInstallAgentArgs(rest);
|
|
@@ -420,7 +426,13 @@ if (command === 'install-mcp') {
|
|
|
420
426
|
if (command === 'mcp') {
|
|
421
427
|
const [subcommand, clientName] = rest;
|
|
422
428
|
if (subcommand === 'serve') {
|
|
423
|
-
await
|
|
429
|
+
const control = await tryStartMcpProcessControl();
|
|
430
|
+
try {
|
|
431
|
+
await startBridgeMcpServer();
|
|
432
|
+
} catch (error) {
|
|
433
|
+
await control?.dispose();
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
424
436
|
await new Promise(() => {});
|
|
425
437
|
}
|
|
426
438
|
if (subcommand === 'config') {
|
|
@@ -528,12 +540,26 @@ async function main() {
|
|
|
528
540
|
|
|
529
541
|
if (command === 'restart') {
|
|
530
542
|
const result = await restartBridgeDaemon();
|
|
543
|
+
const mcpProcesses = await restartRegisteredMcpProcesses();
|
|
544
|
+
if (mcpProcesses.restartFailed > 0) {
|
|
545
|
+
process.exitCode = 1;
|
|
546
|
+
}
|
|
531
547
|
printJson({
|
|
532
|
-
ok:
|
|
533
|
-
summary:
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
548
|
+
ok: mcpProcesses.restartFailed === 0,
|
|
549
|
+
summary: `${
|
|
550
|
+
result.previouslyRunning
|
|
551
|
+
? 'Browser Bridge daemon restarted.'
|
|
552
|
+
: 'Browser Bridge daemon started.'
|
|
553
|
+
}${
|
|
554
|
+
mcpProcesses.restartRequested > 0
|
|
555
|
+
? ` Restart requested for ${mcpProcesses.restartRequested} MCP server(s).`
|
|
556
|
+
: ''
|
|
557
|
+
}${
|
|
558
|
+
mcpProcesses.restartFailed > 0
|
|
559
|
+
? ` Could not contact ${mcpProcesses.restartFailed} MCP server(s).`
|
|
560
|
+
: ''
|
|
561
|
+
}`,
|
|
562
|
+
evidence: { ...result, mcpProcesses },
|
|
537
563
|
});
|
|
538
564
|
return;
|
|
539
565
|
}
|
|
@@ -623,7 +649,25 @@ async function main() {
|
|
|
623
649
|
await ensureClientConnection();
|
|
624
650
|
const results = await Promise.all(
|
|
625
651
|
calls.map(async (call) => {
|
|
626
|
-
if (!call || typeof call !== 'object' ||
|
|
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') {
|
|
627
671
|
return {
|
|
628
672
|
method: '',
|
|
629
673
|
tabId: null,
|
|
@@ -640,33 +684,62 @@ async function main() {
|
|
|
640
684
|
response: null,
|
|
641
685
|
};
|
|
642
686
|
}
|
|
643
|
-
if (!METHODS.includes(/** @type {BridgeMethod} */ (
|
|
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
|
+
) {
|
|
644
711
|
return {
|
|
645
|
-
method
|
|
712
|
+
method,
|
|
646
713
|
tabId: null,
|
|
647
714
|
ok: false,
|
|
648
|
-
summary:
|
|
715
|
+
summary: 'INVALID_REQUEST: Batch call params must be a JSON object.',
|
|
649
716
|
evidence: null,
|
|
650
717
|
durationMs: 0,
|
|
651
718
|
approxTokens: 0,
|
|
652
719
|
meta: { protocol_version: getProtocolVersion() },
|
|
653
720
|
error: {
|
|
654
721
|
code: 'INVALID_REQUEST',
|
|
655
|
-
message:
|
|
722
|
+
message: 'Batch call params must be a JSON object.',
|
|
656
723
|
},
|
|
657
724
|
response: null,
|
|
658
725
|
};
|
|
659
726
|
}
|
|
660
|
-
const
|
|
727
|
+
const params =
|
|
728
|
+
batchCall.params === undefined
|
|
729
|
+
? {}
|
|
730
|
+
: /** @type {Record<string, unknown>} */ (batchCall.params);
|
|
661
731
|
const tabId =
|
|
662
|
-
methodNeedsTab(
|
|
732
|
+
methodNeedsTab(method) &&
|
|
733
|
+
typeof batchCall.tabId === 'number' &&
|
|
734
|
+
Number.isInteger(batchCall.tabId) &&
|
|
735
|
+
batchCall.tabId > 0
|
|
736
|
+
? batchCall.tabId
|
|
737
|
+
: null;
|
|
663
738
|
const startTime = Date.now();
|
|
664
739
|
try {
|
|
665
|
-
const response = await client
|
|
666
|
-
method,
|
|
740
|
+
const response = await requestBridge(client, method, params, {
|
|
667
741
|
tabId,
|
|
668
|
-
|
|
669
|
-
meta: { source: REQUEST_SOURCE },
|
|
742
|
+
source: REQUEST_SOURCE,
|
|
670
743
|
});
|
|
671
744
|
return summarizeBatchResponseItem({
|
|
672
745
|
method,
|
|
@@ -684,6 +757,9 @@ async function main() {
|
|
|
684
757
|
}
|
|
685
758
|
})
|
|
686
759
|
);
|
|
760
|
+
if (results.some((result) => !result.ok)) {
|
|
761
|
+
process.exitCode = 1;
|
|
762
|
+
}
|
|
687
763
|
printJson(results);
|
|
688
764
|
return;
|
|
689
765
|
}
|
|
@@ -817,22 +893,7 @@ async function main() {
|
|
|
817
893
|
const { tabId, rest: iArgs } = extractTabFlag(rest);
|
|
818
894
|
const sub = iArgs[0];
|
|
819
895
|
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;
|
|
896
|
+
const { pattern, isBlock, statusCode, body } = parseInterceptAddArgs(iArgs.slice(1));
|
|
836
897
|
const response = await requestBridge(
|
|
837
898
|
client,
|
|
838
899
|
'network.intercept.add',
|
|
@@ -847,7 +908,9 @@ async function main() {
|
|
|
847
908
|
await printSummary(response);
|
|
848
909
|
} else if (sub === 'remove') {
|
|
849
910
|
const ruleId = iArgs[1];
|
|
850
|
-
if (!ruleId
|
|
911
|
+
if (!ruleId || iArgs.length !== 2) {
|
|
912
|
+
throw new Error('Usage: intercept remove <ruleId>');
|
|
913
|
+
}
|
|
851
914
|
const response = await requestBridge(
|
|
852
915
|
client,
|
|
853
916
|
'network.intercept.remove',
|
|
@@ -856,6 +919,9 @@ async function main() {
|
|
|
856
919
|
);
|
|
857
920
|
await printSummary(response);
|
|
858
921
|
} else if (sub === 'list') {
|
|
922
|
+
if (iArgs.length !== 1) {
|
|
923
|
+
throw new Error('Usage: intercept list');
|
|
924
|
+
}
|
|
859
925
|
const response = await requestBridge(
|
|
860
926
|
client,
|
|
861
927
|
'network.intercept.list',
|
|
@@ -864,6 +930,9 @@ async function main() {
|
|
|
864
930
|
);
|
|
865
931
|
await printSummary(response);
|
|
866
932
|
} else if (sub === 'clear') {
|
|
933
|
+
if (iArgs.length !== 1) {
|
|
934
|
+
throw new Error('Usage: intercept clear');
|
|
935
|
+
}
|
|
867
936
|
const response = await requestBridge(
|
|
868
937
|
client,
|
|
869
938
|
'network.intercept.clear',
|
|
@@ -945,10 +1014,13 @@ async function handleProxyCommand(args) {
|
|
|
945
1014
|
options,
|
|
946
1015
|
randomUUID
|
|
947
1016
|
);
|
|
948
|
-
|
|
1017
|
+
assertProxyBindSafety(existing, options, bindHost);
|
|
1018
|
+
await atomicWriteFile(getBridgeAuthTokenPath(), `${token}\n`, {
|
|
1019
|
+
encoding: 'utf8',
|
|
1020
|
+
mode: 0o600,
|
|
1021
|
+
});
|
|
949
1022
|
const configPath = getProxyConfigPath();
|
|
950
|
-
await
|
|
951
|
-
await fs.promises.writeFile(
|
|
1023
|
+
await atomicWriteFile(
|
|
952
1024
|
configPath,
|
|
953
1025
|
`${JSON.stringify(
|
|
954
1026
|
{
|
|
@@ -963,7 +1035,6 @@ async function handleProxyCommand(args) {
|
|
|
963
1035
|
{ encoding: 'utf8', mode: 0o600 }
|
|
964
1036
|
);
|
|
965
1037
|
const result = await restartBridgeDaemon();
|
|
966
|
-
const exampleHost = getProxyExampleHost(bindHost);
|
|
967
1038
|
const tokenNote =
|
|
968
1039
|
tokenSource === 'existing'
|
|
969
1040
|
? ' (unchanged - already-configured clients keep working; pass --rotate-token to generate a new secret)'
|
|
@@ -978,16 +1049,26 @@ async function handleProxyCommand(args) {
|
|
|
978
1049
|
`Daemon: ${result.previouslyRunning ? 'restarted' : 'started'} (pid ${result.pid})`,
|
|
979
1050
|
];
|
|
980
1051
|
if (tokenSource !== 'existing') {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
+
}
|
|
991
1072
|
}
|
|
992
1073
|
lines.push('');
|
|
993
1074
|
process.stdout.write(lines.join('\n'));
|
|
@@ -1059,6 +1140,10 @@ function parseProxyEnableArgs(args) {
|
|
|
1059
1140
|
options.rotateToken = true;
|
|
1060
1141
|
continue;
|
|
1061
1142
|
}
|
|
1143
|
+
if (arg === '--unsafe-plaintext') {
|
|
1144
|
+
options.unsafePlaintext = true;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1062
1147
|
throw new Error(`Unknown proxy enable option "${arg}".`);
|
|
1063
1148
|
}
|
|
1064
1149
|
if (options.token && options.rotateToken) {
|
|
@@ -1104,9 +1189,11 @@ async function handleRemoteCommand(args) {
|
|
|
1104
1189
|
if (subcommand === 'add') {
|
|
1105
1190
|
const [name, endpoint, ...optionArgs] = restArgs;
|
|
1106
1191
|
if (!name || !endpoint) {
|
|
1107
|
-
throw new Error(
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
'Usage: bbx remote add <name> <host:port> (--token <token>|--token-file <path>)'
|
|
1194
|
+
);
|
|
1108
1195
|
}
|
|
1109
|
-
const token = parseRemoteTokenOption(optionArgs);
|
|
1196
|
+
const token = await parseRemoteTokenOption(optionArgs);
|
|
1110
1197
|
const { host, port } = parseRemoteEndpoint(endpoint);
|
|
1111
1198
|
const remote = await addRemoteDestination({
|
|
1112
1199
|
id: normalizeDestinationId(name),
|
|
@@ -1198,22 +1285,121 @@ async function handleRemoteCommand(args) {
|
|
|
1198
1285
|
|
|
1199
1286
|
/**
|
|
1200
1287
|
* @param {string[]} args
|
|
1201
|
-
* @returns {string}
|
|
1288
|
+
* @returns {Promise<string>}
|
|
1202
1289
|
*/
|
|
1203
|
-
function parseRemoteTokenOption(args) {
|
|
1290
|
+
async function parseRemoteTokenOption(args) {
|
|
1204
1291
|
let token = '';
|
|
1292
|
+
let tokenFile = '';
|
|
1205
1293
|
for (let index = 0; index < args.length; index += 1) {
|
|
1206
1294
|
const arg = args[index];
|
|
1207
1295
|
if (arg === '--token') {
|
|
1208
|
-
|
|
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;
|
|
1209
1309
|
continue;
|
|
1210
1310
|
}
|
|
1211
1311
|
throw new Error(`Unknown remote add option "${arg}".`);
|
|
1212
1312
|
}
|
|
1213
|
-
if (!token.trim()) {
|
|
1214
|
-
throw new Error(
|
|
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.');
|
|
1215
1401
|
}
|
|
1216
|
-
return
|
|
1402
|
+
return { pattern, isBlock, statusCode, body };
|
|
1217
1403
|
}
|
|
1218
1404
|
|
|
1219
1405
|
/**
|
|
@@ -1249,25 +1435,36 @@ function getClientTimeoutOverride() {
|
|
|
1249
1435
|
*
|
|
1250
1436
|
* @returns {{
|
|
1251
1437
|
* mcpDetectors?: Record<string, () => boolean>,
|
|
1438
|
+
* skillDetectors?: Record<string, () => boolean>,
|
|
1252
1439
|
* }}
|
|
1253
1440
|
*/
|
|
1254
1441
|
function getSetupStatusTestOverrides() {
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
+
);
|
|
1257
1454
|
}
|
|
1258
1455
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
.
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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;
|
|
1271
1468
|
}
|
|
1272
1469
|
|
|
1273
1470
|
/**
|