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