chrome-ai-bridge 2.3.10 → 2.5.1
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 +28 -40
- package/build/extension/README.md +10 -10
- package/build/extension/background.mjs +159 -36
- package/build/extension/manifest.json +2 -2
- package/build/extension/relay-server.ts +55 -7
- package/build/extension/ui/connect.html +1 -1
- package/build/extension/ui/connect.js +46 -10
- package/build/src/cli.js +6 -1
- package/build/src/config.js +2 -4
- package/build/src/extension/relay-server.js +47 -7
- package/build/src/fast-cdp/agent-context.js +2 -2
- package/build/src/fast-cdp/{mcp-logger.js → debug-logger.js} +11 -11
- package/build/src/fast-cdp/extension-raw.js +55 -6
- package/build/src/fast-cdp/fast-chat.js +137 -98
- package/build/src/fast-cdp/network-interceptor.js +96 -26
- package/build/src/logger.js +3 -3
- package/build/src/main.js +104 -568
- package/build/src/plugin-api.js +1 -1
- package/build/src/runtime-scope.js +1 -1
- package/build/src/tools/ai-helpers.js +72 -17
- package/build/src/tools/chatgpt-gemini-web.js +1 -1
- package/build/src/tools/chatgpt-web.js +7 -7
- package/build/src/tools/gemini-web.js +10 -22
- package/build/src/tools/optional-tools.js +8 -5
- package/package.json +17 -18
- package/scripts/cab +200 -0
- package/scripts/cli.mjs +1 -1
- package/build/src/McpResponse.js +0 -60
- package/build/src/stdio-http-proxy.js +0 -157
|
@@ -55,6 +55,12 @@ export class RelayServer extends EventEmitter {
|
|
|
55
55
|
private discoveryServer: http.Server | null = null;
|
|
56
56
|
private discoveryPort: number | null = null;
|
|
57
57
|
private keepAliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
58
|
+
private _lastDiscoveryOptions: {
|
|
59
|
+
tabUrl?: string;
|
|
60
|
+
tabId?: number;
|
|
61
|
+
newTab?: boolean;
|
|
62
|
+
allowTabTakeover?: boolean;
|
|
63
|
+
} = {};
|
|
58
64
|
|
|
59
65
|
constructor(options: RelayServerOptions = {}) {
|
|
60
66
|
super();
|
|
@@ -219,6 +225,9 @@ export class RelayServer extends EventEmitter {
|
|
|
219
225
|
this.ready = true;
|
|
220
226
|
debugLog(`[RelayServer] Connection ready for tab ${this.tabId}`);
|
|
221
227
|
this.emit('ready', this.tabId);
|
|
228
|
+
// Release discovery port after WebSocket is established.
|
|
229
|
+
// 1-second grace period lets Extension finish processing the response.
|
|
230
|
+
setTimeout(() => this.stopDiscoveryServer(), 1000);
|
|
222
231
|
break;
|
|
223
232
|
case 'pong':
|
|
224
233
|
debugLog('[RelayServer] Received keep-alive pong');
|
|
@@ -333,6 +342,7 @@ export class RelayServer extends EventEmitter {
|
|
|
333
342
|
newTab?: boolean;
|
|
334
343
|
allowTabTakeover?: boolean;
|
|
335
344
|
} = {}): Promise<number | null> {
|
|
345
|
+
this._lastDiscoveryOptions = options;
|
|
336
346
|
const ports = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
337
347
|
const wsUrl = this.getConnectionURL();
|
|
338
348
|
|
|
@@ -404,6 +414,34 @@ export class RelayServer extends EventEmitter {
|
|
|
404
414
|
return null;
|
|
405
415
|
}
|
|
406
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Release the discovery HTTP server (port).
|
|
419
|
+
* Called automatically after the Extension WebSocket connects (ready event).
|
|
420
|
+
* The port becomes available for other sessions.
|
|
421
|
+
*/
|
|
422
|
+
stopDiscoveryServer(): void {
|
|
423
|
+
if (this.discoveryServer) {
|
|
424
|
+
this.discoveryServer.close();
|
|
425
|
+
debugLog(`[RelayServer] Discovery server released (port ${this.discoveryPort})`);
|
|
426
|
+
this.discoveryServer = null;
|
|
427
|
+
this.discoveryPort = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Re-acquire a discovery port (e.g. after WebSocket disconnect for reconnection).
|
|
433
|
+
* Uses the same options as the last startDiscoveryServer() call.
|
|
434
|
+
*/
|
|
435
|
+
async restartDiscoveryServer(options?: {
|
|
436
|
+
tabUrl?: string;
|
|
437
|
+
tabId?: number;
|
|
438
|
+
newTab?: boolean;
|
|
439
|
+
allowTabTakeover?: boolean;
|
|
440
|
+
}): Promise<number | null> {
|
|
441
|
+
this.stopDiscoveryServer();
|
|
442
|
+
return this.startDiscoveryServer(options || this._lastDiscoveryOptions);
|
|
443
|
+
}
|
|
444
|
+
|
|
407
445
|
/**
|
|
408
446
|
* Stop server
|
|
409
447
|
*/
|
|
@@ -425,16 +463,26 @@ export class RelayServer extends EventEmitter {
|
|
|
425
463
|
new Error('RELAY_STOPPED: Relay stopped before request completion'),
|
|
426
464
|
);
|
|
427
465
|
|
|
428
|
-
|
|
429
|
-
this.discoveryServer.close();
|
|
430
|
-
this.discoveryServer = null;
|
|
431
|
-
this.discoveryPort = null;
|
|
432
|
-
}
|
|
466
|
+
this.stopDiscoveryServer();
|
|
433
467
|
|
|
434
468
|
if (this.wss) {
|
|
469
|
+
// Capture ref before nulling so isReady() returns false immediately
|
|
470
|
+
const wss = this.wss;
|
|
471
|
+
this.wss = null;
|
|
472
|
+
|
|
435
473
|
return new Promise((resolve) => {
|
|
436
|
-
|
|
437
|
-
|
|
474
|
+
const timeout = setTimeout(() => {
|
|
475
|
+
debugLog('[RelayServer] stop() timed out after 5s — force-terminating remaining clients');
|
|
476
|
+
for (const client of wss.clients) {
|
|
477
|
+
try { client.terminate(); } catch { /* ignore */ }
|
|
478
|
+
}
|
|
479
|
+
try { wss.close(); } catch { /* ignore */ }
|
|
480
|
+
debugLog('[RelayServer] Server stopped (forced)');
|
|
481
|
+
resolve();
|
|
482
|
+
}, 5000);
|
|
483
|
+
|
|
484
|
+
wss.close(() => {
|
|
485
|
+
clearTimeout(timeout);
|
|
438
486
|
debugLog('[RelayServer] Server stopped');
|
|
439
487
|
resolve();
|
|
440
488
|
});
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
</div>
|
|
383
383
|
|
|
384
384
|
<div id="tab-selection" class="hidden">
|
|
385
|
-
<div class="section-label">Select page to expose to
|
|
385
|
+
<div class="section-label">Select page to expose to Chrome AI Bridge:</div>
|
|
386
386
|
<div class="tabs-list" id="tabs-list"></div>
|
|
387
387
|
</div>
|
|
388
388
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* chrome-ai-bridge Connect UI
|
|
3
3
|
* Extension2-style simple flow:
|
|
4
|
-
* 1.
|
|
4
|
+
* 1. Chrome AI Bridge opens connect.html?relayUrl=ws://...
|
|
5
5
|
* 2. Tab list is displayed
|
|
6
6
|
* 3. User selects tab -> Click "Connect"
|
|
7
7
|
* 4. Done
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
class ConnectUI {
|
|
11
11
|
constructor() {
|
|
12
|
-
this.
|
|
12
|
+
this.relayUrl = null;
|
|
13
13
|
this.sessionId = null;
|
|
14
14
|
this.allowTabTakeover = false;
|
|
15
15
|
this.autoMode = false;
|
|
@@ -50,7 +50,7 @@ class ConnectUI {
|
|
|
50
50
|
try {
|
|
51
51
|
// Parse URL parameters (Extension2 style: parameters are always provided)
|
|
52
52
|
const params = new URLSearchParams(window.location.search);
|
|
53
|
-
this.
|
|
53
|
+
this.relayUrl = params.get('relayUrl');
|
|
54
54
|
this.sessionId = params.get('sessionId');
|
|
55
55
|
this.allowTabTakeover = params.get('allowTabTakeover') === 'true';
|
|
56
56
|
this.autoMode = params.get('auto') === 'true';
|
|
@@ -61,8 +61,8 @@ class ConnectUI {
|
|
|
61
61
|
rawTabId && /^\d+$/.test(rawTabId) ? Number(rawTabId) : null;
|
|
62
62
|
|
|
63
63
|
// Validate relay URL
|
|
64
|
-
if (!this.
|
|
65
|
-
this.showError('Missing
|
|
64
|
+
if (!this.relayUrl) {
|
|
65
|
+
this.showError('Missing relayUrl parameter. Make sure Chrome AI Bridge is running.');
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -87,7 +87,7 @@ class ConnectUI {
|
|
|
87
87
|
|
|
88
88
|
validateRelayUrl() {
|
|
89
89
|
try {
|
|
90
|
-
const url = new URL(this.
|
|
90
|
+
const url = new URL(this.relayUrl);
|
|
91
91
|
if (!['127.0.0.1', 'localhost', '::1'].includes(url.hostname)) {
|
|
92
92
|
this.showError('Invalid relay URL: must be loopback address (127.0.0.1)');
|
|
93
93
|
return false;
|
|
@@ -109,7 +109,7 @@ class ConnectUI {
|
|
|
109
109
|
|
|
110
110
|
const relayResponse = await chrome.runtime.sendMessage({
|
|
111
111
|
type: 'connectToRelay',
|
|
112
|
-
|
|
112
|
+
relayUrl: this.relayUrl,
|
|
113
113
|
sessionId: this.sessionId,
|
|
114
114
|
});
|
|
115
115
|
if (!relayResponse || !relayResponse.success) {
|
|
@@ -118,7 +118,7 @@ class ConnectUI {
|
|
|
118
118
|
|
|
119
119
|
const connectResponse = await chrome.runtime.sendMessage({
|
|
120
120
|
type: 'connectToTab',
|
|
121
|
-
|
|
121
|
+
relayUrl: this.relayUrl,
|
|
122
122
|
tabId: this.autoTabId,
|
|
123
123
|
tabUrl: this.autoTabUrl,
|
|
124
124
|
newTab: this.autoNewTab,
|
|
@@ -263,7 +263,7 @@ class ConnectUI {
|
|
|
263
263
|
// Step 1: Connect to relay
|
|
264
264
|
const relayResponse = await chrome.runtime.sendMessage({
|
|
265
265
|
type: 'connectToRelay',
|
|
266
|
-
|
|
266
|
+
relayUrl: this.relayUrl,
|
|
267
267
|
sessionId: this.sessionId,
|
|
268
268
|
});
|
|
269
269
|
|
|
@@ -274,7 +274,7 @@ class ConnectUI {
|
|
|
274
274
|
// Step 2: Connect to tab
|
|
275
275
|
const connectResponse = await chrome.runtime.sendMessage({
|
|
276
276
|
type: 'connectToTab',
|
|
277
|
-
|
|
277
|
+
relayUrl: this.relayUrl,
|
|
278
278
|
tabId: tab.id,
|
|
279
279
|
windowId: tab.windowId,
|
|
280
280
|
sessionId: this.sessionId,
|
|
@@ -340,6 +340,18 @@ class ConnectUI {
|
|
|
340
340
|
return div.innerHTML;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
formatDiscoverySummary(summary) {
|
|
344
|
+
if (!summary || typeof summary !== 'object') {
|
|
345
|
+
return 'n/a';
|
|
346
|
+
}
|
|
347
|
+
return [
|
|
348
|
+
`new=${summary.newRelayCount ?? 0}`,
|
|
349
|
+
`ok=${summary.successCount ?? 0}`,
|
|
350
|
+
`fail=${summary.failureCount ?? 0}`,
|
|
351
|
+
`cooldown=${Boolean(summary.skippedCooldown)}`,
|
|
352
|
+
].join(', ');
|
|
353
|
+
}
|
|
354
|
+
|
|
343
355
|
// ========== Debug Panel Methods ==========
|
|
344
356
|
|
|
345
357
|
initDebugPanel() {
|
|
@@ -398,11 +410,35 @@ class ConnectUI {
|
|
|
398
410
|
// Update stats
|
|
399
411
|
const stats = response.stats;
|
|
400
412
|
const state = response.state;
|
|
413
|
+
const discovery = state.discovery || {};
|
|
401
414
|
this.debugStatsEl.innerHTML = `
|
|
402
415
|
<strong>Total Logs:</strong> ${stats.total} |
|
|
403
416
|
<strong>Active Connections:</strong> ${state.activeConnections?.length || 0} |
|
|
404
417
|
<strong>Pending:</strong> ${state.pendingTabSelection?.length || 0}
|
|
405
418
|
<br>
|
|
419
|
+
<strong>Discovery:</strong>
|
|
420
|
+
mode=${discovery.mode || 'n/a'},
|
|
421
|
+
running=${Boolean(discovery.isRunning)},
|
|
422
|
+
scheduled=${Boolean(discovery.hasScheduledTick)},
|
|
423
|
+
interval=${discovery.intervalMs ?? 'n/a'}ms,
|
|
424
|
+
streak=${discovery.emptyDiscoveryStreak ?? 'n/a'},
|
|
425
|
+
lastPort=${discovery.lastSuccessfulPort ?? 'n/a'}
|
|
426
|
+
<br>
|
|
427
|
+
<strong>Last Tick:</strong>
|
|
428
|
+
started=${discovery.lastTickStartedAt || 'n/a'},
|
|
429
|
+
done=${discovery.lastTickFinishedAt || 'n/a'},
|
|
430
|
+
dur=${discovery.lastTickDurationMs ?? 'n/a'}ms,
|
|
431
|
+
result=${this.formatDiscoverySummary(discovery.lastSummary)},
|
|
432
|
+
error=${discovery.lastError || 'none'}
|
|
433
|
+
<br>
|
|
434
|
+
<strong>Last Probe:</strong>
|
|
435
|
+
at=${discovery.lastRelayProbeAt || 'n/a'},
|
|
436
|
+
port=${discovery.lastRelayProbePort ?? 'n/a'},
|
|
437
|
+
status=${discovery.lastRelayProbeStatus || 'n/a'},
|
|
438
|
+
error=${discovery.lastRelayProbeError || 'none'},
|
|
439
|
+
keepAlive=${Boolean(discovery.keepAliveActive)},
|
|
440
|
+
alarm=${discovery.lastKeepAliveAlarmAt || 'n/a'}
|
|
441
|
+
<br>
|
|
406
442
|
<strong>By Category:</strong>
|
|
407
443
|
${Object.entries(stats.byCategory || {}).map(([cat, count]) => `${cat}: ${count}`).join(', ') || 'none'}
|
|
408
444
|
`;
|
package/build/src/cli.js
CHANGED
|
@@ -21,13 +21,18 @@ export const cliOptions = {
|
|
|
21
21
|
description: 'Port for Extension Bridge WebSocket relay server. Default: 0 (auto-assign).',
|
|
22
22
|
default: 0,
|
|
23
23
|
},
|
|
24
|
+
daemon: {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
description: 'Run as HTTP-only daemon. Used by cab CLI.',
|
|
27
|
+
default: true,
|
|
28
|
+
},
|
|
24
29
|
};
|
|
25
30
|
export function parseArguments(version, argv = process.argv) {
|
|
26
31
|
const yargsInstance = yargs(hideBin(argv))
|
|
27
32
|
.scriptName('npx chrome-ai-bridge@latest')
|
|
28
33
|
.options(cliOptions)
|
|
29
34
|
.example([
|
|
30
|
-
['$0', 'Start
|
|
35
|
+
['$0', 'Start Chrome AI Bridge daemon (requires chrome-ai-bridge extension)'],
|
|
31
36
|
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
|
|
32
37
|
['$0 --help', 'Print CLI options'],
|
|
33
38
|
]);
|
package/build/src/config.js
CHANGED
|
@@ -37,15 +37,13 @@ export const GEMINI_CONFIG = {
|
|
|
37
37
|
BASE_URL: 'https://gemini.google.com/',
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* The Primary instance exposes an HTTP endpoint on this port
|
|
42
|
-
* Secondary instances connect as stdio-to-HTTP proxies.
|
|
40
|
+
* HTTP server configuration for the daemon.
|
|
41
|
+
* The Primary instance exposes an HTTP endpoint on this port.
|
|
43
42
|
*/
|
|
44
43
|
export const IPC_CONFIG = {
|
|
45
44
|
port: Number(process.env.CAI_IPC_PORT) || 9321,
|
|
46
45
|
host: '127.0.0.1',
|
|
47
46
|
healthPath: '/health',
|
|
48
|
-
mcpPath: '/mcp',
|
|
49
47
|
};
|
|
50
48
|
/**
|
|
51
49
|
* Get session configuration from environment variables or defaults.
|
|
@@ -29,6 +29,7 @@ export class RelayServer extends EventEmitter {
|
|
|
29
29
|
discoveryServer = null;
|
|
30
30
|
discoveryPort = null;
|
|
31
31
|
keepAliveTimer = null;
|
|
32
|
+
_lastDiscoveryOptions = {};
|
|
32
33
|
constructor(options = {}) {
|
|
33
34
|
super();
|
|
34
35
|
this.host = options.host || '127.0.0.1';
|
|
@@ -172,6 +173,9 @@ export class RelayServer extends EventEmitter {
|
|
|
172
173
|
this.ready = true;
|
|
173
174
|
debugLog(`[RelayServer] Connection ready for tab ${this.tabId}`);
|
|
174
175
|
this.emit('ready', this.tabId);
|
|
176
|
+
// Release discovery port after WebSocket is established.
|
|
177
|
+
// 1-second grace period lets Extension finish processing the response.
|
|
178
|
+
setTimeout(() => this.stopDiscoveryServer(), 1000);
|
|
175
179
|
break;
|
|
176
180
|
case 'pong':
|
|
177
181
|
debugLog('[RelayServer] Received keep-alive pong');
|
|
@@ -269,6 +273,7 @@ export class RelayServer extends EventEmitter {
|
|
|
269
273
|
* Extension polls this endpoint when user clicks the extension icon.
|
|
270
274
|
*/
|
|
271
275
|
async startDiscoveryServer(options = {}) {
|
|
276
|
+
this._lastDiscoveryOptions = options;
|
|
272
277
|
const ports = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
|
|
273
278
|
const wsUrl = this.getConnectionURL();
|
|
274
279
|
for (const port of ports) {
|
|
@@ -332,6 +337,27 @@ export class RelayServer extends EventEmitter {
|
|
|
332
337
|
debugLog('[RelayServer] Could not start discovery server on any port');
|
|
333
338
|
return null;
|
|
334
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Release the discovery HTTP server (port).
|
|
342
|
+
* Called automatically after the Extension WebSocket connects (ready event).
|
|
343
|
+
* The port becomes available for other sessions.
|
|
344
|
+
*/
|
|
345
|
+
stopDiscoveryServer() {
|
|
346
|
+
if (this.discoveryServer) {
|
|
347
|
+
this.discoveryServer.close();
|
|
348
|
+
debugLog(`[RelayServer] Discovery server released (port ${this.discoveryPort})`);
|
|
349
|
+
this.discoveryServer = null;
|
|
350
|
+
this.discoveryPort = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Re-acquire a discovery port (e.g. after WebSocket disconnect for reconnection).
|
|
355
|
+
* Uses the same options as the last startDiscoveryServer() call.
|
|
356
|
+
*/
|
|
357
|
+
async restartDiscoveryServer(options) {
|
|
358
|
+
this.stopDiscoveryServer();
|
|
359
|
+
return this.startDiscoveryServer(options || this._lastDiscoveryOptions);
|
|
360
|
+
}
|
|
335
361
|
/**
|
|
336
362
|
* Stop server
|
|
337
363
|
*/
|
|
@@ -349,15 +375,29 @@ export class RelayServer extends EventEmitter {
|
|
|
349
375
|
this.ready = false;
|
|
350
376
|
this.tabId = null;
|
|
351
377
|
this.rejectPendingRequests(new Error('RELAY_STOPPED: Relay stopped before request completion'));
|
|
352
|
-
|
|
353
|
-
this.discoveryServer.close();
|
|
354
|
-
this.discoveryServer = null;
|
|
355
|
-
this.discoveryPort = null;
|
|
356
|
-
}
|
|
378
|
+
this.stopDiscoveryServer();
|
|
357
379
|
if (this.wss) {
|
|
380
|
+
// Capture ref before nulling so isReady() returns false immediately
|
|
381
|
+
const wss = this.wss;
|
|
382
|
+
this.wss = null;
|
|
358
383
|
return new Promise((resolve) => {
|
|
359
|
-
|
|
360
|
-
|
|
384
|
+
const timeout = setTimeout(() => {
|
|
385
|
+
debugLog('[RelayServer] stop() timed out after 5s — force-terminating remaining clients');
|
|
386
|
+
for (const client of wss.clients) {
|
|
387
|
+
try {
|
|
388
|
+
client.terminate();
|
|
389
|
+
}
|
|
390
|
+
catch { /* ignore */ }
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
wss.close();
|
|
394
|
+
}
|
|
395
|
+
catch { /* ignore */ }
|
|
396
|
+
debugLog('[RelayServer] Server stopped (forced)');
|
|
397
|
+
resolve();
|
|
398
|
+
}, 5000);
|
|
399
|
+
wss.close(() => {
|
|
400
|
+
clearTimeout(timeout);
|
|
361
401
|
debugLog('[RelayServer] Server stopped');
|
|
362
402
|
resolve();
|
|
363
403
|
});
|
|
@@ -18,7 +18,7 @@ let currentAgentId = null;
|
|
|
18
18
|
* 1. If CAI_AGENT_ID environment variable is set, use it + PID
|
|
19
19
|
* 2. Otherwise, generate from PID + timestamp
|
|
20
20
|
*
|
|
21
|
-
* @param clientName Optional client name
|
|
21
|
+
* @param clientName Optional client name (e.g., "claude-code")
|
|
22
22
|
* @returns Unique agent ID
|
|
23
23
|
*/
|
|
24
24
|
export function generateAgentId(clientName) {
|
|
@@ -28,7 +28,7 @@ export function generateAgentId(clientName) {
|
|
|
28
28
|
return `${envAgentId}-${process.pid}`;
|
|
29
29
|
}
|
|
30
30
|
if (clientName) {
|
|
31
|
-
// Use client name if available
|
|
31
|
+
// Use client name if available
|
|
32
32
|
return `${clientName}-${process.pid}`;
|
|
33
33
|
}
|
|
34
34
|
// Fallback: generate from PID + timestamp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Outputs logs to stderr and appends to .local/
|
|
2
|
+
* Debug Logger
|
|
3
|
+
* Outputs logs to stderr and appends to .local/debug.log
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -21,7 +21,7 @@ const LOG_LEVEL_NAMES = {
|
|
|
21
21
|
let currentLogLevel = LogLevel.DEBUG;
|
|
22
22
|
// Log file path
|
|
23
23
|
function getLogFilePath() {
|
|
24
|
-
return path.join(process.cwd(), '.local', '
|
|
24
|
+
return path.join(process.cwd(), '.local', 'debug.log');
|
|
25
25
|
}
|
|
26
26
|
// Max log file size (5MB)
|
|
27
27
|
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
@@ -52,7 +52,7 @@ function rotateLogIfNeeded(logPath) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Main logging function
|
|
54
54
|
*/
|
|
55
|
-
export function
|
|
55
|
+
export function debugLog(level, category, message, data) {
|
|
56
56
|
if (level < currentLogLevel)
|
|
57
57
|
return;
|
|
58
58
|
const timestamp = new Date().toISOString();
|
|
@@ -76,35 +76,35 @@ export function mcpLog(level, category, message, data) {
|
|
|
76
76
|
}
|
|
77
77
|
// Convenience methods
|
|
78
78
|
export function logDebug(category, message, data) {
|
|
79
|
-
|
|
79
|
+
debugLog(LogLevel.DEBUG, category, message, data);
|
|
80
80
|
}
|
|
81
81
|
export function logInfo(category, message, data) {
|
|
82
|
-
|
|
82
|
+
debugLog(LogLevel.INFO, category, message, data);
|
|
83
83
|
}
|
|
84
84
|
export function logWarn(category, message, data) {
|
|
85
|
-
|
|
85
|
+
debugLog(LogLevel.WARN, category, message, data);
|
|
86
86
|
}
|
|
87
87
|
export function logError(category, message, data) {
|
|
88
|
-
|
|
88
|
+
debugLog(LogLevel.ERROR, category, message, data);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Log connection state changes
|
|
92
92
|
*/
|
|
93
93
|
export function logConnectionState(kind, state, details) {
|
|
94
94
|
const level = state === 'disconnected' || state === 'unhealthy' ? LogLevel.WARN : LogLevel.INFO;
|
|
95
|
-
|
|
95
|
+
debugLog(level, 'connection', `${kind} ${state}`, details);
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* Log relay server events
|
|
99
99
|
*/
|
|
100
100
|
export function logRelay(event, details) {
|
|
101
101
|
const level = event === 'error' || event === 'timeout' ? LogLevel.ERROR : LogLevel.INFO;
|
|
102
|
-
|
|
102
|
+
debugLog(level, 'relay', event, details);
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Log extension communication
|
|
106
106
|
*/
|
|
107
107
|
export function logExtension(event, details) {
|
|
108
108
|
const level = event === 'timeout' ? LogLevel.ERROR : LogLevel.INFO;
|
|
109
|
-
|
|
109
|
+
debugLog(level, 'extension', event, details);
|
|
110
110
|
}
|
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
4
7
|
import { RelayServer } from '../extension/relay-server.js';
|
|
5
|
-
import { logRelay, logExtension, logInfo, logError } from './
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { logRelay, logExtension, logInfo, logError } from './debug-logger.js';
|
|
9
|
+
// Wake connect page disabled by default — it opens a chrome-extension:// URL
|
|
10
|
+
// that gets ERR_BLOCKED_BY_CLIENT and annoys users. Discovery polling is the
|
|
11
|
+
// primary mechanism; wake is only useful in rare edge cases.
|
|
12
|
+
const ENABLE_WAKE_CONNECT_PAGE = process.env.CAI_ENABLE_WAKE_CONNECT_PAGE === '1';
|
|
13
|
+
const EXTENSION_ID_ALPHABET = 'abcdefghijklmnop';
|
|
14
|
+
const EXTENSION_ID_PATTERN = /^[a-p]{32}$/;
|
|
15
|
+
const MANIFEST_FILE_URL = new URL('../../extension/manifest.json', import.meta.url);
|
|
16
|
+
let cachedExtensionId = null;
|
|
17
|
+
function deriveExtensionIdFromManifestKey(manifestKey) {
|
|
18
|
+
const derKey = Buffer.from(manifestKey, 'base64');
|
|
19
|
+
if (derKey.length === 0) {
|
|
20
|
+
throw new Error('EXT_CONFIG_ERROR: manifest key is empty or invalid base64');
|
|
21
|
+
}
|
|
22
|
+
const hash = crypto.createHash('sha256').update(derKey).digest();
|
|
23
|
+
let extensionId = '';
|
|
24
|
+
for (const byte of hash.subarray(0, 16)) {
|
|
25
|
+
extensionId += EXTENSION_ID_ALPHABET[byte >> 4];
|
|
26
|
+
extensionId += EXTENSION_ID_ALPHABET[byte & 0x0f];
|
|
27
|
+
}
|
|
28
|
+
return extensionId;
|
|
29
|
+
}
|
|
30
|
+
function resolveExtensionId() {
|
|
31
|
+
if (cachedExtensionId) {
|
|
32
|
+
return cachedExtensionId;
|
|
33
|
+
}
|
|
34
|
+
const envId = process.env.CAI_EXTENSION_ID?.trim();
|
|
35
|
+
if (envId) {
|
|
36
|
+
if (!EXTENSION_ID_PATTERN.test(envId)) {
|
|
37
|
+
throw new Error(`EXT_CONFIG_ERROR: CAI_EXTENSION_ID must match ${EXTENSION_ID_PATTERN.source}, got "${envId}"`);
|
|
38
|
+
}
|
|
39
|
+
cachedExtensionId = envId;
|
|
40
|
+
return cachedExtensionId;
|
|
41
|
+
}
|
|
42
|
+
const manifestPath = fileURLToPath(MANIFEST_FILE_URL);
|
|
43
|
+
try {
|
|
44
|
+
const manifestRaw = fs.readFileSync(manifestPath, 'utf8');
|
|
45
|
+
const manifest = JSON.parse(manifestRaw);
|
|
46
|
+
if (!manifest.key) {
|
|
47
|
+
throw new Error('manifest key is missing');
|
|
48
|
+
}
|
|
49
|
+
cachedExtensionId = deriveExtensionIdFromManifestKey(manifest.key);
|
|
50
|
+
return cachedExtensionId;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const relativePath = path.relative(process.cwd(), manifestPath);
|
|
54
|
+
throw new Error(`EXT_CONFIG_ERROR: failed to resolve extension ID from ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
9
57
|
/**
|
|
10
58
|
* Get Chrome executable path for current platform
|
|
11
59
|
*/
|
|
@@ -93,8 +141,9 @@ function spawnChromeWithConnectUrl(connectUrl) {
|
|
|
93
141
|
}
|
|
94
142
|
}
|
|
95
143
|
function buildConnectUrl(options) {
|
|
144
|
+
const extensionId = resolveExtensionId();
|
|
96
145
|
const params = new URLSearchParams();
|
|
97
|
-
params.set('
|
|
146
|
+
params.set('relayUrl', options.wsUrl);
|
|
98
147
|
params.set('sessionId', options.sessionId);
|
|
99
148
|
if (options.tabUrl)
|
|
100
149
|
params.set('tabUrl', options.tabUrl);
|
|
@@ -106,7 +155,7 @@ function buildConnectUrl(options) {
|
|
|
106
155
|
params.set('allowTabTakeover', 'true');
|
|
107
156
|
if (options.auto)
|
|
108
157
|
params.set('auto', 'true');
|
|
109
|
-
return `chrome-extension://${
|
|
158
|
+
return `chrome-extension://${extensionId}/ui/connect.html?${params.toString()}`;
|
|
110
159
|
}
|
|
111
160
|
export async function connectViaExtensionRaw(options) {
|
|
112
161
|
const startTime = Date.now();
|