@sellable/mcp 0.1.219 → 0.1.221

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/server.js CHANGED
@@ -17,7 +17,7 @@ import { copySenderConfigTool, getEngageMemoryTool, migrateFlatConfigsTool, reco
17
17
  import { getEngageStateTool, setEngageStateTool, } from "./tools/engage-state.js";
18
18
  import { bulkEnrichWithProspeo, enrichWithProspeo, getProspeoCredits, } from "./tools/enrichment.js";
19
19
  import { getCampaignFramework } from "./tools/framework.js";
20
- import { cancelLeadImport, confirmLeadList, getProviderPrompt, importLeads, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
20
+ import { cancelLeadImport, confirmProspeoCompanyAccounts, confirmLeadList, getProviderPrompt, importLeads, loadCsvDomains, loadCsvLinkedinLeads, lookupSalesNavFilter, saveDomainFilters, searchApollo, searchProspeoCompanies, searchProspeo, searchSalesNav, searchSignals, selectPromisingPosts, setHeadlineICPCriteria, } from "./tools/leads.js";
21
21
  import { fetchCompany, fetchCompanyPosts, fetchLinkedInPosts, fetchLinkedInProfile, fetchPostEngagers, getLinkedInProfile, getUserPosts, } from "./tools/linkedin.js";
22
22
  import { getCampaignNavigationState } from "./tools/navigation.js";
23
23
  import { addOnDemandLeads, createOnDemandCampaign, createOnDemandTable, initOnDemandSequence, pauseOnDemandCampaign, startOnDemandCampaign, } from "./tools/one-off.js";
@@ -311,6 +311,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
311
311
  case "search_prospeo":
312
312
  result = await searchProspeo(args);
313
313
  break;
314
+ case "search_prospeo_companies":
315
+ result = await searchProspeoCompanies(args);
316
+ break;
317
+ case "confirm_prospeo_company_accounts":
318
+ result = await confirmProspeoCompanyAccounts(args);
319
+ break;
314
320
  case "load_csv_domains":
315
321
  result = await loadCsvDomains(args);
316
322
  break;
@@ -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 getSuggestedStandardMappings(headers, linkedInColumn) {
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 = findHeader(filteredHeaders, normalizedHeaders, [
195
- "name",
196
- "full name",
197
- "contact name",
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 && lastNameIndex !== -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 = findHeader(filteredHeaders, normalizedHeaders, [
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 = findHeader(filteredHeaders, normalizedHeaders, [
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 = findHeader(filteredHeaders, normalizedHeaders, [
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) {