@browserbridge/bbx 1.1.0 → 1.2.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/package.json +1 -1
- package/packages/agent-client/src/cli.js +6 -4
- package/packages/agent-client/src/client.js +105 -4
- package/packages/agent-client/src/command-registry.js +1 -1
- package/packages/mcp-server/src/handlers-capture.js +279 -0
- package/packages/mcp-server/src/handlers-dom.js +196 -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 +296 -0
- package/packages/mcp-server/src/handlers.js +59 -1176
- package/packages/mcp-server/src/server.js +1 -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 +16 -0
- package/packages/native-host/src/daemon-logger.js +157 -0
- package/packages/native-host/src/daemon-process.js +42 -16
- package/packages/native-host/src/daemon.js +106 -10
- package/packages/protocol/src/capabilities.js +1 -0
- package/packages/protocol/src/registry.js +2 -0
- package/packages/protocol/src/types.js +1 -1
|
@@ -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
|
|
@@ -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,6 +10,7 @@
|
|
|
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));
|
|
@@ -24,4 +25,19 @@ try {
|
|
|
24
25
|
process.stderr.write(
|
|
25
26
|
`Browser Bridge: native host auto-install skipped (${message}).\nRun \`bbx install\` manually if needed.\n`
|
|
26
27
|
);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const restartResult = await restartBridgeDaemonIfRunning();
|
|
33
|
+
if (restartResult) {
|
|
34
|
+
process.stdout.write(
|
|
35
|
+
'Browser Bridge: restarted the local daemon to use the updated install.\n'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
process.stderr.write(
|
|
41
|
+
`Browser Bridge: native host installed, but daemon restart failed (${message}).\nRun \`bbx restart\` if needed.\n`
|
|
42
|
+
);
|
|
27
43
|
}
|
|
@@ -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
|
+
}
|
|
@@ -196,6 +196,48 @@ export async function stopBridgeDaemon(options = {}) {
|
|
|
196
196
|
* }>}
|
|
197
197
|
*/
|
|
198
198
|
export async function restartBridgeDaemon(options = {}) {
|
|
199
|
+
const stopResult = await stopBridgeDaemon(options);
|
|
200
|
+
return restartBridgeDaemonAfterStop(stopResult, options);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Restart the daemon only when one is already running. This is useful during
|
|
205
|
+
* package upgrades where the launcher changed and the in-memory daemon should
|
|
206
|
+
* pick up the new install, without eagerly starting a fresh background process.
|
|
207
|
+
*
|
|
208
|
+
* @param {RestartBridgeDaemonOptions} [options={}]
|
|
209
|
+
* @returns {Promise<{
|
|
210
|
+
* transport: string,
|
|
211
|
+
* socketPath: string,
|
|
212
|
+
* pidPath: string,
|
|
213
|
+
* pid: number | null,
|
|
214
|
+
* previouslyRunning: boolean,
|
|
215
|
+
* previousPid: number | null,
|
|
216
|
+
* removedStaleSocket: boolean,
|
|
217
|
+
* } | null>}
|
|
218
|
+
*/
|
|
219
|
+
export async function restartBridgeDaemonIfRunning(options = {}) {
|
|
220
|
+
const stopResult = await stopBridgeDaemon(options);
|
|
221
|
+
if (!stopResult.previouslyRunning) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return restartBridgeDaemonAfterStop(stopResult, options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {Awaited<ReturnType<typeof stopBridgeDaemon>>} stopResult
|
|
229
|
+
* @param {RestartBridgeDaemonOptions} [options={}]
|
|
230
|
+
* @returns {Promise<{
|
|
231
|
+
* transport: string,
|
|
232
|
+
* socketPath: string,
|
|
233
|
+
* pidPath: string,
|
|
234
|
+
* pid: number | null,
|
|
235
|
+
* previouslyRunning: boolean,
|
|
236
|
+
* previousPid: number | null,
|
|
237
|
+
* removedStaleSocket: boolean,
|
|
238
|
+
* }>}
|
|
239
|
+
*/
|
|
240
|
+
async function restartBridgeDaemonAfterStop(stopResult, options = {}) {
|
|
199
241
|
const {
|
|
200
242
|
transport = getBridgeTransport(),
|
|
201
243
|
socketPath = undefined,
|
|
@@ -204,27 +246,11 @@ export async function restartBridgeDaemon(options = {}) {
|
|
|
204
246
|
pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
|
|
205
247
|
pingDaemonFn = pingExistingDaemon,
|
|
206
248
|
readPidFn = readDaemonPidFile,
|
|
207
|
-
findPidByTransportFn = findDaemonPidByTransport,
|
|
208
|
-
killFn = process.kill.bind(process),
|
|
209
|
-
rmFn = fs.promises.rm,
|
|
210
249
|
sleepFn = sleep,
|
|
211
250
|
spawnDaemonFn = spawnBridgeDaemonProcess,
|
|
212
251
|
} = options;
|
|
213
252
|
const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
214
253
|
|
|
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
254
|
spawnDaemonFn();
|
|
229
255
|
|
|
230
256
|
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);
|
|
@@ -232,6 +261,7 @@ export class BridgeDaemon {
|
|
|
232
261
|
return undefined;
|
|
233
262
|
}
|
|
234
263
|
this.pendingRequests.delete(requestId);
|
|
264
|
+
this.requestStartTimes.delete(requestId);
|
|
235
265
|
this.removePendingRequestIndex(this.pendingRequestsByOwnerSocket, pending.socket, requestId);
|
|
236
266
|
for (const targetSocket of pending.targets) {
|
|
237
267
|
this.removePendingRequestIndex(this.pendingRequestsByTargetSocket, targetSocket, requestId);
|
|
@@ -287,6 +317,11 @@ export class BridgeDaemon {
|
|
|
287
317
|
socket.__lastActiveAt = Date.now();
|
|
288
318
|
this.extensionSockets.set(extensionId, socket);
|
|
289
319
|
this.invalidateConnectedExtensionsCache();
|
|
320
|
+
this.logger.info('extension registered', {
|
|
321
|
+
extensionId,
|
|
322
|
+
browserName: socket.__browserName ?? null,
|
|
323
|
+
profileLabel: socket.__profileLabel ?? null,
|
|
324
|
+
});
|
|
290
325
|
void writeJsonLine(socket, { type: 'registered', role: 'extension' });
|
|
291
326
|
return;
|
|
292
327
|
}
|
|
@@ -295,6 +330,7 @@ export class BridgeDaemon {
|
|
|
295
330
|
const clientId = message.clientId || randomUUID();
|
|
296
331
|
this.agentSockets.set(clientId, socket);
|
|
297
332
|
socket.__clientId = clientId;
|
|
333
|
+
this.logger.info('agent registered', { clientId });
|
|
298
334
|
void writeJsonLine(socket, {
|
|
299
335
|
type: 'registered',
|
|
300
336
|
role: 'agent',
|
|
@@ -320,7 +356,9 @@ export class BridgeDaemon {
|
|
|
320
356
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
321
357
|
);
|
|
322
358
|
}
|
|
323
|
-
this.logger.
|
|
359
|
+
this.logger.info('Removing stale socket from previous run', {
|
|
360
|
+
socketPath: this.socketPath,
|
|
361
|
+
});
|
|
324
362
|
} catch (error) {
|
|
325
363
|
if (error instanceof Error && error.message.startsWith('Another daemon')) {
|
|
326
364
|
throw error;
|
|
@@ -333,15 +371,14 @@ export class BridgeDaemon {
|
|
|
333
371
|
this.server = net.createServer((socket) => {
|
|
334
372
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
335
373
|
typedSocket.on('error', (err) => {
|
|
336
|
-
this.logger.error
|
|
374
|
+
this.logger.error('socket error', { message: err.message });
|
|
337
375
|
});
|
|
338
376
|
parseJsonLines(typedSocket, (raw) => {
|
|
339
377
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
340
378
|
void this.handleClientMessage(typedSocket, message).catch((err) => {
|
|
341
|
-
this.logger.error
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
);
|
|
379
|
+
this.logger.error('handler error', {
|
|
380
|
+
message: err instanceof Error ? err.message : String(err),
|
|
381
|
+
});
|
|
345
382
|
});
|
|
346
383
|
});
|
|
347
384
|
typedSocket.on('close', () => this.handleSocketClose(typedSocket));
|
|
@@ -362,6 +399,13 @@ export class BridgeDaemon {
|
|
|
362
399
|
await fs.promises.chmod(this.socketPath, 0o600);
|
|
363
400
|
}
|
|
364
401
|
|
|
402
|
+
this.logger.info('Daemon listening', {
|
|
403
|
+
transport: formatBridgeTransport(this.transport),
|
|
404
|
+
socketPath: this.socketPath ?? null,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
this.startedAt = Date.now();
|
|
408
|
+
|
|
365
409
|
return this;
|
|
366
410
|
}
|
|
367
411
|
|
|
@@ -483,10 +527,12 @@ export class BridgeDaemon {
|
|
|
483
527
|
if (this.extensionSockets.size === 0) {
|
|
484
528
|
const response = createSuccess(request.id, {
|
|
485
529
|
daemon: 'ok',
|
|
530
|
+
daemonVersion: DAEMON_VERSION,
|
|
486
531
|
extensionConnected: false,
|
|
487
532
|
socketPath: this.socketPath,
|
|
488
533
|
transport: formatBridgeTransport(this.transport),
|
|
489
534
|
connectedExtensions: [],
|
|
535
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
490
536
|
...getVersionNegotiationPayload(request.meta?.protocol_version),
|
|
491
537
|
});
|
|
492
538
|
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
@@ -502,6 +548,26 @@ export class BridgeDaemon {
|
|
|
502
548
|
return;
|
|
503
549
|
}
|
|
504
550
|
|
|
551
|
+
if (request.method === 'daemon.metrics') {
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
const uptimeMs = this.startedAt > 0 ? now - this.startedAt : 0;
|
|
554
|
+
const avgResponseTimeMs =
|
|
555
|
+
this.requestsProcessed > 0
|
|
556
|
+
? Math.round(this.totalResponseTimeMs / this.requestsProcessed)
|
|
557
|
+
: 0;
|
|
558
|
+
const response = createSuccess(request.id, {
|
|
559
|
+
uptimeMs,
|
|
560
|
+
activeAgents: this.agentSockets.size,
|
|
561
|
+
activeExtensions: this.extensionSockets.size,
|
|
562
|
+
pendingRequests: this.pendingRequests.size,
|
|
563
|
+
requestsProcessed: this.requestsProcessed,
|
|
564
|
+
requestsFailed: this.requestsFailed,
|
|
565
|
+
avgResponseTimeMs,
|
|
566
|
+
});
|
|
567
|
+
await writeJsonLine(socket, { type: 'agent.response', response });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
505
571
|
if (request.method === 'setup.get_status') {
|
|
506
572
|
const response = createSuccess(request.id, await this.setupStatusLoader(), {
|
|
507
573
|
method: request.method,
|
|
@@ -593,6 +659,7 @@ export class BridgeDaemon {
|
|
|
593
659
|
const pending = this.pendingRequests.get(request.id);
|
|
594
660
|
if (!pending) return;
|
|
595
661
|
this.clearPendingRequest(request.id, pending);
|
|
662
|
+
this.recordRequestCompletion(request.id, false);
|
|
596
663
|
const response = createFailure(
|
|
597
664
|
request.id,
|
|
598
665
|
ERROR_CODES.TIMEOUT,
|
|
@@ -604,6 +671,12 @@ export class BridgeDaemon {
|
|
|
604
671
|
});
|
|
605
672
|
}, this.pendingTimeoutMs),
|
|
606
673
|
});
|
|
674
|
+
this.logger.info('request routed', {
|
|
675
|
+
requestId: request.id,
|
|
676
|
+
method: request.method,
|
|
677
|
+
clientId: socket.__clientId ?? null,
|
|
678
|
+
targetCount: targets.length,
|
|
679
|
+
});
|
|
607
680
|
const broadcastPayload = { type: 'extension.request', request };
|
|
608
681
|
await Promise.all(targets.map((extSocket) => writeJsonLine(extSocket, broadcastPayload)));
|
|
609
682
|
}
|
|
@@ -697,6 +770,7 @@ export class BridgeDaemon {
|
|
|
697
770
|
if (responseMessage.ok) {
|
|
698
771
|
clearTimeout(pending.timeoutId);
|
|
699
772
|
this.clearPendingRequest(responseMessage.id, pending);
|
|
773
|
+
this.recordRequestCompletion(responseMessage.id, true);
|
|
700
774
|
this.pushLog({
|
|
701
775
|
at: new Date().toISOString(),
|
|
702
776
|
method: responseMessage.meta?.method ?? null,
|
|
@@ -715,6 +789,8 @@ export class BridgeDaemon {
|
|
|
715
789
|
transport: formatBridgeTransport(this.transport),
|
|
716
790
|
connectedExtensions: this.getConnectedExtensionsSnapshot(),
|
|
717
791
|
.../** @type {Record<string, unknown>} */ (responseMessage.result),
|
|
792
|
+
daemonVersion: DAEMON_VERSION,
|
|
793
|
+
daemon_supported_versions: SUPPORTED_VERSIONS,
|
|
718
794
|
},
|
|
719
795
|
{
|
|
720
796
|
...responseMessage.meta,
|
|
@@ -742,11 +818,13 @@ export class BridgeDaemon {
|
|
|
742
818
|
*/
|
|
743
819
|
handleSocketClose(socket) {
|
|
744
820
|
if (socket.__extensionId) {
|
|
821
|
+
this.logger.info('extension disconnected', { extensionId: socket.__extensionId });
|
|
745
822
|
this.extensionSockets.delete(socket.__extensionId);
|
|
746
823
|
this.invalidateConnectedExtensionsCache();
|
|
747
824
|
}
|
|
748
825
|
|
|
749
826
|
if (socket.__clientId) {
|
|
827
|
+
this.logger.info('agent disconnected', { clientId: socket.__clientId });
|
|
750
828
|
this.agentSockets.delete(socket.__clientId);
|
|
751
829
|
}
|
|
752
830
|
|
|
@@ -759,6 +837,7 @@ export class BridgeDaemon {
|
|
|
759
837
|
}
|
|
760
838
|
clearTimeout(pending.timeoutId);
|
|
761
839
|
this.clearPendingRequest(id, pending);
|
|
840
|
+
this.recordRequestCompletion(id, false);
|
|
762
841
|
}
|
|
763
842
|
}
|
|
764
843
|
|
|
@@ -790,6 +869,7 @@ export class BridgeDaemon {
|
|
|
790
869
|
|
|
791
870
|
clearTimeout(pending.timeoutId);
|
|
792
871
|
this.clearPendingRequest(requestId, pending);
|
|
872
|
+
this.recordRequestCompletion(requestId, false);
|
|
793
873
|
|
|
794
874
|
const response =
|
|
795
875
|
pending.lastErrorResponse ??
|
|
@@ -815,6 +895,22 @@ export class BridgeDaemon {
|
|
|
815
895
|
});
|
|
816
896
|
}
|
|
817
897
|
|
|
898
|
+
/**
|
|
899
|
+
* @param {string} requestId
|
|
900
|
+
* @param {boolean} ok
|
|
901
|
+
* @returns {void}
|
|
902
|
+
*/
|
|
903
|
+
recordRequestCompletion(requestId, ok) {
|
|
904
|
+
const startedAt = this.requestStartTimes.get(requestId);
|
|
905
|
+
this.requestsProcessed += 1;
|
|
906
|
+
if (!ok) {
|
|
907
|
+
this.requestsFailed += 1;
|
|
908
|
+
}
|
|
909
|
+
if (typeof startedAt === 'number') {
|
|
910
|
+
this.totalResponseTimeMs += Date.now() - startedAt;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
818
914
|
/**
|
|
819
915
|
* @param {Record<string, unknown>} entry
|
|
820
916
|
* @returns {void}
|
|
@@ -82,6 +82,7 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
82
82
|
'performance.get_metrics': 'Read browser performance metrics.',
|
|
83
83
|
'log.tail': 'Tail recent bridge log entries.',
|
|
84
84
|
'health.ping': 'Check daemon, extension, and access-routing health.',
|
|
85
|
+
'daemon.metrics': 'Daemon health and performance metrics.',
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
/**
|
|
@@ -124,6 +125,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
124
125
|
),
|
|
125
126
|
'log.tail': createRegistryEntry('log.tail', 'system', false, [], 'trivial'),
|
|
126
127
|
'health.ping': createRegistryEntry('health.ping', 'system', false, [], 'trivial'),
|
|
128
|
+
'daemon.metrics': createRegistryEntry('daemon.metrics', 'system', false, [], 'trivial'),
|
|
127
129
|
// tabs — trivial
|
|
128
130
|
'tabs.list': createRegistryEntry('tabs.list', 'tabs', false, [], 'trivial'),
|
|
129
131
|
'tabs.create': createRegistryEntry('tabs.create', 'tabs', false, ['url', 'active'], 'trivial'),
|
|
@@ -15,7 +15,7 @@ export {};
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* @typedef {'access.request' | 'tabs.list' | 'tabs.create' | 'tabs.close' | 'skill.get_runtime_context' | 'setup.get_status' | 'setup.install' | 'page.get_state' | 'page.evaluate' | 'page.get_console' | 'page.wait_for_load_state' | 'page.get_storage' | 'page.get_text' | 'page.get_network' | 'navigation.navigate' | 'navigation.reload' | 'navigation.go_back' | 'navigation.go_forward' | 'dom.query' | 'dom.describe' | 'dom.get_text' | 'dom.get_attributes' | 'dom.wait_for' | 'dom.find_by_text' | 'dom.find_by_role' | 'dom.get_html' | 'dom.get_accessibility_tree' | 'layout.get_box_model' | 'layout.hit_test' | 'styles.get_computed' | 'styles.get_matched_rules' | 'viewport.scroll' | 'viewport.resize' | 'input.click' | 'input.focus' | 'input.type' | 'input.press_key' | 'input.set_checked' | 'input.select_option' | 'input.hover' | 'input.drag' | 'input.scroll_into_view' | 'screenshot.capture_region' | 'screenshot.capture_element' | 'screenshot.capture_full_page' | 'patch.apply_styles' | 'patch.apply_dom' | 'patch.list' | 'patch.rollback' | 'patch.commit_session_baseline' | 'cdp.get_document' | 'cdp.get_dom_snapshot' | 'cdp.get_box_model' | 'cdp.get_computed_styles_for_node' | 'cdp.dispatch_key_event' | 'performance.get_metrics' | 'log.tail' | 'health.ping'} BridgeMethod
|
|
18
|
+
* @typedef {'access.request' | 'tabs.list' | 'tabs.create' | 'tabs.close' | 'skill.get_runtime_context' | 'setup.get_status' | 'setup.install' | 'page.get_state' | 'page.evaluate' | 'page.get_console' | 'page.wait_for_load_state' | 'page.get_storage' | 'page.get_text' | 'page.get_network' | 'navigation.navigate' | 'navigation.reload' | 'navigation.go_back' | 'navigation.go_forward' | 'dom.query' | 'dom.describe' | 'dom.get_text' | 'dom.get_attributes' | 'dom.wait_for' | 'dom.find_by_text' | 'dom.find_by_role' | 'dom.get_html' | 'dom.get_accessibility_tree' | 'layout.get_box_model' | 'layout.hit_test' | 'styles.get_computed' | 'styles.get_matched_rules' | 'viewport.scroll' | 'viewport.resize' | 'input.click' | 'input.focus' | 'input.type' | 'input.press_key' | 'input.set_checked' | 'input.select_option' | 'input.hover' | 'input.drag' | 'input.scroll_into_view' | 'screenshot.capture_region' | 'screenshot.capture_element' | 'screenshot.capture_full_page' | 'patch.apply_styles' | 'patch.apply_dom' | 'patch.list' | 'patch.rollback' | 'patch.commit_session_baseline' | 'cdp.get_document' | 'cdp.get_dom_snapshot' | 'cdp.get_box_model' | 'cdp.get_computed_styles_for_node' | 'cdp.dispatch_key_event' | 'performance.get_metrics' | 'log.tail' | 'health.ping' | 'daemon.metrics'} BridgeMethod
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/**
|