@browserbridge/bbx 1.1.0 → 1.3.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 -5
- package/package.json +1 -1
- package/packages/agent-client/src/cli.js +30 -20
- package/packages/agent-client/src/client.js +105 -42
- package/packages/agent-client/src/command-registry.js +4 -14
- package/packages/agent-client/src/detect.js +3 -3
- package/packages/agent-client/src/install.js +3 -7
- package/packages/agent-client/src/mcp-config.js +1 -3
- package/packages/agent-client/src/runtime.js +7 -41
- package/packages/agent-client/src/setup-status.js +3 -13
- package/packages/agent-client/src/types.ts +131 -0
- package/packages/mcp-server/src/handlers-capture.js +291 -0
- package/packages/mcp-server/src/handlers-dom.js +203 -0
- package/packages/mcp-server/src/handlers-navigation.js +79 -0
- package/packages/mcp-server/src/handlers-page.js +365 -0
- package/packages/mcp-server/src/handlers-utils.js +318 -0
- package/packages/mcp-server/src/handlers.js +59 -1176
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/bin/bridge-daemon.js +2 -1
- package/packages/native-host/bin/install-manifest.js +8 -0
- package/packages/native-host/bin/postinstall.js +46 -9
- package/packages/native-host/src/daemon-logger.js +157 -0
- package/packages/native-host/src/daemon-process.js +43 -18
- package/packages/native-host/src/daemon.js +133 -12
- package/packages/native-host/src/framing.js +13 -0
- package/packages/native-host/src/native-host.js +7 -5
- package/packages/protocol/src/capabilities.js +1 -0
- package/packages/protocol/src/protocol.js +40 -0
- package/packages/protocol/src/registry.js +5 -9
- package/packages/protocol/src/types.ts +572 -0
- package/packages/protocol/src/types.js +0 -626
|
@@ -401,7 +401,7 @@ export function createBridgeMcpServer() {
|
|
|
401
401
|
.optional()
|
|
402
402
|
.describe('Mouse button for click (default: left)'),
|
|
403
403
|
clickCount: z.number().optional().describe('Click count (1=single, 2=double)'),
|
|
404
|
-
text: z.string().optional().describe('Text to type (for type action)'),
|
|
404
|
+
text: z.string().max(100000).optional().describe('Text to type (for type action)'),
|
|
405
405
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
406
406
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
407
407
|
key: z
|
|
@@ -527,6 +527,7 @@ export function createBridgeMcpServer() {
|
|
|
527
527
|
.optional()
|
|
528
528
|
.describe('Element reference (for element action, preferred)'),
|
|
529
529
|
selector: z.string().optional().describe('CSS selector (used if no elementRef)'),
|
|
530
|
+
nodeId: z.number().optional().describe('CDP node id for cdp_box_model/cdp_computed_styles'),
|
|
530
531
|
rect: z
|
|
531
532
|
.object({
|
|
532
533
|
x: z.number().describe('Region left edge (viewport pixels)'),
|
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
formatBridgeTransport,
|
|
7
7
|
getBridgeTransport,
|
|
8
8
|
} from '../src/config.js';
|
|
9
|
+
import { DaemonLogger } from '../src/daemon-logger.js';
|
|
9
10
|
import { clearDaemonPidFile, writeDaemonPidFile } from '../src/daemon-process.js';
|
|
10
11
|
|
|
11
12
|
applyWindowsTcpTransportDefaults();
|
|
12
13
|
const transport = getBridgeTransport();
|
|
13
|
-
const daemon = new BridgeDaemon({ transport });
|
|
14
|
+
const daemon = new BridgeDaemon({ transport, logger: new DaemonLogger() });
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @param {unknown} error
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import { restartBridgeDaemonIfRunning } from '../src/daemon-process.js';
|
|
6
7
|
import {
|
|
7
8
|
installNativeManifest,
|
|
8
9
|
parseExtensionId,
|
|
@@ -77,3 +78,10 @@ for (const [index, target] of targets.entries()) {
|
|
|
77
78
|
browser: target,
|
|
78
79
|
});
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
if (!uninstall) {
|
|
83
|
+
const restartResult = await restartBridgeDaemonIfRunning();
|
|
84
|
+
if (restartResult) {
|
|
85
|
+
process.stdout.write('Restarted Browser Bridge daemon to use the updated install.\n');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -10,18 +10,55 @@
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { restartBridgeDaemonIfRunning } from '../src/daemon-process.js';
|
|
13
14
|
import { installNativeManifest } from '../src/install-manifest.js';
|
|
14
15
|
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const repoRoot = path.resolve(__dirname, '../../..');
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
/**
|
|
20
|
+
* @param {{
|
|
21
|
+
* installNativeManifestFn?: typeof installNativeManifest,
|
|
22
|
+
* restartBridgeDaemonIfRunningFn?: typeof restartBridgeDaemonIfRunning,
|
|
23
|
+
* stdout?: Pick<NodeJS.WriteStream, 'write'>,
|
|
24
|
+
* stderr?: Pick<NodeJS.WriteStream, 'write'>,
|
|
25
|
+
* exit?: (code?: number) => void,
|
|
26
|
+
* }} [deps]
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
export async function runPostinstall({
|
|
30
|
+
installNativeManifestFn = installNativeManifest,
|
|
31
|
+
restartBridgeDaemonIfRunningFn = restartBridgeDaemonIfRunning,
|
|
32
|
+
stdout = process.stdout,
|
|
33
|
+
stderr = process.stderr,
|
|
34
|
+
exit = (code) => process.exit(code),
|
|
35
|
+
} = {}) {
|
|
36
|
+
try {
|
|
37
|
+
await installNativeManifestFn({ repoRoot, preserveCustomExtensionId: true });
|
|
38
|
+
stdout.write('Browser Bridge: native host installed. Run `bbx doctor` to verify.\n');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Non-fatal - user can run `bbx install` manually.
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
stderr.write(
|
|
43
|
+
`Browser Bridge: native host auto-install skipped (${message}).\nRun \`bbx install\` manually if needed.\n`
|
|
44
|
+
);
|
|
45
|
+
exit(0);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const restartResult = await restartBridgeDaemonIfRunningFn();
|
|
51
|
+
if (restartResult) {
|
|
52
|
+
stdout.write('Browser Bridge: restarted the local daemon to use the updated install.\n');
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
stderr.write(
|
|
57
|
+
`Browser Bridge: native host installed, but daemon restart failed (${message}).\nRun \`bbx restart\` if needed.\n`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
63
|
+
await runPostinstall();
|
|
27
64
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {{ write: (chunk: string) => void }} LogStream */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @enum {number}
|
|
7
|
+
* @private
|
|
8
|
+
*/
|
|
9
|
+
const LOG_LEVELS = /** @type {const} */ ({
|
|
10
|
+
debug: 0,
|
|
11
|
+
info: 1,
|
|
12
|
+
warn: 2,
|
|
13
|
+
error: 3,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** @typedef {keyof typeof LOG_LEVELS} LogLevel */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* debug(message: string, extra?: Record<string, unknown>): void;
|
|
21
|
+
* info(message: string, extra?: Record<string, unknown>): void;
|
|
22
|
+
* warn(message: string, extra?: Record<string, unknown>): void;
|
|
23
|
+
* error(message: string, extra?: Record<string, unknown>): void;
|
|
24
|
+
* }} DaemonLoggerLike
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Structured JSON logger for the daemon. Each call emits a single NDJSON line
|
|
29
|
+
* with a timestamp, level, message, and optional structured fields.
|
|
30
|
+
*/
|
|
31
|
+
export class DaemonLogger {
|
|
32
|
+
/** @type {LogStream} */
|
|
33
|
+
#stream;
|
|
34
|
+
/** @type {LogLevel} */
|
|
35
|
+
#minLevel;
|
|
36
|
+
/** @type {Record<string, unknown>} */
|
|
37
|
+
#defaults;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {{ stream?: LogStream, minLevel?: LogLevel, defaults?: Record<string, unknown> }} [options]
|
|
41
|
+
*/
|
|
42
|
+
constructor({ stream, minLevel = 'info', defaults = {} } = {}) {
|
|
43
|
+
this.#stream = stream ?? /** @type {LogStream} */ (process.stderr);
|
|
44
|
+
this.#minLevel = minLevel;
|
|
45
|
+
this.#defaults = defaults;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {LogLevel} level
|
|
50
|
+
* @param {string} message
|
|
51
|
+
* @param {Record<string, unknown>} [extra]
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
#write(level, message, extra) {
|
|
55
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[this.#minLevel]) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const entry = {
|
|
59
|
+
...this.#defaults,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
level,
|
|
62
|
+
message,
|
|
63
|
+
...extra,
|
|
64
|
+
};
|
|
65
|
+
this.#stream.write(`${JSON.stringify(entry)}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} message
|
|
70
|
+
* @param {Record<string, unknown>} [extra]
|
|
71
|
+
* @returns {void}
|
|
72
|
+
*/
|
|
73
|
+
debug(message, extra) {
|
|
74
|
+
this.#write('debug', message, extra);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} message
|
|
79
|
+
* @param {Record<string, unknown>} [extra]
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
info(message, extra) {
|
|
83
|
+
this.#write('info', message, extra);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} message
|
|
88
|
+
* @param {Record<string, unknown>} [extra]
|
|
89
|
+
* @returns {void}
|
|
90
|
+
*/
|
|
91
|
+
warn(message, extra) {
|
|
92
|
+
this.#write('warn', message, extra);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} message
|
|
97
|
+
* @param {Record<string, unknown>} [extra]
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
error(message, extra) {
|
|
101
|
+
this.#write('error', message, extra);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A silent logger that discards all output. Useful in tests.
|
|
107
|
+
* @type {DaemonLoggerLike}
|
|
108
|
+
*/
|
|
109
|
+
export const silentLogger = {
|
|
110
|
+
debug() {},
|
|
111
|
+
info() {},
|
|
112
|
+
warn() {},
|
|
113
|
+
error() {},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wrap a legacy `Pick<Console, 'log' | 'error'>` logger into the new
|
|
118
|
+
* `DaemonLoggerLike` interface. Used for backward compatibility with
|
|
119
|
+
* callers that still pass `{ log() {}, error() {} }`.
|
|
120
|
+
*
|
|
121
|
+
* @param {Pick<Console, 'log' | 'error'>} legacy
|
|
122
|
+
* @returns {DaemonLoggerLike}
|
|
123
|
+
*/
|
|
124
|
+
export function wrapLegacyLogger(legacy) {
|
|
125
|
+
return {
|
|
126
|
+
debug(message, extra) {
|
|
127
|
+
legacy.log?.(`[debug] ${message}`, extra ?? '');
|
|
128
|
+
},
|
|
129
|
+
info(message, extra) {
|
|
130
|
+
legacy.log?.(`[info] ${message}`, extra ?? '');
|
|
131
|
+
},
|
|
132
|
+
warn(message, extra) {
|
|
133
|
+
legacy.log?.(`[warn] ${message}`, extra ?? '');
|
|
134
|
+
},
|
|
135
|
+
error(message, extra) {
|
|
136
|
+
legacy.error?.(`[error] ${message}`, extra ?? '');
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Accept either a `DaemonLoggerLike` or a legacy `Pick<Console, 'log' | 'error'>`
|
|
143
|
+
* and return a normalized `DaemonLoggerLike`. If the input already has an `info`
|
|
144
|
+
* method it is returned as-is; otherwise it is wrapped via `wrapLegacyLogger`.
|
|
145
|
+
*
|
|
146
|
+
* @param {DaemonLoggerLike | Pick<Console, 'log' | 'error'> | undefined} logger
|
|
147
|
+
* @returns {DaemonLoggerLike}
|
|
148
|
+
*/
|
|
149
|
+
export function normalizeDaemonLogger(logger) {
|
|
150
|
+
if (logger === undefined) {
|
|
151
|
+
return new DaemonLogger();
|
|
152
|
+
}
|
|
153
|
+
if (typeof (/** @type {any} */ (logger).info) === 'function') {
|
|
154
|
+
return /** @type {DaemonLoggerLike} */ (logger);
|
|
155
|
+
}
|
|
156
|
+
return wrapLegacyLogger(/** @type {Pick<Console, 'log' | 'error'>} */ (logger));
|
|
157
|
+
}
|
|
@@ -10,7 +10,6 @@ import { pingExistingDaemon } from './daemon.js';
|
|
|
10
10
|
import {
|
|
11
11
|
createSocketBridgeTransport,
|
|
12
12
|
formatBridgeTransport,
|
|
13
|
-
getBridgeDir,
|
|
14
13
|
getBridgeTransport,
|
|
15
14
|
getDaemonPidPath,
|
|
16
15
|
} from './config.js';
|
|
@@ -74,7 +73,7 @@ export function spawnBridgeDaemonProcess() {
|
|
|
74
73
|
* @returns {Promise<void>}
|
|
75
74
|
*/
|
|
76
75
|
export async function writeDaemonPidFile(pid = process.pid, pidPath = getDaemonPidPath()) {
|
|
77
|
-
await fs.promises.mkdir(
|
|
76
|
+
await fs.promises.mkdir(path.dirname(pidPath), { recursive: true });
|
|
78
77
|
await fs.promises.writeFile(pidPath, `${pid}\n`, 'utf8');
|
|
79
78
|
}
|
|
80
79
|
|
|
@@ -196,6 +195,48 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
196
195
|
* }>}
|
|
197
196
|
*/
|
|
198
197
|
export async function restartBridgeDaemon(options = {}) {
|
|
198
|
+
const stopResult = await stopBridgeDaemon(options);
|
|
199
|
+
return restartBridgeDaemonAfterStop(stopResult, options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Restart the daemon only when one is already running. This is useful during
|
|
204
|
+
* package upgrades where the launcher changed and the in-memory daemon should
|
|
205
|
+
* pick up the new install, without eagerly starting a fresh background process.
|
|
206
|
+
*
|
|
207
|
+
* @param {RestartBridgeDaemonOptions} [options={}]
|
|
208
|
+
* @returns {Promise<{
|
|
209
|
+
* transport: string,
|
|
210
|
+
* socketPath: string,
|
|
211
|
+
* pidPath: string,
|
|
212
|
+
* pid: number | null,
|
|
213
|
+
* previouslyRunning: boolean,
|
|
214
|
+
* previousPid: number | null,
|
|
215
|
+
* removedStaleSocket: boolean,
|
|
216
|
+
* } | null>}
|
|
217
|
+
*/
|
|
218
|
+
export async function restartBridgeDaemonIfRunning(options = {}) {
|
|
219
|
+
const stopResult = await stopBridgeDaemon(options);
|
|
220
|
+
if (!stopResult.previouslyRunning) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return restartBridgeDaemonAfterStop(stopResult, options);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {Awaited<ReturnType<typeof stopBridgeDaemon>>} stopResult
|
|
228
|
+
* @param {RestartBridgeDaemonOptions} [options={}]
|
|
229
|
+
* @returns {Promise<{
|
|
230
|
+
* transport: string,
|
|
231
|
+
* socketPath: string,
|
|
232
|
+
* pidPath: string,
|
|
233
|
+
* pid: number | null,
|
|
234
|
+
* previouslyRunning: boolean,
|
|
235
|
+
* previousPid: number | null,
|
|
236
|
+
* removedStaleSocket: boolean,
|
|
237
|
+
* }>}
|
|
238
|
+
*/
|
|
239
|
+
async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
199
240
|
const {
|
|
200
241
|
transport = getBridgeTransport(),
|
|
201
242
|
socketPath = undefined,
|
|
@@ -204,27 +245,11 @@ export async function restartBridgeDaemon(options = {}) {
|
|
|
204
245
|
pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
|
|
205
246
|
pingDaemonFn = pingExistingDaemon,
|
|
206
247
|
readPidFn = readDaemonPidFile,
|
|
207
|
-
findPidByTransportFn = findDaemonPidByTransport,
|
|
208
|
-
killFn = process.kill.bind(process),
|
|
209
|
-
rmFn = fs.promises.rm,
|
|
210
248
|
sleepFn = sleep,
|
|
211
249
|
spawnDaemonFn = spawnBridgeDaemonProcess,
|
|
212
250
|
} = options;
|
|
213
251
|
const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
214
252
|
|
|
215
|
-
const stopResult = await stopBridgeDaemon({
|
|
216
|
-
transport: resolvedTransport,
|
|
217
|
-
pidPath,
|
|
218
|
-
timeoutMs,
|
|
219
|
-
pollIntervalMs,
|
|
220
|
-
pingDaemonFn,
|
|
221
|
-
readPidFn,
|
|
222
|
-
findPidByTransportFn,
|
|
223
|
-
killFn,
|
|
224
|
-
rmFn,
|
|
225
|
-
sleepFn,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
253
|
spawnDaemonFn();
|
|
229
254
|
|
|
230
255
|
const started = await waitForDaemonReachability({
|
|
@@ -36,13 +36,17 @@ import {
|
|
|
36
36
|
getBridgeTransport,
|
|
37
37
|
getSocketPath,
|
|
38
38
|
} from './config.js';
|
|
39
|
+
import { normalizeDaemonLogger } from './daemon-logger.js';
|
|
39
40
|
import { writeJsonLine } from './framing.js';
|
|
40
41
|
|
|
42
|
+
const DAEMON_VERSION = loadDaemonVersion();
|
|
43
|
+
|
|
41
44
|
/** @typedef {import('../../protocol/src/types.js').BridgeRequest} BridgeRequest */
|
|
42
45
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallParams} SetupInstallParams */
|
|
43
46
|
/** @typedef {import('../../protocol/src/types.js').SetupInstallResult} SetupInstallResult */
|
|
44
47
|
/** @typedef {import('../../protocol/src/types.js').SetupStatus} SetupStatus */
|
|
45
48
|
/** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
|
|
49
|
+
/** @typedef {import('./daemon-logger.js').DaemonLoggerLike} DaemonLoggerLike */
|
|
46
50
|
/** @typedef {import('node:net').Socket & { __clientId?: string, __extensionId?: string, __browserName?: string, __profileLabel?: string, __accessEnabled?: boolean, __lastActiveAt?: number }} ClientSocket */
|
|
47
51
|
/** @typedef {{ socket: ClientSocket, timeoutId: NodeJS.Timeout, source?: string, method?: string, targets: Set<ClientSocket>, lastErrorResponse?: import('../../protocol/src/types.js').BridgeResponse }} PendingEntry */
|
|
48
52
|
/**
|
|
@@ -95,11 +99,24 @@ function getVersionNegotiationPayload(requestedVersion) {
|
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @returns {string | null}
|
|
104
|
+
*/
|
|
105
|
+
function loadDaemonVersion() {
|
|
106
|
+
try {
|
|
107
|
+
const raw = fs.readFileSync(new URL('../../../package.json', import.meta.url), 'utf8');
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
return parsed && typeof parsed.version === 'string' ? parsed.version : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
/**
|
|
99
116
|
* @param {string} socketPath
|
|
100
117
|
* @returns {boolean}
|
|
101
118
|
*/
|
|
102
|
-
function isWindowsNamedPipePath(socketPath) {
|
|
119
|
+
export function isWindowsNamedPipePath(socketPath) {
|
|
103
120
|
return socketPath.startsWith('\\\\.\\pipe\\');
|
|
104
121
|
}
|
|
105
122
|
|
|
@@ -129,7 +146,7 @@ export class BridgeDaemon {
|
|
|
129
146
|
* listenOptions?: import('node:net').ListenOptions | null,
|
|
130
147
|
* setupStatusLoader?: () => Promise<SetupStatus>,
|
|
131
148
|
* setupInstaller?: (params: Record<string, unknown>) => Promise<SetupInstallResult>,
|
|
132
|
-
* logger?: Pick<Console, 'log' | 'error'>
|
|
149
|
+
* logger?: DaemonLoggerLike | Pick<Console, 'log' | 'error'>
|
|
133
150
|
* }} [options={}]
|
|
134
151
|
*/
|
|
135
152
|
constructor({
|
|
@@ -138,7 +155,7 @@ export class BridgeDaemon {
|
|
|
138
155
|
listenOptions = null,
|
|
139
156
|
setupStatusLoader = collectSetupStatus,
|
|
140
157
|
setupInstaller = installSetupTarget,
|
|
141
|
-
logger =
|
|
158
|
+
logger = undefined,
|
|
142
159
|
} = {}) {
|
|
143
160
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
144
161
|
this.socketPath =
|
|
@@ -146,7 +163,8 @@ export class BridgeDaemon {
|
|
|
146
163
|
this.listenOptions = listenOptions ?? getBridgeListenTarget(this.transport);
|
|
147
164
|
this.setupStatusLoader = setupStatusLoader;
|
|
148
165
|
this.setupInstaller = setupInstaller;
|
|
149
|
-
|
|
166
|
+
/** @type {DaemonLoggerLike} */
|
|
167
|
+
this.logger = normalizeDaemonLogger(logger);
|
|
150
168
|
/** @type {net.Server | null} */
|
|
151
169
|
this.server = null;
|
|
152
170
|
/** @type {net.AddressInfo | string | null} */
|
|
@@ -168,6 +186,16 @@ export class BridgeDaemon {
|
|
|
168
186
|
this.connectedExtensionsCache = null;
|
|
169
187
|
/** @type {Promise<void> | null} */
|
|
170
188
|
this.stopPromise = null;
|
|
189
|
+
/** @type {number} */
|
|
190
|
+
this.startedAt = 0;
|
|
191
|
+
/** @type {number} */
|
|
192
|
+
this.requestsProcessed = 0;
|
|
193
|
+
/** @type {number} */
|
|
194
|
+
this.requestsFailed = 0;
|
|
195
|
+
/** @type {number} */
|
|
196
|
+
this.totalResponseTimeMs = 0;
|
|
197
|
+
/** @type {Map<string, number>} */
|
|
198
|
+
this.requestStartTimes = new Map();
|
|
171
199
|
}
|
|
172
200
|
|
|
173
201
|
/**
|
|
@@ -216,6 +244,7 @@ export class BridgeDaemon {
|
|
|
216
244
|
*/
|
|
217
245
|
trackPendingRequest(requestId, pending) {
|
|
218
246
|
this.pendingRequests.set(requestId, pending);
|
|
247
|
+
this.requestStartTimes.set(requestId, Date.now());
|
|
219
248
|
this.addPendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
220
249
|
for (const targetSocket of pending.targets) {
|
|
221
250
|
this.addPendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
@@ -287,6 +316,11 @@ export class BridgeDaemon {
|
|
|
287
316
|
socket.__lastActiveAt = Date.now();
|
|
288
317
|
this.extensionSockets.set(extensionId, socket);
|
|
289
318
|
this.invalidateConnectedExtensionsCache();
|
|
319
|
+
this.logger.info('extension registered', {
|
|
320
|
+
extensionId,
|
|
321
|
+
browserName: socket.__browserName ?? null,
|
|
322
|
+
profileLabel: socket.__profileLabel ?? null,
|
|
323
|
+
});
|
|
290
324
|
void writeJsonLine(socket, { type: 'registered', role: 'extension' });
|
|
291
325
|
return;
|
|
292
326
|
}
|
|
@@ -295,6 +329,7 @@ export class BridgeDaemon {
|
|
|
295
329
|
const clientId = message.clientId || randomUUID();
|
|
296
330
|
this.agentSockets.set(clientId, socket);
|
|
297
331
|
socket.__clientId = clientId;
|
|
332
|
+
this.logger.info('agent registered', { clientId });
|
|
298
333
|
void writeJsonLine(socket, {
|
|
299
334
|
type: 'registered',
|
|
300
335
|
role: 'agent',
|
|
@@ -320,7 +355,9 @@ export class BridgeDaemon {
|
|
|
320
355
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
321
356
|
);
|
|
322
357
|
}
|
|
323
|
-
this.logger.
|
|
358
|
+
this.logger.info('Removing stale socket from previous run', {
|
|
359
|
+
socketPath: this.socketPath,
|
|
360
|
+
});
|
|
324
361
|
} catch (error) {
|
|
325
362
|
if (error instanceof Error && error.message.startsWith('Another daemon')) {
|
|
326
363
|
throw error;
|
|
@@ -333,15 +370,14 @@ export class BridgeDaemon {
|
|
|
333
370
|
this.server = net.createServer((socket) => {
|
|
334
371
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
335
372
|
typedSocket.on('error', (err) => {
|
|
336
|
-
this.logger.error
|
|
373
|
+
this.logger.error('socket error', { message: err.message });
|
|
337
374
|
});
|
|
338
375
|
parseJsonLines(typedSocket, (raw) => {
|
|
339
376
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
340
377
|
void this.handleClientMessage(typedSocket, message).catch((err) => {
|
|
341
|
-
this.logger.error
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
);
|
|
378
|
+
this.logger.error('handler error', {
|
|
379
|
+
message: err instanceof Error ? err.message : String(err),
|
|
380
|
+
});
|
|
345
381
|
});
|
|
346
382
|
});
|
|
347
383
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
@@ -362,6 +398,13 @@ export class BridgeDaemon {
|
|
|
362
398
|
await fs.promises.chmod(this.socketPath, 0o600);
|
|
363
399
|
}
|
|
364
400
|
|
|
401
|
+
this.logger.info('Daemon listening', {
|
|
402
|
+
transport: formatBridgeTransport(this.transport),
|
|
403
|
+
socketPath: this.socketPath ?? null,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
this.startedAt = Date.now();
|
|
407
|
+
|
|
365
408
|
return this;
|
|
366
409
|
}
|
|
367
410
|
|
|
@@ -483,10 +526,12 @@ export class BridgeDaemon {
|
|
|
483
526
|
if (this.extensionSockets.size === 0) {
|
|
484
527
|
const response = createSuccess(request.id, {
|
|
485
528
|
daemon: 'ok',
|
|
529
|
+
daemonVersion: DAEMON_VERSION,
|
|
486
530
|
extensionConnected: false,
|
|
487
531
|
socketPath: this.socketPath,
|
|
488
532
|
transport: formatBridgeTransport(this.transport),
|
|
489
533
|
connectedExtensions: [],
|
|
534
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
490
535
|
...getVersionNegotiationPayload(request.meta?.protocol_version),
|
|
491
536
|
});
|
|
492
537
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
@@ -495,8 +540,30 @@ export class BridgeDaemon {
|
|
|
495
540
|
}
|
|
496
541
|
|
|
497
542
|
if (request.method === 'log.tail') {
|
|
543
|
+
const limit =
|
|
544
|
+
typeof request.params.limit === 'number' ? request.params.limit : DEFAULT_LOG_TAIL_LIMIT;
|
|
498
545
|
const response = createSuccess(request.id, {
|
|
499
|
-
entries: this.recentLog.slice(-
|
|
546
|
+
entries: this.recentLog.slice(-limit),
|
|
547
|
+
});
|
|
548
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (request.method === 'daemon.metrics') {
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
const uptimeMs = this.startedAt > 0 ? now - this.startedAt : 0;
|
|
555
|
+
const avgResponseTimeMs =
|
|
556
|
+
this.requestsProcessed > 0
|
|
557
|
+
? Math.round(this.totalResponseTimeMs / this.requestsProcessed)
|
|
558
|
+
: 0;
|
|
559
|
+
const response = createSuccess(request.id, {
|
|
560
|
+
uptimeMs,
|
|
561
|
+
activeAgents: this.agentSockets.size,
|
|
562
|
+
activeExtensions: this.extensionSockets.size,
|
|
563
|
+
pendingRequests: this.pendingRequests.size,
|
|
564
|
+
requestsProcessed: this.requestsProcessed,
|
|
565
|
+
requestsFailed: this.requestsFailed,
|
|
566
|
+
avgResponseTimeMs,
|
|
500
567
|
});
|
|
501
568
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
502
569
|
return;
|
|
@@ -593,6 +660,7 @@ export class BridgeDaemon {
|
|
|
593
660
|
const pending = this.pendingRequests.get(request.id);
|
|
594
661
|
if (!pending) return;
|
|
595
662
|
this.clearPendingRequest(request.id, pending);
|
|
663
|
+
this.recordRequestCompletion(request.id, false);
|
|
596
664
|
const response = createFailure(
|
|
597
665
|
request.id,
|
|
598
666
|
ERROR_CODES.TIMEOUT,
|
|
@@ -604,8 +672,37 @@ export class BridgeDaemon {
|
|
|
604
672
|
});
|
|
605
673
|
}, this.pendingTimeoutMs),
|
|
606
674
|
});
|
|
675
|
+
this.logger.info('request routed', {
|
|
676
|
+
requestId: request.id,
|
|
677
|
+
method: request.method,
|
|
678
|
+
clientId: socket.__clientId ?? null,
|
|
679
|
+
targetCount: targets.length,
|
|
680
|
+
});
|
|
607
681
|
const broadcastPayload = { type: 'extension.request', request };
|
|
608
|
-
await Promise.all(
|
|
682
|
+
await Promise.all(
|
|
683
|
+
targets.map(async (extSocket) => {
|
|
684
|
+
try {
|
|
685
|
+
await writeJsonLine(extSocket, broadcastPayload);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
this.logger.error('request route failed', {
|
|
688
|
+
requestId: request.id,
|
|
689
|
+
method: request.method,
|
|
690
|
+
message: error instanceof Error ? error.message : String(error),
|
|
691
|
+
});
|
|
692
|
+
const pending = this.pendingRequests.get(request.id);
|
|
693
|
+
if (!pending) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this.removePendingTarget(request.id, pending, extSocket);
|
|
697
|
+
if (extSocket.__extensionId) {
|
|
698
|
+
this.extensionSockets.delete(extSocket.__extensionId);
|
|
699
|
+
this.invalidateConnectedExtensionsCache();
|
|
700
|
+
}
|
|
701
|
+
extSocket.destroy(error instanceof Error ? error : undefined);
|
|
702
|
+
await this.finishPendingRequestIfExhausted(request.id, pending);
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
);
|
|
609
706
|
}
|
|
610
707
|
|
|
611
708
|
/**
|
|
@@ -697,6 +794,7 @@ export class BridgeDaemon {
|
|
|
697
794
|
if (responseMessage.ok) {
|
|
698
795
|
clearTimeout(pending.timeoutId);
|
|
699
796
|
this.clearPendingRequest(responseMessage.id, pending);
|
|
797
|
+
this.recordRequestCompletion(responseMessage.id, true);
|
|
700
798
|
this.pushLog({
|
|
701
799
|
at: new Date().toISOString(),
|
|
702
800
|
method: responseMessage.meta?.method ?? null,
|
|
@@ -715,6 +813,8 @@ export class BridgeDaemon {
|
|
|
715
813
|
transport: formatBridgeTransport(this.transport),
|
|
716
814
|
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
717
815
|
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
816
|
+
daemonVersion: DAEMON_VERSION,
|
|
817
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
718
818
|
},
|
|
719
819
|
{
|
|
720
820
|
...responseMessage.meta,
|
|
@@ -742,11 +842,13 @@ export class BridgeDaemon {
|
|
|
742
842
|
*/
|
|
743
843
|
handleSocketClose(socket) {
|
|
744
844
|
if (socket.__extensionId) {
|
|
845
|
+
this.logger.info('extension disconnected', { extensionId: socket.__extensionId });
|
|
745
846
|
this.extensionSockets.delete(socket.__extensionId);
|
|
746
847
|
this.invalidateConnectedExtensionsCache();
|
|
747
848
|
}
|
|
748
849
|
|
|
749
850
|
if (socket.__clientId) {
|
|
851
|
+
this.logger.info('agent disconnected', { clientId: socket.__clientId });
|
|
750
852
|
this.agentSockets.delete(socket.__clientId);
|
|
751
853
|
}
|
|
752
854
|
|
|
@@ -759,6 +861,7 @@ export class BridgeDaemon {
|
|
|
759
861
|
}
|
|
760
862
|
clearTimeout(pending.timeoutId);
|
|
761
863
|
this.clearPendingRequest(id, pending);
|
|
864
|
+
this.recordRequestCompletion(id, false);
|
|
762
865
|
}
|
|
763
866
|
}
|
|
764
867
|
|
|
@@ -790,6 +893,7 @@ export class BridgeDaemon {
|
|
|
790
893
|
|
|
791
894
|
clearTimeout(pending.timeoutId);
|
|
792
895
|
this.clearPendingRequest(requestId, pending);
|
|
896
|
+
this.recordRequestCompletion(requestId, false);
|
|
793
897
|
|
|
794
898
|
const response =
|
|
795
899
|
pending.lastErrorResponse ??
|
|
@@ -815,6 +919,23 @@ export class BridgeDaemon {
|
|
|
815
919
|
});
|
|
816
920
|
}
|
|
817
921
|
|
|
922
|
+
/**
|
|
923
|
+
* @param {string} requestId
|
|
924
|
+
* @param {boolean} ok
|
|
925
|
+
* @returns {void}
|
|
926
|
+
*/
|
|
927
|
+
recordRequestCompletion(requestId, ok) {
|
|
928
|
+
const startedAt = this.requestStartTimes.get(requestId);
|
|
929
|
+
this.requestStartTimes.delete(requestId);
|
|
930
|
+
this.requestsProcessed += 1;
|
|
931
|
+
if (!ok) {
|
|
932
|
+
this.requestsFailed += 1;
|
|
933
|
+
}
|
|
934
|
+
if (typeof startedAt === 'number') {
|
|
935
|
+
this.totalResponseTimeMs += Date.now() - startedAt;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
818
939
|
/**
|
|
819
940
|
* @param {Record<string, unknown>} entry
|
|
820
941
|
* @returns {void}
|