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.
- package/build/extension/README.md +181 -0
- package/build/extension/background.mjs +1263 -0
- package/build/extension/debug-logger.mjs +148 -0
- package/build/extension/icons/icon-128.png +0 -0
- package/build/extension/icons/icon-16.png +0 -0
- package/build/extension/icons/icon-32.png +0 -0
- package/build/extension/icons/icon-48.png +0 -0
- package/build/extension/icons/icon.svg +19 -0
- package/build/extension/manifest.json +28 -0
- package/build/extension/relay-server.ts +505 -0
- package/build/extension/ui/connect.html +429 -0
- package/build/extension/ui/connect.js +491 -0
- package/package.json +2 -1
|
@@ -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.
|
|
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",
|