chrome-ai-bridge 2.3.9 → 2.3.10

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.
@@ -0,0 +1,491 @@
1
+ /**
2
+ * chrome-ai-bridge Connect UI
3
+ * Extension2-style simple flow:
4
+ * 1. MCP server opens connect.html?mcpRelayUrl=ws://...
5
+ * 2. Tab list is displayed
6
+ * 3. User selects tab -> Click "Connect"
7
+ * 4. Done
8
+ */
9
+
10
+ class ConnectUI {
11
+ constructor() {
12
+ this.mcpRelayUrl = null;
13
+ this.sessionId = null;
14
+ this.allowTabTakeover = false;
15
+ this.autoMode = false;
16
+ this.autoTabId = null;
17
+ this.autoTabUrl = null;
18
+ this.autoNewTab = false;
19
+ this.debugPanelVisible = false;
20
+ this.autoRefreshInterval = null;
21
+
22
+ // DOM elements
23
+ this.statusEl = document.getElementById('status');
24
+ this.statusTextEl = document.getElementById('status-text');
25
+ this.statusIconEl = this.statusEl.querySelector('.status-icon');
26
+ this.tabSelectionEl = document.getElementById('tab-selection');
27
+ this.tabsListEl = document.getElementById('tabs-list');
28
+ this.connectedViewEl = document.getElementById('connected-view');
29
+ this.connectedTabTitleEl = document.getElementById('connected-tab-title');
30
+ this.connectedTabIdEl = document.getElementById('connected-tab-id');
31
+ this.disconnectBtnEl = document.getElementById('disconnect-btn');
32
+ this.errorViewEl = document.getElementById('error-view');
33
+ this.errorMessageEl = document.getElementById('error-message');
34
+
35
+ // Debug panel elements
36
+ this.debugToggleEl = document.getElementById('debug-toggle');
37
+ this.debugPanelEl = document.getElementById('debug-panel');
38
+ this.logFilterEl = document.getElementById('log-filter');
39
+ this.refreshLogsEl = document.getElementById('refresh-logs');
40
+ this.clearLogsEl = document.getElementById('clear-logs');
41
+ this.exportLogsEl = document.getElementById('export-logs');
42
+ this.debugStatsEl = document.getElementById('debug-stats');
43
+ this.logOutputEl = document.getElementById('log-output');
44
+
45
+ this.init();
46
+ this.initDebugPanel();
47
+ }
48
+
49
+ async init() {
50
+ try {
51
+ // Parse URL parameters (Extension2 style: parameters are always provided)
52
+ const params = new URLSearchParams(window.location.search);
53
+ this.mcpRelayUrl = params.get('mcpRelayUrl');
54
+ this.sessionId = params.get('sessionId');
55
+ this.allowTabTakeover = params.get('allowTabTakeover') === 'true';
56
+ this.autoMode = params.get('auto') === 'true';
57
+ this.autoTabUrl = params.get('tabUrl');
58
+ this.autoNewTab = params.get('newTab') === 'true';
59
+ const rawTabId = params.get('tabId');
60
+ this.autoTabId =
61
+ rawTabId && /^\d+$/.test(rawTabId) ? Number(rawTabId) : null;
62
+
63
+ // Validate relay URL
64
+ if (!this.mcpRelayUrl) {
65
+ this.showError('Missing mcpRelayUrl parameter. Make sure the MCP server is running.');
66
+ return;
67
+ }
68
+
69
+ if (!this.validateRelayUrl()) {
70
+ return;
71
+ }
72
+
73
+ if (this.autoMode) {
74
+ const connected = await this.tryAutoConnect();
75
+ if (connected) {
76
+ return;
77
+ }
78
+ }
79
+
80
+ // Show tab selection UI
81
+ await this.loadTabs();
82
+
83
+ } catch (error) {
84
+ this.showError(`Initialization failed: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ validateRelayUrl() {
89
+ try {
90
+ const url = new URL(this.mcpRelayUrl);
91
+ if (!['127.0.0.1', 'localhost', '::1'].includes(url.hostname)) {
92
+ this.showError('Invalid relay URL: must be loopback address (127.0.0.1)');
93
+ return false;
94
+ }
95
+ return true;
96
+ } catch {
97
+ this.showError('Invalid relay URL format');
98
+ return false;
99
+ }
100
+ }
101
+
102
+ async tryAutoConnect() {
103
+ if (!this.autoTabUrl && !this.autoTabId) {
104
+ return false;
105
+ }
106
+
107
+ try {
108
+ this.showStatus('Auto-connecting...', 'info', '⚡');
109
+
110
+ const relayResponse = await chrome.runtime.sendMessage({
111
+ type: 'connectToRelay',
112
+ mcpRelayUrl: this.mcpRelayUrl,
113
+ sessionId: this.sessionId,
114
+ });
115
+ if (!relayResponse || !relayResponse.success) {
116
+ throw new Error(relayResponse?.error || 'Relay connection failed');
117
+ }
118
+
119
+ const connectResponse = await chrome.runtime.sendMessage({
120
+ type: 'connectToTab',
121
+ mcpRelayUrl: this.mcpRelayUrl,
122
+ tabId: this.autoTabId,
123
+ tabUrl: this.autoTabUrl,
124
+ newTab: this.autoNewTab,
125
+ sessionId: this.sessionId,
126
+ allowTabTakeover: this.allowTabTakeover,
127
+ });
128
+ if (!connectResponse || !connectResponse.success) {
129
+ throw new Error(connectResponse?.error || 'Tab connection failed');
130
+ }
131
+
132
+ this.showStatus('Connected. Closing...', 'success', '✅');
133
+ setTimeout(() => {
134
+ window.close();
135
+ }, 300);
136
+ return true;
137
+ } catch (error) {
138
+ const message =
139
+ error && typeof error === 'object' && 'message' in error
140
+ ? error.message
141
+ : String(error);
142
+ this.showStatus(
143
+ `Auto-connect failed (${message}). Select tab manually.`,
144
+ 'error',
145
+ '⚠️',
146
+ );
147
+ return false;
148
+ }
149
+ }
150
+
151
+ async loadTabs() {
152
+ try {
153
+ const tabs = await chrome.tabs.query({});
154
+
155
+ // Filter out extension pages and chrome:// URLs
156
+ const filteredTabs = tabs.filter(tab => {
157
+ if (!tab.url) return false;
158
+ if (tab.url.startsWith('chrome://')) return false;
159
+ if (tab.url.startsWith('chrome-extension://')) return false;
160
+ if (tab.url.startsWith('devtools://')) return false;
161
+ return true;
162
+ });
163
+
164
+ if (filteredTabs.length === 0) {
165
+ this.showError('No tabs available. Open a web page first.');
166
+ return;
167
+ }
168
+
169
+ this.renderTabs(filteredTabs);
170
+ this.showStatus('Select a tab to connect', 'info', '📋');
171
+ this.tabSelectionEl.classList.remove('hidden');
172
+
173
+ } catch (error) {
174
+ this.showError(`Failed to load tabs: ${error.message}`);
175
+ }
176
+ }
177
+
178
+ renderTabs(tabs) {
179
+ this.tabsListEl.innerHTML = '';
180
+
181
+ // Sort tabs: active tab first, then by window/tab order
182
+ const sortedTabs = [...tabs].sort((a, b) => {
183
+ if (a.active && !b.active) return -1;
184
+ if (!a.active && b.active) return 1;
185
+ if (a.windowId !== b.windowId) return a.windowId - b.windowId;
186
+ return a.index - b.index;
187
+ });
188
+
189
+ sortedTabs.forEach(tab => {
190
+ const tabItem = document.createElement('div');
191
+ tabItem.className = `tab-item${tab.active ? ' active' : ''}`;
192
+ tabItem.dataset.tabId = tab.id;
193
+
194
+ // Favicon
195
+ const faviconEl = document.createElement('div');
196
+ faviconEl.className = 'tab-favicon';
197
+ if (tab.favIconUrl && !tab.favIconUrl.startsWith('chrome://')) {
198
+ const img = document.createElement('img');
199
+ img.src = tab.favIconUrl;
200
+ img.onerror = () => { faviconEl.textContent = '🌐'; };
201
+ faviconEl.appendChild(img);
202
+ } else {
203
+ faviconEl.textContent = '🌐';
204
+ }
205
+
206
+ // Tab info
207
+ const infoEl = document.createElement('div');
208
+ infoEl.className = 'tab-info';
209
+
210
+ const titleRow = document.createElement('div');
211
+ titleRow.className = 'tab-title-row';
212
+
213
+ const titleEl = document.createElement('span');
214
+ titleEl.className = 'tab-title';
215
+ titleEl.textContent = tab.title || 'Untitled';
216
+ titleRow.appendChild(titleEl);
217
+
218
+ if (tab.active) {
219
+ const badge = document.createElement('span');
220
+ badge.className = 'tab-active-badge';
221
+ badge.textContent = 'Active';
222
+ titleRow.appendChild(badge);
223
+ }
224
+
225
+ const urlEl = document.createElement('div');
226
+ urlEl.className = 'tab-url';
227
+ urlEl.textContent = tab.url || '';
228
+
229
+ const idEl = document.createElement('div');
230
+ idEl.className = 'tab-id';
231
+ idEl.textContent = `Tab ID: ${tab.id}`;
232
+
233
+ infoEl.appendChild(titleRow);
234
+ infoEl.appendChild(urlEl);
235
+ infoEl.appendChild(idEl);
236
+
237
+ // Connect button
238
+ const connectBtn = document.createElement('button');
239
+ connectBtn.className = 'tab-connect-btn';
240
+ connectBtn.textContent = 'Connect';
241
+ connectBtn.addEventListener('click', (e) => {
242
+ e.stopPropagation();
243
+ this.connectToTab(tab);
244
+ });
245
+
246
+ tabItem.appendChild(faviconEl);
247
+ tabItem.appendChild(infoEl);
248
+ tabItem.appendChild(connectBtn);
249
+
250
+ // Also connect on row click
251
+ tabItem.addEventListener('click', () => {
252
+ this.connectToTab(tab);
253
+ });
254
+
255
+ this.tabsListEl.appendChild(tabItem);
256
+ });
257
+ }
258
+
259
+ async connectToTab(tab) {
260
+ try {
261
+ this.showStatus(`Connecting to "${tab.title}"...`, 'info', '⏳');
262
+
263
+ // Step 1: Connect to relay
264
+ const relayResponse = await chrome.runtime.sendMessage({
265
+ type: 'connectToRelay',
266
+ mcpRelayUrl: this.mcpRelayUrl,
267
+ sessionId: this.sessionId,
268
+ });
269
+
270
+ if (!relayResponse || !relayResponse.success) {
271
+ throw new Error(relayResponse?.error || 'Relay connection failed');
272
+ }
273
+
274
+ // Step 2: Connect to tab
275
+ const connectResponse = await chrome.runtime.sendMessage({
276
+ type: 'connectToTab',
277
+ mcpRelayUrl: this.mcpRelayUrl,
278
+ tabId: tab.id,
279
+ windowId: tab.windowId,
280
+ sessionId: this.sessionId,
281
+ allowTabTakeover: this.allowTabTakeover,
282
+ });
283
+
284
+ if (!connectResponse || !connectResponse.success) {
285
+ throw new Error(connectResponse?.error || 'Tab connection failed');
286
+ }
287
+
288
+ // Show connected view
289
+ this.tabSelectionEl.classList.add('hidden');
290
+ this.connectedViewEl.classList.remove('hidden');
291
+ this.connectedTabTitleEl.textContent = tab.title || 'Untitled';
292
+ this.connectedTabIdEl.textContent = tab.id;
293
+ this.showStatus('Connected', 'success', '✅');
294
+
295
+ // Set up disconnect button
296
+ this.disconnectBtnEl.onclick = () => this.disconnect(tab.id);
297
+
298
+ } catch (error) {
299
+ this.showError(`Connection failed: ${error.message}`);
300
+ }
301
+ }
302
+
303
+ async disconnect(tabId) {
304
+ try {
305
+ await chrome.runtime.sendMessage({
306
+ type: 'disconnect',
307
+ tabId: tabId
308
+ });
309
+ } catch {
310
+ // Ignore disconnect errors
311
+ }
312
+ this.reset();
313
+ }
314
+
315
+ reset() {
316
+ this.connectedViewEl.classList.add('hidden');
317
+ this.errorViewEl.classList.add('hidden');
318
+ this.loadTabs();
319
+ }
320
+
321
+ showStatus(message, type = 'info', icon = '⏳') {
322
+ this.statusTextEl.textContent = message;
323
+ this.statusIconEl.textContent = icon;
324
+ this.statusEl.className = `status ${type}`;
325
+ this.statusEl.classList.remove('hidden');
326
+ this.errorViewEl.classList.add('hidden');
327
+ }
328
+
329
+ showError(message) {
330
+ this.showStatus(message, 'error', '❌');
331
+ this.tabSelectionEl.classList.add('hidden');
332
+ this.connectedViewEl.classList.add('hidden');
333
+ this.errorViewEl.classList.remove('hidden');
334
+ this.errorMessageEl.textContent = message;
335
+ }
336
+
337
+ escapeHtml(text) {
338
+ const div = document.createElement('div');
339
+ div.textContent = text;
340
+ return div.innerHTML;
341
+ }
342
+
343
+ // ========== Debug Panel Methods ==========
344
+
345
+ initDebugPanel() {
346
+ this.debugToggleEl.addEventListener('click', () => {
347
+ this.toggleDebugPanel();
348
+ });
349
+
350
+ this.logFilterEl.addEventListener('change', () => {
351
+ this.refreshLogs();
352
+ });
353
+
354
+ this.refreshLogsEl.addEventListener('click', () => {
355
+ this.refreshLogs();
356
+ });
357
+
358
+ this.clearLogsEl.addEventListener('click', () => {
359
+ this.clearLogs();
360
+ });
361
+
362
+ this.exportLogsEl.addEventListener('click', () => {
363
+ this.exportLogs();
364
+ });
365
+ }
366
+
367
+ toggleDebugPanel() {
368
+ this.debugPanelVisible = !this.debugPanelVisible;
369
+ if (this.debugPanelVisible) {
370
+ this.debugPanelEl.classList.remove('hidden');
371
+ this.debugToggleEl.textContent = 'Hide Debug Logs';
372
+ this.refreshLogs();
373
+ this.autoRefreshInterval = setInterval(() => this.refreshLogs(), 2000);
374
+ } else {
375
+ this.debugPanelEl.classList.add('hidden');
376
+ this.debugToggleEl.textContent = 'Show Debug Logs';
377
+ if (this.autoRefreshInterval) {
378
+ clearInterval(this.autoRefreshInterval);
379
+ this.autoRefreshInterval = null;
380
+ }
381
+ }
382
+ }
383
+
384
+ async refreshLogs() {
385
+ try {
386
+ const filter = this.logFilterEl.value || null;
387
+ const response = await chrome.runtime.sendMessage({
388
+ type: 'getDebugLogs',
389
+ filter: filter,
390
+ limit: 100
391
+ });
392
+
393
+ if (!response || !response.success) {
394
+ this.logOutputEl.textContent = 'Failed to fetch logs';
395
+ return;
396
+ }
397
+
398
+ // Update stats
399
+ const stats = response.stats;
400
+ const state = response.state;
401
+ this.debugStatsEl.innerHTML = `
402
+ <strong>Total Logs:</strong> ${stats.total} |
403
+ <strong>Active Connections:</strong> ${state.activeConnections?.length || 0} |
404
+ <strong>Pending:</strong> ${state.pendingTabSelection?.length || 0}
405
+ <br>
406
+ <strong>By Category:</strong>
407
+ ${Object.entries(stats.byCategory || {}).map(([cat, count]) => `${cat}: ${count}`).join(', ') || 'none'}
408
+ `;
409
+
410
+ // Render logs
411
+ const logs = response.logs || [];
412
+ if (logs.length === 0) {
413
+ this.logOutputEl.innerHTML = '<span style="color: #888;">No logs yet</span>';
414
+ return;
415
+ }
416
+
417
+ const html = logs.map(log => this.formatLogEntry(log)).join('');
418
+ this.logOutputEl.innerHTML = html;
419
+
420
+ // Scroll to bottom
421
+ this.logOutputEl.scrollTop = this.logOutputEl.scrollHeight;
422
+ } catch (error) {
423
+ this.logOutputEl.textContent = `Error: ${error.message}`;
424
+ }
425
+ }
426
+
427
+ formatLogEntry(log) {
428
+ const ts = log.ts ? log.ts.split('T')[1].split('.')[0] : '';
429
+ const cat = log.category || 'unknown';
430
+ const catClass = `cat-${cat}`;
431
+ const msg = this.escapeHtml(log.message || '');
432
+ const data = log.data ? this.escapeHtml(JSON.stringify(log.data)) : '';
433
+
434
+ return `<div class="log-entry">` +
435
+ `<span class="ts">${ts}</span> ` +
436
+ `<span class="${catClass}">[${cat.toUpperCase()}]</span> ` +
437
+ `<span class="msg">${msg}</span>` +
438
+ (data ? `<span class="data">${data}</span>` : '') +
439
+ `</div>`;
440
+ }
441
+
442
+ async clearLogs() {
443
+ try {
444
+ await chrome.runtime.sendMessage({ type: 'clearDebugLogs' });
445
+ this.refreshLogs();
446
+ } catch (error) {
447
+ this.showError(`Failed to clear logs: ${error.message}`);
448
+ }
449
+ }
450
+
451
+ async exportLogs() {
452
+ try {
453
+ const response = await chrome.runtime.sendMessage({
454
+ type: 'getDebugLogs',
455
+ filter: null,
456
+ limit: 500
457
+ });
458
+
459
+ if (!response || !response.success) {
460
+ this.showError('Failed to export logs');
461
+ return;
462
+ }
463
+
464
+ const exportData = {
465
+ timestamp: new Date().toISOString(),
466
+ stats: response.stats,
467
+ state: response.state,
468
+ logs: response.logs
469
+ };
470
+
471
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
472
+ const url = URL.createObjectURL(blob);
473
+ const a = document.createElement('a');
474
+ a.href = url;
475
+ a.download = `chrome-ai-bridge-debug-${Date.now()}.json`;
476
+ a.click();
477
+ URL.revokeObjectURL(url);
478
+ } catch (error) {
479
+ this.showError(`Failed to export logs: ${error.message}`);
480
+ }
481
+ }
482
+ }
483
+
484
+ // Initialize when DOM is ready
485
+ if (document.readyState === 'loading') {
486
+ document.addEventListener('DOMContentLoaded', () => {
487
+ new ConnectUI();
488
+ });
489
+ } else {
490
+ new ConnectUI();
491
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-ai-bridge",
3
- "version": "2.3.9",
3
+ "version": "2.3.10",
4
4
  "description": "MCP server bridging Chrome extension and AI assistants (ChatGPT, Gemini). Extension-only mode - no Puppeteer.",
5
5
  "type": "module",
6
6
  "bin": "./scripts/cli.mjs",
@@ -46,6 +46,7 @@
46
46
  },
47
47
  "files": [
48
48
  "build/src",
49
+ "build/extension",
49
50
  "scripts/cli.mjs",
50
51
  "scripts/browser-globals-mock.mjs",
51
52
  "README.md",