@rashidazarang/airtable-mcp 1.5.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.
- package/.github/ISSUE_TEMPLATE/bug-report.yml +173 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +209 -0
- package/.github/ISSUE_TEMPLATE/security-report.yml +216 -0
- package/.github/pull_request_template.md +245 -0
- package/.github/workflows/ci-cd.yml +408 -0
- package/.github/workflows/security-audit.yml +316 -0
- package/API_DOCUMENTATION.md +897 -0
- package/CODE_OF_CONDUCT.md +181 -0
- package/Dockerfile.production +127 -0
- package/README.md +55 -10
- package/RELEASE_NOTES_v1.6.0.md +248 -0
- package/airtable-clipper/CHANGELOG.md +198 -0
- package/airtable-clipper/CHROME_STORE_SUBMISSION.md +343 -0
- package/airtable-clipper/LAUNCH_STRATEGY.md +495 -0
- package/airtable-clipper/LICENSE +21 -0
- package/airtable-clipper/OAUTH_SETUP.md +51 -0
- package/airtable-clipper/PRIVACY_POLICY.md +187 -0
- package/airtable-clipper/README.md +575 -0
- package/airtable-clipper/SUBMIT_TO_CHROME_STORE.md +273 -0
- package/airtable-clipper/build.sh +85 -0
- package/airtable-clipper/docs/QUICK_START.md +99 -0
- package/airtable-clipper/docs/SETUP.md +291 -0
- package/airtable-clipper/extension/background.js +337 -0
- package/airtable-clipper/extension/base-setup.html +324 -0
- package/airtable-clipper/extension/base-setup.js +471 -0
- package/airtable-clipper/extension/content.js +771 -0
- package/airtable-clipper/extension/icons/README.md +69 -0
- package/airtable-clipper/extension/icons/icon-16.png +3 -0
- package/airtable-clipper/extension/manifest.json +73 -0
- package/airtable-clipper/extension/popup.html +144 -0
- package/airtable-clipper/extension/popup.js +475 -0
- package/airtable-clipper/extension/styles/content.css +229 -0
- package/airtable-clipper/extension/styles/popup.css +477 -0
- package/airtable-clipper/privacy-policy.md +63 -0
- package/airtable-clipper/releases/v1.0.0/background.js +337 -0
- package/airtable-clipper/releases/v1.0.0/base-setup.html +324 -0
- package/airtable-clipper/releases/v1.0.0/base-setup.js +471 -0
- package/airtable-clipper/releases/v1.0.0/content.js +771 -0
- package/airtable-clipper/releases/v1.0.0/icons/README.md +69 -0
- package/airtable-clipper/releases/v1.0.0/icons/icon-128.png +2 -0
- package/airtable-clipper/releases/v1.0.0/icons/icon-16.png +3 -0
- package/airtable-clipper/releases/v1.0.0/icons/icon-32.png +2 -0
- package/airtable-clipper/releases/v1.0.0/icons/icon-48.png +2 -0
- package/airtable-clipper/releases/v1.0.0/manifest.json +73 -0
- package/airtable-clipper/releases/v1.0.0/popup.html +144 -0
- package/airtable-clipper/releases/v1.0.0/popup.js +475 -0
- package/airtable-clipper/releases/v1.0.0/sidepanel.html +25 -0
- package/airtable-clipper/releases/v1.0.0/styles/content.css +229 -0
- package/airtable-clipper/releases/v1.0.0/styles/popup.css +477 -0
- package/airtable-clipper/releases/v1.0.1/background.js +337 -0
- package/airtable-clipper/releases/v1.0.1/base-setup.html +324 -0
- package/airtable-clipper/releases/v1.0.1/base-setup.js +471 -0
- package/airtable-clipper/releases/v1.0.1/content.js +771 -0
- package/airtable-clipper/releases/v1.0.1/icons/README.md +69 -0
- package/airtable-clipper/releases/v1.0.1/icons/icon-128.png +2 -0
- package/airtable-clipper/releases/v1.0.1/icons/icon-16.png +3 -0
- package/airtable-clipper/releases/v1.0.1/icons/icon-32.png +2 -0
- package/airtable-clipper/releases/v1.0.1/icons/icon-48.png +2 -0
- package/airtable-clipper/releases/v1.0.1/manifest.json +70 -0
- package/airtable-clipper/releases/v1.0.1/popup.html +157 -0
- package/airtable-clipper/releases/v1.0.1/popup.js +562 -0
- package/airtable-clipper/releases/v1.0.1/sidepanel.html +25 -0
- package/airtable-clipper/releases/v1.0.1/styles/content.css +229 -0
- package/airtable-clipper/releases/v1.0.1/styles/popup.css +647 -0
- package/airtable-clipper/releases/v1.0.2/background.js +337 -0
- package/airtable-clipper/releases/v1.0.2/base-setup.html +324 -0
- package/airtable-clipper/releases/v1.0.2/base-setup.js +471 -0
- package/airtable-clipper/releases/v1.0.2/content.js +771 -0
- package/airtable-clipper/releases/v1.0.2/icons/README.md +69 -0
- package/airtable-clipper/releases/v1.0.2/icons/icon-128.png +2 -0
- package/airtable-clipper/releases/v1.0.2/icons/icon-16.png +3 -0
- package/airtable-clipper/releases/v1.0.2/icons/icon-32.png +2 -0
- package/airtable-clipper/releases/v1.0.2/icons/icon-48.png +2 -0
- package/airtable-clipper/releases/v1.0.2/manifest.json +62 -0
- package/airtable-clipper/releases/v1.0.2/popup.html +157 -0
- package/airtable-clipper/releases/v1.0.2/popup.js +567 -0
- package/airtable-clipper/releases/v1.0.2/sidepanel.html +25 -0
- package/airtable-clipper/releases/v1.0.2/styles/content.css +229 -0
- package/airtable-clipper/releases/v1.0.2/styles/popup.css +647 -0
- package/airtable-clipper/terms-of-service.md +124 -0
- package/airtable-clipper/test-credentials.md +61 -0
- package/airtable-clipper/test-extension/background.js +337 -0
- package/airtable-clipper/test-extension/base-setup.html +324 -0
- package/airtable-clipper/test-extension/base-setup.js +471 -0
- package/airtable-clipper/test-extension/content.js +873 -0
- package/airtable-clipper/test-extension/icons/README.md +69 -0
- package/airtable-clipper/test-extension/icons/icon-128.png +2 -0
- package/airtable-clipper/test-extension/icons/icon-16.png +3 -0
- package/airtable-clipper/test-extension/icons/icon-32.png +2 -0
- package/airtable-clipper/test-extension/icons/icon-48.png +2 -0
- package/airtable-clipper/test-extension/manifest.json +72 -0
- package/airtable-clipper/test-extension/popup.html +274 -0
- package/airtable-clipper/test-extension/popup.js +729 -0
- package/airtable-clipper/test-extension/sidepanel.html +25 -0
- package/airtable-clipper/test-extension/styles/content.css +229 -0
- package/airtable-clipper/test-extension/styles/popup.css +794 -0
- package/airtable_mcp_v2.js +1505 -0
- package/airtable_mcp_v2_oauth.js +1048 -0
- package/airtable_mcp_v3_advanced.js +1161 -0
- package/airtable_simple.js +447 -1
- package/airtable_simple_production.js +532 -0
- package/docker-compose.production.yml +366 -0
- package/helm/airtable-mcp/Chart.yaml +122 -0
- package/helm/airtable-mcp/values.yaml +538 -0
- package/k8s/deployment.yaml +402 -0
- package/k8s/namespace.yaml +108 -0
- package/k8s/service.yaml +194 -0
- package/monitoring/alerts.yml +289 -0
- package/monitoring/prometheus.yml +224 -0
- package/package.json +6 -6
- package/test_v1.6.0_comprehensive.sh +187 -0
- package/.claude/settings.local.json +0 -12
- package/airtable-mcp-1.1.0.tgz +0 -0
- package/airtable_enhanced.js +0 -499
- package/airtable_simple_v1.2.4_backup.js +0 -277
- package/airtable_v1.4.0.js +0 -654
- package/rashidazarang-airtable-mcp-1.1.0.tgz +0 -0
- package/rashidazarang-airtable-mcp-1.2.0.tgz +0 -0
- package/rashidazarang-airtable-mcp-1.2.1.tgz +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# 🚀 Setup Guide - Airtable Clipper
|
|
2
|
+
|
|
3
|
+
This guide will walk you through setting up Airtable Clipper in under 5 minutes.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before you start, make sure you have:
|
|
8
|
+
|
|
9
|
+
- **Chrome Browser** (version 88 or higher)
|
|
10
|
+
- **Airtable Account** (free or paid)
|
|
11
|
+
- **Internet Connection** for Airtable API access
|
|
12
|
+
|
|
13
|
+
## Step 1: Install the Extension
|
|
14
|
+
|
|
15
|
+
### Option A: Chrome Web Store (Recommended)
|
|
16
|
+
1. Visit the [Airtable Clipper Chrome Web Store page](https://chrome.google.com/webstore/detail/airtable-clipper/EXTENSION_ID)
|
|
17
|
+
2. Click **"Add to Chrome"**
|
|
18
|
+
3. Confirm by clicking **"Add Extension"**
|
|
19
|
+
4. You'll see the Airtable Clipper icon (📎) in your Chrome toolbar
|
|
20
|
+
|
|
21
|
+
### Option B: Manual Installation (Developers)
|
|
22
|
+
1. Download the latest release from [GitHub](https://github.com/rashidazarang/airtable-clipper/releases)
|
|
23
|
+
2. Unzip the downloaded file
|
|
24
|
+
3. Open Chrome and go to `chrome://extensions/`
|
|
25
|
+
4. Enable **"Developer mode"** (toggle in top-right)
|
|
26
|
+
5. Click **"Load unpacked"** and select the `extension` folder
|
|
27
|
+
6. The extension will appear in your toolbar
|
|
28
|
+
|
|
29
|
+
## Step 2: Get Your Airtable Credentials
|
|
30
|
+
|
|
31
|
+
### 2.1 Create a Personal Access Token
|
|
32
|
+
|
|
33
|
+
1. **Go to Airtable Token Creation Page**
|
|
34
|
+
- Visit [airtable.com/create/tokens](https://airtable.com/create/tokens)
|
|
35
|
+
- Sign in to your Airtable account if prompted
|
|
36
|
+
|
|
37
|
+
2. **Create New Token**
|
|
38
|
+
- Click **"Create new token"**
|
|
39
|
+
- **Name**: `Airtable Clipper` (or any name you prefer)
|
|
40
|
+
- **Description**: `Chrome extension for web clipping`
|
|
41
|
+
|
|
42
|
+
3. **Add Required Scopes**
|
|
43
|
+
Select these permissions:
|
|
44
|
+
- ✅ `data.records:read` - Read records from your tables
|
|
45
|
+
- ✅ `data.records:write` - Create and update records
|
|
46
|
+
- ✅ `schema.bases:read` - Read table structures
|
|
47
|
+
|
|
48
|
+
4. **Add Base Access**
|
|
49
|
+
- Click **"Add a base"**
|
|
50
|
+
- Select the base(s) you want to clip data to
|
|
51
|
+
- Choose **"All current and future tables in this base"**
|
|
52
|
+
|
|
53
|
+
5. **Create Token**
|
|
54
|
+
- Click **"Create token"**
|
|
55
|
+
- **⚠️ Copy and save the token immediately** - it starts with `pat...`
|
|
56
|
+
- You won't be able to see it again!
|
|
57
|
+
|
|
58
|
+
### 2.2 Get Your Base ID
|
|
59
|
+
|
|
60
|
+
1. **Open Your Airtable Base**
|
|
61
|
+
- Go to [airtable.com](https://airtable.com)
|
|
62
|
+
- Open the base where you want to save clipped data
|
|
63
|
+
|
|
64
|
+
2. **Copy Base ID from URL**
|
|
65
|
+
- Look at the URL in your browser address bar
|
|
66
|
+
- Find the part that looks like: `https://airtable.com/appXXXXXXXXXXXXXX/...`
|
|
67
|
+
- Copy the **Base ID** (the part starting with `app`)
|
|
68
|
+
- Example: `appTV04Fyu1Gvbunq`
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
## Step 3: Configure the Extension
|
|
73
|
+
|
|
74
|
+
1. **Open Extension Popup**
|
|
75
|
+
- Click the Airtable Clipper icon (📎) in your Chrome toolbar
|
|
76
|
+
- You should see the setup screen
|
|
77
|
+
|
|
78
|
+
2. **Enter Your Credentials**
|
|
79
|
+
- **Personal Access Token**: Paste your token (starts with `pat...`)
|
|
80
|
+
- **Base ID**: Paste your base ID (starts with `app...`)
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
3. **Test Connection**
|
|
85
|
+
- Click **"Connect to Airtable"**
|
|
86
|
+
- You should see "Connected successfully!" message
|
|
87
|
+
- The extension will load your table names automatically
|
|
88
|
+
|
|
89
|
+
4. **Configure Tables (Optional)**
|
|
90
|
+
- **Default Table**: Where general web clips will be saved (default: "Clips")
|
|
91
|
+
- **LinkedIn Table**: Where LinkedIn profiles will be saved (default: "Contacts")
|
|
92
|
+
- You can change these later in settings
|
|
93
|
+
|
|
94
|
+
## Step 4: Create Tables (If Needed)
|
|
95
|
+
|
|
96
|
+
The extension works best with properly structured tables. Here are recommended setups:
|
|
97
|
+
|
|
98
|
+
### For LinkedIn Contacts (Recommended Table: "Contacts")
|
|
99
|
+
|
|
100
|
+
Create a table with these fields:
|
|
101
|
+
|
|
102
|
+
| Field Name | Field Type | Description |
|
|
103
|
+
|------------|------------|-------------|
|
|
104
|
+
| Name | Single line text | Contact's full name |
|
|
105
|
+
| Title | Single line text | Job title |
|
|
106
|
+
| Company | Single line text | Company name |
|
|
107
|
+
| Location | Single line text | Location/city |
|
|
108
|
+
| Email | Email | Email address (auto-guessed) |
|
|
109
|
+
| Phone | Phone number | Phone number |
|
|
110
|
+
| LinkedIn URL | URL | Link to LinkedIn profile |
|
|
111
|
+
| Date Added | Date | When contact was saved |
|
|
112
|
+
| Source | Single select | How contact was added |
|
|
113
|
+
| Lead Score | Number | Data quality score (0-100) |
|
|
114
|
+
| Notes | Long text | Additional information |
|
|
115
|
+
| Tags | Multiple select | Relevant tags |
|
|
116
|
+
|
|
117
|
+
### For General Web Clips (Recommended Table: "Clips")
|
|
118
|
+
|
|
119
|
+
Create a table with these fields:
|
|
120
|
+
|
|
121
|
+
| Field Name | Field Type | Description |
|
|
122
|
+
|------------|------------|-------------|
|
|
123
|
+
| Title | Single line text | Page/article title |
|
|
124
|
+
| URL | URL | Source webpage URL |
|
|
125
|
+
| Content | Long text | Extracted content |
|
|
126
|
+
| Date Saved | Date | When content was clipped |
|
|
127
|
+
| Type | Single select | Content type (Article, Profile, etc.) |
|
|
128
|
+
| Domain | Single line text | Website domain |
|
|
129
|
+
| Tags | Multiple select | Auto-generated tags |
|
|
130
|
+
| Quality Score | Number | Extraction quality (0-100) |
|
|
131
|
+
|
|
132
|
+
### Quick Table Creation
|
|
133
|
+
|
|
134
|
+
You can also let the extension suggest table structures:
|
|
135
|
+
|
|
136
|
+
1. Try to save some content first
|
|
137
|
+
2. If tables don't exist, the extension will show an error with suggested fields
|
|
138
|
+
3. Create the tables manually with the suggested structure
|
|
139
|
+
4. Try saving again
|
|
140
|
+
|
|
141
|
+
## Step 5: Test the Extension
|
|
142
|
+
|
|
143
|
+
### Test 1: Save a LinkedIn Profile
|
|
144
|
+
|
|
145
|
+
1. **Visit a LinkedIn Profile**
|
|
146
|
+
- Go to any public LinkedIn profile (e.g., `linkedin.com/in/username`)
|
|
147
|
+
- Make sure you're not on the LinkedIn home feed
|
|
148
|
+
|
|
149
|
+
2. **Save the Profile**
|
|
150
|
+
- Click the Airtable Clipper icon
|
|
151
|
+
- Click **"Save Profile"** or just **"Quick Save"**
|
|
152
|
+
- You should see a success notification
|
|
153
|
+
|
|
154
|
+
3. **Check Airtable**
|
|
155
|
+
- Open your Airtable base
|
|
156
|
+
- Check the "Contacts" table (or your LinkedIn table)
|
|
157
|
+
- You should see the profile data saved
|
|
158
|
+
|
|
159
|
+
### Test 2: Save a General Web Page
|
|
160
|
+
|
|
161
|
+
1. **Visit Any Website**
|
|
162
|
+
- Go to an article, blog post, or any webpage
|
|
163
|
+
- Try a site like Medium, GitHub, or Stack Overflow
|
|
164
|
+
|
|
165
|
+
2. **Save the Page**
|
|
166
|
+
- Click the Airtable Clipper icon
|
|
167
|
+
- Click **"Quick Save"**
|
|
168
|
+
- Wait for the success notification
|
|
169
|
+
|
|
170
|
+
3. **Check Airtable**
|
|
171
|
+
- Check your "Clips" table (or default table)
|
|
172
|
+
- You should see the page data saved
|
|
173
|
+
|
|
174
|
+
## Step 6: Explore Features
|
|
175
|
+
|
|
176
|
+
### Floating Action Button
|
|
177
|
+
- Visit supported sites (LinkedIn, GitHub, etc.)
|
|
178
|
+
- Look for the floating 📎 button in the bottom-right corner
|
|
179
|
+
- Click it for quick saving
|
|
180
|
+
|
|
181
|
+
### Right-Click Menu
|
|
182
|
+
- Right-click anywhere on a webpage
|
|
183
|
+
- Look for "Save to Airtable" in the context menu
|
|
184
|
+
- Great for saving selected text or specific content
|
|
185
|
+
|
|
186
|
+
### Bulk Mode
|
|
187
|
+
- Hold **Shift** and click the extension icon
|
|
188
|
+
- Or use keyboard shortcut: **Cmd+Shift+B** (Mac) or **Ctrl+Shift+B** (Windows)
|
|
189
|
+
- Click multiple items on the page to select them
|
|
190
|
+
- Click **"Save Selected"** to save everything at once
|
|
191
|
+
|
|
192
|
+
### Keyboard Shortcuts
|
|
193
|
+
- **Quick Save**: `Cmd+Shift+S` (Mac) or `Ctrl+Shift+S` (Windows)
|
|
194
|
+
- **Bulk Mode**: `Cmd+Shift+B` (Mac) or `Ctrl+Shift+B` (Windows)
|
|
195
|
+
|
|
196
|
+
## Troubleshooting Common Issues
|
|
197
|
+
|
|
198
|
+
### ❌ "Connection Failed" Error
|
|
199
|
+
|
|
200
|
+
**Check Your Token:**
|
|
201
|
+
- Make sure you copied the complete token (starts with `pat`)
|
|
202
|
+
- Token should be about 40-50 characters long
|
|
203
|
+
- Verify you have the required scopes enabled
|
|
204
|
+
|
|
205
|
+
**Check Your Base ID:**
|
|
206
|
+
- Make sure it starts with `app` (not `tbl` or other prefixes)
|
|
207
|
+
- Should be exactly 17 characters long
|
|
208
|
+
- Copy from the URL, not from the table ID
|
|
209
|
+
|
|
210
|
+
**Test Manually:**
|
|
211
|
+
```bash
|
|
212
|
+
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
|
213
|
+
https://api.airtable.com/v0/meta/bases/YOUR_BASE_ID/tables
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### ❌ "Failed to Extract Data" Error
|
|
217
|
+
|
|
218
|
+
**Page Not Supported:**
|
|
219
|
+
- Try refreshing the page and trying again
|
|
220
|
+
- Some dynamic sites need a moment to load content
|
|
221
|
+
- Not all websites may be supported yet
|
|
222
|
+
|
|
223
|
+
**LinkedIn Specific:**
|
|
224
|
+
- Make sure you're on a profile page (`linkedin.com/in/username`)
|
|
225
|
+
- Some profiles may be private or have limited public data
|
|
226
|
+
- Try a different profile
|
|
227
|
+
|
|
228
|
+
### ❌ "Table Not Found" Error
|
|
229
|
+
|
|
230
|
+
**Create Missing Tables:**
|
|
231
|
+
- The extension can't create tables automatically
|
|
232
|
+
- Create tables manually in Airtable with the suggested field names
|
|
233
|
+
- Make sure table names match exactly (case-sensitive)
|
|
234
|
+
|
|
235
|
+
### 🐌 Slow Performance
|
|
236
|
+
|
|
237
|
+
**Browser Issues:**
|
|
238
|
+
- Close unused tabs to free up memory
|
|
239
|
+
- Disable other extensions temporarily
|
|
240
|
+
- Restart Chrome if it becomes sluggish
|
|
241
|
+
|
|
242
|
+
**Network Issues:**
|
|
243
|
+
- Check your internet connection
|
|
244
|
+
- Airtable API may be temporarily slow
|
|
245
|
+
- Try again in a few minutes
|
|
246
|
+
|
|
247
|
+
## Advanced Configuration
|
|
248
|
+
|
|
249
|
+
### Custom Field Mapping
|
|
250
|
+
|
|
251
|
+
You can customize how extracted data maps to your Airtable fields:
|
|
252
|
+
|
|
253
|
+
1. Click the extension icon
|
|
254
|
+
2. Click **"Settings"** at the bottom
|
|
255
|
+
3. Modify the table and field mappings
|
|
256
|
+
4. Changes are saved automatically
|
|
257
|
+
|
|
258
|
+
### Automation Settings
|
|
259
|
+
|
|
260
|
+
- **Auto-Save LinkedIn**: Automatically save LinkedIn profiles when you visit them
|
|
261
|
+
- **Duplicate Detection**: Skip saving if a similar record already exists
|
|
262
|
+
- **Notifications**: Control when you see success/error messages
|
|
263
|
+
|
|
264
|
+
### Privacy Settings
|
|
265
|
+
|
|
266
|
+
- **Data Processing**: All data processing happens locally in your browser
|
|
267
|
+
- **No Tracking**: The extension doesn't track your browsing or send data to external servers
|
|
268
|
+
- **Storage**: Settings are synced across your Chrome browsers via Chrome Sync
|
|
269
|
+
|
|
270
|
+
## Getting Help
|
|
271
|
+
|
|
272
|
+
If you're still having issues:
|
|
273
|
+
|
|
274
|
+
1. **Check the Documentation**: [Full docs](../README.md)
|
|
275
|
+
2. **Search Issues**: [GitHub Issues](https://github.com/rashidazarang/airtable-clipper/issues)
|
|
276
|
+
3. **Ask for Help**: [GitHub Discussions](https://github.com/rashidazarang/airtable-clipper/discussions)
|
|
277
|
+
4. **Report Bugs**: [Create a new issue](https://github.com/rashidazarang/airtable-clipper/issues/new)
|
|
278
|
+
|
|
279
|
+
Include this information when asking for help:
|
|
280
|
+
- Chrome version
|
|
281
|
+
- Extension version
|
|
282
|
+
- Website you're trying to clip from
|
|
283
|
+
- Error message (if any)
|
|
284
|
+
- Steps to reproduce the issue
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
**Next Steps:**
|
|
289
|
+
- [Learn advanced features](ADVANCED.md)
|
|
290
|
+
- [See use case examples](EXAMPLES.md)
|
|
291
|
+
- [Contribute to the project](../README.md#contributing)
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// Airtable Clipper - Background Service Worker
|
|
2
|
+
// Handles context menus, notifications, and cross-tab communication
|
|
3
|
+
|
|
4
|
+
class BackgroundService {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.init();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
init() {
|
|
10
|
+
// Create context menu on installation
|
|
11
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
12
|
+
this.createContextMenus();
|
|
13
|
+
this.setupNotifications();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Handle context menu clicks
|
|
17
|
+
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
18
|
+
this.handleContextMenuClick(info, tab);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Handle messages from content scripts and popup
|
|
22
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
23
|
+
this.handleMessage(message, sender, sendResponse);
|
|
24
|
+
return true; // Keep message channel open for async responses
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Handle extension icon clicks
|
|
28
|
+
chrome.action.onClicked.addListener((tab) => {
|
|
29
|
+
this.handleActionClick(tab);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Periodic cleanup
|
|
33
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
34
|
+
if (alarm.name === 'cleanup') {
|
|
35
|
+
this.cleanupStorage();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Set up cleanup alarm
|
|
40
|
+
chrome.alarms.create('cleanup', { delayInMinutes: 60, periodInMinutes: 60 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createContextMenus() {
|
|
44
|
+
// Remove existing menus
|
|
45
|
+
chrome.contextMenus.removeAll();
|
|
46
|
+
|
|
47
|
+
// Main menu
|
|
48
|
+
chrome.contextMenus.create({
|
|
49
|
+
id: 'save-to-airtable',
|
|
50
|
+
title: 'Save to Airtable',
|
|
51
|
+
contexts: ['page', 'selection', 'link', 'image']
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// LinkedIn specific
|
|
55
|
+
chrome.contextMenus.create({
|
|
56
|
+
id: 'save-linkedin-profile',
|
|
57
|
+
title: 'Save LinkedIn Profile',
|
|
58
|
+
contexts: ['page'],
|
|
59
|
+
documentUrlPatterns: ['*://www.linkedin.com/in/*', '*://linkedin.com/in/*']
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Quick save selection
|
|
63
|
+
chrome.contextMenus.create({
|
|
64
|
+
id: 'quick-save-selection',
|
|
65
|
+
title: 'Quick Save Selected Text',
|
|
66
|
+
contexts: ['selection']
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async handleContextMenuClick(info, tab) {
|
|
71
|
+
try {
|
|
72
|
+
switch (info.menuItemId) {
|
|
73
|
+
case 'save-to-airtable':
|
|
74
|
+
await this.triggerSave(tab, { type: 'general', info });
|
|
75
|
+
break;
|
|
76
|
+
case 'save-linkedin-profile':
|
|
77
|
+
await this.triggerSave(tab, { type: 'linkedin', info });
|
|
78
|
+
break;
|
|
79
|
+
case 'quick-save-selection':
|
|
80
|
+
await this.triggerSave(tab, { type: 'selection', text: info.selectionText });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Context menu error:', error);
|
|
85
|
+
this.showErrorNotification(error.message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async triggerSave(tab, data) {
|
|
90
|
+
// Send message to content script to initiate save
|
|
91
|
+
try {
|
|
92
|
+
const response = await chrome.tabs.sendMessage(tab.id, {
|
|
93
|
+
action: 'triggerSave',
|
|
94
|
+
data: data
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (response && response.success) {
|
|
98
|
+
this.showSuccessNotification('Content saved to Airtable!');
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error(response?.error || 'Failed to save content');
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Save trigger error:', error);
|
|
104
|
+
// Fallback: open popup if content script isn't available
|
|
105
|
+
chrome.action.openPopup();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async handleMessage(message, sender, sendResponse) {
|
|
110
|
+
try {
|
|
111
|
+
switch (message.action) {
|
|
112
|
+
case 'saveToAirtable':
|
|
113
|
+
const result = await this.saveToAirtable(message.data);
|
|
114
|
+
sendResponse({ success: true, result });
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'getSettings':
|
|
118
|
+
const settings = await this.getSettings();
|
|
119
|
+
sendResponse({ success: true, settings });
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'saveSettings':
|
|
123
|
+
await this.saveSettings(message.settings);
|
|
124
|
+
sendResponse({ success: true });
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'showNotification':
|
|
128
|
+
this.showNotification(message.title, message.message, message.type);
|
|
129
|
+
sendResponse({ success: true });
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'openSidePanel':
|
|
133
|
+
if (sender.tab) {
|
|
134
|
+
chrome.sidePanel.open({ tabId: sender.tab.id });
|
|
135
|
+
}
|
|
136
|
+
sendResponse({ success: true });
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
default:
|
|
140
|
+
sendResponse({ success: false, error: 'Unknown action' });
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Message handler error:', error);
|
|
144
|
+
sendResponse({ success: false, error: error.message });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async saveToAirtable(data) {
|
|
149
|
+
// Get user settings
|
|
150
|
+
const settings = await this.getSettings();
|
|
151
|
+
|
|
152
|
+
if (!settings.airtableToken || !settings.baseId) {
|
|
153
|
+
throw new Error('Airtable credentials not configured');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Import Airtable client
|
|
157
|
+
const { AirtableClient } = await import('./lib/airtable-client.js');
|
|
158
|
+
const client = new AirtableClient(settings.airtableToken, settings.baseId);
|
|
159
|
+
|
|
160
|
+
// Process and save data
|
|
161
|
+
const processedData = await this.processData(data, settings);
|
|
162
|
+
const result = await client.createRecord(processedData.table, processedData.fields);
|
|
163
|
+
|
|
164
|
+
// Update usage stats
|
|
165
|
+
await this.updateUsageStats();
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async processData(data, settings) {
|
|
171
|
+
// Smart data processing based on content type
|
|
172
|
+
if (data.type === 'linkedin') {
|
|
173
|
+
return {
|
|
174
|
+
table: settings.linkedinTable || 'Contacts',
|
|
175
|
+
fields: this.processLinkedInData(data)
|
|
176
|
+
};
|
|
177
|
+
} else if (data.type === 'selection') {
|
|
178
|
+
return {
|
|
179
|
+
table: settings.defaultTable || 'Clips',
|
|
180
|
+
fields: {
|
|
181
|
+
'Content': data.text,
|
|
182
|
+
'Source URL': data.url,
|
|
183
|
+
'Date Saved': new Date().toISOString(),
|
|
184
|
+
'Type': 'Text Selection'
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
} else {
|
|
188
|
+
return {
|
|
189
|
+
table: settings.defaultTable || 'Clips',
|
|
190
|
+
fields: {
|
|
191
|
+
'Title': data.title || 'Untitled',
|
|
192
|
+
'URL': data.url,
|
|
193
|
+
'Content': data.content,
|
|
194
|
+
'Date Saved': new Date().toISOString(),
|
|
195
|
+
'Type': 'Web Page'
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
processLinkedInData(data) {
|
|
202
|
+
return {
|
|
203
|
+
'Name': data.name || '',
|
|
204
|
+
'Title': data.title || '',
|
|
205
|
+
'Company': data.company || '',
|
|
206
|
+
'Location': data.location || '',
|
|
207
|
+
'LinkedIn URL': data.url || '',
|
|
208
|
+
'Date Added': new Date().toISOString(),
|
|
209
|
+
'Source': 'LinkedIn',
|
|
210
|
+
'Notes': data.about ? data.about.substring(0, 500) + '...' : ''
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getSettings() {
|
|
215
|
+
const result = await chrome.storage.sync.get([
|
|
216
|
+
'airtableToken',
|
|
217
|
+
'baseId',
|
|
218
|
+
'linkedinTable',
|
|
219
|
+
'defaultTable',
|
|
220
|
+
'autoSave',
|
|
221
|
+
'notifications'
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
airtableToken: result.airtableToken || '',
|
|
226
|
+
baseId: result.baseId || '',
|
|
227
|
+
linkedinTable: result.linkedinTable || 'Contacts',
|
|
228
|
+
defaultTable: result.defaultTable || 'Clips',
|
|
229
|
+
autoSave: result.autoSave || false,
|
|
230
|
+
notifications: result.notifications !== false // Default to true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async saveSettings(settings) {
|
|
235
|
+
await chrome.storage.sync.set(settings);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async updateUsageStats() {
|
|
239
|
+
const stats = await chrome.storage.local.get(['usageStats']);
|
|
240
|
+
const currentStats = stats.usageStats || {
|
|
241
|
+
totalClips: 0,
|
|
242
|
+
weeklyClips: 0,
|
|
243
|
+
lastResetDate: new Date().toDateString()
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Reset weekly counter if it's a new week
|
|
247
|
+
const today = new Date().toDateString();
|
|
248
|
+
if (currentStats.lastResetDate !== today) {
|
|
249
|
+
const lastReset = new Date(currentStats.lastResetDate);
|
|
250
|
+
const daysSinceReset = Math.floor((Date.now() - lastReset.getTime()) / (1000 * 60 * 60 * 24));
|
|
251
|
+
|
|
252
|
+
if (daysSinceReset >= 7) {
|
|
253
|
+
currentStats.weeklyClips = 0;
|
|
254
|
+
currentStats.lastResetDate = today;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
currentStats.totalClips++;
|
|
259
|
+
currentStats.weeklyClips++;
|
|
260
|
+
|
|
261
|
+
await chrome.storage.local.set({ usageStats: currentStats });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setupNotifications() {
|
|
265
|
+
// Check if notifications are supported
|
|
266
|
+
if ('notifications' in chrome) {
|
|
267
|
+
// Request notification permission if needed
|
|
268
|
+
chrome.notifications.getPermissionLevel((level) => {
|
|
269
|
+
if (level !== 'granted') {
|
|
270
|
+
console.log('Notification permission not granted');
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
showSuccessNotification(message) {
|
|
277
|
+
this.showNotification('Success!', message, 'success');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
showErrorNotification(message) {
|
|
281
|
+
this.showNotification('Error', message, 'error');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async showNotification(title, message, type = 'basic') {
|
|
285
|
+
const settings = await this.getSettings();
|
|
286
|
+
if (!settings.notifications) return;
|
|
287
|
+
|
|
288
|
+
const iconUrl = type === 'error' ? 'icons/icon-error-48.png' : 'icons/icon-48.png';
|
|
289
|
+
|
|
290
|
+
chrome.notifications.create({
|
|
291
|
+
type: 'basic',
|
|
292
|
+
iconUrl: iconUrl,
|
|
293
|
+
title: title,
|
|
294
|
+
message: message
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async handleActionClick(tab) {
|
|
299
|
+
// Check if this is a LinkedIn profile page
|
|
300
|
+
if (tab.url.includes('linkedin.com/in/')) {
|
|
301
|
+
// Try to extract LinkedIn data automatically
|
|
302
|
+
try {
|
|
303
|
+
await this.triggerSave(tab, { type: 'linkedin' });
|
|
304
|
+
} catch (error) {
|
|
305
|
+
// Fallback to opening popup
|
|
306
|
+
chrome.action.openPopup();
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// Open popup for general pages
|
|
310
|
+
chrome.action.openPopup();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async cleanupStorage() {
|
|
315
|
+
// Clean up old temporary data
|
|
316
|
+
const result = await chrome.storage.local.get(null);
|
|
317
|
+
const keysToRemove = [];
|
|
318
|
+
|
|
319
|
+
for (const [key, value] of Object.entries(result)) {
|
|
320
|
+
// Remove temporary data older than 24 hours
|
|
321
|
+
if (key.startsWith('temp_') && value.timestamp) {
|
|
322
|
+
const age = Date.now() - value.timestamp;
|
|
323
|
+
if (age > 24 * 60 * 60 * 1000) { // 24 hours
|
|
324
|
+
keysToRemove.push(key);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (keysToRemove.length > 0) {
|
|
330
|
+
await chrome.storage.local.remove(keysToRemove);
|
|
331
|
+
console.log(`Cleaned up ${keysToRemove.length} temporary items`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Initialize the background service
|
|
337
|
+
new BackgroundService();
|