salesprompter-cli 0.1.49 → 0.1.51
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 +5 -0
- package/dist/cli.js +791 -9
- package/dist/linkedin-companies.js +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,6 +70,11 @@ salesprompter leads:collect \
|
|
|
70
70
|
--linkedin-url "$SALES_NAV_PEOPLE_URL" \
|
|
71
71
|
--max-results 1000
|
|
72
72
|
|
|
73
|
+
# Enrich one or more stored imports: Sales Navigator company -> official domain -> Hunter email
|
|
74
|
+
# This updates workspace data only. It does not create or start outreach.
|
|
75
|
+
salesprompter leads:enrich-import \
|
|
76
|
+
--run-id "$CLI_IMPORT_RUN_ID"
|
|
77
|
+
|
|
73
78
|
salesprompter affiliate:launch \
|
|
74
79
|
--affiliate-link "https://example.com/?ref=you" \
|
|
75
80
|
--linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
|
package/dist/cli.js
CHANGED
|
@@ -258,6 +258,51 @@ const CliEmailEnrichmentCompaniesResponseSchema = z.object({
|
|
|
258
258
|
linkedinCompanyPage: z.string().nullable()
|
|
259
259
|
}))
|
|
260
260
|
});
|
|
261
|
+
const CliImportCompanyCandidateSchema = z.object({
|
|
262
|
+
companyKey: z.string().min(1),
|
|
263
|
+
companyId: z.string().nullable(),
|
|
264
|
+
companyName: z.string().nullable(),
|
|
265
|
+
salesNavCompanyUrl: z.string().nullable(),
|
|
266
|
+
linkedinCompanyUrl: z.string().nullable(),
|
|
267
|
+
personCount: z.number().int().nonnegative(),
|
|
268
|
+
profileStatus: z.string().nullable(),
|
|
269
|
+
profileDomain: z.string().nullable()
|
|
270
|
+
});
|
|
271
|
+
const CliImportCompanyCandidatesResponseSchema = z.object({
|
|
272
|
+
companies: z.array(CliImportCompanyCandidateSchema)
|
|
273
|
+
});
|
|
274
|
+
const CliImportCompanySaveResponseSchema = z.object({
|
|
275
|
+
saved: z.number().int().nonnegative(),
|
|
276
|
+
enriched: z.number().int().nonnegative(),
|
|
277
|
+
domains: z.number().int().nonnegative(),
|
|
278
|
+
notFound: z.number().int().nonnegative(),
|
|
279
|
+
failed: z.number().int().nonnegative()
|
|
280
|
+
});
|
|
281
|
+
const CliImportHunterBatchResponseSchema = z.object({
|
|
282
|
+
selected: z.number().int().nonnegative(),
|
|
283
|
+
processed: z.number().int().nonnegative(),
|
|
284
|
+
found: z.number().int().nonnegative(),
|
|
285
|
+
notFound: z.number().int().nonnegative(),
|
|
286
|
+
failed: z.number().int().nonnegative(),
|
|
287
|
+
halted: z.boolean(),
|
|
288
|
+
error: z.string().nullable()
|
|
289
|
+
});
|
|
290
|
+
const CliImportEnrichmentStatusSchema = z.object({
|
|
291
|
+
runCount: z.coerce.number().int().nonnegative(),
|
|
292
|
+
memberships: z.coerce.number().int().nonnegative(),
|
|
293
|
+
uniquePeople: z.coerce.number().int().nonnegative(),
|
|
294
|
+
distinctCompanies: z.coerce.number().int().nonnegative(),
|
|
295
|
+
companiesEnriched: z.coerce.number().int().nonnegative(),
|
|
296
|
+
companiesNotFound: z.coerce.number().int().nonnegative(),
|
|
297
|
+
companiesFailed: z.coerce.number().int().nonnegative(),
|
|
298
|
+
domainsFound: z.coerce.number().int().nonnegative(),
|
|
299
|
+
peopleWithDomain: z.coerce.number().int().nonnegative(),
|
|
300
|
+
emailsFound: z.coerce.number().int().nonnegative(),
|
|
301
|
+
emailsNotFound: z.coerce.number().int().nonnegative(),
|
|
302
|
+
emailsFailed: z.coerce.number().int().nonnegative(),
|
|
303
|
+
emailsProcessing: z.coerce.number().int().nonnegative(),
|
|
304
|
+
emailsPending: z.coerce.number().int().nonnegative()
|
|
305
|
+
});
|
|
261
306
|
const SalesNavigatorLaunchDiagnosticsSchema = z.object({
|
|
262
307
|
orderedCandidateAgentIds: z.array(z.string().min(1)),
|
|
263
308
|
runningAgentIds: z.array(z.string().min(1)),
|
|
@@ -479,6 +524,7 @@ const cliPacks = [
|
|
|
479
524
|
commands: [
|
|
480
525
|
"leads:discover",
|
|
481
526
|
"leads:collect",
|
|
527
|
+
"leads:enrich-import",
|
|
482
528
|
"search:run",
|
|
483
529
|
"search:status",
|
|
484
530
|
"search:export",
|
|
@@ -517,6 +563,7 @@ const helpAliasByCommandName = new Map([
|
|
|
517
563
|
["linkedin-products:scrape", "market:scrape"],
|
|
518
564
|
["salesnav:from-product-category", "leads:discover"],
|
|
519
565
|
["salesnav:people:collect", "leads:collect"],
|
|
566
|
+
["salesnav:people:enrich", "leads:enrich-import"],
|
|
520
567
|
["salesnav:crawl", "search:run"],
|
|
521
568
|
["salesnav:crawl:status", "search:status"],
|
|
522
569
|
["salesnav:export", "search:export"],
|
|
@@ -562,6 +609,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
562
609
|
"linkedin-products:scrape",
|
|
563
610
|
"salesnav:from-product-category",
|
|
564
611
|
"salesnav:people:collect",
|
|
612
|
+
"salesnav:people:enrich",
|
|
565
613
|
"salesnav:crawl",
|
|
566
614
|
"salesnav:crawl:status",
|
|
567
615
|
"salesnav:export",
|
|
@@ -6696,6 +6744,29 @@ async function enrichDirectEmailCompaniesViaApp(session, payload) {
|
|
|
6696
6744
|
}), CliEmailEnrichmentCompaniesResponseSchema);
|
|
6697
6745
|
return value;
|
|
6698
6746
|
}
|
|
6747
|
+
async function runCliImportEnrichmentAction(session, body, schema) {
|
|
6748
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/leads/imports/enrichment`, {
|
|
6749
|
+
method: "POST",
|
|
6750
|
+
headers: {
|
|
6751
|
+
"Content-Type": "application/json",
|
|
6752
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6753
|
+
},
|
|
6754
|
+
body: JSON.stringify(body)
|
|
6755
|
+
}), schema);
|
|
6756
|
+
return value;
|
|
6757
|
+
}
|
|
6758
|
+
async function getCliImportCompanyCandidatesViaApp(session, input) {
|
|
6759
|
+
return runCliImportEnrichmentAction(session, { action: "company-candidates", ...input }, CliImportCompanyCandidatesResponseSchema);
|
|
6760
|
+
}
|
|
6761
|
+
async function saveCliImportCompanyProfilesViaApp(session, profiles) {
|
|
6762
|
+
return runCliImportEnrichmentAction(session, { action: "save-companies", profiles }, CliImportCompanySaveResponseSchema);
|
|
6763
|
+
}
|
|
6764
|
+
async function runCliImportHunterBatchViaApp(session, input) {
|
|
6765
|
+
return runCliImportEnrichmentAction(session, { action: "hunter", ...input }, CliImportHunterBatchResponseSchema);
|
|
6766
|
+
}
|
|
6767
|
+
async function getCliImportEnrichmentStatusViaApp(session, runIds) {
|
|
6768
|
+
return runCliImportEnrichmentAction(session, { action: "status", runIds }, CliImportEnrichmentStatusSchema);
|
|
6769
|
+
}
|
|
6699
6770
|
async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
6700
6771
|
const url = new URL('/api/cli/linkedin-companies/status', session.apiBaseUrl);
|
|
6701
6772
|
url.searchParams.set('clientId', String(payload.clientId));
|
|
@@ -6726,6 +6797,16 @@ class LocalSalesNavigatorRateLimitError extends Error {
|
|
|
6726
6797
|
this.retryAfterMs = retryAfterMs;
|
|
6727
6798
|
}
|
|
6728
6799
|
}
|
|
6800
|
+
class LocalSalesNavigatorHttpError extends Error {
|
|
6801
|
+
status;
|
|
6802
|
+
body;
|
|
6803
|
+
constructor(status, body, message) {
|
|
6804
|
+
super(message);
|
|
6805
|
+
this.name = "LocalSalesNavigatorHttpError";
|
|
6806
|
+
this.status = status;
|
|
6807
|
+
this.body = body;
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6729
6810
|
function stripShellQuotes(value) {
|
|
6730
6811
|
const trimmed = value.trim();
|
|
6731
6812
|
if (trimmed.length >= 2) {
|
|
@@ -7755,6 +7836,8 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
|
|
|
7755
7836
|
status: "task",
|
|
7756
7837
|
requestId: pending.requestId,
|
|
7757
7838
|
url: pending.url,
|
|
7839
|
+
headers: pending.headers,
|
|
7840
|
+
requiredHeaders: pending.requiredHeaders,
|
|
7758
7841
|
}
|
|
7759
7842
|
: { status: "idle" });
|
|
7760
7843
|
return;
|
|
@@ -7793,7 +7876,10 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
|
|
|
7793
7876
|
return;
|
|
7794
7877
|
}
|
|
7795
7878
|
if (!Number.isFinite(status) || status < 200 || status >= 300) {
|
|
7796
|
-
|
|
7879
|
+
const recovery = status === 401 || status === 403
|
|
7880
|
+
? " Refresh the Sales Navigator search, capture the current csrf-token from a native Lead Search request, and replay this task with the supplied headers."
|
|
7881
|
+
: "";
|
|
7882
|
+
current.reject(new LocalSalesNavigatorHttpError(status, body, `Sales Navigator browser relay failed with HTTP ${status}: ${bodyPreview}.${recovery}`));
|
|
7797
7883
|
writeLocalAccountSearchRelayJson(response, 200, {
|
|
7798
7884
|
status: "failed",
|
|
7799
7885
|
});
|
|
@@ -7828,21 +7914,36 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
|
|
|
7828
7914
|
}
|
|
7829
7915
|
return {
|
|
7830
7916
|
port: address.port,
|
|
7831
|
-
request(
|
|
7917
|
+
request(request) {
|
|
7832
7918
|
if (pending) {
|
|
7833
7919
|
throw new Error("Account Search browser relay already has a pending task.");
|
|
7834
7920
|
}
|
|
7835
|
-
const parsedUrl = new URL(url);
|
|
7921
|
+
const parsedUrl = new URL(request.url);
|
|
7836
7922
|
if (!/(^|\.)linkedin\.com$/i.test(parsedUrl.hostname) ||
|
|
7837
7923
|
!(parsedUrl.pathname.includes("/sales-api/salesApiAccountSearch") ||
|
|
7838
|
-
parsedUrl.pathname.includes("/sales-api/salesApiLeadSearch")
|
|
7839
|
-
|
|
7924
|
+
parsedUrl.pathname.includes("/sales-api/salesApiLeadSearch") ||
|
|
7925
|
+
parsedUrl.pathname.includes("/sales-api/salesApiCompanies"))) {
|
|
7926
|
+
throw new Error("Browser relay only accepts LinkedIn Account Search, Lead Search, or company-detail API URLs.");
|
|
7840
7927
|
}
|
|
7928
|
+
const headers = Object.fromEntries(Object.entries(request.headers)
|
|
7929
|
+
.map(([name, value]) => [name.toLowerCase(), value])
|
|
7930
|
+
.filter(([name]) => ![
|
|
7931
|
+
"cookie",
|
|
7932
|
+
"host",
|
|
7933
|
+
"origin",
|
|
7934
|
+
"referer",
|
|
7935
|
+
"content-length",
|
|
7936
|
+
"user-agent",
|
|
7937
|
+
].includes(name)));
|
|
7938
|
+
headers.accept ??= "*/*";
|
|
7939
|
+
headers["x-restli-protocol-version"] ??= "2.0.0";
|
|
7841
7940
|
requestSequence += 1;
|
|
7842
7941
|
return new Promise((resolve, reject) => {
|
|
7843
7942
|
pending = {
|
|
7844
7943
|
requestId: `sales-nav-search-${requestSequence}`,
|
|
7845
|
-
url,
|
|
7944
|
+
url: request.url,
|
|
7945
|
+
headers,
|
|
7946
|
+
requiredHeaders: ["csrf-token"],
|
|
7846
7947
|
resolve,
|
|
7847
7948
|
reject,
|
|
7848
7949
|
};
|
|
@@ -13281,7 +13382,7 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13281
13382
|
retryMaxDelayMs: 10_000
|
|
13282
13383
|
},
|
|
13283
13384
|
executeRequest: browserRelay
|
|
13284
|
-
? (request) => browserRelay.request(request
|
|
13385
|
+
? (request) => browserRelay.request(request)
|
|
13285
13386
|
: undefined
|
|
13286
13387
|
});
|
|
13287
13388
|
}
|
|
@@ -13303,6 +13404,14 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13303
13404
|
dryRun: false,
|
|
13304
13405
|
collected: collected.people.length,
|
|
13305
13406
|
totalResults: collected.totalResults,
|
|
13407
|
+
collectionComplete: collected.totalResults != null &&
|
|
13408
|
+
collected.people.length >= collected.totalResults,
|
|
13409
|
+
truncated: collected.totalResults != null &&
|
|
13410
|
+
collected.totalResults > collected.people.length &&
|
|
13411
|
+
collected.people.length >= maxResults,
|
|
13412
|
+
remainingResults: collected.totalResults == null
|
|
13413
|
+
? null
|
|
13414
|
+
: Math.max(0, collected.totalResults - collected.people.length),
|
|
13306
13415
|
fetchedPages: collected.fetchedPages,
|
|
13307
13416
|
runId: imported.runId,
|
|
13308
13417
|
imported: imported.imported,
|
|
@@ -13314,6 +13423,657 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
|
|
|
13314
13423
|
await writeJsonFile(options.out, payload);
|
|
13315
13424
|
return payload;
|
|
13316
13425
|
}
|
|
13426
|
+
class CliImportCompanyRateLimitError extends Error {
|
|
13427
|
+
constructor(message) {
|
|
13428
|
+
super(message);
|
|
13429
|
+
this.name = "CliImportCompanyRateLimitError";
|
|
13430
|
+
}
|
|
13431
|
+
}
|
|
13432
|
+
function collectRunId(value, previous) {
|
|
13433
|
+
return [...previous, ...value.split(",")]
|
|
13434
|
+
.map((entry) => entry.trim())
|
|
13435
|
+
.filter(Boolean);
|
|
13436
|
+
}
|
|
13437
|
+
function normalizeCompanyWebsite(value) {
|
|
13438
|
+
const trimmed = value?.trim() ?? "";
|
|
13439
|
+
if (!trimmed)
|
|
13440
|
+
return null;
|
|
13441
|
+
try {
|
|
13442
|
+
return new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`).toString();
|
|
13443
|
+
}
|
|
13444
|
+
catch {
|
|
13445
|
+
return null;
|
|
13446
|
+
}
|
|
13447
|
+
}
|
|
13448
|
+
const CLI_IMPORT_COMPANY_DECORATION = "(entityUrn,name,employeeCount,employeeDisplayCount,employeeCountRange,account(saved,noteCount,listCount,crmStatus,starred),pictureInfo,companyPictureDisplayImage,companyBackgroundCoverImage,description,industry,location,headquarters,website,revenueRange,crmOpportunities,flagshipCompanyUrl,employeeGrowthPercentages,employees*~fs_salesProfile(entityUrn,firstName,lastName,fullName,pictureInfo,profilePictureDisplayImage),specialties,type,yearFounded)";
|
|
13449
|
+
class CliImportCompanySessionError extends Error {
|
|
13450
|
+
constructor(message) {
|
|
13451
|
+
super(message);
|
|
13452
|
+
this.name = "CliImportCompanySessionError";
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
function buildCliImportAccountSearchUrl(companyName) {
|
|
13456
|
+
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
13457
|
+
"https://www.linkedin.com";
|
|
13458
|
+
return (`${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiAccountSearch` +
|
|
13459
|
+
`?q=searchQuery&query=(spellCorrectionEnabled:true,keywords:${encodeLinkedInRestliValue(companyName)})` +
|
|
13460
|
+
"&start=0&count=25" +
|
|
13461
|
+
"&decorationId=com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-4");
|
|
13462
|
+
}
|
|
13463
|
+
function encodeLinkedInRestliValue(value) {
|
|
13464
|
+
return encodeURIComponent(value)
|
|
13465
|
+
.replace(/\(/g, "%28")
|
|
13466
|
+
.replace(/\)/g, "%29");
|
|
13467
|
+
}
|
|
13468
|
+
function buildCliImportCompanyDetailsUrl(companyIds) {
|
|
13469
|
+
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
13470
|
+
"https://www.linkedin.com";
|
|
13471
|
+
return (`${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiCompanies` +
|
|
13472
|
+
`?ids=List(${companyIds.map((companyId) => encodeURIComponent(companyId)).join(",")})` +
|
|
13473
|
+
`&decoration=${encodeLinkedInRestliValue(CLI_IMPORT_COMPANY_DECORATION)}`);
|
|
13474
|
+
}
|
|
13475
|
+
function buildCliImportCompanyHeaders(config, companyId) {
|
|
13476
|
+
return {
|
|
13477
|
+
...buildLinkedInSalesNavigatorAccountSearchHeaders(config),
|
|
13478
|
+
referer: companyId
|
|
13479
|
+
? `https://www.linkedin.com/sales/company/${companyId}`
|
|
13480
|
+
: "https://www.linkedin.com/sales/search/company",
|
|
13481
|
+
};
|
|
13482
|
+
}
|
|
13483
|
+
async function fetchCliImportSalesNavigatorJson(params) {
|
|
13484
|
+
if (params.browserRelay) {
|
|
13485
|
+
try {
|
|
13486
|
+
const result = await params.browserRelay.request({
|
|
13487
|
+
url: params.url,
|
|
13488
|
+
headers: params.config
|
|
13489
|
+
? buildCliImportCompanyHeaders(params.config, params.companyId)
|
|
13490
|
+
: {},
|
|
13491
|
+
});
|
|
13492
|
+
const body = cliImportCompanyRecord(result.body);
|
|
13493
|
+
return { status: 200, body, finalUrl: params.url };
|
|
13494
|
+
}
|
|
13495
|
+
catch (error) {
|
|
13496
|
+
if (error instanceof LocalSalesNavigatorRateLimitError) {
|
|
13497
|
+
throw new CliImportCompanyRateLimitError(error.message);
|
|
13498
|
+
}
|
|
13499
|
+
if (error instanceof LocalSalesNavigatorHttpError) {
|
|
13500
|
+
if (error.status === 401 || error.status === 403) {
|
|
13501
|
+
throw new CliImportCompanySessionError(error.message);
|
|
13502
|
+
}
|
|
13503
|
+
return {
|
|
13504
|
+
status: error.status,
|
|
13505
|
+
body: cliImportCompanyRecord(error.body),
|
|
13506
|
+
finalUrl: params.url,
|
|
13507
|
+
};
|
|
13508
|
+
}
|
|
13509
|
+
throw error;
|
|
13510
|
+
}
|
|
13511
|
+
}
|
|
13512
|
+
if (!params.config) {
|
|
13513
|
+
throw new Error("Company enrichment requires a LinkedIn session or --browser-relay-port.");
|
|
13514
|
+
}
|
|
13515
|
+
const controller = new AbortController();
|
|
13516
|
+
const timeout = setTimeout(() => controller.abort(), params.timeoutMs);
|
|
13517
|
+
try {
|
|
13518
|
+
const response = await fetch(params.url, {
|
|
13519
|
+
method: "GET",
|
|
13520
|
+
signal: controller.signal,
|
|
13521
|
+
headers: buildCliImportCompanyHeaders(params.config, params.companyId),
|
|
13522
|
+
});
|
|
13523
|
+
const finalUrl = response.url || params.url;
|
|
13524
|
+
if (response.status === 429 || response.status === 999) {
|
|
13525
|
+
throw new CliImportCompanyRateLimitError(`LinkedIn rate limited ${params.label} (${response.status}). Resume the same command after cooldown.`);
|
|
13526
|
+
}
|
|
13527
|
+
if (response.status === 401 ||
|
|
13528
|
+
response.status === 403 ||
|
|
13529
|
+
/\/uas\/login|\/checkpoint\/challenge|\/authwall/i.test(finalUrl)) {
|
|
13530
|
+
throw new CliImportCompanySessionError(`LinkedIn rejected ${params.label}. Refresh the Sales Navigator session, then rerun the same command.`);
|
|
13531
|
+
}
|
|
13532
|
+
const text = await response.text();
|
|
13533
|
+
if (/sign in to linkedin|authwall|checkpoint\/challenge/i.test(text)) {
|
|
13534
|
+
throw new CliImportCompanySessionError(`LinkedIn redirected ${params.label} to authentication. Refresh the Sales Navigator session, then rerun the same command.`);
|
|
13535
|
+
}
|
|
13536
|
+
let body = null;
|
|
13537
|
+
if (text.trim()) {
|
|
13538
|
+
try {
|
|
13539
|
+
const parsed = JSON.parse(text);
|
|
13540
|
+
body =
|
|
13541
|
+
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
13542
|
+
? parsed
|
|
13543
|
+
: null;
|
|
13544
|
+
}
|
|
13545
|
+
catch {
|
|
13546
|
+
if (response.ok) {
|
|
13547
|
+
throw new Error(`${params.label} returned a non-JSON response (${response.status}).`);
|
|
13548
|
+
}
|
|
13549
|
+
}
|
|
13550
|
+
}
|
|
13551
|
+
return { status: response.status, body, finalUrl };
|
|
13552
|
+
}
|
|
13553
|
+
catch (error) {
|
|
13554
|
+
if (error instanceof CliImportCompanyRateLimitError ||
|
|
13555
|
+
error instanceof CliImportCompanySessionError) {
|
|
13556
|
+
throw error;
|
|
13557
|
+
}
|
|
13558
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
13559
|
+
throw new Error(`${params.label} timed out after ${params.timeoutMs}ms.`);
|
|
13560
|
+
}
|
|
13561
|
+
throw error;
|
|
13562
|
+
}
|
|
13563
|
+
finally {
|
|
13564
|
+
clearTimeout(timeout);
|
|
13565
|
+
}
|
|
13566
|
+
}
|
|
13567
|
+
function exactCliImportAccountMatch(candidate, accounts) {
|
|
13568
|
+
const sourceName = candidate.companyName ?? "";
|
|
13569
|
+
const expectedNames = new Set([sourceName, aggressivelyCleanLookupCompanyName(sourceName)]
|
|
13570
|
+
.map((value) => normalizeLooseMatchText(value))
|
|
13571
|
+
.filter(Boolean));
|
|
13572
|
+
if (expectedNames.size === 0)
|
|
13573
|
+
return null;
|
|
13574
|
+
return (accounts.find((account) => {
|
|
13575
|
+
const companyName = typeof account.companyName === "string" ? account.companyName : "";
|
|
13576
|
+
const normalizedNames = [
|
|
13577
|
+
companyName,
|
|
13578
|
+
aggressivelyCleanLookupCompanyName(companyName),
|
|
13579
|
+
].map((value) => normalizeLooseMatchText(value));
|
|
13580
|
+
return normalizedNames.some((value) => value && expectedNames.has(value));
|
|
13581
|
+
}) ?? null);
|
|
13582
|
+
}
|
|
13583
|
+
async function resolveCliImportCompanyIdentity(candidate, config, timeoutMs, browserRelay) {
|
|
13584
|
+
let resolvedCompanyId = candidate.companyId && /^\d+$/.test(candidate.companyId)
|
|
13585
|
+
? candidate.companyId
|
|
13586
|
+
: null;
|
|
13587
|
+
let salesNavCompanyUrl = candidate.salesNavCompanyUrl;
|
|
13588
|
+
let linkedinCompanyUrl = candidate.linkedinCompanyUrl;
|
|
13589
|
+
let matchedCompanyName = null;
|
|
13590
|
+
let accountSearchUsed = false;
|
|
13591
|
+
let account = null;
|
|
13592
|
+
if (!resolvedCompanyId && candidate.companyName) {
|
|
13593
|
+
accountSearchUsed = true;
|
|
13594
|
+
const queryUrl = buildCliImportAccountSearchUrl(candidate.companyName);
|
|
13595
|
+
const response = await fetchCliImportSalesNavigatorJson({
|
|
13596
|
+
url: queryUrl,
|
|
13597
|
+
config,
|
|
13598
|
+
browserRelay,
|
|
13599
|
+
timeoutMs,
|
|
13600
|
+
label: `Sales Navigator Account Search for ${candidate.companyName}`,
|
|
13601
|
+
});
|
|
13602
|
+
if (response.status < 200 || response.status >= 300) {
|
|
13603
|
+
throw new Error(`Sales Navigator Account Search for ${candidate.companyName} returned ${response.status}.`);
|
|
13604
|
+
}
|
|
13605
|
+
if (response.status >= 200 && response.status < 300 && response.body) {
|
|
13606
|
+
const accounts = extractLocalSalesNavigatorElements(response.body)
|
|
13607
|
+
.map((element) => typeof element === "object" && element !== null && !Array.isArray(element)
|
|
13608
|
+
? normalizeLocalSalesNavigatorAccount(element, queryUrl)
|
|
13609
|
+
: null)
|
|
13610
|
+
.filter((value) => value !== null);
|
|
13611
|
+
account = exactCliImportAccountMatch(candidate, accounts);
|
|
13612
|
+
}
|
|
13613
|
+
salesNavCompanyUrl =
|
|
13614
|
+
(typeof account?.salesNavCompanyUrl === "string"
|
|
13615
|
+
? account.salesNavCompanyUrl
|
|
13616
|
+
: null) ?? salesNavCompanyUrl;
|
|
13617
|
+
linkedinCompanyUrl =
|
|
13618
|
+
(typeof account?.linkedinCompanyUrl === "string"
|
|
13619
|
+
? account.linkedinCompanyUrl
|
|
13620
|
+
: null) ?? linkedinCompanyUrl;
|
|
13621
|
+
matchedCompanyName =
|
|
13622
|
+
typeof account?.companyName === "string" ? account.companyName : null;
|
|
13623
|
+
resolvedCompanyId =
|
|
13624
|
+
(typeof account?.companyId === "string" && /^\d+$/.test(account.companyId)
|
|
13625
|
+
? account.companyId
|
|
13626
|
+
: null) ??
|
|
13627
|
+
extractLinkedInCompanyIdFromUrl(salesNavCompanyUrl) ??
|
|
13628
|
+
extractLinkedInCompanyIdFromUrl(linkedinCompanyUrl) ??
|
|
13629
|
+
null;
|
|
13630
|
+
}
|
|
13631
|
+
if (!linkedinCompanyUrl && resolvedCompanyId) {
|
|
13632
|
+
linkedinCompanyUrl = normalizeLinkedInCompanyPage(resolvedCompanyId);
|
|
13633
|
+
}
|
|
13634
|
+
if (!salesNavCompanyUrl && resolvedCompanyId) {
|
|
13635
|
+
salesNavCompanyUrl = `https://www.linkedin.com/sales/company/${resolvedCompanyId}`;
|
|
13636
|
+
}
|
|
13637
|
+
return {
|
|
13638
|
+
candidate,
|
|
13639
|
+
resolvedCompanyId,
|
|
13640
|
+
salesNavCompanyUrl,
|
|
13641
|
+
linkedinCompanyUrl,
|
|
13642
|
+
matchedCompanyName,
|
|
13643
|
+
accountSearchUsed,
|
|
13644
|
+
account,
|
|
13645
|
+
};
|
|
13646
|
+
}
|
|
13647
|
+
function cliImportCompanyProfileCommon(resolution) {
|
|
13648
|
+
return {
|
|
13649
|
+
companyKey: resolution.candidate.companyKey,
|
|
13650
|
+
sourceCompanyId: resolution.candidate.companyId,
|
|
13651
|
+
resolvedCompanyId: resolution.resolvedCompanyId,
|
|
13652
|
+
sourceCompanyName: resolution.candidate.companyName,
|
|
13653
|
+
companyName: resolution.matchedCompanyName ?? resolution.candidate.companyName,
|
|
13654
|
+
salesNavCompanyUrl: resolution.salesNavCompanyUrl,
|
|
13655
|
+
linkedinCompanyUrl: resolution.linkedinCompanyUrl,
|
|
13656
|
+
enrichmentSource: resolution.accountSearchUsed
|
|
13657
|
+
? "sales_api_account_search+sales_api_company_detail"
|
|
13658
|
+
: "sales_api_company_detail",
|
|
13659
|
+
};
|
|
13660
|
+
}
|
|
13661
|
+
function formatCliImportCompanyLocation(value) {
|
|
13662
|
+
if (typeof value === "string")
|
|
13663
|
+
return normalizeLookupWhitespace(value) || null;
|
|
13664
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
13665
|
+
return null;
|
|
13666
|
+
}
|
|
13667
|
+
const record = value;
|
|
13668
|
+
const orderedKeys = [
|
|
13669
|
+
"line1",
|
|
13670
|
+
"line2",
|
|
13671
|
+
"city",
|
|
13672
|
+
"geographicArea",
|
|
13673
|
+
"postalCode",
|
|
13674
|
+
"country",
|
|
13675
|
+
];
|
|
13676
|
+
return (orderedKeys
|
|
13677
|
+
.map((key) => typeof record[key] === "string"
|
|
13678
|
+
? normalizeLookupWhitespace(record[key])
|
|
13679
|
+
: "")
|
|
13680
|
+
.filter(Boolean)
|
|
13681
|
+
.join(", ") || null);
|
|
13682
|
+
}
|
|
13683
|
+
function normalizeCliImportCompanyDomain(website) {
|
|
13684
|
+
if (!website)
|
|
13685
|
+
return null;
|
|
13686
|
+
try {
|
|
13687
|
+
const hostname = new URL(website).hostname.replace(/^www\./i, "").toLowerCase();
|
|
13688
|
+
return hostname && !/(^|\.)linkedin\.com$/i.test(hostname) ? hostname : null;
|
|
13689
|
+
}
|
|
13690
|
+
catch {
|
|
13691
|
+
return null;
|
|
13692
|
+
}
|
|
13693
|
+
}
|
|
13694
|
+
function compactCliImportCompanyString(value) {
|
|
13695
|
+
return typeof value === "string" ? normalizeLookupWhitespace(value) || null : null;
|
|
13696
|
+
}
|
|
13697
|
+
function cliImportCompanyRecord(value) {
|
|
13698
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
13699
|
+
? value
|
|
13700
|
+
: null;
|
|
13701
|
+
}
|
|
13702
|
+
function buildCliImportCompanyProfile(resolution, detail, input) {
|
|
13703
|
+
const common = cliImportCompanyProfileCommon(resolution);
|
|
13704
|
+
if (!detail) {
|
|
13705
|
+
return {
|
|
13706
|
+
...common,
|
|
13707
|
+
status: input.status === 404 || input.status === 410 ? "not_found" : "failed",
|
|
13708
|
+
error: input.error ??
|
|
13709
|
+
`Sales Navigator company detail returned ${input.status || "no result"}`,
|
|
13710
|
+
rawPayload: {
|
|
13711
|
+
accountSearchUsed: resolution.accountSearchUsed,
|
|
13712
|
+
httpStatus: input.status,
|
|
13713
|
+
},
|
|
13714
|
+
};
|
|
13715
|
+
}
|
|
13716
|
+
const accountRaw = cliImportCompanyRecord(resolution.account?.rawLocalSalesNavigatorAccountResult);
|
|
13717
|
+
const accountWebsite = compactCliImportCompanyString(resolution.account?.website);
|
|
13718
|
+
const website = normalizeCompanyWebsite(compactCliImportCompanyString(detail.website) ?? accountWebsite ?? undefined);
|
|
13719
|
+
const specialties = Array.isArray(detail.specialties)
|
|
13720
|
+
? detail.specialties
|
|
13721
|
+
.filter((value) => typeof value === "string")
|
|
13722
|
+
.map((value) => normalizeLookupWhitespace(value))
|
|
13723
|
+
.filter(Boolean)
|
|
13724
|
+
.join(", ") || null
|
|
13725
|
+
: compactCliImportCompanyString(detail.specialties);
|
|
13726
|
+
const foundedCandidate = detail.yearFounded ?? detail.foundedYear;
|
|
13727
|
+
const founded = typeof foundedCandidate === "number" && Number.isInteger(foundedCandidate)
|
|
13728
|
+
? foundedCandidate
|
|
13729
|
+
: typeof foundedCandidate === "string" && /^\d{4}$/.test(foundedCandidate)
|
|
13730
|
+
? Number(foundedCandidate)
|
|
13731
|
+
: null;
|
|
13732
|
+
const revenueRange = cliImportCompanyRecord(detail.revenueRange);
|
|
13733
|
+
const employeeGrowthPercentages = Array.isArray(detail.employeeGrowthPercentages)
|
|
13734
|
+
? detail.employeeGrowthPercentages.filter((value) => typeof value === "object" && value !== null && !Array.isArray(value))
|
|
13735
|
+
: [];
|
|
13736
|
+
const flagshipCompanyUrl = normalizeCompanyWebsite(compactCliImportCompanyString(detail.flagshipCompanyUrl) ?? undefined);
|
|
13737
|
+
const linkedinCompanyUrl = flagshipCompanyUrl && /(^|\.)linkedin\.com$/i.test(new URL(flagshipCompanyUrl).hostname)
|
|
13738
|
+
? flagshipCompanyUrl
|
|
13739
|
+
: resolution.linkedinCompanyUrl;
|
|
13740
|
+
const accountEmployeeCount = typeof resolution.account?.employeeCount === "number"
|
|
13741
|
+
? resolution.account.employeeCount
|
|
13742
|
+
: typeof detail.employeeDisplayCount === "number"
|
|
13743
|
+
? Math.max(0, Math.trunc(detail.employeeDisplayCount))
|
|
13744
|
+
: typeof detail.employeeCount === "number"
|
|
13745
|
+
? Math.max(0, Math.trunc(detail.employeeCount))
|
|
13746
|
+
: null;
|
|
13747
|
+
const followerCount = typeof resolution.account?.followerCount === "number"
|
|
13748
|
+
? resolution.account.followerCount
|
|
13749
|
+
: null;
|
|
13750
|
+
return {
|
|
13751
|
+
...common,
|
|
13752
|
+
companyName: compactCliImportCompanyString(detail.name) ?? common.companyName,
|
|
13753
|
+
linkedinCompanyUrl,
|
|
13754
|
+
website,
|
|
13755
|
+
domain: normalizeCliImportCompanyDomain(website),
|
|
13756
|
+
industry: compactCliImportCompanyString(detail.industry) ??
|
|
13757
|
+
compactCliImportCompanyString(resolution.account?.industry),
|
|
13758
|
+
companySize: extractFirstNestedStringByKeys(accountRaw, [
|
|
13759
|
+
"employeeCountRange",
|
|
13760
|
+
"companySize",
|
|
13761
|
+
]) ?? compactCliImportCompanyString(detail.employeeCountRange),
|
|
13762
|
+
companyType: compactCliImportCompanyString(detail.type),
|
|
13763
|
+
location: formatCliImportCompanyLocation(detail.location) ??
|
|
13764
|
+
compactCliImportCompanyString(resolution.account?.location),
|
|
13765
|
+
headquarters: formatCliImportCompanyLocation(detail.headquarters) ??
|
|
13766
|
+
compactCliImportCompanyString(resolution.account?.headquarters),
|
|
13767
|
+
employeesOnLinkedIn: accountEmployeeCount,
|
|
13768
|
+
followerCount,
|
|
13769
|
+
description: compactCliImportCompanyString(detail.description) ??
|
|
13770
|
+
compactCliImportCompanyString(resolution.account?.description),
|
|
13771
|
+
tagline: null,
|
|
13772
|
+
specialties,
|
|
13773
|
+
founded,
|
|
13774
|
+
revenueRange,
|
|
13775
|
+
employeeGrowthPercentages,
|
|
13776
|
+
logoUrl: buildLinkedInImageUrl(detail.companyPictureDisplayImage) ??
|
|
13777
|
+
buildLinkedInImageUrl(detail.pictureInfo),
|
|
13778
|
+
bannerUrl: buildLinkedInImageUrl(detail.companyBackgroundCoverImage),
|
|
13779
|
+
flagshipCompanyUrl,
|
|
13780
|
+
status: "enriched",
|
|
13781
|
+
error: null,
|
|
13782
|
+
rawPayload: {
|
|
13783
|
+
accountSearchUsed: resolution.accountSearchUsed,
|
|
13784
|
+
httpStatus: input.status,
|
|
13785
|
+
detail,
|
|
13786
|
+
accountSearchResult: accountRaw,
|
|
13787
|
+
},
|
|
13788
|
+
};
|
|
13789
|
+
}
|
|
13790
|
+
async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
13791
|
+
if (options.companiesOnly && options.hunterOnly) {
|
|
13792
|
+
throw new Error("Use either --companies-only or --hunter-only, not both.");
|
|
13793
|
+
}
|
|
13794
|
+
const runIds = Array.from(new Set(options.runId.map((runId) => z.string().uuid().parse(runId))));
|
|
13795
|
+
const companyTimeoutMs = z.coerce.number().int().min(1000).max(120000).parse(options.companyTimeoutMs);
|
|
13796
|
+
const companyDelayMinMs = z.coerce.number().int().min(0).parse(options.companyDelayMinMs);
|
|
13797
|
+
const companyDelayMaxMs = z.coerce.number().int().min(companyDelayMinMs).parse(options.companyDelayMaxMs);
|
|
13798
|
+
const browserRelayPort = options.browserRelayPort == null
|
|
13799
|
+
? null
|
|
13800
|
+
: z.coerce.number().int().min(0).max(65535).parse(options.browserRelayPort);
|
|
13801
|
+
const maxCompanies = z.coerce.number().int().min(0).max(1000).parse(options.maxCompanies);
|
|
13802
|
+
const companySaveBatchSize = z.coerce.number().int().min(1).max(200).parse(options.companySaveBatchSize);
|
|
13803
|
+
const hunterBatchSize = z.coerce.number().int().min(1).max(150).parse(options.hunterBatchSize);
|
|
13804
|
+
const maxHunterBatches = z.coerce.number().int().min(0).max(100000).parse(options.maxHunterBatches);
|
|
13805
|
+
const session = await requireAuthSession();
|
|
13806
|
+
const startedAt = new Date().toISOString();
|
|
13807
|
+
const initialStatus = await getCliImportEnrichmentStatusViaApp(session, runIds);
|
|
13808
|
+
if (options.dryRun) {
|
|
13809
|
+
const companyCandidates = options.hunterOnly
|
|
13810
|
+
? []
|
|
13811
|
+
: (await getCliImportCompanyCandidatesViaApp(session, {
|
|
13812
|
+
runIds,
|
|
13813
|
+
limit: 1000,
|
|
13814
|
+
retry: options.retryCompanies
|
|
13815
|
+
})).companies.slice(0, maxCompanies || undefined);
|
|
13816
|
+
return {
|
|
13817
|
+
status: "ok",
|
|
13818
|
+
dryRun: true,
|
|
13819
|
+
runIds,
|
|
13820
|
+
initial: initialStatus,
|
|
13821
|
+
companyCandidates: companyCandidates.length,
|
|
13822
|
+
next: options.companiesOnly
|
|
13823
|
+
? "company enrichment only"
|
|
13824
|
+
: options.hunterOnly
|
|
13825
|
+
? "Hunter enrichment only"
|
|
13826
|
+
: "company profiles, verified domains, then Hunter"
|
|
13827
|
+
};
|
|
13828
|
+
}
|
|
13829
|
+
let companies = {
|
|
13830
|
+
selected: 0,
|
|
13831
|
+
saved: 0,
|
|
13832
|
+
enriched: 0,
|
|
13833
|
+
domains: 0,
|
|
13834
|
+
notFound: 0,
|
|
13835
|
+
failed: 0
|
|
13836
|
+
};
|
|
13837
|
+
if (!options.hunterOnly) {
|
|
13838
|
+
const companyCandidates = (await getCliImportCompanyCandidatesViaApp(session, {
|
|
13839
|
+
runIds,
|
|
13840
|
+
limit: 1000,
|
|
13841
|
+
retry: options.retryCompanies
|
|
13842
|
+
})).companies.slice(0, maxCompanies || undefined);
|
|
13843
|
+
companies.selected = companyCandidates.length;
|
|
13844
|
+
if (companyCandidates.length > 0) {
|
|
13845
|
+
const browserRelay = browserRelayPort == null
|
|
13846
|
+
? null
|
|
13847
|
+
: await createLocalAccountSearchBrowserRelay(browserRelayPort);
|
|
13848
|
+
try {
|
|
13849
|
+
const config = browserRelay ? null : await readLinkedInDirectLookupConfig();
|
|
13850
|
+
let pendingProfiles = [];
|
|
13851
|
+
const flushCompanyProfiles = async () => {
|
|
13852
|
+
while (pendingProfiles.length > 0) {
|
|
13853
|
+
const batch = pendingProfiles.splice(0, companySaveBatchSize);
|
|
13854
|
+
const saved = await saveCliImportCompanyProfilesViaApp(session, batch);
|
|
13855
|
+
companies = {
|
|
13856
|
+
...companies,
|
|
13857
|
+
saved: companies.saved + saved.saved,
|
|
13858
|
+
enriched: companies.enriched + saved.enriched,
|
|
13859
|
+
domains: companies.domains + saved.domains,
|
|
13860
|
+
notFound: companies.notFound + saved.notFound,
|
|
13861
|
+
failed: companies.failed + saved.failed
|
|
13862
|
+
};
|
|
13863
|
+
writeProgress(`Company enrichment: ${companies.saved}/${companyCandidates.length} saved, ${companies.domains} domains.`);
|
|
13864
|
+
}
|
|
13865
|
+
};
|
|
13866
|
+
const detailCache = new Map();
|
|
13867
|
+
const enrichResolvedCompanies = async (resolutions) => {
|
|
13868
|
+
const resolutionBatchSize = 20;
|
|
13869
|
+
for (let offset = 0; offset < resolutions.length; offset += resolutionBatchSize) {
|
|
13870
|
+
const resolutionBatch = resolutions.slice(offset, offset + resolutionBatchSize);
|
|
13871
|
+
const ids = Array.from(new Set(resolutionBatch
|
|
13872
|
+
.map((resolution) => resolution.resolvedCompanyId)
|
|
13873
|
+
.filter((value) => Boolean(value)))).filter((companyId) => !detailCache.has(companyId));
|
|
13874
|
+
if (ids.length > 0) {
|
|
13875
|
+
const response = await fetchCliImportSalesNavigatorJson({
|
|
13876
|
+
url: buildCliImportCompanyDetailsUrl(ids),
|
|
13877
|
+
config,
|
|
13878
|
+
browserRelay,
|
|
13879
|
+
timeoutMs: companyTimeoutMs,
|
|
13880
|
+
label: `Sales Navigator company details ${companies.saved + 1}-${companies.saved + resolutionBatch.length}`,
|
|
13881
|
+
companyId: ids[0] ?? null,
|
|
13882
|
+
});
|
|
13883
|
+
const results = cliImportCompanyRecord(response.body?.results) ?? {};
|
|
13884
|
+
const errors = cliImportCompanyRecord(response.body?.errors) ?? {};
|
|
13885
|
+
const statuses = cliImportCompanyRecord(response.body?.statuses) ?? {};
|
|
13886
|
+
for (const companyId of ids) {
|
|
13887
|
+
const detail = cliImportCompanyRecord(results[companyId]);
|
|
13888
|
+
const statusValue = statuses[companyId];
|
|
13889
|
+
const status = typeof statusValue === "number"
|
|
13890
|
+
? statusValue
|
|
13891
|
+
: typeof statusValue === "string" && /^\d+$/.test(statusValue)
|
|
13892
|
+
? Number(statusValue)
|
|
13893
|
+
: response.status;
|
|
13894
|
+
const errorValue = errors[companyId];
|
|
13895
|
+
detailCache.set(companyId, {
|
|
13896
|
+
detail,
|
|
13897
|
+
status,
|
|
13898
|
+
error: errorValue
|
|
13899
|
+
? JSON.stringify(errorValue).slice(0, 3000)
|
|
13900
|
+
: response.status >= 200 && response.status < 300
|
|
13901
|
+
? null
|
|
13902
|
+
: `Sales Navigator company details returned ${response.status}`,
|
|
13903
|
+
});
|
|
13904
|
+
}
|
|
13905
|
+
}
|
|
13906
|
+
for (const resolution of resolutionBatch) {
|
|
13907
|
+
const cached = detailCache.get(resolution.resolvedCompanyId ?? "");
|
|
13908
|
+
pendingProfiles.push(buildCliImportCompanyProfile(resolution, cached?.detail ?? null, {
|
|
13909
|
+
status: cached?.status ?? 500,
|
|
13910
|
+
error: cached?.error ?? "Sales Navigator returned no company detail",
|
|
13911
|
+
}));
|
|
13912
|
+
}
|
|
13913
|
+
await flushCompanyProfiles();
|
|
13914
|
+
if (offset + resolutionBatchSize < resolutions.length &&
|
|
13915
|
+
companyDelayMaxMs > 0) {
|
|
13916
|
+
await delay(randomIntegerBetween(companyDelayMinMs, companyDelayMaxMs));
|
|
13917
|
+
}
|
|
13918
|
+
}
|
|
13919
|
+
};
|
|
13920
|
+
const knownCandidates = companyCandidates.filter((candidate) => candidate.companyId && /^\d+$/.test(candidate.companyId));
|
|
13921
|
+
const knownResolutions = await Promise.all(knownCandidates.map((candidate) => resolveCliImportCompanyIdentity(candidate, config, companyTimeoutMs, browserRelay)));
|
|
13922
|
+
await enrichResolvedCompanies(knownResolutions);
|
|
13923
|
+
const knownByAlias = new Map();
|
|
13924
|
+
for (const candidate of knownCandidates) {
|
|
13925
|
+
for (const alias of [
|
|
13926
|
+
candidate.companyName ?? "",
|
|
13927
|
+
aggressivelyCleanLookupCompanyName(candidate.companyName ?? ""),
|
|
13928
|
+
]) {
|
|
13929
|
+
const key = normalizeLooseMatchText(alias);
|
|
13930
|
+
if (!key)
|
|
13931
|
+
continue;
|
|
13932
|
+
const entries = knownByAlias.get(key) ?? [];
|
|
13933
|
+
entries.push(candidate);
|
|
13934
|
+
knownByAlias.set(key, entries);
|
|
13935
|
+
}
|
|
13936
|
+
}
|
|
13937
|
+
const unresolvedCandidates = [];
|
|
13938
|
+
const localAliasResolutions = [];
|
|
13939
|
+
for (const candidate of companyCandidates) {
|
|
13940
|
+
if (candidate.companyId && /^\d+$/.test(candidate.companyId))
|
|
13941
|
+
continue;
|
|
13942
|
+
const matches = new Map();
|
|
13943
|
+
for (const alias of [
|
|
13944
|
+
candidate.companyName ?? "",
|
|
13945
|
+
aggressivelyCleanLookupCompanyName(candidate.companyName ?? ""),
|
|
13946
|
+
]) {
|
|
13947
|
+
const key = normalizeLooseMatchText(alias);
|
|
13948
|
+
for (const match of knownByAlias.get(key) ?? []) {
|
|
13949
|
+
if (match.companyId)
|
|
13950
|
+
matches.set(match.companyId, match);
|
|
13951
|
+
}
|
|
13952
|
+
}
|
|
13953
|
+
if (matches.size === 1) {
|
|
13954
|
+
const match = Array.from(matches.values())[0];
|
|
13955
|
+
localAliasResolutions.push({
|
|
13956
|
+
candidate,
|
|
13957
|
+
resolvedCompanyId: match.companyId,
|
|
13958
|
+
salesNavCompanyUrl: match.salesNavCompanyUrl ??
|
|
13959
|
+
`https://www.linkedin.com/sales/company/${match.companyId}`,
|
|
13960
|
+
linkedinCompanyUrl: match.linkedinCompanyUrl ??
|
|
13961
|
+
normalizeLinkedInCompanyPage(match.companyId),
|
|
13962
|
+
matchedCompanyName: match.companyName,
|
|
13963
|
+
accountSearchUsed: false,
|
|
13964
|
+
account: null,
|
|
13965
|
+
});
|
|
13966
|
+
}
|
|
13967
|
+
else {
|
|
13968
|
+
unresolvedCandidates.push(candidate);
|
|
13969
|
+
}
|
|
13970
|
+
}
|
|
13971
|
+
await enrichResolvedCompanies(localAliasResolutions);
|
|
13972
|
+
writeProgress(`Company identities: ${knownResolutions.length} imported IDs, ${localAliasResolutions.length} local aliases, ${unresolvedCandidates.length} Account Search candidates.`);
|
|
13973
|
+
const accountSearchGroupSize = 5;
|
|
13974
|
+
let accountSearches = 0;
|
|
13975
|
+
for (let offset = 0; offset < unresolvedCandidates.length; offset += accountSearchGroupSize) {
|
|
13976
|
+
const group = unresolvedCandidates.slice(offset, offset + accountSearchGroupSize);
|
|
13977
|
+
const resolvedGroup = [];
|
|
13978
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
13979
|
+
const candidate = group[index];
|
|
13980
|
+
let resolution = null;
|
|
13981
|
+
try {
|
|
13982
|
+
resolution = await resolveCliImportCompanyIdentity(candidate, config, companyTimeoutMs, browserRelay);
|
|
13983
|
+
}
|
|
13984
|
+
catch (error) {
|
|
13985
|
+
if (error instanceof CliImportCompanyRateLimitError ||
|
|
13986
|
+
error instanceof CliImportCompanySessionError) {
|
|
13987
|
+
throw error;
|
|
13988
|
+
}
|
|
13989
|
+
pendingProfiles.push(buildCliImportCompanyProfile({
|
|
13990
|
+
candidate,
|
|
13991
|
+
resolvedCompanyId: null,
|
|
13992
|
+
salesNavCompanyUrl: candidate.salesNavCompanyUrl,
|
|
13993
|
+
linkedinCompanyUrl: candidate.linkedinCompanyUrl,
|
|
13994
|
+
matchedCompanyName: null,
|
|
13995
|
+
accountSearchUsed: true,
|
|
13996
|
+
account: null,
|
|
13997
|
+
}, null, {
|
|
13998
|
+
status: 500,
|
|
13999
|
+
error: error instanceof Error
|
|
14000
|
+
? error.message
|
|
14001
|
+
: String(error),
|
|
14002
|
+
}));
|
|
14003
|
+
}
|
|
14004
|
+
accountSearches += 1;
|
|
14005
|
+
if (resolution?.resolvedCompanyId) {
|
|
14006
|
+
resolvedGroup.push(resolution);
|
|
14007
|
+
}
|
|
14008
|
+
else if (resolution) {
|
|
14009
|
+
pendingProfiles.push(buildCliImportCompanyProfile(resolution, null, {
|
|
14010
|
+
status: 404,
|
|
14011
|
+
error: "No exact LinkedIn company was found by Account Search",
|
|
14012
|
+
}));
|
|
14013
|
+
}
|
|
14014
|
+
if (index < group.length - 1 &&
|
|
14015
|
+
companyDelayMaxMs > 0) {
|
|
14016
|
+
await delay(randomIntegerBetween(companyDelayMinMs, companyDelayMaxMs));
|
|
14017
|
+
}
|
|
14018
|
+
}
|
|
14019
|
+
await flushCompanyProfiles();
|
|
14020
|
+
await enrichResolvedCompanies(resolvedGroup);
|
|
14021
|
+
writeProgress(`Account Search: ${Math.min(offset + group.length, unresolvedCandidates.length)}/${unresolvedCandidates.length} checked (${accountSearches} requests).`);
|
|
14022
|
+
}
|
|
14023
|
+
}
|
|
14024
|
+
finally {
|
|
14025
|
+
await browserRelay?.close();
|
|
14026
|
+
}
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
14029
|
+
let hunter = {
|
|
14030
|
+
batches: 0,
|
|
14031
|
+
selected: 0,
|
|
14032
|
+
processed: 0,
|
|
14033
|
+
found: 0,
|
|
14034
|
+
notFound: 0,
|
|
14035
|
+
failed: 0
|
|
14036
|
+
};
|
|
14037
|
+
if (!options.companiesOnly) {
|
|
14038
|
+
const retryBefore = options.retryEmails ? startedAt : null;
|
|
14039
|
+
while (maxHunterBatches === 0 || hunter.batches < maxHunterBatches) {
|
|
14040
|
+
const batch = await runCliImportHunterBatchViaApp(session, {
|
|
14041
|
+
runIds,
|
|
14042
|
+
limit: hunterBatchSize,
|
|
14043
|
+
retryBefore
|
|
14044
|
+
});
|
|
14045
|
+
if (batch.selected === 0)
|
|
14046
|
+
break;
|
|
14047
|
+
hunter = {
|
|
14048
|
+
batches: hunter.batches + 1,
|
|
14049
|
+
selected: hunter.selected + batch.selected,
|
|
14050
|
+
processed: hunter.processed + batch.processed,
|
|
14051
|
+
found: hunter.found + batch.found,
|
|
14052
|
+
notFound: hunter.notFound + batch.notFound,
|
|
14053
|
+
failed: hunter.failed + batch.failed
|
|
14054
|
+
};
|
|
14055
|
+
writeProgress(`Hunter enrichment: ${hunter.processed} checked, ${hunter.found} emails found.`);
|
|
14056
|
+
if (batch.halted) {
|
|
14057
|
+
throw new Error(`Hunter enrichment stopped: ${batch.error ?? "provider unavailable"}. Saved progress will be reused.`);
|
|
14058
|
+
}
|
|
14059
|
+
}
|
|
14060
|
+
}
|
|
14061
|
+
const finalStatus = await getCliImportEnrichmentStatusViaApp(session, runIds);
|
|
14062
|
+
const payload = {
|
|
14063
|
+
status: "ok",
|
|
14064
|
+
dryRun: false,
|
|
14065
|
+
runIds,
|
|
14066
|
+
companies,
|
|
14067
|
+
hunter,
|
|
14068
|
+
before: initialStatus,
|
|
14069
|
+
after: finalStatus,
|
|
14070
|
+
outreachStarted: false,
|
|
14071
|
+
workspaceUrl: `${session.apiBaseUrl}/leads/cli-imports?runId=${encodeURIComponent(runIds[0])}`
|
|
14072
|
+
};
|
|
14073
|
+
if (options.out)
|
|
14074
|
+
await writeJsonFile(options.out, payload);
|
|
14075
|
+
return payload;
|
|
14076
|
+
}
|
|
13317
14077
|
async function runAffiliateLaunchCommand(options) {
|
|
13318
14078
|
const affiliateLink = z.string().url().parse(options.affiliateLink);
|
|
13319
14079
|
const linkedInUrl = z.string().url().parse(options.linkedinUrl);
|
|
@@ -13359,7 +14119,7 @@ async function runAffiliateLaunchCommand(options) {
|
|
|
13359
14119
|
retryMaxDelayMs: 10_000,
|
|
13360
14120
|
},
|
|
13361
14121
|
executeRequest: browserRelay
|
|
13362
|
-
? (request) => browserRelay.request(request
|
|
14122
|
+
? (request) => browserRelay.request(request)
|
|
13363
14123
|
: undefined,
|
|
13364
14124
|
});
|
|
13365
14125
|
}
|
|
@@ -13415,6 +14175,28 @@ program
|
|
|
13415
14175
|
.action(async (options) => {
|
|
13416
14176
|
printOutput(await runSalesNavigatorPeopleCollectCommand(options));
|
|
13417
14177
|
});
|
|
14178
|
+
program
|
|
14179
|
+
.command("salesnav:people:enrich")
|
|
14180
|
+
.alias("leads:enrich-import")
|
|
14181
|
+
.description("Enrich stored CLI imports with company profiles, verified domains, and Hunter emails.")
|
|
14182
|
+
.requiredOption("--run-id <uuid>", "Workspace import run id; repeat the option or pass comma-separated ids", collectRunId, [])
|
|
14183
|
+
.option("--browser-relay-port <number>", "Use the signed-in browser for Account Search and company-detail requests")
|
|
14184
|
+
.option("--company-timeout-ms <number>", "Timeout for each company lookup", "20000")
|
|
14185
|
+
.option("--company-delay-min-ms <number>", "Minimum delay between LinkedIn company lookups", "1500")
|
|
14186
|
+
.option("--company-delay-max-ms <number>", "Maximum delay between LinkedIn company lookups", "3000")
|
|
14187
|
+
.option("--max-companies <number>", "Maximum companies for this invocation; 0 processes all eligible companies", "0")
|
|
14188
|
+
.option("--company-save-batch-size <number>", "Company rows saved per request", "50")
|
|
14189
|
+
.option("--hunter-batch-size <number>", "Hunter contacts checked per request", "100")
|
|
14190
|
+
.option("--max-hunter-batches <number>", "Maximum Hunter batches; 0 continues until no eligible people remain", "0")
|
|
14191
|
+
.option("--companies-only", "Stop after company profiles and domains", false)
|
|
14192
|
+
.option("--hunter-only", "Skip company lookup and use already stored domains", false)
|
|
14193
|
+
.option("--retry-companies", "Retry previously saved company failures", false)
|
|
14194
|
+
.option("--retry-emails", "Retry previously checked unresolved emails once", false)
|
|
14195
|
+
.option("--dry-run", "Show scope without LinkedIn or Hunter requests", false)
|
|
14196
|
+
.option("--out <path>", "Optional JSON result path")
|
|
14197
|
+
.action(async (options) => {
|
|
14198
|
+
printOutput(await runSalesNavigatorImportEnrichmentCommand(options));
|
|
14199
|
+
});
|
|
13418
14200
|
addAffiliateAudienceOptions(program
|
|
13419
14201
|
.command("affiliate:launch")
|
|
13420
14202
|
.description("Build an affiliate audience from an affiliate link and LinkedIn search URL."))
|
|
@@ -14464,7 +15246,7 @@ program
|
|
|
14464
15246
|
};
|
|
14465
15247
|
};
|
|
14466
15248
|
const executeRequest = (request) => browserRelay
|
|
14467
|
-
? browserRelay.request(request
|
|
15249
|
+
? browserRelay.request(request)
|
|
14468
15250
|
: fetchLocalSalesNavigatorRequest(request, retry);
|
|
14469
15251
|
try {
|
|
14470
15252
|
let requestsThisRun = 0;
|
|
@@ -135,8 +135,20 @@ export function parseLinkedInCompanyPage(html, requestUrl) {
|
|
|
135
135
|
normalizeWhitespace($("h1").first().text()) ||
|
|
136
136
|
normalizeWhitespace($('meta[property="og:title"]').attr("content")) ||
|
|
137
137
|
undefined;
|
|
138
|
-
const
|
|
139
|
-
|
|
138
|
+
const jsonLdUrl = normalizeWhitespace(String(jsonLd?.url ?? ""));
|
|
139
|
+
const jsonLdWebsite = (() => {
|
|
140
|
+
if (!jsonLdUrl)
|
|
141
|
+
return undefined;
|
|
142
|
+
try {
|
|
143
|
+
const hostname = new URL(jsonLdUrl).hostname;
|
|
144
|
+
return /(^|\.)linkedin\.com$/i.test(hostname) ? undefined : jsonLdUrl;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
const website = normalizeWhitespace(definitions.get("website")) ||
|
|
151
|
+
jsonLdWebsite ||
|
|
140
152
|
undefined;
|
|
141
153
|
const description = normalizeWhitespace(String(jsonLd?.description ?? "")) ||
|
|
142
154
|
normalizeWhitespace($('meta[name="description"]').attr("content")) ||
|
package/package.json
CHANGED