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.
@@ -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
- if (this.discoveryServer) {
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
- this.wss!.close(() => {
437
- this.wss = null;
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 MCP server:</div>
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. MCP server opens connect.html?mcpRelayUrl=ws://...
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.mcpRelayUrl = null;
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.mcpRelayUrl = params.get('mcpRelayUrl');
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.mcpRelayUrl) {
65
- this.showError('Missing mcpRelayUrl parameter. Make sure the MCP server is running.');
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.mcpRelayUrl);
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
- mcpRelayUrl: this.mcpRelayUrl,
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
- mcpRelayUrl: this.mcpRelayUrl,
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
- mcpRelayUrl: this.mcpRelayUrl,
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
- mcpRelayUrl: this.mcpRelayUrl,
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 MCP server (requires chrome-ai-bridge extension)'],
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
  ]);
@@ -37,15 +37,13 @@ export const GEMINI_CONFIG = {
37
37
  BASE_URL: 'https://gemini.google.com/',
38
38
  };
39
39
  /**
40
- * IPC configuration for multi-client MCP support.
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
- if (this.discoveryServer) {
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
- this.wss.close(() => {
360
- this.wss = null;
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 from MCP initialize (e.g., "claude-code")
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 (from MCP initialize)
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
- * MCP Debug Logger
3
- * Outputs logs to stderr and appends to .local/mcp-debug.log
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', 'mcp-debug.log');
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 mcpLog(level, category, message, data) {
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
- mcpLog(LogLevel.DEBUG, category, message, data);
79
+ debugLog(LogLevel.DEBUG, category, message, data);
80
80
  }
81
81
  export function logInfo(category, message, data) {
82
- mcpLog(LogLevel.INFO, category, message, data);
82
+ debugLog(LogLevel.INFO, category, message, data);
83
83
  }
84
84
  export function logWarn(category, message, data) {
85
- mcpLog(LogLevel.WARN, category, message, data);
85
+ debugLog(LogLevel.WARN, category, message, data);
86
86
  }
87
87
  export function logError(category, message, data) {
88
- mcpLog(LogLevel.ERROR, category, message, data);
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
- mcpLog(level, 'connection', `${kind} ${state}`, details);
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
- mcpLog(level, 'relay', event, details);
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
- mcpLog(level, 'extension', event, details);
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 './mcp-logger.js';
6
- // Stable extension ID (from manifest.json key)
7
- const EXTENSION_ID = 'ibjplbopgmcacpmfpnaeoloepdhenlbm';
8
- const ENABLE_WAKE_CONNECT_PAGE = process.env.CAI_ENABLE_WAKE_CONNECT_PAGE !== '0';
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('mcpRelayUrl', options.wsUrl);
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://${EXTENSION_ID}/ui/connect.html?${params.toString()}`;
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();