@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,771 @@
1
+ // Airtable Clipper Content Script
2
+ // Runs on all web pages to provide extraction and interaction capabilities
3
+
4
+ import { LinkedInScraper } from './lib/linkedin-scraper.js';
5
+
6
+ class ContentScriptController {
7
+ constructor() {
8
+ this.isLinkedInPage = window.location.hostname.includes('linkedin.com');
9
+ this.bulkModeActive = false;
10
+ this.selectedElements = new Set();
11
+ this.floatingButton = null;
12
+ this.init();
13
+ }
14
+
15
+ init() {
16
+ // Wait for page to load
17
+ if (document.readyState === 'loading') {
18
+ document.addEventListener('DOMContentLoaded', () => this.setup());
19
+ } else {
20
+ this.setup();
21
+ }
22
+ }
23
+
24
+ setup() {
25
+ // Listen for messages from popup and background
26
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
27
+ this.handleMessage(message, sender, sendResponse);
28
+ return true; // Keep message channel open for async responses
29
+ });
30
+
31
+ // Create floating action button if appropriate
32
+ if (this.shouldShowFloatingButton()) {
33
+ this.createFloatingButton();
34
+ }
35
+
36
+ // LinkedIn-specific setup
37
+ if (this.isLinkedInPage) {
38
+ this.setupLinkedInFeatures();
39
+ }
40
+
41
+ // Setup general extraction features
42
+ this.setupGeneralFeatures();
43
+ }
44
+
45
+ async handleMessage(message, sender, sendResponse) {
46
+ try {
47
+ switch (message.action) {
48
+ case 'extractPageData':
49
+ const pageData = await this.extractPageData(message.type);
50
+ sendResponse({ success: true, data: pageData });
51
+ break;
52
+
53
+ case 'extractLinkedInProfile':
54
+ const profileData = await this.extractLinkedInProfile();
55
+ sendResponse({ success: true, data: profileData });
56
+ break;
57
+
58
+ case 'enterBulkMode':
59
+ this.enterBulkMode();
60
+ sendResponse({ success: true });
61
+ break;
62
+
63
+ case 'exitBulkMode':
64
+ this.exitBulkMode();
65
+ sendResponse({ success: true });
66
+ break;
67
+
68
+ case 'triggerSave':
69
+ await this.triggerSave(message.data);
70
+ sendResponse({ success: true });
71
+ break;
72
+
73
+ default:
74
+ sendResponse({ success: false, error: 'Unknown action' });
75
+ }
76
+ } catch (error) {
77
+ console.error('Content script error:', error);
78
+ sendResponse({ success: false, error: error.message });
79
+ }
80
+ }
81
+
82
+ shouldShowFloatingButton() {
83
+ // Show floating button on supported sites
84
+ const supportedSites = [
85
+ 'linkedin.com',
86
+ 'github.com',
87
+ 'stackoverflow.com',
88
+ 'medium.com',
89
+ 'dev.to'
90
+ ];
91
+
92
+ return supportedSites.some(site => window.location.hostname.includes(site));
93
+ }
94
+
95
+ createFloatingButton() {
96
+ // Remove existing button if any
97
+ if (this.floatingButton) {
98
+ this.floatingButton.remove();
99
+ }
100
+
101
+ // Create floating action button
102
+ this.floatingButton = document.createElement('div');
103
+ this.floatingButton.id = 'airtable-clipper-fab';
104
+ this.floatingButton.innerHTML = `
105
+ <div class="fab-main" title="Save to Airtable">
106
+ 📎
107
+ </div>
108
+ `;
109
+
110
+ // Add styles
111
+ this.floatingButton.style.cssText = `
112
+ position: fixed;
113
+ bottom: 20px;
114
+ right: 20px;
115
+ z-index: 10000;
116
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
117
+ `;
118
+
119
+ // Style the main button
120
+ const fabMain = this.floatingButton.querySelector('.fab-main');
121
+ fabMain.style.cssText = `
122
+ width: 56px;
123
+ height: 56px;
124
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
125
+ border-radius: 50%;
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ color: white;
130
+ font-size: 20px;
131
+ cursor: pointer;
132
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
133
+ transition: all 0.3s ease;
134
+ user-select: none;
135
+ `;
136
+
137
+ // Add hover effects
138
+ fabMain.addEventListener('mouseenter', () => {
139
+ fabMain.style.transform = 'scale(1.1)';
140
+ fabMain.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
141
+ });
142
+
143
+ fabMain.addEventListener('mouseleave', () => {
144
+ fabMain.style.transform = 'scale(1)';
145
+ fabMain.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
146
+ });
147
+
148
+ // Add click handler
149
+ fabMain.addEventListener('click', (e) => {
150
+ e.preventDefault();
151
+ e.stopPropagation();
152
+ this.handleFloatingButtonClick();
153
+ });
154
+
155
+ // Add to page
156
+ document.body.appendChild(this.floatingButton);
157
+ }
158
+
159
+ async handleFloatingButtonClick() {
160
+ try {
161
+ if (this.isLinkedInPage && window.location.pathname.includes('/in/')) {
162
+ // LinkedIn profile page - extract profile
163
+ await this.triggerSave({ type: 'linkedin' });
164
+ } else {
165
+ // General page - extract page data
166
+ await this.triggerSave({ type: 'general' });
167
+ }
168
+ } catch (error) {
169
+ console.error('Floating button click failed:', error);
170
+ this.showNotification('Failed to save content', 'error');
171
+ }
172
+ }
173
+
174
+ async triggerSave(data) {
175
+ try {
176
+ let extractedData;
177
+
178
+ if (data.type === 'linkedin') {
179
+ extractedData = await this.extractLinkedInProfile();
180
+ } else {
181
+ extractedData = await this.extractPageData(data.type);
182
+ }
183
+
184
+ // Send to background script for saving
185
+ const response = await chrome.runtime.sendMessage({
186
+ action: 'saveToAirtable',
187
+ data: {
188
+ ...data,
189
+ ...extractedData,
190
+ timestamp: new Date().toISOString()
191
+ }
192
+ });
193
+
194
+ if (response && response.success) {
195
+ this.showNotification('Saved to Airtable!', 'success');
196
+ } else {
197
+ throw new Error(response?.error || 'Failed to save');
198
+ }
199
+ } catch (error) {
200
+ console.error('Save failed:', error);
201
+ this.showNotification(`Save failed: ${error.message}`, 'error');
202
+ }
203
+ }
204
+
205
+ async extractPageData(type = 'general') {
206
+ const data = {
207
+ url: window.location.href,
208
+ title: document.title,
209
+ domain: window.location.hostname,
210
+ extractedAt: new Date().toISOString()
211
+ };
212
+
213
+ // Extract meta information
214
+ const description = document.querySelector('meta[name="description"]');
215
+ if (description) {
216
+ data.description = description.content;
217
+ }
218
+
219
+ const keywords = document.querySelector('meta[name="keywords"]');
220
+ if (keywords) {
221
+ data.keywords = keywords.content;
222
+ }
223
+
224
+ // Extract main content based on page type
225
+ if (type === 'selection' && window.getSelection().toString()) {
226
+ data.content = window.getSelection().toString();
227
+ data.type = 'Text Selection';
228
+ } else {
229
+ data.content = this.extractMainContent();
230
+ data.type = 'Web Page';
231
+ }
232
+
233
+ // Site-specific extractions
234
+ if (window.location.hostname.includes('github.com')) {
235
+ data.github = this.extractGitHubData();
236
+ } else if (window.location.hostname.includes('stackoverflow.com')) {
237
+ data.stackoverflow = this.extractStackOverflowData();
238
+ } else if (window.location.hostname.includes('medium.com')) {
239
+ data.medium = this.extractMediumData();
240
+ }
241
+
242
+ return data;
243
+ }
244
+
245
+ extractMainContent() {
246
+ // Try different selectors for main content
247
+ const contentSelectors = [
248
+ 'main',
249
+ 'article',
250
+ '[role="main"]',
251
+ '.content',
252
+ '.post-content',
253
+ '.entry-content',
254
+ '#content',
255
+ '.main-content'
256
+ ];
257
+
258
+ for (const selector of contentSelectors) {
259
+ const element = document.querySelector(selector);
260
+ if (element) {
261
+ return this.getTextContent(element).substring(0, 5000); // Limit to 5000 chars
262
+ }
263
+ }
264
+
265
+ // Fallback: get text from body, excluding navigation and footer
266
+ const body = document.body.cloneNode(true);
267
+
268
+ // Remove common navigation and footer elements
269
+ const elementsToRemove = ['nav', 'header', 'footer', '.nav', '.navigation', '.menu', '.sidebar'];
270
+ elementsToRemove.forEach(selector => {
271
+ const elements = body.querySelectorAll(selector);
272
+ elements.forEach(el => el.remove());
273
+ });
274
+
275
+ return this.getTextContent(body).substring(0, 5000);
276
+ }
277
+
278
+ getTextContent(element) {
279
+ // Get clean text content from element
280
+ const text = element.textContent || element.innerText || '';
281
+ return text.replace(/\s+/g, ' ').trim();
282
+ }
283
+
284
+ async extractLinkedInProfile() {
285
+ if (!this.isLinkedInPage) {
286
+ throw new Error('Not on LinkedIn');
287
+ }
288
+
289
+ const scraper = new LinkedInScraper();
290
+ const profileData = await scraper.extractProfile();
291
+
292
+ return {
293
+ type: 'linkedin',
294
+ ...profileData
295
+ };
296
+ }
297
+
298
+ extractGitHubData() {
299
+ const data = {};
300
+
301
+ // Repository name and owner
302
+ const repoInfo = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
303
+ if (repoInfo) {
304
+ data.owner = repoInfo[1];
305
+ data.repository = repoInfo[2];
306
+ }
307
+
308
+ // Stars and forks
309
+ const stars = document.querySelector('#repo-stars-counter-star');
310
+ if (stars) data.stars = stars.textContent.trim();
311
+
312
+ const forks = document.querySelector('#repo-network-counter');
313
+ if (forks) data.forks = forks.textContent.trim();
314
+
315
+ // Description
316
+ const description = document.querySelector('[data-pjax="#repo-content-pjax-container"] p');
317
+ if (description) data.description = description.textContent.trim();
318
+
319
+ // Language
320
+ const language = document.querySelector('[data-ga-click*="Language"]');
321
+ if (language) data.primaryLanguage = language.textContent.trim();
322
+
323
+ return data;
324
+ }
325
+
326
+ extractStackOverflowData() {
327
+ const data = {};
328
+
329
+ // Question title
330
+ const title = document.querySelector('h1[itemprop="name"]');
331
+ if (title) data.questionTitle = title.textContent.trim();
332
+
333
+ // Question score
334
+ const score = document.querySelector('.js-vote-count');
335
+ if (score) data.score = score.textContent.trim();
336
+
337
+ // Tags
338
+ const tags = Array.from(document.querySelectorAll('.post-tag'))
339
+ .map(tag => tag.textContent.trim());
340
+ if (tags.length > 0) data.tags = tags;
341
+
342
+ // Views
343
+ const views = document.querySelector('[title*="viewed"]');
344
+ if (views) data.views = views.textContent.trim();
345
+
346
+ return data;
347
+ }
348
+
349
+ extractMediumData() {
350
+ const data = {};
351
+
352
+ // Author
353
+ const author = document.querySelector('[data-testid="authorName"]');
354
+ if (author) data.author = author.textContent.trim();
355
+
356
+ // Publication date
357
+ const date = document.querySelector('[data-testid="storyPublishDate"]');
358
+ if (date) data.publishDate = date.textContent.trim();
359
+
360
+ // Reading time
361
+ const readTime = document.querySelector('[data-testid="storyReadTime"]');
362
+ if (readTime) data.readingTime = readTime.textContent.trim();
363
+
364
+ // Claps
365
+ const claps = document.querySelector('[data-testid="clapCount"]');
366
+ if (claps) data.claps = claps.textContent.trim();
367
+
368
+ return data;
369
+ }
370
+
371
+ setupLinkedInFeatures() {
372
+ // Auto-save feature for LinkedIn profiles
373
+ if (window.location.pathname.includes('/in/')) {
374
+ this.checkAutoSaveSettings();
375
+ }
376
+ }
377
+
378
+ async checkAutoSaveSettings() {
379
+ try {
380
+ const response = await chrome.runtime.sendMessage({
381
+ action: 'getSettings'
382
+ });
383
+
384
+ if (response.success && response.settings.autoSave) {
385
+ // Auto-save after a delay to ensure page is loaded
386
+ setTimeout(() => {
387
+ this.triggerSave({ type: 'linkedin', auto: true });
388
+ }, 3000);
389
+ }
390
+ } catch (error) {
391
+ console.error('Failed to check auto-save settings:', error);
392
+ }
393
+ }
394
+
395
+ setupGeneralFeatures() {
396
+ // Text selection features
397
+ document.addEventListener('mouseup', () => {
398
+ const selection = window.getSelection().toString().trim();
399
+ if (selection.length > 10) {
400
+ this.showSelectionToolbar(selection);
401
+ } else {
402
+ this.hideSelectionToolbar();
403
+ }
404
+ });
405
+ }
406
+
407
+ showSelectionToolbar(selectedText) {
408
+ // Remove existing toolbar
409
+ this.hideSelectionToolbar();
410
+
411
+ const selection = window.getSelection();
412
+ const range = selection.getRangeAt(0);
413
+ const rect = range.getBoundingClientRect();
414
+
415
+ // Create toolbar
416
+ const toolbar = document.createElement('div');
417
+ toolbar.id = 'airtable-clipper-selection-toolbar';
418
+ toolbar.innerHTML = `
419
+ <button class="selection-btn" data-action="save">
420
+ 📎 Save to Airtable
421
+ </button>
422
+ `;
423
+
424
+ // Style toolbar
425
+ toolbar.style.cssText = `
426
+ position: fixed;
427
+ top: ${rect.top + window.scrollY - 40}px;
428
+ left: ${rect.left + window.scrollX}px;
429
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
430
+ color: white;
431
+ padding: 8px 12px;
432
+ border-radius: 6px;
433
+ font-size: 12px;
434
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
435
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
436
+ z-index: 10001;
437
+ user-select: none;
438
+ `;
439
+
440
+ // Style button
441
+ const button = toolbar.querySelector('.selection-btn');
442
+ button.style.cssText = `
443
+ background: none;
444
+ border: none;
445
+ color: white;
446
+ cursor: pointer;
447
+ font-size: 12px;
448
+ font-family: inherit;
449
+ `;
450
+
451
+ // Add click handler
452
+ button.addEventListener('click', () => {
453
+ this.triggerSave({ type: 'selection', text: selectedText });
454
+ this.hideSelectionToolbar();
455
+ });
456
+
457
+ document.body.appendChild(toolbar);
458
+
459
+ // Auto-hide after 5 seconds
460
+ setTimeout(() => this.hideSelectionToolbar(), 5000);
461
+ }
462
+
463
+ hideSelectionToolbar() {
464
+ const toolbar = document.getElementById('airtable-clipper-selection-toolbar');
465
+ if (toolbar) {
466
+ toolbar.remove();
467
+ }
468
+ }
469
+
470
+ enterBulkMode() {
471
+ if (this.bulkModeActive) return;
472
+
473
+ this.bulkModeActive = true;
474
+ this.selectedElements.clear();
475
+
476
+ // Add bulk mode overlay
477
+ this.createBulkModeOverlay();
478
+
479
+ // Add click handlers to selectable elements
480
+ this.addBulkModeHandlers();
481
+
482
+ this.showNotification('Bulk mode activated. Click items to select them.', 'info');
483
+ }
484
+
485
+ createBulkModeOverlay() {
486
+ const overlay = document.createElement('div');
487
+ overlay.id = 'airtable-clipper-bulk-overlay';
488
+ overlay.innerHTML = `
489
+ <div class="bulk-header">
490
+ <span class="bulk-title">📋 Bulk Mode Active</span>
491
+ <span class="bulk-counter">0 items selected</span>
492
+ <div class="bulk-actions">
493
+ <button class="bulk-btn save-selected">Save Selected</button>
494
+ <button class="bulk-btn cancel-bulk">Cancel</button>
495
+ </div>
496
+ </div>
497
+ `;
498
+
499
+ // Style overlay
500
+ overlay.style.cssText = `
501
+ position: fixed;
502
+ top: 0;
503
+ left: 0;
504
+ right: 0;
505
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
506
+ color: white;
507
+ padding: 12px 20px;
508
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
509
+ font-size: 14px;
510
+ z-index: 10002;
511
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
512
+ `;
513
+
514
+ // Style header
515
+ const header = overlay.querySelector('.bulk-header');
516
+ header.style.cssText = `
517
+ display: flex;
518
+ align-items: center;
519
+ gap: 20px;
520
+ `;
521
+
522
+ // Style buttons
523
+ const buttons = overlay.querySelectorAll('.bulk-btn');
524
+ buttons.forEach(btn => {
525
+ btn.style.cssText = `
526
+ background: rgba(255,255,255,0.2);
527
+ border: 1px solid rgba(255,255,255,0.3);
528
+ color: white;
529
+ padding: 6px 12px;
530
+ border-radius: 4px;
531
+ cursor: pointer;
532
+ font-size: 12px;
533
+ margin-left: 8px;
534
+ `;
535
+ });
536
+
537
+ // Add event handlers
538
+ overlay.querySelector('.save-selected').addEventListener('click', () => {
539
+ this.saveBulkSelection();
540
+ });
541
+
542
+ overlay.querySelector('.cancel-bulk').addEventListener('click', () => {
543
+ this.exitBulkMode();
544
+ });
545
+
546
+ document.body.appendChild(overlay);
547
+ document.body.style.paddingTop = '60px'; // Make room for overlay
548
+ }
549
+
550
+ addBulkModeHandlers() {
551
+ // Add handlers for clickable elements based on current site
552
+ let selectors = [];
553
+
554
+ if (this.isLinkedInPage) {
555
+ selectors = [
556
+ '.search-result__wrapper',
557
+ '.entity-result',
558
+ '.reusable-search__result-container'
559
+ ];
560
+ } else {
561
+ selectors = [
562
+ 'article',
563
+ '.item',
564
+ '.card',
565
+ '.result',
566
+ '.post'
567
+ ];
568
+ }
569
+
570
+ selectors.forEach(selector => {
571
+ const elements = document.querySelectorAll(selector);
572
+ elements.forEach(element => {
573
+ element.addEventListener('click', this.handleBulkElementClick.bind(this));
574
+ element.style.cursor = 'pointer';
575
+ element.style.transition = 'all 0.2s ease';
576
+ });
577
+ });
578
+ }
579
+
580
+ handleBulkElementClick(event) {
581
+ event.preventDefault();
582
+ event.stopPropagation();
583
+
584
+ const element = event.currentTarget;
585
+ const isSelected = this.selectedElements.has(element);
586
+
587
+ if (isSelected) {
588
+ this.selectedElements.delete(element);
589
+ element.style.background = '';
590
+ element.style.border = '';
591
+ } else {
592
+ this.selectedElements.add(element);
593
+ element.style.background = 'rgba(102, 126, 234, 0.1)';
594
+ element.style.border = '2px solid #667eea';
595
+ }
596
+
597
+ this.updateBulkCounter();
598
+ }
599
+
600
+ updateBulkCounter() {
601
+ const counter = document.querySelector('.bulk-counter');
602
+ if (counter) {
603
+ const count = this.selectedElements.size;
604
+ counter.textContent = `${count} item${count !== 1 ? 's' : ''} selected`;
605
+ }
606
+ }
607
+
608
+ async saveBulkSelection() {
609
+ if (this.selectedElements.size === 0) {
610
+ this.showNotification('No items selected', 'error');
611
+ return;
612
+ }
613
+
614
+ this.showNotification('Saving selected items...', 'info');
615
+
616
+ try {
617
+ const items = [];
618
+
619
+ for (const element of this.selectedElements) {
620
+ const itemData = await this.extractElementData(element);
621
+ if (itemData) {
622
+ items.push(itemData);
623
+ }
624
+ }
625
+
626
+ // Save all items
627
+ const response = await chrome.runtime.sendMessage({
628
+ action: 'saveBulkToAirtable',
629
+ data: {
630
+ items: items,
631
+ type: 'bulk',
632
+ timestamp: new Date().toISOString()
633
+ }
634
+ });
635
+
636
+ if (response && response.success) {
637
+ this.showNotification(`Saved ${items.length} items to Airtable!`, 'success');
638
+ this.exitBulkMode();
639
+ } else {
640
+ throw new Error(response?.error || 'Failed to save bulk items');
641
+ }
642
+ } catch (error) {
643
+ console.error('Bulk save failed:', error);
644
+ this.showNotification(`Bulk save failed: ${error.message}`, 'error');
645
+ }
646
+ }
647
+
648
+ async extractElementData(element) {
649
+ // Extract data from individual element based on site type
650
+ const data = {
651
+ type: 'bulk_item',
652
+ url: window.location.href,
653
+ extractedAt: new Date().toISOString()
654
+ };
655
+
656
+ if (this.isLinkedInPage) {
657
+ // LinkedIn search result
658
+ const nameElement = element.querySelector('.entity-result__title-text a, .search-result__result-link');
659
+ const titleElement = element.querySelector('.entity-result__primary-subtitle, .subline-level-1');
660
+ const companyElement = element.querySelector('.entity-result__secondary-subtitle, .subline-level-2');
661
+
662
+ if (nameElement) {
663
+ data.name = nameElement.textContent.trim();
664
+ data.linkedinUrl = nameElement.href;
665
+ }
666
+ if (titleElement) data.title = titleElement.textContent.trim();
667
+ if (companyElement) data.company = companyElement.textContent.trim();
668
+ } else {
669
+ // General web page element
670
+ const title = element.querySelector('h1, h2, h3, .title, .headline');
671
+ const link = element.querySelector('a[href]');
672
+
673
+ if (title) data.title = title.textContent.trim();
674
+ if (link) data.url = link.href;
675
+
676
+ data.content = this.getTextContent(element).substring(0, 1000);
677
+ }
678
+
679
+ return data;
680
+ }
681
+
682
+ exitBulkMode() {
683
+ if (!this.bulkModeActive) return;
684
+
685
+ this.bulkModeActive = false;
686
+
687
+ // Remove overlay
688
+ const overlay = document.getElementById('airtable-clipper-bulk-overlay');
689
+ if (overlay) {
690
+ overlay.remove();
691
+ document.body.style.paddingTop = '';
692
+ }
693
+
694
+ // Reset selected elements
695
+ this.selectedElements.forEach(element => {
696
+ element.style.background = '';
697
+ element.style.border = '';
698
+ element.style.cursor = '';
699
+ });
700
+ this.selectedElements.clear();
701
+
702
+ this.showNotification('Bulk mode deactivated', 'info');
703
+ }
704
+
705
+ showNotification(message, type = 'info') {
706
+ // Remove existing notification
707
+ const existing = document.getElementById('airtable-clipper-notification');
708
+ if (existing) existing.remove();
709
+
710
+ // Create notification
711
+ const notification = document.createElement('div');
712
+ notification.id = 'airtable-clipper-notification';
713
+ notification.textContent = message;
714
+
715
+ // Style based on type
716
+ const colors = {
717
+ success: '#10b981',
718
+ error: '#ef4444',
719
+ info: '#3b82f6'
720
+ };
721
+
722
+ notification.style.cssText = `
723
+ position: fixed;
724
+ top: 20px;
725
+ right: 20px;
726
+ background: ${colors[type] || colors.info};
727
+ color: white;
728
+ padding: 12px 16px;
729
+ border-radius: 6px;
730
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
731
+ font-size: 14px;
732
+ font-weight: 500;
733
+ z-index: 10003;
734
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
735
+ max-width: 300px;
736
+ word-wrap: break-word;
737
+ animation: slideInRight 0.3s ease;
738
+ `;
739
+
740
+ // Add animation keyframes
741
+ if (!document.querySelector('#airtable-clipper-animations')) {
742
+ const style = document.createElement('style');
743
+ style.id = 'airtable-clipper-animations';
744
+ style.textContent = `
745
+ @keyframes slideInRight {
746
+ from {
747
+ transform: translateX(100%);
748
+ opacity: 0;
749
+ }
750
+ to {
751
+ transform: translateX(0);
752
+ opacity: 1;
753
+ }
754
+ }
755
+ `;
756
+ document.head.appendChild(style);
757
+ }
758
+
759
+ document.body.appendChild(notification);
760
+
761
+ // Auto-hide after 3 seconds
762
+ setTimeout(() => {
763
+ if (notification && notification.parentNode) {
764
+ notification.remove();
765
+ }
766
+ }, 3000);
767
+ }
768
+ }
769
+
770
+ // Initialize content script
771
+ new ContentScriptController();