salesprompter-cli 0.1.29 → 0.1.30
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/README.md +0 -1
- package/dist/cli.js +262 -2254
- package/dist/deel-outreach.js +1 -16
- package/dist/direct-path.js +1 -16
- package/package.json +1 -2
- package/dist/hunter-emails.js +0 -291
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { access, appendFile, mkdir, readFile,
|
|
3
|
+
import { access, appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
@@ -33,9 +33,7 @@ import { buildSalesNavigatorHistoricalBackfillPlan, ensureSalesNavigatorPeopleCo
|
|
|
33
33
|
const require = createRequire(import.meta.url);
|
|
34
34
|
const { version: packageVersion } = require("../package.json");
|
|
35
35
|
const program = new Command();
|
|
36
|
-
const
|
|
37
|
-
const peopleSearchProvider = new HeuristicPeopleSearchProvider();
|
|
38
|
-
const leadProvider = new AccountLeadProvider(companyProvider, peopleSearchProvider);
|
|
36
|
+
const leadProvider = new AccountLeadProvider(new HeuristicCompanyProvider(), new HeuristicPeopleSearchProvider());
|
|
39
37
|
const enrichmentProvider = new HeuristicEnrichmentProvider();
|
|
40
38
|
const scoringProvider = new HeuristicScoringProvider();
|
|
41
39
|
const syncProvider = new RoutedSyncProvider(new DryRunSyncProvider(), new InstantlySyncProvider());
|
|
@@ -96,22 +94,6 @@ const LinkedInCompanyBackfillStatusResponseSchema = z.object({
|
|
|
96
94
|
failureCode: z.string().nullable().optional(),
|
|
97
95
|
failureMessage: z.string().nullable().optional()
|
|
98
96
|
});
|
|
99
|
-
const PhantombusterContainersSyncResponseSchema = z.object({
|
|
100
|
-
status: z.literal("ok"),
|
|
101
|
-
agentIds: z.array(z.string().min(1)),
|
|
102
|
-
agents: z.array(z.object({
|
|
103
|
-
agentId: z.string().min(1),
|
|
104
|
-
fetched: z.number().int().nonnegative(),
|
|
105
|
-
upserted: z.number().int().nonnegative(),
|
|
106
|
-
resultsSynced: z.number().int().nonnegative()
|
|
107
|
-
})),
|
|
108
|
-
fetched: z.number().int().nonnegative(),
|
|
109
|
-
upserted: z.number().int().nonnegative(),
|
|
110
|
-
resultsSynced: z.number().int().nonnegative(),
|
|
111
|
-
outputsStored: z.number().int().nonnegative(),
|
|
112
|
-
resultObjectsStored: z.number().int().nonnegative(),
|
|
113
|
-
resultRowsStored: z.number().int().nonnegative()
|
|
114
|
-
});
|
|
115
97
|
const CliEmailEnrichmentCompaniesResponseSchema = z.object({
|
|
116
98
|
clientId: z.number().int().positive(),
|
|
117
99
|
companies: z.array(z.object({
|
|
@@ -939,13 +921,6 @@ function splitLookupFullName(fullName) {
|
|
|
939
921
|
function buildSyntheticLookupEmail(contactId) {
|
|
940
922
|
return `linkedin-lookup+${contactId}@salesprompter.invalid`;
|
|
941
923
|
}
|
|
942
|
-
function normalizeLinkedInLookupField(value) {
|
|
943
|
-
if (value == null) {
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
const normalized = normalizeLookupWhitespace(String(value));
|
|
947
|
-
return normalized || undefined;
|
|
948
|
-
}
|
|
949
924
|
function looksLikeLookupCompanyRow(fullName, companyName) {
|
|
950
925
|
const fullNameComparable = normalizeLooseMatchText(fullName);
|
|
951
926
|
const companyComparable = normalizeLooseMatchText(companyName);
|
|
@@ -965,32 +940,19 @@ function parseLinkedInUrlLookupInput(content) {
|
|
|
965
940
|
const parsed = z
|
|
966
941
|
.array(z.object({
|
|
967
942
|
clientId: z.union([z.string(), z.number()]).nullish(),
|
|
968
|
-
contactId: z.union([z.string(), z.number()]).nullish(),
|
|
969
|
-
companyId: z.union([z.string(), z.number()]).nullish(),
|
|
970
943
|
fullName: z.string().nullish(),
|
|
971
944
|
companyName: z.string().nullish(),
|
|
972
945
|
email: z.string().nullish(),
|
|
973
|
-
|
|
974
|
-
jobTitle: z.string().nullish(),
|
|
975
|
-
jobtitle: z.string().nullish(),
|
|
976
|
-
title: z.string().nullish(),
|
|
977
|
-
linkedin_company_url: z.string().nullish(),
|
|
978
|
-
linkedinCompanyUrl: z.string().nullish(),
|
|
979
|
-
deep_dive_recommended_role: z.string().nullish(),
|
|
980
|
-
deepDiveRecommendedRole: z.string().nullish()
|
|
946
|
+
jobTitle: z.string().nullish()
|
|
981
947
|
}))
|
|
982
948
|
.parse(JSON.parse(trimmed));
|
|
983
949
|
return parsed
|
|
984
950
|
.map((row) => ({
|
|
985
951
|
clientId: row.clientId == null ? null : String(row.clientId).trim() || null,
|
|
986
|
-
contactId: row.contactId == null ? undefined : String(row.contactId).trim() || undefined,
|
|
987
|
-
companyId: row.companyId == null ? undefined : String(row.companyId).trim() || undefined,
|
|
988
952
|
fullName: row.fullName?.trim() ?? "",
|
|
989
953
|
companyName: row.companyName?.trim() ?? "",
|
|
990
|
-
email: row.email?.trim() ||
|
|
991
|
-
jobTitle: row.jobTitle?.trim() ||
|
|
992
|
-
linkedinCompanyUrl: row.linkedin_company_url?.trim() || row.linkedinCompanyUrl?.trim() || undefined,
|
|
993
|
-
deepDiveRecommendedRole: row.deep_dive_recommended_role?.trim() || row.deepDiveRecommendedRole?.trim() || undefined
|
|
954
|
+
email: row.email?.trim() || undefined,
|
|
955
|
+
jobTitle: row.jobTitle?.trim() || undefined
|
|
994
956
|
}))
|
|
995
957
|
.filter((row) => row.fullName.length > 0 || row.companyName.length > 0);
|
|
996
958
|
}
|
|
@@ -1018,35 +980,17 @@ function parseLinkedInUrlLookupInput(content) {
|
|
|
1018
980
|
? headerValues.findIndex((value) => ["companyname", "company_name"].includes(value))
|
|
1019
981
|
: 2;
|
|
1020
982
|
const emailIndex = hasHeader ? headerValues.findIndex((value) => value === "email") : -1;
|
|
1021
|
-
const contactEmailIndex = hasHeader ? headerValues.findIndex((value) => value === "contact_email") : -1;
|
|
1022
983
|
const jobTitleIndex = hasHeader
|
|
1023
984
|
? headerValues.findIndex((value) => ["jobtitle", "job_title", "title"].includes(value))
|
|
1024
985
|
: -1;
|
|
1025
|
-
const contactIdIndex = hasHeader
|
|
1026
|
-
? headerValues.findIndex((value) => ["contactid", "contact_id", "hubspot_contact_id"].includes(value))
|
|
1027
|
-
: -1;
|
|
1028
|
-
const companyIdIndex = hasHeader
|
|
1029
|
-
? headerValues.findIndex((value) => ["companyid", "company_id", "hubspot_company_id"].includes(value))
|
|
1030
|
-
: -1;
|
|
1031
|
-
const linkedinCompanyUrlIndex = hasHeader
|
|
1032
|
-
? headerValues.findIndex((value) => ["linkedin_company_url", "linkedincompanyurl"].includes(value))
|
|
1033
|
-
: -1;
|
|
1034
|
-
const deepDiveRecommendedRoleIndex = hasHeader
|
|
1035
|
-
? headerValues.findIndex((value) => ["deep_dive_recommended_role", "deepdiverecommendedrole"].includes(value))
|
|
1036
|
-
: -1;
|
|
1037
986
|
return dataLines
|
|
1038
987
|
.map((line) => splitLooseDelimitedLine(line, delimiter).map((value) => value.trim()))
|
|
1039
988
|
.map((columns) => ({
|
|
1040
989
|
clientId: clientIdIndex >= 0 ? columns[clientIdIndex] || null : null,
|
|
1041
|
-
contactId: contactIdIndex >= 0 ? columns[contactIdIndex] || undefined : undefined,
|
|
1042
|
-
companyId: companyIdIndex >= 0 ? columns[companyIdIndex] || undefined : undefined,
|
|
1043
990
|
fullName: fullNameIndex >= 0 ? columns[fullNameIndex] || "" : "",
|
|
1044
991
|
companyName: companyNameIndex >= 0 ? columns[companyNameIndex] || "" : "",
|
|
1045
|
-
email:
|
|
1046
|
-
|
|
1047
|
-
jobTitle: jobTitleIndex >= 0 ? columns[jobTitleIndex] || undefined : undefined,
|
|
1048
|
-
linkedinCompanyUrl: linkedinCompanyUrlIndex >= 0 ? columns[linkedinCompanyUrlIndex] || undefined : undefined,
|
|
1049
|
-
deepDiveRecommendedRole: deepDiveRecommendedRoleIndex >= 0 ? columns[deepDiveRecommendedRoleIndex] || undefined : undefined
|
|
992
|
+
email: emailIndex >= 0 ? columns[emailIndex] || undefined : undefined,
|
|
993
|
+
jobTitle: jobTitleIndex >= 0 ? columns[jobTitleIndex] || undefined : undefined
|
|
1050
994
|
}))
|
|
1051
995
|
.filter((row) => row.fullName.length > 0 || row.companyName.length > 0);
|
|
1052
996
|
}
|
|
@@ -1101,7 +1045,7 @@ function parseLinkedInCompanyLookupInput(content) {
|
|
|
1101
1045
|
}
|
|
1102
1046
|
function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
1103
1047
|
return rows.flatMap((row, index) => {
|
|
1104
|
-
const contactId =
|
|
1048
|
+
const contactId = String(index + 1);
|
|
1105
1049
|
const syntheticEmail = row.email?.trim() || buildSyntheticLookupEmail(contactId);
|
|
1106
1050
|
const rawCompanyName = normalizeLookupWhitespace(row.companyName);
|
|
1107
1051
|
const cleanedCompanyName = normalizeLookupCompanyForSearch(cleanedCompanyMap.get(normalizeLookupCompanyForCleaning(rawCompanyName)) ?? rawCompanyName);
|
|
@@ -1115,10 +1059,7 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1115
1059
|
companyName: cleanedCompanyName,
|
|
1116
1060
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1117
1061
|
email: syntheticEmail,
|
|
1118
|
-
jobTitle: row.jobTitle
|
|
1119
|
-
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1120
|
-
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1121
|
-
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined
|
|
1062
|
+
jobTitle: row.jobTitle
|
|
1122
1063
|
}
|
|
1123
1064
|
];
|
|
1124
1065
|
}
|
|
@@ -1133,10 +1074,7 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1133
1074
|
companyName: cleanedCompanyName,
|
|
1134
1075
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1135
1076
|
email: syntheticEmail,
|
|
1136
|
-
jobTitle: row.jobTitle
|
|
1137
|
-
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1138
|
-
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1139
|
-
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined
|
|
1077
|
+
jobTitle: row.jobTitle
|
|
1140
1078
|
}
|
|
1141
1079
|
];
|
|
1142
1080
|
const rawDiffers = rawSplit.firstName !== cleanedSplit.firstName ||
|
|
@@ -1150,9 +1088,6 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1150
1088
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1151
1089
|
email: syntheticEmail,
|
|
1152
1090
|
jobTitle: row.jobTitle,
|
|
1153
|
-
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1154
|
-
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1155
|
-
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined,
|
|
1156
1091
|
isVariation: true
|
|
1157
1092
|
});
|
|
1158
1093
|
}
|
|
@@ -1175,132 +1110,10 @@ function readPipedreamLinkedInEnrichmentConfig() {
|
|
|
1175
1110
|
projectEnvironment: resolveConfiguredEnvValue(process.env, "PIPEDREAM_PROJECT_ENVIRONMENT") || ""
|
|
1176
1111
|
};
|
|
1177
1112
|
}
|
|
1178
|
-
function isSyntheticLinkedInLookupEmail(value) {
|
|
1179
|
-
const normalized = normalizeLookupWhitespace(value).toLowerCase();
|
|
1180
|
-
return normalized.endsWith("@salesprompter.invalid");
|
|
1181
|
-
}
|
|
1182
1113
|
function deriveCsrfTokenFromCookie(cookie) {
|
|
1183
1114
|
const match = cookie.match(/JSESSIONID="?([^";]+)"?/i);
|
|
1184
1115
|
return match?.[1]?.trim() || "";
|
|
1185
1116
|
}
|
|
1186
|
-
function normalizeLinkedInDirectLookupCookieHeader(cookie) {
|
|
1187
|
-
const trimmed = normalizeLookupWhitespace(cookie);
|
|
1188
|
-
if (!trimmed) {
|
|
1189
|
-
return "";
|
|
1190
|
-
}
|
|
1191
|
-
if (trimmed.includes("=") || trimmed.includes(";")) {
|
|
1192
|
-
return trimmed;
|
|
1193
|
-
}
|
|
1194
|
-
return `li_at=${trimmed}`;
|
|
1195
|
-
}
|
|
1196
|
-
function parseLocalLinkedInExtensionTokenLog(content) {
|
|
1197
|
-
const matches = [
|
|
1198
|
-
...content.matchAll(/\{"csrfToken":"([^"]+)","extractedFrom":"sales-api\/salesApiLeadSearch"[\s\S]*?"linkedInIdentity":"([^"]+)"[\s\S]*?"sessionCookie":"([\s\S]*?)","syncStatus":"(success|captured)"[\s\S]*?"userAgent":"([^"]+)"\}/g)
|
|
1199
|
-
];
|
|
1200
|
-
const last = matches.at(-1);
|
|
1201
|
-
if (!last) {
|
|
1202
|
-
return null;
|
|
1203
|
-
}
|
|
1204
|
-
const csrfToken = normalizeLookupWhitespace(last[1]);
|
|
1205
|
-
const linkedInIdentity = normalizeLookupWhitespace(last[2]);
|
|
1206
|
-
const sessionCookie = normalizeLookupWhitespace(last[3]?.replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
|
|
1207
|
-
const userAgent = normalizeLookupWhitespace(last[5]);
|
|
1208
|
-
if (!csrfToken || !linkedInIdentity || !sessionCookie || !userAgent) {
|
|
1209
|
-
return null;
|
|
1210
|
-
}
|
|
1211
|
-
return {
|
|
1212
|
-
csrfToken,
|
|
1213
|
-
linkedInIdentity,
|
|
1214
|
-
sessionCookie,
|
|
1215
|
-
userAgent
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
async function readLocalLinkedInExtensionTokenLog(filePath) {
|
|
1219
|
-
try {
|
|
1220
|
-
const content = await readFile(filePath, "latin1");
|
|
1221
|
-
return parseLocalLinkedInExtensionTokenLog(content);
|
|
1222
|
-
}
|
|
1223
|
-
catch {
|
|
1224
|
-
return null;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
async function listChromeExtensionTokenLogCandidates() {
|
|
1228
|
-
const overrideFile = normalizeLookupWhitespace(process.env.SALESPROMPTER_LINKEDIN_EXTENSION_TOKENS_LOG_PATH);
|
|
1229
|
-
if (overrideFile) {
|
|
1230
|
-
return [overrideFile];
|
|
1231
|
-
}
|
|
1232
|
-
const overrideDir = normalizeLookupWhitespace(process.env.SALESPROMPTER_LINKEDIN_EXTENSION_TOKENS_DIR);
|
|
1233
|
-
if (overrideDir) {
|
|
1234
|
-
try {
|
|
1235
|
-
const files = await readdir(overrideDir);
|
|
1236
|
-
return files
|
|
1237
|
-
.filter((file) => file.endsWith(".log") || file.endsWith(".ldb"))
|
|
1238
|
-
.map((file) => path.join(overrideDir, file))
|
|
1239
|
-
.sort()
|
|
1240
|
-
.reverse();
|
|
1241
|
-
}
|
|
1242
|
-
catch {
|
|
1243
|
-
return [];
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
const chromeRootCandidates = [
|
|
1247
|
-
path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome"),
|
|
1248
|
-
path.join(os.homedir(), "Library", "Application Support", "Chromium")
|
|
1249
|
-
];
|
|
1250
|
-
const paths = [];
|
|
1251
|
-
for (const chromeRoot of chromeRootCandidates) {
|
|
1252
|
-
let profileDirs = [];
|
|
1253
|
-
try {
|
|
1254
|
-
profileDirs = await readdir(chromeRoot);
|
|
1255
|
-
}
|
|
1256
|
-
catch {
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
for (const profileDir of profileDirs) {
|
|
1260
|
-
const extensionSettingsRoot = path.join(chromeRoot, profileDir, "Local Extension Settings");
|
|
1261
|
-
let extensionIds = [];
|
|
1262
|
-
try {
|
|
1263
|
-
extensionIds = await readdir(extensionSettingsRoot);
|
|
1264
|
-
}
|
|
1265
|
-
catch {
|
|
1266
|
-
continue;
|
|
1267
|
-
}
|
|
1268
|
-
for (const extensionId of extensionIds) {
|
|
1269
|
-
const extensionDir = path.join(extensionSettingsRoot, extensionId);
|
|
1270
|
-
let files = [];
|
|
1271
|
-
try {
|
|
1272
|
-
files = await readdir(extensionDir);
|
|
1273
|
-
}
|
|
1274
|
-
catch {
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
for (const file of files) {
|
|
1278
|
-
if (!file.endsWith(".log")) {
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
paths.push(path.join(extensionDir, file));
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
return paths.sort().reverse();
|
|
1287
|
-
}
|
|
1288
|
-
async function readLocalLinkedInExtensionDirectLookupConfig() {
|
|
1289
|
-
const candidates = await listChromeExtensionTokenLogCandidates();
|
|
1290
|
-
for (const candidate of candidates) {
|
|
1291
|
-
const snapshot = await readLocalLinkedInExtensionTokenLog(candidate);
|
|
1292
|
-
if (!snapshot) {
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1295
|
-
return {
|
|
1296
|
-
csrfToken: snapshot.csrfToken,
|
|
1297
|
-
identity: snapshot.linkedInIdentity,
|
|
1298
|
-
cookie: normalizeLinkedInDirectLookupCookieHeader(snapshot.sessionCookie),
|
|
1299
|
-
userAgent: snapshot.userAgent
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
return null;
|
|
1303
|
-
}
|
|
1304
1117
|
function readLinkedInDirectLookupEnvConfig() {
|
|
1305
1118
|
const cookie = process.env.SALESPROMPTER_LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
1306
1119
|
process.env.LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
@@ -1317,7 +1130,7 @@ function readLinkedInDirectLookupEnvConfig() {
|
|
|
1317
1130
|
return {
|
|
1318
1131
|
csrfToken,
|
|
1319
1132
|
identity,
|
|
1320
|
-
cookie
|
|
1133
|
+
cookie,
|
|
1321
1134
|
userAgent: process.env.SALESPROMPTER_LINKEDIN_USER_AGENT?.trim() ||
|
|
1322
1135
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
1323
1136
|
};
|
|
@@ -1367,7 +1180,7 @@ async function readStoredLinkedInDirectLookupConfig() {
|
|
|
1367
1180
|
return {
|
|
1368
1181
|
csrfToken,
|
|
1369
1182
|
identity,
|
|
1370
|
-
cookie:
|
|
1183
|
+
cookie: claimed.sessionCookie,
|
|
1371
1184
|
userAgent
|
|
1372
1185
|
};
|
|
1373
1186
|
}
|
|
@@ -1381,11 +1194,6 @@ async function readLinkedInDirectLookupConfig() {
|
|
|
1381
1194
|
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1382
1195
|
return envConfig;
|
|
1383
1196
|
}
|
|
1384
|
-
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
1385
|
-
if (localExtensionConfig) {
|
|
1386
|
-
cachedLinkedInDirectLookupConfig = localExtensionConfig;
|
|
1387
|
-
return localExtensionConfig;
|
|
1388
|
-
}
|
|
1389
1197
|
const storedConfig = await readStoredLinkedInDirectLookupConfig();
|
|
1390
1198
|
if (storedConfig) {
|
|
1391
1199
|
cachedLinkedInDirectLookupConfig = storedConfig;
|
|
@@ -1402,200 +1210,46 @@ function buildLinkedInSalesApiUrl(params) {
|
|
|
1402
1210
|
const encodedFirstName = encodeURIComponent(params.firstName);
|
|
1403
1211
|
const encodedLastName = encodeURIComponent(params.lastName);
|
|
1404
1212
|
const encodedCompanyName = encodeURIComponent(params.companyName);
|
|
1405
|
-
const encodedKeywords = encodeURIComponent(params.keywordsText?.trim() || params.companyName);
|
|
1406
1213
|
const filters = params.searchMode === "current_company"
|
|
1407
1214
|
? `(type:FIRST_NAME,values:List((text:${encodedFirstName},selectionType:INCLUDED))),(type:LAST_NAME,values:List((text:${encodedLastName},selectionType:INCLUDED))),(type:CURRENT_COMPANY,values:List((text:${encodedCompanyName},selectionType:INCLUDED)))`
|
|
1408
1215
|
: `(type:FIRST_NAME,values:List((text:${encodedFirstName},selectionType:INCLUDED))),(type:LAST_NAME,values:List((text:${encodedLastName},selectionType:INCLUDED)))`;
|
|
1409
|
-
const keywordsSegment = params.searchMode === "
|
|
1216
|
+
const keywordsSegment = params.searchMode === "keywords" ? `,keywords:${encodedCompanyName}` : "";
|
|
1410
1217
|
return `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiLeadSearch?q=searchQuery&query=(recentSearchParam:(id:${Date.now()},doLogHistory:true),filters:List(${filters})${keywordsSegment})&start=0&count=25&trackingParam=(sessionId:${generateLinkedInSessionId()})&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14`;
|
|
1411
1218
|
}
|
|
1412
|
-
function extractLookupTitleKeywords(value) {
|
|
1413
|
-
const shortAllowlist = new Set(["hr", "it", "cfo"]);
|
|
1414
|
-
return normalizeLooseMatchText(value)
|
|
1415
|
-
.split(/\s+/)
|
|
1416
|
-
.filter((token) => token.length >= 4 || shortAllowlist.has(token))
|
|
1417
|
-
.filter((token) => ![
|
|
1418
|
-
"head",
|
|
1419
|
-
"senior",
|
|
1420
|
-
"consultant",
|
|
1421
|
-
"manager",
|
|
1422
|
-
"specialist",
|
|
1423
|
-
"lead",
|
|
1424
|
-
"global",
|
|
1425
|
-
"team",
|
|
1426
|
-
"group"
|
|
1427
|
-
].includes(token))
|
|
1428
|
-
.slice(0, 4);
|
|
1429
|
-
}
|
|
1430
|
-
function buildDeepDiveRoleSearchKeywords(role) {
|
|
1431
|
-
const normalized = normalizeLooseMatchText(role);
|
|
1432
|
-
switch (normalized) {
|
|
1433
|
-
case "budgetholder":
|
|
1434
|
-
return ["finance", "procurement", "purchasing", "accounting", "controlling", "cfo"];
|
|
1435
|
-
case "decisionmaker":
|
|
1436
|
-
return ["director", "head", "vp", "chief", "leiter", "lead"];
|
|
1437
|
-
case "champion":
|
|
1438
|
-
return ["hr", "workplace", "operations", "it", "people", "office"];
|
|
1439
|
-
case "executivesponsor":
|
|
1440
|
-
return ["executive", "board", "chief", "managing", "director", "ceo"];
|
|
1441
|
-
case "influencer":
|
|
1442
|
-
return ["specialist", "manager", "consultant", "project", "workplace", "hr"];
|
|
1443
|
-
case "legalandcompliance":
|
|
1444
|
-
return ["legal", "compliance", "datenschutz", "counsel"];
|
|
1445
|
-
case "blocker":
|
|
1446
|
-
return ["procurement", "legal", "compliance", "security"];
|
|
1447
|
-
case "enduser":
|
|
1448
|
-
return ["workplace", "office", "operations", "assistant", "admin"];
|
|
1449
|
-
default:
|
|
1450
|
-
return [];
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
1219
|
function buildLinkedInAccountSearchApiUrl(companyName) {
|
|
1454
1220
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
1455
1221
|
"https://www.linkedin.com";
|
|
1456
1222
|
const encodedCompanyName = encodeURIComponent(companyName);
|
|
1457
1223
|
return `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiAccountSearch?q=searchQuery&query=(recentSearchParam:(id:${Date.now()},doLogHistory:true),spellCorrectionEnabled:true,keywords:${encodedCompanyName})&start=0&count=10&trackingParam=(sessionId:${generateLinkedInSessionId()})&decorationId=com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-14`;
|
|
1458
1224
|
}
|
|
1459
|
-
|
|
1225
|
+
function buildLinkedInLookupSearchVariants(contact) {
|
|
1460
1226
|
const variants = [];
|
|
1461
1227
|
const seen = new Set();
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
const at = email.lastIndexOf("@");
|
|
1485
|
-
return at >= 0 ? email.slice(at + 1) : "";
|
|
1486
|
-
})();
|
|
1487
|
-
if (emailDomain) {
|
|
1488
|
-
const host = emailDomain.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
1489
|
-
if (host) {
|
|
1490
|
-
addCompanyCandidate(host.replace(/[-_]+/g, " "), 100);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
if (contact.jobTitle && contact.deepDiveRecommendedRole) {
|
|
1494
|
-
const primaryWord = normalizeLookupWhitespace(contact.companyNameOriginal ?? contact.companyName)
|
|
1495
|
-
.split(/\s+/)
|
|
1496
|
-
.filter((part) => part.length >= 4)
|
|
1497
|
-
.slice(-1)[0];
|
|
1498
|
-
if (primaryWord) {
|
|
1499
|
-
addCompanyCandidate(primaryWord, 45);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
const companyHints = await buildLinkedInProfileCompanyHints(contact, timeoutMs);
|
|
1503
|
-
for (const phrase of companyHints.phrases) {
|
|
1504
|
-
const tokenCount = normalizeLooseMatchText(phrase).split(/\s+/).filter(Boolean).length;
|
|
1505
|
-
if (tokenCount >= 1 && tokenCount <= 4) {
|
|
1506
|
-
addCompanyCandidate(phrase, tokenCount <= 2 ? 75 : 60);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
for (const keyword of companyHints.keywords.slice(0, 5)) {
|
|
1510
|
-
addCompanyCandidate(keyword, keyword.includes(".") ? 90 : 55);
|
|
1511
|
-
}
|
|
1512
|
-
const titleKeywords = Array.from(new Set([
|
|
1513
|
-
...extractLookupTitleKeywords(contact.jobTitle),
|
|
1514
|
-
...buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole)
|
|
1515
|
-
])).slice(0, 6);
|
|
1516
|
-
const rankedCompanyCandidates = Array.from(companyCandidateScores.entries())
|
|
1517
|
-
.sort((left, right) => right[1] - left[1] || left[0].length - right[0].length)
|
|
1518
|
-
.slice(0, 6);
|
|
1519
|
-
const emailHostCandidate = (() => {
|
|
1520
|
-
if (!emailDomain) {
|
|
1521
|
-
return "";
|
|
1522
|
-
}
|
|
1523
|
-
return normalizeLookupWhitespace(emailDomain.replace(/^www\./i, "").split(".")[0] ?? "").replace(/[-_]+/g, " ");
|
|
1524
|
-
})();
|
|
1525
|
-
const cleanCompanyCandidate = normalizeLookupWhitespace(contact.companyName) ||
|
|
1526
|
-
normalizeLookupWhitespace(contact.companyNameOriginal) ||
|
|
1527
|
-
"";
|
|
1528
|
-
const linkedInHandleCandidate = linkedInHandle && !/^\d+$/.test(linkedInHandle)
|
|
1529
|
-
? normalizeLookupWhitespace(linkedInHandle.replace(/[-_]+/g, " "))
|
|
1530
|
-
: "";
|
|
1531
|
-
const pushVariant = (companyName, searchMode) => {
|
|
1532
|
-
const normalizedCompany = normalizeLookupWhitespace(companyName);
|
|
1533
|
-
if (!normalizedCompany) {
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
const keywordsText = searchMode === "keywords_title" && titleKeywords.length > 0
|
|
1537
|
-
? `${normalizedCompany} ${titleKeywords.join(" ")}`
|
|
1538
|
-
: undefined;
|
|
1539
|
-
if (searchMode === "keywords_title" && !keywordsText) {
|
|
1540
|
-
return;
|
|
1541
|
-
}
|
|
1542
|
-
const key = [
|
|
1543
|
-
contact.firstName.trim().toLowerCase(),
|
|
1544
|
-
contact.lastName.trim().toLowerCase(),
|
|
1545
|
-
normalizedCompany.toLowerCase(),
|
|
1546
|
-
searchMode,
|
|
1547
|
-
keywordsText?.toLowerCase() ?? ""
|
|
1548
|
-
].join("|");
|
|
1549
|
-
if (seen.has(key)) {
|
|
1550
|
-
return;
|
|
1228
|
+
const companyCandidates = [
|
|
1229
|
+
normalizeLookupWhitespace(contact.companyName),
|
|
1230
|
+
normalizeLookupWhitespace(contact.companyNameOriginal)
|
|
1231
|
+
].filter(Boolean);
|
|
1232
|
+
for (const companyName of companyCandidates) {
|
|
1233
|
+
for (const searchMode of ["current_company", "keywords"]) {
|
|
1234
|
+
const key = [
|
|
1235
|
+
contact.firstName.trim().toLowerCase(),
|
|
1236
|
+
contact.lastName.trim().toLowerCase(),
|
|
1237
|
+
companyName.toLowerCase(),
|
|
1238
|
+
searchMode
|
|
1239
|
+
].join("|");
|
|
1240
|
+
if (seen.has(key)) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
seen.add(key);
|
|
1244
|
+
variants.push({
|
|
1245
|
+
firstName: contact.firstName,
|
|
1246
|
+
lastName: contact.lastName,
|
|
1247
|
+
companyName,
|
|
1248
|
+
searchMode
|
|
1249
|
+
});
|
|
1551
1250
|
}
|
|
1552
|
-
seen.add(key);
|
|
1553
|
-
variants.push({
|
|
1554
|
-
firstName: contact.firstName,
|
|
1555
|
-
lastName: contact.lastName,
|
|
1556
|
-
companyName: normalizedCompany,
|
|
1557
|
-
searchMode,
|
|
1558
|
-
keywordsText
|
|
1559
|
-
});
|
|
1560
|
-
};
|
|
1561
|
-
const rankedCompanyNames = rankedCompanyCandidates.map(([companyName]) => companyName);
|
|
1562
|
-
const currentCompanyStageCandidates = [
|
|
1563
|
-
emailHostCandidate,
|
|
1564
|
-
linkedInHandleCandidate,
|
|
1565
|
-
...resolvedCompanyAliases,
|
|
1566
|
-
...rankedCompanyNames.filter((companyName) => (companyCandidateScores.get(companyName) ?? 0) >= 90)
|
|
1567
|
-
];
|
|
1568
|
-
const keywordStageCandidates = [
|
|
1569
|
-
cleanCompanyCandidate,
|
|
1570
|
-
...rankedCompanyNames
|
|
1571
|
-
];
|
|
1572
|
-
const keywordTitleStageCandidates = [
|
|
1573
|
-
cleanCompanyCandidate,
|
|
1574
|
-
...rankedCompanyNames
|
|
1575
|
-
];
|
|
1576
|
-
const fallbackCurrentCompanyCandidates = [
|
|
1577
|
-
cleanCompanyCandidate,
|
|
1578
|
-
normalizeLookupWhitespace(contact.companyNameOriginal),
|
|
1579
|
-
...rankedCompanyNames
|
|
1580
|
-
];
|
|
1581
|
-
for (const companyName of currentCompanyStageCandidates) {
|
|
1582
|
-
pushVariant(companyName, "current_company");
|
|
1583
|
-
}
|
|
1584
|
-
for (const companyName of keywordStageCandidates) {
|
|
1585
|
-
pushVariant(companyName, "keywords");
|
|
1586
|
-
}
|
|
1587
|
-
for (const companyName of keywordTitleStageCandidates) {
|
|
1588
|
-
pushVariant(companyName, "keywords_title");
|
|
1589
|
-
}
|
|
1590
|
-
for (const companyName of fallbackCurrentCompanyCandidates) {
|
|
1591
|
-
pushVariant(companyName, "current_company");
|
|
1592
|
-
}
|
|
1593
|
-
for (const [companyName] of rankedCompanyCandidates) {
|
|
1594
|
-
pushVariant(companyName, "current_company");
|
|
1595
|
-
pushVariant(companyName, "keywords");
|
|
1596
|
-
pushVariant(companyName, "keywords_title");
|
|
1597
1251
|
}
|
|
1598
|
-
return variants
|
|
1252
|
+
return variants;
|
|
1599
1253
|
}
|
|
1600
1254
|
function normalizeSalesNavLeadUrl(value) {
|
|
1601
1255
|
const trimmed = String(value ?? "").trim();
|
|
@@ -1617,21 +1271,14 @@ function normalizePublicLinkedInProfileUrl(value) {
|
|
|
1617
1271
|
if (!trimmed) {
|
|
1618
1272
|
return null;
|
|
1619
1273
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
parsed = new URL(trimmed);
|
|
1623
|
-
}
|
|
1624
|
-
catch {
|
|
1625
|
-
return null;
|
|
1626
|
-
}
|
|
1627
|
-
if (!/(^|\.)linkedin\.com$/i.test(parsed.hostname)) {
|
|
1274
|
+
const publicMatch = trimmed.match(/https:\/\/www\.linkedin\.com\/in\/[^/?#]+\/?/i);
|
|
1275
|
+
if (!publicMatch) {
|
|
1628
1276
|
return null;
|
|
1629
1277
|
}
|
|
1630
|
-
const
|
|
1631
|
-
if (!
|
|
1278
|
+
const candidate = publicMatch[0] ?? null;
|
|
1279
|
+
if (!candidate) {
|
|
1632
1280
|
return null;
|
|
1633
1281
|
}
|
|
1634
|
-
const candidate = `https://www.linkedin.com/in/${pathMatch[1]}`;
|
|
1635
1282
|
return normalizeSalesNavLeadUrl(candidate) ? null : candidate;
|
|
1636
1283
|
}
|
|
1637
1284
|
function extractLinkedInProfileUrlFromSalesApiElement(element) {
|
|
@@ -1774,112 +1421,6 @@ function extractLinkedInCompanyNameFromSalesApiElement(element) {
|
|
|
1774
1421
|
}
|
|
1775
1422
|
return null;
|
|
1776
1423
|
}
|
|
1777
|
-
function extractLinkedInFullNameFromSalesApiElement(element) {
|
|
1778
|
-
if (!element) {
|
|
1779
|
-
return null;
|
|
1780
|
-
}
|
|
1781
|
-
const directCandidates = [
|
|
1782
|
-
typeof element.fullName === "string" ? element.fullName : null,
|
|
1783
|
-
typeof element.name === "string" ? element.name : null
|
|
1784
|
-
].filter(Boolean);
|
|
1785
|
-
for (const candidate of directCandidates) {
|
|
1786
|
-
const normalized = normalizeLookupWhitespace(candidate);
|
|
1787
|
-
if (normalized) {
|
|
1788
|
-
return normalized;
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
const firstName = typeof element.firstName === "string" ? normalizeLookupWhitespace(element.firstName) : "";
|
|
1792
|
-
const lastName = typeof element.lastName === "string" ? normalizeLookupWhitespace(element.lastName) : "";
|
|
1793
|
-
const combined = normalizeLookupWhitespace(`${firstName} ${lastName}`);
|
|
1794
|
-
return combined || null;
|
|
1795
|
-
}
|
|
1796
|
-
function extractLinkedInTitleFromSalesApiElement(element) {
|
|
1797
|
-
if (!element) {
|
|
1798
|
-
return null;
|
|
1799
|
-
}
|
|
1800
|
-
const directCandidates = [
|
|
1801
|
-
typeof element.title === "string" ? element.title : null,
|
|
1802
|
-
typeof element.occupation === "string" ? element.occupation : null
|
|
1803
|
-
].filter(Boolean);
|
|
1804
|
-
for (const candidate of directCandidates) {
|
|
1805
|
-
const normalized = normalizeLookupWhitespace(candidate);
|
|
1806
|
-
if (normalized) {
|
|
1807
|
-
return normalized;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
const currentPosition = Array.isArray(element.currentPositions) && element.currentPositions.length > 0
|
|
1811
|
-
? element.currentPositions[0]
|
|
1812
|
-
: null;
|
|
1813
|
-
const currentTitle = currentPosition && typeof currentPosition.title === "string"
|
|
1814
|
-
? normalizeLookupWhitespace(currentPosition.title)
|
|
1815
|
-
: "";
|
|
1816
|
-
return currentTitle || null;
|
|
1817
|
-
}
|
|
1818
|
-
function scoreLinkedInSalesApiElementMatch(contact, element) {
|
|
1819
|
-
const fullName = extractLinkedInFullNameFromSalesApiElement(element);
|
|
1820
|
-
const companyName = extractLinkedInCompanyNameFromSalesApiElement(Array.isArray(element?.currentPositions) && element.currentPositions.length > 0
|
|
1821
|
-
? element.currentPositions[0]
|
|
1822
|
-
: element) ?? extractLinkedInCompanyNameFromSalesApiElement(element);
|
|
1823
|
-
const title = extractLinkedInTitleFromSalesApiElement(element);
|
|
1824
|
-
const expectedFullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
1825
|
-
const candidateFullName = normalizeLooseMatchText(fullName);
|
|
1826
|
-
const expectedCompanies = Array.from(new Set([
|
|
1827
|
-
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
1828
|
-
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName)),
|
|
1829
|
-
normalizeLooseMatchText(normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? ""),
|
|
1830
|
-
normalizeLooseMatchText((() => {
|
|
1831
|
-
const email = normalizeLookupWhitespace(contact.email);
|
|
1832
|
-
if (!email || isSyntheticLinkedInLookupEmail(email)) {
|
|
1833
|
-
return "";
|
|
1834
|
-
}
|
|
1835
|
-
return email.split("@")[1]?.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
1836
|
-
})())
|
|
1837
|
-
].filter(Boolean)));
|
|
1838
|
-
const candidateCompany = normalizeLooseMatchText(companyName);
|
|
1839
|
-
const candidateTitle = normalizeLooseMatchText(title);
|
|
1840
|
-
let score = 0;
|
|
1841
|
-
let exactNameMatch = false;
|
|
1842
|
-
let companyMatchCount = 0;
|
|
1843
|
-
if (expectedFullName && candidateFullName === expectedFullName) {
|
|
1844
|
-
score += 120;
|
|
1845
|
-
exactNameMatch = true;
|
|
1846
|
-
}
|
|
1847
|
-
else if (expectedFullName &&
|
|
1848
|
-
candidateFullName.includes(normalizeLooseMatchText(contact.firstName)) &&
|
|
1849
|
-
candidateFullName.includes(normalizeLooseMatchText(contact.lastName))) {
|
|
1850
|
-
score += 90;
|
|
1851
|
-
}
|
|
1852
|
-
for (const companyHint of expectedCompanies) {
|
|
1853
|
-
if (!companyHint) {
|
|
1854
|
-
continue;
|
|
1855
|
-
}
|
|
1856
|
-
if (candidateCompany === companyHint) {
|
|
1857
|
-
score += 40;
|
|
1858
|
-
companyMatchCount += 1;
|
|
1859
|
-
}
|
|
1860
|
-
else if (candidateCompany.includes(companyHint) || companyHint.includes(candidateCompany)) {
|
|
1861
|
-
score += 25;
|
|
1862
|
-
companyMatchCount += 1;
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
const titleHints = [
|
|
1866
|
-
...extractLookupTitleKeywords(contact.jobTitle),
|
|
1867
|
-
...buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole)
|
|
1868
|
-
].slice(0, 6);
|
|
1869
|
-
for (const hint of titleHints) {
|
|
1870
|
-
if (hint && candidateTitle.includes(normalizeLooseMatchText(hint))) {
|
|
1871
|
-
score += 6;
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
return {
|
|
1875
|
-
score,
|
|
1876
|
-
fullName,
|
|
1877
|
-
companyName,
|
|
1878
|
-
title,
|
|
1879
|
-
exactNameMatch,
|
|
1880
|
-
companyMatchCount
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
1424
|
function extractLinkedInCompanyEmployeeCountFromSalesApiElement(element) {
|
|
1884
1425
|
if (!element) {
|
|
1885
1426
|
return null;
|
|
@@ -1928,111 +1469,6 @@ function buildLinkedInCompanyLookupVariants(params) {
|
|
|
1928
1469
|
}
|
|
1929
1470
|
return variants;
|
|
1930
1471
|
}
|
|
1931
|
-
function buildDirectCompanyContextKey(contact) {
|
|
1932
|
-
return normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName);
|
|
1933
|
-
}
|
|
1934
|
-
async function resolveDirectLinkedInCompanyContexts(params) {
|
|
1935
|
-
const perCompanyBudgetMs = Math.min(params.timeoutMs, 10_000);
|
|
1936
|
-
const primaryByCompany = new Map();
|
|
1937
|
-
for (const contact of params.contacts) {
|
|
1938
|
-
const key = buildDirectCompanyContextKey(contact);
|
|
1939
|
-
if (!key || primaryByCompany.has(key)) {
|
|
1940
|
-
continue;
|
|
1941
|
-
}
|
|
1942
|
-
primaryByCompany.set(key, contact);
|
|
1943
|
-
}
|
|
1944
|
-
const contexts = new Map();
|
|
1945
|
-
for (const [companyKey, contact] of primaryByCompany.entries()) {
|
|
1946
|
-
const aliases = new Set();
|
|
1947
|
-
const addAlias = (value) => {
|
|
1948
|
-
const normalized = normalizeLookupWhitespace(value);
|
|
1949
|
-
if (!normalized) {
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
aliases.add(normalized);
|
|
1953
|
-
};
|
|
1954
|
-
addAlias(contact.companyNameOriginal);
|
|
1955
|
-
addAlias(contact.companyName);
|
|
1956
|
-
const existingHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "");
|
|
1957
|
-
if (existingHandle && !/^\d+$/.test(existingHandle)) {
|
|
1958
|
-
addAlias(existingHandle.replace(/[-_]+/g, " "));
|
|
1959
|
-
}
|
|
1960
|
-
let matchedCompanyUrl = contact.linkedinCompanyUrl ?? null;
|
|
1961
|
-
let matchedSalesNavCompanyUrl = null;
|
|
1962
|
-
let matchedCompanyName = null;
|
|
1963
|
-
let matchedCompanyEmployeeCount = null;
|
|
1964
|
-
const companyDeadline = Date.now() + perCompanyBudgetMs;
|
|
1965
|
-
const variants = buildLinkedInCompanyLookupVariants({
|
|
1966
|
-
contactId: contact.contact_id,
|
|
1967
|
-
companyName: contact.companyName,
|
|
1968
|
-
companyNameOriginal: contact.companyNameOriginal
|
|
1969
|
-
}).slice(0, 4);
|
|
1970
|
-
for (const variant of variants) {
|
|
1971
|
-
if (Date.now() >= companyDeadline) {
|
|
1972
|
-
break;
|
|
1973
|
-
}
|
|
1974
|
-
const controller = new AbortController();
|
|
1975
|
-
const timeout = setTimeout(controller.abort.bind(controller), Math.min(6_000, Math.max(1_000, companyDeadline - Date.now())));
|
|
1976
|
-
try {
|
|
1977
|
-
const response = await fetch(buildLinkedInAccountSearchApiUrl(variant.companyName), {
|
|
1978
|
-
method: "GET",
|
|
1979
|
-
signal: controller.signal,
|
|
1980
|
-
headers: {
|
|
1981
|
-
accept: "*/*",
|
|
1982
|
-
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
1983
|
-
"csrf-token": params.config.csrfToken,
|
|
1984
|
-
referer: "https://www.linkedin.com/sales/search/company",
|
|
1985
|
-
"sec-fetch-dest": "empty",
|
|
1986
|
-
"sec-fetch-mode": "cors",
|
|
1987
|
-
"sec-fetch-site": "same-origin",
|
|
1988
|
-
"user-agent": params.config.userAgent,
|
|
1989
|
-
"x-li-identity": params.config.identity,
|
|
1990
|
-
"x-li-lang": "en_US",
|
|
1991
|
-
"x-li-page-instance": "urn:li:page:d_sales2_search_accounts;13Jvve6kRGCao+iP0wwAag==",
|
|
1992
|
-
"x-restli-protocol-version": "2.0.0",
|
|
1993
|
-
cookie: params.config.cookie
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
1996
|
-
if (!response.ok) {
|
|
1997
|
-
if (response.status === 429) {
|
|
1998
|
-
break;
|
|
1999
|
-
}
|
|
2000
|
-
continue;
|
|
2001
|
-
}
|
|
2002
|
-
const data = (await response.json());
|
|
2003
|
-
const first = data.elements?.[0];
|
|
2004
|
-
const companyUrl = extractLinkedInCompanyUrlFromSalesApiElement(first);
|
|
2005
|
-
const salesNavCompanyUrl = extractLinkedInSalesNavCompanyUrlFromSalesApiElement(first);
|
|
2006
|
-
const companyName = extractLinkedInCompanyNameFromSalesApiElement(first);
|
|
2007
|
-
if (companyUrl || salesNavCompanyUrl || companyName) {
|
|
2008
|
-
matchedCompanyUrl = companyUrl ?? matchedCompanyUrl;
|
|
2009
|
-
matchedSalesNavCompanyUrl = salesNavCompanyUrl ?? matchedSalesNavCompanyUrl;
|
|
2010
|
-
matchedCompanyName = companyName ?? matchedCompanyName;
|
|
2011
|
-
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first);
|
|
2012
|
-
addAlias(companyName);
|
|
2013
|
-
addAlias(companyUrl ? normalizeLinkedInCompanyHandle(companyUrl)?.replace(/[-_]+/g, " ") : null);
|
|
2014
|
-
addAlias(salesNavCompanyUrl ? normalizeLookupWhitespace(salesNavCompanyUrl.split("/sales/company/")[1]?.split(/[/?#]/)[0] ?? "") : null);
|
|
2015
|
-
break;
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
catch {
|
|
2019
|
-
// Try next company variant.
|
|
2020
|
-
}
|
|
2021
|
-
finally {
|
|
2022
|
-
clearTimeout(timeout);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
contexts.set(companyKey, {
|
|
2026
|
-
normalizedCompanyKey: companyKey,
|
|
2027
|
-
aliases: Array.from(aliases),
|
|
2028
|
-
linkedinCompanyUrl: matchedCompanyUrl,
|
|
2029
|
-
salesNavCompanyUrl: matchedSalesNavCompanyUrl,
|
|
2030
|
-
matchedCompanyName,
|
|
2031
|
-
matchedCompanyEmployeeCount
|
|
2032
|
-
});
|
|
2033
|
-
}
|
|
2034
|
-
return contexts;
|
|
2035
|
-
}
|
|
2036
1472
|
function buildPublicLinkedInCompanySearchUrl(companyName) {
|
|
2037
1473
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_COMPANY_SEARCH_BASE_URL?.trim() ||
|
|
2038
1474
|
"https://duckduckgo.com/html/";
|
|
@@ -2096,8 +1532,7 @@ function extractSerperLinkedInCompanyCandidates(payload) {
|
|
|
2096
1532
|
const organic = "organic" in payload && Array.isArray(payload.organic)
|
|
2097
1533
|
? (payload.organic ?? [])
|
|
2098
1534
|
: [];
|
|
2099
|
-
const
|
|
2100
|
-
const candidates = [];
|
|
1535
|
+
const candidates = new Set();
|
|
2101
1536
|
for (const result of organic) {
|
|
2102
1537
|
if (!result || typeof result !== "object") {
|
|
2103
1538
|
continue;
|
|
@@ -2107,685 +1542,60 @@ function extractSerperLinkedInCompanyCandidates(payload) {
|
|
|
2107
1542
|
: "";
|
|
2108
1543
|
const handle = normalizeLinkedInCompanyHandle(link);
|
|
2109
1544
|
if (handle) {
|
|
2110
|
-
|
|
2111
|
-
if (!seen.has(url)) {
|
|
2112
|
-
seen.add(url);
|
|
2113
|
-
candidates.push({
|
|
2114
|
-
url,
|
|
2115
|
-
title: "title" in result && typeof result.title === "string"
|
|
2116
|
-
? normalizeLookupWhitespace(result.title)
|
|
2117
|
-
: "",
|
|
2118
|
-
snippet: "snippet" in result && typeof result.snippet === "string"
|
|
2119
|
-
? normalizeLookupWhitespace(result.snippet)
|
|
2120
|
-
: ""
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
1545
|
+
candidates.add(normalizeLinkedInCompanyPage(handle));
|
|
2123
1546
|
}
|
|
2124
1547
|
}
|
|
2125
|
-
return candidates;
|
|
1548
|
+
return Array.from(candidates);
|
|
2126
1549
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
1550
|
+
function decodeHtmlEntities(value) {
|
|
1551
|
+
return value
|
|
1552
|
+
.replace(/&/gi, "&")
|
|
1553
|
+
.replace(/"/gi, '"')
|
|
1554
|
+
.replace(/'/gi, "'")
|
|
1555
|
+
.replace(/</gi, "<")
|
|
1556
|
+
.replace(/>/gi, ">");
|
|
1557
|
+
}
|
|
1558
|
+
async function fetchLinkedInCompanyPageSignals(url, timeoutMs) {
|
|
1559
|
+
const controller = new AbortController();
|
|
1560
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
1561
|
+
try {
|
|
1562
|
+
const response = await fetch(url, {
|
|
1563
|
+
method: "GET",
|
|
1564
|
+
signal: controller.signal,
|
|
1565
|
+
headers: {
|
|
1566
|
+
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
const html = await response.text();
|
|
1570
|
+
const finalUrl = response.url || url;
|
|
1571
|
+
const title = decodeHtmlEntities(html.match(/<title[^>]*>([^<]+)/i)?.[1] || "") ||
|
|
1572
|
+
decodeHtmlEntities(html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
1573
|
+
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
1574
|
+
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
1575
|
+
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
1576
|
+
const unavailable = response.status >= 400 ||
|
|
1577
|
+
unavailableText.includes("page not found") ||
|
|
1578
|
+
unavailableText.includes("this page does not exist") ||
|
|
1579
|
+
unavailableText.includes("page isnt available");
|
|
1580
|
+
const handle = normalizeLinkedInCompanyHandle(finalUrl) ?? normalizeLinkedInCompanyHandle(url);
|
|
1581
|
+
if (!handle) {
|
|
1582
|
+
return null;
|
|
2142
1583
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
push(normalized);
|
|
2146
|
-
push(normalizeLookupCompanyForSearch(normalized));
|
|
2147
|
-
push(aggressivelyCleanLookupCompanyName(normalized));
|
|
2148
|
-
const titleStripped = normalized
|
|
2149
|
-
.replace(/\|\s*linkedin$/i, "")
|
|
2150
|
-
.replace(/\|\s*overview$/i, "")
|
|
2151
|
-
.replace(/\b(linkedin|home|about|posts|see all details)\b/gi, " ")
|
|
2152
|
-
.replace(/\s+/g, " ")
|
|
2153
|
-
.trim();
|
|
2154
|
-
push(titleStripped);
|
|
2155
|
-
const parts = titleStripped
|
|
2156
|
-
.split(/[|,·•:()/-]+/)
|
|
2157
|
-
.map((part) => normalizeLookupWhitespace(part))
|
|
2158
|
-
.filter(Boolean);
|
|
2159
|
-
for (const part of parts) {
|
|
2160
|
-
push(part);
|
|
2161
|
-
}
|
|
2162
|
-
const looseTokens = normalizeLooseMatchText(titleStripped)
|
|
2163
|
-
.split(/\s+/)
|
|
2164
|
-
.filter((token) => token.length >= 4)
|
|
2165
|
-
.filter((token) => ![
|
|
2166
|
-
"group",
|
|
2167
|
-
"holding",
|
|
2168
|
-
"services",
|
|
2169
|
-
"service",
|
|
2170
|
-
"consulting",
|
|
2171
|
-
"gmbh",
|
|
2172
|
-
"publishing",
|
|
2173
|
-
"company",
|
|
2174
|
-
"linkedin",
|
|
2175
|
-
"deutschland"
|
|
2176
|
-
].includes(token));
|
|
2177
|
-
if (looseTokens.length > 0) {
|
|
2178
|
-
push(looseTokens[0]);
|
|
2179
|
-
push(looseTokens.slice(0, 2).join(" "));
|
|
2180
|
-
push(looseTokens.slice(-2).join(" "));
|
|
2181
|
-
}
|
|
2182
|
-
return Array.from(phrases);
|
|
2183
|
-
}
|
|
2184
|
-
async function buildLinkedInProfileCompanyHints(contact, timeoutMs) {
|
|
2185
|
-
const phrases = new Set();
|
|
2186
|
-
const keywords = new Set();
|
|
2187
|
-
const addPhrase = (value) => {
|
|
2188
|
-
for (const phrase of extractKeywordPhrases(value)) {
|
|
2189
|
-
phrases.add(phrase);
|
|
2190
|
-
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2191
|
-
.split(/\s+/)
|
|
2192
|
-
.filter((token) => token.length >= 4)
|
|
2193
|
-
.filter((token) => ![
|
|
2194
|
-
"group",
|
|
2195
|
-
"holding",
|
|
2196
|
-
"services",
|
|
2197
|
-
"service",
|
|
2198
|
-
"consulting",
|
|
2199
|
-
"gmbh",
|
|
2200
|
-
"publishing",
|
|
2201
|
-
"company",
|
|
2202
|
-
"linkedin",
|
|
2203
|
-
"deutschland"
|
|
2204
|
-
].includes(token));
|
|
2205
|
-
for (const token of looseTokens.slice(0, 5)) {
|
|
2206
|
-
keywords.add(token);
|
|
2207
|
-
}
|
|
2208
|
-
if (looseTokens.length > 1) {
|
|
2209
|
-
keywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2210
|
-
keywords.add(looseTokens.slice(-2).join(" "));
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
};
|
|
2214
|
-
addPhrase(contact.companyNameOriginal ?? contact.companyName);
|
|
2215
|
-
const linkedInHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "");
|
|
2216
|
-
if (linkedInHandle && !/^\d+$/.test(linkedInHandle)) {
|
|
2217
|
-
addPhrase(linkedInHandle.replace(/[-_]+/g, " "));
|
|
2218
|
-
}
|
|
2219
|
-
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2220
|
-
const emailDomain = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail)
|
|
2221
|
-
? normalizedEmail.split("@")[1] ?? ""
|
|
2222
|
-
: "";
|
|
2223
|
-
if (emailDomain) {
|
|
2224
|
-
const normalizedDomain = emailDomain.replace(/^www\./i, "");
|
|
2225
|
-
keywords.add(normalizedDomain);
|
|
2226
|
-
const host = normalizedDomain.split(".")[0] ?? "";
|
|
2227
|
-
if (host) {
|
|
2228
|
-
addPhrase(host.replace(/[-_]+/g, " "));
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
const companyUrl = contact.linkedinCompanyUrl?.trim();
|
|
2232
|
-
if (companyUrl) {
|
|
2233
|
-
const cacheKey = companyUrl.replace(/\/$/, "");
|
|
2234
|
-
let cachedHints = linkedInCompanyHintCache.get(cacheKey);
|
|
2235
|
-
if (!cachedHints) {
|
|
2236
|
-
const signals = await fetchLinkedInCompanyPageSignals(companyUrl, timeoutMs);
|
|
2237
|
-
cachedHints = signals ? [...extractKeywordPhrases(signals.title), ...extractKeywordPhrases(signals.description)] : [];
|
|
2238
|
-
linkedInCompanyHintCache.set(cacheKey, cachedHints);
|
|
2239
|
-
}
|
|
2240
|
-
for (const hint of cachedHints) {
|
|
2241
|
-
addPhrase(hint);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
return {
|
|
2245
|
-
phrases: Array.from(phrases)
|
|
2246
|
-
.map((value) => normalizeLookupWhitespace(value))
|
|
2247
|
-
.filter((value) => value.length > 0),
|
|
2248
|
-
keywords: Array.from(keywords)
|
|
2249
|
-
.map((value) => normalizeLookupWhitespace(value))
|
|
2250
|
-
.filter((value) => value.length > 0)
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
async function buildSerperLinkedInProfileQueries(contact, timeoutMs) {
|
|
2254
|
-
const fullName = normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`);
|
|
2255
|
-
const title = normalizeLookupWhitespace(contact.jobTitle);
|
|
2256
|
-
const queryEntries = [];
|
|
2257
|
-
const seenQueries = new Set();
|
|
2258
|
-
const pushQuery = (query, score) => {
|
|
2259
|
-
const normalized = normalizeLookupWhitespace(query);
|
|
2260
|
-
if (!normalized) {
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
const key = normalized.toLowerCase();
|
|
2264
|
-
if (seenQueries.has(key)) {
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
seenQueries.add(key);
|
|
2268
|
-
queryEntries.push({ query: normalized, score });
|
|
2269
|
-
};
|
|
2270
|
-
const { phrases, keywords } = await buildLinkedInProfileCompanyHints(contact, timeoutMs);
|
|
2271
|
-
const enrichedPhrases = new Set(phrases);
|
|
2272
|
-
const enrichedKeywords = new Set(keywords);
|
|
2273
|
-
const preferredPhrases = [];
|
|
2274
|
-
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2275
|
-
const trustedEmailDomain = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail)
|
|
2276
|
-
? normalizedEmail.split("@")[1]?.replace(/^www\./i, "") ?? ""
|
|
2277
|
-
: "";
|
|
2278
|
-
const emailHost = trustedEmailDomain.split(".")[0] ?? "";
|
|
2279
|
-
const emailDomain = trustedEmailDomain;
|
|
2280
|
-
const linkedInHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? "";
|
|
2281
|
-
if (contact.linkedinCompanyUrl?.trim()) {
|
|
2282
|
-
const companySignals = await fetchLinkedInCompanyPageSignals(contact.linkedinCompanyUrl.trim(), timeoutMs);
|
|
2283
|
-
for (const phrase of [
|
|
2284
|
-
...extractKeywordPhrases(companySignals?.title),
|
|
2285
|
-
...extractKeywordPhrases(companySignals?.description)
|
|
2286
|
-
]) {
|
|
2287
|
-
enrichedPhrases.add(phrase);
|
|
2288
|
-
preferredPhrases.push(phrase);
|
|
2289
|
-
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2290
|
-
.split(/\s+/)
|
|
2291
|
-
.filter((token) => token.length >= 4)
|
|
2292
|
-
.filter((token) => ![
|
|
2293
|
-
"group",
|
|
2294
|
-
"holding",
|
|
2295
|
-
"services",
|
|
2296
|
-
"service",
|
|
2297
|
-
"consulting",
|
|
2298
|
-
"gmbh",
|
|
2299
|
-
"publishing",
|
|
2300
|
-
"company",
|
|
2301
|
-
"linkedin",
|
|
2302
|
-
"deutschland"
|
|
2303
|
-
].includes(token));
|
|
2304
|
-
for (const token of looseTokens.slice(0, 4)) {
|
|
2305
|
-
enrichedKeywords.add(token);
|
|
2306
|
-
}
|
|
2307
|
-
if (looseTokens.length > 1) {
|
|
2308
|
-
enrichedKeywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
const phrasePriority = (value) => {
|
|
2313
|
-
const loose = normalizeLooseMatchText(value);
|
|
2314
|
-
const tokenCount = loose.split(/\s+/).filter(Boolean).length;
|
|
2315
|
-
let score = 0;
|
|
2316
|
-
if (emailHost && loose.includes(normalizeLooseMatchText(emailHost)))
|
|
2317
|
-
score += 80;
|
|
2318
|
-
if (linkedInHandle && loose.includes(normalizeLooseMatchText(linkedInHandle)))
|
|
2319
|
-
score += 60;
|
|
2320
|
-
if (tokenCount >= 1 && tokenCount <= 4)
|
|
2321
|
-
score += 40;
|
|
2322
|
-
if (!/\b(gmbh|holding|services|service|consulting|kg|co)\b/i.test(value))
|
|
2323
|
-
score += 20;
|
|
2324
|
-
if (tokenCount > 7)
|
|
2325
|
-
score -= 40;
|
|
2326
|
-
return score;
|
|
2327
|
-
};
|
|
2328
|
-
const keywordPriority = (value) => {
|
|
2329
|
-
const loose = normalizeLooseMatchText(value);
|
|
2330
|
-
let score = 0;
|
|
2331
|
-
if (emailHost && loose.includes(normalizeLooseMatchText(emailHost)))
|
|
2332
|
-
score += 80;
|
|
2333
|
-
if (linkedInHandle && loose.includes(normalizeLooseMatchText(linkedInHandle)))
|
|
2334
|
-
score += 60;
|
|
2335
|
-
if (value.includes("."))
|
|
2336
|
-
score += 20;
|
|
2337
|
-
if (loose.split(/\s+/).filter(Boolean).length <= 2)
|
|
2338
|
-
score += 10;
|
|
2339
|
-
return score;
|
|
2340
|
-
};
|
|
2341
|
-
const rankedPhrases = [...enrichedPhrases].sort((left, right) => {
|
|
2342
|
-
const preferredDelta = Number(preferredPhrases.includes(right)) - Number(preferredPhrases.includes(left));
|
|
2343
|
-
if (preferredDelta !== 0) {
|
|
2344
|
-
return preferredDelta;
|
|
2345
|
-
}
|
|
2346
|
-
return phrasePriority(right) - phrasePriority(left);
|
|
2347
|
-
});
|
|
2348
|
-
const cleanPhrases = rankedPhrases.slice(0, 6);
|
|
2349
|
-
const fallbackKeywords = new Set(enrichedKeywords);
|
|
2350
|
-
for (const phrase of cleanPhrases) {
|
|
2351
|
-
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2352
|
-
.split(/\s+/)
|
|
2353
|
-
.filter((token) => token.length >= 4)
|
|
2354
|
-
.filter((token) => ![
|
|
2355
|
-
"group",
|
|
2356
|
-
"holding",
|
|
2357
|
-
"services",
|
|
2358
|
-
"service",
|
|
2359
|
-
"consulting",
|
|
2360
|
-
"gmbh",
|
|
2361
|
-
"publishing",
|
|
2362
|
-
"company",
|
|
2363
|
-
"linkedin",
|
|
2364
|
-
"deutschland"
|
|
2365
|
-
].includes(token));
|
|
2366
|
-
for (const token of looseTokens.slice(0, 3)) {
|
|
2367
|
-
fallbackKeywords.add(token);
|
|
2368
|
-
}
|
|
2369
|
-
if (looseTokens.length > 1) {
|
|
2370
|
-
fallbackKeywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
if (emailHost) {
|
|
2374
|
-
fallbackKeywords.add(emailHost);
|
|
2375
|
-
}
|
|
2376
|
-
if (emailDomain) {
|
|
2377
|
-
fallbackKeywords.add(emailDomain);
|
|
2378
|
-
}
|
|
2379
|
-
if (linkedInHandle) {
|
|
2380
|
-
fallbackKeywords.add(linkedInHandle);
|
|
2381
|
-
}
|
|
2382
|
-
const cleanKeywords = [...fallbackKeywords]
|
|
2383
|
-
.sort((left, right) => keywordPriority(right) - keywordPriority(left))
|
|
2384
|
-
.slice(0, 5);
|
|
2385
|
-
cleanKeywords.forEach((keyword, index) => {
|
|
2386
|
-
const keywordScore = 260 - index * 15;
|
|
2387
|
-
pushQuery(`site:linkedin.com/in "${fullName}" ${keyword} linkedin`, keywordScore);
|
|
2388
|
-
pushQuery(`site:linkedin.com/in ${fullName} ${keyword} linkedin`, keywordScore - 5);
|
|
2389
|
-
if (title) {
|
|
2390
|
-
pushQuery(`site:linkedin.com/in "${fullName}" ${keyword} "${title}"`, keywordScore - 10);
|
|
2391
|
-
}
|
|
2392
|
-
});
|
|
2393
|
-
cleanPhrases.forEach((companyName, index) => {
|
|
2394
|
-
const phraseScore = 180 - index * 10;
|
|
2395
|
-
pushQuery(`site:linkedin.com/in "${fullName}" "${companyName}"`, phraseScore);
|
|
2396
|
-
pushQuery(`site:linkedin.com/in ${fullName} ${companyName} linkedin`, phraseScore - 5);
|
|
2397
|
-
if (title) {
|
|
2398
|
-
pushQuery(`site:linkedin.com/in "${fullName}" "${companyName}" "${title}"`, phraseScore - 10);
|
|
2399
|
-
pushQuery(`site:linkedin.com/in ${fullName} ${companyName} ${title} linkedin`, phraseScore - 15);
|
|
2400
|
-
}
|
|
2401
|
-
});
|
|
2402
|
-
if (emailDomain) {
|
|
2403
|
-
pushQuery(`site:linkedin.com/in "${fullName}" "${emailDomain}" linkedin`, 240);
|
|
2404
|
-
}
|
|
2405
|
-
pushQuery(`site:linkedin.com/in "${fullName}" linkedin`, 50);
|
|
2406
|
-
if (title) {
|
|
2407
|
-
pushQuery(`site:linkedin.com/in "${fullName}" "${title}" linkedin`, 40);
|
|
2408
|
-
}
|
|
2409
|
-
return queryEntries
|
|
2410
|
-
.sort((left, right) => right.score - left.score)
|
|
2411
|
-
.map((entry) => entry.query);
|
|
2412
|
-
}
|
|
2413
|
-
function extractPublicLinkedInProfileSearchCandidates(bodyText) {
|
|
2414
|
-
const candidates = new Set();
|
|
2415
|
-
const directMatches = bodyText.match(/https:\/\/(?:(?:www|[a-z]{2})\.)?linkedin\.com\/in\/[^"'&<>\s)]+/gi) ?? [];
|
|
2416
|
-
for (const match of directMatches) {
|
|
2417
|
-
const normalized = normalizePublicLinkedInProfileUrl(match);
|
|
2418
|
-
if (normalized) {
|
|
2419
|
-
candidates.add(normalized);
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
const encodedMatches = bodyText.match(/https?%3A%2F%2F(?:(?:www|[a-z]{2})\.)?linkedin\.com%2Fin%2F[^"'&<>\s)]+/gi) ?? [];
|
|
2423
|
-
for (const match of encodedMatches) {
|
|
2424
|
-
try {
|
|
2425
|
-
const decoded = decodeURIComponent(match);
|
|
2426
|
-
const normalized = normalizePublicLinkedInProfileUrl(decoded);
|
|
2427
|
-
if (normalized) {
|
|
2428
|
-
candidates.add(normalized);
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
|
-
catch {
|
|
2432
|
-
// Ignore malformed encoded fragments.
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
return Array.from(candidates);
|
|
2436
|
-
}
|
|
2437
|
-
function buildPublicLinkedInProfileSearchUrl(query) {
|
|
2438
|
-
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_PROFILE_SEARCH_BASE_URL?.trim() ||
|
|
2439
|
-
"https://duckduckgo.com/html/";
|
|
2440
|
-
const url = new URL(baseUrl);
|
|
2441
|
-
url.searchParams.set("q", query);
|
|
2442
|
-
return url.toString();
|
|
2443
|
-
}
|
|
2444
|
-
async function fetchSerperSearchResults(query, num, timeoutMs) {
|
|
2445
|
-
if (serperCreditsExhausted) {
|
|
2446
|
-
return null;
|
|
2447
|
-
}
|
|
2448
|
-
const apiKey = getSerperApiKey();
|
|
2449
|
-
if (!apiKey) {
|
|
2450
|
-
return null;
|
|
2451
|
-
}
|
|
2452
|
-
const cacheKey = `${query}::${num}`;
|
|
2453
|
-
if (serperSearchCache.has(cacheKey)) {
|
|
2454
|
-
return serperSearchCache.get(cacheKey) ?? null;
|
|
2455
|
-
}
|
|
2456
|
-
const controller = new AbortController();
|
|
2457
|
-
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2458
|
-
try {
|
|
2459
|
-
const response = await fetch(getSerperSearchEndpoint(), {
|
|
2460
|
-
method: "POST",
|
|
2461
|
-
signal: controller.signal,
|
|
2462
|
-
headers: {
|
|
2463
|
-
"X-API-KEY": apiKey,
|
|
2464
|
-
"Content-Type": "application/json"
|
|
2465
|
-
},
|
|
2466
|
-
body: JSON.stringify({ q: query, num })
|
|
2467
|
-
});
|
|
2468
|
-
if (!response.ok) {
|
|
2469
|
-
const bodyText = await response.text().catch(() => "");
|
|
2470
|
-
if (response.status === 400 &&
|
|
2471
|
-
/not enough credits/i.test(bodyText)) {
|
|
2472
|
-
serperCreditsExhausted = true;
|
|
2473
|
-
}
|
|
2474
|
-
serperSearchCache.set(cacheKey, null);
|
|
2475
|
-
return null;
|
|
2476
|
-
}
|
|
2477
|
-
const parsed = await response.json();
|
|
2478
|
-
serperSearchCache.set(cacheKey, parsed);
|
|
2479
|
-
return parsed;
|
|
2480
|
-
}
|
|
2481
|
-
catch {
|
|
2482
|
-
return null;
|
|
2483
|
-
}
|
|
2484
|
-
finally {
|
|
2485
|
-
clearTimeout(timeout);
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
function extractSerperLinkedInProfileCandidates(payload) {
|
|
2489
|
-
if (!payload || typeof payload !== "object") {
|
|
2490
|
-
return [];
|
|
2491
|
-
}
|
|
2492
|
-
const organic = "organic" in payload && Array.isArray(payload.organic)
|
|
2493
|
-
? (payload.organic ?? [])
|
|
2494
|
-
: [];
|
|
2495
|
-
const seen = new Set();
|
|
2496
|
-
const candidates = [];
|
|
2497
|
-
for (const result of organic) {
|
|
2498
|
-
if (!result || typeof result !== "object")
|
|
2499
|
-
continue;
|
|
2500
|
-
const link = "link" in result && typeof result.link === "string"
|
|
2501
|
-
? result.link
|
|
2502
|
-
: "";
|
|
2503
|
-
const normalized = normalizePublicLinkedInProfileUrl(link);
|
|
2504
|
-
if (normalized) {
|
|
2505
|
-
const canonical = normalized.replace(/\/$/, "");
|
|
2506
|
-
if (!seen.has(canonical)) {
|
|
2507
|
-
seen.add(canonical);
|
|
2508
|
-
candidates.push({
|
|
2509
|
-
url: canonical,
|
|
2510
|
-
title: "title" in result && typeof result.title === "string"
|
|
2511
|
-
? normalizeLookupWhitespace(result.title)
|
|
2512
|
-
: "",
|
|
2513
|
-
snippet: "snippet" in result && typeof result.snippet === "string"
|
|
2514
|
-
? normalizeLookupWhitespace(result.snippet)
|
|
2515
|
-
: ""
|
|
2516
|
-
});
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
return candidates;
|
|
2521
|
-
}
|
|
2522
|
-
async function fetchLinkedInProfilePageSignals(url, timeoutMs) {
|
|
2523
|
-
const cacheKey = normalizePublicLinkedInProfileUrl(url)?.replace(/\/$/, "") ?? url.replace(/\/$/, "");
|
|
2524
|
-
if (linkedInProfilePageSignalCache.has(cacheKey)) {
|
|
2525
|
-
return linkedInProfilePageSignalCache.get(cacheKey) ?? null;
|
|
2526
|
-
}
|
|
2527
|
-
const controller = new AbortController();
|
|
2528
|
-
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2529
|
-
try {
|
|
2530
|
-
const targetUrl = rewriteLinkedInUrlForConfiguredBase(url);
|
|
2531
|
-
const response = await fetch(targetUrl, {
|
|
2532
|
-
method: "GET",
|
|
2533
|
-
signal: controller.signal,
|
|
2534
|
-
headers: {
|
|
2535
|
-
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
2536
|
-
}
|
|
2537
|
-
});
|
|
2538
|
-
const html = await response.text();
|
|
2539
|
-
const finalUrl = normalizePublicLinkedInProfileUrl(url) ||
|
|
2540
|
-
normalizePublicLinkedInProfileUrl(response.url || url);
|
|
2541
|
-
if (!finalUrl) {
|
|
2542
|
-
return null;
|
|
2543
|
-
}
|
|
2544
|
-
const title = decodeHtmlEntities(html.match(/<title[^>]*>([^<]+)/i)?.[1] || "") ||
|
|
2545
|
-
decodeHtmlEntities(html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2546
|
-
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2547
|
-
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
2548
|
-
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
2549
|
-
const unavailable = response.status >= 400 ||
|
|
2550
|
-
unavailableText.includes("page not found") ||
|
|
2551
|
-
unavailableText.includes("profile not found") ||
|
|
2552
|
-
unavailableText.includes("member profile") && unavailableText.includes("not available");
|
|
2553
|
-
const result = {
|
|
2554
|
-
normalizedUrl: finalUrl.replace(/\/$/, ""),
|
|
2555
|
-
title: normalizeLookupWhitespace(title),
|
|
2556
|
-
description: normalizeLookupWhitespace(description),
|
|
2557
|
-
bodyText: normalizeLookupWhitespace(bodyText),
|
|
2558
|
-
unavailable
|
|
2559
|
-
};
|
|
2560
|
-
linkedInProfilePageSignalCache.set(cacheKey, result);
|
|
2561
|
-
return result;
|
|
2562
|
-
}
|
|
2563
|
-
catch {
|
|
2564
|
-
linkedInProfilePageSignalCache.set(cacheKey, null);
|
|
2565
|
-
return null;
|
|
2566
|
-
}
|
|
2567
|
-
finally {
|
|
2568
|
-
clearTimeout(timeout);
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
function scoreLinkedInProfilePageSignals(contact, signals) {
|
|
2572
|
-
const fullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
2573
|
-
const companyHints = [
|
|
2574
|
-
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
2575
|
-
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName))
|
|
2576
|
-
].filter(Boolean);
|
|
2577
|
-
const titleHint = normalizeLooseMatchText(contact.jobTitle);
|
|
2578
|
-
const haystack = normalizeLooseMatchText(`${signals.title} ${signals.description} ${signals.bodyText}`);
|
|
2579
|
-
let score = 0;
|
|
2580
|
-
if (fullName && haystack.includes(fullName))
|
|
2581
|
-
score += 120;
|
|
2582
|
-
for (const hint of companyHints) {
|
|
2583
|
-
if (hint && haystack.includes(hint))
|
|
2584
|
-
score += 30;
|
|
2585
|
-
}
|
|
2586
|
-
if (titleHint) {
|
|
2587
|
-
const titleWords = titleHint.split(/\s+/).filter((token) => token.length >= 4).slice(0, 4);
|
|
2588
|
-
score += titleWords.filter((token) => haystack.includes(token)).length * 8;
|
|
2589
|
-
}
|
|
2590
|
-
const slug = signals.normalizedUrl.split("/in/")[1]?.replace(/\/$/, "") ?? "";
|
|
2591
|
-
const slugText = normalizeLooseMatchText(slug.replace(/[-_]+/g, " "));
|
|
2592
|
-
if (fullName && slugText.includes(contact.firstName.toLowerCase()) && slugText.includes(contact.lastName.toLowerCase())) {
|
|
2593
|
-
score += 40;
|
|
2594
|
-
}
|
|
2595
|
-
return score;
|
|
2596
|
-
}
|
|
2597
|
-
function analyzeSerperLinkedInProfileCandidate(contact, candidate) {
|
|
2598
|
-
const fullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
2599
|
-
const titleHint = normalizeLooseMatchText(contact.jobTitle);
|
|
2600
|
-
const companyTokens = [
|
|
2601
|
-
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
2602
|
-
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName)),
|
|
2603
|
-
normalizeLooseMatchText(normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? ""),
|
|
2604
|
-
normalizeLooseMatchText((() => {
|
|
2605
|
-
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2606
|
-
if (!normalizedEmail || isSyntheticLinkedInLookupEmail(normalizedEmail)) {
|
|
2607
|
-
return "";
|
|
2608
|
-
}
|
|
2609
|
-
return normalizedEmail.split("@")[1]?.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
2610
|
-
})())
|
|
2611
|
-
].filter(Boolean);
|
|
2612
|
-
const haystack = normalizeLooseMatchText(`${candidate.title} ${candidate.snippet}`);
|
|
2613
|
-
let score = 0;
|
|
2614
|
-
let companyMatches = 0;
|
|
2615
|
-
let titleMatches = 0;
|
|
2616
|
-
if (fullName && haystack.includes(fullName))
|
|
2617
|
-
score += 120;
|
|
2618
|
-
for (const token of companyTokens) {
|
|
2619
|
-
if (!token)
|
|
2620
|
-
continue;
|
|
2621
|
-
if (haystack.includes(token)) {
|
|
2622
|
-
companyMatches += 1;
|
|
2623
|
-
score += token.split(/\s+/).length <= 2 ? 30 : 20;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
if (titleHint) {
|
|
2627
|
-
const titleWords = titleHint.split(/\s+/).filter((token) => token.length >= 4).slice(0, 4);
|
|
2628
|
-
titleMatches = titleWords.filter((token) => haystack.includes(token)).length;
|
|
2629
|
-
score += titleMatches * 8;
|
|
2630
|
-
}
|
|
2631
|
-
const slugText = normalizeLooseMatchText(candidate.url.split("/in/")[1]?.replace(/\/$/, "").replace(/[-_]+/g, " ") ?? "");
|
|
2632
|
-
if (fullName &&
|
|
2633
|
-
slugText.includes(contact.firstName.toLowerCase()) &&
|
|
2634
|
-
slugText.includes(contact.lastName.toLowerCase()) &&
|
|
2635
|
-
(companyMatches > 0 || titleMatches > 0)) {
|
|
2636
|
-
score += 40;
|
|
2637
|
-
}
|
|
2638
|
-
return { score, companyMatches, titleMatches };
|
|
2639
|
-
}
|
|
2640
|
-
async function searchSerperLinkedInProfileUrl(contact, timeoutMs, options) {
|
|
2641
|
-
if (!contact.firstName || !contact.lastName) {
|
|
2642
|
-
return null;
|
|
2643
|
-
}
|
|
2644
|
-
const maxQueries = options?.maxQueries && Number.isFinite(options.maxQueries) && options.maxQueries > 0
|
|
2645
|
-
? Math.trunc(options.maxQueries)
|
|
2646
|
-
: Number.POSITIVE_INFINITY;
|
|
2647
|
-
for (const query of (await buildSerperLinkedInProfileQueries(contact, timeoutMs)).slice(0, maxQueries)) {
|
|
2648
|
-
try {
|
|
2649
|
-
const parsed = await fetchSerperSearchResults(query, 5, timeoutMs);
|
|
2650
|
-
if (!parsed) {
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
const candidates = extractSerperLinkedInProfileCandidates(parsed);
|
|
2654
|
-
let bestUrl = null;
|
|
2655
|
-
let bestScore = 0;
|
|
2656
|
-
for (const candidate of candidates) {
|
|
2657
|
-
const serperAnalysis = analyzeSerperLinkedInProfileCandidate(contact, candidate);
|
|
2658
|
-
const serperScore = serperAnalysis.score;
|
|
2659
|
-
if (serperScore >= 150 && (serperAnalysis.companyMatches > 0 || serperAnalysis.titleMatches > 0)) {
|
|
2660
|
-
return candidate.url;
|
|
2661
|
-
}
|
|
2662
|
-
const signals = await fetchLinkedInProfilePageSignals(candidate.url, timeoutMs);
|
|
2663
|
-
if (!signals || signals.unavailable) {
|
|
2664
|
-
if (serperScore > bestScore) {
|
|
2665
|
-
bestScore = serperScore;
|
|
2666
|
-
bestUrl = candidate.url;
|
|
2667
|
-
}
|
|
2668
|
-
continue;
|
|
2669
|
-
}
|
|
2670
|
-
const score = Math.max(serperScore, scoreLinkedInProfilePageSignals(contact, signals));
|
|
2671
|
-
if (score > bestScore) {
|
|
2672
|
-
bestScore = score;
|
|
2673
|
-
bestUrl = signals.normalizedUrl;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
if (bestUrl && bestScore >= 130) {
|
|
2677
|
-
return bestUrl;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
catch {
|
|
2681
|
-
// Continue with the next query variant.
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
return searchPublicLinkedInProfileUrl(contact, timeoutMs, {
|
|
2685
|
-
maxQueries: Math.min(Number.isFinite(maxQueries) ? maxQueries : 4, 4)
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
function decodeHtmlEntities(value) {
|
|
2689
|
-
return value
|
|
2690
|
-
.replace(/&/gi, "&")
|
|
2691
|
-
.replace(/"/gi, '"')
|
|
2692
|
-
.replace(/'/gi, "'")
|
|
2693
|
-
.replace(/</gi, "<")
|
|
2694
|
-
.replace(/>/gi, ">");
|
|
2695
|
-
}
|
|
2696
|
-
async function fetchLinkedInCompanyPageSignals(url, timeoutMs) {
|
|
2697
|
-
const cacheKey = url.replace(/\/$/, "");
|
|
2698
|
-
if (linkedInCompanyPageSignalCache.has(cacheKey)) {
|
|
2699
|
-
return linkedInCompanyPageSignalCache.get(cacheKey) ?? null;
|
|
2700
|
-
}
|
|
2701
|
-
const controller = new AbortController();
|
|
2702
|
-
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2703
|
-
try {
|
|
2704
|
-
const response = await fetch(url, {
|
|
2705
|
-
method: "GET",
|
|
2706
|
-
signal: controller.signal,
|
|
2707
|
-
headers: {
|
|
2708
|
-
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
2709
|
-
}
|
|
2710
|
-
});
|
|
2711
|
-
const html = await response.text();
|
|
2712
|
-
const finalUrl = response.url || url;
|
|
2713
|
-
const title = decodeHtmlEntities(html.match(/<title[^>]*>([^<]+)/i)?.[1] || "") ||
|
|
2714
|
-
decodeHtmlEntities(html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2715
|
-
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2716
|
-
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
2717
|
-
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
2718
|
-
const unavailable = response.status >= 400 ||
|
|
2719
|
-
unavailableText.includes("page not found") ||
|
|
2720
|
-
unavailableText.includes("this page does not exist") ||
|
|
2721
|
-
unavailableText.includes("page isnt available");
|
|
2722
|
-
const result = {
|
|
2723
|
-
normalizedUrl: normalizeLinkedInCompanyHandle(finalUrl ?? "") || normalizeLinkedInCompanyHandle(url)
|
|
2724
|
-
? normalizeLinkedInCompanyPage(normalizeLinkedInCompanyHandle(finalUrl ?? "") ?? normalizeLinkedInCompanyHandle(url) ?? "")
|
|
2725
|
-
: finalUrl,
|
|
1584
|
+
return {
|
|
1585
|
+
normalizedUrl: normalizeLinkedInCompanyPage(handle),
|
|
2726
1586
|
title: normalizeLookupWhitespace(title),
|
|
2727
1587
|
description: normalizeLookupWhitespace(description),
|
|
2728
1588
|
bodyText: normalizeLookupWhitespace(bodyText),
|
|
2729
1589
|
unavailable
|
|
2730
1590
|
};
|
|
2731
|
-
linkedInCompanyPageSignalCache.set(cacheKey, result);
|
|
2732
|
-
return result;
|
|
2733
1591
|
}
|
|
2734
1592
|
catch {
|
|
2735
|
-
linkedInCompanyPageSignalCache.set(cacheKey, null);
|
|
2736
1593
|
return null;
|
|
2737
1594
|
}
|
|
2738
1595
|
finally {
|
|
2739
1596
|
clearTimeout(timeout);
|
|
2740
1597
|
}
|
|
2741
1598
|
}
|
|
2742
|
-
async function searchPublicLinkedInProfileUrl(contact, timeoutMs, options) {
|
|
2743
|
-
const maxQueries = options?.maxQueries && Number.isFinite(options.maxQueries) && options.maxQueries > 0
|
|
2744
|
-
? Math.trunc(options.maxQueries)
|
|
2745
|
-
: 4;
|
|
2746
|
-
const queries = (await buildSerperLinkedInProfileQueries(contact, timeoutMs)).slice(0, maxQueries);
|
|
2747
|
-
for (const query of queries) {
|
|
2748
|
-
const controller = new AbortController();
|
|
2749
|
-
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2750
|
-
try {
|
|
2751
|
-
const response = await fetch(buildPublicLinkedInProfileSearchUrl(query), {
|
|
2752
|
-
method: "GET",
|
|
2753
|
-
signal: controller.signal,
|
|
2754
|
-
headers: {
|
|
2755
|
-
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
2756
|
-
}
|
|
2757
|
-
});
|
|
2758
|
-
if (!response.ok) {
|
|
2759
|
-
continue;
|
|
2760
|
-
}
|
|
2761
|
-
const bodyText = await response.text();
|
|
2762
|
-
const candidates = extractPublicLinkedInProfileSearchCandidates(bodyText);
|
|
2763
|
-
let bestUrl = null;
|
|
2764
|
-
let bestScore = 0;
|
|
2765
|
-
for (const candidateUrl of candidates.slice(0, 5)) {
|
|
2766
|
-
const signals = await fetchLinkedInProfilePageSignals(candidateUrl, timeoutMs);
|
|
2767
|
-
if (!signals || signals.unavailable) {
|
|
2768
|
-
continue;
|
|
2769
|
-
}
|
|
2770
|
-
const score = scoreLinkedInProfilePageSignals(contact, signals);
|
|
2771
|
-
if (score > bestScore) {
|
|
2772
|
-
bestScore = score;
|
|
2773
|
-
bestUrl = signals.normalizedUrl;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
if (bestUrl && bestScore >= 130) {
|
|
2777
|
-
return bestUrl;
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
catch {
|
|
2781
|
-
// Continue with the next query variant.
|
|
2782
|
-
}
|
|
2783
|
-
finally {
|
|
2784
|
-
clearTimeout(timeout);
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
return null;
|
|
2788
|
-
}
|
|
2789
1599
|
function scoreLinkedInCompanyPageSignals(companyName, signals) {
|
|
2790
1600
|
const inputTokens = normalizeLooseMatchText(companyName).split(/\s+/).filter((token) => token.length >= 4);
|
|
2791
1601
|
const haystack = normalizeLooseMatchText(`${signals.title} ${signals.description}`);
|
|
@@ -2800,20 +1610,6 @@ function scoreLinkedInCompanyPageSignals(companyName, signals) {
|
|
|
2800
1610
|
}
|
|
2801
1611
|
return score;
|
|
2802
1612
|
}
|
|
2803
|
-
function scoreSerperLinkedInCompanyCandidate(companyName, candidate) {
|
|
2804
|
-
const inputTokens = normalizeLooseMatchText(companyName).split(/\s+/).filter((token) => token.length >= 4);
|
|
2805
|
-
const haystack = normalizeLooseMatchText(`${candidate.title} ${candidate.snippet}`);
|
|
2806
|
-
let score = scoreLinkedInCompanyUrlCandidate(companyName, candidate.url);
|
|
2807
|
-
for (const token of inputTokens) {
|
|
2808
|
-
if (haystack.includes(token)) {
|
|
2809
|
-
score += 12;
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
if (haystack.includes(normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(companyName)))) {
|
|
2813
|
-
score += 40;
|
|
2814
|
-
}
|
|
2815
|
-
return score;
|
|
2816
|
-
}
|
|
2817
1613
|
function scoreLinkedInCompanyUrlCandidate(companyName, url) {
|
|
2818
1614
|
const handle = normalizeLinkedInCompanyHandle(url);
|
|
2819
1615
|
if (!handle || /^\d+$/.test(handle)) {
|
|
@@ -2907,15 +1703,9 @@ async function searchSerperLinkedInCompanyUrl(companyName, timeoutMs) {
|
|
|
2907
1703
|
const parsed = (await response.json());
|
|
2908
1704
|
const candidates = extractSerperLinkedInCompanyCandidates(parsed);
|
|
2909
1705
|
const ranked = candidates
|
|
2910
|
-
.map((
|
|
2911
|
-
...candidate,
|
|
2912
|
-
score: scoreSerperLinkedInCompanyCandidate(companyName, candidate)
|
|
2913
|
-
}))
|
|
1706
|
+
.map((url) => ({ url, score: scoreLinkedInCompanyUrlCandidate(companyName, url) }))
|
|
2914
1707
|
.filter((candidate) => candidate.score > 0)
|
|
2915
1708
|
.sort((left, right) => right.score - left.score);
|
|
2916
|
-
if (ranked[0] && ranked[0].score >= 80) {
|
|
2917
|
-
return ranked[0].url;
|
|
2918
|
-
}
|
|
2919
1709
|
let anySignalsFetched = false;
|
|
2920
1710
|
let bestValidated = null;
|
|
2921
1711
|
for (const candidate of ranked.slice(0, 3)) {
|
|
@@ -2955,11 +1745,6 @@ async function searchSerperLinkedInCompanyUrl(companyName, timeoutMs) {
|
|
|
2955
1745
|
}
|
|
2956
1746
|
async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
2957
1747
|
const config = await readLinkedInDirectLookupConfig();
|
|
2958
|
-
const companyContexts = await resolveDirectLinkedInCompanyContexts({
|
|
2959
|
-
contacts: params.contacts.filter((contact) => !contact.isVariation),
|
|
2960
|
-
timeoutMs: params.timeoutMs,
|
|
2961
|
-
config
|
|
2962
|
-
});
|
|
2963
1748
|
const groupedContacts = new Map();
|
|
2964
1749
|
for (const contact of params.contacts) {
|
|
2965
1750
|
const key = contact.email?.trim().toLowerCase() || `contact:${contact.contact_id}`;
|
|
@@ -2968,25 +1753,15 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
2968
1753
|
groupedContacts.set(key, existing);
|
|
2969
1754
|
}
|
|
2970
1755
|
const results = [];
|
|
2971
|
-
|
|
2972
|
-
? Math.trunc(params.perAttemptTimeoutMs)
|
|
2973
|
-
: Math.min(params.timeoutMs, 8_000);
|
|
2974
|
-
const perContactBudgetMs = params.perContactBudgetMs && Number.isFinite(params.perContactBudgetMs) && params.perContactBudgetMs > 0
|
|
2975
|
-
? Math.trunc(params.perContactBudgetMs)
|
|
2976
|
-
: Math.min(params.timeoutMs, 15_000);
|
|
2977
|
-
const rateLimitCooldownMs = Math.max(750, Math.min(3_000, Math.trunc(perAttemptTimeoutMs / 2)));
|
|
2978
|
-
const maxRateLimitCooldowns = 4;
|
|
2979
|
-
let rateLimitCooldownUntil = 0;
|
|
2980
|
-
let consecutiveRateLimitCount = 0;
|
|
2981
|
-
let totalRateLimitCooldowns = 0;
|
|
1756
|
+
let rateLimited = false;
|
|
2982
1757
|
for (const variations of groupedContacts.values()) {
|
|
2983
1758
|
const primary = variations.find((contact) => !contact.isVariation) ?? variations[0];
|
|
2984
1759
|
const blankPerson = !primary?.firstName.trim() || !primary?.lastName.trim();
|
|
2985
|
-
if (
|
|
1760
|
+
if (rateLimited) {
|
|
2986
1761
|
results.push({
|
|
2987
1762
|
contact_id: primary.contact_id,
|
|
2988
1763
|
linkedin_url: null,
|
|
2989
|
-
error: "LinkedIn rate limit
|
|
1764
|
+
error: "LinkedIn rate limit"
|
|
2990
1765
|
});
|
|
2991
1766
|
continue;
|
|
2992
1767
|
}
|
|
@@ -3000,23 +1775,11 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
3000
1775
|
}
|
|
3001
1776
|
let matchedUrl = null;
|
|
3002
1777
|
let matchedSalesNavUrl = null;
|
|
3003
|
-
let matchedFullName = null;
|
|
3004
|
-
let matchedCompanyName = null;
|
|
3005
|
-
let matchedTitle = null;
|
|
3006
1778
|
let lastError = null;
|
|
3007
|
-
const contactDeadline = Date.now() + perContactBudgetMs;
|
|
3008
|
-
const companyContext = companyContexts.get(buildDirectCompanyContextKey(primary));
|
|
3009
1779
|
for (const candidate of variations) {
|
|
3010
|
-
for (const searchVariant of
|
|
3011
|
-
if (Date.now() < rateLimitCooldownUntil) {
|
|
3012
|
-
await new Promise((resolve) => setTimeout(resolve, rateLimitCooldownUntil - Date.now()));
|
|
3013
|
-
}
|
|
3014
|
-
if (Date.now() >= contactDeadline) {
|
|
3015
|
-
lastError = lastError || "Direct lookup budget exhausted";
|
|
3016
|
-
break;
|
|
3017
|
-
}
|
|
1780
|
+
for (const searchVariant of buildLinkedInLookupSearchVariants(candidate)) {
|
|
3018
1781
|
const controller = new AbortController();
|
|
3019
|
-
const timeout = setTimeout(controller.abort.bind(controller), Math.min(
|
|
1782
|
+
const timeout = setTimeout(controller.abort.bind(controller), Math.min(params.timeoutMs, 20_000));
|
|
3020
1783
|
try {
|
|
3021
1784
|
const response = await fetch(buildLinkedInSalesApiUrl(searchVariant), {
|
|
3022
1785
|
method: "GET",
|
|
@@ -3037,51 +1800,20 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
3037
1800
|
}
|
|
3038
1801
|
});
|
|
3039
1802
|
if (response.status === 429) {
|
|
1803
|
+
rateLimited = true;
|
|
3040
1804
|
lastError = "LinkedIn rate limit";
|
|
3041
|
-
consecutiveRateLimitCount += 1;
|
|
3042
|
-
totalRateLimitCooldowns += 1;
|
|
3043
|
-
rateLimitCooldownUntil =
|
|
3044
|
-
Date.now() + Math.min(15_000, rateLimitCooldownMs * Math.max(1, consecutiveRateLimitCount));
|
|
3045
|
-
if (totalRateLimitCooldowns >= maxRateLimitCooldowns) {
|
|
3046
|
-
break;
|
|
3047
|
-
}
|
|
3048
1805
|
break;
|
|
3049
1806
|
}
|
|
3050
1807
|
if (!response.ok) {
|
|
3051
1808
|
lastError = `LinkedIn returned ${response.status}`;
|
|
3052
1809
|
continue;
|
|
3053
1810
|
}
|
|
3054
|
-
consecutiveRateLimitCount = 0;
|
|
3055
|
-
rateLimitCooldownUntil = 0;
|
|
3056
1811
|
const data = (await response.json());
|
|
3057
1812
|
const profilesFound = data.paging?.total ?? 0;
|
|
3058
1813
|
if (profilesFound > 0) {
|
|
3059
|
-
const
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
...scoreLinkedInSalesApiElementMatch(candidate, element)
|
|
3063
|
-
}))
|
|
3064
|
-
.sort((left, right) => right.score - left.score)[0];
|
|
3065
|
-
const hasTrustedCompanyContext = Boolean(candidate.linkedinCompanyUrl ||
|
|
3066
|
-
companyContext?.linkedinCompanyUrl ||
|
|
3067
|
-
companyContext?.matchedCompanyName);
|
|
3068
|
-
const hasTrustedEmailContext = Boolean(candidate.email && !isSyntheticLinkedInLookupEmail(candidate.email));
|
|
3069
|
-
const acceptBestCandidate = Boolean(bestCandidate &&
|
|
3070
|
-
(bestCandidate.score >= 140 ||
|
|
3071
|
-
(bestCandidate.exactNameMatch &&
|
|
3072
|
-
(bestCandidate.companyMatchCount > 0 || hasTrustedCompanyContext || hasTrustedEmailContext))));
|
|
3073
|
-
if (bestCandidate && acceptBestCandidate) {
|
|
3074
|
-
matchedUrl = extractLinkedInProfileUrlFromSalesApiElement(bestCandidate.element) ?? null;
|
|
3075
|
-
matchedSalesNavUrl = extractLinkedInSalesNavLeadUrlFromSalesApiElement(bestCandidate.element) ?? null;
|
|
3076
|
-
matchedFullName = bestCandidate.fullName;
|
|
3077
|
-
matchedCompanyName = bestCandidate.companyName;
|
|
3078
|
-
matchedTitle = bestCandidate.title;
|
|
3079
|
-
}
|
|
3080
|
-
else {
|
|
3081
|
-
lastError = bestCandidate
|
|
3082
|
-
? `LinkedIn top result score too low (${bestCandidate.score})`
|
|
3083
|
-
: "LinkedIn returned no usable results";
|
|
3084
|
-
}
|
|
1814
|
+
const first = data.elements?.[0];
|
|
1815
|
+
matchedUrl = extractLinkedInProfileUrlFromSalesApiElement(first) ?? null;
|
|
1816
|
+
matchedSalesNavUrl = extractLinkedInSalesNavLeadUrlFromSalesApiElement(first) ?? null;
|
|
3085
1817
|
if (matchedUrl || matchedSalesNavUrl) {
|
|
3086
1818
|
break;
|
|
3087
1819
|
}
|
|
@@ -3093,14 +1825,11 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
3093
1825
|
finally {
|
|
3094
1826
|
clearTimeout(timeout);
|
|
3095
1827
|
}
|
|
3096
|
-
if (matchedUrl || matchedSalesNavUrl ||
|
|
1828
|
+
if (matchedUrl || matchedSalesNavUrl || rateLimited) {
|
|
3097
1829
|
break;
|
|
3098
1830
|
}
|
|
3099
1831
|
}
|
|
3100
|
-
if (matchedUrl || matchedSalesNavUrl ||
|
|
3101
|
-
break;
|
|
3102
|
-
}
|
|
3103
|
-
if (Date.now() >= contactDeadline) {
|
|
1832
|
+
if (matchedUrl || matchedSalesNavUrl || rateLimited) {
|
|
3104
1833
|
break;
|
|
3105
1834
|
}
|
|
3106
1835
|
}
|
|
@@ -3108,21 +1837,16 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
3108
1837
|
contact_id: primary.contact_id,
|
|
3109
1838
|
linkedin_url: matchedUrl ?? matchedSalesNavUrl,
|
|
3110
1839
|
sales_nav_profile_url: matchedSalesNavUrl,
|
|
3111
|
-
matched_full_name: matchedFullName,
|
|
3112
|
-
matched_company_name: matchedCompanyName,
|
|
3113
|
-
matched_title: matchedTitle,
|
|
3114
1840
|
error: matchedUrl || matchedSalesNavUrl ? null : lastError
|
|
3115
1841
|
});
|
|
3116
1842
|
}
|
|
3117
1843
|
return {
|
|
3118
1844
|
success: true,
|
|
3119
|
-
contacts: results
|
|
3120
|
-
companyContexts: Array.from(companyContexts.values())
|
|
1845
|
+
contacts: results
|
|
3121
1846
|
};
|
|
3122
1847
|
}
|
|
3123
1848
|
async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
3124
1849
|
const config = await readLinkedInDirectLookupConfig();
|
|
3125
|
-
const precomputedContextByKey = new Map((params.precomputedContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
3126
1850
|
const primaryContacts = new Map();
|
|
3127
1851
|
for (const contact of params.contacts) {
|
|
3128
1852
|
const existing = primaryContacts.get(contact.contact_id);
|
|
@@ -3146,23 +1870,11 @@ async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
|
3146
1870
|
companyName: contact.companyName,
|
|
3147
1871
|
companyNameOriginal: contact.companyNameOriginal
|
|
3148
1872
|
});
|
|
3149
|
-
|
|
3150
|
-
let
|
|
3151
|
-
let
|
|
3152
|
-
let
|
|
3153
|
-
let matchedCompanyEmployeeCount = precomputedContext?.matchedCompanyEmployeeCount ?? null;
|
|
1873
|
+
let matchedCompanyUrl = null;
|
|
1874
|
+
let matchedSalesNavCompanyUrl = null;
|
|
1875
|
+
let matchedCompanyName = null;
|
|
1876
|
+
let matchedCompanyEmployeeCount = null;
|
|
3154
1877
|
let lastError = null;
|
|
3155
|
-
if (matchedCompanyUrl || matchedSalesNavCompanyUrl || matchedCompanyName) {
|
|
3156
|
-
results.push({
|
|
3157
|
-
contact_id: contact.contact_id,
|
|
3158
|
-
linkedin_company_url: matchedCompanyUrl,
|
|
3159
|
-
sales_nav_company_url: matchedSalesNavCompanyUrl,
|
|
3160
|
-
matched_company_name: matchedCompanyName,
|
|
3161
|
-
matched_company_employee_count: matchedCompanyEmployeeCount,
|
|
3162
|
-
error: null
|
|
3163
|
-
});
|
|
3164
|
-
continue;
|
|
3165
|
-
}
|
|
3166
1878
|
for (const variant of variants) {
|
|
3167
1879
|
const controller = new AbortController();
|
|
3168
1880
|
const timeout = setTimeout(controller.abort.bind(controller), Math.min(params.timeoutMs, 20_000));
|
|
@@ -3305,34 +2017,9 @@ async function invokeLinkedInUrlEnrichmentWorkflow(params) {
|
|
|
3305
2017
|
}
|
|
3306
2018
|
}
|
|
3307
2019
|
function normalizeWorkflowLinkedInUrlResult(params) {
|
|
3308
|
-
const inputContactIds = new Set(params.contacts.map((contact) => contact.contact_id));
|
|
3309
2020
|
const contactIdsBySyntheticEmail = new Map(params.contacts
|
|
3310
2021
|
.filter((contact) => contact.email)
|
|
3311
2022
|
.map((contact) => [String(contact.email).toLowerCase(), contact.contact_id]));
|
|
3312
|
-
const contactIdsByNormalizedIdentity = new Map(params.contacts
|
|
3313
|
-
.filter((contact) => !contact.isVariation)
|
|
3314
|
-
.map((contact) => {
|
|
3315
|
-
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3316
|
-
const companyName = normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName);
|
|
3317
|
-
return [`${fullName}|${companyName}`, contact.contact_id];
|
|
3318
|
-
})
|
|
3319
|
-
.filter(([key]) => key !== "|"));
|
|
3320
|
-
const normalizedNameCounts = new Map();
|
|
3321
|
-
for (const contact of params.contacts) {
|
|
3322
|
-
if (contact.isVariation)
|
|
3323
|
-
continue;
|
|
3324
|
-
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3325
|
-
if (!fullName)
|
|
3326
|
-
continue;
|
|
3327
|
-
normalizedNameCounts.set(fullName, (normalizedNameCounts.get(fullName) ?? 0) + 1);
|
|
3328
|
-
}
|
|
3329
|
-
const contactIdsByNormalizedName = new Map(params.contacts
|
|
3330
|
-
.filter((contact) => !contact.isVariation)
|
|
3331
|
-
.map((contact) => {
|
|
3332
|
-
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3333
|
-
return [fullName, contact.contact_id];
|
|
3334
|
-
})
|
|
3335
|
-
.filter(([fullName]) => Boolean(fullName) && (normalizedNameCounts.get(fullName) ?? 0) === 1));
|
|
3336
2023
|
const rowsByContactId = new Map();
|
|
3337
2024
|
const body = params.parsedBody && typeof params.parsedBody === "object" && !Array.isArray(params.parsedBody)
|
|
3338
2025
|
? params.parsedBody
|
|
@@ -3342,34 +2029,13 @@ function normalizeWorkflowLinkedInUrlResult(params) {
|
|
|
3342
2029
|
...(Array.isArray(body?.profiles) ? body?.profiles : [])
|
|
3343
2030
|
];
|
|
3344
2031
|
for (const contact of workflowRows) {
|
|
3345
|
-
const fullNameCandidate = normalizeLookupWhitespace(typeof contact.full_name === "string"
|
|
3346
|
-
? contact.full_name
|
|
3347
|
-
: typeof contact.fullName === "string"
|
|
3348
|
-
? contact.fullName
|
|
3349
|
-
: typeof contact.name === "string"
|
|
3350
|
-
? contact.name
|
|
3351
|
-
: [contact.first_name, contact.last_name]
|
|
3352
|
-
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
3353
|
-
.join(" "));
|
|
3354
|
-
const companyNameCandidate = normalizeLookupWhitespace(typeof contact.company_name === "string"
|
|
3355
|
-
? contact.company_name
|
|
3356
|
-
: typeof contact.companyName === "string"
|
|
3357
|
-
? contact.companyName
|
|
3358
|
-
: typeof contact.current_company === "string"
|
|
3359
|
-
? contact.current_company
|
|
3360
|
-
: "");
|
|
3361
|
-
const normalizedIdentityKey = `${normalizeLooseMatchText(fullNameCandidate)}|${normalizeLooseMatchText(companyNameCandidate)}`;
|
|
3362
2032
|
const explicitContactId = typeof contact.contact_id === "string"
|
|
3363
2033
|
? contact.contact_id
|
|
3364
2034
|
: typeof contact.contact_id === "number"
|
|
3365
2035
|
? String(contact.contact_id)
|
|
3366
2036
|
: "";
|
|
3367
2037
|
const emailKey = typeof contact.email === "string" ? contact.email.toLowerCase() : "";
|
|
3368
|
-
const contactId =
|
|
3369
|
-
contactIdsBySyntheticEmail.get(emailKey) ||
|
|
3370
|
-
contactIdsByNormalizedIdentity.get(normalizedIdentityKey) ||
|
|
3371
|
-
contactIdsByNormalizedName.get(normalizeLooseMatchText(fullNameCandidate)) ||
|
|
3372
|
-
"";
|
|
2038
|
+
const contactId = explicitContactId || contactIdsBySyntheticEmail.get(emailKey) || "";
|
|
3373
2039
|
const linkedinUrl = normalizePublicLinkedInProfileUrl(typeof contact.linkedin_profile_url === "string"
|
|
3374
2040
|
? contact.linkedin_profile_url
|
|
3375
2041
|
: typeof contact.linkedinProfileUrl === "string"
|
|
@@ -3456,8 +2122,7 @@ async function fetchSalesNavLookupCandidates(params) {
|
|
|
3456
2122
|
}
|
|
3457
2123
|
async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
3458
2124
|
const results = [];
|
|
3459
|
-
for (const row of params.rows) {
|
|
3460
|
-
const contactId = normalizeLinkedInLookupField(row.contactId) ?? `${results.length + 1}`;
|
|
2125
|
+
for (const [index, row] of params.rows.entries()) {
|
|
3461
2126
|
const candidates = await fetchSalesNavLookupCandidates({
|
|
3462
2127
|
companyName: row.companyName,
|
|
3463
2128
|
orgId: params.orgId
|
|
@@ -3500,250 +2165,32 @@ async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
|
3500
2165
|
normalizeLinkedInCompanyHandle(best?.companyUrl ?? "");
|
|
3501
2166
|
if (handle) {
|
|
3502
2167
|
return normalizeLinkedInCompanyPage(handle);
|
|
3503
|
-
}
|
|
3504
|
-
const numericCompanyUrl = typeof best?.companyUrl === "string" ? best.companyUrl.trim() : "";
|
|
3505
|
-
return numericCompanyUrl.length > 0 ? numericCompanyUrl : null;
|
|
3506
|
-
})();
|
|
3507
|
-
const salesNavCompanyUrl = typeof best?.companyUrl === "string" && /\/sales\/company\//i.test(best.companyUrl)
|
|
3508
|
-
? best.companyUrl
|
|
3509
|
-
: null;
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
});
|
|
3530
|
-
}
|
|
3531
|
-
return results;
|
|
3532
|
-
}
|
|
3533
|
-
function shouldUseSalesNavRowPrepass(params) {
|
|
3534
|
-
const env = params.env ?? process.env;
|
|
3535
|
-
const explicit = env.SALESPROMPTER_LINKEDIN_ROW_PREPASS?.trim().toLowerCase();
|
|
3536
|
-
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3537
|
-
return false;
|
|
3538
|
-
}
|
|
3539
|
-
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3540
|
-
return true;
|
|
3541
|
-
}
|
|
3542
|
-
const hasOrgId = Boolean(params.orgId?.trim());
|
|
3543
|
-
const hasSupabase = Boolean(env.NEXT_PUBLIC_SUPABASE_URL?.trim() && env.SUPABASE_SERVICE_ROLE_KEY?.trim());
|
|
3544
|
-
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_ROW_PREPASS_MAX_ROWS ?? 200);
|
|
3545
|
-
if (!hasOrgId || !hasSupabase) {
|
|
3546
|
-
return false;
|
|
3547
|
-
}
|
|
3548
|
-
return params.rows.length <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : 200);
|
|
3549
|
-
}
|
|
3550
|
-
function shouldUseDirectPeopleLookup(params) {
|
|
3551
|
-
const env = params.env ?? process.env;
|
|
3552
|
-
const explicit = env.SALESPROMPTER_LINKEDIN_DIRECT_PROFILE_LOOKUP?.trim().toLowerCase();
|
|
3553
|
-
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3554
|
-
return false;
|
|
3555
|
-
}
|
|
3556
|
-
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3557
|
-
return true;
|
|
3558
|
-
}
|
|
3559
|
-
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_DIRECT_PROFILE_MAX_ROWS ?? 50);
|
|
3560
|
-
return params.rowCount <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : 50);
|
|
3561
|
-
}
|
|
3562
|
-
function shouldUseWorkflowPeopleLookup(params) {
|
|
3563
|
-
const env = params.env ?? process.env;
|
|
3564
|
-
const explicit = env.SALESPROMPTER_LINKEDIN_WORKFLOW_PROFILE_LOOKUP?.trim().toLowerCase();
|
|
3565
|
-
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3566
|
-
return false;
|
|
3567
|
-
}
|
|
3568
|
-
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3569
|
-
return true;
|
|
3570
|
-
}
|
|
3571
|
-
const hasSerper = Boolean(getSerperApiKey(env));
|
|
3572
|
-
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_WORKFLOW_PROFILE_MAX_ROWS ?? (hasSerper ? 75 : 250));
|
|
3573
|
-
return params.rowCount <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : hasSerper ? 75 : 250);
|
|
3574
|
-
}
|
|
3575
|
-
function shouldUseBulkProfileResolutionStrategy(params) {
|
|
3576
|
-
const env = params.env ?? process.env;
|
|
3577
|
-
const explicit = env.SALESPROMPTER_LINKEDIN_BULK_MODE?.trim().toLowerCase();
|
|
3578
|
-
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3579
|
-
return false;
|
|
3580
|
-
}
|
|
3581
|
-
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3582
|
-
return true;
|
|
3583
|
-
}
|
|
3584
|
-
const minRows = Number(env.SALESPROMPTER_LINKEDIN_BULK_MODE_MIN_ROWS ?? 75);
|
|
3585
|
-
return params.rowCount >= (Number.isFinite(minRows) && minRows > 0 ? minRows : 75);
|
|
3586
|
-
}
|
|
3587
|
-
function resolveLinkedInBulkStrategyConfig(params) {
|
|
3588
|
-
const env = params.env ?? process.env;
|
|
3589
|
-
const bulkMode = shouldUseBulkProfileResolutionStrategy({
|
|
3590
|
-
rowCount: params.rowCount,
|
|
3591
|
-
env
|
|
3592
|
-
});
|
|
3593
|
-
const serperConcurrencyDefault = bulkMode ? 12 : 6;
|
|
3594
|
-
const serperConcurrency = Number(env.SALESPROMPTER_LINKEDIN_SERPER_CONCURRENCY ?? serperConcurrencyDefault);
|
|
3595
|
-
const serperMaxQueriesDefault = bulkMode ? 4 : 8;
|
|
3596
|
-
const serperMaxQueries = Number(env.SALESPROMPTER_LINKEDIN_SERPER_MAX_QUERIES ?? serperMaxQueriesDefault);
|
|
3597
|
-
const workflowStageBudgetDefault = bulkMode ? 8_000 : 15_000;
|
|
3598
|
-
const workflowStageBudgetMs = Number(env.SALESPROMPTER_LINKEDIN_WORKFLOW_STAGE_TIMEOUT_MS ?? workflowStageBudgetDefault);
|
|
3599
|
-
const serperStageBudgetDefault = bulkMode
|
|
3600
|
-
? Math.max(15_000, Math.min(params.timeoutMs * 2, 45_000))
|
|
3601
|
-
: Math.max(10_000, Math.min(params.timeoutMs, 20_000));
|
|
3602
|
-
const serperStageBudgetMs = Number(env.SALESPROMPTER_LINKEDIN_SERPER_STAGE_TIMEOUT_MS ?? serperStageBudgetDefault);
|
|
3603
|
-
const bulkDirectProfileMaxRowsDefault = 0;
|
|
3604
|
-
const bulkDirectProfileMaxRows = Number(env.SALESPROMPTER_LINKEDIN_BULK_DIRECT_PROFILE_MAX_ROWS ?? bulkDirectProfileMaxRowsDefault);
|
|
3605
|
-
const bulkDirectProfileTimeoutDefault = bulkMode ? Math.min(params.timeoutMs, 6_000) : 0;
|
|
3606
|
-
const bulkDirectProfileTimeoutMs = Number(env.SALESPROMPTER_LINKEDIN_BULK_DIRECT_PROFILE_TIMEOUT_MS ?? bulkDirectProfileTimeoutDefault);
|
|
3607
|
-
return {
|
|
3608
|
-
bulkMode,
|
|
3609
|
-
serperConcurrency: Number.isFinite(serperConcurrency) && serperConcurrency > 0
|
|
3610
|
-
? Math.trunc(serperConcurrency)
|
|
3611
|
-
: serperConcurrencyDefault,
|
|
3612
|
-
serperMaxQueries: Number.isFinite(serperMaxQueries) && serperMaxQueries > 0
|
|
3613
|
-
? Math.trunc(serperMaxQueries)
|
|
3614
|
-
: serperMaxQueriesDefault,
|
|
3615
|
-
workflowStageBudgetMs: Number.isFinite(workflowStageBudgetMs) && workflowStageBudgetMs > 0
|
|
3616
|
-
? Math.trunc(workflowStageBudgetMs)
|
|
3617
|
-
: workflowStageBudgetDefault,
|
|
3618
|
-
serperStageBudgetMs: Number.isFinite(serperStageBudgetMs) && serperStageBudgetMs > 0
|
|
3619
|
-
? Math.trunc(serperStageBudgetMs)
|
|
3620
|
-
: serperStageBudgetDefault,
|
|
3621
|
-
bulkDirectProfileMaxRows: Number.isFinite(bulkDirectProfileMaxRows) && bulkDirectProfileMaxRows > 0
|
|
3622
|
-
? Math.trunc(bulkDirectProfileMaxRows)
|
|
3623
|
-
: 0,
|
|
3624
|
-
bulkDirectProfileTimeoutMs: Number.isFinite(bulkDirectProfileTimeoutMs) && bulkDirectProfileTimeoutMs > 0
|
|
3625
|
-
? Math.trunc(bulkDirectProfileTimeoutMs)
|
|
3626
|
-
: 0
|
|
3627
|
-
};
|
|
3628
|
-
}
|
|
3629
|
-
function shouldAttemptBulkDirectProfileLookup(params) {
|
|
3630
|
-
return (params.strategy.bulkMode &&
|
|
3631
|
-
params.strategy.bulkDirectProfileMaxRows > 0 &&
|
|
3632
|
-
params.strategy.bulkDirectProfileTimeoutMs > 0 &&
|
|
3633
|
-
params.unresolvedRowCount > 0);
|
|
3634
|
-
}
|
|
3635
|
-
function rankContactsForBulkDirectProfileLookup(params) {
|
|
3636
|
-
const scored = params.contacts
|
|
3637
|
-
.filter((contact) => !contact.isVariation)
|
|
3638
|
-
.map((contact) => {
|
|
3639
|
-
const row = params.rowsByContactId.get(contact.contact_id);
|
|
3640
|
-
const normalizedName = normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`);
|
|
3641
|
-
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
3642
|
-
const titleKeywords = extractLookupTitleKeywords(contact.jobTitle);
|
|
3643
|
-
const roleKeywords = buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole);
|
|
3644
|
-
let score = 0;
|
|
3645
|
-
if (row?.linkedinCompanyUrl || contact.linkedinCompanyUrl)
|
|
3646
|
-
score += 80;
|
|
3647
|
-
if (row?.salesNavCompanyUrl)
|
|
3648
|
-
score += 20;
|
|
3649
|
-
if (normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail))
|
|
3650
|
-
score += 40;
|
|
3651
|
-
if (contact.jobTitle?.trim())
|
|
3652
|
-
score += 25;
|
|
3653
|
-
if (contact.deepDiveRecommendedRole?.trim())
|
|
3654
|
-
score += 15;
|
|
3655
|
-
score += Math.min(20, titleKeywords.length * 5);
|
|
3656
|
-
score += Math.min(15, roleKeywords.length * 5);
|
|
3657
|
-
if (/^contact\s+\d+$/i.test(normalizedName))
|
|
3658
|
-
score -= 100;
|
|
3659
|
-
if (/^(hr|support|facility|buchhaltung|rechnungen)$/i.test(normalizedName))
|
|
3660
|
-
score -= 25;
|
|
3661
|
-
return { contact, score };
|
|
3662
|
-
})
|
|
3663
|
-
.filter((entry) => entry.score > 0)
|
|
3664
|
-
.sort((left, right) => right.score - left.score);
|
|
3665
|
-
return scored.slice(0, params.limit).map((entry) => entry.contact);
|
|
3666
|
-
}
|
|
3667
|
-
async function resolveSerperLinkedInProfilesInParallel(params) {
|
|
3668
|
-
const results = new Map();
|
|
3669
|
-
const contacts = params.contacts;
|
|
3670
|
-
const concurrency = Math.max(1, Math.min(params.concurrency ?? 3, contacts.length || 1));
|
|
3671
|
-
const deadline = params.overallBudgetMs && Number.isFinite(params.overallBudgetMs) && params.overallBudgetMs > 0
|
|
3672
|
-
? Date.now() + Math.trunc(params.overallBudgetMs)
|
|
3673
|
-
: Number.POSITIVE_INFINITY;
|
|
3674
|
-
let nextIndex = 0;
|
|
3675
|
-
const worker = async () => {
|
|
3676
|
-
while (true) {
|
|
3677
|
-
if (Date.now() >= deadline) {
|
|
3678
|
-
return;
|
|
3679
|
-
}
|
|
3680
|
-
const index = nextIndex++;
|
|
3681
|
-
if (index >= contacts.length) {
|
|
3682
|
-
return;
|
|
3683
|
-
}
|
|
3684
|
-
const contact = contacts[index];
|
|
3685
|
-
const remainingBudget = deadline - Date.now();
|
|
3686
|
-
if (remainingBudget <= 0) {
|
|
3687
|
-
return;
|
|
3688
|
-
}
|
|
3689
|
-
const linkedinUrl = await searchSerperLinkedInProfileUrl(contact, Math.min(params.timeoutMs, remainingBudget), {
|
|
3690
|
-
maxQueries: params.maxQueries
|
|
3691
|
-
});
|
|
3692
|
-
if (linkedinUrl) {
|
|
3693
|
-
results.set(contact.contact_id, linkedinUrl);
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
};
|
|
3697
|
-
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
3698
|
-
return results;
|
|
3699
|
-
}
|
|
3700
|
-
async function resolveLinkedInCompanyUrlsForContacts(params) {
|
|
3701
|
-
const contacts = params.contacts.filter((contact) => !contact.isVariation && !contact.linkedinCompanyUrl);
|
|
3702
|
-
const uniqueCompanies = new Map();
|
|
3703
|
-
for (const contact of contacts) {
|
|
3704
|
-
const key = normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName);
|
|
3705
|
-
if (!key || uniqueCompanies.has(key)) {
|
|
3706
|
-
continue;
|
|
3707
|
-
}
|
|
3708
|
-
uniqueCompanies.set(key, contact.companyNameOriginal ?? contact.companyName);
|
|
3709
|
-
}
|
|
3710
|
-
const resultsByCompany = new Map();
|
|
3711
|
-
const entries = Array.from(uniqueCompanies.entries());
|
|
3712
|
-
const concurrency = Math.max(1, Math.min(params.concurrency ?? 4, entries.length || 1));
|
|
3713
|
-
const deadline = params.overallBudgetMs && Number.isFinite(params.overallBudgetMs) && params.overallBudgetMs > 0
|
|
3714
|
-
? Date.now() + Math.trunc(params.overallBudgetMs)
|
|
3715
|
-
: Number.POSITIVE_INFINITY;
|
|
3716
|
-
let nextIndex = 0;
|
|
3717
|
-
const worker = async () => {
|
|
3718
|
-
while (true) {
|
|
3719
|
-
if (Date.now() >= deadline) {
|
|
3720
|
-
return;
|
|
3721
|
-
}
|
|
3722
|
-
const index = nextIndex++;
|
|
3723
|
-
if (index >= entries.length) {
|
|
3724
|
-
return;
|
|
3725
|
-
}
|
|
3726
|
-
const [key, companyName] = entries[index];
|
|
3727
|
-
const remainingBudget = deadline - Date.now();
|
|
3728
|
-
if (remainingBudget <= 0) {
|
|
3729
|
-
return;
|
|
3730
|
-
}
|
|
3731
|
-
const perCompanyTimeout = Math.min(params.timeoutMs, remainingBudget);
|
|
3732
|
-
const linkedinUrl = (await searchSerperLinkedInCompanyUrl(companyName, perCompanyTimeout)) ??
|
|
3733
|
-
(await searchPublicLinkedInCompanyUrl(companyName, perCompanyTimeout));
|
|
3734
|
-
if (linkedinUrl) {
|
|
3735
|
-
resultsByCompany.set(key, linkedinUrl);
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
};
|
|
3739
|
-
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
3740
|
-
const results = new Map();
|
|
3741
|
-
for (const contact of params.contacts) {
|
|
3742
|
-
const key = normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName);
|
|
3743
|
-
const linkedinUrl = resultsByCompany.get(key);
|
|
3744
|
-
if (linkedinUrl) {
|
|
3745
|
-
results.set(contact.contact_id, linkedinUrl);
|
|
3746
|
-
}
|
|
2168
|
+
}
|
|
2169
|
+
const numericCompanyUrl = typeof best?.companyUrl === "string" ? best.companyUrl.trim() : "";
|
|
2170
|
+
return numericCompanyUrl.length > 0 ? numericCompanyUrl : null;
|
|
2171
|
+
})();
|
|
2172
|
+
const salesNavCompanyUrl = typeof best?.companyUrl === "string" && /\/sales\/company\//i.test(best.companyUrl)
|
|
2173
|
+
? best.companyUrl
|
|
2174
|
+
: null;
|
|
2175
|
+
results.push({
|
|
2176
|
+
clientId: row.clientId,
|
|
2177
|
+
fullName: row.fullName,
|
|
2178
|
+
companyName: row.companyName,
|
|
2179
|
+
linkedinUrl,
|
|
2180
|
+
salesNavProfileUrl,
|
|
2181
|
+
linkedinCompanyUrl,
|
|
2182
|
+
salesNavCompanyUrl,
|
|
2183
|
+
found: Boolean(linkedinUrl),
|
|
2184
|
+
companyFound: Boolean(linkedinCompanyUrl),
|
|
2185
|
+
contactId: String(index + 1),
|
|
2186
|
+
source: linkedinUrl ? "salesnav-supabase" : null,
|
|
2187
|
+
companySource: linkedinCompanyUrl ? "salesnav-supabase" : null,
|
|
2188
|
+
matchedFullName: best?.fullName ?? null,
|
|
2189
|
+
matchedCompanyName: best?.companyName ?? null,
|
|
2190
|
+
matchedTitle: best?.title ?? null,
|
|
2191
|
+
matchedOrgId: best?.orgId ?? null,
|
|
2192
|
+
matchedCompanyEmployeeCount: null
|
|
2193
|
+
});
|
|
3747
2194
|
}
|
|
3748
2195
|
return results;
|
|
3749
2196
|
}
|
|
@@ -4093,14 +2540,8 @@ function writeWizardSection(title, description) {
|
|
|
4093
2540
|
}
|
|
4094
2541
|
writeWizardLine();
|
|
4095
2542
|
}
|
|
4096
|
-
function isOpaqueOrgId(value) {
|
|
4097
|
-
return /^org_[A-Za-z0-9]+$/.test(value);
|
|
4098
|
-
}
|
|
4099
2543
|
function getOrgLabel(session) {
|
|
4100
2544
|
const label = session.user.orgName ?? session.user.orgSlug ?? session.user.orgId ?? null;
|
|
4101
|
-
if (label && isOpaqueOrgId(label)) {
|
|
4102
|
-
return null;
|
|
4103
|
-
}
|
|
4104
2545
|
return label;
|
|
4105
2546
|
}
|
|
4106
2547
|
function resolveSessionOrgId(session) {
|
|
@@ -4324,13 +2765,19 @@ async function promptYesNo(rl, prompt, defaultValue) {
|
|
|
4324
2765
|
}
|
|
4325
2766
|
async function ensureWizardSession(options) {
|
|
4326
2767
|
if (shouldBypassAuth()) {
|
|
4327
|
-
return
|
|
2768
|
+
return {
|
|
2769
|
+
session: null,
|
|
2770
|
+
restoredFromCache: false
|
|
2771
|
+
};
|
|
4328
2772
|
}
|
|
4329
2773
|
try {
|
|
4330
2774
|
const session = await requireAuthSession();
|
|
4331
2775
|
writeSessionSummary(session);
|
|
4332
2776
|
writeWizardLine();
|
|
4333
|
-
return
|
|
2777
|
+
return {
|
|
2778
|
+
session,
|
|
2779
|
+
restoredFromCache: true
|
|
2780
|
+
};
|
|
4334
2781
|
}
|
|
4335
2782
|
catch (error) {
|
|
4336
2783
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4346,6 +2793,29 @@ async function ensureWizardSession(options) {
|
|
|
4346
2793
|
});
|
|
4347
2794
|
writeSessionSummary(result.session);
|
|
4348
2795
|
writeWizardLine();
|
|
2796
|
+
return {
|
|
2797
|
+
session: result.session,
|
|
2798
|
+
restoredFromCache: false
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async function confirmWizardWorkspace(rl, session, options) {
|
|
2802
|
+
const orgLabel = getOrgLabel(session);
|
|
2803
|
+
const promptLabel = orgLabel ? `workspace ${orgLabel}` : "this signed-in account without a selected workspace";
|
|
2804
|
+
const useCurrentWorkspace = await promptYesNo(rl, `Use ${promptLabel} for this CLI run?`, true);
|
|
2805
|
+
if (useCurrentWorkspace) {
|
|
2806
|
+
writeWizardLine();
|
|
2807
|
+
return session;
|
|
2808
|
+
}
|
|
2809
|
+
writeWizardLine();
|
|
2810
|
+
writeWizardLine("Choose the workspace for this CLI session in the browser.");
|
|
2811
|
+
writeWizardLine();
|
|
2812
|
+
await clearAuthSession();
|
|
2813
|
+
const result = await performLogin({
|
|
2814
|
+
apiUrl: options?.apiUrl ?? session.apiBaseUrl,
|
|
2815
|
+
timeoutSeconds: options?.timeoutSeconds ?? 180
|
|
2816
|
+
});
|
|
2817
|
+
writeSessionSummary(result.session);
|
|
2818
|
+
writeWizardLine();
|
|
4349
2819
|
return result.session;
|
|
4350
2820
|
}
|
|
4351
2821
|
async function resolveLlmAuthReadiness() {
|
|
@@ -4411,72 +2881,6 @@ async function fetchWorkspaceLeadSearch(session, requestBody) {
|
|
|
4411
2881
|
}
|
|
4412
2882
|
return WorkspaceLeadSearchResponseSchema.parse(payload).leads;
|
|
4413
2883
|
}
|
|
4414
|
-
async function buildWorkspaceLeadAccount(icp, target, leads) {
|
|
4415
|
-
const firstLead = leads[0];
|
|
4416
|
-
if (firstLead) {
|
|
4417
|
-
const keywords = Array.from(new Set([target.companyDomain?.split(".")[0], firstLead.industry, firstLead.region, ...icp.keywords].filter((value) => typeof value === "string" && value.trim().length > 0)));
|
|
4418
|
-
return AccountProfileSchema.parse({
|
|
4419
|
-
companyName: target.companyName?.trim() || firstLead.companyName,
|
|
4420
|
-
domain: target.companyDomain?.trim().toLowerCase() || firstLead.domain,
|
|
4421
|
-
industry: firstLead.industry,
|
|
4422
|
-
region: firstLead.region,
|
|
4423
|
-
employeeCount: firstLead.employeeCount,
|
|
4424
|
-
keywords,
|
|
4425
|
-
sources: ["workspace-qualified-leads"]
|
|
4426
|
-
});
|
|
4427
|
-
}
|
|
4428
|
-
return await companyProvider.resolveCompany({
|
|
4429
|
-
companyDomain: target.companyDomain,
|
|
4430
|
-
companyName: target.companyName
|
|
4431
|
-
}, icp);
|
|
4432
|
-
}
|
|
4433
|
-
async function generateLeadsForCommand(options) {
|
|
4434
|
-
const source = z.enum(["auto", "workspace", "fallback"]).parse(options.source ?? "auto");
|
|
4435
|
-
if (source === "fallback") {
|
|
4436
|
-
return await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4437
|
-
}
|
|
4438
|
-
if (shouldBypassAuth()) {
|
|
4439
|
-
if (source === "workspace") {
|
|
4440
|
-
throw new Error("workspace lead generation requires authentication. Disable SALESPROMPTER_SKIP_AUTH and log in first.");
|
|
4441
|
-
}
|
|
4442
|
-
return await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4443
|
-
}
|
|
4444
|
-
try {
|
|
4445
|
-
const session = await requireAuthSession();
|
|
4446
|
-
const requestBody = options.target.companyDomain || options.target.linkedinCompanyPage
|
|
4447
|
-
? {
|
|
4448
|
-
mode: "target-company",
|
|
4449
|
-
domain: options.target.companyDomain,
|
|
4450
|
-
linkedinCompanyPage: options.target.linkedinCompanyPage,
|
|
4451
|
-
limit: options.count
|
|
4452
|
-
}
|
|
4453
|
-
: {
|
|
4454
|
-
mode: "reference-company",
|
|
4455
|
-
icp: options.icp,
|
|
4456
|
-
limit: options.count
|
|
4457
|
-
};
|
|
4458
|
-
const leads = await fetchWorkspaceLeadSearch(session, requestBody);
|
|
4459
|
-
const account = await buildWorkspaceLeadAccount(options.icp, options.target, leads);
|
|
4460
|
-
return {
|
|
4461
|
-
provider: "salesprompter-app-workspace-search",
|
|
4462
|
-
mode: "real",
|
|
4463
|
-
account,
|
|
4464
|
-
leads,
|
|
4465
|
-
warnings: []
|
|
4466
|
-
};
|
|
4467
|
-
}
|
|
4468
|
-
catch (error) {
|
|
4469
|
-
if (source === "workspace") {
|
|
4470
|
-
throw error;
|
|
4471
|
-
}
|
|
4472
|
-
const fallback = await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4473
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4474
|
-
return {
|
|
4475
|
-
...fallback,
|
|
4476
|
-
warnings: [`Workspace lead search unavailable: ${message}`, ...fallback.warnings]
|
|
4477
|
-
};
|
|
4478
|
-
}
|
|
4479
|
-
}
|
|
4480
2884
|
function buildLinkedInProductsOutputPath(categorySlug) {
|
|
4481
2885
|
return `./data/linkedin-products-${categorySlug}.json`;
|
|
4482
2886
|
}
|
|
@@ -5250,17 +3654,6 @@ async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
|
5250
3654
|
}), LinkedInCompanyBackfillStatusResponseSchema);
|
|
5251
3655
|
return value;
|
|
5252
3656
|
}
|
|
5253
|
-
async function syncPhantombusterContainersViaApp(session, payload) {
|
|
5254
|
-
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/phantombuster/containers/sync`, {
|
|
5255
|
-
method: "POST",
|
|
5256
|
-
headers: {
|
|
5257
|
-
"Content-Type": "application/json",
|
|
5258
|
-
Authorization: `Bearer ${currentSession.accessToken}`
|
|
5259
|
-
},
|
|
5260
|
-
body: JSON.stringify(payload)
|
|
5261
|
-
}), PhantombusterContainersSyncResponseSchema);
|
|
5262
|
-
return value;
|
|
5263
|
-
}
|
|
5264
3657
|
function serializeSalesNavigatorFiltersForApi(filters) {
|
|
5265
3658
|
return filters.map((filter) => ({
|
|
5266
3659
|
type: filter.type,
|
|
@@ -5287,12 +3680,6 @@ function buildSalesNavigatorSliceRawPayload(slice, extra = {}) {
|
|
|
5287
3680
|
resultRetryCount: slice.resultRetryCount ?? null
|
|
5288
3681
|
};
|
|
5289
3682
|
}
|
|
5290
|
-
function parseOptionalSalesNavigatorClientId(value) {
|
|
5291
|
-
if (value == null || String(value).trim().length === 0) {
|
|
5292
|
-
return null;
|
|
5293
|
-
}
|
|
5294
|
-
return z.coerce.number().int().positive().parse(value);
|
|
5295
|
-
}
|
|
5296
3683
|
function buildSalesNavigatorCrawlReportRawPayload(slice, traceId, extra = {}) {
|
|
5297
3684
|
return buildSalesNavigatorSliceRawPayload({
|
|
5298
3685
|
sourceQueryUrl: slice.sourceQueryUrl,
|
|
@@ -5753,12 +4140,11 @@ function isSalesNavigatorSessionError(error) {
|
|
|
5753
4140
|
return /can't connect profile|sales navigator account|upsell|linkedin session invalid|linkedin_rate_limited|too many requests|rate.?limit|invalid session cookie|disconnected by linkedin|linkedin-disconnected-while-using-api|provide a new linkedin session cookie/i.test(message);
|
|
5754
4141
|
}
|
|
5755
4142
|
function isSalesNavigatorResultArtifactError(error) {
|
|
5756
|
-
if (error instanceof SalesNavigatorExportRequestError &&
|
|
5757
|
-
["phantombuster_result_invalid", "partial_result_artifact"].includes(error.errorCode ?? "")) {
|
|
4143
|
+
if (error instanceof SalesNavigatorExportRequestError && error.errorCode === "phantombuster_result_invalid") {
|
|
5758
4144
|
return true;
|
|
5759
4145
|
}
|
|
5760
4146
|
const message = error instanceof Error ? error.message : String(error);
|
|
5761
|
-
return /page has crashed|no valid sales navigator people rows
|
|
4147
|
+
return /page has crashed|no valid sales navigator people rows/i.test(message);
|
|
5762
4148
|
}
|
|
5763
4149
|
function isSalesNavigatorTransientExportError(error) {
|
|
5764
4150
|
if (isSalesNavigatorSessionError(error) || isSalesNavigatorResultArtifactError(error)) {
|
|
@@ -5849,7 +4235,6 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
5849
4235
|
crawlSliceId: context?.crawlSliceId,
|
|
5850
4236
|
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
5851
4237
|
traceId: context?.traceId ?? null,
|
|
5852
|
-
clientId: context?.clientId ?? null,
|
|
5853
4238
|
phase: shouldProbe ? "probe" : "full_export",
|
|
5854
4239
|
requestedProfiles: probeProfiles,
|
|
5855
4240
|
crawlJobId: context?.crawlJobId ?? null,
|
|
@@ -5886,7 +4271,6 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
5886
4271
|
crawlSliceId: context?.crawlSliceId,
|
|
5887
4272
|
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
5888
4273
|
traceId: context?.traceId ?? null,
|
|
5889
|
-
clientId: context?.clientId ?? null,
|
|
5890
4274
|
phase: "full_export_after_probe",
|
|
5891
4275
|
requestedProfiles: attempt.numberOfProfiles,
|
|
5892
4276
|
crawlJobId: context?.crawlJobId ?? null,
|
|
@@ -5985,8 +4369,6 @@ const SALES_NAVIGATOR_SPLIT_TRIGGER_RESULTS = 1500;
|
|
|
5985
4369
|
const SALES_NAVIGATOR_FILTER_IMPACT_MIN_OBSERVATIONS = 3;
|
|
5986
4370
|
let salesNavigatorFilterImpactModel = null;
|
|
5987
4371
|
let salesNavigatorFilterImpactLoaded = false;
|
|
5988
|
-
let linkedInProfileHitCache = null;
|
|
5989
|
-
let linkedInProfileHitCacheLoaded = false;
|
|
5990
4372
|
function getSalesprompterConfigDir() {
|
|
5991
4373
|
const override = process.env.SALESPROMPTER_CONFIG_DIR?.trim();
|
|
5992
4374
|
if (override !== undefined && override.length > 0) {
|
|
@@ -5997,76 +4379,6 @@ function getSalesprompterConfigDir() {
|
|
|
5997
4379
|
function getSalesNavigatorFilterImpactPath() {
|
|
5998
4380
|
return path.join(getSalesprompterConfigDir(), "salesnav-filter-impact.json");
|
|
5999
4381
|
}
|
|
6000
|
-
function getLinkedInProfileHitCachePath() {
|
|
6001
|
-
return path.join(getSalesprompterConfigDir(), "linkedin-profile-hits.json");
|
|
6002
|
-
}
|
|
6003
|
-
function buildLinkedInProfileHitCacheKeys(params) {
|
|
6004
|
-
const keys = new Set();
|
|
6005
|
-
const normalizedName = normalizeLooseMatchText(params.fullName);
|
|
6006
|
-
const normalizedCompany = normalizeLooseMatchText(params.companyName);
|
|
6007
|
-
const normalizedEmail = normalizeLookupWhitespace(params.email);
|
|
6008
|
-
const trustedEmail = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail) ? normalizedEmail.toLowerCase() : "";
|
|
6009
|
-
const contactId = normalizeLinkedInLookupField(params.contactId);
|
|
6010
|
-
if (contactId && !/^[1-9]\d?$/.test(contactId)) {
|
|
6011
|
-
keys.add(`contact:${contactId}`);
|
|
6012
|
-
}
|
|
6013
|
-
if (normalizedName && normalizedCompany && trustedEmail) {
|
|
6014
|
-
keys.add(`identity:${normalizedName}|${normalizedCompany}|${trustedEmail}`);
|
|
6015
|
-
}
|
|
6016
|
-
if (normalizedName && normalizedCompany) {
|
|
6017
|
-
keys.add(`identity:${normalizedName}|${normalizedCompany}`);
|
|
6018
|
-
}
|
|
6019
|
-
return Array.from(keys);
|
|
6020
|
-
}
|
|
6021
|
-
async function loadLinkedInProfileHitCache() {
|
|
6022
|
-
if (linkedInProfileHitCacheLoaded) {
|
|
6023
|
-
return linkedInProfileHitCache;
|
|
6024
|
-
}
|
|
6025
|
-
linkedInProfileHitCacheLoaded = true;
|
|
6026
|
-
try {
|
|
6027
|
-
const content = await readFile(getLinkedInProfileHitCachePath(), "utf8");
|
|
6028
|
-
const parsed = JSON.parse(content);
|
|
6029
|
-
if (parsed?.version === 1 && parsed.entries && typeof parsed.entries === "object") {
|
|
6030
|
-
linkedInProfileHitCache = parsed;
|
|
6031
|
-
}
|
|
6032
|
-
}
|
|
6033
|
-
catch {
|
|
6034
|
-
linkedInProfileHitCache = null;
|
|
6035
|
-
}
|
|
6036
|
-
return linkedInProfileHitCache;
|
|
6037
|
-
}
|
|
6038
|
-
async function persistLinkedInProfileHitCache() {
|
|
6039
|
-
if (!linkedInProfileHitCache) {
|
|
6040
|
-
return;
|
|
6041
|
-
}
|
|
6042
|
-
const filePath = getLinkedInProfileHitCachePath();
|
|
6043
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
6044
|
-
await writeFile(filePath, `${JSON.stringify(linkedInProfileHitCache, null, 2)}\n`, "utf8");
|
|
6045
|
-
}
|
|
6046
|
-
function upsertLinkedInProfileHitCacheEntry(params) {
|
|
6047
|
-
if (!params.linkedinUrl && !params.salesNavProfileUrl && !params.linkedinCompanyUrl && !params.salesNavCompanyUrl) {
|
|
6048
|
-
return;
|
|
6049
|
-
}
|
|
6050
|
-
if (!linkedInProfileHitCache) {
|
|
6051
|
-
linkedInProfileHitCache = {
|
|
6052
|
-
version: 1,
|
|
6053
|
-
updatedAt: new Date().toISOString(),
|
|
6054
|
-
entries: {}
|
|
6055
|
-
};
|
|
6056
|
-
}
|
|
6057
|
-
const updatedAt = new Date().toISOString();
|
|
6058
|
-
linkedInProfileHitCache.updatedAt = updatedAt;
|
|
6059
|
-
const entry = {
|
|
6060
|
-
linkedinUrl: params.linkedinUrl,
|
|
6061
|
-
salesNavProfileUrl: params.salesNavProfileUrl,
|
|
6062
|
-
linkedinCompanyUrl: params.linkedinCompanyUrl,
|
|
6063
|
-
salesNavCompanyUrl: params.salesNavCompanyUrl,
|
|
6064
|
-
updatedAt
|
|
6065
|
-
};
|
|
6066
|
-
for (const key of buildLinkedInProfileHitCacheKeys(params)) {
|
|
6067
|
-
linkedInProfileHitCache.entries[key] = entry;
|
|
6068
|
-
}
|
|
6069
|
-
}
|
|
6070
4382
|
async function loadSalesNavigatorFilterImpactModel() {
|
|
6071
4383
|
if (salesNavigatorFilterImpactLoaded) {
|
|
6072
4384
|
return salesNavigatorFilterImpactModel;
|
|
@@ -6339,7 +4651,6 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
6339
4651
|
}, {
|
|
6340
4652
|
crawlJobId: jobId,
|
|
6341
4653
|
crawlSliceId: slice.id,
|
|
6342
|
-
clientId: options.clientId ?? null,
|
|
6343
4654
|
traceId: options.traceId
|
|
6344
4655
|
});
|
|
6345
4656
|
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, {
|
|
@@ -6616,7 +4927,6 @@ async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
|
6616
4927
|
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
6617
4928
|
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
6618
4929
|
claimedSlices: claimedSliceNumber,
|
|
6619
|
-
clientId: options.clientId ?? null,
|
|
6620
4930
|
traceId: options.traceId,
|
|
6621
4931
|
logger: options.logger
|
|
6622
4932
|
}).then((value) => ({ slot, value })));
|
|
@@ -6889,12 +5199,15 @@ async function runWizard(options) {
|
|
|
6889
5199
|
writeWizardLine("Salesprompter");
|
|
6890
5200
|
writeWizardLine("Start with a company website, LinkedIn product page, or category URL. I will guide you from there.");
|
|
6891
5201
|
writeWizardLine();
|
|
6892
|
-
await ensureWizardSession(options);
|
|
6893
5202
|
const rl = createInterface({
|
|
6894
5203
|
input: process.stdin,
|
|
6895
5204
|
output: process.stdout
|
|
6896
5205
|
});
|
|
6897
5206
|
try {
|
|
5207
|
+
const wizardSession = await ensureWizardSession(options);
|
|
5208
|
+
if (wizardSession.session && wizardSession.restoredFromCache) {
|
|
5209
|
+
await confirmWizardWorkspace(rl, wizardSession.session, options);
|
|
5210
|
+
}
|
|
6898
5211
|
const flow = await promptChoice(rl, "What do you want help with?", [
|
|
6899
5212
|
{
|
|
6900
5213
|
value: "product-market",
|
|
@@ -7352,7 +5665,6 @@ program
|
|
|
7352
5665
|
const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
|
|
7353
5666
|
const cleanedCompanyMap = await buildCompanyNameCleaningMap(rows, companyCleaningMode);
|
|
7354
5667
|
const contacts = toLinkedInUrlLookupContacts(rows, cleanedCompanyMap);
|
|
7355
|
-
await loadLinkedInProfileHitCache();
|
|
7356
5668
|
if (options.dryRun) {
|
|
7357
5669
|
const payload = {
|
|
7358
5670
|
status: "ok",
|
|
@@ -7368,70 +5680,79 @@ program
|
|
|
7368
5680
|
printOutput(payload);
|
|
7369
5681
|
return;
|
|
7370
5682
|
}
|
|
7371
|
-
const
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
timeoutMs
|
|
5683
|
+
const enrichedRows = await resolveLinkedInUrlsFromSalesNavRows({
|
|
5684
|
+
rows,
|
|
5685
|
+
orgId: String(options.orgId ?? "").trim() || undefined
|
|
7375
5686
|
});
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
continue;
|
|
5687
|
+
let directAttempted = false;
|
|
5688
|
+
let workflowAttempted = false;
|
|
5689
|
+
const missingRows = enrichedRows.filter((row) => !row.found);
|
|
5690
|
+
if (missingRows.length > 0) {
|
|
5691
|
+
const directContacts = contacts.filter((contact) => missingRows.some((row) => row.contactId === contact.contact_id));
|
|
5692
|
+
let linkedInUrlByContactId = new Map();
|
|
5693
|
+
try {
|
|
5694
|
+
directAttempted = true;
|
|
5695
|
+
const result = await invokeLinkedInUrlEnrichmentDirect({
|
|
5696
|
+
contacts: directContacts,
|
|
5697
|
+
timeoutMs
|
|
5698
|
+
});
|
|
5699
|
+
linkedInUrlByContactId = new Map(result.contacts.map((contact) => [
|
|
5700
|
+
contact.contact_id,
|
|
5701
|
+
{
|
|
5702
|
+
linkedinUrl: contact.linkedin_url ?? null,
|
|
5703
|
+
salesNavProfileUrl: contact.sales_nav_profile_url ?? null,
|
|
5704
|
+
linkedinCompanyUrl: null,
|
|
5705
|
+
salesNavCompanyUrl: null
|
|
5706
|
+
}
|
|
5707
|
+
]));
|
|
5708
|
+
for (const row of enrichedRows) {
|
|
5709
|
+
if (row.found)
|
|
5710
|
+
continue;
|
|
5711
|
+
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
5712
|
+
if (profile?.linkedinUrl) {
|
|
5713
|
+
row.linkedinUrl = profile.linkedinUrl;
|
|
5714
|
+
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
5715
|
+
row.found = true;
|
|
5716
|
+
row.source = "linkedin-direct";
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
7409
5719
|
}
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
5720
|
+
catch (error) {
|
|
5721
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5722
|
+
if (!/Missing LinkedIn direct lookup session/i.test(message)) {
|
|
5723
|
+
throw error;
|
|
5724
|
+
}
|
|
5725
|
+
workflowAttempted = true;
|
|
5726
|
+
const workflow = await invokeLinkedInUrlEnrichmentWorkflow({
|
|
5727
|
+
contacts: directContacts,
|
|
5728
|
+
externalUserId: String(options.orgId ?? "").trim() || sessionOrgId || "cli_direct_lookup",
|
|
5729
|
+
timeoutMs
|
|
5730
|
+
});
|
|
5731
|
+
if (!workflow.response.ok) {
|
|
5732
|
+
throw new Error(`LinkedIn enrichment workflow returned ${workflow.response.status}: ${workflow.bodyText.slice(0, 300)}`);
|
|
5733
|
+
}
|
|
5734
|
+
linkedInUrlByContactId = normalizeWorkflowLinkedInUrlResult({
|
|
5735
|
+
parsedBody: workflow.parsedBody,
|
|
5736
|
+
contacts: directContacts
|
|
5737
|
+
});
|
|
5738
|
+
for (const row of enrichedRows) {
|
|
5739
|
+
if (row.found)
|
|
5740
|
+
continue;
|
|
5741
|
+
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
5742
|
+
if (profile?.linkedinUrl) {
|
|
5743
|
+
row.linkedinUrl = profile.linkedinUrl;
|
|
5744
|
+
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
5745
|
+
row.linkedinCompanyUrl = profile.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
5746
|
+
row.salesNavCompanyUrl = profile.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
5747
|
+
row.found = true;
|
|
5748
|
+
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
5749
|
+
row.source = "workflow";
|
|
5750
|
+
row.companySource =
|
|
5751
|
+
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "workflow" : row.companySource ?? null;
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
7422
5754
|
}
|
|
7423
|
-
row.linkedinUrl = cachedEntry.linkedinUrl ?? row.linkedinUrl ?? null;
|
|
7424
|
-
row.salesNavProfileUrl = cachedEntry.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
7425
|
-
row.linkedinCompanyUrl = cachedEntry.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
7426
|
-
row.salesNavCompanyUrl = cachedEntry.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
7427
|
-
row.found = Boolean(row.linkedinUrl || row.salesNavProfileUrl);
|
|
7428
|
-
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
7429
|
-
row.source = row.found ? "cache" : row.source;
|
|
7430
|
-
row.companySource =
|
|
7431
|
-
row.companyFound && !row.companySource ? "cache" : row.companySource;
|
|
7432
5755
|
}
|
|
7433
|
-
let directAttempted = false;
|
|
7434
|
-
let workflowAttempted = false;
|
|
7435
5756
|
const parsedClientIds = Array.from(new Set(rows
|
|
7436
5757
|
.map((row) => Number(row.clientId))
|
|
7437
5758
|
.filter((value) => Number.isFinite(value) && value > 0)));
|
|
@@ -7478,266 +5799,38 @@ program
|
|
|
7478
5799
|
writeProgress(`Skipping app-backed company enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
7479
5800
|
}
|
|
7480
5801
|
}
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
contacts: contactsMissingCompanyUrl,
|
|
7486
|
-
timeoutMs: Math.min(timeoutMs, 15_000),
|
|
7487
|
-
concurrency: strategy.bulkMode ? 6 : 3,
|
|
7488
|
-
overallBudgetMs: strategy.bulkMode ? 20_000 : 10_000
|
|
7489
|
-
});
|
|
7490
|
-
for (const row of enrichedRows) {
|
|
7491
|
-
if (row.linkedinCompanyUrl) {
|
|
7492
|
-
continue;
|
|
7493
|
-
}
|
|
7494
|
-
const linkedinCompanyUrl = companyUrlByContactId.get(row.contactId);
|
|
7495
|
-
if (!linkedinCompanyUrl) {
|
|
7496
|
-
continue;
|
|
7497
|
-
}
|
|
7498
|
-
row.linkedinCompanyUrl = linkedinCompanyUrl;
|
|
7499
|
-
row.companyFound = true;
|
|
7500
|
-
row.companySource = "web-search";
|
|
7501
|
-
}
|
|
7502
|
-
}
|
|
7503
|
-
const missingRows = enrichedRows.filter((row) => !row.found);
|
|
7504
|
-
const useDirectPeopleLookup = !strategy.bulkMode &&
|
|
7505
|
-
shouldUseDirectPeopleLookup({
|
|
7506
|
-
rowCount: missingRows.length
|
|
7507
|
-
});
|
|
7508
|
-
const useWorkflowPeopleLookup = !strategy.bulkMode &&
|
|
7509
|
-
shouldUseWorkflowPeopleLookup({
|
|
7510
|
-
rowCount: missingRows.length
|
|
7511
|
-
});
|
|
7512
|
-
if (missingRows.length > 0) {
|
|
7513
|
-
const rowByContactId = new Map(enrichedRows.map((row) => [row.contactId, row]));
|
|
7514
|
-
const directContacts = contacts
|
|
7515
|
-
.filter((contact) => missingRows.some((row) => row.contactId === contact.contact_id))
|
|
7516
|
-
.map((contact) => {
|
|
7517
|
-
const row = rowByContactId.get(contact.contact_id);
|
|
7518
|
-
if (!row) {
|
|
7519
|
-
return contact;
|
|
7520
|
-
}
|
|
7521
|
-
return {
|
|
7522
|
-
...contact,
|
|
7523
|
-
linkedinCompanyUrl: row.linkedinCompanyUrl ?? contact.linkedinCompanyUrl,
|
|
7524
|
-
companyNameOriginal: row.matchedCompanyName ?? contact.companyNameOriginal,
|
|
7525
|
-
companyName: row.matchedCompanyName && normalizeLookupCompanyForSearch(row.matchedCompanyName)
|
|
7526
|
-
? normalizeLookupCompanyForSearch(row.matchedCompanyName)
|
|
7527
|
-
: contact.companyName
|
|
7528
|
-
};
|
|
5802
|
+
try {
|
|
5803
|
+
const companyResult = await invokeLinkedInCompanyEnrichmentDirect({
|
|
5804
|
+
contacts,
|
|
5805
|
+
timeoutMs
|
|
7529
5806
|
});
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
});
|
|
7538
|
-
const directCompanyContextByKey = new Map((result.companyContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
7539
|
-
linkedInUrlByContactId = new Map(result.contacts.map((contact) => [
|
|
7540
|
-
contact.contact_id,
|
|
7541
|
-
{
|
|
7542
|
-
linkedinUrl: contact.linkedin_url ?? null,
|
|
7543
|
-
salesNavProfileUrl: contact.sales_nav_profile_url ?? null,
|
|
7544
|
-
linkedinCompanyUrl: null,
|
|
7545
|
-
salesNavCompanyUrl: null,
|
|
7546
|
-
matchedFullName: contact.matched_full_name ?? null,
|
|
7547
|
-
matchedCompanyName: contact.matched_company_name ?? null,
|
|
7548
|
-
matchedTitle: contact.matched_title ?? null
|
|
7549
|
-
}
|
|
7550
|
-
]));
|
|
7551
|
-
for (const row of enrichedRows) {
|
|
7552
|
-
if (row.found)
|
|
7553
|
-
continue;
|
|
7554
|
-
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
7555
|
-
if (profile?.linkedinUrl) {
|
|
7556
|
-
row.linkedinUrl = profile.linkedinUrl;
|
|
7557
|
-
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
7558
|
-
row.found = true;
|
|
7559
|
-
row.source = "linkedin-direct";
|
|
7560
|
-
row.matchedFullName = profile.matchedFullName ?? row.matchedFullName ?? null;
|
|
7561
|
-
row.matchedCompanyName = profile.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
7562
|
-
row.matchedTitle = profile.matchedTitle ?? row.matchedTitle ?? null;
|
|
7563
|
-
}
|
|
7564
|
-
const directContact = directContacts.find((candidate) => candidate.contact_id === row.contactId && !candidate.isVariation);
|
|
7565
|
-
const companyContext = directContact
|
|
7566
|
-
? directCompanyContextByKey.get(buildDirectCompanyContextKey(directContact))
|
|
7567
|
-
: null;
|
|
7568
|
-
if (companyContext && !row.linkedinCompanyUrl) {
|
|
7569
|
-
row.linkedinCompanyUrl = companyContext.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
7570
|
-
row.salesNavCompanyUrl = companyContext.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
7571
|
-
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
7572
|
-
row.companySource =
|
|
7573
|
-
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
7574
|
-
row.matchedCompanyName = companyContext.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
7575
|
-
row.matchedCompanyEmployeeCount =
|
|
7576
|
-
companyContext.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
7577
|
-
}
|
|
7578
|
-
}
|
|
7579
|
-
const contactsStillMissingCompany = contacts.filter((contact) => !contact.isVariation &&
|
|
7580
|
-
enrichedRows.some((row) => row.contactId === contact.contact_id && !row.linkedinCompanyUrl && !row.salesNavCompanyUrl));
|
|
7581
|
-
if (contactsStillMissingCompany.length > 0) {
|
|
7582
|
-
const companyResult = await invokeLinkedInCompanyEnrichmentDirect({
|
|
7583
|
-
contacts: contactsStillMissingCompany,
|
|
7584
|
-
timeoutMs,
|
|
7585
|
-
precomputedContexts: result.companyContexts
|
|
7586
|
-
});
|
|
7587
|
-
const companyByContactId = new Map(companyResult.contacts.map((contact) => [
|
|
7588
|
-
contact.contact_id,
|
|
7589
|
-
{
|
|
7590
|
-
linkedinCompanyUrl: contact.linkedin_company_url ?? null,
|
|
7591
|
-
salesNavCompanyUrl: contact.sales_nav_company_url ?? null,
|
|
7592
|
-
matchedCompanyName: contact.matched_company_name ?? null,
|
|
7593
|
-
matchedCompanyEmployeeCount: contact.matched_company_employee_count ?? null
|
|
7594
|
-
}
|
|
7595
|
-
]));
|
|
7596
|
-
for (const row of enrichedRows) {
|
|
7597
|
-
const company = companyByContactId.get(row.contactId);
|
|
7598
|
-
if (!company || row.linkedinCompanyUrl) {
|
|
7599
|
-
continue;
|
|
7600
|
-
}
|
|
7601
|
-
row.linkedinCompanyUrl = company.linkedinCompanyUrl;
|
|
7602
|
-
row.salesNavCompanyUrl = company.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
7603
|
-
row.companyFound = Boolean(company.linkedinCompanyUrl || company.salesNavCompanyUrl);
|
|
7604
|
-
row.companySource =
|
|
7605
|
-
company.linkedinCompanyUrl || company.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
7606
|
-
row.matchedCompanyName = company.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
7607
|
-
row.matchedCompanyEmployeeCount =
|
|
7608
|
-
company.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
7609
|
-
}
|
|
7610
|
-
}
|
|
7611
|
-
}
|
|
7612
|
-
catch (error) {
|
|
7613
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7614
|
-
if (!/Missing LinkedIn direct lookup session/i.test(message)) {
|
|
7615
|
-
throw error;
|
|
7616
|
-
}
|
|
7617
|
-
}
|
|
7618
|
-
}
|
|
7619
|
-
const stillMissingAfterDirect = enrichedRows.filter((row) => !row.found);
|
|
7620
|
-
const contactsStillMissing = directContacts.filter((contact) => stillMissingAfterDirect.some((row) => row.contactId === contact.contact_id));
|
|
7621
|
-
if (contactsStillMissing.length > 0 && useWorkflowPeopleLookup) {
|
|
7622
|
-
workflowAttempted = true;
|
|
7623
|
-
try {
|
|
7624
|
-
const workflow = await invokeLinkedInUrlEnrichmentWorkflow({
|
|
7625
|
-
contacts: contactsStillMissing,
|
|
7626
|
-
externalUserId: orgId || sessionOrgId || "cli_direct_lookup",
|
|
7627
|
-
timeoutMs: Math.min(timeoutMs, strategy.workflowStageBudgetMs)
|
|
7628
|
-
});
|
|
7629
|
-
if (!workflow.response.ok) {
|
|
7630
|
-
throw new Error(`LinkedIn enrichment workflow returned ${workflow.response.status}: ${workflow.bodyText.slice(0, 300)}`);
|
|
7631
|
-
}
|
|
7632
|
-
linkedInUrlByContactId = normalizeWorkflowLinkedInUrlResult({
|
|
7633
|
-
parsedBody: workflow.parsedBody,
|
|
7634
|
-
contacts: contactsStillMissing
|
|
7635
|
-
});
|
|
7636
|
-
for (const row of enrichedRows) {
|
|
7637
|
-
if (row.found)
|
|
7638
|
-
continue;
|
|
7639
|
-
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
7640
|
-
if (profile?.linkedinUrl) {
|
|
7641
|
-
row.linkedinUrl = profile.linkedinUrl;
|
|
7642
|
-
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
7643
|
-
row.linkedinCompanyUrl = profile.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
7644
|
-
row.salesNavCompanyUrl = profile.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
7645
|
-
row.found = true;
|
|
7646
|
-
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
7647
|
-
row.source = "workflow";
|
|
7648
|
-
row.companySource =
|
|
7649
|
-
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "workflow" : row.companySource ?? null;
|
|
7650
|
-
}
|
|
7651
|
-
}
|
|
7652
|
-
}
|
|
7653
|
-
catch (error) {
|
|
7654
|
-
writeProgress(`Skipping workflow profile enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
5807
|
+
const companyByContactId = new Map(companyResult.contacts.map((contact) => [
|
|
5808
|
+
contact.contact_id,
|
|
5809
|
+
{
|
|
5810
|
+
linkedinCompanyUrl: contact.linkedin_company_url ?? null,
|
|
5811
|
+
salesNavCompanyUrl: contact.sales_nav_company_url ?? null,
|
|
5812
|
+
matchedCompanyName: contact.matched_company_name ?? null,
|
|
5813
|
+
matchedCompanyEmployeeCount: contact.matched_company_employee_count ?? null
|
|
7655
5814
|
}
|
|
7656
|
-
|
|
7657
|
-
const serperContacts = directContacts.filter((contact) => enrichedRows.some((row) => row.contactId === contact.contact_id && !row.found));
|
|
7658
|
-
if (strategy.bulkMode && serperContacts.length > 0) {
|
|
7659
|
-
writeProgress(`Using bulk profile resolution strategy for ${serperContacts.length} remaining contacts.`);
|
|
7660
|
-
}
|
|
7661
|
-
const serperResults = await resolveSerperLinkedInProfilesInParallel({
|
|
7662
|
-
contacts: serperContacts.filter((contact) => !contact.isVariation),
|
|
7663
|
-
timeoutMs,
|
|
7664
|
-
concurrency: Math.min(strategy.serperConcurrency, serperContacts.length || 1),
|
|
7665
|
-
maxQueries: strategy.serperMaxQueries,
|
|
7666
|
-
overallBudgetMs: strategy.serperStageBudgetMs
|
|
7667
|
-
});
|
|
5815
|
+
]));
|
|
7668
5816
|
for (const row of enrichedRows) {
|
|
7669
|
-
|
|
5817
|
+
const company = companyByContactId.get(row.contactId);
|
|
5818
|
+
if (!company || row.linkedinCompanyUrl) {
|
|
7670
5819
|
continue;
|
|
7671
|
-
const linkedinUrl = serperResults.get(row.contactId);
|
|
7672
|
-
if (!linkedinUrl)
|
|
7673
|
-
continue;
|
|
7674
|
-
row.linkedinUrl = linkedinUrl;
|
|
7675
|
-
row.found = true;
|
|
7676
|
-
row.source = "web-search";
|
|
7677
|
-
}
|
|
7678
|
-
const stillMissingAfterSerper = enrichedRows.filter((row) => !row.found);
|
|
7679
|
-
if (shouldAttemptBulkDirectProfileLookup({
|
|
7680
|
-
strategy,
|
|
7681
|
-
unresolvedRowCount: stillMissingAfterSerper.length
|
|
7682
|
-
})) {
|
|
7683
|
-
const bulkDirectCandidates = rankContactsForBulkDirectProfileLookup({
|
|
7684
|
-
contacts: directContacts.filter((contact) => stillMissingAfterSerper.some((row) => row.contactId === contact.contact_id)),
|
|
7685
|
-
rowsByContactId: rowByContactId,
|
|
7686
|
-
limit: strategy.bulkDirectProfileMaxRows
|
|
7687
|
-
});
|
|
7688
|
-
if (bulkDirectCandidates.length > 0) {
|
|
7689
|
-
writeProgress(`Using bulk direct profile follow-up for ${bulkDirectCandidates.length} high-signal unresolved contacts.`);
|
|
7690
|
-
try {
|
|
7691
|
-
directAttempted = true;
|
|
7692
|
-
const result = await invokeLinkedInUrlEnrichmentDirect({
|
|
7693
|
-
contacts: bulkDirectCandidates,
|
|
7694
|
-
timeoutMs: strategy.bulkDirectProfileTimeoutMs,
|
|
7695
|
-
perAttemptTimeoutMs: Math.min(strategy.bulkDirectProfileTimeoutMs, 2_500),
|
|
7696
|
-
perContactBudgetMs: strategy.bulkDirectProfileTimeoutMs
|
|
7697
|
-
});
|
|
7698
|
-
const directCompanyContextByKey = new Map((result.companyContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
7699
|
-
const bulkDirectByContactId = new Map(result.contacts.map((contact) => [
|
|
7700
|
-
contact.contact_id,
|
|
7701
|
-
{
|
|
7702
|
-
linkedinUrl: contact.linkedin_url ?? null,
|
|
7703
|
-
salesNavProfileUrl: contact.sales_nav_profile_url ?? null
|
|
7704
|
-
}
|
|
7705
|
-
]));
|
|
7706
|
-
for (const row of enrichedRows) {
|
|
7707
|
-
if (row.found)
|
|
7708
|
-
continue;
|
|
7709
|
-
const profile = bulkDirectByContactId.get(row.contactId);
|
|
7710
|
-
if (profile?.linkedinUrl) {
|
|
7711
|
-
row.linkedinUrl = profile.linkedinUrl;
|
|
7712
|
-
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
7713
|
-
row.found = true;
|
|
7714
|
-
row.source = "linkedin-direct";
|
|
7715
|
-
}
|
|
7716
|
-
const directContact = bulkDirectCandidates.find((candidate) => candidate.contact_id === row.contactId && !candidate.isVariation);
|
|
7717
|
-
const companyContext = directContact
|
|
7718
|
-
? directCompanyContextByKey.get(buildDirectCompanyContextKey(directContact))
|
|
7719
|
-
: null;
|
|
7720
|
-
if (companyContext && !row.linkedinCompanyUrl) {
|
|
7721
|
-
row.linkedinCompanyUrl = companyContext.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
7722
|
-
row.salesNavCompanyUrl = companyContext.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
7723
|
-
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
7724
|
-
row.companySource =
|
|
7725
|
-
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
7726
|
-
row.matchedCompanyName = companyContext.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
7727
|
-
row.matchedCompanyEmployeeCount =
|
|
7728
|
-
companyContext.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
7729
|
-
}
|
|
7730
|
-
}
|
|
7731
|
-
}
|
|
7732
|
-
catch (error) {
|
|
7733
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7734
|
-
if (!/Missing LinkedIn direct lookup session/i.test(message)) {
|
|
7735
|
-
writeProgress(`Skipping bulk direct profile follow-up: ${message}`);
|
|
7736
|
-
}
|
|
7737
|
-
}
|
|
7738
5820
|
}
|
|
5821
|
+
row.linkedinCompanyUrl = company.linkedinCompanyUrl;
|
|
5822
|
+
row.salesNavCompanyUrl = company.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
5823
|
+
row.companyFound = Boolean(company.linkedinCompanyUrl || company.salesNavCompanyUrl);
|
|
5824
|
+
row.companySource =
|
|
5825
|
+
company.linkedinCompanyUrl || company.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
5826
|
+
row.matchedCompanyName = company.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
5827
|
+
row.matchedCompanyEmployeeCount =
|
|
5828
|
+
company.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
7739
5829
|
}
|
|
7740
5830
|
}
|
|
5831
|
+
catch (error) {
|
|
5832
|
+
writeProgress(`Skipping separate company enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
5833
|
+
}
|
|
7741
5834
|
const payload = {
|
|
7742
5835
|
status: "ok",
|
|
7743
5836
|
orgId: String(options.orgId ?? "").trim() || null,
|
|
@@ -7746,23 +5839,8 @@ program
|
|
|
7746
5839
|
companiesFound: enrichedRows.filter((row) => row.companyFound).length,
|
|
7747
5840
|
directAttempted,
|
|
7748
5841
|
workflowAttempted,
|
|
7749
|
-
bulkMode: strategy.bulkMode,
|
|
7750
5842
|
rows: enrichedRows
|
|
7751
5843
|
};
|
|
7752
|
-
for (const row of enrichedRows) {
|
|
7753
|
-
const contact = contactById.get(row.contactId);
|
|
7754
|
-
upsertLinkedInProfileHitCacheEntry({
|
|
7755
|
-
fullName: row.fullName,
|
|
7756
|
-
companyName: row.companyName,
|
|
7757
|
-
email: contact?.email,
|
|
7758
|
-
contactId: row.contactId,
|
|
7759
|
-
linkedinUrl: row.linkedinUrl ?? null,
|
|
7760
|
-
salesNavProfileUrl: row.salesNavProfileUrl ?? null,
|
|
7761
|
-
linkedinCompanyUrl: row.linkedinCompanyUrl ?? null,
|
|
7762
|
-
salesNavCompanyUrl: row.salesNavCompanyUrl ?? null
|
|
7763
|
-
});
|
|
7764
|
-
}
|
|
7765
|
-
await persistLinkedInProfileHitCache();
|
|
7766
5844
|
if (options.out) {
|
|
7767
5845
|
await writeJsonFile(options.out, payload);
|
|
7768
5846
|
}
|
|
@@ -8100,14 +6178,12 @@ program
|
|
|
8100
6178
|
});
|
|
8101
6179
|
program
|
|
8102
6180
|
.command("leads:generate")
|
|
8103
|
-
.description("Generate leads
|
|
6181
|
+
.description("Generate leads for a target account or from fallback seeds.")
|
|
8104
6182
|
.requiredOption("--icp <path>", "Path to ICP JSON")
|
|
8105
6183
|
.option("--count <number>", "Number of leads to generate", "10")
|
|
8106
6184
|
.option("--domain <domain>", "Target a specific company domain like company.com")
|
|
8107
6185
|
.option("--company-domain <domain>", "Deprecated alias for --domain")
|
|
8108
6186
|
.option("--company-name <name>", "Optional company name override for a targeted domain")
|
|
8109
|
-
.option("--linkedin-company-page <url>", "LinkedIn company page to target when the domain is unknown")
|
|
8110
|
-
.option("--source <source>", "auto|workspace|fallback", "auto")
|
|
8111
6187
|
.requiredOption("--out <path>", "Output file path")
|
|
8112
6188
|
.action(async (options) => {
|
|
8113
6189
|
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
@@ -8115,15 +6191,9 @@ program
|
|
|
8115
6191
|
const domain = options.domain ?? options.companyDomain;
|
|
8116
6192
|
const target = {
|
|
8117
6193
|
companyDomain: domain,
|
|
8118
|
-
companyName: options.companyName
|
|
8119
|
-
linkedinCompanyPage: options.linkedinCompanyPage
|
|
6194
|
+
companyName: options.companyName
|
|
8120
6195
|
};
|
|
8121
|
-
const result = await
|
|
8122
|
-
icp,
|
|
8123
|
-
count,
|
|
8124
|
-
target,
|
|
8125
|
-
source: options.source
|
|
8126
|
-
});
|
|
6196
|
+
const result = await leadProvider.generateLeads(icp, count, target);
|
|
8127
6197
|
await writeJsonFile(options.out, result.leads);
|
|
8128
6198
|
printOutput({
|
|
8129
6199
|
status: "ok",
|
|
@@ -8168,8 +6238,6 @@ program
|
|
|
8168
6238
|
.option("--domain <domain>", "Target a specific company domain like company.com")
|
|
8169
6239
|
.option("--company-domain <domain>", "Deprecated alias for --domain")
|
|
8170
6240
|
.option("--company-name <name>", "Optional company name override for a targeted domain")
|
|
8171
|
-
.option("--linkedin-company-page <url>", "LinkedIn company page to target when the domain is unknown")
|
|
8172
|
-
.option("--source <source>", "auto|workspace|fallback", "auto")
|
|
8173
6241
|
.option("--out-prefix <path>", "Output path prefix (writes <prefix>-leads.json, <prefix>-enriched.json, <prefix>-scored.json)", "./data/leads-pipeline")
|
|
8174
6242
|
.action(async (options) => {
|
|
8175
6243
|
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
@@ -8177,19 +6245,13 @@ program
|
|
|
8177
6245
|
const domain = options.domain ?? options.companyDomain;
|
|
8178
6246
|
const target = {
|
|
8179
6247
|
companyDomain: domain,
|
|
8180
|
-
companyName: options.companyName
|
|
8181
|
-
linkedinCompanyPage: options.linkedinCompanyPage
|
|
6248
|
+
companyName: options.companyName
|
|
8182
6249
|
};
|
|
8183
6250
|
const outPrefix = String(options.outPrefix);
|
|
8184
6251
|
const leadsOut = `${outPrefix}-leads.json`;
|
|
8185
6252
|
const enrichedOut = `${outPrefix}-enriched.json`;
|
|
8186
6253
|
const scoredOut = `${outPrefix}-scored.json`;
|
|
8187
|
-
const generated = await
|
|
8188
|
-
icp,
|
|
8189
|
-
count,
|
|
8190
|
-
target,
|
|
8191
|
-
source: options.source
|
|
8192
|
-
});
|
|
6254
|
+
const generated = await leadProvider.generateLeads(icp, count, target);
|
|
8193
6255
|
await writeJsonFile(leadsOut, generated.leads);
|
|
8194
6256
|
const enriched = await enrichmentProvider.enrichLeads(generated.leads);
|
|
8195
6257
|
await writeJsonFile(enrichedOut, enriched);
|
|
@@ -8927,7 +6989,6 @@ program
|
|
|
8927
6989
|
.option("--max-results-per-search <number>", "Maximum results allowed for a sliced search", "2500")
|
|
8928
6990
|
.option("--number-of-profiles <number>", "Profiles to export per sliced query", "2500")
|
|
8929
6991
|
.option("--slice-preset <name>", "Slice preset label stored with the export runs", "human-resources-crawl")
|
|
8930
|
-
.option("--client-id <number>", "Client id used to generate and store the legacy Neon lead list projection")
|
|
8931
6992
|
.option("--max-split-depth <number>", "Maximum number of adaptive split dimensions to use", "6")
|
|
8932
6993
|
.option("--max-slices <number>", "Safety cap for total claimed slices in this invocation", "1000")
|
|
8933
6994
|
.option("--max-retries <number>", "Retries for non-splitting export failures", "3")
|
|
@@ -8946,7 +7007,6 @@ program
|
|
|
8946
7007
|
const jobId = z.string().uuid().optional().parse(options.jobId);
|
|
8947
7008
|
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
8948
7009
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
8949
|
-
const clientId = parseOptionalSalesNavigatorClientId(options.clientId);
|
|
8950
7010
|
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
8951
7011
|
const maxSlices = z.coerce.number().int().min(1).max(10000).parse(options.maxSlices);
|
|
8952
7012
|
const maxRetries = z.coerce.number().int().min(0).max(5).parse(options.maxRetries);
|
|
@@ -8966,7 +7026,6 @@ program
|
|
|
8966
7026
|
jobId: jobId ?? null,
|
|
8967
7027
|
maxResultsPerSearch,
|
|
8968
7028
|
numberOfProfiles,
|
|
8969
|
-
clientId,
|
|
8970
7029
|
slicePreset: options.slicePreset,
|
|
8971
7030
|
maxSplitDepth,
|
|
8972
7031
|
maxSlices,
|
|
@@ -9067,7 +7126,6 @@ program
|
|
|
9067
7126
|
traceId: logger.traceId,
|
|
9068
7127
|
command: {
|
|
9069
7128
|
sourceQueryUrl: queryUrl,
|
|
9070
|
-
clientId,
|
|
9071
7129
|
slicePreset: options.slicePreset,
|
|
9072
7130
|
maxResultsPerSearch,
|
|
9073
7131
|
numberOfProfiles,
|
|
@@ -9089,7 +7147,6 @@ program
|
|
|
9089
7147
|
splitTrail: seed.splitTrail,
|
|
9090
7148
|
rawPayload: {
|
|
9091
7149
|
workflow: "salesnav:crawl",
|
|
9092
|
-
clientId,
|
|
9093
7150
|
traceId: logger.traceId
|
|
9094
7151
|
}
|
|
9095
7152
|
}
|
|
@@ -9129,7 +7186,6 @@ program
|
|
|
9129
7186
|
idlePollSeconds,
|
|
9130
7187
|
idleMaxPolls,
|
|
9131
7188
|
parallelExports,
|
|
9132
|
-
clientId,
|
|
9133
7189
|
traceId: logger.traceId,
|
|
9134
7190
|
logger
|
|
9135
7191
|
});
|
|
@@ -9210,43 +7266,6 @@ program
|
|
|
9210
7266
|
recentEvents
|
|
9211
7267
|
});
|
|
9212
7268
|
});
|
|
9213
|
-
program
|
|
9214
|
-
.command("phantombuster:containers:sync")
|
|
9215
|
-
.alias("pb:containers:sync")
|
|
9216
|
-
.description("Fetch Phantombuster containers for configured agents and store them in Neon.")
|
|
9217
|
-
.option("--agent-id <id>", "Phantombuster agent id to sync. Repeat to sync multiple agents.", collectStringOptionValue, [])
|
|
9218
|
-
.option("--limit <number>", "Maximum containers to fetch per Phantombuster page", "100")
|
|
9219
|
-
.option("--max-pages <number>", "Maximum Phantombuster pages to fetch per agent", "50")
|
|
9220
|
-
.option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
|
|
9221
|
-
.option("--before-ended-at <iso>", "Only fetch containers that ended before this ISO timestamp")
|
|
9222
|
-
.option("--metadata-only", "Store container metadata without fetching output and result objects", false)
|
|
9223
|
-
.option("--out <path>", "Optional local JSON output path")
|
|
9224
|
-
.action(async (options) => {
|
|
9225
|
-
const agentIds = z.array(z.string().min(1)).parse(options.agentId);
|
|
9226
|
-
const limit = z.coerce.number().int().min(1).max(500).parse(options.limit);
|
|
9227
|
-
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
9228
|
-
const mode = z.enum(["all", "finalized"]).parse(options.mode);
|
|
9229
|
-
const beforeEndedAt = options.beforeEndedAt
|
|
9230
|
-
? z.string().datetime().parse(options.beforeEndedAt)
|
|
9231
|
-
: undefined;
|
|
9232
|
-
const session = await requireAuthSession();
|
|
9233
|
-
const result = await syncPhantombusterContainersViaApp(session, {
|
|
9234
|
-
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
9235
|
-
limit,
|
|
9236
|
-
maxPages,
|
|
9237
|
-
mode,
|
|
9238
|
-
beforeEndedAt,
|
|
9239
|
-
includeResults: !options.metadataOnly
|
|
9240
|
-
});
|
|
9241
|
-
const payload = {
|
|
9242
|
-
...result,
|
|
9243
|
-
dryRun: false
|
|
9244
|
-
};
|
|
9245
|
-
if (options.out) {
|
|
9246
|
-
await writeJsonFile(options.out, payload);
|
|
9247
|
-
}
|
|
9248
|
-
printOutput(payload);
|
|
9249
|
-
});
|
|
9250
7269
|
program
|
|
9251
7270
|
.command("salesnav:export")
|
|
9252
7271
|
.alias("search:export")
|
|
@@ -9255,18 +7274,12 @@ program
|
|
|
9255
7274
|
.option("--max-results-per-search <number>", "Maximum results allowed for a sliced search", "2500")
|
|
9256
7275
|
.option("--number-of-profiles <number>", "Profiles to export per sliced query", "2500")
|
|
9257
7276
|
.option("--slice-preset <name>", "Slice preset label stored with the export run", "human-resources-default")
|
|
9258
|
-
.option("--client-id <number>", "Client id used to generate and store the legacy Neon lead list projection")
|
|
9259
|
-
.option("--agent-busy-wait-seconds <number>", "Seconds to wait before retrying when the export agent is already busy", "30")
|
|
9260
|
-
.option("--agent-busy-max-waits <number>", "How many busy-agent waits to tolerate before failing the export", "20")
|
|
9261
7277
|
.option("--out <path>", "Optional local JSON output path")
|
|
9262
7278
|
.option("--dry-run", "Only generate sliced query URLs without exporting them", false)
|
|
9263
7279
|
.action(async (options) => {
|
|
9264
7280
|
const queryUrls = z.array(z.string().url()).min(1).parse(options.queryUrl);
|
|
9265
7281
|
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
9266
7282
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9267
|
-
const agentBusyWaitSeconds = z.coerce.number().int().min(1).max(300).parse(options.agentBusyWaitSeconds);
|
|
9268
|
-
const agentBusyMaxWaits = z.coerce.number().int().min(0).max(100).parse(options.agentBusyMaxWaits);
|
|
9269
|
-
const clientId = parseOptionalSalesNavigatorClientId(options.clientId);
|
|
9270
7283
|
const prepared = queryUrls.map((queryUrl) => buildSalesNavigatorPeopleSlice(queryUrl));
|
|
9271
7284
|
const effectiveDryRun = Boolean(options.dryRun || shouldBypassAuth());
|
|
9272
7285
|
if (effectiveDryRun) {
|
|
@@ -9288,10 +7301,10 @@ program
|
|
|
9288
7301
|
printOutput(payload);
|
|
9289
7302
|
return;
|
|
9290
7303
|
}
|
|
9291
|
-
|
|
7304
|
+
const session = await requireAuthSession();
|
|
9292
7305
|
const exported = [];
|
|
9293
7306
|
for (const item of prepared) {
|
|
9294
|
-
const result = await
|
|
7307
|
+
const result = await runSalesNavigatorExport(session, {
|
|
9295
7308
|
sourceQueryUrl: item.sourceQueryUrl,
|
|
9296
7309
|
slicedQueryUrl: item.slicedQueryUrl,
|
|
9297
7310
|
appliedFilters: item.appliedFilters,
|
|
@@ -9300,17 +7313,12 @@ program
|
|
|
9300
7313
|
slicePreset: options.slicePreset,
|
|
9301
7314
|
rawPayload: {
|
|
9302
7315
|
workflow: "salesnav:export",
|
|
9303
|
-
clientId,
|
|
9304
7316
|
sourceQueryUrl: item.sourceQueryUrl,
|
|
9305
7317
|
slicedQueryUrl: item.slicedQueryUrl,
|
|
9306
7318
|
appliedFilters: item.appliedFilters
|
|
9307
7319
|
}
|
|
9308
|
-
}, {
|
|
9309
|
-
waitSeconds: agentBusyWaitSeconds,
|
|
9310
|
-
maxWaits: agentBusyMaxWaits
|
|
9311
7320
|
});
|
|
9312
7321
|
exported.push(result);
|
|
9313
|
-
session = await requireAuthSession();
|
|
9314
7322
|
}
|
|
9315
7323
|
const payload = {
|
|
9316
7324
|
status: "ok",
|