@dynamicu/chromedebug-mcp 2.2.0

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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. package/src/validation/schemas.js +391 -0
@@ -0,0 +1,510 @@
1
+ // Network Tracker for Chrome Debug Full Data Recording
2
+ // Wraps fetch and XMLHttpRequest to track all network activity
3
+
4
+ class NetworkTracker {
5
+ constructor() {
6
+ this.isTracking = false;
7
+ this.originalFetch = null;
8
+ this.originalXHROpen = null;
9
+ this.originalXHRSend = null;
10
+ this.requestMap = new Map(); // Track ongoing requests
11
+ this.eventBuffer = [];
12
+ this.recordingId = null;
13
+
14
+ // Configuration
15
+ this.captureRequestBody = true;
16
+ this.captureResponseBody = true;
17
+ this.maxBodySize = 10 * 1024; // 10KB limit for request/response bodies
18
+ this.bufferFlushDelay = 2000; // Flush every 2 seconds
19
+ this.flushTimer = null;
20
+
21
+ // Callbacks and dependencies
22
+ this.onDataReady = null;
23
+ this.piiRedactor = null;
24
+ this.performanceMonitor = null;
25
+ }
26
+
27
+ init(options = {}) {
28
+ this.recordingId = options.recordingId || 'default';
29
+ this.onDataReady = options.onDataReady;
30
+ this.piiRedactor = options.piiRedactor;
31
+ this.performanceMonitor = options.performanceMonitor;
32
+
33
+ // Override settings
34
+ if (options.captureRequestBody !== undefined) this.captureRequestBody = options.captureRequestBody;
35
+ if (options.captureResponseBody !== undefined) this.captureResponseBody = options.captureResponseBody;
36
+ if (options.maxBodySize) this.maxBodySize = options.maxBodySize;
37
+ if (options.bufferFlushDelay) this.bufferFlushDelay = options.bufferFlushDelay;
38
+
39
+ console.log('[NetworkTracker] Initialized with options:', {
40
+ recordingId: this.recordingId,
41
+ captureRequestBody: this.captureRequestBody,
42
+ captureResponseBody: this.captureResponseBody,
43
+ maxBodySize: this.maxBodySize
44
+ });
45
+ }
46
+
47
+ startTracking() {
48
+ if (this.isTracking) {
49
+ console.warn('[NetworkTracker] Already tracking network requests');
50
+ return;
51
+ }
52
+
53
+ try {
54
+ this.wrapFetch();
55
+ this.wrapXMLHttpRequest();
56
+
57
+ this.isTracking = true;
58
+ this.startBufferFlusher();
59
+
60
+ console.log('[NetworkTracker] Started network request tracking');
61
+
62
+ // Record performance event
63
+ if (this.performanceMonitor) {
64
+ this.performanceMonitor.recordEvent('network_tracking_started', {
65
+ timestamp: Date.now()
66
+ });
67
+ }
68
+
69
+ } catch (error) {
70
+ console.error('[NetworkTracker] Failed to start tracking:', error);
71
+ }
72
+ }
73
+
74
+ stopTracking() {
75
+ if (!this.isTracking) return;
76
+
77
+ this.restoreFetch();
78
+ this.restoreXMLHttpRequest();
79
+
80
+ this.stopBufferFlusher();
81
+ this.flushBuffer(); // Flush any remaining data
82
+
83
+ this.isTracking = false;
84
+ console.log('[NetworkTracker] Stopped network request tracking');
85
+
86
+ // Record performance event
87
+ if (this.performanceMonitor) {
88
+ this.performanceMonitor.recordEvent('network_tracking_stopped', {
89
+ timestamp: Date.now(),
90
+ totalRequests: this.requestMap.size
91
+ });
92
+ }
93
+ }
94
+
95
+ wrapFetch() {
96
+ this.originalFetch = window.fetch;
97
+
98
+ window.fetch = async (input, init = {}) => {
99
+ const requestId = this.generateRequestId();
100
+ const startTime = Date.now();
101
+
102
+ let url, method, headers, body;
103
+
104
+ // Parse input (URL or Request object)
105
+ if (typeof input === 'string') {
106
+ url = input;
107
+ method = init.method || 'GET';
108
+ headers = init.headers || {};
109
+ body = init.body;
110
+ } else if (input instanceof Request) {
111
+ url = input.url;
112
+ method = input.method;
113
+ headers = Object.fromEntries(input.headers.entries());
114
+ body = init.body || input.body;
115
+ }
116
+
117
+ // Create request data
118
+ const requestData = {
119
+ type: 'network_request',
120
+ timestamp: startTime,
121
+ recording_id: this.recordingId,
122
+ request_id: requestId,
123
+ method: method,
124
+ url: url,
125
+ headers: this.processHeaders(headers),
126
+ body: await this.processRequestBody(body),
127
+ fetch_type: 'fetch'
128
+ };
129
+
130
+ // Store request for later correlation with response
131
+ this.requestMap.set(requestId, {
132
+ startTime,
133
+ requestData
134
+ });
135
+
136
+ try {
137
+ // Make the actual request
138
+ const response = await this.originalFetch(input, init);
139
+
140
+ // Process response
141
+ await this.handleFetchResponse(requestId, response.clone());
142
+
143
+ return response;
144
+ } catch (error) {
145
+ // Handle fetch error
146
+ this.handleRequestError(requestId, error);
147
+ throw error;
148
+ }
149
+ };
150
+ }
151
+
152
+ wrapXMLHttpRequest() {
153
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
154
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
155
+
156
+ XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
157
+ // Store request details
158
+ this._chromePilotRequestId = this._chromePilotRequestId || generateRequestId();
159
+ this._chromePilotMethod = method;
160
+ this._chromePilotUrl = url;
161
+ this._chromePilotStartTime = Date.now();
162
+
163
+ return this.originalXHROpen.call(this, method, url, async, user, password);
164
+ };
165
+
166
+ XMLHttpRequest.prototype.send = function(body) {
167
+ const requestId = this._chromePilotRequestId;
168
+ const tracker = window.networkTracker; // Reference to this instance
169
+
170
+ if (requestId && tracker) {
171
+ // Create request data
172
+ const requestData = {
173
+ type: 'network_request',
174
+ timestamp: this._chromePilotStartTime,
175
+ recording_id: tracker.recordingId,
176
+ request_id: requestId,
177
+ method: this._chromePilotMethod,
178
+ url: this._chromePilotUrl,
179
+ headers: tracker.getXHRHeaders(this),
180
+ body: tracker.processRequestBodySync(body),
181
+ fetch_type: 'xhr'
182
+ };
183
+
184
+ // Store request
185
+ tracker.requestMap.set(requestId, {
186
+ startTime: this._chromePilotStartTime,
187
+ requestData,
188
+ xhr: this
189
+ });
190
+
191
+ // Add event listeners for response
192
+ this.addEventListener('readystatechange', function() {
193
+ if (this.readyState === XMLHttpRequest.DONE) {
194
+ tracker.handleXHRResponse(requestId, this);
195
+ }
196
+ });
197
+
198
+ this.addEventListener('error', function() {
199
+ tracker.handleRequestError(requestId, new Error('XHR Error'));
200
+ });
201
+ }
202
+
203
+ return tracker.originalXHRSend.call(this, body);
204
+ };
205
+
206
+ // Store reference for XHR access
207
+ window.networkTracker = this;
208
+ }
209
+
210
+ async handleFetchResponse(requestId, response) {
211
+ const request = this.requestMap.get(requestId);
212
+ if (!request) return;
213
+
214
+ const endTime = Date.now();
215
+ const duration = endTime - request.startTime;
216
+
217
+ // Get response headers
218
+ const responseHeaders = Object.fromEntries(response.headers.entries());
219
+
220
+ // Get response body if configured
221
+ let responseBody = null;
222
+ if (this.captureResponseBody) {
223
+ try {
224
+ const contentType = response.headers.get('content-type') || '';
225
+
226
+ if (contentType.includes('application/json') ||
227
+ contentType.includes('text/') ||
228
+ contentType.includes('application/xml')) {
229
+
230
+ const text = await response.text();
231
+ responseBody = this.truncateData(text, this.maxBodySize);
232
+ }
233
+ } catch (error) {
234
+ console.warn('[NetworkTracker] Failed to read response body:', error);
235
+ }
236
+ }
237
+
238
+ // Create complete request record
239
+ const networkEvent = {
240
+ ...request.requestData,
241
+ response_status: response.status,
242
+ response_headers: this.processHeaders(responseHeaders),
243
+ response_body: responseBody,
244
+ duration_ms: duration,
245
+ completed_at: endTime
246
+ };
247
+
248
+ // Apply PII redaction
249
+ if (this.piiRedactor) {
250
+ networkEvent = this.piiRedactor.redactData(networkEvent, 'network_request');
251
+ }
252
+
253
+ this.eventBuffer.push(networkEvent);
254
+ this.requestMap.delete(requestId);
255
+
256
+ // Record performance metrics
257
+ if (this.performanceMonitor) {
258
+ this.performanceMonitor.recordEvent('network_request', {
259
+ method: networkEvent.method,
260
+ status: response.status,
261
+ duration: duration
262
+ });
263
+ }
264
+ }
265
+
266
+ handleXHRResponse(requestId, xhr) {
267
+ const request = this.requestMap.get(requestId);
268
+ if (!request) return;
269
+
270
+ const endTime = Date.now();
271
+ const duration = endTime - request.startTime;
272
+
273
+ // Get response data
274
+ let responseBody = null;
275
+ if (this.captureResponseBody && xhr.responseText) {
276
+ responseBody = this.truncateData(xhr.responseText, this.maxBodySize);
277
+ }
278
+
279
+ // Create complete request record
280
+ const networkEvent = {
281
+ ...request.requestData,
282
+ response_status: xhr.status,
283
+ response_headers: this.getXHRResponseHeaders(xhr),
284
+ response_body: responseBody,
285
+ duration_ms: duration,
286
+ completed_at: endTime
287
+ };
288
+
289
+ // Apply PII redaction
290
+ if (this.piiRedactor) {
291
+ networkEvent = this.piiRedactor.redactData(networkEvent, 'network_request');
292
+ }
293
+
294
+ this.eventBuffer.push(networkEvent);
295
+ this.requestMap.delete(requestId);
296
+
297
+ // Record performance metrics
298
+ if (this.performanceMonitor) {
299
+ this.performanceMonitor.recordEvent('network_request', {
300
+ method: networkEvent.method,
301
+ status: xhr.status,
302
+ duration: duration
303
+ });
304
+ }
305
+ }
306
+
307
+ handleRequestError(requestId, error) {
308
+ const request = this.requestMap.get(requestId);
309
+ if (!request) return;
310
+
311
+ const endTime = Date.now();
312
+ const duration = endTime - request.startTime;
313
+
314
+ const networkEvent = {
315
+ ...request.requestData,
316
+ response_status: 0,
317
+ error: error.message,
318
+ duration_ms: duration,
319
+ completed_at: endTime
320
+ };
321
+
322
+ this.eventBuffer.push(networkEvent);
323
+ this.requestMap.delete(requestId);
324
+ }
325
+
326
+ async processRequestBody(body) {
327
+ if (!this.captureRequestBody || !body) return null;
328
+
329
+ try {
330
+ if (typeof body === 'string') {
331
+ return this.truncateData(body, this.maxBodySize);
332
+ }
333
+
334
+ if (body instanceof FormData) {
335
+ const formObject = {};
336
+ for (const [key, value] of body.entries()) {
337
+ formObject[key] = typeof value === 'string' ?
338
+ this.truncateData(value, this.maxBodySize) :
339
+ '[File]';
340
+ }
341
+ return formObject;
342
+ }
343
+
344
+ if (body instanceof URLSearchParams) {
345
+ return Object.fromEntries(body.entries());
346
+ }
347
+
348
+ if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
349
+ return `[Binary Data: ${body.byteLength} bytes]`;
350
+ }
351
+
352
+ // Try to stringify other objects
353
+ const stringified = JSON.stringify(body);
354
+ return this.truncateData(stringified, this.maxBodySize);
355
+
356
+ } catch (error) {
357
+ return '[Unable to serialize request body]';
358
+ }
359
+ }
360
+
361
+ processRequestBodySync(body) {
362
+ // Synchronous version for XHR
363
+ if (!this.captureRequestBody || !body) return null;
364
+
365
+ if (typeof body === 'string') {
366
+ return this.truncateData(body, this.maxBodySize);
367
+ }
368
+
369
+ try {
370
+ const stringified = JSON.stringify(body);
371
+ return this.truncateData(stringified, this.maxBodySize);
372
+ } catch (error) {
373
+ return '[Unable to serialize request body]';
374
+ }
375
+ }
376
+
377
+ processHeaders(headers) {
378
+ if (!headers) return {};
379
+
380
+ const processed = {};
381
+
382
+ if (headers instanceof Headers) {
383
+ for (const [key, value] of headers.entries()) {
384
+ processed[key] = value;
385
+ }
386
+ } else if (typeof headers === 'object') {
387
+ Object.assign(processed, headers);
388
+ }
389
+
390
+ return processed;
391
+ }
392
+
393
+ getXHRHeaders(xhr) {
394
+ // XHR doesn't provide easy access to request headers
395
+ // This is a limitation of the XMLHttpRequest API
396
+ return {};
397
+ }
398
+
399
+ getXHRResponseHeaders(xhr) {
400
+ const headers = {};
401
+ const headerString = xhr.getAllResponseHeaders();
402
+
403
+ if (headerString) {
404
+ const lines = headerString.split('\r\n');
405
+ for (const line of lines) {
406
+ const parts = line.split(': ');
407
+ if (parts.length === 2) {
408
+ headers[parts[0].toLowerCase()] = parts[1];
409
+ }
410
+ }
411
+ }
412
+
413
+ return headers;
414
+ }
415
+
416
+ truncateData(data, maxSize) {
417
+ if (!data || typeof data !== 'string') return data;
418
+
419
+ if (data.length <= maxSize) return data;
420
+
421
+ return data.substring(0, maxSize) + `... [Truncated: ${data.length} total chars]`;
422
+ }
423
+
424
+ generateRequestId() {
425
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
426
+ }
427
+
428
+ restoreFetch() {
429
+ if (this.originalFetch) {
430
+ window.fetch = this.originalFetch;
431
+ this.originalFetch = null;
432
+ }
433
+ }
434
+
435
+ restoreXMLHttpRequest() {
436
+ if (this.originalXHROpen) {
437
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
438
+ this.originalXHROpen = null;
439
+ }
440
+
441
+ if (this.originalXHRSend) {
442
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
443
+ this.originalXHRSend = null;
444
+ }
445
+
446
+ // Clean up global reference
447
+ if (window.networkTracker === this) {
448
+ delete window.networkTracker;
449
+ }
450
+ }
451
+
452
+ startBufferFlusher() {
453
+ if (this.flushTimer) return;
454
+
455
+ this.flushTimer = setInterval(() => {
456
+ if (this.eventBuffer.length > 0) {
457
+ this.flushBuffer();
458
+ }
459
+ }, this.bufferFlushDelay);
460
+ }
461
+
462
+ stopBufferFlusher() {
463
+ if (this.flushTimer) {
464
+ clearInterval(this.flushTimer);
465
+ this.flushTimer = null;
466
+ }
467
+ }
468
+
469
+ flushBuffer() {
470
+ if (this.eventBuffer.length === 0) return;
471
+
472
+ const events = [...this.eventBuffer];
473
+ this.eventBuffer = [];
474
+
475
+ console.log(`[NetworkTracker] Flushing ${events.length} network requests`);
476
+
477
+ // Send data via callback
478
+ if (this.onDataReady) {
479
+ this.onDataReady(events);
480
+ }
481
+ }
482
+
483
+ getStats() {
484
+ return {
485
+ isTracking: this.isTracking,
486
+ pendingRequests: this.requestMap.size,
487
+ bufferSize: this.eventBuffer.length,
488
+ recordingId: this.recordingId
489
+ };
490
+ }
491
+
492
+ destroy() {
493
+ this.stopTracking();
494
+ this.eventBuffer = [];
495
+ this.requestMap.clear();
496
+ this.onDataReady = null;
497
+ this.piiRedactor = null;
498
+ this.performanceMonitor = null;
499
+ }
500
+ }
501
+
502
+ // Helper function for XHR (global scope)
503
+ function generateRequestId() {
504
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
505
+ }
506
+
507
+ // Export for use in content script
508
+ if (typeof module !== 'undefined' && module.exports) {
509
+ module.exports = NetworkTracker;
510
+ }
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Chrome Debug Offscreen Recorder</title>
6
+ </head>
7
+ <body>
8
+ <script src="frame-capture.js"></script>
9
+ </body>
10
+ </html>
@@ -0,0 +1,203 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Chrome Debug Settings</title>
5
+ <style>
6
+ body {
7
+ font-family: system-ui, -apple-system, sans-serif;
8
+ max-width: 600px;
9
+ margin: 20px auto;
10
+ padding: 20px;
11
+ background: #f5f5f5;
12
+ }
13
+
14
+ .container {
15
+ background: white;
16
+ border-radius: 8px;
17
+ padding: 30px;
18
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
19
+ }
20
+
21
+ h1 {
22
+ margin-top: 0;
23
+ color: #333;
24
+ }
25
+
26
+ .setting-group {
27
+ margin-bottom: 25px;
28
+ }
29
+
30
+ label {
31
+ display: block;
32
+ margin-bottom: 8px;
33
+ font-weight: 500;
34
+ color: #555;
35
+ }
36
+
37
+ input[type="number"] {
38
+ width: 150px;
39
+ padding: 8px 12px;
40
+ border: 1px solid #ddd;
41
+ border-radius: 4px;
42
+ font-size: 14px;
43
+ }
44
+
45
+ .help-text {
46
+ margin-top: 5px;
47
+ font-size: 13px;
48
+ color: #666;
49
+ }
50
+
51
+ button {
52
+ background: #2196F3;
53
+ color: white;
54
+ border: none;
55
+ padding: 10px 20px;
56
+ border-radius: 4px;
57
+ font-size: 14px;
58
+ cursor: pointer;
59
+ margin-right: 10px;
60
+ }
61
+
62
+ button:hover {
63
+ background: #1976D2;
64
+ }
65
+
66
+ button.secondary {
67
+ background: #757575;
68
+ }
69
+
70
+ button.secondary:hover {
71
+ background: #616161;
72
+ }
73
+
74
+ .status {
75
+ margin-top: 20px;
76
+ padding: 10px 15px;
77
+ border-radius: 4px;
78
+ font-size: 14px;
79
+ display: none;
80
+ }
81
+
82
+ .status.success {
83
+ background: #E8F5E9;
84
+ color: #2E7D32;
85
+ border: 1px solid #81C784;
86
+ }
87
+
88
+ .status.error {
89
+ background: #FFEBEE;
90
+ color: #C62828;
91
+ border: 1px solid #EF5350;
92
+ }
93
+
94
+ .status.info {
95
+ background: #E3F2FD;
96
+ color: #1565C0;
97
+ border: 1px solid #64B5F6;
98
+ }
99
+
100
+ h3 {
101
+ margin: 0 0 15px 0;
102
+ color: #333;
103
+ font-size: 16px;
104
+ }
105
+
106
+ input[type="radio"] {
107
+ margin-right: 8px;
108
+ }
109
+
110
+ label {
111
+ display: flex;
112
+ align-items: center;
113
+ margin-bottom: 12px;
114
+ cursor: pointer;
115
+ }
116
+
117
+ .site-section {
118
+ margin: 20px 0;
119
+ }
120
+
121
+ .site-section label {
122
+ display: block;
123
+ margin-bottom: 8px;
124
+ }
125
+
126
+ textarea {
127
+ width: 100%;
128
+ padding: 10px;
129
+ border: 1px solid #ddd;
130
+ border-radius: 4px;
131
+ font-size: 13px;
132
+ font-family: 'Courier New', monospace;
133
+ resize: vertical;
134
+ min-height: 120px;
135
+ }
136
+
137
+ textarea:focus {
138
+ border-color: #2196F3;
139
+ outline: none;
140
+ box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="container">
146
+ <h1>Chrome Debug Settings</h1>
147
+
148
+ <div class="setting-group">
149
+ <label for="server-port">HTTP Server Port</label>
150
+ <input type="number" id="server-port" min="1024" max="65535" placeholder="Auto">
151
+ <div class="help-text">
152
+ Leave empty for automatic port discovery. The server typically runs on port 3000.
153
+ </div>
154
+ </div>
155
+
156
+ <div class="setting-group">
157
+ <label>Server Status</label>
158
+ <button id="test-connection">Test Connection</button>
159
+ <button id="auto-detect" class="secondary">Auto Detect Port</button>
160
+ </div>
161
+
162
+ <div class="setting-group">
163
+ <h3>Site Restrictions</h3>
164
+ <label>
165
+ <input type="radio" name="site-mode" value="whitelist" checked>
166
+ Whitelist Mode (only run on specified sites)
167
+ </label>
168
+ <label>
169
+ <input type="radio" name="site-mode" value="blacklist">
170
+ Blacklist Mode (run everywhere except specified sites)
171
+ </label>
172
+
173
+ <div id="allowed-sites-section" class="site-section">
174
+ <label for="allowed-sites">Allowed Sites (Whitelist)</label>
175
+ <div class="help-text">Chrome Debug will only run on these sites. Use * for wildcards (e.g., *.example.com)</div>
176
+ <textarea id="allowed-sites" rows="6" placeholder="localhost:*&#10;127.0.0.1:*&#10;*.test&#10;*.local&#10;*.dev&#10;example.com"></textarea>
177
+ </div>
178
+
179
+ <div id="blocked-sites-section" class="site-section" style="display: none;">
180
+ <label for="blocked-sites">Blocked Sites (Blacklist)</label>
181
+ <div class="help-text">Chrome Debug will NOT run on these sites. Use * for wildcards (e.g., *.youtube.com)</div>
182
+ <textarea id="blocked-sites" rows="6" placeholder="youtube.com&#10;*.youtube.com&#10;google.com&#10;*.google.com&#10;facebook.com&#10;twitter.com"></textarea>
183
+ </div>
184
+
185
+ <div style="margin-top: 15px;">
186
+ <button id="test-current-site" class="secondary">Test Current Tab</button>
187
+ <button id="add-current-site" class="secondary">Add Current Site</button>
188
+ <button id="import-export" class="secondary">Import/Export</button>
189
+ </div>
190
+ </div>
191
+
192
+ <div id="status" class="status"></div>
193
+
194
+ <div style="margin-top: 30px;">
195
+ <button id="save">Save Settings</button>
196
+ <button id="reset" class="secondary">Reset to Default</button>
197
+ </div>
198
+ </div>
199
+
200
+ <script src="extension-config.js"></script>
201
+ <script src="options.js"></script>
202
+ </body>
203
+ </html>