chrome-ai-bridge 2.4.0 → 2.5.2

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.
@@ -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.
@@ -377,9 +377,27 @@ export class RelayServer extends EventEmitter {
377
377
  this.rejectPendingRequests(new Error('RELAY_STOPPED: Relay stopped before request completion'));
378
378
  this.stopDiscoveryServer();
379
379
  if (this.wss) {
380
+ // Capture ref before nulling so isReady() returns false immediately
381
+ const wss = this.wss;
382
+ this.wss = null;
380
383
  return new Promise((resolve) => {
381
- this.wss.close(() => {
382
- 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);
383
401
  debugLog('[RelayServer] Server stopped');
384
402
  resolve();
385
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,14 +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
+ import { logRelay, logExtension, logInfo, logError } from './debug-logger.js';
8
9
  // Wake connect page disabled by default — it opens a chrome-extension:// URL
9
10
  // that gets ERR_BLOCKED_BY_CLIENT and annoys users. Discovery polling is the
10
11
  // primary mechanism; wake is only useful in rare edge cases.
11
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
+ }
12
57
  /**
13
58
  * Get Chrome executable path for current platform
14
59
  */
@@ -96,8 +141,9 @@ function spawnChromeWithConnectUrl(connectUrl) {
96
141
  }
97
142
  }
98
143
  function buildConnectUrl(options) {
144
+ const extensionId = resolveExtensionId();
99
145
  const params = new URLSearchParams();
100
- params.set('mcpRelayUrl', options.wsUrl);
146
+ params.set('relayUrl', options.wsUrl);
101
147
  params.set('sessionId', options.sessionId);
102
148
  if (options.tabUrl)
103
149
  params.set('tabUrl', options.tabUrl);
@@ -109,7 +155,7 @@ function buildConnectUrl(options) {
109
155
  params.set('allowTabTakeover', 'true');
110
156
  if (options.auto)
111
157
  params.set('auto', 'true');
112
- return `chrome-extension://${EXTENSION_ID}/ui/connect.html?${params.toString()}`;
158
+ return `chrome-extension://${extensionId}/ui/connect.html?${params.toString()}`;
113
159
  }
114
160
  export async function connectViaExtensionRaw(options) {
115
161
  const startTime = Date.now();