chrome-ai-bridge 2.3.1 → 2.3.3
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/build/src/config.js +24 -1
- package/build/src/main.js +201 -46
- package/build/src/process-lock.js +24 -0
- package/package.json +1 -1
- package/scripts/cli.mjs +13 -37
package/build/src/config.js
CHANGED
|
@@ -58,7 +58,30 @@ export function getSessionConfig() {
|
|
|
58
58
|
};
|
|
59
59
|
return {
|
|
60
60
|
sessionTtlMinutes: raw.ttl > 0 ? raw.ttl : 30,
|
|
61
|
-
maxAgents: raw.max > 0 ? Math.floor(raw.max) :
|
|
61
|
+
maxAgents: raw.max > 0 ? Math.floor(raw.max) : 20,
|
|
62
62
|
cleanupIntervalMinutes: raw.interval > 0 ? raw.interval : 5,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Get IPC overload protection settings from environment variables or defaults.
|
|
67
|
+
*/
|
|
68
|
+
export function getIpcGuardConfig() {
|
|
69
|
+
const raw = {
|
|
70
|
+
maxSessions: Number(process.env.CAI_IPC_MAX_SESSIONS),
|
|
71
|
+
maxQueue: Number(process.env.CAI_IPC_MAX_QUEUE),
|
|
72
|
+
queueWaitTimeoutMs: Number(process.env.CAI_IPC_QUEUE_WAIT_TIMEOUT_MS),
|
|
73
|
+
sessionIdleMs: Number(process.env.CAI_IPC_SESSION_IDLE_MS),
|
|
74
|
+
startupDelayJitterMs: Number(process.env.CAI_STARTUP_DELAY_JITTER_MS),
|
|
75
|
+
startupProcessThreshold: Number(process.env.CAI_STARTUP_PROCESS_THRESHOLD),
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
maxSessions: raw.maxSessions > 0 ? Math.floor(raw.maxSessions) : 16,
|
|
79
|
+
maxQueue: raw.maxQueue > 0 ? Math.floor(raw.maxQueue) : 64,
|
|
80
|
+
queueWaitTimeoutMs: raw.queueWaitTimeoutMs > 0 ? Math.floor(raw.queueWaitTimeoutMs) : 10_000,
|
|
81
|
+
sessionIdleMs: raw.sessionIdleMs > 0 ? Math.floor(raw.sessionIdleMs) : 300_000,
|
|
82
|
+
startupDelayJitterMs: raw.startupDelayJitterMs > 0 ? Math.floor(raw.startupDelayJitterMs) : 1_500,
|
|
83
|
+
startupProcessThreshold: raw.startupProcessThreshold > 0
|
|
84
|
+
? Math.floor(raw.startupProcessThreshold)
|
|
85
|
+
: 8,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import assert from 'node:assert';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
18
19
|
import { randomUUID } from 'node:crypto';
|
|
19
20
|
import http from 'node:http';
|
|
20
21
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -31,8 +32,8 @@ import { getFastContext } from './fast-cdp/fast-context.js';
|
|
|
31
32
|
import { cleanupAllConnections } from './fast-cdp/fast-chat.js';
|
|
32
33
|
import { generateAgentId, setAgentId } from './fast-cdp/agent-context.js';
|
|
33
34
|
import { cleanupStaleSessions } from './fast-cdp/session-manager.js';
|
|
34
|
-
import { getSessionConfig, IPC_CONFIG } from './config.js';
|
|
35
|
-
import {
|
|
35
|
+
import { getIpcGuardConfig, getSessionConfig, IPC_CONFIG } from './config.js';
|
|
36
|
+
import { releaseLock, tryAcquireLockSafe, checkExistingPrimary, updateLockPort } from './process-lock.js';
|
|
36
37
|
import { checkPrimaryHealth, startProxyMode } from './stdio-http-proxy.js';
|
|
37
38
|
function readPackageJson() {
|
|
38
39
|
const currentDir = import.meta.dirname;
|
|
@@ -56,28 +57,87 @@ logger(`Starting Chrome AI Bridge v${version} (Extension-only mode)`);
|
|
|
56
57
|
// Initialize agent ID for Agent Teams support
|
|
57
58
|
const agentId = generateAgentId();
|
|
58
59
|
setAgentId(agentId);
|
|
59
|
-
// ─── Multi-client routing ───
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// ─── Multi-client routing with retry ───
|
|
61
|
+
// Handles concurrent startup of many processes (e.g. tproj 16-pane scenario).
|
|
62
|
+
// Each process tries to become Primary or fall back to Proxy mode,
|
|
63
|
+
// with exponential backoff + jitter to avoid thundering herd.
|
|
64
|
+
const MAX_STARTUP_ATTEMPTS = 5;
|
|
65
|
+
const BASE_DELAY_MS = 300;
|
|
66
|
+
const HEALTH_CHECK_RETRIES = 3;
|
|
67
|
+
const HEALTH_CHECK_INTERVAL_MS = 500;
|
|
68
|
+
const ipcGuardConfig = getIpcGuardConfig();
|
|
69
|
+
const instanceId = randomUUID();
|
|
70
|
+
let becamePrimary = false;
|
|
71
|
+
function countLocalBridgeInstances() {
|
|
72
|
+
try {
|
|
73
|
+
const output = execFileSync('ps', ['-axo', 'command'], {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
76
|
+
});
|
|
77
|
+
const lines = output.split('\n').filter(Boolean);
|
|
78
|
+
return lines.filter(line => line.includes('chrome-ai-bridge') &&
|
|
79
|
+
(line.includes('build/src/main.js') || line.includes('scripts/cli.mjs'))).length;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 0;
|
|
68
83
|
}
|
|
69
|
-
logger(`[main] Primary (port=${existingPrimary.port}) not healthy. Starting as Primary.`);
|
|
70
84
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
async function applyStartupJitterIfNeeded() {
|
|
86
|
+
const instanceCount = countLocalBridgeInstances();
|
|
87
|
+
if (instanceCount < ipcGuardConfig.startupProcessThreshold) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const delayMs = Math.floor(Math.random() * ipcGuardConfig.startupDelayJitterMs);
|
|
91
|
+
logger(`[main] High startup concurrency detected (${instanceCount} processes). Applying jitter=${delayMs}ms.`);
|
|
92
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
76
93
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
await
|
|
94
|
+
await applyStartupJitterIfNeeded();
|
|
95
|
+
for (let attempt = 0; attempt < MAX_STARTUP_ATTEMPTS; attempt++) {
|
|
96
|
+
// 1. Try to become Primary (non-throwing)
|
|
97
|
+
const lockAcquired = await tryAcquireLockSafe(IPC_CONFIG.port, instanceId);
|
|
98
|
+
if (lockAcquired) {
|
|
99
|
+
becamePrimary = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
// 2. Lock held by another process — try to connect as Proxy
|
|
103
|
+
const existingPrimary = checkExistingPrimary();
|
|
104
|
+
if (existingPrimary && existingPrimary.port > 0) {
|
|
105
|
+
for (let hc = 0; hc < HEALTH_CHECK_RETRIES; hc++) {
|
|
106
|
+
const healthy = await checkPrimaryHealth(existingPrimary.port);
|
|
107
|
+
if (healthy) {
|
|
108
|
+
logger(`[main] Primary is healthy (port=${existingPrimary.port}). Entering proxy mode.`);
|
|
109
|
+
await startProxyMode(existingPrimary.port); // never returns
|
|
110
|
+
}
|
|
111
|
+
if (hc < HEALTH_CHECK_RETRIES - 1) {
|
|
112
|
+
const jitter = Math.random() * HEALTH_CHECK_INTERVAL_MS;
|
|
113
|
+
await new Promise(r => setTimeout(r, HEALTH_CHECK_INTERVAL_MS + jitter));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
logger(`[main] Primary (port=${existingPrimary.port}) not healthy after ${HEALTH_CHECK_RETRIES} retries.`);
|
|
117
|
+
}
|
|
118
|
+
// 3. Neither Primary nor Proxy — backoff with jitter and retry
|
|
119
|
+
if (attempt < MAX_STARTUP_ATTEMPTS - 1) {
|
|
120
|
+
const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
121
|
+
const jitter = Math.random() * BASE_DELAY_MS;
|
|
122
|
+
const delay = backoff + jitter;
|
|
123
|
+
logger(`[main] Startup attempt ${attempt + 1}/${MAX_STARTUP_ATTEMPTS} failed. Retrying in ${Math.round(delay)}ms...`);
|
|
124
|
+
await new Promise(r => setTimeout(r, delay));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!becamePrimary) {
|
|
128
|
+
// Final fallback: one last proxy attempt before giving up
|
|
129
|
+
const existingPrimary = checkExistingPrimary();
|
|
130
|
+
if (existingPrimary && existingPrimary.port > 0) {
|
|
131
|
+
const healthy = await checkPrimaryHealth(existingPrimary.port);
|
|
132
|
+
if (healthy) {
|
|
133
|
+
logger(`[main] Final fallback: entering proxy mode (port=${existingPrimary.port}).`);
|
|
134
|
+
await startProxyMode(existingPrimary.port); // never returns
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
logger('[main] Failed to start as Primary or Proxy after all retries. Exiting.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
// ─── Primary mode ───
|
|
81
141
|
// Start session cleanup timer
|
|
82
142
|
const sessionConfig = getSessionConfig();
|
|
83
143
|
const cleanupTimer = setInterval(async () => {
|
|
@@ -191,6 +251,75 @@ logDisclaimers();
|
|
|
191
251
|
// ─── IPC HTTP server (for proxy clients) ───
|
|
192
252
|
{
|
|
193
253
|
const ipcTransports = {};
|
|
254
|
+
const ipcSessionLastActivity = new Map();
|
|
255
|
+
const initQueue = [];
|
|
256
|
+
let initializingCount = 0;
|
|
257
|
+
const getSessionLoad = () => Object.keys(ipcTransports).length + initializingCount;
|
|
258
|
+
const touchIpcSession = (sessionId) => {
|
|
259
|
+
ipcSessionLastActivity.set(sessionId, Date.now());
|
|
260
|
+
};
|
|
261
|
+
const cleanupIpcSession = (sessionId) => {
|
|
262
|
+
if (ipcTransports[sessionId]) {
|
|
263
|
+
delete ipcTransports[sessionId];
|
|
264
|
+
}
|
|
265
|
+
ipcSessionLastActivity.delete(sessionId);
|
|
266
|
+
drainInitQueue();
|
|
267
|
+
};
|
|
268
|
+
function sendJsonRpcError(res, code, message, id = null, statusCode = 400) {
|
|
269
|
+
res.writeHead(statusCode).end(JSON.stringify({
|
|
270
|
+
jsonrpc: '2.0',
|
|
271
|
+
error: { code, message },
|
|
272
|
+
id,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
function drainInitQueue() {
|
|
276
|
+
while (initQueue.length > 0 && getSessionLoad() < ipcGuardConfig.maxSessions) {
|
|
277
|
+
const waiter = initQueue.shift();
|
|
278
|
+
if (!waiter)
|
|
279
|
+
break;
|
|
280
|
+
clearTimeout(waiter.timeout);
|
|
281
|
+
waiter.resolve();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function waitForInitCapacity() {
|
|
285
|
+
if (getSessionLoad() < ipcGuardConfig.maxSessions) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (initQueue.length >= ipcGuardConfig.maxQueue) {
|
|
289
|
+
throw new Error('SERVER_QUEUE_FULL');
|
|
290
|
+
}
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
const timeout = setTimeout(() => {
|
|
293
|
+
const index = initQueue.findIndex(item => item.resolve === resolve);
|
|
294
|
+
if (index >= 0) {
|
|
295
|
+
initQueue.splice(index, 1);
|
|
296
|
+
}
|
|
297
|
+
reject(new Error('SERVER_BUSY_TIMEOUT'));
|
|
298
|
+
}, ipcGuardConfig.queueWaitTimeoutMs);
|
|
299
|
+
timeout.unref();
|
|
300
|
+
initQueue.push({ resolve, reject, timeout });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const idleCleanupTimer = setInterval(async () => {
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
const staleSessionIds = Array.from(ipcSessionLastActivity.entries())
|
|
306
|
+
.filter(([, lastActivity]) => now - lastActivity > ipcGuardConfig.sessionIdleMs)
|
|
307
|
+
.map(([sessionId]) => sessionId);
|
|
308
|
+
if (staleSessionIds.length === 0) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
logger(`[ipc] Closing ${staleSessionIds.length} idle session(s) older than ${ipcGuardConfig.sessionIdleMs}ms.`);
|
|
312
|
+
for (const staleSessionId of staleSessionIds) {
|
|
313
|
+
try {
|
|
314
|
+
await ipcTransports[staleSessionId]?.close();
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Ignore transport close errors and continue cleanup.
|
|
318
|
+
}
|
|
319
|
+
cleanupIpcSession(staleSessionId);
|
|
320
|
+
}
|
|
321
|
+
}, Math.max(10_000, Math.min(60_000, Math.floor(ipcGuardConfig.sessionIdleMs / 2))));
|
|
322
|
+
idleCleanupTimer.unref();
|
|
194
323
|
const ipcServer = http.createServer(async (req, res) => {
|
|
195
324
|
if (!req.url || !req.method) {
|
|
196
325
|
res.writeHead(400).end();
|
|
@@ -199,7 +328,15 @@ logDisclaimers();
|
|
|
199
328
|
const url = new URL(req.url, `http://${IPC_CONFIG.host}:${IPC_CONFIG.port}`);
|
|
200
329
|
// Health endpoint
|
|
201
330
|
if (url.pathname === IPC_CONFIG.healthPath) {
|
|
202
|
-
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
331
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({
|
|
332
|
+
status: 'ok',
|
|
333
|
+
pid: process.pid,
|
|
334
|
+
version,
|
|
335
|
+
instanceId,
|
|
336
|
+
activeSessions: Object.keys(ipcTransports).length,
|
|
337
|
+
queuedInitializations: initQueue.length,
|
|
338
|
+
sessionCapacity: ipcGuardConfig.maxSessions,
|
|
339
|
+
}));
|
|
203
340
|
return;
|
|
204
341
|
}
|
|
205
342
|
// MCP endpoint
|
|
@@ -227,40 +364,54 @@ logDisclaimers();
|
|
|
227
364
|
json = body ? JSON.parse(body) : null;
|
|
228
365
|
}
|
|
229
366
|
catch {
|
|
230
|
-
res
|
|
231
|
-
jsonrpc: '2.0',
|
|
232
|
-
error: { code: -32700, message: 'Parse error' },
|
|
233
|
-
id: null,
|
|
234
|
-
}));
|
|
367
|
+
sendJsonRpcError(res, -32700, 'Parse error');
|
|
235
368
|
return;
|
|
236
369
|
}
|
|
237
370
|
let ipcTransport;
|
|
238
371
|
if (sessionId && ipcTransports[sessionId]) {
|
|
239
372
|
ipcTransport = ipcTransports[sessionId];
|
|
373
|
+
touchIpcSession(sessionId);
|
|
240
374
|
}
|
|
241
375
|
else if (!sessionId && isInitializeRequest(json)) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (ipcTransport?.sessionId) {
|
|
250
|
-
delete ipcTransports[ipcTransport.sessionId];
|
|
376
|
+
try {
|
|
377
|
+
await waitForInitCapacity();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
const message = error instanceof Error ? error.message : 'SERVER_BUSY_TIMEOUT';
|
|
381
|
+
if (message === 'SERVER_QUEUE_FULL') {
|
|
382
|
+
sendJsonRpcError(res, -32002, message, null, 503);
|
|
251
383
|
}
|
|
252
|
-
|
|
253
|
-
|
|
384
|
+
else {
|
|
385
|
+
sendJsonRpcError(res, -32001, message, null, 503);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
initializingCount++;
|
|
390
|
+
try {
|
|
391
|
+
ipcTransport = new StreamableHTTPServerTransport({
|
|
392
|
+
sessionIdGenerator: () => randomUUID(),
|
|
393
|
+
onsessioninitialized: newSessionId => {
|
|
394
|
+
ipcTransports[newSessionId] = ipcTransport;
|
|
395
|
+
touchIpcSession(newSessionId);
|
|
396
|
+
},
|
|
397
|
+
onsessionclosed: closedSessionId => {
|
|
398
|
+
cleanupIpcSession(closedSessionId);
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
ipcTransport.onclose = () => {
|
|
402
|
+
if (ipcTransport?.sessionId) {
|
|
403
|
+
cleanupIpcSession(ipcTransport.sessionId);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
await server.connect(ipcTransport);
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
initializingCount = Math.max(0, initializingCount - 1);
|
|
410
|
+
drainInitQueue();
|
|
411
|
+
}
|
|
254
412
|
}
|
|
255
413
|
else {
|
|
256
|
-
res
|
|
257
|
-
jsonrpc: '2.0',
|
|
258
|
-
error: {
|
|
259
|
-
code: -32000,
|
|
260
|
-
message: 'Bad Request: No valid session ID provided',
|
|
261
|
-
},
|
|
262
|
-
id: null,
|
|
263
|
-
}));
|
|
414
|
+
sendJsonRpcError(res, -32000, 'Bad Request: No valid session ID provided');
|
|
264
415
|
return;
|
|
265
416
|
}
|
|
266
417
|
try {
|
|
@@ -288,8 +439,12 @@ logDisclaimers();
|
|
|
288
439
|
res.writeHead(400).end('Invalid or missing session ID');
|
|
289
440
|
return;
|
|
290
441
|
}
|
|
442
|
+
touchIpcSession(sessionId);
|
|
291
443
|
try {
|
|
292
444
|
await ipcTransports[sessionId].handleRequest(req, res);
|
|
445
|
+
if (req.method === 'DELETE') {
|
|
446
|
+
cleanupIpcSession(sessionId);
|
|
447
|
+
}
|
|
293
448
|
}
|
|
294
449
|
catch (error) {
|
|
295
450
|
if (!res.headersSent) {
|
|
@@ -133,6 +133,30 @@ async function handleExistingLock() {
|
|
|
133
133
|
logger(`[process-lock] Primary is alive (pid=${info.pid}, port=${info.port}). Cannot acquire lock.`);
|
|
134
134
|
return false;
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Try to acquire lock without throwing on failure.
|
|
138
|
+
* Returns true if lock acquired, false if another process holds it.
|
|
139
|
+
* Used by the retry-based startup loop in main.ts.
|
|
140
|
+
*/
|
|
141
|
+
export async function tryAcquireLockSafe(port, instanceId) {
|
|
142
|
+
const fd = tryCreateLock(port, instanceId);
|
|
143
|
+
if (fd !== null) {
|
|
144
|
+
lockFd = fd;
|
|
145
|
+
logger(`[process-lock] Lock acquired (pid=${process.pid}, port=${port}, instanceId=${instanceId.slice(0, 8)})`);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
const canRetry = await handleExistingLock();
|
|
149
|
+
if (!canRetry) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const fd2 = tryCreateLock(port, instanceId);
|
|
153
|
+
if (fd2 !== null) {
|
|
154
|
+
lockFd = fd2;
|
|
155
|
+
logger(`[process-lock] Lock acquired after cleanup (pid=${process.pid}, port=${port})`);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
136
160
|
/**
|
|
137
161
|
* Acquire an exclusive process lock. Call once at startup for Primary mode.
|
|
138
162
|
*
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -2,48 +2,24 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI Entry Point for chrome-ai-bridge
|
|
4
4
|
*
|
|
5
|
-
* This
|
|
6
|
-
*
|
|
7
|
-
* chrome-ai-bridge (if globally installed)
|
|
8
|
-
*
|
|
9
|
-
* Launches the MCP server with browser globals mock:
|
|
10
|
-
* - Loads browser-globals-mock.mjs BEFORE main.js
|
|
11
|
-
* - Ensures chrome-devtools-frontend modules work in Node.js
|
|
12
|
-
* - Simple execution: no wrapper, no hot-reload
|
|
5
|
+
* This entrypoint runs the MCP server in-process to avoid spawning an extra
|
|
6
|
+
* wrapper process per client (important for multi-pane usage).
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
|
-
import {spawn} from 'node:child_process';
|
|
16
|
-
import process from 'node:process';
|
|
17
|
-
import {fileURLToPath} from 'node:url';
|
|
18
9
|
import path from 'node:path';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
19
12
|
|
|
20
|
-
// Resolve paths
|
|
21
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
14
|
const mockPath = path.join(__dirname, 'browser-globals-mock.mjs');
|
|
23
15
|
const mainPath = path.join(__dirname, '..', 'build', 'src', 'main.js');
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
stdio: 'inherit',
|
|
36
|
-
env: process.env,
|
|
37
|
-
},
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
child.on('exit', (code, signal) => {
|
|
41
|
-
if (signal) {
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
process.exit(code ?? 0);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Forward signals
|
|
48
|
-
process.on('SIGTERM', () => child?.kill('SIGTERM'));
|
|
49
|
-
process.on('SIGINT', () => child?.kill('SIGINT'));
|
|
17
|
+
try {
|
|
18
|
+
// Ensure browser globals are defined before loading main server modules.
|
|
19
|
+
await import(pathToFileURL(mockPath).href);
|
|
20
|
+
await import(pathToFileURL(mainPath).href);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
23
|
+
console.error(`[cli] Failed to start chrome-ai-bridge: ${message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|