salesprompter-cli 0.1.34 → 0.1.36
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 +1 -0
- package/dist/cli.js +2305 -206
- package/dist/deel-outreach.js +16 -1
- package/dist/direct-path.js +16 -1
- package/package.json +2 -1
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, writeFile } from "node:fs/promises";
|
|
3
|
+
import { access, appendFile, mkdir, readFile, readdir, 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,7 +33,9 @@ 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
|
|
36
|
+
const companyProvider = new HeuristicCompanyProvider();
|
|
37
|
+
const peopleSearchProvider = new HeuristicPeopleSearchProvider();
|
|
38
|
+
const leadProvider = new AccountLeadProvider(companyProvider, peopleSearchProvider);
|
|
37
39
|
const enrichmentProvider = new HeuristicEnrichmentProvider();
|
|
38
40
|
const scoringProvider = new HeuristicScoringProvider();
|
|
39
41
|
const syncProvider = new RoutedSyncProvider(new DryRunSyncProvider(), new InstantlySyncProvider());
|
|
@@ -120,6 +122,22 @@ const LinkedInCompanyBackfillStatusResponseSchema = z.object({
|
|
|
120
122
|
failureCode: z.string().nullable().optional(),
|
|
121
123
|
failureMessage: z.string().nullable().optional()
|
|
122
124
|
});
|
|
125
|
+
const PhantombusterContainersSyncResponseSchema = z.object({
|
|
126
|
+
status: z.literal("ok"),
|
|
127
|
+
agentIds: z.array(z.string().min(1)),
|
|
128
|
+
agents: z.array(z.object({
|
|
129
|
+
agentId: z.string().min(1),
|
|
130
|
+
fetched: z.number().int().nonnegative(),
|
|
131
|
+
upserted: z.number().int().nonnegative(),
|
|
132
|
+
resultsSynced: z.number().int().nonnegative()
|
|
133
|
+
})),
|
|
134
|
+
fetched: z.number().int().nonnegative(),
|
|
135
|
+
upserted: z.number().int().nonnegative(),
|
|
136
|
+
resultsSynced: z.number().int().nonnegative(),
|
|
137
|
+
outputsStored: z.number().int().nonnegative(),
|
|
138
|
+
resultObjectsStored: z.number().int().nonnegative(),
|
|
139
|
+
resultRowsStored: z.number().int().nonnegative()
|
|
140
|
+
});
|
|
123
141
|
const CliEmailEnrichmentCompaniesResponseSchema = z.object({
|
|
124
142
|
clientId: z.number().int().positive(),
|
|
125
143
|
companies: z.array(z.object({
|
|
@@ -948,6 +966,13 @@ function splitLookupFullName(fullName) {
|
|
|
948
966
|
function buildSyntheticLookupEmail(contactId) {
|
|
949
967
|
return `linkedin-lookup+${contactId}@salesprompter.invalid`;
|
|
950
968
|
}
|
|
969
|
+
function normalizeLinkedInLookupField(value) {
|
|
970
|
+
if (value == null) {
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
const normalized = normalizeLookupWhitespace(String(value));
|
|
974
|
+
return normalized || undefined;
|
|
975
|
+
}
|
|
951
976
|
function looksLikeLookupCompanyRow(fullName, companyName) {
|
|
952
977
|
const fullNameComparable = normalizeLooseMatchText(fullName);
|
|
953
978
|
const companyComparable = normalizeLooseMatchText(companyName);
|
|
@@ -967,19 +992,32 @@ function parseLinkedInUrlLookupInput(content) {
|
|
|
967
992
|
const parsed = z
|
|
968
993
|
.array(z.object({
|
|
969
994
|
clientId: z.union([z.string(), z.number()]).nullish(),
|
|
995
|
+
contactId: z.union([z.string(), z.number()]).nullish(),
|
|
996
|
+
companyId: z.union([z.string(), z.number()]).nullish(),
|
|
970
997
|
fullName: z.string().nullish(),
|
|
971
998
|
companyName: z.string().nullish(),
|
|
972
999
|
email: z.string().nullish(),
|
|
973
|
-
|
|
1000
|
+
contact_email: z.string().nullish(),
|
|
1001
|
+
jobTitle: z.string().nullish(),
|
|
1002
|
+
jobtitle: z.string().nullish(),
|
|
1003
|
+
title: z.string().nullish(),
|
|
1004
|
+
linkedin_company_url: z.string().nullish(),
|
|
1005
|
+
linkedinCompanyUrl: z.string().nullish(),
|
|
1006
|
+
deep_dive_recommended_role: z.string().nullish(),
|
|
1007
|
+
deepDiveRecommendedRole: z.string().nullish()
|
|
974
1008
|
}))
|
|
975
1009
|
.parse(JSON.parse(trimmed));
|
|
976
1010
|
return parsed
|
|
977
1011
|
.map((row) => ({
|
|
978
1012
|
clientId: row.clientId == null ? null : String(row.clientId).trim() || null,
|
|
1013
|
+
contactId: row.contactId == null ? undefined : String(row.contactId).trim() || undefined,
|
|
1014
|
+
companyId: row.companyId == null ? undefined : String(row.companyId).trim() || undefined,
|
|
979
1015
|
fullName: row.fullName?.trim() ?? "",
|
|
980
1016
|
companyName: row.companyName?.trim() ?? "",
|
|
981
|
-
email: row.email?.trim() || undefined,
|
|
982
|
-
jobTitle: row.jobTitle?.trim() || undefined
|
|
1017
|
+
email: row.email?.trim() || row.contact_email?.trim() || undefined,
|
|
1018
|
+
jobTitle: row.jobTitle?.trim() || row.jobtitle?.trim() || row.title?.trim() || undefined,
|
|
1019
|
+
linkedinCompanyUrl: row.linkedin_company_url?.trim() || row.linkedinCompanyUrl?.trim() || undefined,
|
|
1020
|
+
deepDiveRecommendedRole: row.deep_dive_recommended_role?.trim() || row.deepDiveRecommendedRole?.trim() || undefined
|
|
983
1021
|
}))
|
|
984
1022
|
.filter((row) => row.fullName.length > 0 || row.companyName.length > 0);
|
|
985
1023
|
}
|
|
@@ -1007,17 +1045,35 @@ function parseLinkedInUrlLookupInput(content) {
|
|
|
1007
1045
|
? headerValues.findIndex((value) => ["companyname", "company_name"].includes(value))
|
|
1008
1046
|
: 2;
|
|
1009
1047
|
const emailIndex = hasHeader ? headerValues.findIndex((value) => value === "email") : -1;
|
|
1048
|
+
const contactEmailIndex = hasHeader ? headerValues.findIndex((value) => value === "contact_email") : -1;
|
|
1010
1049
|
const jobTitleIndex = hasHeader
|
|
1011
1050
|
? headerValues.findIndex((value) => ["jobtitle", "job_title", "title"].includes(value))
|
|
1012
1051
|
: -1;
|
|
1052
|
+
const contactIdIndex = hasHeader
|
|
1053
|
+
? headerValues.findIndex((value) => ["contactid", "contact_id", "hubspot_contact_id"].includes(value))
|
|
1054
|
+
: -1;
|
|
1055
|
+
const companyIdIndex = hasHeader
|
|
1056
|
+
? headerValues.findIndex((value) => ["companyid", "company_id", "hubspot_company_id"].includes(value))
|
|
1057
|
+
: -1;
|
|
1058
|
+
const linkedinCompanyUrlIndex = hasHeader
|
|
1059
|
+
? headerValues.findIndex((value) => ["linkedin_company_url", "linkedincompanyurl"].includes(value))
|
|
1060
|
+
: -1;
|
|
1061
|
+
const deepDiveRecommendedRoleIndex = hasHeader
|
|
1062
|
+
? headerValues.findIndex((value) => ["deep_dive_recommended_role", "deepdiverecommendedrole"].includes(value))
|
|
1063
|
+
: -1;
|
|
1013
1064
|
return dataLines
|
|
1014
1065
|
.map((line) => splitLooseDelimitedLine(line, delimiter).map((value) => value.trim()))
|
|
1015
1066
|
.map((columns) => ({
|
|
1016
1067
|
clientId: clientIdIndex >= 0 ? columns[clientIdIndex] || null : null,
|
|
1068
|
+
contactId: contactIdIndex >= 0 ? columns[contactIdIndex] || undefined : undefined,
|
|
1069
|
+
companyId: companyIdIndex >= 0 ? columns[companyIdIndex] || undefined : undefined,
|
|
1017
1070
|
fullName: fullNameIndex >= 0 ? columns[fullNameIndex] || "" : "",
|
|
1018
1071
|
companyName: companyNameIndex >= 0 ? columns[companyNameIndex] || "" : "",
|
|
1019
|
-
email: emailIndex >= 0 ? columns[emailIndex] || undefined : undefined
|
|
1020
|
-
|
|
1072
|
+
email: (emailIndex >= 0 ? columns[emailIndex] || undefined : undefined) ??
|
|
1073
|
+
(contactEmailIndex >= 0 ? columns[contactEmailIndex] || undefined : undefined),
|
|
1074
|
+
jobTitle: jobTitleIndex >= 0 ? columns[jobTitleIndex] || undefined : undefined,
|
|
1075
|
+
linkedinCompanyUrl: linkedinCompanyUrlIndex >= 0 ? columns[linkedinCompanyUrlIndex] || undefined : undefined,
|
|
1076
|
+
deepDiveRecommendedRole: deepDiveRecommendedRoleIndex >= 0 ? columns[deepDiveRecommendedRoleIndex] || undefined : undefined
|
|
1021
1077
|
}))
|
|
1022
1078
|
.filter((row) => row.fullName.length > 0 || row.companyName.length > 0);
|
|
1023
1079
|
}
|
|
@@ -1072,7 +1128,7 @@ function parseLinkedInCompanyLookupInput(content) {
|
|
|
1072
1128
|
}
|
|
1073
1129
|
function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
1074
1130
|
return rows.flatMap((row, index) => {
|
|
1075
|
-
const contactId = String(index + 1);
|
|
1131
|
+
const contactId = normalizeLinkedInLookupField(row.contactId) ?? String(index + 1);
|
|
1076
1132
|
const syntheticEmail = row.email?.trim() || buildSyntheticLookupEmail(contactId);
|
|
1077
1133
|
const rawCompanyName = normalizeLookupWhitespace(row.companyName);
|
|
1078
1134
|
const cleanedCompanyName = normalizeLookupCompanyForSearch(cleanedCompanyMap.get(normalizeLookupCompanyForCleaning(rawCompanyName)) ?? rawCompanyName);
|
|
@@ -1086,7 +1142,10 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1086
1142
|
companyName: cleanedCompanyName,
|
|
1087
1143
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1088
1144
|
email: syntheticEmail,
|
|
1089
|
-
jobTitle: row.jobTitle
|
|
1145
|
+
jobTitle: row.jobTitle,
|
|
1146
|
+
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1147
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1148
|
+
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined
|
|
1090
1149
|
}
|
|
1091
1150
|
];
|
|
1092
1151
|
}
|
|
@@ -1101,7 +1160,10 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1101
1160
|
companyName: cleanedCompanyName,
|
|
1102
1161
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1103
1162
|
email: syntheticEmail,
|
|
1104
|
-
jobTitle: row.jobTitle
|
|
1163
|
+
jobTitle: row.jobTitle,
|
|
1164
|
+
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1165
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1166
|
+
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined
|
|
1105
1167
|
}
|
|
1106
1168
|
];
|
|
1107
1169
|
const rawDiffers = rawSplit.firstName !== cleanedSplit.firstName ||
|
|
@@ -1115,6 +1177,9 @@ function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
|
1115
1177
|
companyNameOriginal: rawCompanyName || undefined,
|
|
1116
1178
|
email: syntheticEmail,
|
|
1117
1179
|
jobTitle: row.jobTitle,
|
|
1180
|
+
companyId: normalizeLinkedInLookupField(row.companyId),
|
|
1181
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || undefined,
|
|
1182
|
+
deepDiveRecommendedRole: row.deepDiveRecommendedRole?.trim() || undefined,
|
|
1118
1183
|
isVariation: true
|
|
1119
1184
|
});
|
|
1120
1185
|
}
|
|
@@ -1137,10 +1202,132 @@ function readPipedreamLinkedInEnrichmentConfig() {
|
|
|
1137
1202
|
projectEnvironment: resolveConfiguredEnvValue(process.env, "PIPEDREAM_PROJECT_ENVIRONMENT") || ""
|
|
1138
1203
|
};
|
|
1139
1204
|
}
|
|
1205
|
+
function isSyntheticLinkedInLookupEmail(value) {
|
|
1206
|
+
const normalized = normalizeLookupWhitespace(value).toLowerCase();
|
|
1207
|
+
return normalized.endsWith("@salesprompter.invalid");
|
|
1208
|
+
}
|
|
1140
1209
|
function deriveCsrfTokenFromCookie(cookie) {
|
|
1141
1210
|
const match = cookie.match(/JSESSIONID="?([^";]+)"?/i);
|
|
1142
1211
|
return match?.[1]?.trim() || "";
|
|
1143
1212
|
}
|
|
1213
|
+
function normalizeLinkedInDirectLookupCookieHeader(cookie) {
|
|
1214
|
+
const trimmed = normalizeLookupWhitespace(cookie);
|
|
1215
|
+
if (!trimmed) {
|
|
1216
|
+
return "";
|
|
1217
|
+
}
|
|
1218
|
+
if (trimmed.includes("=") || trimmed.includes(";")) {
|
|
1219
|
+
return trimmed;
|
|
1220
|
+
}
|
|
1221
|
+
return `li_at=${trimmed}`;
|
|
1222
|
+
}
|
|
1223
|
+
function parseLocalLinkedInExtensionTokenLog(content) {
|
|
1224
|
+
const matches = [
|
|
1225
|
+
...content.matchAll(/\{"csrfToken":"([^"]+)","extractedFrom":"sales-api\/salesApiLeadSearch"[\s\S]*?"linkedInIdentity":"([^"]+)"[\s\S]*?"sessionCookie":"([\s\S]*?)","syncStatus":"(success|captured)"[\s\S]*?"userAgent":"([^"]+)"\}/g)
|
|
1226
|
+
];
|
|
1227
|
+
const last = matches.at(-1);
|
|
1228
|
+
if (!last) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const csrfToken = normalizeLookupWhitespace(last[1]);
|
|
1232
|
+
const linkedInIdentity = normalizeLookupWhitespace(last[2]);
|
|
1233
|
+
const sessionCookie = normalizeLookupWhitespace(last[3]?.replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
|
|
1234
|
+
const userAgent = normalizeLookupWhitespace(last[5]);
|
|
1235
|
+
if (!csrfToken || !linkedInIdentity || !sessionCookie || !userAgent) {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
csrfToken,
|
|
1240
|
+
linkedInIdentity,
|
|
1241
|
+
sessionCookie,
|
|
1242
|
+
userAgent
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
async function readLocalLinkedInExtensionTokenLog(filePath) {
|
|
1246
|
+
try {
|
|
1247
|
+
const content = await readFile(filePath, "latin1");
|
|
1248
|
+
return parseLocalLinkedInExtensionTokenLog(content);
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function listChromeExtensionTokenLogCandidates() {
|
|
1255
|
+
const overrideFile = normalizeLookupWhitespace(process.env.SALESPROMPTER_LINKEDIN_EXTENSION_TOKENS_LOG_PATH);
|
|
1256
|
+
if (overrideFile) {
|
|
1257
|
+
return [overrideFile];
|
|
1258
|
+
}
|
|
1259
|
+
const overrideDir = normalizeLookupWhitespace(process.env.SALESPROMPTER_LINKEDIN_EXTENSION_TOKENS_DIR);
|
|
1260
|
+
if (overrideDir) {
|
|
1261
|
+
try {
|
|
1262
|
+
const files = await readdir(overrideDir);
|
|
1263
|
+
return files
|
|
1264
|
+
.filter((file) => file.endsWith(".log") || file.endsWith(".ldb"))
|
|
1265
|
+
.map((file) => path.join(overrideDir, file))
|
|
1266
|
+
.sort()
|
|
1267
|
+
.reverse();
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
return [];
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const chromeRootCandidates = [
|
|
1274
|
+
path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome"),
|
|
1275
|
+
path.join(os.homedir(), "Library", "Application Support", "Chromium")
|
|
1276
|
+
];
|
|
1277
|
+
const paths = [];
|
|
1278
|
+
for (const chromeRoot of chromeRootCandidates) {
|
|
1279
|
+
let profileDirs = [];
|
|
1280
|
+
try {
|
|
1281
|
+
profileDirs = await readdir(chromeRoot);
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
for (const profileDir of profileDirs) {
|
|
1287
|
+
const extensionSettingsRoot = path.join(chromeRoot, profileDir, "Local Extension Settings");
|
|
1288
|
+
let extensionIds = [];
|
|
1289
|
+
try {
|
|
1290
|
+
extensionIds = await readdir(extensionSettingsRoot);
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
for (const extensionId of extensionIds) {
|
|
1296
|
+
const extensionDir = path.join(extensionSettingsRoot, extensionId);
|
|
1297
|
+
let files = [];
|
|
1298
|
+
try {
|
|
1299
|
+
files = await readdir(extensionDir);
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
for (const file of files) {
|
|
1305
|
+
if (!file.endsWith(".log")) {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
paths.push(path.join(extensionDir, file));
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return paths.sort().reverse();
|
|
1314
|
+
}
|
|
1315
|
+
async function readLocalLinkedInExtensionDirectLookupConfig() {
|
|
1316
|
+
const candidates = await listChromeExtensionTokenLogCandidates();
|
|
1317
|
+
for (const candidate of candidates) {
|
|
1318
|
+
const snapshot = await readLocalLinkedInExtensionTokenLog(candidate);
|
|
1319
|
+
if (!snapshot) {
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
csrfToken: snapshot.csrfToken,
|
|
1324
|
+
identity: snapshot.linkedInIdentity,
|
|
1325
|
+
cookie: normalizeLinkedInDirectLookupCookieHeader(snapshot.sessionCookie),
|
|
1326
|
+
userAgent: snapshot.userAgent
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1144
1331
|
function readLinkedInDirectLookupEnvConfig() {
|
|
1145
1332
|
const cookie = process.env.SALESPROMPTER_LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
1146
1333
|
process.env.LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
@@ -1157,7 +1344,7 @@ function readLinkedInDirectLookupEnvConfig() {
|
|
|
1157
1344
|
return {
|
|
1158
1345
|
csrfToken,
|
|
1159
1346
|
identity,
|
|
1160
|
-
cookie,
|
|
1347
|
+
cookie: normalizeLinkedInDirectLookupCookieHeader(cookie),
|
|
1161
1348
|
userAgent: process.env.SALESPROMPTER_LINKEDIN_USER_AGENT?.trim() ||
|
|
1162
1349
|
"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"
|
|
1163
1350
|
};
|
|
@@ -1207,7 +1394,7 @@ async function readStoredLinkedInDirectLookupConfig() {
|
|
|
1207
1394
|
return {
|
|
1208
1395
|
csrfToken,
|
|
1209
1396
|
identity,
|
|
1210
|
-
cookie: claimed.sessionCookie,
|
|
1397
|
+
cookie: normalizeLinkedInDirectLookupCookieHeader(claimed.sessionCookie),
|
|
1211
1398
|
userAgent
|
|
1212
1399
|
};
|
|
1213
1400
|
}
|
|
@@ -1221,6 +1408,11 @@ async function readLinkedInDirectLookupConfig() {
|
|
|
1221
1408
|
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1222
1409
|
return envConfig;
|
|
1223
1410
|
}
|
|
1411
|
+
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
1412
|
+
if (localExtensionConfig) {
|
|
1413
|
+
cachedLinkedInDirectLookupConfig = localExtensionConfig;
|
|
1414
|
+
return localExtensionConfig;
|
|
1415
|
+
}
|
|
1224
1416
|
const storedConfig = await readStoredLinkedInDirectLookupConfig();
|
|
1225
1417
|
if (storedConfig) {
|
|
1226
1418
|
cachedLinkedInDirectLookupConfig = storedConfig;
|
|
@@ -1237,46 +1429,200 @@ function buildLinkedInSalesApiUrl(params) {
|
|
|
1237
1429
|
const encodedFirstName = encodeURIComponent(params.firstName);
|
|
1238
1430
|
const encodedLastName = encodeURIComponent(params.lastName);
|
|
1239
1431
|
const encodedCompanyName = encodeURIComponent(params.companyName);
|
|
1432
|
+
const encodedKeywords = encodeURIComponent(params.keywordsText?.trim() || params.companyName);
|
|
1240
1433
|
const filters = params.searchMode === "current_company"
|
|
1241
1434
|
? `(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)))`
|
|
1242
1435
|
: `(type:FIRST_NAME,values:List((text:${encodedFirstName},selectionType:INCLUDED))),(type:LAST_NAME,values:List((text:${encodedLastName},selectionType:INCLUDED)))`;
|
|
1243
|
-
const keywordsSegment = params.searchMode === "
|
|
1436
|
+
const keywordsSegment = params.searchMode === "current_company" ? "" : `,keywords:${encodedKeywords}`;
|
|
1244
1437
|
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`;
|
|
1245
1438
|
}
|
|
1439
|
+
function extractLookupTitleKeywords(value) {
|
|
1440
|
+
const shortAllowlist = new Set(["hr", "it", "cfo"]);
|
|
1441
|
+
return normalizeLooseMatchText(value)
|
|
1442
|
+
.split(/\s+/)
|
|
1443
|
+
.filter((token) => token.length >= 4 || shortAllowlist.has(token))
|
|
1444
|
+
.filter((token) => ![
|
|
1445
|
+
"head",
|
|
1446
|
+
"senior",
|
|
1447
|
+
"consultant",
|
|
1448
|
+
"manager",
|
|
1449
|
+
"specialist",
|
|
1450
|
+
"lead",
|
|
1451
|
+
"global",
|
|
1452
|
+
"team",
|
|
1453
|
+
"group"
|
|
1454
|
+
].includes(token))
|
|
1455
|
+
.slice(0, 4);
|
|
1456
|
+
}
|
|
1457
|
+
function buildDeepDiveRoleSearchKeywords(role) {
|
|
1458
|
+
const normalized = normalizeLooseMatchText(role);
|
|
1459
|
+
switch (normalized) {
|
|
1460
|
+
case "budgetholder":
|
|
1461
|
+
return ["finance", "procurement", "purchasing", "accounting", "controlling", "cfo"];
|
|
1462
|
+
case "decisionmaker":
|
|
1463
|
+
return ["director", "head", "vp", "chief", "leiter", "lead"];
|
|
1464
|
+
case "champion":
|
|
1465
|
+
return ["hr", "workplace", "operations", "it", "people", "office"];
|
|
1466
|
+
case "executivesponsor":
|
|
1467
|
+
return ["executive", "board", "chief", "managing", "director", "ceo"];
|
|
1468
|
+
case "influencer":
|
|
1469
|
+
return ["specialist", "manager", "consultant", "project", "workplace", "hr"];
|
|
1470
|
+
case "legalandcompliance":
|
|
1471
|
+
return ["legal", "compliance", "datenschutz", "counsel"];
|
|
1472
|
+
case "blocker":
|
|
1473
|
+
return ["procurement", "legal", "compliance", "security"];
|
|
1474
|
+
case "enduser":
|
|
1475
|
+
return ["workplace", "office", "operations", "assistant", "admin"];
|
|
1476
|
+
default:
|
|
1477
|
+
return [];
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1246
1480
|
function buildLinkedInAccountSearchApiUrl(companyName) {
|
|
1247
1481
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
1248
1482
|
"https://www.linkedin.com";
|
|
1249
1483
|
const encodedCompanyName = encodeURIComponent(companyName);
|
|
1250
1484
|
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`;
|
|
1251
1485
|
}
|
|
1252
|
-
function buildLinkedInLookupSearchVariants(contact) {
|
|
1486
|
+
async function buildLinkedInLookupSearchVariants(contact, timeoutMs, resolvedCompanyAliases = []) {
|
|
1253
1487
|
const variants = [];
|
|
1254
1488
|
const seen = new Set();
|
|
1255
|
-
const
|
|
1256
|
-
|
|
1257
|
-
normalizeLookupWhitespace(
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1489
|
+
const companyCandidateScores = new Map();
|
|
1490
|
+
const addCompanyCandidate = (value, score) => {
|
|
1491
|
+
const normalized = normalizeLookupWhitespace(value);
|
|
1492
|
+
if (!normalized) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
companyCandidateScores.set(normalized, Math.max(score, companyCandidateScores.get(normalized) ?? 0));
|
|
1496
|
+
};
|
|
1497
|
+
addCompanyCandidate(contact.companyName, 80);
|
|
1498
|
+
addCompanyCandidate(contact.companyNameOriginal, 70);
|
|
1499
|
+
const linkedInHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "");
|
|
1500
|
+
if (linkedInHandle && !/^\d+$/.test(linkedInHandle)) {
|
|
1501
|
+
addCompanyCandidate(linkedInHandle.replace(/[-_]+/g, " "), 95);
|
|
1502
|
+
}
|
|
1503
|
+
for (const alias of resolvedCompanyAliases) {
|
|
1504
|
+
addCompanyCandidate(alias, 110);
|
|
1505
|
+
}
|
|
1506
|
+
const emailDomain = (() => {
|
|
1507
|
+
const email = normalizeLookupWhitespace(contact.email);
|
|
1508
|
+
if (!email || isSyntheticLinkedInLookupEmail(email)) {
|
|
1509
|
+
return "";
|
|
1510
|
+
}
|
|
1511
|
+
const at = email.lastIndexOf("@");
|
|
1512
|
+
return at >= 0 ? email.slice(at + 1) : "";
|
|
1513
|
+
})();
|
|
1514
|
+
if (emailDomain) {
|
|
1515
|
+
const host = emailDomain.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
1516
|
+
if (host) {
|
|
1517
|
+
addCompanyCandidate(host.replace(/[-_]+/g, " "), 100);
|
|
1277
1518
|
}
|
|
1278
1519
|
}
|
|
1279
|
-
|
|
1520
|
+
if (contact.jobTitle && contact.deepDiveRecommendedRole) {
|
|
1521
|
+
const primaryWord = normalizeLookupWhitespace(contact.companyNameOriginal ?? contact.companyName)
|
|
1522
|
+
.split(/\s+/)
|
|
1523
|
+
.filter((part) => part.length >= 4)
|
|
1524
|
+
.slice(-1)[0];
|
|
1525
|
+
if (primaryWord) {
|
|
1526
|
+
addCompanyCandidate(primaryWord, 45);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const companyHints = await buildLinkedInProfileCompanyHints(contact, timeoutMs);
|
|
1530
|
+
for (const phrase of companyHints.phrases) {
|
|
1531
|
+
const tokenCount = normalizeLooseMatchText(phrase).split(/\s+/).filter(Boolean).length;
|
|
1532
|
+
if (tokenCount >= 1 && tokenCount <= 4) {
|
|
1533
|
+
addCompanyCandidate(phrase, tokenCount <= 2 ? 75 : 60);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
for (const keyword of companyHints.keywords.slice(0, 5)) {
|
|
1537
|
+
addCompanyCandidate(keyword, keyword.includes(".") ? 90 : 55);
|
|
1538
|
+
}
|
|
1539
|
+
const titleKeywords = Array.from(new Set([
|
|
1540
|
+
...extractLookupTitleKeywords(contact.jobTitle),
|
|
1541
|
+
...buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole)
|
|
1542
|
+
])).slice(0, 6);
|
|
1543
|
+
const rankedCompanyCandidates = Array.from(companyCandidateScores.entries())
|
|
1544
|
+
.sort((left, right) => right[1] - left[1] || left[0].length - right[0].length)
|
|
1545
|
+
.slice(0, 6);
|
|
1546
|
+
const emailHostCandidate = (() => {
|
|
1547
|
+
if (!emailDomain) {
|
|
1548
|
+
return "";
|
|
1549
|
+
}
|
|
1550
|
+
return normalizeLookupWhitespace(emailDomain.replace(/^www\./i, "").split(".")[0] ?? "").replace(/[-_]+/g, " ");
|
|
1551
|
+
})();
|
|
1552
|
+
const cleanCompanyCandidate = normalizeLookupWhitespace(contact.companyName) ||
|
|
1553
|
+
normalizeLookupWhitespace(contact.companyNameOriginal) ||
|
|
1554
|
+
"";
|
|
1555
|
+
const linkedInHandleCandidate = linkedInHandle && !/^\d+$/.test(linkedInHandle)
|
|
1556
|
+
? normalizeLookupWhitespace(linkedInHandle.replace(/[-_]+/g, " "))
|
|
1557
|
+
: "";
|
|
1558
|
+
const pushVariant = (companyName, searchMode) => {
|
|
1559
|
+
const normalizedCompany = normalizeLookupWhitespace(companyName);
|
|
1560
|
+
if (!normalizedCompany) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const keywordsText = searchMode === "keywords_title" && titleKeywords.length > 0
|
|
1564
|
+
? `${normalizedCompany} ${titleKeywords.join(" ")}`
|
|
1565
|
+
: undefined;
|
|
1566
|
+
if (searchMode === "keywords_title" && !keywordsText) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const key = [
|
|
1570
|
+
contact.firstName.trim().toLowerCase(),
|
|
1571
|
+
contact.lastName.trim().toLowerCase(),
|
|
1572
|
+
normalizedCompany.toLowerCase(),
|
|
1573
|
+
searchMode,
|
|
1574
|
+
keywordsText?.toLowerCase() ?? ""
|
|
1575
|
+
].join("|");
|
|
1576
|
+
if (seen.has(key)) {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
seen.add(key);
|
|
1580
|
+
variants.push({
|
|
1581
|
+
firstName: contact.firstName,
|
|
1582
|
+
lastName: contact.lastName,
|
|
1583
|
+
companyName: normalizedCompany,
|
|
1584
|
+
searchMode,
|
|
1585
|
+
keywordsText
|
|
1586
|
+
});
|
|
1587
|
+
};
|
|
1588
|
+
const rankedCompanyNames = rankedCompanyCandidates.map(([companyName]) => companyName);
|
|
1589
|
+
const currentCompanyStageCandidates = [
|
|
1590
|
+
emailHostCandidate,
|
|
1591
|
+
linkedInHandleCandidate,
|
|
1592
|
+
...resolvedCompanyAliases,
|
|
1593
|
+
...rankedCompanyNames.filter((companyName) => (companyCandidateScores.get(companyName) ?? 0) >= 90)
|
|
1594
|
+
];
|
|
1595
|
+
const keywordStageCandidates = [
|
|
1596
|
+
cleanCompanyCandidate,
|
|
1597
|
+
...rankedCompanyNames
|
|
1598
|
+
];
|
|
1599
|
+
const keywordTitleStageCandidates = [
|
|
1600
|
+
cleanCompanyCandidate,
|
|
1601
|
+
...rankedCompanyNames
|
|
1602
|
+
];
|
|
1603
|
+
const fallbackCurrentCompanyCandidates = [
|
|
1604
|
+
cleanCompanyCandidate,
|
|
1605
|
+
normalizeLookupWhitespace(contact.companyNameOriginal),
|
|
1606
|
+
...rankedCompanyNames
|
|
1607
|
+
];
|
|
1608
|
+
for (const companyName of currentCompanyStageCandidates) {
|
|
1609
|
+
pushVariant(companyName, "current_company");
|
|
1610
|
+
}
|
|
1611
|
+
for (const companyName of keywordStageCandidates) {
|
|
1612
|
+
pushVariant(companyName, "keywords");
|
|
1613
|
+
}
|
|
1614
|
+
for (const companyName of keywordTitleStageCandidates) {
|
|
1615
|
+
pushVariant(companyName, "keywords_title");
|
|
1616
|
+
}
|
|
1617
|
+
for (const companyName of fallbackCurrentCompanyCandidates) {
|
|
1618
|
+
pushVariant(companyName, "current_company");
|
|
1619
|
+
}
|
|
1620
|
+
for (const [companyName] of rankedCompanyCandidates) {
|
|
1621
|
+
pushVariant(companyName, "current_company");
|
|
1622
|
+
pushVariant(companyName, "keywords");
|
|
1623
|
+
pushVariant(companyName, "keywords_title");
|
|
1624
|
+
}
|
|
1625
|
+
return variants.slice(0, 12);
|
|
1280
1626
|
}
|
|
1281
1627
|
function normalizeSalesNavLeadUrl(value) {
|
|
1282
1628
|
const trimmed = String(value ?? "").trim();
|
|
@@ -1298,14 +1644,21 @@ function normalizePublicLinkedInProfileUrl(value) {
|
|
|
1298
1644
|
if (!trimmed) {
|
|
1299
1645
|
return null;
|
|
1300
1646
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1647
|
+
let parsed;
|
|
1648
|
+
try {
|
|
1649
|
+
parsed = new URL(trimmed);
|
|
1650
|
+
}
|
|
1651
|
+
catch {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
if (!/(^|\.)linkedin\.com$/i.test(parsed.hostname)) {
|
|
1303
1655
|
return null;
|
|
1304
1656
|
}
|
|
1305
|
-
const
|
|
1306
|
-
if (!
|
|
1657
|
+
const pathMatch = parsed.pathname.match(/^\/in\/([^/?#]+)\/?/i);
|
|
1658
|
+
if (!pathMatch?.[1]) {
|
|
1307
1659
|
return null;
|
|
1308
1660
|
}
|
|
1661
|
+
const candidate = `https://www.linkedin.com/in/${pathMatch[1]}`;
|
|
1309
1662
|
return normalizeSalesNavLeadUrl(candidate) ? null : candidate;
|
|
1310
1663
|
}
|
|
1311
1664
|
function extractLinkedInProfileUrlFromSalesApiElement(element) {
|
|
@@ -1448,6 +1801,112 @@ function extractLinkedInCompanyNameFromSalesApiElement(element) {
|
|
|
1448
1801
|
}
|
|
1449
1802
|
return null;
|
|
1450
1803
|
}
|
|
1804
|
+
function extractLinkedInFullNameFromSalesApiElement(element) {
|
|
1805
|
+
if (!element) {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
const directCandidates = [
|
|
1809
|
+
typeof element.fullName === "string" ? element.fullName : null,
|
|
1810
|
+
typeof element.name === "string" ? element.name : null
|
|
1811
|
+
].filter(Boolean);
|
|
1812
|
+
for (const candidate of directCandidates) {
|
|
1813
|
+
const normalized = normalizeLookupWhitespace(candidate);
|
|
1814
|
+
if (normalized) {
|
|
1815
|
+
return normalized;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const firstName = typeof element.firstName === "string" ? normalizeLookupWhitespace(element.firstName) : "";
|
|
1819
|
+
const lastName = typeof element.lastName === "string" ? normalizeLookupWhitespace(element.lastName) : "";
|
|
1820
|
+
const combined = normalizeLookupWhitespace(`${firstName} ${lastName}`);
|
|
1821
|
+
return combined || null;
|
|
1822
|
+
}
|
|
1823
|
+
function extractLinkedInTitleFromSalesApiElement(element) {
|
|
1824
|
+
if (!element) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
const directCandidates = [
|
|
1828
|
+
typeof element.title === "string" ? element.title : null,
|
|
1829
|
+
typeof element.occupation === "string" ? element.occupation : null
|
|
1830
|
+
].filter(Boolean);
|
|
1831
|
+
for (const candidate of directCandidates) {
|
|
1832
|
+
const normalized = normalizeLookupWhitespace(candidate);
|
|
1833
|
+
if (normalized) {
|
|
1834
|
+
return normalized;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
const currentPosition = Array.isArray(element.currentPositions) && element.currentPositions.length > 0
|
|
1838
|
+
? element.currentPositions[0]
|
|
1839
|
+
: null;
|
|
1840
|
+
const currentTitle = currentPosition && typeof currentPosition.title === "string"
|
|
1841
|
+
? normalizeLookupWhitespace(currentPosition.title)
|
|
1842
|
+
: "";
|
|
1843
|
+
return currentTitle || null;
|
|
1844
|
+
}
|
|
1845
|
+
function scoreLinkedInSalesApiElementMatch(contact, element) {
|
|
1846
|
+
const fullName = extractLinkedInFullNameFromSalesApiElement(element);
|
|
1847
|
+
const companyName = extractLinkedInCompanyNameFromSalesApiElement(Array.isArray(element?.currentPositions) && element.currentPositions.length > 0
|
|
1848
|
+
? element.currentPositions[0]
|
|
1849
|
+
: element) ?? extractLinkedInCompanyNameFromSalesApiElement(element);
|
|
1850
|
+
const title = extractLinkedInTitleFromSalesApiElement(element);
|
|
1851
|
+
const expectedFullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
1852
|
+
const candidateFullName = normalizeLooseMatchText(fullName);
|
|
1853
|
+
const expectedCompanies = Array.from(new Set([
|
|
1854
|
+
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
1855
|
+
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName)),
|
|
1856
|
+
normalizeLooseMatchText(normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? ""),
|
|
1857
|
+
normalizeLooseMatchText((() => {
|
|
1858
|
+
const email = normalizeLookupWhitespace(contact.email);
|
|
1859
|
+
if (!email || isSyntheticLinkedInLookupEmail(email)) {
|
|
1860
|
+
return "";
|
|
1861
|
+
}
|
|
1862
|
+
return email.split("@")[1]?.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
1863
|
+
})())
|
|
1864
|
+
].filter(Boolean)));
|
|
1865
|
+
const candidateCompany = normalizeLooseMatchText(companyName);
|
|
1866
|
+
const candidateTitle = normalizeLooseMatchText(title);
|
|
1867
|
+
let score = 0;
|
|
1868
|
+
let exactNameMatch = false;
|
|
1869
|
+
let companyMatchCount = 0;
|
|
1870
|
+
if (expectedFullName && candidateFullName === expectedFullName) {
|
|
1871
|
+
score += 120;
|
|
1872
|
+
exactNameMatch = true;
|
|
1873
|
+
}
|
|
1874
|
+
else if (expectedFullName &&
|
|
1875
|
+
candidateFullName.includes(normalizeLooseMatchText(contact.firstName)) &&
|
|
1876
|
+
candidateFullName.includes(normalizeLooseMatchText(contact.lastName))) {
|
|
1877
|
+
score += 90;
|
|
1878
|
+
}
|
|
1879
|
+
for (const companyHint of expectedCompanies) {
|
|
1880
|
+
if (!companyHint) {
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (candidateCompany === companyHint) {
|
|
1884
|
+
score += 40;
|
|
1885
|
+
companyMatchCount += 1;
|
|
1886
|
+
}
|
|
1887
|
+
else if (candidateCompany.includes(companyHint) || companyHint.includes(candidateCompany)) {
|
|
1888
|
+
score += 25;
|
|
1889
|
+
companyMatchCount += 1;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const titleHints = [
|
|
1893
|
+
...extractLookupTitleKeywords(contact.jobTitle),
|
|
1894
|
+
...buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole)
|
|
1895
|
+
].slice(0, 6);
|
|
1896
|
+
for (const hint of titleHints) {
|
|
1897
|
+
if (hint && candidateTitle.includes(normalizeLooseMatchText(hint))) {
|
|
1898
|
+
score += 6;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return {
|
|
1902
|
+
score,
|
|
1903
|
+
fullName,
|
|
1904
|
+
companyName,
|
|
1905
|
+
title,
|
|
1906
|
+
exactNameMatch,
|
|
1907
|
+
companyMatchCount
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1451
1910
|
function extractLinkedInCompanyEmployeeCountFromSalesApiElement(element) {
|
|
1452
1911
|
if (!element) {
|
|
1453
1912
|
return null;
|
|
@@ -1496,6 +1955,111 @@ function buildLinkedInCompanyLookupVariants(params) {
|
|
|
1496
1955
|
}
|
|
1497
1956
|
return variants;
|
|
1498
1957
|
}
|
|
1958
|
+
function buildDirectCompanyContextKey(contact) {
|
|
1959
|
+
return normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName);
|
|
1960
|
+
}
|
|
1961
|
+
async function resolveDirectLinkedInCompanyContexts(params) {
|
|
1962
|
+
const perCompanyBudgetMs = Math.min(params.timeoutMs, 10_000);
|
|
1963
|
+
const primaryByCompany = new Map();
|
|
1964
|
+
for (const contact of params.contacts) {
|
|
1965
|
+
const key = buildDirectCompanyContextKey(contact);
|
|
1966
|
+
if (!key || primaryByCompany.has(key)) {
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
primaryByCompany.set(key, contact);
|
|
1970
|
+
}
|
|
1971
|
+
const contexts = new Map();
|
|
1972
|
+
for (const [companyKey, contact] of primaryByCompany.entries()) {
|
|
1973
|
+
const aliases = new Set();
|
|
1974
|
+
const addAlias = (value) => {
|
|
1975
|
+
const normalized = normalizeLookupWhitespace(value);
|
|
1976
|
+
if (!normalized) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
aliases.add(normalized);
|
|
1980
|
+
};
|
|
1981
|
+
addAlias(contact.companyNameOriginal);
|
|
1982
|
+
addAlias(contact.companyName);
|
|
1983
|
+
const existingHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "");
|
|
1984
|
+
if (existingHandle && !/^\d+$/.test(existingHandle)) {
|
|
1985
|
+
addAlias(existingHandle.replace(/[-_]+/g, " "));
|
|
1986
|
+
}
|
|
1987
|
+
let matchedCompanyUrl = contact.linkedinCompanyUrl ?? null;
|
|
1988
|
+
let matchedSalesNavCompanyUrl = null;
|
|
1989
|
+
let matchedCompanyName = null;
|
|
1990
|
+
let matchedCompanyEmployeeCount = null;
|
|
1991
|
+
const companyDeadline = Date.now() + perCompanyBudgetMs;
|
|
1992
|
+
const variants = buildLinkedInCompanyLookupVariants({
|
|
1993
|
+
contactId: contact.contact_id,
|
|
1994
|
+
companyName: contact.companyName,
|
|
1995
|
+
companyNameOriginal: contact.companyNameOriginal
|
|
1996
|
+
}).slice(0, 4);
|
|
1997
|
+
for (const variant of variants) {
|
|
1998
|
+
if (Date.now() >= companyDeadline) {
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
const controller = new AbortController();
|
|
2002
|
+
const timeout = setTimeout(controller.abort.bind(controller), Math.min(6_000, Math.max(1_000, companyDeadline - Date.now())));
|
|
2003
|
+
try {
|
|
2004
|
+
const response = await fetch(buildLinkedInAccountSearchApiUrl(variant.companyName), {
|
|
2005
|
+
method: "GET",
|
|
2006
|
+
signal: controller.signal,
|
|
2007
|
+
headers: {
|
|
2008
|
+
accept: "*/*",
|
|
2009
|
+
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
2010
|
+
"csrf-token": params.config.csrfToken,
|
|
2011
|
+
referer: "https://www.linkedin.com/sales/search/company",
|
|
2012
|
+
"sec-fetch-dest": "empty",
|
|
2013
|
+
"sec-fetch-mode": "cors",
|
|
2014
|
+
"sec-fetch-site": "same-origin",
|
|
2015
|
+
"user-agent": params.config.userAgent,
|
|
2016
|
+
"x-li-identity": params.config.identity,
|
|
2017
|
+
"x-li-lang": "en_US",
|
|
2018
|
+
"x-li-page-instance": "urn:li:page:d_sales2_search_accounts;13Jvve6kRGCao+iP0wwAag==",
|
|
2019
|
+
"x-restli-protocol-version": "2.0.0",
|
|
2020
|
+
cookie: params.config.cookie
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
if (!response.ok) {
|
|
2024
|
+
if (response.status === 429) {
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
const data = (await response.json());
|
|
2030
|
+
const first = data.elements?.[0];
|
|
2031
|
+
const companyUrl = extractLinkedInCompanyUrlFromSalesApiElement(first);
|
|
2032
|
+
const salesNavCompanyUrl = extractLinkedInSalesNavCompanyUrlFromSalesApiElement(first);
|
|
2033
|
+
const companyName = extractLinkedInCompanyNameFromSalesApiElement(first);
|
|
2034
|
+
if (companyUrl || salesNavCompanyUrl || companyName) {
|
|
2035
|
+
matchedCompanyUrl = companyUrl ?? matchedCompanyUrl;
|
|
2036
|
+
matchedSalesNavCompanyUrl = salesNavCompanyUrl ?? matchedSalesNavCompanyUrl;
|
|
2037
|
+
matchedCompanyName = companyName ?? matchedCompanyName;
|
|
2038
|
+
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first);
|
|
2039
|
+
addAlias(companyName);
|
|
2040
|
+
addAlias(companyUrl ? normalizeLinkedInCompanyHandle(companyUrl)?.replace(/[-_]+/g, " ") : null);
|
|
2041
|
+
addAlias(salesNavCompanyUrl ? normalizeLookupWhitespace(salesNavCompanyUrl.split("/sales/company/")[1]?.split(/[/?#]/)[0] ?? "") : null);
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
catch {
|
|
2046
|
+
// Try next company variant.
|
|
2047
|
+
}
|
|
2048
|
+
finally {
|
|
2049
|
+
clearTimeout(timeout);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
contexts.set(companyKey, {
|
|
2053
|
+
normalizedCompanyKey: companyKey,
|
|
2054
|
+
aliases: Array.from(aliases),
|
|
2055
|
+
linkedinCompanyUrl: matchedCompanyUrl,
|
|
2056
|
+
salesNavCompanyUrl: matchedSalesNavCompanyUrl,
|
|
2057
|
+
matchedCompanyName,
|
|
2058
|
+
matchedCompanyEmployeeCount
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
return contexts;
|
|
2062
|
+
}
|
|
1499
2063
|
function buildPublicLinkedInCompanySearchUrl(companyName) {
|
|
1500
2064
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_COMPANY_SEARCH_BASE_URL?.trim() ||
|
|
1501
2065
|
"https://duckduckgo.com/html/";
|
|
@@ -1559,7 +2123,8 @@ function extractSerperLinkedInCompanyCandidates(payload) {
|
|
|
1559
2123
|
const organic = "organic" in payload && Array.isArray(payload.organic)
|
|
1560
2124
|
? (payload.organic ?? [])
|
|
1561
2125
|
: [];
|
|
1562
|
-
const
|
|
2126
|
+
const seen = new Set();
|
|
2127
|
+
const candidates = [];
|
|
1563
2128
|
for (const result of organic) {
|
|
1564
2129
|
if (!result || typeof result !== "object") {
|
|
1565
2130
|
continue;
|
|
@@ -1569,60 +2134,685 @@ function extractSerperLinkedInCompanyCandidates(payload) {
|
|
|
1569
2134
|
: "";
|
|
1570
2135
|
const handle = normalizeLinkedInCompanyHandle(link);
|
|
1571
2136
|
if (handle) {
|
|
1572
|
-
|
|
2137
|
+
const url = normalizeLinkedInCompanyPage(handle);
|
|
2138
|
+
if (!seen.has(url)) {
|
|
2139
|
+
seen.add(url);
|
|
2140
|
+
candidates.push({
|
|
2141
|
+
url,
|
|
2142
|
+
title: "title" in result && typeof result.title === "string"
|
|
2143
|
+
? normalizeLookupWhitespace(result.title)
|
|
2144
|
+
: "",
|
|
2145
|
+
snippet: "snippet" in result && typeof result.snippet === "string"
|
|
2146
|
+
? normalizeLookupWhitespace(result.snippet)
|
|
2147
|
+
: ""
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
1573
2150
|
}
|
|
1574
2151
|
}
|
|
1575
|
-
return
|
|
1576
|
-
}
|
|
1577
|
-
function decodeHtmlEntities(value) {
|
|
1578
|
-
return value
|
|
1579
|
-
.replace(/&/gi, "&")
|
|
1580
|
-
.replace(/"/gi, '"')
|
|
1581
|
-
.replace(/'/gi, "'")
|
|
1582
|
-
.replace(/</gi, "<")
|
|
1583
|
-
.replace(/>/gi, ">");
|
|
2152
|
+
return candidates;
|
|
1584
2153
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
1601
|
-
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
1602
|
-
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
1603
|
-
const unavailable = response.status >= 400 ||
|
|
1604
|
-
unavailableText.includes("page not found") ||
|
|
1605
|
-
unavailableText.includes("this page does not exist") ||
|
|
1606
|
-
unavailableText.includes("page isnt available");
|
|
1607
|
-
const handle = normalizeLinkedInCompanyHandle(finalUrl) ?? normalizeLinkedInCompanyHandle(url);
|
|
1608
|
-
if (!handle) {
|
|
1609
|
-
return null;
|
|
2154
|
+
const linkedInCompanyHintCache = new Map();
|
|
2155
|
+
const linkedInProfilePageSignalCache = new Map();
|
|
2156
|
+
const linkedInCompanyPageSignalCache = new Map();
|
|
2157
|
+
const serperSearchCache = new Map();
|
|
2158
|
+
let serperCreditsExhausted = false;
|
|
2159
|
+
function extractKeywordPhrases(value) {
|
|
2160
|
+
const normalized = normalizeLookupWhitespace(value);
|
|
2161
|
+
if (!normalized) {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
const phrases = new Set();
|
|
2165
|
+
const push = (candidate) => {
|
|
2166
|
+
const cleaned = normalizeLookupWhitespace(candidate);
|
|
2167
|
+
if (!cleaned || cleaned.length < 3) {
|
|
2168
|
+
return;
|
|
1610
2169
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
2170
|
+
phrases.add(cleaned);
|
|
2171
|
+
};
|
|
2172
|
+
push(normalized);
|
|
2173
|
+
push(normalizeLookupCompanyForSearch(normalized));
|
|
2174
|
+
push(aggressivelyCleanLookupCompanyName(normalized));
|
|
2175
|
+
const titleStripped = normalized
|
|
2176
|
+
.replace(/\|\s*linkedin$/i, "")
|
|
2177
|
+
.replace(/\|\s*overview$/i, "")
|
|
2178
|
+
.replace(/\b(linkedin|home|about|posts|see all details)\b/gi, " ")
|
|
2179
|
+
.replace(/\s+/g, " ")
|
|
2180
|
+
.trim();
|
|
2181
|
+
push(titleStripped);
|
|
2182
|
+
const parts = titleStripped
|
|
2183
|
+
.split(/[|,·•:()/-]+/)
|
|
2184
|
+
.map((part) => normalizeLookupWhitespace(part))
|
|
2185
|
+
.filter(Boolean);
|
|
2186
|
+
for (const part of parts) {
|
|
2187
|
+
push(part);
|
|
2188
|
+
}
|
|
2189
|
+
const looseTokens = normalizeLooseMatchText(titleStripped)
|
|
2190
|
+
.split(/\s+/)
|
|
2191
|
+
.filter((token) => token.length >= 4)
|
|
2192
|
+
.filter((token) => ![
|
|
2193
|
+
"group",
|
|
2194
|
+
"holding",
|
|
2195
|
+
"services",
|
|
2196
|
+
"service",
|
|
2197
|
+
"consulting",
|
|
2198
|
+
"gmbh",
|
|
2199
|
+
"publishing",
|
|
2200
|
+
"company",
|
|
2201
|
+
"linkedin",
|
|
2202
|
+
"deutschland"
|
|
2203
|
+
].includes(token));
|
|
2204
|
+
if (looseTokens.length > 0) {
|
|
2205
|
+
push(looseTokens[0]);
|
|
2206
|
+
push(looseTokens.slice(0, 2).join(" "));
|
|
2207
|
+
push(looseTokens.slice(-2).join(" "));
|
|
2208
|
+
}
|
|
2209
|
+
return Array.from(phrases);
|
|
2210
|
+
}
|
|
2211
|
+
async function buildLinkedInProfileCompanyHints(contact, timeoutMs) {
|
|
2212
|
+
const phrases = new Set();
|
|
2213
|
+
const keywords = new Set();
|
|
2214
|
+
const addPhrase = (value) => {
|
|
2215
|
+
for (const phrase of extractKeywordPhrases(value)) {
|
|
2216
|
+
phrases.add(phrase);
|
|
2217
|
+
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2218
|
+
.split(/\s+/)
|
|
2219
|
+
.filter((token) => token.length >= 4)
|
|
2220
|
+
.filter((token) => ![
|
|
2221
|
+
"group",
|
|
2222
|
+
"holding",
|
|
2223
|
+
"services",
|
|
2224
|
+
"service",
|
|
2225
|
+
"consulting",
|
|
2226
|
+
"gmbh",
|
|
2227
|
+
"publishing",
|
|
2228
|
+
"company",
|
|
2229
|
+
"linkedin",
|
|
2230
|
+
"deutschland"
|
|
2231
|
+
].includes(token));
|
|
2232
|
+
for (const token of looseTokens.slice(0, 5)) {
|
|
2233
|
+
keywords.add(token);
|
|
2234
|
+
}
|
|
2235
|
+
if (looseTokens.length > 1) {
|
|
2236
|
+
keywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2237
|
+
keywords.add(looseTokens.slice(-2).join(" "));
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
addPhrase(contact.companyNameOriginal ?? contact.companyName);
|
|
2242
|
+
const linkedInHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "");
|
|
2243
|
+
if (linkedInHandle && !/^\d+$/.test(linkedInHandle)) {
|
|
2244
|
+
addPhrase(linkedInHandle.replace(/[-_]+/g, " "));
|
|
2245
|
+
}
|
|
2246
|
+
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2247
|
+
const emailDomain = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail)
|
|
2248
|
+
? normalizedEmail.split("@")[1] ?? ""
|
|
2249
|
+
: "";
|
|
2250
|
+
if (emailDomain) {
|
|
2251
|
+
const normalizedDomain = emailDomain.replace(/^www\./i, "");
|
|
2252
|
+
keywords.add(normalizedDomain);
|
|
2253
|
+
const host = normalizedDomain.split(".")[0] ?? "";
|
|
2254
|
+
if (host) {
|
|
2255
|
+
addPhrase(host.replace(/[-_]+/g, " "));
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
const companyUrl = contact.linkedinCompanyUrl?.trim();
|
|
2259
|
+
if (companyUrl) {
|
|
2260
|
+
const cacheKey = companyUrl.replace(/\/$/, "");
|
|
2261
|
+
let cachedHints = linkedInCompanyHintCache.get(cacheKey);
|
|
2262
|
+
if (!cachedHints) {
|
|
2263
|
+
const signals = await fetchLinkedInCompanyPageSignals(companyUrl, timeoutMs);
|
|
2264
|
+
cachedHints = signals ? [...extractKeywordPhrases(signals.title), ...extractKeywordPhrases(signals.description)] : [];
|
|
2265
|
+
linkedInCompanyHintCache.set(cacheKey, cachedHints);
|
|
2266
|
+
}
|
|
2267
|
+
for (const hint of cachedHints) {
|
|
2268
|
+
addPhrase(hint);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return {
|
|
2272
|
+
phrases: Array.from(phrases)
|
|
2273
|
+
.map((value) => normalizeLookupWhitespace(value))
|
|
2274
|
+
.filter((value) => value.length > 0),
|
|
2275
|
+
keywords: Array.from(keywords)
|
|
2276
|
+
.map((value) => normalizeLookupWhitespace(value))
|
|
2277
|
+
.filter((value) => value.length > 0)
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
async function buildSerperLinkedInProfileQueries(contact, timeoutMs) {
|
|
2281
|
+
const fullName = normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`);
|
|
2282
|
+
const title = normalizeLookupWhitespace(contact.jobTitle);
|
|
2283
|
+
const queryEntries = [];
|
|
2284
|
+
const seenQueries = new Set();
|
|
2285
|
+
const pushQuery = (query, score) => {
|
|
2286
|
+
const normalized = normalizeLookupWhitespace(query);
|
|
2287
|
+
if (!normalized) {
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const key = normalized.toLowerCase();
|
|
2291
|
+
if (seenQueries.has(key)) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
seenQueries.add(key);
|
|
2295
|
+
queryEntries.push({ query: normalized, score });
|
|
2296
|
+
};
|
|
2297
|
+
const { phrases, keywords } = await buildLinkedInProfileCompanyHints(contact, timeoutMs);
|
|
2298
|
+
const enrichedPhrases = new Set(phrases);
|
|
2299
|
+
const enrichedKeywords = new Set(keywords);
|
|
2300
|
+
const preferredPhrases = [];
|
|
2301
|
+
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2302
|
+
const trustedEmailDomain = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail)
|
|
2303
|
+
? normalizedEmail.split("@")[1]?.replace(/^www\./i, "") ?? ""
|
|
2304
|
+
: "";
|
|
2305
|
+
const emailHost = trustedEmailDomain.split(".")[0] ?? "";
|
|
2306
|
+
const emailDomain = trustedEmailDomain;
|
|
2307
|
+
const linkedInHandle = normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? "";
|
|
2308
|
+
if (contact.linkedinCompanyUrl?.trim()) {
|
|
2309
|
+
const companySignals = await fetchLinkedInCompanyPageSignals(contact.linkedinCompanyUrl.trim(), timeoutMs);
|
|
2310
|
+
for (const phrase of [
|
|
2311
|
+
...extractKeywordPhrases(companySignals?.title),
|
|
2312
|
+
...extractKeywordPhrases(companySignals?.description)
|
|
2313
|
+
]) {
|
|
2314
|
+
enrichedPhrases.add(phrase);
|
|
2315
|
+
preferredPhrases.push(phrase);
|
|
2316
|
+
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2317
|
+
.split(/\s+/)
|
|
2318
|
+
.filter((token) => token.length >= 4)
|
|
2319
|
+
.filter((token) => ![
|
|
2320
|
+
"group",
|
|
2321
|
+
"holding",
|
|
2322
|
+
"services",
|
|
2323
|
+
"service",
|
|
2324
|
+
"consulting",
|
|
2325
|
+
"gmbh",
|
|
2326
|
+
"publishing",
|
|
2327
|
+
"company",
|
|
2328
|
+
"linkedin",
|
|
2329
|
+
"deutschland"
|
|
2330
|
+
].includes(token));
|
|
2331
|
+
for (const token of looseTokens.slice(0, 4)) {
|
|
2332
|
+
enrichedKeywords.add(token);
|
|
2333
|
+
}
|
|
2334
|
+
if (looseTokens.length > 1) {
|
|
2335
|
+
enrichedKeywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
const phrasePriority = (value) => {
|
|
2340
|
+
const loose = normalizeLooseMatchText(value);
|
|
2341
|
+
const tokenCount = loose.split(/\s+/).filter(Boolean).length;
|
|
2342
|
+
let score = 0;
|
|
2343
|
+
if (emailHost && loose.includes(normalizeLooseMatchText(emailHost)))
|
|
2344
|
+
score += 80;
|
|
2345
|
+
if (linkedInHandle && loose.includes(normalizeLooseMatchText(linkedInHandle)))
|
|
2346
|
+
score += 60;
|
|
2347
|
+
if (tokenCount >= 1 && tokenCount <= 4)
|
|
2348
|
+
score += 40;
|
|
2349
|
+
if (!/\b(gmbh|holding|services|service|consulting|kg|co)\b/i.test(value))
|
|
2350
|
+
score += 20;
|
|
2351
|
+
if (tokenCount > 7)
|
|
2352
|
+
score -= 40;
|
|
2353
|
+
return score;
|
|
2354
|
+
};
|
|
2355
|
+
const keywordPriority = (value) => {
|
|
2356
|
+
const loose = normalizeLooseMatchText(value);
|
|
2357
|
+
let score = 0;
|
|
2358
|
+
if (emailHost && loose.includes(normalizeLooseMatchText(emailHost)))
|
|
2359
|
+
score += 80;
|
|
2360
|
+
if (linkedInHandle && loose.includes(normalizeLooseMatchText(linkedInHandle)))
|
|
2361
|
+
score += 60;
|
|
2362
|
+
if (value.includes("."))
|
|
2363
|
+
score += 20;
|
|
2364
|
+
if (loose.split(/\s+/).filter(Boolean).length <= 2)
|
|
2365
|
+
score += 10;
|
|
2366
|
+
return score;
|
|
2367
|
+
};
|
|
2368
|
+
const rankedPhrases = [...enrichedPhrases].sort((left, right) => {
|
|
2369
|
+
const preferredDelta = Number(preferredPhrases.includes(right)) - Number(preferredPhrases.includes(left));
|
|
2370
|
+
if (preferredDelta !== 0) {
|
|
2371
|
+
return preferredDelta;
|
|
2372
|
+
}
|
|
2373
|
+
return phrasePriority(right) - phrasePriority(left);
|
|
2374
|
+
});
|
|
2375
|
+
const cleanPhrases = rankedPhrases.slice(0, 6);
|
|
2376
|
+
const fallbackKeywords = new Set(enrichedKeywords);
|
|
2377
|
+
for (const phrase of cleanPhrases) {
|
|
2378
|
+
const looseTokens = normalizeLooseMatchText(phrase)
|
|
2379
|
+
.split(/\s+/)
|
|
2380
|
+
.filter((token) => token.length >= 4)
|
|
2381
|
+
.filter((token) => ![
|
|
2382
|
+
"group",
|
|
2383
|
+
"holding",
|
|
2384
|
+
"services",
|
|
2385
|
+
"service",
|
|
2386
|
+
"consulting",
|
|
2387
|
+
"gmbh",
|
|
2388
|
+
"publishing",
|
|
2389
|
+
"company",
|
|
2390
|
+
"linkedin",
|
|
2391
|
+
"deutschland"
|
|
2392
|
+
].includes(token));
|
|
2393
|
+
for (const token of looseTokens.slice(0, 3)) {
|
|
2394
|
+
fallbackKeywords.add(token);
|
|
2395
|
+
}
|
|
2396
|
+
if (looseTokens.length > 1) {
|
|
2397
|
+
fallbackKeywords.add(looseTokens.slice(0, 2).join(" "));
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (emailHost) {
|
|
2401
|
+
fallbackKeywords.add(emailHost);
|
|
2402
|
+
}
|
|
2403
|
+
if (emailDomain) {
|
|
2404
|
+
fallbackKeywords.add(emailDomain);
|
|
2405
|
+
}
|
|
2406
|
+
if (linkedInHandle) {
|
|
2407
|
+
fallbackKeywords.add(linkedInHandle);
|
|
2408
|
+
}
|
|
2409
|
+
const cleanKeywords = [...fallbackKeywords]
|
|
2410
|
+
.sort((left, right) => keywordPriority(right) - keywordPriority(left))
|
|
2411
|
+
.slice(0, 5);
|
|
2412
|
+
cleanKeywords.forEach((keyword, index) => {
|
|
2413
|
+
const keywordScore = 260 - index * 15;
|
|
2414
|
+
pushQuery(`site:linkedin.com/in "${fullName}" ${keyword} linkedin`, keywordScore);
|
|
2415
|
+
pushQuery(`site:linkedin.com/in ${fullName} ${keyword} linkedin`, keywordScore - 5);
|
|
2416
|
+
if (title) {
|
|
2417
|
+
pushQuery(`site:linkedin.com/in "${fullName}" ${keyword} "${title}"`, keywordScore - 10);
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
cleanPhrases.forEach((companyName, index) => {
|
|
2421
|
+
const phraseScore = 180 - index * 10;
|
|
2422
|
+
pushQuery(`site:linkedin.com/in "${fullName}" "${companyName}"`, phraseScore);
|
|
2423
|
+
pushQuery(`site:linkedin.com/in ${fullName} ${companyName} linkedin`, phraseScore - 5);
|
|
2424
|
+
if (title) {
|
|
2425
|
+
pushQuery(`site:linkedin.com/in "${fullName}" "${companyName}" "${title}"`, phraseScore - 10);
|
|
2426
|
+
pushQuery(`site:linkedin.com/in ${fullName} ${companyName} ${title} linkedin`, phraseScore - 15);
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
if (emailDomain) {
|
|
2430
|
+
pushQuery(`site:linkedin.com/in "${fullName}" "${emailDomain}" linkedin`, 240);
|
|
2431
|
+
}
|
|
2432
|
+
pushQuery(`site:linkedin.com/in "${fullName}" linkedin`, 50);
|
|
2433
|
+
if (title) {
|
|
2434
|
+
pushQuery(`site:linkedin.com/in "${fullName}" "${title}" linkedin`, 40);
|
|
2435
|
+
}
|
|
2436
|
+
return queryEntries
|
|
2437
|
+
.sort((left, right) => right.score - left.score)
|
|
2438
|
+
.map((entry) => entry.query);
|
|
2439
|
+
}
|
|
2440
|
+
function extractPublicLinkedInProfileSearchCandidates(bodyText) {
|
|
2441
|
+
const candidates = new Set();
|
|
2442
|
+
const directMatches = bodyText.match(/https:\/\/(?:(?:www|[a-z]{2})\.)?linkedin\.com\/in\/[^"'&<>\s)]+/gi) ?? [];
|
|
2443
|
+
for (const match of directMatches) {
|
|
2444
|
+
const normalized = normalizePublicLinkedInProfileUrl(match);
|
|
2445
|
+
if (normalized) {
|
|
2446
|
+
candidates.add(normalized);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const encodedMatches = bodyText.match(/https?%3A%2F%2F(?:(?:www|[a-z]{2})\.)?linkedin\.com%2Fin%2F[^"'&<>\s)]+/gi) ?? [];
|
|
2450
|
+
for (const match of encodedMatches) {
|
|
2451
|
+
try {
|
|
2452
|
+
const decoded = decodeURIComponent(match);
|
|
2453
|
+
const normalized = normalizePublicLinkedInProfileUrl(decoded);
|
|
2454
|
+
if (normalized) {
|
|
2455
|
+
candidates.add(normalized);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
// Ignore malformed encoded fragments.
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return Array.from(candidates);
|
|
2463
|
+
}
|
|
2464
|
+
function buildPublicLinkedInProfileSearchUrl(query) {
|
|
2465
|
+
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_PROFILE_SEARCH_BASE_URL?.trim() ||
|
|
2466
|
+
"https://duckduckgo.com/html/";
|
|
2467
|
+
const url = new URL(baseUrl);
|
|
2468
|
+
url.searchParams.set("q", query);
|
|
2469
|
+
return url.toString();
|
|
2470
|
+
}
|
|
2471
|
+
async function fetchSerperSearchResults(query, num, timeoutMs) {
|
|
2472
|
+
if (serperCreditsExhausted) {
|
|
2473
|
+
return null;
|
|
2474
|
+
}
|
|
2475
|
+
const apiKey = getSerperApiKey();
|
|
2476
|
+
if (!apiKey) {
|
|
2477
|
+
return null;
|
|
2478
|
+
}
|
|
2479
|
+
const cacheKey = `${query}::${num}`;
|
|
2480
|
+
if (serperSearchCache.has(cacheKey)) {
|
|
2481
|
+
return serperSearchCache.get(cacheKey) ?? null;
|
|
2482
|
+
}
|
|
2483
|
+
const controller = new AbortController();
|
|
2484
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2485
|
+
try {
|
|
2486
|
+
const response = await fetch(getSerperSearchEndpoint(), {
|
|
2487
|
+
method: "POST",
|
|
2488
|
+
signal: controller.signal,
|
|
2489
|
+
headers: {
|
|
2490
|
+
"X-API-KEY": apiKey,
|
|
2491
|
+
"Content-Type": "application/json"
|
|
2492
|
+
},
|
|
2493
|
+
body: JSON.stringify({ q: query, num })
|
|
2494
|
+
});
|
|
2495
|
+
if (!response.ok) {
|
|
2496
|
+
const bodyText = await response.text().catch(() => "");
|
|
2497
|
+
if (response.status === 400 &&
|
|
2498
|
+
/not enough credits/i.test(bodyText)) {
|
|
2499
|
+
serperCreditsExhausted = true;
|
|
2500
|
+
}
|
|
2501
|
+
serperSearchCache.set(cacheKey, null);
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
const parsed = await response.json();
|
|
2505
|
+
serperSearchCache.set(cacheKey, parsed);
|
|
2506
|
+
return parsed;
|
|
2507
|
+
}
|
|
2508
|
+
catch {
|
|
2509
|
+
return null;
|
|
2510
|
+
}
|
|
2511
|
+
finally {
|
|
2512
|
+
clearTimeout(timeout);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function extractSerperLinkedInProfileCandidates(payload) {
|
|
2516
|
+
if (!payload || typeof payload !== "object") {
|
|
2517
|
+
return [];
|
|
2518
|
+
}
|
|
2519
|
+
const organic = "organic" in payload && Array.isArray(payload.organic)
|
|
2520
|
+
? (payload.organic ?? [])
|
|
2521
|
+
: [];
|
|
2522
|
+
const seen = new Set();
|
|
2523
|
+
const candidates = [];
|
|
2524
|
+
for (const result of organic) {
|
|
2525
|
+
if (!result || typeof result !== "object")
|
|
2526
|
+
continue;
|
|
2527
|
+
const link = "link" in result && typeof result.link === "string"
|
|
2528
|
+
? result.link
|
|
2529
|
+
: "";
|
|
2530
|
+
const normalized = normalizePublicLinkedInProfileUrl(link);
|
|
2531
|
+
if (normalized) {
|
|
2532
|
+
const canonical = normalized.replace(/\/$/, "");
|
|
2533
|
+
if (!seen.has(canonical)) {
|
|
2534
|
+
seen.add(canonical);
|
|
2535
|
+
candidates.push({
|
|
2536
|
+
url: canonical,
|
|
2537
|
+
title: "title" in result && typeof result.title === "string"
|
|
2538
|
+
? normalizeLookupWhitespace(result.title)
|
|
2539
|
+
: "",
|
|
2540
|
+
snippet: "snippet" in result && typeof result.snippet === "string"
|
|
2541
|
+
? normalizeLookupWhitespace(result.snippet)
|
|
2542
|
+
: ""
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
return candidates;
|
|
2548
|
+
}
|
|
2549
|
+
async function fetchLinkedInProfilePageSignals(url, timeoutMs) {
|
|
2550
|
+
const cacheKey = normalizePublicLinkedInProfileUrl(url)?.replace(/\/$/, "") ?? url.replace(/\/$/, "");
|
|
2551
|
+
if (linkedInProfilePageSignalCache.has(cacheKey)) {
|
|
2552
|
+
return linkedInProfilePageSignalCache.get(cacheKey) ?? null;
|
|
2553
|
+
}
|
|
2554
|
+
const controller = new AbortController();
|
|
2555
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2556
|
+
try {
|
|
2557
|
+
const targetUrl = rewriteLinkedInUrlForConfiguredBase(url);
|
|
2558
|
+
const response = await fetch(targetUrl, {
|
|
2559
|
+
method: "GET",
|
|
2560
|
+
signal: controller.signal,
|
|
2561
|
+
headers: {
|
|
2562
|
+
"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"
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
const html = await response.text();
|
|
2566
|
+
const finalUrl = normalizePublicLinkedInProfileUrl(url) ||
|
|
2567
|
+
normalizePublicLinkedInProfileUrl(response.url || url);
|
|
2568
|
+
if (!finalUrl) {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const title = decodeHtmlEntities(html.match(/<title[^>]*>([^<]+)/i)?.[1] || "") ||
|
|
2572
|
+
decodeHtmlEntities(html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2573
|
+
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2574
|
+
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
2575
|
+
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
2576
|
+
const unavailable = response.status >= 400 ||
|
|
2577
|
+
unavailableText.includes("page not found") ||
|
|
2578
|
+
unavailableText.includes("profile not found") ||
|
|
2579
|
+
unavailableText.includes("member profile") && unavailableText.includes("not available");
|
|
2580
|
+
const result = {
|
|
2581
|
+
normalizedUrl: finalUrl.replace(/\/$/, ""),
|
|
2582
|
+
title: normalizeLookupWhitespace(title),
|
|
2583
|
+
description: normalizeLookupWhitespace(description),
|
|
2584
|
+
bodyText: normalizeLookupWhitespace(bodyText),
|
|
2585
|
+
unavailable
|
|
2586
|
+
};
|
|
2587
|
+
linkedInProfilePageSignalCache.set(cacheKey, result);
|
|
2588
|
+
return result;
|
|
2589
|
+
}
|
|
2590
|
+
catch {
|
|
2591
|
+
linkedInProfilePageSignalCache.set(cacheKey, null);
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
finally {
|
|
2595
|
+
clearTimeout(timeout);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function scoreLinkedInProfilePageSignals(contact, signals) {
|
|
2599
|
+
const fullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
2600
|
+
const companyHints = [
|
|
2601
|
+
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
2602
|
+
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName))
|
|
2603
|
+
].filter(Boolean);
|
|
2604
|
+
const titleHint = normalizeLooseMatchText(contact.jobTitle);
|
|
2605
|
+
const haystack = normalizeLooseMatchText(`${signals.title} ${signals.description} ${signals.bodyText}`);
|
|
2606
|
+
let score = 0;
|
|
2607
|
+
if (fullName && haystack.includes(fullName))
|
|
2608
|
+
score += 120;
|
|
2609
|
+
for (const hint of companyHints) {
|
|
2610
|
+
if (hint && haystack.includes(hint))
|
|
2611
|
+
score += 30;
|
|
2612
|
+
}
|
|
2613
|
+
if (titleHint) {
|
|
2614
|
+
const titleWords = titleHint.split(/\s+/).filter((token) => token.length >= 4).slice(0, 4);
|
|
2615
|
+
score += titleWords.filter((token) => haystack.includes(token)).length * 8;
|
|
2616
|
+
}
|
|
2617
|
+
const slug = signals.normalizedUrl.split("/in/")[1]?.replace(/\/$/, "") ?? "";
|
|
2618
|
+
const slugText = normalizeLooseMatchText(slug.replace(/[-_]+/g, " "));
|
|
2619
|
+
if (fullName && slugText.includes(contact.firstName.toLowerCase()) && slugText.includes(contact.lastName.toLowerCase())) {
|
|
2620
|
+
score += 40;
|
|
2621
|
+
}
|
|
2622
|
+
return score;
|
|
2623
|
+
}
|
|
2624
|
+
function analyzeSerperLinkedInProfileCandidate(contact, candidate) {
|
|
2625
|
+
const fullName = normalizeLooseMatchText(`${contact.firstName} ${contact.lastName}`);
|
|
2626
|
+
const titleHint = normalizeLooseMatchText(contact.jobTitle);
|
|
2627
|
+
const companyTokens = [
|
|
2628
|
+
normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName),
|
|
2629
|
+
normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(contact.companyNameOriginal ?? contact.companyName)),
|
|
2630
|
+
normalizeLooseMatchText(normalizeLinkedInCompanyHandle(contact.linkedinCompanyUrl ?? "")?.replace(/[-_]+/g, " ") ?? ""),
|
|
2631
|
+
normalizeLooseMatchText((() => {
|
|
2632
|
+
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
2633
|
+
if (!normalizedEmail || isSyntheticLinkedInLookupEmail(normalizedEmail)) {
|
|
2634
|
+
return "";
|
|
2635
|
+
}
|
|
2636
|
+
return normalizedEmail.split("@")[1]?.replace(/^www\./i, "").split(".")[0] ?? "";
|
|
2637
|
+
})())
|
|
2638
|
+
].filter(Boolean);
|
|
2639
|
+
const haystack = normalizeLooseMatchText(`${candidate.title} ${candidate.snippet}`);
|
|
2640
|
+
let score = 0;
|
|
2641
|
+
let companyMatches = 0;
|
|
2642
|
+
let titleMatches = 0;
|
|
2643
|
+
if (fullName && haystack.includes(fullName))
|
|
2644
|
+
score += 120;
|
|
2645
|
+
for (const token of companyTokens) {
|
|
2646
|
+
if (!token)
|
|
2647
|
+
continue;
|
|
2648
|
+
if (haystack.includes(token)) {
|
|
2649
|
+
companyMatches += 1;
|
|
2650
|
+
score += token.split(/\s+/).length <= 2 ? 30 : 20;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
if (titleHint) {
|
|
2654
|
+
const titleWords = titleHint.split(/\s+/).filter((token) => token.length >= 4).slice(0, 4);
|
|
2655
|
+
titleMatches = titleWords.filter((token) => haystack.includes(token)).length;
|
|
2656
|
+
score += titleMatches * 8;
|
|
2657
|
+
}
|
|
2658
|
+
const slugText = normalizeLooseMatchText(candidate.url.split("/in/")[1]?.replace(/\/$/, "").replace(/[-_]+/g, " ") ?? "");
|
|
2659
|
+
if (fullName &&
|
|
2660
|
+
slugText.includes(contact.firstName.toLowerCase()) &&
|
|
2661
|
+
slugText.includes(contact.lastName.toLowerCase()) &&
|
|
2662
|
+
(companyMatches > 0 || titleMatches > 0)) {
|
|
2663
|
+
score += 40;
|
|
2664
|
+
}
|
|
2665
|
+
return { score, companyMatches, titleMatches };
|
|
2666
|
+
}
|
|
2667
|
+
async function searchSerperLinkedInProfileUrl(contact, timeoutMs, options) {
|
|
2668
|
+
if (!contact.firstName || !contact.lastName) {
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
const maxQueries = options?.maxQueries && Number.isFinite(options.maxQueries) && options.maxQueries > 0
|
|
2672
|
+
? Math.trunc(options.maxQueries)
|
|
2673
|
+
: Number.POSITIVE_INFINITY;
|
|
2674
|
+
for (const query of (await buildSerperLinkedInProfileQueries(contact, timeoutMs)).slice(0, maxQueries)) {
|
|
2675
|
+
try {
|
|
2676
|
+
const parsed = await fetchSerperSearchResults(query, 5, timeoutMs);
|
|
2677
|
+
if (!parsed) {
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const candidates = extractSerperLinkedInProfileCandidates(parsed);
|
|
2681
|
+
let bestUrl = null;
|
|
2682
|
+
let bestScore = 0;
|
|
2683
|
+
for (const candidate of candidates) {
|
|
2684
|
+
const serperAnalysis = analyzeSerperLinkedInProfileCandidate(contact, candidate);
|
|
2685
|
+
const serperScore = serperAnalysis.score;
|
|
2686
|
+
if (serperScore >= 150 && (serperAnalysis.companyMatches > 0 || serperAnalysis.titleMatches > 0)) {
|
|
2687
|
+
return candidate.url;
|
|
2688
|
+
}
|
|
2689
|
+
const signals = await fetchLinkedInProfilePageSignals(candidate.url, timeoutMs);
|
|
2690
|
+
if (!signals || signals.unavailable) {
|
|
2691
|
+
if (serperScore > bestScore) {
|
|
2692
|
+
bestScore = serperScore;
|
|
2693
|
+
bestUrl = candidate.url;
|
|
2694
|
+
}
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2697
|
+
const score = Math.max(serperScore, scoreLinkedInProfilePageSignals(contact, signals));
|
|
2698
|
+
if (score > bestScore) {
|
|
2699
|
+
bestScore = score;
|
|
2700
|
+
bestUrl = signals.normalizedUrl;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
if (bestUrl && bestScore >= 130) {
|
|
2704
|
+
return bestUrl;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
// Continue with the next query variant.
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return searchPublicLinkedInProfileUrl(contact, timeoutMs, {
|
|
2712
|
+
maxQueries: Math.min(Number.isFinite(maxQueries) ? maxQueries : 4, 4)
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
function decodeHtmlEntities(value) {
|
|
2716
|
+
return value
|
|
2717
|
+
.replace(/&/gi, "&")
|
|
2718
|
+
.replace(/"/gi, '"')
|
|
2719
|
+
.replace(/'/gi, "'")
|
|
2720
|
+
.replace(/</gi, "<")
|
|
2721
|
+
.replace(/>/gi, ">");
|
|
2722
|
+
}
|
|
2723
|
+
async function fetchLinkedInCompanyPageSignals(url, timeoutMs) {
|
|
2724
|
+
const cacheKey = url.replace(/\/$/, "");
|
|
2725
|
+
if (linkedInCompanyPageSignalCache.has(cacheKey)) {
|
|
2726
|
+
return linkedInCompanyPageSignalCache.get(cacheKey) ?? null;
|
|
2727
|
+
}
|
|
2728
|
+
const controller = new AbortController();
|
|
2729
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2730
|
+
try {
|
|
2731
|
+
const response = await fetch(url, {
|
|
2732
|
+
method: "GET",
|
|
2733
|
+
signal: controller.signal,
|
|
2734
|
+
headers: {
|
|
2735
|
+
"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"
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
const html = await response.text();
|
|
2739
|
+
const finalUrl = response.url || url;
|
|
2740
|
+
const title = decodeHtmlEntities(html.match(/<title[^>]*>([^<]+)/i)?.[1] || "") ||
|
|
2741
|
+
decodeHtmlEntities(html.match(/<meta[^>]+property="og:title"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2742
|
+
const description = decodeHtmlEntities(html.match(/<meta[^>]+name="description"[^>]+content="([^"]+)/i)?.[1] || "");
|
|
2743
|
+
const bodyText = decodeHtmlEntities(html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " "));
|
|
2744
|
+
const unavailableText = normalizeLooseMatchText(`${title} ${description} ${bodyText}`);
|
|
2745
|
+
const unavailable = response.status >= 400 ||
|
|
2746
|
+
unavailableText.includes("page not found") ||
|
|
2747
|
+
unavailableText.includes("this page does not exist") ||
|
|
2748
|
+
unavailableText.includes("page isnt available");
|
|
2749
|
+
const result = {
|
|
2750
|
+
normalizedUrl: normalizeLinkedInCompanyHandle(finalUrl ?? "") || normalizeLinkedInCompanyHandle(url)
|
|
2751
|
+
? normalizeLinkedInCompanyPage(normalizeLinkedInCompanyHandle(finalUrl ?? "") ?? normalizeLinkedInCompanyHandle(url) ?? "")
|
|
2752
|
+
: finalUrl,
|
|
1613
2753
|
title: normalizeLookupWhitespace(title),
|
|
1614
2754
|
description: normalizeLookupWhitespace(description),
|
|
1615
2755
|
bodyText: normalizeLookupWhitespace(bodyText),
|
|
1616
2756
|
unavailable
|
|
1617
2757
|
};
|
|
2758
|
+
linkedInCompanyPageSignalCache.set(cacheKey, result);
|
|
2759
|
+
return result;
|
|
1618
2760
|
}
|
|
1619
2761
|
catch {
|
|
2762
|
+
linkedInCompanyPageSignalCache.set(cacheKey, null);
|
|
1620
2763
|
return null;
|
|
1621
2764
|
}
|
|
1622
2765
|
finally {
|
|
1623
2766
|
clearTimeout(timeout);
|
|
1624
2767
|
}
|
|
1625
2768
|
}
|
|
2769
|
+
async function searchPublicLinkedInProfileUrl(contact, timeoutMs, options) {
|
|
2770
|
+
const maxQueries = options?.maxQueries && Number.isFinite(options.maxQueries) && options.maxQueries > 0
|
|
2771
|
+
? Math.trunc(options.maxQueries)
|
|
2772
|
+
: 4;
|
|
2773
|
+
const queries = (await buildSerperLinkedInProfileQueries(contact, timeoutMs)).slice(0, maxQueries);
|
|
2774
|
+
for (const query of queries) {
|
|
2775
|
+
const controller = new AbortController();
|
|
2776
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(timeoutMs, 12_000));
|
|
2777
|
+
try {
|
|
2778
|
+
const response = await fetch(buildPublicLinkedInProfileSearchUrl(query), {
|
|
2779
|
+
method: "GET",
|
|
2780
|
+
signal: controller.signal,
|
|
2781
|
+
headers: {
|
|
2782
|
+
"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"
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
if (!response.ok) {
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
const bodyText = await response.text();
|
|
2789
|
+
const candidates = extractPublicLinkedInProfileSearchCandidates(bodyText);
|
|
2790
|
+
let bestUrl = null;
|
|
2791
|
+
let bestScore = 0;
|
|
2792
|
+
for (const candidateUrl of candidates.slice(0, 5)) {
|
|
2793
|
+
const signals = await fetchLinkedInProfilePageSignals(candidateUrl, timeoutMs);
|
|
2794
|
+
if (!signals || signals.unavailable) {
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
const score = scoreLinkedInProfilePageSignals(contact, signals);
|
|
2798
|
+
if (score > bestScore) {
|
|
2799
|
+
bestScore = score;
|
|
2800
|
+
bestUrl = signals.normalizedUrl;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (bestUrl && bestScore >= 130) {
|
|
2804
|
+
return bestUrl;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
catch {
|
|
2808
|
+
// Continue with the next query variant.
|
|
2809
|
+
}
|
|
2810
|
+
finally {
|
|
2811
|
+
clearTimeout(timeout);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
return null;
|
|
2815
|
+
}
|
|
1626
2816
|
function scoreLinkedInCompanyPageSignals(companyName, signals) {
|
|
1627
2817
|
const inputTokens = normalizeLooseMatchText(companyName).split(/\s+/).filter((token) => token.length >= 4);
|
|
1628
2818
|
const haystack = normalizeLooseMatchText(`${signals.title} ${signals.description}`);
|
|
@@ -1637,6 +2827,20 @@ function scoreLinkedInCompanyPageSignals(companyName, signals) {
|
|
|
1637
2827
|
}
|
|
1638
2828
|
return score;
|
|
1639
2829
|
}
|
|
2830
|
+
function scoreSerperLinkedInCompanyCandidate(companyName, candidate) {
|
|
2831
|
+
const inputTokens = normalizeLooseMatchText(companyName).split(/\s+/).filter((token) => token.length >= 4);
|
|
2832
|
+
const haystack = normalizeLooseMatchText(`${candidate.title} ${candidate.snippet}`);
|
|
2833
|
+
let score = scoreLinkedInCompanyUrlCandidate(companyName, candidate.url);
|
|
2834
|
+
for (const token of inputTokens) {
|
|
2835
|
+
if (haystack.includes(token)) {
|
|
2836
|
+
score += 12;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (haystack.includes(normalizeLooseMatchText(aggressivelyCleanLookupCompanyName(companyName)))) {
|
|
2840
|
+
score += 40;
|
|
2841
|
+
}
|
|
2842
|
+
return score;
|
|
2843
|
+
}
|
|
1640
2844
|
function scoreLinkedInCompanyUrlCandidate(companyName, url) {
|
|
1641
2845
|
const handle = normalizeLinkedInCompanyHandle(url);
|
|
1642
2846
|
if (!handle || /^\d+$/.test(handle)) {
|
|
@@ -1730,9 +2934,15 @@ async function searchSerperLinkedInCompanyUrl(companyName, timeoutMs) {
|
|
|
1730
2934
|
const parsed = (await response.json());
|
|
1731
2935
|
const candidates = extractSerperLinkedInCompanyCandidates(parsed);
|
|
1732
2936
|
const ranked = candidates
|
|
1733
|
-
.map((
|
|
2937
|
+
.map((candidate) => ({
|
|
2938
|
+
...candidate,
|
|
2939
|
+
score: scoreSerperLinkedInCompanyCandidate(companyName, candidate)
|
|
2940
|
+
}))
|
|
1734
2941
|
.filter((candidate) => candidate.score > 0)
|
|
1735
2942
|
.sort((left, right) => right.score - left.score);
|
|
2943
|
+
if (ranked[0] && ranked[0].score >= 80) {
|
|
2944
|
+
return ranked[0].url;
|
|
2945
|
+
}
|
|
1736
2946
|
let anySignalsFetched = false;
|
|
1737
2947
|
let bestValidated = null;
|
|
1738
2948
|
for (const candidate of ranked.slice(0, 3)) {
|
|
@@ -1772,6 +2982,11 @@ async function searchSerperLinkedInCompanyUrl(companyName, timeoutMs) {
|
|
|
1772
2982
|
}
|
|
1773
2983
|
async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
1774
2984
|
const config = await readLinkedInDirectLookupConfig();
|
|
2985
|
+
const companyContexts = await resolveDirectLinkedInCompanyContexts({
|
|
2986
|
+
contacts: params.contacts.filter((contact) => !contact.isVariation),
|
|
2987
|
+
timeoutMs: params.timeoutMs,
|
|
2988
|
+
config
|
|
2989
|
+
});
|
|
1775
2990
|
const groupedContacts = new Map();
|
|
1776
2991
|
for (const contact of params.contacts) {
|
|
1777
2992
|
const key = contact.email?.trim().toLowerCase() || `contact:${contact.contact_id}`;
|
|
@@ -1780,15 +2995,25 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
1780
2995
|
groupedContacts.set(key, existing);
|
|
1781
2996
|
}
|
|
1782
2997
|
const results = [];
|
|
1783
|
-
|
|
2998
|
+
const perAttemptTimeoutMs = params.perAttemptTimeoutMs && Number.isFinite(params.perAttemptTimeoutMs) && params.perAttemptTimeoutMs > 0
|
|
2999
|
+
? Math.trunc(params.perAttemptTimeoutMs)
|
|
3000
|
+
: Math.min(params.timeoutMs, 8_000);
|
|
3001
|
+
const perContactBudgetMs = params.perContactBudgetMs && Number.isFinite(params.perContactBudgetMs) && params.perContactBudgetMs > 0
|
|
3002
|
+
? Math.trunc(params.perContactBudgetMs)
|
|
3003
|
+
: Math.min(params.timeoutMs, 15_000);
|
|
3004
|
+
const rateLimitCooldownMs = Math.max(750, Math.min(3_000, Math.trunc(perAttemptTimeoutMs / 2)));
|
|
3005
|
+
const maxRateLimitCooldowns = 4;
|
|
3006
|
+
let rateLimitCooldownUntil = 0;
|
|
3007
|
+
let consecutiveRateLimitCount = 0;
|
|
3008
|
+
let totalRateLimitCooldowns = 0;
|
|
1784
3009
|
for (const variations of groupedContacts.values()) {
|
|
1785
3010
|
const primary = variations.find((contact) => !contact.isVariation) ?? variations[0];
|
|
1786
3011
|
const blankPerson = !primary?.firstName.trim() || !primary?.lastName.trim();
|
|
1787
|
-
if (
|
|
3012
|
+
if (totalRateLimitCooldowns >= maxRateLimitCooldowns) {
|
|
1788
3013
|
results.push({
|
|
1789
3014
|
contact_id: primary.contact_id,
|
|
1790
3015
|
linkedin_url: null,
|
|
1791
|
-
error: "LinkedIn rate limit"
|
|
3016
|
+
error: "LinkedIn rate limit budget exhausted"
|
|
1792
3017
|
});
|
|
1793
3018
|
continue;
|
|
1794
3019
|
}
|
|
@@ -1802,11 +3027,23 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
1802
3027
|
}
|
|
1803
3028
|
let matchedUrl = null;
|
|
1804
3029
|
let matchedSalesNavUrl = null;
|
|
3030
|
+
let matchedFullName = null;
|
|
3031
|
+
let matchedCompanyName = null;
|
|
3032
|
+
let matchedTitle = null;
|
|
1805
3033
|
let lastError = null;
|
|
3034
|
+
const contactDeadline = Date.now() + perContactBudgetMs;
|
|
3035
|
+
const companyContext = companyContexts.get(buildDirectCompanyContextKey(primary));
|
|
1806
3036
|
for (const candidate of variations) {
|
|
1807
|
-
for (const searchVariant of buildLinkedInLookupSearchVariants(candidate)) {
|
|
3037
|
+
for (const searchVariant of await buildLinkedInLookupSearchVariants(candidate, params.timeoutMs, companyContext?.aliases ?? [])) {
|
|
3038
|
+
if (Date.now() < rateLimitCooldownUntil) {
|
|
3039
|
+
await new Promise((resolve) => setTimeout(resolve, rateLimitCooldownUntil - Date.now()));
|
|
3040
|
+
}
|
|
3041
|
+
if (Date.now() >= contactDeadline) {
|
|
3042
|
+
lastError = lastError || "Direct lookup budget exhausted";
|
|
3043
|
+
break;
|
|
3044
|
+
}
|
|
1808
3045
|
const controller = new AbortController();
|
|
1809
|
-
const timeout = setTimeout(controller.abort.bind(controller), Math.min(
|
|
3046
|
+
const timeout = setTimeout(controller.abort.bind(controller), Math.min(perAttemptTimeoutMs, Math.max(1_000, contactDeadline - Date.now())));
|
|
1810
3047
|
try {
|
|
1811
3048
|
const response = await fetch(buildLinkedInSalesApiUrl(searchVariant), {
|
|
1812
3049
|
method: "GET",
|
|
@@ -1827,20 +3064,51 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
1827
3064
|
}
|
|
1828
3065
|
});
|
|
1829
3066
|
if (response.status === 429) {
|
|
1830
|
-
rateLimited = true;
|
|
1831
3067
|
lastError = "LinkedIn rate limit";
|
|
3068
|
+
consecutiveRateLimitCount += 1;
|
|
3069
|
+
totalRateLimitCooldowns += 1;
|
|
3070
|
+
rateLimitCooldownUntil =
|
|
3071
|
+
Date.now() + Math.min(15_000, rateLimitCooldownMs * Math.max(1, consecutiveRateLimitCount));
|
|
3072
|
+
if (totalRateLimitCooldowns >= maxRateLimitCooldowns) {
|
|
3073
|
+
break;
|
|
3074
|
+
}
|
|
1832
3075
|
break;
|
|
1833
3076
|
}
|
|
1834
3077
|
if (!response.ok) {
|
|
1835
3078
|
lastError = `LinkedIn returned ${response.status}`;
|
|
1836
3079
|
continue;
|
|
1837
3080
|
}
|
|
3081
|
+
consecutiveRateLimitCount = 0;
|
|
3082
|
+
rateLimitCooldownUntil = 0;
|
|
1838
3083
|
const data = (await response.json());
|
|
1839
3084
|
const profilesFound = data.paging?.total ?? 0;
|
|
1840
3085
|
if (profilesFound > 0) {
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
3086
|
+
const bestCandidate = (data.elements ?? [])
|
|
3087
|
+
.map((element) => ({
|
|
3088
|
+
element,
|
|
3089
|
+
...scoreLinkedInSalesApiElementMatch(candidate, element)
|
|
3090
|
+
}))
|
|
3091
|
+
.sort((left, right) => right.score - left.score)[0];
|
|
3092
|
+
const hasTrustedCompanyContext = Boolean(candidate.linkedinCompanyUrl ||
|
|
3093
|
+
companyContext?.linkedinCompanyUrl ||
|
|
3094
|
+
companyContext?.matchedCompanyName);
|
|
3095
|
+
const hasTrustedEmailContext = Boolean(candidate.email && !isSyntheticLinkedInLookupEmail(candidate.email));
|
|
3096
|
+
const acceptBestCandidate = Boolean(bestCandidate &&
|
|
3097
|
+
(bestCandidate.score >= 140 ||
|
|
3098
|
+
(bestCandidate.exactNameMatch &&
|
|
3099
|
+
(bestCandidate.companyMatchCount > 0 || hasTrustedCompanyContext || hasTrustedEmailContext))));
|
|
3100
|
+
if (bestCandidate && acceptBestCandidate) {
|
|
3101
|
+
matchedUrl = extractLinkedInProfileUrlFromSalesApiElement(bestCandidate.element) ?? null;
|
|
3102
|
+
matchedSalesNavUrl = extractLinkedInSalesNavLeadUrlFromSalesApiElement(bestCandidate.element) ?? null;
|
|
3103
|
+
matchedFullName = bestCandidate.fullName;
|
|
3104
|
+
matchedCompanyName = bestCandidate.companyName;
|
|
3105
|
+
matchedTitle = bestCandidate.title;
|
|
3106
|
+
}
|
|
3107
|
+
else {
|
|
3108
|
+
lastError = bestCandidate
|
|
3109
|
+
? `LinkedIn top result score too low (${bestCandidate.score})`
|
|
3110
|
+
: "LinkedIn returned no usable results";
|
|
3111
|
+
}
|
|
1844
3112
|
if (matchedUrl || matchedSalesNavUrl) {
|
|
1845
3113
|
break;
|
|
1846
3114
|
}
|
|
@@ -1852,11 +3120,14 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
1852
3120
|
finally {
|
|
1853
3121
|
clearTimeout(timeout);
|
|
1854
3122
|
}
|
|
1855
|
-
if (matchedUrl || matchedSalesNavUrl ||
|
|
3123
|
+
if (matchedUrl || matchedSalesNavUrl || totalRateLimitCooldowns >= maxRateLimitCooldowns) {
|
|
1856
3124
|
break;
|
|
1857
3125
|
}
|
|
1858
3126
|
}
|
|
1859
|
-
if (matchedUrl || matchedSalesNavUrl ||
|
|
3127
|
+
if (matchedUrl || matchedSalesNavUrl || totalRateLimitCooldowns >= maxRateLimitCooldowns) {
|
|
3128
|
+
break;
|
|
3129
|
+
}
|
|
3130
|
+
if (Date.now() >= contactDeadline) {
|
|
1860
3131
|
break;
|
|
1861
3132
|
}
|
|
1862
3133
|
}
|
|
@@ -1864,16 +3135,21 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
1864
3135
|
contact_id: primary.contact_id,
|
|
1865
3136
|
linkedin_url: matchedUrl ?? matchedSalesNavUrl,
|
|
1866
3137
|
sales_nav_profile_url: matchedSalesNavUrl,
|
|
3138
|
+
matched_full_name: matchedFullName,
|
|
3139
|
+
matched_company_name: matchedCompanyName,
|
|
3140
|
+
matched_title: matchedTitle,
|
|
1867
3141
|
error: matchedUrl || matchedSalesNavUrl ? null : lastError
|
|
1868
3142
|
});
|
|
1869
3143
|
}
|
|
1870
3144
|
return {
|
|
1871
3145
|
success: true,
|
|
1872
|
-
contacts: results
|
|
3146
|
+
contacts: results,
|
|
3147
|
+
companyContexts: Array.from(companyContexts.values())
|
|
1873
3148
|
};
|
|
1874
3149
|
}
|
|
1875
3150
|
async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
1876
3151
|
const config = await readLinkedInDirectLookupConfig();
|
|
3152
|
+
const precomputedContextByKey = new Map((params.precomputedContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
1877
3153
|
const primaryContacts = new Map();
|
|
1878
3154
|
for (const contact of params.contacts) {
|
|
1879
3155
|
const existing = primaryContacts.get(contact.contact_id);
|
|
@@ -1897,11 +3173,23 @@ async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
|
1897
3173
|
companyName: contact.companyName,
|
|
1898
3174
|
companyNameOriginal: contact.companyNameOriginal
|
|
1899
3175
|
});
|
|
1900
|
-
|
|
1901
|
-
let
|
|
1902
|
-
let
|
|
1903
|
-
let
|
|
3176
|
+
const precomputedContext = precomputedContextByKey.get(buildDirectCompanyContextKey(contact));
|
|
3177
|
+
let matchedCompanyUrl = precomputedContext?.linkedinCompanyUrl ?? null;
|
|
3178
|
+
let matchedSalesNavCompanyUrl = precomputedContext?.salesNavCompanyUrl ?? null;
|
|
3179
|
+
let matchedCompanyName = precomputedContext?.matchedCompanyName ?? null;
|
|
3180
|
+
let matchedCompanyEmployeeCount = precomputedContext?.matchedCompanyEmployeeCount ?? null;
|
|
1904
3181
|
let lastError = null;
|
|
3182
|
+
if (matchedCompanyUrl || matchedSalesNavCompanyUrl || matchedCompanyName) {
|
|
3183
|
+
results.push({
|
|
3184
|
+
contact_id: contact.contact_id,
|
|
3185
|
+
linkedin_company_url: matchedCompanyUrl,
|
|
3186
|
+
sales_nav_company_url: matchedSalesNavCompanyUrl,
|
|
3187
|
+
matched_company_name: matchedCompanyName,
|
|
3188
|
+
matched_company_employee_count: matchedCompanyEmployeeCount,
|
|
3189
|
+
error: null
|
|
3190
|
+
});
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
1905
3193
|
for (const variant of variants) {
|
|
1906
3194
|
const controller = new AbortController();
|
|
1907
3195
|
const timeout = setTimeout(controller.abort.bind(controller), Math.min(params.timeoutMs, 20_000));
|
|
@@ -2044,9 +3332,34 @@ async function invokeLinkedInUrlEnrichmentWorkflow(params) {
|
|
|
2044
3332
|
}
|
|
2045
3333
|
}
|
|
2046
3334
|
function normalizeWorkflowLinkedInUrlResult(params) {
|
|
3335
|
+
const inputContactIds = new Set(params.contacts.map((contact) => contact.contact_id));
|
|
2047
3336
|
const contactIdsBySyntheticEmail = new Map(params.contacts
|
|
2048
3337
|
.filter((contact) => contact.email)
|
|
2049
3338
|
.map((contact) => [String(contact.email).toLowerCase(), contact.contact_id]));
|
|
3339
|
+
const contactIdsByNormalizedIdentity = new Map(params.contacts
|
|
3340
|
+
.filter((contact) => !contact.isVariation)
|
|
3341
|
+
.map((contact) => {
|
|
3342
|
+
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3343
|
+
const companyName = normalizeLooseMatchText(contact.companyNameOriginal ?? contact.companyName);
|
|
3344
|
+
return [`${fullName}|${companyName}`, contact.contact_id];
|
|
3345
|
+
})
|
|
3346
|
+
.filter(([key]) => key !== "|"));
|
|
3347
|
+
const normalizedNameCounts = new Map();
|
|
3348
|
+
for (const contact of params.contacts) {
|
|
3349
|
+
if (contact.isVariation)
|
|
3350
|
+
continue;
|
|
3351
|
+
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3352
|
+
if (!fullName)
|
|
3353
|
+
continue;
|
|
3354
|
+
normalizedNameCounts.set(fullName, (normalizedNameCounts.get(fullName) ?? 0) + 1);
|
|
3355
|
+
}
|
|
3356
|
+
const contactIdsByNormalizedName = new Map(params.contacts
|
|
3357
|
+
.filter((contact) => !contact.isVariation)
|
|
3358
|
+
.map((contact) => {
|
|
3359
|
+
const fullName = normalizeLooseMatchText(normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`));
|
|
3360
|
+
return [fullName, contact.contact_id];
|
|
3361
|
+
})
|
|
3362
|
+
.filter(([fullName]) => Boolean(fullName) && (normalizedNameCounts.get(fullName) ?? 0) === 1));
|
|
2050
3363
|
const rowsByContactId = new Map();
|
|
2051
3364
|
const body = params.parsedBody && typeof params.parsedBody === "object" && !Array.isArray(params.parsedBody)
|
|
2052
3365
|
? params.parsedBody
|
|
@@ -2056,13 +3369,34 @@ function normalizeWorkflowLinkedInUrlResult(params) {
|
|
|
2056
3369
|
...(Array.isArray(body?.profiles) ? body?.profiles : [])
|
|
2057
3370
|
];
|
|
2058
3371
|
for (const contact of workflowRows) {
|
|
3372
|
+
const fullNameCandidate = normalizeLookupWhitespace(typeof contact.full_name === "string"
|
|
3373
|
+
? contact.full_name
|
|
3374
|
+
: typeof contact.fullName === "string"
|
|
3375
|
+
? contact.fullName
|
|
3376
|
+
: typeof contact.name === "string"
|
|
3377
|
+
? contact.name
|
|
3378
|
+
: [contact.first_name, contact.last_name]
|
|
3379
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
3380
|
+
.join(" "));
|
|
3381
|
+
const companyNameCandidate = normalizeLookupWhitespace(typeof contact.company_name === "string"
|
|
3382
|
+
? contact.company_name
|
|
3383
|
+
: typeof contact.companyName === "string"
|
|
3384
|
+
? contact.companyName
|
|
3385
|
+
: typeof contact.current_company === "string"
|
|
3386
|
+
? contact.current_company
|
|
3387
|
+
: "");
|
|
3388
|
+
const normalizedIdentityKey = `${normalizeLooseMatchText(fullNameCandidate)}|${normalizeLooseMatchText(companyNameCandidate)}`;
|
|
2059
3389
|
const explicitContactId = typeof contact.contact_id === "string"
|
|
2060
3390
|
? contact.contact_id
|
|
2061
3391
|
: typeof contact.contact_id === "number"
|
|
2062
3392
|
? String(contact.contact_id)
|
|
2063
3393
|
: "";
|
|
2064
3394
|
const emailKey = typeof contact.email === "string" ? contact.email.toLowerCase() : "";
|
|
2065
|
-
const contactId =
|
|
3395
|
+
const contactId = (inputContactIds.has(explicitContactId) ? explicitContactId : "") ||
|
|
3396
|
+
contactIdsBySyntheticEmail.get(emailKey) ||
|
|
3397
|
+
contactIdsByNormalizedIdentity.get(normalizedIdentityKey) ||
|
|
3398
|
+
contactIdsByNormalizedName.get(normalizeLooseMatchText(fullNameCandidate)) ||
|
|
3399
|
+
"";
|
|
2066
3400
|
const linkedinUrl = normalizePublicLinkedInProfileUrl(typeof contact.linkedin_profile_url === "string"
|
|
2067
3401
|
? contact.linkedin_profile_url
|
|
2068
3402
|
: typeof contact.linkedinProfileUrl === "string"
|
|
@@ -2149,7 +3483,8 @@ async function fetchSalesNavLookupCandidates(params) {
|
|
|
2149
3483
|
}
|
|
2150
3484
|
async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
2151
3485
|
const results = [];
|
|
2152
|
-
for (const
|
|
3486
|
+
for (const row of params.rows) {
|
|
3487
|
+
const contactId = normalizeLinkedInLookupField(row.contactId) ?? `${results.length + 1}`;
|
|
2153
3488
|
const candidates = await fetchSalesNavLookupCandidates({
|
|
2154
3489
|
companyName: row.companyName,
|
|
2155
3490
|
orgId: params.orgId
|
|
@@ -2199,19 +3534,20 @@ async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
|
2199
3534
|
const salesNavCompanyUrl = typeof best?.companyUrl === "string" && /\/sales\/company\//i.test(best.companyUrl)
|
|
2200
3535
|
? best.companyUrl
|
|
2201
3536
|
: null;
|
|
3537
|
+
const existingLinkedInCompanyUrl = row.linkedinCompanyUrl?.trim() || null;
|
|
2202
3538
|
results.push({
|
|
2203
3539
|
clientId: row.clientId,
|
|
2204
3540
|
fullName: row.fullName,
|
|
2205
3541
|
companyName: row.companyName,
|
|
2206
3542
|
linkedinUrl,
|
|
2207
3543
|
salesNavProfileUrl,
|
|
2208
|
-
linkedinCompanyUrl,
|
|
3544
|
+
linkedinCompanyUrl: linkedinCompanyUrl ?? existingLinkedInCompanyUrl,
|
|
2209
3545
|
salesNavCompanyUrl,
|
|
2210
3546
|
found: Boolean(linkedinUrl),
|
|
2211
|
-
companyFound: Boolean(linkedinCompanyUrl),
|
|
2212
|
-
contactId
|
|
3547
|
+
companyFound: Boolean(linkedinCompanyUrl ?? existingLinkedInCompanyUrl),
|
|
3548
|
+
contactId,
|
|
2213
3549
|
source: linkedinUrl ? "salesnav-supabase" : null,
|
|
2214
|
-
companySource: linkedinCompanyUrl ? "salesnav-supabase" : null,
|
|
3550
|
+
companySource: linkedinCompanyUrl ? "salesnav-supabase" : existingLinkedInCompanyUrl ? "input" : null,
|
|
2215
3551
|
matchedFullName: best?.fullName ?? null,
|
|
2216
3552
|
matchedCompanyName: best?.companyName ?? null,
|
|
2217
3553
|
matchedTitle: best?.title ?? null,
|
|
@@ -2221,6 +3557,223 @@ async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
|
2221
3557
|
}
|
|
2222
3558
|
return results;
|
|
2223
3559
|
}
|
|
3560
|
+
function shouldUseSalesNavRowPrepass(params) {
|
|
3561
|
+
const env = params.env ?? process.env;
|
|
3562
|
+
const explicit = env.SALESPROMPTER_LINKEDIN_ROW_PREPASS?.trim().toLowerCase();
|
|
3563
|
+
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3564
|
+
return false;
|
|
3565
|
+
}
|
|
3566
|
+
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3567
|
+
return true;
|
|
3568
|
+
}
|
|
3569
|
+
const hasOrgId = Boolean(params.orgId?.trim());
|
|
3570
|
+
const hasSupabase = Boolean(env.NEXT_PUBLIC_SUPABASE_URL?.trim() && env.SUPABASE_SERVICE_ROLE_KEY?.trim());
|
|
3571
|
+
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_ROW_PREPASS_MAX_ROWS ?? 200);
|
|
3572
|
+
if (!hasOrgId || !hasSupabase) {
|
|
3573
|
+
return false;
|
|
3574
|
+
}
|
|
3575
|
+
return params.rows.length <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : 200);
|
|
3576
|
+
}
|
|
3577
|
+
function shouldUseDirectPeopleLookup(params) {
|
|
3578
|
+
const env = params.env ?? process.env;
|
|
3579
|
+
const explicit = env.SALESPROMPTER_LINKEDIN_DIRECT_PROFILE_LOOKUP?.trim().toLowerCase();
|
|
3580
|
+
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3581
|
+
return false;
|
|
3582
|
+
}
|
|
3583
|
+
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3584
|
+
return true;
|
|
3585
|
+
}
|
|
3586
|
+
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_DIRECT_PROFILE_MAX_ROWS ?? 50);
|
|
3587
|
+
return params.rowCount <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : 50);
|
|
3588
|
+
}
|
|
3589
|
+
function shouldUseWorkflowPeopleLookup(params) {
|
|
3590
|
+
const env = params.env ?? process.env;
|
|
3591
|
+
const explicit = env.SALESPROMPTER_LINKEDIN_WORKFLOW_PROFILE_LOOKUP?.trim().toLowerCase();
|
|
3592
|
+
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3593
|
+
return false;
|
|
3594
|
+
}
|
|
3595
|
+
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3596
|
+
return true;
|
|
3597
|
+
}
|
|
3598
|
+
const hasSerper = Boolean(getSerperApiKey(env));
|
|
3599
|
+
const maxRows = Number(env.SALESPROMPTER_LINKEDIN_WORKFLOW_PROFILE_MAX_ROWS ?? (hasSerper ? 75 : 250));
|
|
3600
|
+
return params.rowCount <= (Number.isFinite(maxRows) && maxRows > 0 ? maxRows : hasSerper ? 75 : 250);
|
|
3601
|
+
}
|
|
3602
|
+
function shouldUseBulkProfileResolutionStrategy(params) {
|
|
3603
|
+
const env = params.env ?? process.env;
|
|
3604
|
+
const explicit = env.SALESPROMPTER_LINKEDIN_BULK_MODE?.trim().toLowerCase();
|
|
3605
|
+
if (explicit === "0" || explicit === "false" || explicit === "off") {
|
|
3606
|
+
return false;
|
|
3607
|
+
}
|
|
3608
|
+
if (explicit === "1" || explicit === "true" || explicit === "on") {
|
|
3609
|
+
return true;
|
|
3610
|
+
}
|
|
3611
|
+
const minRows = Number(env.SALESPROMPTER_LINKEDIN_BULK_MODE_MIN_ROWS ?? 75);
|
|
3612
|
+
return params.rowCount >= (Number.isFinite(minRows) && minRows > 0 ? minRows : 75);
|
|
3613
|
+
}
|
|
3614
|
+
function resolveLinkedInBulkStrategyConfig(params) {
|
|
3615
|
+
const env = params.env ?? process.env;
|
|
3616
|
+
const bulkMode = shouldUseBulkProfileResolutionStrategy({
|
|
3617
|
+
rowCount: params.rowCount,
|
|
3618
|
+
env
|
|
3619
|
+
});
|
|
3620
|
+
const serperConcurrencyDefault = bulkMode ? 12 : 6;
|
|
3621
|
+
const serperConcurrency = Number(env.SALESPROMPTER_LINKEDIN_SERPER_CONCURRENCY ?? serperConcurrencyDefault);
|
|
3622
|
+
const serperMaxQueriesDefault = bulkMode ? 4 : 8;
|
|
3623
|
+
const serperMaxQueries = Number(env.SALESPROMPTER_LINKEDIN_SERPER_MAX_QUERIES ?? serperMaxQueriesDefault);
|
|
3624
|
+
const workflowStageBudgetDefault = bulkMode ? 8_000 : 15_000;
|
|
3625
|
+
const workflowStageBudgetMs = Number(env.SALESPROMPTER_LINKEDIN_WORKFLOW_STAGE_TIMEOUT_MS ?? workflowStageBudgetDefault);
|
|
3626
|
+
const serperStageBudgetDefault = bulkMode
|
|
3627
|
+
? Math.max(15_000, Math.min(params.timeoutMs * 2, 45_000))
|
|
3628
|
+
: Math.max(10_000, Math.min(params.timeoutMs, 20_000));
|
|
3629
|
+
const serperStageBudgetMs = Number(env.SALESPROMPTER_LINKEDIN_SERPER_STAGE_TIMEOUT_MS ?? serperStageBudgetDefault);
|
|
3630
|
+
const bulkDirectProfileMaxRowsDefault = 0;
|
|
3631
|
+
const bulkDirectProfileMaxRows = Number(env.SALESPROMPTER_LINKEDIN_BULK_DIRECT_PROFILE_MAX_ROWS ?? bulkDirectProfileMaxRowsDefault);
|
|
3632
|
+
const bulkDirectProfileTimeoutDefault = bulkMode ? Math.min(params.timeoutMs, 6_000) : 0;
|
|
3633
|
+
const bulkDirectProfileTimeoutMs = Number(env.SALESPROMPTER_LINKEDIN_BULK_DIRECT_PROFILE_TIMEOUT_MS ?? bulkDirectProfileTimeoutDefault);
|
|
3634
|
+
return {
|
|
3635
|
+
bulkMode,
|
|
3636
|
+
serperConcurrency: Number.isFinite(serperConcurrency) && serperConcurrency > 0
|
|
3637
|
+
? Math.trunc(serperConcurrency)
|
|
3638
|
+
: serperConcurrencyDefault,
|
|
3639
|
+
serperMaxQueries: Number.isFinite(serperMaxQueries) && serperMaxQueries > 0
|
|
3640
|
+
? Math.trunc(serperMaxQueries)
|
|
3641
|
+
: serperMaxQueriesDefault,
|
|
3642
|
+
workflowStageBudgetMs: Number.isFinite(workflowStageBudgetMs) && workflowStageBudgetMs > 0
|
|
3643
|
+
? Math.trunc(workflowStageBudgetMs)
|
|
3644
|
+
: workflowStageBudgetDefault,
|
|
3645
|
+
serperStageBudgetMs: Number.isFinite(serperStageBudgetMs) && serperStageBudgetMs > 0
|
|
3646
|
+
? Math.trunc(serperStageBudgetMs)
|
|
3647
|
+
: serperStageBudgetDefault,
|
|
3648
|
+
bulkDirectProfileMaxRows: Number.isFinite(bulkDirectProfileMaxRows) && bulkDirectProfileMaxRows > 0
|
|
3649
|
+
? Math.trunc(bulkDirectProfileMaxRows)
|
|
3650
|
+
: 0,
|
|
3651
|
+
bulkDirectProfileTimeoutMs: Number.isFinite(bulkDirectProfileTimeoutMs) && bulkDirectProfileTimeoutMs > 0
|
|
3652
|
+
? Math.trunc(bulkDirectProfileTimeoutMs)
|
|
3653
|
+
: 0
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
function shouldAttemptBulkDirectProfileLookup(params) {
|
|
3657
|
+
return (params.strategy.bulkMode &&
|
|
3658
|
+
params.strategy.bulkDirectProfileMaxRows > 0 &&
|
|
3659
|
+
params.strategy.bulkDirectProfileTimeoutMs > 0 &&
|
|
3660
|
+
params.unresolvedRowCount > 0);
|
|
3661
|
+
}
|
|
3662
|
+
function rankContactsForBulkDirectProfileLookup(params) {
|
|
3663
|
+
const scored = params.contacts
|
|
3664
|
+
.filter((contact) => !contact.isVariation)
|
|
3665
|
+
.map((contact) => {
|
|
3666
|
+
const row = params.rowsByContactId.get(contact.contact_id);
|
|
3667
|
+
const normalizedName = normalizeLookupWhitespace(`${contact.firstName} ${contact.lastName}`);
|
|
3668
|
+
const normalizedEmail = normalizeLookupWhitespace(contact.email);
|
|
3669
|
+
const titleKeywords = extractLookupTitleKeywords(contact.jobTitle);
|
|
3670
|
+
const roleKeywords = buildDeepDiveRoleSearchKeywords(contact.deepDiveRecommendedRole);
|
|
3671
|
+
let score = 0;
|
|
3672
|
+
if (row?.linkedinCompanyUrl || contact.linkedinCompanyUrl)
|
|
3673
|
+
score += 80;
|
|
3674
|
+
if (row?.salesNavCompanyUrl)
|
|
3675
|
+
score += 20;
|
|
3676
|
+
if (normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail))
|
|
3677
|
+
score += 40;
|
|
3678
|
+
if (contact.jobTitle?.trim())
|
|
3679
|
+
score += 25;
|
|
3680
|
+
if (contact.deepDiveRecommendedRole?.trim())
|
|
3681
|
+
score += 15;
|
|
3682
|
+
score += Math.min(20, titleKeywords.length * 5);
|
|
3683
|
+
score += Math.min(15, roleKeywords.length * 5);
|
|
3684
|
+
if (/^contact\s+\d+$/i.test(normalizedName))
|
|
3685
|
+
score -= 100;
|
|
3686
|
+
if (/^(hr|support|facility|buchhaltung|rechnungen)$/i.test(normalizedName))
|
|
3687
|
+
score -= 25;
|
|
3688
|
+
return { contact, score };
|
|
3689
|
+
})
|
|
3690
|
+
.filter((entry) => entry.score > 0)
|
|
3691
|
+
.sort((left, right) => right.score - left.score);
|
|
3692
|
+
return scored.slice(0, params.limit).map((entry) => entry.contact);
|
|
3693
|
+
}
|
|
3694
|
+
async function resolveSerperLinkedInProfilesInParallel(params) {
|
|
3695
|
+
const results = new Map();
|
|
3696
|
+
const contacts = params.contacts;
|
|
3697
|
+
const concurrency = Math.max(1, Math.min(params.concurrency ?? 3, contacts.length || 1));
|
|
3698
|
+
const deadline = params.overallBudgetMs && Number.isFinite(params.overallBudgetMs) && params.overallBudgetMs > 0
|
|
3699
|
+
? Date.now() + Math.trunc(params.overallBudgetMs)
|
|
3700
|
+
: Number.POSITIVE_INFINITY;
|
|
3701
|
+
let nextIndex = 0;
|
|
3702
|
+
const worker = async () => {
|
|
3703
|
+
while (true) {
|
|
3704
|
+
if (Date.now() >= deadline) {
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
const index = nextIndex++;
|
|
3708
|
+
if (index >= contacts.length) {
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
const contact = contacts[index];
|
|
3712
|
+
const remainingBudget = deadline - Date.now();
|
|
3713
|
+
if (remainingBudget <= 0) {
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
const linkedinUrl = await searchSerperLinkedInProfileUrl(contact, Math.min(params.timeoutMs, remainingBudget), {
|
|
3717
|
+
maxQueries: params.maxQueries
|
|
3718
|
+
});
|
|
3719
|
+
if (linkedinUrl) {
|
|
3720
|
+
results.set(contact.contact_id, linkedinUrl);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
};
|
|
3724
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
3725
|
+
return results;
|
|
3726
|
+
}
|
|
3727
|
+
async function resolveLinkedInCompanyUrlsForContacts(params) {
|
|
3728
|
+
const contacts = params.contacts.filter((contact) => !contact.isVariation && !contact.linkedinCompanyUrl);
|
|
3729
|
+
const uniqueCompanies = new Map();
|
|
3730
|
+
for (const contact of contacts) {
|
|
3731
|
+
const key = normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName);
|
|
3732
|
+
if (!key || uniqueCompanies.has(key)) {
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3735
|
+
uniqueCompanies.set(key, contact.companyNameOriginal ?? contact.companyName);
|
|
3736
|
+
}
|
|
3737
|
+
const resultsByCompany = new Map();
|
|
3738
|
+
const entries = Array.from(uniqueCompanies.entries());
|
|
3739
|
+
const concurrency = Math.max(1, Math.min(params.concurrency ?? 4, entries.length || 1));
|
|
3740
|
+
const deadline = params.overallBudgetMs && Number.isFinite(params.overallBudgetMs) && params.overallBudgetMs > 0
|
|
3741
|
+
? Date.now() + Math.trunc(params.overallBudgetMs)
|
|
3742
|
+
: Number.POSITIVE_INFINITY;
|
|
3743
|
+
let nextIndex = 0;
|
|
3744
|
+
const worker = async () => {
|
|
3745
|
+
while (true) {
|
|
3746
|
+
if (Date.now() >= deadline) {
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
const index = nextIndex++;
|
|
3750
|
+
if (index >= entries.length) {
|
|
3751
|
+
return;
|
|
3752
|
+
}
|
|
3753
|
+
const [key, companyName] = entries[index];
|
|
3754
|
+
const remainingBudget = deadline - Date.now();
|
|
3755
|
+
if (remainingBudget <= 0) {
|
|
3756
|
+
return;
|
|
3757
|
+
}
|
|
3758
|
+
const perCompanyTimeout = Math.min(params.timeoutMs, remainingBudget);
|
|
3759
|
+
const linkedinUrl = (await searchSerperLinkedInCompanyUrl(companyName, perCompanyTimeout)) ??
|
|
3760
|
+
(await searchPublicLinkedInCompanyUrl(companyName, perCompanyTimeout));
|
|
3761
|
+
if (linkedinUrl) {
|
|
3762
|
+
resultsByCompany.set(key, linkedinUrl);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3766
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
3767
|
+
const results = new Map();
|
|
3768
|
+
for (const contact of params.contacts) {
|
|
3769
|
+
const key = normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName);
|
|
3770
|
+
const linkedinUrl = resultsByCompany.get(key);
|
|
3771
|
+
if (linkedinUrl) {
|
|
3772
|
+
results.set(contact.contact_id, linkedinUrl);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
return results;
|
|
3776
|
+
}
|
|
2224
3777
|
function buildCommandLine(args) {
|
|
2225
3778
|
return args.map((arg) => shellQuote(arg)).join(" ");
|
|
2226
3779
|
}
|
|
@@ -2836,6 +4389,87 @@ async function promptText(rl, prompt, options = {}) {
|
|
|
2836
4389
|
writeWizardLine("This field is required.");
|
|
2837
4390
|
}
|
|
2838
4391
|
}
|
|
4392
|
+
async function promptLongPastedText(rl, prompt, options = {}) {
|
|
4393
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
4394
|
+
return await promptText(rl, prompt, options);
|
|
4395
|
+
}
|
|
4396
|
+
const suffix = options.defaultValue !== undefined ? ` [${options.defaultValue}]` : "";
|
|
4397
|
+
while (true) {
|
|
4398
|
+
const answer = await new Promise((resolve, reject) => {
|
|
4399
|
+
const stdin = process.stdin;
|
|
4400
|
+
let value = "";
|
|
4401
|
+
let finished = false;
|
|
4402
|
+
const cleanup = () => {
|
|
4403
|
+
if (finished) {
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
finished = true;
|
|
4407
|
+
stdin.off("data", onData);
|
|
4408
|
+
process.off("SIGINT", onSigint);
|
|
4409
|
+
if (stdin.isTTY) {
|
|
4410
|
+
stdin.setRawMode(false);
|
|
4411
|
+
}
|
|
4412
|
+
stdin.pause();
|
|
4413
|
+
rl.resume?.();
|
|
4414
|
+
};
|
|
4415
|
+
const finish = () => {
|
|
4416
|
+
cleanup();
|
|
4417
|
+
process.stdout.write("\n");
|
|
4418
|
+
resolve(value.trim());
|
|
4419
|
+
};
|
|
4420
|
+
const cancel = () => {
|
|
4421
|
+
cleanup();
|
|
4422
|
+
process.stdout.write("\n");
|
|
4423
|
+
reject(new Error("prompt cancelled"));
|
|
4424
|
+
};
|
|
4425
|
+
const onSigint = () => {
|
|
4426
|
+
cancel();
|
|
4427
|
+
};
|
|
4428
|
+
const onData = (chunk) => {
|
|
4429
|
+
for (const byte of chunk) {
|
|
4430
|
+
if (byte === 3) {
|
|
4431
|
+
cancel();
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
if (byte === 13 || byte === 10) {
|
|
4435
|
+
finish();
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
if (byte === 127 || byte === 8) {
|
|
4439
|
+
if (value.length > 0) {
|
|
4440
|
+
value = value.slice(0, -1);
|
|
4441
|
+
process.stdout.write("\b \b");
|
|
4442
|
+
}
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
// Ignore escape sequences for arrows and other terminal controls.
|
|
4446
|
+
if (byte === 27) {
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
const character = Buffer.from([byte]).toString("utf8");
|
|
4450
|
+
value += character;
|
|
4451
|
+
process.stdout.write(character);
|
|
4452
|
+
}
|
|
4453
|
+
};
|
|
4454
|
+
rl.pause?.();
|
|
4455
|
+
process.stdout.write(`${prompt}${suffix}: `);
|
|
4456
|
+
stdin.setRawMode(true);
|
|
4457
|
+
stdin.resume();
|
|
4458
|
+
stdin.on("data", onData);
|
|
4459
|
+
process.on("SIGINT", onSigint);
|
|
4460
|
+
});
|
|
4461
|
+
if (answer.length > 0) {
|
|
4462
|
+
return answer;
|
|
4463
|
+
}
|
|
4464
|
+
if (options.defaultValue !== undefined) {
|
|
4465
|
+
return options.defaultValue;
|
|
4466
|
+
}
|
|
4467
|
+
if (!options.required) {
|
|
4468
|
+
return "";
|
|
4469
|
+
}
|
|
4470
|
+
writeWizardLine("This field is required.");
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
2839
4473
|
async function promptYesNo(rl, prompt, defaultValue) {
|
|
2840
4474
|
while (true) {
|
|
2841
4475
|
const answer = (await rl.question(`${prompt} [${defaultValue ? "Y/n" : "y/N"}]: `)).trim().toLowerCase();
|
|
@@ -3127,6 +4761,72 @@ async function fetchWorkspaceLeadSearch(session, requestBody) {
|
|
|
3127
4761
|
}
|
|
3128
4762
|
return WorkspaceLeadSearchResponseSchema.parse(payload).leads;
|
|
3129
4763
|
}
|
|
4764
|
+
async function buildWorkspaceLeadAccount(icp, target, leads) {
|
|
4765
|
+
const firstLead = leads[0];
|
|
4766
|
+
if (firstLead) {
|
|
4767
|
+
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)));
|
|
4768
|
+
return AccountProfileSchema.parse({
|
|
4769
|
+
companyName: target.companyName?.trim() || firstLead.companyName,
|
|
4770
|
+
domain: target.companyDomain?.trim().toLowerCase() || firstLead.domain,
|
|
4771
|
+
industry: firstLead.industry,
|
|
4772
|
+
region: firstLead.region,
|
|
4773
|
+
employeeCount: firstLead.employeeCount,
|
|
4774
|
+
keywords,
|
|
4775
|
+
sources: ["workspace-qualified-leads"]
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
return await companyProvider.resolveCompany({
|
|
4779
|
+
companyDomain: target.companyDomain,
|
|
4780
|
+
companyName: target.companyName
|
|
4781
|
+
}, icp);
|
|
4782
|
+
}
|
|
4783
|
+
async function generateLeadsForCommand(options) {
|
|
4784
|
+
const source = z.enum(["auto", "workspace", "fallback"]).parse(options.source ?? "auto");
|
|
4785
|
+
if (source === "fallback") {
|
|
4786
|
+
return await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4787
|
+
}
|
|
4788
|
+
if (shouldBypassAuth()) {
|
|
4789
|
+
if (source === "workspace") {
|
|
4790
|
+
throw new Error("workspace lead generation requires authentication. Disable SALESPROMPTER_SKIP_AUTH and log in first.");
|
|
4791
|
+
}
|
|
4792
|
+
return await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4793
|
+
}
|
|
4794
|
+
try {
|
|
4795
|
+
const session = await requireAuthSession();
|
|
4796
|
+
const requestBody = options.target.companyDomain || options.target.linkedinCompanyPage
|
|
4797
|
+
? {
|
|
4798
|
+
mode: "target-company",
|
|
4799
|
+
domain: options.target.companyDomain,
|
|
4800
|
+
linkedinCompanyPage: options.target.linkedinCompanyPage,
|
|
4801
|
+
limit: options.count
|
|
4802
|
+
}
|
|
4803
|
+
: {
|
|
4804
|
+
mode: "reference-company",
|
|
4805
|
+
icp: options.icp,
|
|
4806
|
+
limit: options.count
|
|
4807
|
+
};
|
|
4808
|
+
const leads = await fetchWorkspaceLeadSearch(session, requestBody);
|
|
4809
|
+
const account = await buildWorkspaceLeadAccount(options.icp, options.target, leads);
|
|
4810
|
+
return {
|
|
4811
|
+
provider: "salesprompter-app-workspace-search",
|
|
4812
|
+
mode: "real",
|
|
4813
|
+
account,
|
|
4814
|
+
leads,
|
|
4815
|
+
warnings: []
|
|
4816
|
+
};
|
|
4817
|
+
}
|
|
4818
|
+
catch (error) {
|
|
4819
|
+
if (source === "workspace") {
|
|
4820
|
+
throw error;
|
|
4821
|
+
}
|
|
4822
|
+
const fallback = await leadProvider.generateLeads(options.icp, options.count, options.target);
|
|
4823
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4824
|
+
return {
|
|
4825
|
+
...fallback,
|
|
4826
|
+
warnings: [`Workspace lead search unavailable: ${message}`, ...fallback.warnings]
|
|
4827
|
+
};
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
3130
4830
|
function buildLinkedInProductsOutputPath(categorySlug) {
|
|
3131
4831
|
return `./data/linkedin-products-${categorySlug}.json`;
|
|
3132
4832
|
}
|
|
@@ -3913,6 +5613,17 @@ async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
|
3913
5613
|
}), LinkedInCompanyBackfillStatusResponseSchema);
|
|
3914
5614
|
return value;
|
|
3915
5615
|
}
|
|
5616
|
+
async function syncPhantombusterContainersViaApp(session, payload) {
|
|
5617
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/phantombuster/containers/sync`, {
|
|
5618
|
+
method: "POST",
|
|
5619
|
+
headers: {
|
|
5620
|
+
"Content-Type": "application/json",
|
|
5621
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
5622
|
+
},
|
|
5623
|
+
body: JSON.stringify(payload)
|
|
5624
|
+
}), PhantombusterContainersSyncResponseSchema);
|
|
5625
|
+
return value;
|
|
5626
|
+
}
|
|
3916
5627
|
function serializeSalesNavigatorFiltersForApi(filters) {
|
|
3917
5628
|
return filters.map((filter) => ({
|
|
3918
5629
|
type: filter.type,
|
|
@@ -3939,6 +5650,12 @@ function buildSalesNavigatorSliceRawPayload(slice, extra = {}) {
|
|
|
3939
5650
|
resultRetryCount: slice.resultRetryCount ?? null
|
|
3940
5651
|
};
|
|
3941
5652
|
}
|
|
5653
|
+
function parseOptionalSalesNavigatorClientId(value) {
|
|
5654
|
+
if (value == null || String(value).trim().length === 0) {
|
|
5655
|
+
return null;
|
|
5656
|
+
}
|
|
5657
|
+
return z.coerce.number().int().positive().parse(value);
|
|
5658
|
+
}
|
|
3942
5659
|
function buildSalesNavigatorCrawlReportRawPayload(slice, traceId, extra = {}) {
|
|
3943
5660
|
return buildSalesNavigatorSliceRawPayload({
|
|
3944
5661
|
sourceQueryUrl: slice.sourceQueryUrl,
|
|
@@ -4399,11 +6116,12 @@ function isSalesNavigatorSessionError(error) {
|
|
|
4399
6116
|
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);
|
|
4400
6117
|
}
|
|
4401
6118
|
function isSalesNavigatorResultArtifactError(error) {
|
|
4402
|
-
if (error instanceof SalesNavigatorExportRequestError &&
|
|
6119
|
+
if (error instanceof SalesNavigatorExportRequestError &&
|
|
6120
|
+
["phantombuster_result_invalid", "partial_result_artifact"].includes(error.errorCode ?? "")) {
|
|
4403
6121
|
return true;
|
|
4404
6122
|
}
|
|
4405
6123
|
const message = error instanceof Error ? error.message : String(error);
|
|
4406
|
-
return /page has crashed|no valid sales navigator people rows/i.test(message);
|
|
6124
|
+
return /page has crashed|no valid sales navigator people rows|partial result artifact|returned \d+ valid sales navigator people rows, but \d+ were expected/i.test(message);
|
|
4407
6125
|
}
|
|
4408
6126
|
function isSalesNavigatorTransientExportError(error) {
|
|
4409
6127
|
if (isSalesNavigatorSessionError(error) || isSalesNavigatorResultArtifactError(error)) {
|
|
@@ -4494,6 +6212,7 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
4494
6212
|
crawlSliceId: context?.crawlSliceId,
|
|
4495
6213
|
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
4496
6214
|
traceId: context?.traceId ?? null,
|
|
6215
|
+
clientId: context?.clientId ?? null,
|
|
4497
6216
|
phase: shouldProbe ? "probe" : "full_export",
|
|
4498
6217
|
requestedProfiles: probeProfiles,
|
|
4499
6218
|
crawlJobId: context?.crawlJobId ?? null,
|
|
@@ -4530,6 +6249,7 @@ async function runSalesNavigatorCrawlAttempt(session, attempt, options, context)
|
|
|
4530
6249
|
crawlSliceId: context?.crawlSliceId,
|
|
4531
6250
|
rawPayload: buildSalesNavigatorSliceRawPayload(attempt, {
|
|
4532
6251
|
traceId: context?.traceId ?? null,
|
|
6252
|
+
clientId: context?.clientId ?? null,
|
|
4533
6253
|
phase: "full_export_after_probe",
|
|
4534
6254
|
requestedProfiles: attempt.numberOfProfiles,
|
|
4535
6255
|
crawlJobId: context?.crawlJobId ?? null,
|
|
@@ -4628,6 +6348,8 @@ const SALES_NAVIGATOR_SPLIT_TRIGGER_RESULTS = 1500;
|
|
|
4628
6348
|
const SALES_NAVIGATOR_FILTER_IMPACT_MIN_OBSERVATIONS = 3;
|
|
4629
6349
|
let salesNavigatorFilterImpactModel = null;
|
|
4630
6350
|
let salesNavigatorFilterImpactLoaded = false;
|
|
6351
|
+
let linkedInProfileHitCache = null;
|
|
6352
|
+
let linkedInProfileHitCacheLoaded = false;
|
|
4631
6353
|
function getSalesprompterConfigDir() {
|
|
4632
6354
|
const override = process.env.SALESPROMPTER_CONFIG_DIR?.trim();
|
|
4633
6355
|
if (override !== undefined && override.length > 0) {
|
|
@@ -4638,6 +6360,76 @@ function getSalesprompterConfigDir() {
|
|
|
4638
6360
|
function getSalesNavigatorFilterImpactPath() {
|
|
4639
6361
|
return path.join(getSalesprompterConfigDir(), "salesnav-filter-impact.json");
|
|
4640
6362
|
}
|
|
6363
|
+
function getLinkedInProfileHitCachePath() {
|
|
6364
|
+
return path.join(getSalesprompterConfigDir(), "linkedin-profile-hits.json");
|
|
6365
|
+
}
|
|
6366
|
+
function buildLinkedInProfileHitCacheKeys(params) {
|
|
6367
|
+
const keys = new Set();
|
|
6368
|
+
const normalizedName = normalizeLooseMatchText(params.fullName);
|
|
6369
|
+
const normalizedCompany = normalizeLooseMatchText(params.companyName);
|
|
6370
|
+
const normalizedEmail = normalizeLookupWhitespace(params.email);
|
|
6371
|
+
const trustedEmail = normalizedEmail && !isSyntheticLinkedInLookupEmail(normalizedEmail) ? normalizedEmail.toLowerCase() : "";
|
|
6372
|
+
const contactId = normalizeLinkedInLookupField(params.contactId);
|
|
6373
|
+
if (contactId && !/^[1-9]\d?$/.test(contactId)) {
|
|
6374
|
+
keys.add(`contact:${contactId}`);
|
|
6375
|
+
}
|
|
6376
|
+
if (normalizedName && normalizedCompany && trustedEmail) {
|
|
6377
|
+
keys.add(`identity:${normalizedName}|${normalizedCompany}|${trustedEmail}`);
|
|
6378
|
+
}
|
|
6379
|
+
if (normalizedName && normalizedCompany) {
|
|
6380
|
+
keys.add(`identity:${normalizedName}|${normalizedCompany}`);
|
|
6381
|
+
}
|
|
6382
|
+
return Array.from(keys);
|
|
6383
|
+
}
|
|
6384
|
+
async function loadLinkedInProfileHitCache() {
|
|
6385
|
+
if (linkedInProfileHitCacheLoaded) {
|
|
6386
|
+
return linkedInProfileHitCache;
|
|
6387
|
+
}
|
|
6388
|
+
linkedInProfileHitCacheLoaded = true;
|
|
6389
|
+
try {
|
|
6390
|
+
const content = await readFile(getLinkedInProfileHitCachePath(), "utf8");
|
|
6391
|
+
const parsed = JSON.parse(content);
|
|
6392
|
+
if (parsed?.version === 1 && parsed.entries && typeof parsed.entries === "object") {
|
|
6393
|
+
linkedInProfileHitCache = parsed;
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
catch {
|
|
6397
|
+
linkedInProfileHitCache = null;
|
|
6398
|
+
}
|
|
6399
|
+
return linkedInProfileHitCache;
|
|
6400
|
+
}
|
|
6401
|
+
async function persistLinkedInProfileHitCache() {
|
|
6402
|
+
if (!linkedInProfileHitCache) {
|
|
6403
|
+
return;
|
|
6404
|
+
}
|
|
6405
|
+
const filePath = getLinkedInProfileHitCachePath();
|
|
6406
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
6407
|
+
await writeFile(filePath, `${JSON.stringify(linkedInProfileHitCache, null, 2)}\n`, "utf8");
|
|
6408
|
+
}
|
|
6409
|
+
function upsertLinkedInProfileHitCacheEntry(params) {
|
|
6410
|
+
if (!params.linkedinUrl && !params.salesNavProfileUrl && !params.linkedinCompanyUrl && !params.salesNavCompanyUrl) {
|
|
6411
|
+
return;
|
|
6412
|
+
}
|
|
6413
|
+
if (!linkedInProfileHitCache) {
|
|
6414
|
+
linkedInProfileHitCache = {
|
|
6415
|
+
version: 1,
|
|
6416
|
+
updatedAt: new Date().toISOString(),
|
|
6417
|
+
entries: {}
|
|
6418
|
+
};
|
|
6419
|
+
}
|
|
6420
|
+
const updatedAt = new Date().toISOString();
|
|
6421
|
+
linkedInProfileHitCache.updatedAt = updatedAt;
|
|
6422
|
+
const entry = {
|
|
6423
|
+
linkedinUrl: params.linkedinUrl,
|
|
6424
|
+
salesNavProfileUrl: params.salesNavProfileUrl,
|
|
6425
|
+
linkedinCompanyUrl: params.linkedinCompanyUrl,
|
|
6426
|
+
salesNavCompanyUrl: params.salesNavCompanyUrl,
|
|
6427
|
+
updatedAt
|
|
6428
|
+
};
|
|
6429
|
+
for (const key of buildLinkedInProfileHitCacheKeys(params)) {
|
|
6430
|
+
linkedInProfileHitCache.entries[key] = entry;
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
4641
6433
|
async function loadSalesNavigatorFilterImpactModel() {
|
|
4642
6434
|
if (salesNavigatorFilterImpactLoaded) {
|
|
4643
6435
|
return salesNavigatorFilterImpactModel;
|
|
@@ -4910,6 +6702,7 @@ async function processSalesNavigatorClaimedCrawlSlice(session, jobId, slice, opt
|
|
|
4910
6702
|
}, {
|
|
4911
6703
|
crawlJobId: jobId,
|
|
4912
6704
|
crawlSliceId: slice.id,
|
|
6705
|
+
clientId: options.clientId ?? null,
|
|
4913
6706
|
traceId: options.traceId
|
|
4914
6707
|
});
|
|
4915
6708
|
const reported = await reportSalesNavigatorCrawlSlice(currentSession, jobId, {
|
|
@@ -5186,6 +6979,7 @@ async function executeSalesNavigatorCrawlJob(session, jobId, options) {
|
|
|
5186
6979
|
agentBusyWaitSeconds: options.agentBusyWaitSeconds,
|
|
5187
6980
|
agentBusyMaxWaits: options.agentBusyMaxWaits,
|
|
5188
6981
|
claimedSlices: claimedSliceNumber,
|
|
6982
|
+
clientId: options.clientId ?? null,
|
|
5189
6983
|
traceId: options.traceId,
|
|
5190
6984
|
logger: options.logger
|
|
5191
6985
|
}).then((value) => ({ slot, value })));
|
|
@@ -5432,7 +7226,7 @@ async function runDirectSalesNavigatorSearchWizard(input) {
|
|
|
5432
7226
|
}
|
|
5433
7227
|
async function runProductMarketWizard(rl) {
|
|
5434
7228
|
writeWizardSection("Find leads from a product market", "Start from a company website, LinkedIn company page, product page, or category page. I will turn that into intended job titles and durable Sales Navigator crawls.");
|
|
5435
|
-
const input = await
|
|
7229
|
+
const input = await promptLongPastedText(rl, "What company website or LinkedIn page should I start from?", {
|
|
5436
7230
|
required: true
|
|
5437
7231
|
});
|
|
5438
7232
|
if (isSalesNavigatorPeopleSearchUrl(input)) {
|
|
@@ -6149,6 +7943,7 @@ program
|
|
|
6149
7943
|
const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
|
|
6150
7944
|
const cleanedCompanyMap = await buildCompanyNameCleaningMap(rows, companyCleaningMode);
|
|
6151
7945
|
const contacts = toLinkedInUrlLookupContacts(rows, cleanedCompanyMap);
|
|
7946
|
+
await loadLinkedInProfileHitCache();
|
|
6152
7947
|
if (options.dryRun) {
|
|
6153
7948
|
const payload = {
|
|
6154
7949
|
status: "ok",
|
|
@@ -6164,79 +7959,70 @@ program
|
|
|
6164
7959
|
printOutput(payload);
|
|
6165
7960
|
return;
|
|
6166
7961
|
}
|
|
6167
|
-
const
|
|
6168
|
-
|
|
6169
|
-
|
|
7962
|
+
const orgId = String(options.orgId ?? "").trim() || undefined;
|
|
7963
|
+
const strategy = resolveLinkedInBulkStrategyConfig({
|
|
7964
|
+
rowCount: rows.length,
|
|
7965
|
+
timeoutMs
|
|
6170
7966
|
});
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
7967
|
+
const useSalesNavRowPrepass = !strategy.bulkMode &&
|
|
7968
|
+
shouldUseSalesNavRowPrepass({
|
|
7969
|
+
rows,
|
|
7970
|
+
orgId
|
|
7971
|
+
});
|
|
7972
|
+
const enrichedRows = useSalesNavRowPrepass
|
|
7973
|
+
? await resolveLinkedInUrlsFromSalesNavRows({
|
|
7974
|
+
rows,
|
|
7975
|
+
orgId
|
|
7976
|
+
})
|
|
7977
|
+
: rows.map((row, index) => ({
|
|
7978
|
+
clientId: row.clientId,
|
|
7979
|
+
fullName: row.fullName,
|
|
7980
|
+
companyName: row.companyName,
|
|
7981
|
+
linkedinUrl: null,
|
|
7982
|
+
salesNavProfileUrl: null,
|
|
7983
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl?.trim() || null,
|
|
7984
|
+
salesNavCompanyUrl: null,
|
|
7985
|
+
found: false,
|
|
7986
|
+
companyFound: Boolean(row.linkedinCompanyUrl?.trim()),
|
|
7987
|
+
contactId: normalizeLinkedInLookupField(row.contactId) ?? `${index + 1}`,
|
|
7988
|
+
source: null,
|
|
7989
|
+
companySource: row.linkedinCompanyUrl?.trim() ? "input" : null,
|
|
7990
|
+
matchedFullName: null,
|
|
7991
|
+
matchedCompanyName: null,
|
|
7992
|
+
matchedTitle: null,
|
|
7993
|
+
matchedOrgId: null,
|
|
7994
|
+
matchedCompanyEmployeeCount: null
|
|
7995
|
+
}));
|
|
7996
|
+
const contactById = new Map(contacts.filter((contact) => !contact.isVariation).map((contact) => [contact.contact_id, contact]));
|
|
7997
|
+
for (const row of enrichedRows) {
|
|
7998
|
+
if (row.found) {
|
|
7999
|
+
continue;
|
|
6203
8000
|
}
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
throw new Error(`LinkedIn enrichment workflow returned ${workflow.response.status}: ${workflow.bodyText.slice(0, 300)}`);
|
|
6217
|
-
}
|
|
6218
|
-
linkedInUrlByContactId = normalizeWorkflowLinkedInUrlResult({
|
|
6219
|
-
parsedBody: workflow.parsedBody,
|
|
6220
|
-
contacts: directContacts
|
|
6221
|
-
});
|
|
6222
|
-
for (const row of enrichedRows) {
|
|
6223
|
-
if (row.found)
|
|
6224
|
-
continue;
|
|
6225
|
-
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
6226
|
-
if (profile?.linkedinUrl) {
|
|
6227
|
-
row.linkedinUrl = profile.linkedinUrl;
|
|
6228
|
-
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
6229
|
-
row.linkedinCompanyUrl = profile.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
6230
|
-
row.salesNavCompanyUrl = profile.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
6231
|
-
row.found = true;
|
|
6232
|
-
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
6233
|
-
row.source = "workflow";
|
|
6234
|
-
row.companySource =
|
|
6235
|
-
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "workflow" : row.companySource ?? null;
|
|
6236
|
-
}
|
|
6237
|
-
}
|
|
8001
|
+
const contact = contactById.get(row.contactId);
|
|
8002
|
+
const cacheKeys = buildLinkedInProfileHitCacheKeys({
|
|
8003
|
+
fullName: row.fullName,
|
|
8004
|
+
companyName: row.companyName,
|
|
8005
|
+
email: contact?.email,
|
|
8006
|
+
contactId: row.contactId
|
|
8007
|
+
});
|
|
8008
|
+
const cachedEntry = cacheKeys
|
|
8009
|
+
.map((key) => linkedInProfileHitCache?.entries[key] ?? null)
|
|
8010
|
+
.find(Boolean);
|
|
8011
|
+
if (!cachedEntry) {
|
|
8012
|
+
continue;
|
|
6238
8013
|
}
|
|
8014
|
+
row.linkedinUrl = cachedEntry.linkedinUrl ?? row.linkedinUrl ?? null;
|
|
8015
|
+
row.salesNavProfileUrl = cachedEntry.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
8016
|
+
row.linkedinCompanyUrl = cachedEntry.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
8017
|
+
row.salesNavCompanyUrl = cachedEntry.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
8018
|
+
row.found = Boolean(row.linkedinUrl || row.salesNavProfileUrl);
|
|
8019
|
+
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
8020
|
+
row.source = row.found ? "cache" : row.source;
|
|
8021
|
+
row.companySource =
|
|
8022
|
+
row.companyFound && !row.companySource ? "cache" : row.companySource;
|
|
6239
8023
|
}
|
|
8024
|
+
let directAttempted = false;
|
|
8025
|
+
let workflowAttempted = false;
|
|
6240
8026
|
const parsedClientIds = Array.from(new Set(rows
|
|
6241
8027
|
.map((row) => Number(row.clientId))
|
|
6242
8028
|
.filter((value) => Number.isFinite(value) && value > 0)));
|
|
@@ -6283,37 +8069,265 @@ program
|
|
|
6283
8069
|
writeProgress(`Skipping app-backed company enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
6284
8070
|
}
|
|
6285
8071
|
}
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
8072
|
+
const contactsMissingCompanyUrl = contacts.filter((contact) => !contact.isVariation &&
|
|
8073
|
+
enrichedRows.some((row) => row.contactId === contact.contact_id && !row.linkedinCompanyUrl));
|
|
8074
|
+
if (contactsMissingCompanyUrl.length > 0) {
|
|
8075
|
+
const companyUrlByContactId = await resolveLinkedInCompanyUrlsForContacts({
|
|
8076
|
+
contacts: contactsMissingCompanyUrl,
|
|
8077
|
+
timeoutMs: Math.min(timeoutMs, 15_000),
|
|
8078
|
+
concurrency: strategy.bulkMode ? 6 : 3,
|
|
8079
|
+
overallBudgetMs: strategy.bulkMode ? 20_000 : 10_000
|
|
6290
8080
|
});
|
|
6291
|
-
const companyByContactId = new Map(companyResult.contacts.map((contact) => [
|
|
6292
|
-
contact.contact_id,
|
|
6293
|
-
{
|
|
6294
|
-
linkedinCompanyUrl: contact.linkedin_company_url ?? null,
|
|
6295
|
-
salesNavCompanyUrl: contact.sales_nav_company_url ?? null,
|
|
6296
|
-
matchedCompanyName: contact.matched_company_name ?? null,
|
|
6297
|
-
matchedCompanyEmployeeCount: contact.matched_company_employee_count ?? null
|
|
6298
|
-
}
|
|
6299
|
-
]));
|
|
6300
8081
|
for (const row of enrichedRows) {
|
|
6301
|
-
|
|
6302
|
-
if (!company || row.linkedinCompanyUrl) {
|
|
8082
|
+
if (row.linkedinCompanyUrl) {
|
|
6303
8083
|
continue;
|
|
6304
8084
|
}
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
row.
|
|
6311
|
-
row.
|
|
6312
|
-
company.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
8085
|
+
const linkedinCompanyUrl = companyUrlByContactId.get(row.contactId);
|
|
8086
|
+
if (!linkedinCompanyUrl) {
|
|
8087
|
+
continue;
|
|
8088
|
+
}
|
|
8089
|
+
row.linkedinCompanyUrl = linkedinCompanyUrl;
|
|
8090
|
+
row.companyFound = true;
|
|
8091
|
+
row.companySource = "web-search";
|
|
6313
8092
|
}
|
|
6314
8093
|
}
|
|
6315
|
-
|
|
6316
|
-
|
|
8094
|
+
const missingRows = enrichedRows.filter((row) => !row.found);
|
|
8095
|
+
const useDirectPeopleLookup = !strategy.bulkMode &&
|
|
8096
|
+
shouldUseDirectPeopleLookup({
|
|
8097
|
+
rowCount: missingRows.length
|
|
8098
|
+
});
|
|
8099
|
+
const useWorkflowPeopleLookup = !strategy.bulkMode &&
|
|
8100
|
+
shouldUseWorkflowPeopleLookup({
|
|
8101
|
+
rowCount: missingRows.length
|
|
8102
|
+
});
|
|
8103
|
+
if (missingRows.length > 0) {
|
|
8104
|
+
const rowByContactId = new Map(enrichedRows.map((row) => [row.contactId, row]));
|
|
8105
|
+
const directContacts = contacts
|
|
8106
|
+
.filter((contact) => missingRows.some((row) => row.contactId === contact.contact_id))
|
|
8107
|
+
.map((contact) => {
|
|
8108
|
+
const row = rowByContactId.get(contact.contact_id);
|
|
8109
|
+
if (!row) {
|
|
8110
|
+
return contact;
|
|
8111
|
+
}
|
|
8112
|
+
return {
|
|
8113
|
+
...contact,
|
|
8114
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl ?? contact.linkedinCompanyUrl,
|
|
8115
|
+
companyNameOriginal: row.matchedCompanyName ?? contact.companyNameOriginal,
|
|
8116
|
+
companyName: row.matchedCompanyName && normalizeLookupCompanyForSearch(row.matchedCompanyName)
|
|
8117
|
+
? normalizeLookupCompanyForSearch(row.matchedCompanyName)
|
|
8118
|
+
: contact.companyName
|
|
8119
|
+
};
|
|
8120
|
+
});
|
|
8121
|
+
let linkedInUrlByContactId = new Map();
|
|
8122
|
+
if (useDirectPeopleLookup) {
|
|
8123
|
+
try {
|
|
8124
|
+
directAttempted = true;
|
|
8125
|
+
const result = await invokeLinkedInUrlEnrichmentDirect({
|
|
8126
|
+
contacts: directContacts,
|
|
8127
|
+
timeoutMs
|
|
8128
|
+
});
|
|
8129
|
+
const directCompanyContextByKey = new Map((result.companyContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
8130
|
+
linkedInUrlByContactId = new Map(result.contacts.map((contact) => [
|
|
8131
|
+
contact.contact_id,
|
|
8132
|
+
{
|
|
8133
|
+
linkedinUrl: contact.linkedin_url ?? null,
|
|
8134
|
+
salesNavProfileUrl: contact.sales_nav_profile_url ?? null,
|
|
8135
|
+
linkedinCompanyUrl: null,
|
|
8136
|
+
salesNavCompanyUrl: null,
|
|
8137
|
+
matchedFullName: contact.matched_full_name ?? null,
|
|
8138
|
+
matchedCompanyName: contact.matched_company_name ?? null,
|
|
8139
|
+
matchedTitle: contact.matched_title ?? null
|
|
8140
|
+
}
|
|
8141
|
+
]));
|
|
8142
|
+
for (const row of enrichedRows) {
|
|
8143
|
+
if (row.found)
|
|
8144
|
+
continue;
|
|
8145
|
+
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
8146
|
+
if (profile?.linkedinUrl) {
|
|
8147
|
+
row.linkedinUrl = profile.linkedinUrl;
|
|
8148
|
+
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
8149
|
+
row.found = true;
|
|
8150
|
+
row.source = "linkedin-direct";
|
|
8151
|
+
row.matchedFullName = profile.matchedFullName ?? row.matchedFullName ?? null;
|
|
8152
|
+
row.matchedCompanyName = profile.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
8153
|
+
row.matchedTitle = profile.matchedTitle ?? row.matchedTitle ?? null;
|
|
8154
|
+
}
|
|
8155
|
+
const directContact = directContacts.find((candidate) => candidate.contact_id === row.contactId && !candidate.isVariation);
|
|
8156
|
+
const companyContext = directContact
|
|
8157
|
+
? directCompanyContextByKey.get(buildDirectCompanyContextKey(directContact))
|
|
8158
|
+
: null;
|
|
8159
|
+
if (companyContext && !row.linkedinCompanyUrl) {
|
|
8160
|
+
row.linkedinCompanyUrl = companyContext.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
8161
|
+
row.salesNavCompanyUrl = companyContext.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
8162
|
+
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
8163
|
+
row.companySource =
|
|
8164
|
+
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
8165
|
+
row.matchedCompanyName = companyContext.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
8166
|
+
row.matchedCompanyEmployeeCount =
|
|
8167
|
+
companyContext.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
const contactsStillMissingCompany = contacts.filter((contact) => !contact.isVariation &&
|
|
8171
|
+
enrichedRows.some((row) => row.contactId === contact.contact_id && !row.linkedinCompanyUrl && !row.salesNavCompanyUrl));
|
|
8172
|
+
if (contactsStillMissingCompany.length > 0) {
|
|
8173
|
+
const companyResult = await invokeLinkedInCompanyEnrichmentDirect({
|
|
8174
|
+
contacts: contactsStillMissingCompany,
|
|
8175
|
+
timeoutMs,
|
|
8176
|
+
precomputedContexts: result.companyContexts
|
|
8177
|
+
});
|
|
8178
|
+
const companyByContactId = new Map(companyResult.contacts.map((contact) => [
|
|
8179
|
+
contact.contact_id,
|
|
8180
|
+
{
|
|
8181
|
+
linkedinCompanyUrl: contact.linkedin_company_url ?? null,
|
|
8182
|
+
salesNavCompanyUrl: contact.sales_nav_company_url ?? null,
|
|
8183
|
+
matchedCompanyName: contact.matched_company_name ?? null,
|
|
8184
|
+
matchedCompanyEmployeeCount: contact.matched_company_employee_count ?? null
|
|
8185
|
+
}
|
|
8186
|
+
]));
|
|
8187
|
+
for (const row of enrichedRows) {
|
|
8188
|
+
const company = companyByContactId.get(row.contactId);
|
|
8189
|
+
if (!company || row.linkedinCompanyUrl) {
|
|
8190
|
+
continue;
|
|
8191
|
+
}
|
|
8192
|
+
row.linkedinCompanyUrl = company.linkedinCompanyUrl;
|
|
8193
|
+
row.salesNavCompanyUrl = company.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
8194
|
+
row.companyFound = Boolean(company.linkedinCompanyUrl || company.salesNavCompanyUrl);
|
|
8195
|
+
row.companySource =
|
|
8196
|
+
company.linkedinCompanyUrl || company.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
8197
|
+
row.matchedCompanyName = company.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
8198
|
+
row.matchedCompanyEmployeeCount =
|
|
8199
|
+
company.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
}
|
|
8203
|
+
catch (error) {
|
|
8204
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8205
|
+
if (!/Missing LinkedIn direct lookup session/i.test(message)) {
|
|
8206
|
+
throw error;
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
}
|
|
8210
|
+
const stillMissingAfterDirect = enrichedRows.filter((row) => !row.found);
|
|
8211
|
+
const contactsStillMissing = directContacts.filter((contact) => stillMissingAfterDirect.some((row) => row.contactId === contact.contact_id));
|
|
8212
|
+
if (contactsStillMissing.length > 0 && useWorkflowPeopleLookup) {
|
|
8213
|
+
workflowAttempted = true;
|
|
8214
|
+
try {
|
|
8215
|
+
const workflow = await invokeLinkedInUrlEnrichmentWorkflow({
|
|
8216
|
+
contacts: contactsStillMissing,
|
|
8217
|
+
externalUserId: orgId || sessionOrgId || "cli_direct_lookup",
|
|
8218
|
+
timeoutMs: Math.min(timeoutMs, strategy.workflowStageBudgetMs)
|
|
8219
|
+
});
|
|
8220
|
+
if (!workflow.response.ok) {
|
|
8221
|
+
throw new Error(`LinkedIn enrichment workflow returned ${workflow.response.status}: ${workflow.bodyText.slice(0, 300)}`);
|
|
8222
|
+
}
|
|
8223
|
+
linkedInUrlByContactId = normalizeWorkflowLinkedInUrlResult({
|
|
8224
|
+
parsedBody: workflow.parsedBody,
|
|
8225
|
+
contacts: contactsStillMissing
|
|
8226
|
+
});
|
|
8227
|
+
for (const row of enrichedRows) {
|
|
8228
|
+
if (row.found)
|
|
8229
|
+
continue;
|
|
8230
|
+
const profile = linkedInUrlByContactId.get(row.contactId);
|
|
8231
|
+
if (profile?.linkedinUrl) {
|
|
8232
|
+
row.linkedinUrl = profile.linkedinUrl;
|
|
8233
|
+
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
8234
|
+
row.linkedinCompanyUrl = profile.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
8235
|
+
row.salesNavCompanyUrl = profile.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
8236
|
+
row.found = true;
|
|
8237
|
+
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
8238
|
+
row.source = "workflow";
|
|
8239
|
+
row.companySource =
|
|
8240
|
+
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "workflow" : row.companySource ?? null;
|
|
8241
|
+
}
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
catch (error) {
|
|
8245
|
+
writeProgress(`Skipping workflow profile enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
const serperContacts = directContacts.filter((contact) => enrichedRows.some((row) => row.contactId === contact.contact_id && !row.found));
|
|
8249
|
+
if (strategy.bulkMode && serperContacts.length > 0) {
|
|
8250
|
+
writeProgress(`Using bulk profile resolution strategy for ${serperContacts.length} remaining contacts.`);
|
|
8251
|
+
}
|
|
8252
|
+
const serperResults = await resolveSerperLinkedInProfilesInParallel({
|
|
8253
|
+
contacts: serperContacts.filter((contact) => !contact.isVariation),
|
|
8254
|
+
timeoutMs,
|
|
8255
|
+
concurrency: Math.min(strategy.serperConcurrency, serperContacts.length || 1),
|
|
8256
|
+
maxQueries: strategy.serperMaxQueries,
|
|
8257
|
+
overallBudgetMs: strategy.serperStageBudgetMs
|
|
8258
|
+
});
|
|
8259
|
+
for (const row of enrichedRows) {
|
|
8260
|
+
if (row.found)
|
|
8261
|
+
continue;
|
|
8262
|
+
const linkedinUrl = serperResults.get(row.contactId);
|
|
8263
|
+
if (!linkedinUrl)
|
|
8264
|
+
continue;
|
|
8265
|
+
row.linkedinUrl = linkedinUrl;
|
|
8266
|
+
row.found = true;
|
|
8267
|
+
row.source = "web-search";
|
|
8268
|
+
}
|
|
8269
|
+
const stillMissingAfterSerper = enrichedRows.filter((row) => !row.found);
|
|
8270
|
+
if (shouldAttemptBulkDirectProfileLookup({
|
|
8271
|
+
strategy,
|
|
8272
|
+
unresolvedRowCount: stillMissingAfterSerper.length
|
|
8273
|
+
})) {
|
|
8274
|
+
const bulkDirectCandidates = rankContactsForBulkDirectProfileLookup({
|
|
8275
|
+
contacts: directContacts.filter((contact) => stillMissingAfterSerper.some((row) => row.contactId === contact.contact_id)),
|
|
8276
|
+
rowsByContactId: rowByContactId,
|
|
8277
|
+
limit: strategy.bulkDirectProfileMaxRows
|
|
8278
|
+
});
|
|
8279
|
+
if (bulkDirectCandidates.length > 0) {
|
|
8280
|
+
writeProgress(`Using bulk direct profile follow-up for ${bulkDirectCandidates.length} high-signal unresolved contacts.`);
|
|
8281
|
+
try {
|
|
8282
|
+
directAttempted = true;
|
|
8283
|
+
const result = await invokeLinkedInUrlEnrichmentDirect({
|
|
8284
|
+
contacts: bulkDirectCandidates,
|
|
8285
|
+
timeoutMs: strategy.bulkDirectProfileTimeoutMs,
|
|
8286
|
+
perAttemptTimeoutMs: Math.min(strategy.bulkDirectProfileTimeoutMs, 2_500),
|
|
8287
|
+
perContactBudgetMs: strategy.bulkDirectProfileTimeoutMs
|
|
8288
|
+
});
|
|
8289
|
+
const directCompanyContextByKey = new Map((result.companyContexts ?? []).map((context) => [context.normalizedCompanyKey, context]));
|
|
8290
|
+
const bulkDirectByContactId = new Map(result.contacts.map((contact) => [
|
|
8291
|
+
contact.contact_id,
|
|
8292
|
+
{
|
|
8293
|
+
linkedinUrl: contact.linkedin_url ?? null,
|
|
8294
|
+
salesNavProfileUrl: contact.sales_nav_profile_url ?? null
|
|
8295
|
+
}
|
|
8296
|
+
]));
|
|
8297
|
+
for (const row of enrichedRows) {
|
|
8298
|
+
if (row.found)
|
|
8299
|
+
continue;
|
|
8300
|
+
const profile = bulkDirectByContactId.get(row.contactId);
|
|
8301
|
+
if (profile?.linkedinUrl) {
|
|
8302
|
+
row.linkedinUrl = profile.linkedinUrl;
|
|
8303
|
+
row.salesNavProfileUrl = profile.salesNavProfileUrl ?? row.salesNavProfileUrl ?? null;
|
|
8304
|
+
row.found = true;
|
|
8305
|
+
row.source = "linkedin-direct";
|
|
8306
|
+
}
|
|
8307
|
+
const directContact = bulkDirectCandidates.find((candidate) => candidate.contact_id === row.contactId && !candidate.isVariation);
|
|
8308
|
+
const companyContext = directContact
|
|
8309
|
+
? directCompanyContextByKey.get(buildDirectCompanyContextKey(directContact))
|
|
8310
|
+
: null;
|
|
8311
|
+
if (companyContext && !row.linkedinCompanyUrl) {
|
|
8312
|
+
row.linkedinCompanyUrl = companyContext.linkedinCompanyUrl ?? row.linkedinCompanyUrl ?? null;
|
|
8313
|
+
row.salesNavCompanyUrl = companyContext.salesNavCompanyUrl ?? row.salesNavCompanyUrl ?? null;
|
|
8314
|
+
row.companyFound = Boolean(row.linkedinCompanyUrl || row.salesNavCompanyUrl);
|
|
8315
|
+
row.companySource =
|
|
8316
|
+
row.linkedinCompanyUrl || row.salesNavCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
8317
|
+
row.matchedCompanyName = companyContext.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
8318
|
+
row.matchedCompanyEmployeeCount =
|
|
8319
|
+
companyContext.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
8323
|
+
catch (error) {
|
|
8324
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8325
|
+
if (!/Missing LinkedIn direct lookup session/i.test(message)) {
|
|
8326
|
+
writeProgress(`Skipping bulk direct profile follow-up: ${message}`);
|
|
8327
|
+
}
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
6317
8331
|
}
|
|
6318
8332
|
const payload = {
|
|
6319
8333
|
status: "ok",
|
|
@@ -6323,8 +8337,23 @@ program
|
|
|
6323
8337
|
companiesFound: enrichedRows.filter((row) => row.companyFound).length,
|
|
6324
8338
|
directAttempted,
|
|
6325
8339
|
workflowAttempted,
|
|
8340
|
+
bulkMode: strategy.bulkMode,
|
|
6326
8341
|
rows: enrichedRows
|
|
6327
8342
|
};
|
|
8343
|
+
for (const row of enrichedRows) {
|
|
8344
|
+
const contact = contactById.get(row.contactId);
|
|
8345
|
+
upsertLinkedInProfileHitCacheEntry({
|
|
8346
|
+
fullName: row.fullName,
|
|
8347
|
+
companyName: row.companyName,
|
|
8348
|
+
email: contact?.email,
|
|
8349
|
+
contactId: row.contactId,
|
|
8350
|
+
linkedinUrl: row.linkedinUrl ?? null,
|
|
8351
|
+
salesNavProfileUrl: row.salesNavProfileUrl ?? null,
|
|
8352
|
+
linkedinCompanyUrl: row.linkedinCompanyUrl ?? null,
|
|
8353
|
+
salesNavCompanyUrl: row.salesNavCompanyUrl ?? null
|
|
8354
|
+
});
|
|
8355
|
+
}
|
|
8356
|
+
await persistLinkedInProfileHitCache();
|
|
6328
8357
|
if (options.out) {
|
|
6329
8358
|
await writeJsonFile(options.out, payload);
|
|
6330
8359
|
}
|
|
@@ -6662,12 +8691,14 @@ program
|
|
|
6662
8691
|
});
|
|
6663
8692
|
program
|
|
6664
8693
|
.command("leads:generate")
|
|
6665
|
-
.description("Generate leads
|
|
8694
|
+
.description("Generate leads from your Salesprompter workspace when authenticated, or from fallback seeds.")
|
|
6666
8695
|
.requiredOption("--icp <path>", "Path to ICP JSON")
|
|
6667
8696
|
.option("--count <number>", "Number of leads to generate", "10")
|
|
6668
8697
|
.option("--domain <domain>", "Target a specific company domain like company.com")
|
|
6669
8698
|
.option("--company-domain <domain>", "Deprecated alias for --domain")
|
|
6670
8699
|
.option("--company-name <name>", "Optional company name override for a targeted domain")
|
|
8700
|
+
.option("--linkedin-company-page <url>", "LinkedIn company page to target when the domain is unknown")
|
|
8701
|
+
.option("--source <source>", "auto|workspace|fallback", "auto")
|
|
6671
8702
|
.requiredOption("--out <path>", "Output file path")
|
|
6672
8703
|
.action(async (options) => {
|
|
6673
8704
|
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
@@ -6675,9 +8706,15 @@ program
|
|
|
6675
8706
|
const domain = options.domain ?? options.companyDomain;
|
|
6676
8707
|
const target = {
|
|
6677
8708
|
companyDomain: domain,
|
|
6678
|
-
companyName: options.companyName
|
|
8709
|
+
companyName: options.companyName,
|
|
8710
|
+
linkedinCompanyPage: options.linkedinCompanyPage
|
|
6679
8711
|
};
|
|
6680
|
-
const result = await
|
|
8712
|
+
const result = await generateLeadsForCommand({
|
|
8713
|
+
icp,
|
|
8714
|
+
count,
|
|
8715
|
+
target,
|
|
8716
|
+
source: options.source
|
|
8717
|
+
});
|
|
6681
8718
|
await writeJsonFile(options.out, result.leads);
|
|
6682
8719
|
printOutput({
|
|
6683
8720
|
status: "ok",
|
|
@@ -6722,6 +8759,8 @@ program
|
|
|
6722
8759
|
.option("--domain <domain>", "Target a specific company domain like company.com")
|
|
6723
8760
|
.option("--company-domain <domain>", "Deprecated alias for --domain")
|
|
6724
8761
|
.option("--company-name <name>", "Optional company name override for a targeted domain")
|
|
8762
|
+
.option("--linkedin-company-page <url>", "LinkedIn company page to target when the domain is unknown")
|
|
8763
|
+
.option("--source <source>", "auto|workspace|fallback", "auto")
|
|
6725
8764
|
.option("--out-prefix <path>", "Output path prefix (writes <prefix>-leads.json, <prefix>-enriched.json, <prefix>-scored.json)", "./data/leads-pipeline")
|
|
6726
8765
|
.action(async (options) => {
|
|
6727
8766
|
const icp = await readJsonFile(options.icp, IcpSchema);
|
|
@@ -6729,13 +8768,19 @@ program
|
|
|
6729
8768
|
const domain = options.domain ?? options.companyDomain;
|
|
6730
8769
|
const target = {
|
|
6731
8770
|
companyDomain: domain,
|
|
6732
|
-
companyName: options.companyName
|
|
8771
|
+
companyName: options.companyName,
|
|
8772
|
+
linkedinCompanyPage: options.linkedinCompanyPage
|
|
6733
8773
|
};
|
|
6734
8774
|
const outPrefix = String(options.outPrefix);
|
|
6735
8775
|
const leadsOut = `${outPrefix}-leads.json`;
|
|
6736
8776
|
const enrichedOut = `${outPrefix}-enriched.json`;
|
|
6737
8777
|
const scoredOut = `${outPrefix}-scored.json`;
|
|
6738
|
-
const generated = await
|
|
8778
|
+
const generated = await generateLeadsForCommand({
|
|
8779
|
+
icp,
|
|
8780
|
+
count,
|
|
8781
|
+
target,
|
|
8782
|
+
source: options.source
|
|
8783
|
+
});
|
|
6739
8784
|
await writeJsonFile(leadsOut, generated.leads);
|
|
6740
8785
|
const enriched = await enrichmentProvider.enrichLeads(generated.leads);
|
|
6741
8786
|
await writeJsonFile(enrichedOut, enriched);
|
|
@@ -7473,6 +9518,7 @@ program
|
|
|
7473
9518
|
.option("--max-results-per-search <number>", "Maximum results allowed for a sliced search", "2500")
|
|
7474
9519
|
.option("--number-of-profiles <number>", "Profiles to export per sliced query", "2500")
|
|
7475
9520
|
.option("--slice-preset <name>", "Slice preset label stored with the export runs", "human-resources-crawl")
|
|
9521
|
+
.option("--client-id <number>", "Client id used to generate and store the legacy Neon lead list projection")
|
|
7476
9522
|
.option("--max-split-depth <number>", "Maximum number of adaptive split dimensions to use", "6")
|
|
7477
9523
|
.option("--max-slices <number>", "Safety cap for total claimed slices in this invocation", "1000")
|
|
7478
9524
|
.option("--max-retries <number>", "Retries for non-splitting export failures", "3")
|
|
@@ -7491,6 +9537,7 @@ program
|
|
|
7491
9537
|
const jobId = z.string().uuid().optional().parse(options.jobId);
|
|
7492
9538
|
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
7493
9539
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9540
|
+
const clientId = parseOptionalSalesNavigatorClientId(options.clientId);
|
|
7494
9541
|
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
7495
9542
|
const maxSlices = z.coerce.number().int().min(1).max(10000).parse(options.maxSlices);
|
|
7496
9543
|
const maxRetries = z.coerce.number().int().min(0).max(5).parse(options.maxRetries);
|
|
@@ -7510,6 +9557,7 @@ program
|
|
|
7510
9557
|
jobId: jobId ?? null,
|
|
7511
9558
|
maxResultsPerSearch,
|
|
7512
9559
|
numberOfProfiles,
|
|
9560
|
+
clientId,
|
|
7513
9561
|
slicePreset: options.slicePreset,
|
|
7514
9562
|
maxSplitDepth,
|
|
7515
9563
|
maxSlices,
|
|
@@ -7610,6 +9658,7 @@ program
|
|
|
7610
9658
|
traceId: logger.traceId,
|
|
7611
9659
|
command: {
|
|
7612
9660
|
sourceQueryUrl: queryUrl,
|
|
9661
|
+
clientId,
|
|
7613
9662
|
slicePreset: options.slicePreset,
|
|
7614
9663
|
maxResultsPerSearch,
|
|
7615
9664
|
numberOfProfiles,
|
|
@@ -7631,6 +9680,7 @@ program
|
|
|
7631
9680
|
splitTrail: seed.splitTrail,
|
|
7632
9681
|
rawPayload: {
|
|
7633
9682
|
workflow: "salesnav:crawl",
|
|
9683
|
+
clientId,
|
|
7634
9684
|
traceId: logger.traceId
|
|
7635
9685
|
}
|
|
7636
9686
|
}
|
|
@@ -7670,6 +9720,7 @@ program
|
|
|
7670
9720
|
idlePollSeconds,
|
|
7671
9721
|
idleMaxPolls,
|
|
7672
9722
|
parallelExports,
|
|
9723
|
+
clientId,
|
|
7673
9724
|
traceId: logger.traceId,
|
|
7674
9725
|
logger
|
|
7675
9726
|
});
|
|
@@ -7750,6 +9801,43 @@ program
|
|
|
7750
9801
|
recentEvents
|
|
7751
9802
|
});
|
|
7752
9803
|
});
|
|
9804
|
+
program
|
|
9805
|
+
.command("phantombuster:containers:sync")
|
|
9806
|
+
.alias("pb:containers:sync")
|
|
9807
|
+
.description("Fetch Phantombuster containers for configured agents and store them in Neon.")
|
|
9808
|
+
.option("--agent-id <id>", "Phantombuster agent id to sync. Repeat to sync multiple agents.", collectStringOptionValue, [])
|
|
9809
|
+
.option("--limit <number>", "Maximum containers to fetch per Phantombuster page", "100")
|
|
9810
|
+
.option("--max-pages <number>", "Maximum Phantombuster pages to fetch per agent", "50")
|
|
9811
|
+
.option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
|
|
9812
|
+
.option("--before-ended-at <iso>", "Only fetch containers that ended before this ISO timestamp")
|
|
9813
|
+
.option("--metadata-only", "Store container metadata without fetching output and result objects", false)
|
|
9814
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
9815
|
+
.action(async (options) => {
|
|
9816
|
+
const agentIds = z.array(z.string().min(1)).parse(options.agentId);
|
|
9817
|
+
const limit = z.coerce.number().int().min(1).max(500).parse(options.limit);
|
|
9818
|
+
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
9819
|
+
const mode = z.enum(["all", "finalized"]).parse(options.mode);
|
|
9820
|
+
const beforeEndedAt = options.beforeEndedAt
|
|
9821
|
+
? z.string().datetime().parse(options.beforeEndedAt)
|
|
9822
|
+
: undefined;
|
|
9823
|
+
const session = await requireAuthSession();
|
|
9824
|
+
const result = await syncPhantombusterContainersViaApp(session, {
|
|
9825
|
+
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
9826
|
+
limit,
|
|
9827
|
+
maxPages,
|
|
9828
|
+
mode,
|
|
9829
|
+
beforeEndedAt,
|
|
9830
|
+
includeResults: !options.metadataOnly
|
|
9831
|
+
});
|
|
9832
|
+
const payload = {
|
|
9833
|
+
...result,
|
|
9834
|
+
dryRun: false
|
|
9835
|
+
};
|
|
9836
|
+
if (options.out) {
|
|
9837
|
+
await writeJsonFile(options.out, payload);
|
|
9838
|
+
}
|
|
9839
|
+
printOutput(payload);
|
|
9840
|
+
});
|
|
7753
9841
|
program
|
|
7754
9842
|
.command("salesnav:export")
|
|
7755
9843
|
.alias("search:export")
|
|
@@ -7758,12 +9846,18 @@ program
|
|
|
7758
9846
|
.option("--max-results-per-search <number>", "Maximum results allowed for a sliced search", "2500")
|
|
7759
9847
|
.option("--number-of-profiles <number>", "Profiles to export per sliced query", "2500")
|
|
7760
9848
|
.option("--slice-preset <name>", "Slice preset label stored with the export run", "human-resources-default")
|
|
9849
|
+
.option("--client-id <number>", "Client id used to generate and store the legacy Neon lead list projection")
|
|
9850
|
+
.option("--agent-busy-wait-seconds <number>", "Seconds to wait before retrying when the export agent is already busy", "30")
|
|
9851
|
+
.option("--agent-busy-max-waits <number>", "How many busy-agent waits to tolerate before failing the export", "20")
|
|
7761
9852
|
.option("--out <path>", "Optional local JSON output path")
|
|
7762
9853
|
.option("--dry-run", "Only generate sliced query URLs without exporting them", false)
|
|
7763
9854
|
.action(async (options) => {
|
|
7764
9855
|
const queryUrls = z.array(z.string().url()).min(1).parse(options.queryUrl);
|
|
7765
9856
|
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
7766
9857
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9858
|
+
const agentBusyWaitSeconds = z.coerce.number().int().min(1).max(300).parse(options.agentBusyWaitSeconds);
|
|
9859
|
+
const agentBusyMaxWaits = z.coerce.number().int().min(0).max(100).parse(options.agentBusyMaxWaits);
|
|
9860
|
+
const clientId = parseOptionalSalesNavigatorClientId(options.clientId);
|
|
7767
9861
|
const prepared = queryUrls.map((queryUrl) => buildSalesNavigatorPeopleSlice(queryUrl));
|
|
7768
9862
|
const effectiveDryRun = Boolean(options.dryRun || shouldBypassAuth());
|
|
7769
9863
|
if (effectiveDryRun) {
|
|
@@ -7785,10 +9879,10 @@ program
|
|
|
7785
9879
|
printOutput(payload);
|
|
7786
9880
|
return;
|
|
7787
9881
|
}
|
|
7788
|
-
|
|
9882
|
+
let session = await requireAuthSession();
|
|
7789
9883
|
const exported = [];
|
|
7790
9884
|
for (const item of prepared) {
|
|
7791
|
-
const result = await
|
|
9885
|
+
const result = await runSalesNavigatorExportWithAgentWait(session, {
|
|
7792
9886
|
sourceQueryUrl: item.sourceQueryUrl,
|
|
7793
9887
|
slicedQueryUrl: item.slicedQueryUrl,
|
|
7794
9888
|
appliedFilters: item.appliedFilters,
|
|
@@ -7797,12 +9891,17 @@ program
|
|
|
7797
9891
|
slicePreset: options.slicePreset,
|
|
7798
9892
|
rawPayload: {
|
|
7799
9893
|
workflow: "salesnav:export",
|
|
9894
|
+
clientId,
|
|
7800
9895
|
sourceQueryUrl: item.sourceQueryUrl,
|
|
7801
9896
|
slicedQueryUrl: item.slicedQueryUrl,
|
|
7802
9897
|
appliedFilters: item.appliedFilters
|
|
7803
9898
|
}
|
|
9899
|
+
}, {
|
|
9900
|
+
waitSeconds: agentBusyWaitSeconds,
|
|
9901
|
+
maxWaits: agentBusyMaxWaits
|
|
7804
9902
|
});
|
|
7805
9903
|
exported.push(result);
|
|
9904
|
+
session = await requireAuthSession();
|
|
7806
9905
|
}
|
|
7807
9906
|
const payload = {
|
|
7808
9907
|
status: "ok",
|