@rashidazarang/airtable-mcp 1.6.0 → 2.1.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 (116) hide show
  1. package/.github/ISSUE_TEMPLATE/bug-report.yml +173 -0
  2. package/.github/ISSUE_TEMPLATE/feature-request.yml +209 -0
  3. package/.github/ISSUE_TEMPLATE/security-report.yml +216 -0
  4. package/.github/pull_request_template.md +245 -0
  5. package/.github/workflows/ci-cd.yml +408 -0
  6. package/.github/workflows/security-audit.yml +316 -0
  7. package/API_DOCUMENTATION.md +897 -0
  8. package/CODE_OF_CONDUCT.md +181 -0
  9. package/Dockerfile.production +127 -0
  10. package/README.md +1 -0
  11. package/airtable-clipper/CHANGELOG.md +198 -0
  12. package/airtable-clipper/CHROME_STORE_SUBMISSION.md +343 -0
  13. package/airtable-clipper/LAUNCH_STRATEGY.md +495 -0
  14. package/airtable-clipper/LICENSE +21 -0
  15. package/airtable-clipper/OAUTH_SETUP.md +51 -0
  16. package/airtable-clipper/PRIVACY_POLICY.md +187 -0
  17. package/airtable-clipper/README.md +575 -0
  18. package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +273 -0
  19. package/airtable-clipper/build.sh +85 -0
  20. package/airtable-clipper/docs/QUICK_START.md +99 -0
  21. package/airtable-clipper/docs/SETUP.md +291 -0
  22. package/airtable-clipper/extension/background.js +337 -0
  23. package/airtable-clipper/extension/base-setup.html +324 -0
  24. package/airtable-clipper/extension/base-setup.js +471 -0
  25. package/airtable-clipper/extension/content.js +771 -0
  26. package/airtable-clipper/extension/icons/README.md +69 -0
  27. package/airtable-clipper/extension/icons/icon-16.png +3 -0
  28. package/airtable-clipper/extension/manifest.json +73 -0
  29. package/airtable-clipper/extension/popup.html +144 -0
  30. package/airtable-clipper/extension/popup.js +475 -0
  31. package/airtable-clipper/extension/styles/content.css +229 -0
  32. package/airtable-clipper/extension/styles/popup.css +477 -0
  33. package/airtable-clipper/privacy-policy.md +63 -0
  34. package/airtable-clipper/releases/v1.0.0/background.js +337 -0
  35. package/airtable-clipper/releases/v1.0.0/base-setup.html +324 -0
  36. package/airtable-clipper/releases/v1.0.0/base-setup.js +471 -0
  37. package/airtable-clipper/releases/v1.0.0/content.js +771 -0
  38. package/airtable-clipper/releases/v1.0.0/icons/README.md +69 -0
  39. package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +2 -0
  40. package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +3 -0
  41. package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +2 -0
  42. package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +2 -0
  43. package/airtable-clipper/releases/v1.0.0/manifest.json +73 -0
  44. package/airtable-clipper/releases/v1.0.0/popup.html +144 -0
  45. package/airtable-clipper/releases/v1.0.0/popup.js +475 -0
  46. package/airtable-clipper/releases/v1.0.0/sidepanel.html +25 -0
  47. package/airtable-clipper/releases/v1.0.0/styles/content.css +229 -0
  48. package/airtable-clipper/releases/v1.0.0/styles/popup.css +477 -0
  49. package/airtable-clipper/releases/v1.0.1/background.js +337 -0
  50. package/airtable-clipper/releases/v1.0.1/base-setup.html +324 -0
  51. package/airtable-clipper/releases/v1.0.1/base-setup.js +471 -0
  52. package/airtable-clipper/releases/v1.0.1/content.js +771 -0
  53. package/airtable-clipper/releases/v1.0.1/icons/README.md +69 -0
  54. package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +2 -0
  55. package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +3 -0
  56. package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +2 -0
  57. package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +2 -0
  58. package/airtable-clipper/releases/v1.0.1/manifest.json +70 -0
  59. package/airtable-clipper/releases/v1.0.1/popup.html +157 -0
  60. package/airtable-clipper/releases/v1.0.1/popup.js +562 -0
  61. package/airtable-clipper/releases/v1.0.1/sidepanel.html +25 -0
  62. package/airtable-clipper/releases/v1.0.1/styles/content.css +229 -0
  63. package/airtable-clipper/releases/v1.0.1/styles/popup.css +647 -0
  64. package/airtable-clipper/releases/v1.0.2/background.js +337 -0
  65. package/airtable-clipper/releases/v1.0.2/base-setup.html +324 -0
  66. package/airtable-clipper/releases/v1.0.2/base-setup.js +471 -0
  67. package/airtable-clipper/releases/v1.0.2/content.js +771 -0
  68. package/airtable-clipper/releases/v1.0.2/icons/README.md +69 -0
  69. package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +2 -0
  70. package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +3 -0
  71. package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +2 -0
  72. package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +2 -0
  73. package/airtable-clipper/releases/v1.0.2/manifest.json +62 -0
  74. package/airtable-clipper/releases/v1.0.2/popup.html +157 -0
  75. package/airtable-clipper/releases/v1.0.2/popup.js +567 -0
  76. package/airtable-clipper/releases/v1.0.2/sidepanel.html +25 -0
  77. package/airtable-clipper/releases/v1.0.2/styles/content.css +229 -0
  78. package/airtable-clipper/releases/v1.0.2/styles/popup.css +647 -0
  79. package/airtable-clipper/terms-of-service.md +124 -0
  80. package/airtable-clipper/test-credentials.md +61 -0
  81. package/airtable-clipper/test-extension/background.js +337 -0
  82. package/airtable-clipper/test-extension/base-setup.html +324 -0
  83. package/airtable-clipper/test-extension/base-setup.js +471 -0
  84. package/airtable-clipper/test-extension/content.js +873 -0
  85. package/airtable-clipper/test-extension/icons/README.md +69 -0
  86. package/airtable-clipper/test-extension/icons/icon-128.png +2 -0
  87. package/airtable-clipper/test-extension/icons/icon-16.png +3 -0
  88. package/airtable-clipper/test-extension/icons/icon-32.png +2 -0
  89. package/airtable-clipper/test-extension/icons/icon-48.png +2 -0
  90. package/airtable-clipper/test-extension/manifest.json +72 -0
  91. package/airtable-clipper/test-extension/popup.html +274 -0
  92. package/airtable-clipper/test-extension/popup.js +729 -0
  93. package/airtable-clipper/test-extension/sidepanel.html +25 -0
  94. package/airtable-clipper/test-extension/styles/content.css +229 -0
  95. package/airtable-clipper/test-extension/styles/popup.css +794 -0
  96. package/airtable_mcp_v2.js +1505 -0
  97. package/airtable_mcp_v2_oauth.js +1048 -0
  98. package/airtable_mcp_v3_advanced.js +1161 -0
  99. package/airtable_simple_production.js +532 -0
  100. package/docker-compose.production.yml +366 -0
  101. package/helm/airtable-mcp/Chart.yaml +122 -0
  102. package/helm/airtable-mcp/values.yaml +538 -0
  103. package/k8s/deployment.yaml +402 -0
  104. package/k8s/namespace.yaml +108 -0
  105. package/k8s/service.yaml +194 -0
  106. package/monitoring/alerts.yml +289 -0
  107. package/monitoring/prometheus.yml +224 -0
  108. package/package.json +6 -6
  109. package/.claude/settings.local.json +0 -12
  110. package/airtable-mcp-1.1.0.tgz +0 -0
  111. package/airtable_enhanced.js +0 -499
  112. package/airtable_simple_v1.2.4_backup.js +0 -277
  113. package/airtable_v1.4.0.js +0 -654
  114. package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
  115. package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
  116. package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
