@sellable/mcp 0.1.219 → 0.1.220
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/dist/tools/csv-linkedin.js +149 -22
- package/dist/tools/linkedin.d.ts +37 -0
- package/dist/tools/linkedin.js +47 -17
- package/package.json +1 -1
- package/skills/research/config.json +0 -9
|
@@ -41,6 +41,9 @@ const RESERVED_TARGET_COLUMNS = new Set([
|
|
|
41
41
|
"Open Link",
|
|
42
42
|
"Enrich Prospect",
|
|
43
43
|
]);
|
|
44
|
+
const COLUMN_MAPPING_SAMPLE_SIZE = 50;
|
|
45
|
+
const MIN_COLUMN_CONFIDENCE = 0.65;
|
|
46
|
+
const MIN_DOMAIN_COLUMN_CONFIDENCE = 0.8;
|
|
44
47
|
const CSV_LINKEDIN_LIMITS = {
|
|
45
48
|
maxBytes: MAX_CSV_LINKEDIN_UPLOAD_BYTES,
|
|
46
49
|
maxRows: MAX_CSV_LINKEDIN_UPLOAD_ROWS,
|
|
@@ -187,38 +190,166 @@ function findHeader(csvHeaders, normalizedHeaders, candidates) {
|
|
|
187
190
|
}
|
|
188
191
|
return undefined;
|
|
189
192
|
}
|
|
190
|
-
function
|
|
193
|
+
function getSampleValues(rows, header) {
|
|
194
|
+
return rows
|
|
195
|
+
.slice(0, COLUMN_MAPPING_SAMPLE_SIZE)
|
|
196
|
+
.map((row) => String(row[header] ?? "").trim())
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
function getValueMatchStats(rows, header, predicate) {
|
|
200
|
+
const values = getSampleValues(rows, header);
|
|
201
|
+
const matchCount = values.filter(predicate).length;
|
|
202
|
+
return {
|
|
203
|
+
nonEmptyCount: values.length,
|
|
204
|
+
matchCount,
|
|
205
|
+
confidence: values.length > 0 ? matchCount / values.length : 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function hasConfidentValues(rows, header, predicate, options = {}) {
|
|
209
|
+
if (rows.length === 0) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
const stats = getValueMatchStats(rows, header, predicate);
|
|
213
|
+
if (stats.nonEmptyCount === 0) {
|
|
214
|
+
return options.allowEmpty ?? false;
|
|
215
|
+
}
|
|
216
|
+
const minMatches = Math.min(options.minMatches ?? 2, stats.nonEmptyCount);
|
|
217
|
+
const minConfidence = options.minConfidence ?? MIN_COLUMN_CONFIDENCE;
|
|
218
|
+
return stats.matchCount >= minMatches && stats.confidence >= minConfidence;
|
|
219
|
+
}
|
|
220
|
+
function findConfidentHeader(csvHeaders, normalizedHeaders, candidates, rows, predicate, options) {
|
|
221
|
+
const header = findHeader(csvHeaders, normalizedHeaders, candidates);
|
|
222
|
+
if (!header) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
return hasConfidentValues(rows, header, predicate, options)
|
|
226
|
+
? header
|
|
227
|
+
: undefined;
|
|
228
|
+
}
|
|
229
|
+
function findBestHeaderByContent(headers, rows, predicate, options = {}) {
|
|
230
|
+
if (rows.length === 0) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const candidates = headers
|
|
234
|
+
.map((header) => ({
|
|
235
|
+
header,
|
|
236
|
+
...getValueMatchStats(rows, header, predicate),
|
|
237
|
+
}))
|
|
238
|
+
.filter((entry) => {
|
|
239
|
+
const minMatches = Math.min(options.minMatches ?? 2, entry.nonEmptyCount);
|
|
240
|
+
const minConfidence = options.minConfidence ?? MIN_COLUMN_CONFIDENCE;
|
|
241
|
+
return (entry.nonEmptyCount > 0 &&
|
|
242
|
+
entry.matchCount >= minMatches &&
|
|
243
|
+
entry.confidence >= minConfidence);
|
|
244
|
+
})
|
|
245
|
+
.sort((left, right) => {
|
|
246
|
+
if (right.confidence !== left.confidence) {
|
|
247
|
+
return right.confidence - left.confidence;
|
|
248
|
+
}
|
|
249
|
+
return right.matchCount - left.matchCount;
|
|
250
|
+
});
|
|
251
|
+
if (candidates.length === 0) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
const [best, second] = candidates;
|
|
255
|
+
if (second &&
|
|
256
|
+
second.confidence === best.confidence &&
|
|
257
|
+
second.matchCount === best.matchCount) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return best.header;
|
|
261
|
+
}
|
|
262
|
+
function looksLikePersonName(value) {
|
|
263
|
+
const trimmed = value.trim();
|
|
264
|
+
if (trimmed.length < 2 || trimmed.length > 120) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (/[0-9@/:]/.test(trimmed) || sanitizeCompanyWebsite(trimmed)) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
271
|
+
if (words.length === 0 || words.length > 5) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if (/\b(inc|llc|ltd|corp|company|technologies|technology|security|systems|labs|software|group|studio|studios|ventures|capital|partners|agency|solutions|platform|cloud|data)\b/i.test(trimmed)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return words.every((word) => /\p{L}/u.test(word));
|
|
278
|
+
}
|
|
279
|
+
function looksLikeJobTitle(value) {
|
|
280
|
+
const trimmed = value.trim();
|
|
281
|
+
if (trimmed.length < 2 || trimmed.length > 160) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (/@|https?:\/\//i.test(trimmed) || sanitizeCompanyWebsite(trimmed)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
288
|
+
return words.length > 0 && words.length <= 12 && /\p{L}/u.test(trimmed);
|
|
289
|
+
}
|
|
290
|
+
function looksLikeLocation(value) {
|
|
291
|
+
const trimmed = value.trim();
|
|
292
|
+
if (trimmed.length < 2 || trimmed.length > 120) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
if (/[0-9@/]/.test(trimmed) || sanitizeCompanyWebsite(trimmed)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return /\p{L}/u.test(trimmed);
|
|
299
|
+
}
|
|
300
|
+
function looksLikeCompanyWebsite(value) {
|
|
301
|
+
return sanitizeCompanyWebsite(value) !== null;
|
|
302
|
+
}
|
|
303
|
+
function isWebsiteContentFallbackHeader(header) {
|
|
304
|
+
const key = normalizeHeaderKey(header);
|
|
305
|
+
if (key.includes("linkedin") ||
|
|
306
|
+
key.includes("profile") ||
|
|
307
|
+
key.includes("email")) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const normalized = normalizeHeaderForLookup(header);
|
|
311
|
+
return (normalized.includes("company") ||
|
|
312
|
+
normalized.includes("website") ||
|
|
313
|
+
normalized.includes("domain") ||
|
|
314
|
+
normalized.includes("url") ||
|
|
315
|
+
normalized.includes("site"));
|
|
316
|
+
}
|
|
317
|
+
function getSuggestedStandardMappings(headers, linkedInColumn, rows) {
|
|
191
318
|
const filteredHeaders = headers.filter((header) => header !== linkedInColumn);
|
|
192
319
|
const normalizedHeaders = filteredHeaders.map(normalizeHeaderForLookup);
|
|
193
320
|
const mappings = {};
|
|
194
|
-
const directNameHeader =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
321
|
+
const directNameHeader = findConfidentHeader(filteredHeaders, normalizedHeaders, ["name", "full name", "contact name", "contact full name"], rows, looksLikePersonName, { allowEmpty: true }) ??
|
|
322
|
+
findConfidentHeader(filteredHeaders, normalizedHeaders, [
|
|
323
|
+
"person",
|
|
324
|
+
"person name",
|
|
325
|
+
"prospect",
|
|
326
|
+
"prospect name",
|
|
327
|
+
"lead",
|
|
328
|
+
"lead name",
|
|
329
|
+
"recipient",
|
|
330
|
+
"recipient name",
|
|
331
|
+
], rows, looksLikePersonName);
|
|
199
332
|
if (directNameHeader) {
|
|
200
333
|
mappings["Name"] = directNameHeader;
|
|
201
334
|
}
|
|
202
335
|
else {
|
|
203
336
|
const firstNameIndex = normalizedHeaders.findIndex((header) => header === "first name");
|
|
204
337
|
const lastNameIndex = normalizedHeaders.findIndex((header) => header === "last name");
|
|
205
|
-
if (firstNameIndex !== -1 &&
|
|
338
|
+
if (firstNameIndex !== -1 &&
|
|
339
|
+
lastNameIndex !== -1 &&
|
|
340
|
+
hasConfidentValues(rows, filteredHeaders[firstNameIndex], looksLikePersonName, { allowEmpty: true }) &&
|
|
341
|
+
hasConfidentValues(rows, filteredHeaders[lastNameIndex], looksLikePersonName, { allowEmpty: true })) {
|
|
206
342
|
mappings["Name"] = [
|
|
207
343
|
filteredHeaders[firstNameIndex],
|
|
208
344
|
filteredHeaders[lastNameIndex],
|
|
209
345
|
];
|
|
210
346
|
}
|
|
211
347
|
}
|
|
212
|
-
const titleHeader =
|
|
213
|
-
"title",
|
|
214
|
-
"job title",
|
|
215
|
-
"position",
|
|
216
|
-
"role",
|
|
217
|
-
]);
|
|
348
|
+
const titleHeader = findConfidentHeader(filteredHeaders, normalizedHeaders, ["title", "job title", "position", "role"], rows, looksLikeJobTitle, { allowEmpty: true });
|
|
218
349
|
if (titleHeader) {
|
|
219
350
|
mappings["Title"] = titleHeader;
|
|
220
351
|
}
|
|
221
|
-
const companyWebsiteHeader =
|
|
352
|
+
const companyWebsiteHeader = findConfidentHeader(filteredHeaders, normalizedHeaders, [
|
|
222
353
|
"company website",
|
|
223
354
|
"website",
|
|
224
355
|
"domain",
|
|
@@ -226,16 +357,12 @@ function getSuggestedStandardMappings(headers, linkedInColumn) {
|
|
|
226
357
|
"website url",
|
|
227
358
|
"company url",
|
|
228
359
|
"organization website",
|
|
229
|
-
])
|
|
360
|
+
], rows, looksLikeCompanyWebsite, { allowEmpty: true, minConfidence: MIN_DOMAIN_COLUMN_CONFIDENCE }) ??
|
|
361
|
+
findBestHeaderByContent(filteredHeaders.filter(isWebsiteContentFallbackHeader), rows, looksLikeCompanyWebsite, { minConfidence: MIN_DOMAIN_COLUMN_CONFIDENCE, minMatches: 3 });
|
|
230
362
|
if (companyWebsiteHeader) {
|
|
231
363
|
mappings["Company Website"] = companyWebsiteHeader;
|
|
232
364
|
}
|
|
233
|
-
const locationHeader =
|
|
234
|
-
"location",
|
|
235
|
-
"city",
|
|
236
|
-
"geography",
|
|
237
|
-
"region",
|
|
238
|
-
]);
|
|
365
|
+
const locationHeader = findConfidentHeader(filteredHeaders, normalizedHeaders, ["location", "city", "geography", "region"], rows, looksLikeLocation, { allowEmpty: true });
|
|
239
366
|
if (locationHeader) {
|
|
240
367
|
mappings["Location"] = locationHeader;
|
|
241
368
|
}
|
|
@@ -595,7 +722,7 @@ export function buildCsvLinkedinPreview(input) {
|
|
|
595
722
|
}
|
|
596
723
|
}
|
|
597
724
|
const candidateLinkedInColumns = getCandidateLinkedInColumns(headers, parsed.rows);
|
|
598
|
-
const standardMappings = getSuggestedStandardMappings(headers, resolvedLinkedInColumn);
|
|
725
|
+
const standardMappings = getSuggestedStandardMappings(headers, resolvedLinkedInColumn, parsed.rows);
|
|
599
726
|
let selectedColumns = getSuggestedCarryoverColumns(headers, resolvedLinkedInColumn, standardMappings);
|
|
600
727
|
if (input.selectedColumns) {
|
|
601
728
|
if (input.selectedColumns.length > MAX_CSV_LINKEDIN_UPLOAD_CARRY_COLUMNS) {
|
package/dist/tools/linkedin.d.ts
CHANGED
|
@@ -176,6 +176,42 @@ export declare const linkedinToolDefinitions: ({
|
|
|
176
176
|
required: string[];
|
|
177
177
|
};
|
|
178
178
|
})[];
|
|
179
|
+
interface RawLinkedInPost {
|
|
180
|
+
id?: string;
|
|
181
|
+
entityId?: string;
|
|
182
|
+
activityId?: string;
|
|
183
|
+
author?: {
|
|
184
|
+
authorName?: string;
|
|
185
|
+
authorPublicIdentifier?: string;
|
|
186
|
+
};
|
|
187
|
+
text?: string;
|
|
188
|
+
content?: string;
|
|
189
|
+
activityDate?: string;
|
|
190
|
+
postedAt?: string | {
|
|
191
|
+
date?: string;
|
|
192
|
+
timestamp?: number;
|
|
193
|
+
postedAgoText?: string;
|
|
194
|
+
};
|
|
195
|
+
reactionsCount?: number;
|
|
196
|
+
commentsCount?: number;
|
|
197
|
+
engagement?: {
|
|
198
|
+
likes?: number;
|
|
199
|
+
comments?: number;
|
|
200
|
+
shares?: number;
|
|
201
|
+
};
|
|
202
|
+
activityUrl?: string;
|
|
203
|
+
linkedinUrl?: string;
|
|
204
|
+
shareLinkedinUrl?: string;
|
|
205
|
+
socialContent?: {
|
|
206
|
+
shareUrl?: string;
|
|
207
|
+
};
|
|
208
|
+
header?: {
|
|
209
|
+
linkedinUrl?: string;
|
|
210
|
+
};
|
|
211
|
+
isRepublishedPost?: boolean;
|
|
212
|
+
repostedBy?: unknown;
|
|
213
|
+
repostedAt?: unknown;
|
|
214
|
+
}
|
|
179
215
|
interface SerializedPost {
|
|
180
216
|
text: string;
|
|
181
217
|
date: string;
|
|
@@ -184,6 +220,7 @@ interface SerializedPost {
|
|
|
184
220
|
url: string;
|
|
185
221
|
isRepost: boolean;
|
|
186
222
|
}
|
|
223
|
+
export declare function serializeLinkedInPosts(rawPosts: RawLinkedInPost[] | undefined, context: string): SerializedPost[];
|
|
187
224
|
export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number): Promise<{
|
|
188
225
|
posts: SerializedPost[];
|
|
189
226
|
count: number;
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -137,6 +137,51 @@ export const linkedinToolDefinitions = [
|
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
139
|
];
|
|
140
|
+
function normalizePostDate(post) {
|
|
141
|
+
if (post.activityDate)
|
|
142
|
+
return post.activityDate;
|
|
143
|
+
if (typeof post.postedAt === "string")
|
|
144
|
+
return post.postedAt;
|
|
145
|
+
if (post.postedAt?.date)
|
|
146
|
+
return post.postedAt.date;
|
|
147
|
+
if (typeof post.postedAt?.timestamp === "number") {
|
|
148
|
+
return new Date(post.postedAt.timestamp).toISOString();
|
|
149
|
+
}
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
function normalizePostUrl(post) {
|
|
153
|
+
return (post.activityUrl ||
|
|
154
|
+
post.linkedinUrl ||
|
|
155
|
+
post.shareLinkedinUrl ||
|
|
156
|
+
post.socialContent?.shareUrl ||
|
|
157
|
+
post.header?.linkedinUrl ||
|
|
158
|
+
"");
|
|
159
|
+
}
|
|
160
|
+
function serializeLinkedInPost(post) {
|
|
161
|
+
return {
|
|
162
|
+
text: post.text || post.content || "",
|
|
163
|
+
date: normalizePostDate(post),
|
|
164
|
+
reactions: post.reactionsCount ?? post.engagement?.likes ?? 0,
|
|
165
|
+
comments: post.commentsCount ?? post.engagement?.comments ?? 0,
|
|
166
|
+
url: normalizePostUrl(post),
|
|
167
|
+
isRepost: Boolean(post.isRepublishedPost || post.repostedBy || post.repostedAt),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function isUsableSerializedPost(post) {
|
|
171
|
+
return Boolean(post.text.trim() ||
|
|
172
|
+
post.date ||
|
|
173
|
+
post.url ||
|
|
174
|
+
post.reactions > 0 ||
|
|
175
|
+
post.comments > 0);
|
|
176
|
+
}
|
|
177
|
+
export function serializeLinkedInPosts(rawPosts, context) {
|
|
178
|
+
const raw = rawPosts || [];
|
|
179
|
+
const posts = raw.map(serializeLinkedInPost).filter(isUsableSerializedPost);
|
|
180
|
+
if (raw.length > 0 && posts.length === 0) {
|
|
181
|
+
throw new Error(`${context} returned ${raw.length} post placeholder row(s), but none contained usable text, date, URL, or engagement data.`);
|
|
182
|
+
}
|
|
183
|
+
return posts;
|
|
184
|
+
}
|
|
140
185
|
export async function fetchLinkedInPosts(linkedinUrl, limit = 25) {
|
|
141
186
|
const api = getApi();
|
|
142
187
|
const normalizedLinkedinUrl = normalizeLinkedInProfileInput(linkedinUrl);
|
|
@@ -145,15 +190,7 @@ export async function fetchLinkedInPosts(linkedinUrl, limit = 25) {
|
|
|
145
190
|
limit: String(limit),
|
|
146
191
|
});
|
|
147
192
|
const response = await api.get(`/api/v1/scrape/linkedin/user-posts?${params.toString()}`);
|
|
148
|
-
|
|
149
|
-
const posts = (response.posts || []).map((post) => ({
|
|
150
|
-
text: post.text || "",
|
|
151
|
-
date: post.activityDate || "",
|
|
152
|
-
reactions: post.reactionsCount || 0,
|
|
153
|
-
comments: post.commentsCount || 0,
|
|
154
|
-
url: post.activityUrl || "",
|
|
155
|
-
isRepost: post.isRepublishedPost || false,
|
|
156
|
-
}));
|
|
193
|
+
const posts = serializeLinkedInPosts(response.posts, "LinkedIn user posts fetch");
|
|
157
194
|
return {
|
|
158
195
|
posts,
|
|
159
196
|
count: posts.length,
|
|
@@ -198,14 +235,7 @@ export async function fetchCompanyPosts(companyUrl, limit = 25, sortBy = "recent
|
|
|
198
235
|
sortBy,
|
|
199
236
|
});
|
|
200
237
|
const response = await api.get(`/api/v1/scrape/linkedin/company-posts?${params.toString()}`);
|
|
201
|
-
const posts = (response.posts
|
|
202
|
-
text: post.text || "",
|
|
203
|
-
date: post.activityDate || "",
|
|
204
|
-
reactions: post.reactionsCount || 0,
|
|
205
|
-
comments: post.commentsCount || 0,
|
|
206
|
-
url: post.activityUrl || "",
|
|
207
|
-
isRepost: post.isRepublishedPost || false,
|
|
208
|
-
}));
|
|
238
|
+
const posts = serializeLinkedInPosts(response.posts, "LinkedIn company posts fetch");
|
|
209
239
|
return {
|
|
210
240
|
posts,
|
|
211
241
|
count: posts.length,
|
package/package.json
CHANGED