@@ -0,0 +1,729 @@
1
+ // Airtable Clipper - Simple Popup Controller
2
+ import { AirtableClient, AirtableError } from './lib/airtable-client.js';
3
+
4
+ class PopupController {
5
+ constructor() {
6
+ this.client = null;
7
+ this.currentTab = null;
8
+ this.isConnected = false;
9
+ this.init();
10
+ }
11
+
12
+ async init() {
13
+ try {
14
+ // Get current tab
15
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
16
+ this.currentTab = tabs[0];
17
+
18
+ // Check connection status
19
+ await this.checkConnection();
20
+
21
+ // Update UI for current page
22
+ this.updateUIForCurrentPage();
23
+
24
+ // Bind all event listeners
25
+ this.bindEvents();
26
+ } catch (error) {
27
+ console.error('Init failed:', error);
28
+ this.showToast('Failed to initialize extension', 'error');
29
+ }
30
+ }
31
+
32
+ async checkConnection() {
33
+ try {
34
+ const result = await chrome.storage.local.get(['airtableToken', 'baseId']);
35
+
36
+ if (result.airtableToken && result.baseId) {
37
+ // Test the connection
38
+ this.client = new AirtableClient(result.airtableToken, result.baseId);
39
+ const testResult = await this.client.testConnection();
40
+
41
+ if (testResult.success) {
42
+ this.isConnected = true;
43
+ this.showConnectedView();
44
+ } else {
45
+ this.showSetupView();
46
+ }
47
+ } else {
48
+ this.showSetupView();
49
+ }
50
+ } catch (error) {
51
+ console.error('Connection check failed:', error);
52
+ this.showSetupView();
53
+ }
54
+ }
55
+
56
+ showSetupView() {
57
+ document.getElementById('setupContainer').style.display = 'block';
58
+ document.getElementById('connectedContainer').style.display = 'none';
59
+ this.isConnected = false;
60
+ }
61
+
62
+ showConnectedView() {
63
+ document.getElementById('setupContainer').style.display = 'none';
64
+ document.getElementById('connectedContainer').style.display = 'block';
65
+ this.isConnected = true;
66
+ }
67
+
68
+ updateUIForCurrentPage() {
69
+ if (!this.currentTab || !this.isConnected) return;
70
+
71
+ try {
72
+ const url = new URL(this.currentTab.url);
73
+ const domain = url.hostname.replace('www.', '');
74
+
75
+ // Update page info
76
+ document.getElementById('pageTitle').textContent = this.currentTab.title || 'Current Page';
77
+ document.getElementById('pageUrl').textContent = domain;
78
+
79
+ // Set page icon based on domain
80
+ let icon = '🌐';
81
+ if (domain.includes('linkedin.com')) {
82
+ icon = '💼';
83
+ document.getElementById('linkedinBtn').style.display = 'block';
84
+ document.getElementById('saveTarget').textContent = 'to Contacts';
85
+ } else {
86
+ document.getElementById('linkedinBtn').style.display = 'none';
87
+ document.getElementById('saveTarget').textContent = 'to Web Clips';
88
+ }
89
+
90
+ document.getElementById('pageIcon').textContent = icon;
91
+ } catch (error) {
92
+ console.error('Failed to update UI for page:', error);
93
+ }
94
+ }
95
+
96
+ bindEvents() {
97
+ console.log('Binding events...');
98
+
99
+ // Setup view events
100
+ const oauthBtn = document.getElementById('oauthBtn');
101
+ const manualBtn = document.getElementById('manualConnectBtn');
102
+ const confirmBtn = document.getElementById('confirmBaseBtn');
103
+ const setupBtn = document.getElementById('setupBtn');
104
+
105
+ console.log('Found buttons:', {
106
+ oauth: !!oauthBtn,
107
+ manual: !!manualBtn,
108
+ confirm: !!confirmBtn,
109
+ setup: !!setupBtn
110
+ });
111
+
112
+ oauthBtn?.addEventListener('click', () => {
113
+ console.log('OAuth button clicked');
114
+ this.handleOAuthConnect();
115
+ });
116
+
117
+ manualBtn?.addEventListener('click', () => {
118
+ console.log('Manual connect button clicked');
119
+ this.handleManualConnect();
120
+ });
121
+
122
+ confirmBtn?.addEventListener('click', () => {
123
+ console.log('Confirm base button clicked');
124
+ this.handleConfirmBase();
125
+ });
126
+
127
+ setupBtn?.addEventListener('click', () => {
128
+ console.log('Setup button clicked');
129
+ this.handleSetupDatabase();
130
+ });
131
+
132
+ // Connected view events
133
+ document.getElementById('saveBtn')?.addEventListener('click', () => this.handleQuickSave());
134
+ document.getElementById('linkedinBtn')?.addEventListener('click', () => this.handleLinkedInSave());
135
+ document.getElementById('bulkBtn')?.addEventListener('click', () => this.handleBulkMode());
136
+ document.getElementById('settingsBtn')?.addEventListener('click', () => this.showSettings());
137
+
138
+ // Settings events
139
+ document.getElementById('closeSettings')?.addEventListener('click', () => this.hideSettings());
140
+ document.getElementById('disconnectBtn')?.addEventListener('click', () => this.handleDisconnect());
141
+
142
+ // Auto-save settings
143
+ document.getElementById('autoSave')?.addEventListener('change', () => this.saveSettings());
144
+ document.getElementById('notifications')?.addEventListener('change', () => this.saveSettings());
145
+ }
146
+
147
+ async handleConnect() {
148
+ const connectBtn = document.getElementById('connectBtn');
149
+ const btnText = connectBtn.querySelector('.btn-text');
150
+ const btnSpinner = document.getElementById('connectSpinner');
151
+
152
+ const token = document.getElementById('airtableToken').value.trim();
153
+ const baseId = document.getElementById('baseId').value.trim();
154
+
155
+ if (!token || !baseId) {
156
+ this.showToast('Please enter both token and base ID', 'error');
157
+ return;
158
+ }
159
+
160
+ // Show loading state
161
+ connectBtn.disabled = true;
162
+ btnText.textContent = 'Connecting...';
163
+ btnSpinner.style.display = 'block';
164
+
165
+ try {
166
+ // Test connection
167
+ const testClient = new AirtableClient(token, baseId);
168
+ const result = await testClient.testConnection();
169
+
170
+ if (result.success) {
171
+ // Save credentials
172
+ await chrome.storage.local.set({
173
+ airtableToken: token,
174
+ baseId: baseId
175
+ });
176
+
177
+ this.client = testClient;
178
+ this.isConnected = true;
179
+ this.showConnectedView();
180
+ this.updateUIForCurrentPage();
181
+ this.showToast('Connected successfully!', 'success');
182
+ } else {
183
+ throw new Error(result.error || 'Connection test failed');
184
+ }
185
+ } catch (error) {
186
+ console.error('Connection failed:', error);
187
+ let errorMessage = 'Connection failed';
188
+
189
+ if (error instanceof AirtableError) {
190
+ if (error.message.includes('401')) {
191
+ errorMessage = 'Invalid token. Please check your Personal Access Token.';
192
+ } else if (error.message.includes('404')) {
193
+ errorMessage = 'Base not found. Please check your Base ID.';
194
+ } else {
195
+ errorMessage = error.message;
196
+ }
197
+ } else if (error.message) {
198
+ errorMessage = error.message;
199
+ }
200
+
201
+ this.showToast(errorMessage, 'error');
202
+ } finally {
203
+ // Reset button state
204
+ connectBtn.disabled = false;
205
+ btnText.textContent = 'Connect to Airtable';
206
+ btnSpinner.style.display = 'none';
207
+ }
208
+ }
209
+
210
+ async handleOAuthConnect() {
211
+ const connectBtn = document.getElementById('oauthBtn');
212
+ const btnText = connectBtn.querySelector('.btn-text');
213
+ const btnSpinner = document.getElementById('oauthSpinner');
214
+
215
+ // Show loading state
216
+ connectBtn.disabled = true;
217
+ btnText.textContent = 'Connecting...';
218
+ btnSpinner.style.display = 'block';
219
+
220
+ try {
221
+ // Import and use OAuth
222
+ const { AirtableOAuth } = await import('./lib/airtable-oauth.js');
223
+ const oauth = new AirtableOAuth();
224
+
225
+ // Check if OAuth is available
226
+ if (!chrome.identity || !chrome.identity.launchWebAuthFlow) {
227
+ throw new Error('OAuth not available in this context. Please use manual connection.');
228
+ }
229
+
230
+ // Authenticate
231
+ const tokens = await oauth.authenticate();
232
+
233
+ // Get user's bases
234
+ const basesData = await oauth.getUserBases();
235
+
236
+ if (basesData && basesData.bases && basesData.bases.length > 0) {
237
+ // Show base selector
238
+ this.populateBaseOptions(basesData.bases);
239
+ document.getElementById('baseSelector').style.display = 'block';
240
+ this.showToast('Authentication successful! Please select a base.', 'success');
241
+ } else {
242
+ throw new Error('No Airtable bases found');
243
+ }
244
+
245
+ } catch (error) {
246
+ console.error('OAuth failed:', error);
247
+ this.showToast(`OAuth failed: ${error.message}. Try manual connection below.`, 'error');
248
+
249
+ // Expand manual auth section
250
+ const manualAuth = document.querySelector('.manual-auth');
251
+ if (manualAuth) {
252
+ manualAuth.open = true;
253
+ }
254
+
255
+ // Focus on token input
256
+ setTimeout(() => {
257
+ document.getElementById('tokenInput')?.focus();
258
+ }, 500);
259
+
260
+ } finally {
261
+ // Reset button state
262
+ connectBtn.disabled = false;
263
+ btnText.textContent = 'Connect with Airtable';
264
+ btnSpinner.style.display = 'none';
265
+ }
266
+ }
267
+
268
+ populateBaseOptions(bases) {
269
+ const baseSelect = document.getElementById('baseSelect');
270
+ baseSelect.innerHTML = '<option value="">Choose a base...</option>';
271
+
272
+ bases.forEach(base => {
273
+ const option = document.createElement('option');
274
+ option.value = base.id;
275
+ option.textContent = base.name;
276
+ baseSelect.appendChild(option);
277
+ });
278
+
279
+ // Store bases for later use
280
+ this.availableBases = bases;
281
+ }
282
+
283
+ async handleConfirmBase() {
284
+ const baseSelect = document.getElementById('baseSelect');
285
+ const selectedBaseId = baseSelect.value;
286
+
287
+ if (!selectedBaseId) {
288
+ this.showToast('Please select a base', 'error');
289
+ return;
290
+ }
291
+
292
+ const selectedBase = this.availableBases.find(base => base.id === selectedBaseId);
293
+
294
+ try {
295
+ // Save base selection
296
+ await chrome.storage.local.set({
297
+ selectedBaseId: selectedBaseId,
298
+ selectedBaseName: selectedBase.name,
299
+ useOAuth: true
300
+ });
301
+
302
+ // Create client with OAuth token
303
+ const { AirtableOAuth } = await import('./lib/airtable-oauth.js');
304
+ const oauth = new AirtableOAuth();
305
+ const token = await oauth.getAccessToken();
306
+ this.client = new AirtableClient(token, selectedBaseId);
307
+
308
+ // Switch to connected view
309
+ this.isConnected = true;
310
+ this.showConnectedView();
311
+ this.updateUIForCurrentPage();
312
+ this.showToast(`Connected to ${selectedBase.name}!`, 'success');
313
+ } catch (error) {
314
+ console.error('Base confirmation failed:', error);
315
+ this.showToast(`Failed to connect: ${error.message}`, 'error');
316
+ }
317
+ }
318
+
319
+ async handleManualConnect() {
320
+ const connectBtn = document.getElementById('manualConnectBtn');
321
+ const btnText = connectBtn.querySelector('.btn-text');
322
+ const btnSpinner = document.getElementById('manualSpinner');
323
+
324
+ const token = document.getElementById('tokenInput').value.trim();
325
+ const baseId = document.getElementById('baseInput').value.trim();
326
+
327
+ if (!token || !baseId) {
328
+ this.showToast('Please enter both token and base ID', 'error');
329
+ return;
330
+ }
331
+
332
+ // Show loading state
333
+ connectBtn.disabled = true;
334
+ btnText.textContent = 'Connecting...';
335
+ btnSpinner.style.display = 'block';
336
+
337
+ try {
338
+ console.log('Testing connection with token:', token.substring(0, 10) + '...', 'and base:', baseId);
339
+
340
+ // Test connection
341
+ const testClient = new AirtableClient(token, baseId);
342
+ const result = await testClient.testConnection();
343
+
344
+ console.log('Connection test result:', result);
345
+
346
+ if (result && result.success) {
347
+ // Save credentials
348
+ await chrome.storage.local.set({
349
+ airtableToken: token,
350
+ baseId: baseId
351
+ });
352
+
353
+ this.client = testClient;
354
+ this.isConnected = true;
355
+ this.showConnectedView();
356
+ this.updateUIForCurrentPage();
357
+ this.showToast('Connected successfully!', 'success');
358
+ } else {
359
+ const errorMsg = result?.message || result?.error || 'Connection test failed';
360
+ throw new Error(errorMsg);
361
+ }
362
+ } catch (error) {
363
+ console.error('Connection failed:', error);
364
+ let errorMessage = 'Connection failed: ';
365
+
366
+ if (error.message) {
367
+ if (error.message.includes('401')) {
368
+ errorMessage += 'Invalid token. Please check your Personal Access Token.';
369
+ } else if (error.message.includes('404')) {
370
+ errorMessage += 'Base not found. Please check your Base ID.';
371
+ } else {
372
+ errorMessage += error.message;
373
+ }
374
+ } else {
375
+ errorMessage += 'Unknown error occurred';
376
+ }
377
+
378
+ this.showToast(errorMessage, 'error');
379
+ } finally {
380
+ // Reset button state
381
+ connectBtn.disabled = false;
382
+ btnText.textContent = 'Connect';
383
+ btnSpinner.style.display = 'none';
384
+ }
385
+ }
386
+
387
+ async handleQuickSave() {
388
+ if (!this.isConnected || !this.currentTab) {
389
+ this.showToast('Not connected to Airtable', 'error');
390
+ return;
391
+ }
392
+
393
+ const saveBtn = document.getElementById('saveBtn');
394
+ const originalText = saveBtn.querySelector('.btn-text').textContent;
395
+
396
+ saveBtn.disabled = true;
397
+ saveBtn.querySelector('.btn-text').textContent = 'Saving...';
398
+
399
+ try {
400
+ let saveData;
401
+ let tableName;
402
+
403
+ // Handle LinkedIn pages specially
404
+ if (this.currentTab.url.includes('linkedin.com') && this.currentTab.url.includes('/in/')) {
405
+ console.log('LinkedIn profile detected, extracting profile data...');
406
+
407
+ try {
408
+ // Try to inject content script if needed
409
+ if (chrome.scripting && chrome.scripting.executeScript) {
410
+ await chrome.scripting.executeScript({
411
+ target: { tabId: this.currentTab.id },
412
+ files: ['content.js']
413
+ });
414
+ }
415
+
416
+ // Extract LinkedIn profile data
417
+ const response = await chrome.tabs.sendMessage(this.currentTab.id, {
418
+ action: 'extractLinkedInProfile'
419
+ });
420
+
421
+ if (response && response.success && response.data) {
422
+ saveData = response.data;
423
+ tableName = 'Contacts';
424
+ console.log('LinkedIn profile data extracted:', saveData);
425
+ } else {
426
+ throw new Error('Failed to extract LinkedIn profile data');
427
+ }
428
+ } catch (error) {
429
+ console.log('Content script injection failed, using fallback extraction');
430
+ // Use fallback extraction method
431
+ const fallbackData = await this.extractLinkedInDataFallback();
432
+ if (fallbackData) {
433
+ saveData = fallbackData;
434
+ tableName = 'Contacts';
435
+ } else {
436
+ throw new Error('Failed to extract LinkedIn profile data');
437
+ }
438
+ }
439
+ } else {
440
+ // Regular web page
441
+ saveData = {
442
+ 'Title': this.currentTab.title || 'Untitled',
443
+ 'Website URL': this.currentTab.url,
444
+ 'Domain': new URL(this.currentTab.url).hostname,
445
+ 'Saved At': new Date().toISOString().split('T')[0]
446
+ };
447
+ tableName = 'Web Clips';
448
+ }
449
+
450
+ console.log('Saving to table:', tableName, 'with data:', saveData);
451
+
452
+ // Save to Airtable with duplicate detection
453
+ const result = await this.client.createRecord(tableName, saveData);
454
+
455
+ if (result.success) {
456
+ if (result.action === 'updated') {
457
+ this.showToast(`Already exists in ${tableName} - no duplicate created`, 'warning');
458
+ } else {
459
+ this.showToast(`Saved to ${tableName}!`, 'success');
460
+ }
461
+ } else {
462
+ throw new Error(result.error || 'Failed to save');
463
+ }
464
+ } catch (error) {
465
+ console.error('Quick save failed:', error);
466
+ this.showToast(`Failed to save: ${error.message}`, 'error');
467
+ } finally {
468
+ saveBtn.disabled = false;
469
+ saveBtn.querySelector('.btn-text').textContent = originalText;
470
+ }
471
+ }
472
+
473
+ async handleLinkedInSave() {
474
+ if (!this.isConnected || !this.currentTab) {
475
+ this.showToast('Not connected to Airtable', 'error');
476
+ return;
477
+ }
478
+
479
+ if (!this.currentTab.url.includes('linkedin.com')) {
480
+ this.showToast('This only works on LinkedIn profiles', 'error');
481
+ return;
482
+ }
483
+
484
+ const linkedinBtn = document.getElementById('linkedinBtn');
485
+ const originalText = linkedinBtn.querySelector('.action-title').textContent;
486
+
487
+ linkedinBtn.disabled = true;
488
+ linkedinBtn.querySelector('.action-title').textContent = 'Saving...';
489
+
490
+ try {
491
+ // Try to extract profile data via content script
492
+ let response;
493
+
494
+ try {
495
+ // First try sending message in case content script is already injected
496
+ response = await chrome.tabs.sendMessage(this.currentTab.id, {
497
+ action: 'extractLinkedInProfile'
498
+ });
499
+ } catch (error) {
500
+ console.log('Content script not found, injecting...', error);
501
+
502
+ try {
503
+ // Check if chrome.scripting is available
504
+ if (!chrome.scripting || !chrome.scripting.executeScript) {
505
+ throw new Error('Chrome scripting API not available');
506
+ }
507
+
508
+ // Inject content script if needed
509
+ await chrome.scripting.executeScript({
510
+ target: { tabId: this.currentTab.id },
511
+ files: ['content.js']
512
+ });
513
+
514
+ // Wait for script to load then try again
515
+ await new Promise(resolve => setTimeout(resolve, 1000));
516
+
517
+ response = await chrome.tabs.sendMessage(this.currentTab.id, {
518
+ action: 'extractLinkedInProfile'
519
+ });
520
+ } catch (scriptError) {
521
+ console.error('Failed to inject content script:', scriptError);
522
+
523
+ // Fallback: extract data directly in popup (basic extraction)
524
+ const fallbackData = await this.extractLinkedInDataFallback();
525
+ if (fallbackData) {
526
+ response = { success: true, data: fallbackData };
527
+ } else {
528
+ throw new Error('Could not extract LinkedIn profile data. Please try refreshing the page.');
529
+ }
530
+ }
531
+ }
532
+
533
+ if (response && response.success && response.data) {
534
+ const profileData = response.data;
535
+
536
+ // Save to Contacts table with duplicate detection
537
+ const saveResult = await this.client.createRecord('Contacts', profileData);
538
+
539
+ if (saveResult.success) {
540
+ if (saveResult.action === 'updated') {
541
+ this.showToast('LinkedIn profile already exists - no duplicate created', 'warning');
542
+ } else {
543
+ this.showToast('LinkedIn profile saved!', 'success');
544
+ }
545
+ } else {
546
+ throw new Error(saveResult.error || 'Failed to save profile');
547
+ }
548
+ } else {
549
+ throw new Error('Failed to extract profile data');
550
+ }
551
+ } catch (error) {
552
+ console.error('LinkedIn save failed:', error);
553
+ this.showToast(`Failed to save profile: ${error.message}`, 'error');
554
+ } finally {
555
+ linkedinBtn.disabled = false;
556
+ linkedinBtn.querySelector('.action-title').textContent = originalText;
557
+ }
558
+ }
559
+
560
+ // Fallback method to extract basic LinkedIn data from page URL and title
561
+ async extractLinkedInDataFallback() {
562
+ try {
563
+ if (!this.currentTab.url.includes('linkedin.com/in/')) {
564
+ return null;
565
+ }
566
+
567
+ // Extract name from URL or page title
568
+ let name = '';
569
+ const urlMatch = this.currentTab.url.match(/linkedin\.com\/in\/([^/?]+)/);
570
+ if (urlMatch) {
571
+ name = urlMatch[1].replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
572
+ }
573
+
574
+ // Try to get name from page title
575
+ if (this.currentTab.title) {
576
+ const titleMatch = this.currentTab.title.match(/^([^|]+)/);
577
+ if (titleMatch) {
578
+ const titleName = titleMatch[1].trim();
579
+ if (titleName && titleName !== 'LinkedIn') {
580
+ name = titleName;
581
+ }
582
+ }
583
+ }
584
+
585
+ if (!name) {
586
+ return null;
587
+ }
588
+
589
+ return {
590
+ 'Name': name,
591
+ 'LinkedIn URL': this.currentTab.url,
592
+ 'Date Added': new Date().toISOString().split('T')[0],
593
+ 'Source': 'LinkedIn',
594
+ 'Notes': 'Extracted via fallback method - please update manually'
595
+ };
596
+ } catch (error) {
597
+ console.error('Fallback extraction failed:', error);
598
+ return null;
599
+ }
600
+ }
601
+
602
+ async handleBulkMode() {
603
+ if (!this.isConnected || !this.currentTab) {
604
+ this.showToast('Not connected to Airtable', 'error');
605
+ return;
606
+ }
607
+
608
+ try {
609
+ // Inject content script for bulk selection
610
+ await chrome.scripting.executeScript({
611
+ target: { tabId: this.currentTab.id },
612
+ files: ['content.js']
613
+ });
614
+
615
+ // Send message to activate bulk mode
616
+ await chrome.tabs.sendMessage(this.currentTab.id, {
617
+ action: 'enterBulkMode'
618
+ });
619
+
620
+ this.showToast('Bulk mode activated! Select elements on the page.', 'success');
621
+ window.close(); // Close popup so user can interact with page
622
+ } catch (error) {
623
+ console.error('Bulk mode failed:', error);
624
+ this.showToast(`Failed to activate bulk mode: ${error.message}`, 'error');
625
+ }
626
+ }
627
+
628
+ async handleSetupDatabase() {
629
+ try {
630
+ const result = await chrome.runtime.sendMessage({
631
+ action: 'openDatabaseSetup'
632
+ });
633
+
634
+ if (result.success) {
635
+ this.showToast('Database setup opened!', 'success');
636
+ window.close();
637
+ } else {
638
+ throw new Error('Failed to open setup');
639
+ }
640
+ } catch (error) {
641
+ console.error('Database setup failed:', error);
642
+ this.showToast('Database setup will be available soon', 'info');
643
+ }
644
+ }
645
+
646
+ showSettings() {
647
+ document.getElementById('settingsPanel').style.display = 'block';
648
+ }
649
+
650
+ hideSettings() {
651
+ document.getElementById('settingsPanel').style.display = 'none';
652
+ }
653
+
654
+ async handleDisconnect() {
655
+ try {
656
+ // Clear all possible authentication data
657
+ await chrome.storage.local.remove([
658
+ // Manual token data
659
+ 'airtableToken', 'baseId',
660
+ // OAuth data
661
+ 'selectedBaseId', 'selectedBaseName', 'useOAuth',
662
+ 'airtable_access_token', 'airtable_refresh_token',
663
+ 'airtable_token_expires', 'airtable_token_type', 'airtable_scope'
664
+ ]);
665
+
666
+ this.client = null;
667
+ this.isConnected = false;
668
+ this.showSetupView();
669
+ this.showToast('Disconnected successfully', 'success');
670
+ } catch (error) {
671
+ console.error('Disconnect failed:', error);
672
+ this.showToast('Failed to disconnect', 'error');
673
+ }
674
+ }
675
+
676
+ async saveSettings() {
677
+ try {
678
+ const settings = {
679
+ autoSave: document.getElementById('autoSave').checked,
680
+ notifications: document.getElementById('notifications').checked,
681
+ defaultTable: document.getElementById('defaultTable').value,
682
+ linkedinTable: document.getElementById('linkedinTable').value
683
+ };
684
+
685
+ await chrome.storage.local.set({ settings });
686
+ this.showToast('Settings saved', 'success');
687
+ } catch (error) {
688
+ console.error('Save settings failed:', error);
689
+ this.showToast('Failed to save settings', 'error');
690
+ }
691
+ }
692
+
693
+ showToast(message, type = 'success') {
694
+ const toast = document.getElementById('toast');
695
+ const toastIcon = document.getElementById('toastIcon');
696
+ const toastMessage = document.getElementById('toastMessage');
697
+
698
+ // Set icon based on type
699
+ const icons = {
700
+ success: '✅',
701
+ error: '❌',
702
+ warning: '🔄',
703
+ info: 'ℹ️'
704
+ };
705
+
706
+ toastIcon.textContent = icons[type] || icons.success;
707
+ toastMessage.textContent = message;
708
+
709
+ // Show toast
710
+ toast.classList.add('show');
711
+
712
+ // Hide after 3 seconds
713
+ setTimeout(() => {
714
+ toast.classList.remove('show');
715
+ }, 3000);
716
+ }
717
+ }
718
+
719
+ // Initialize when DOM is ready
720
+ document.addEventListener('DOMContentLoaded', () => {
721
+ new PopupController();
722
+ });
723
+
724
+ // Also initialize immediately in case DOMContentLoaded already fired
725
+ if (document.readyState === 'loading') {
726
+ document.addEventListener('DOMContentLoaded', () => new PopupController());
727
+ } else {
728
+ new PopupController();
729
+ }