salesprompter-cli 0.1.39 → 0.1.40
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 +4 -0
- package/dist/cli.js +778 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,9 @@ salesprompter contacts:find-linkedin-urls --in ./contacts.tsv --out ./contacts.e
|
|
|
60
60
|
# Resolve LinkedIn company URLs for a pasted company list
|
|
61
61
|
salesprompter companies:resolve-linkedin-urls --in ./companies.txt --out ./companies.enriched.json
|
|
62
62
|
|
|
63
|
+
# Import authorized local Sales Navigator people-search results into Salesprompter
|
|
64
|
+
salesprompter search:local-import --query-url '<sales navigator people search URL>' --number-of-profiles all
|
|
65
|
+
|
|
63
66
|
# Build and start email enrichment from pasted company/contact rows
|
|
64
67
|
# When authenticated, Salesprompter first enriches company domains in the app, then the CLI builds the email batch.
|
|
65
68
|
salesprompter contacts:resolve-emails --in ./contacts.tsv --out-dir ./email-run --dry-run
|
|
@@ -80,6 +83,7 @@ salesprompter --help
|
|
|
80
83
|
|
|
81
84
|
- Use your own authorized data access and workspace credentials.
|
|
82
85
|
- Respect provider terms and customer data boundaries.
|
|
86
|
+
- Local Sales Navigator import requires an authorized Sales Navigator session captured from the user's own browser context.
|
|
83
87
|
- The CLI is designed for interactive users and agent-assisted workflows.
|
|
84
88
|
- A repo-local Chrome extension compatibility copy is available in `chrome-extension/` for local LinkedIn session sync and popup copy/debug flows.
|
|
85
89
|
|
package/dist/cli.js
CHANGED
|
@@ -124,6 +124,56 @@ const LinkedInCompanyBackfillStatusResponseSchema = z.object({
|
|
|
124
124
|
failureCode: z.string().nullable().optional(),
|
|
125
125
|
failureMessage: z.string().nullable().optional()
|
|
126
126
|
});
|
|
127
|
+
const SalesNavigatorLocalImportResponseSchema = z.object({
|
|
128
|
+
status: z.literal("ok"),
|
|
129
|
+
runId: z.string().min(1),
|
|
130
|
+
containerId: z.string().min(1),
|
|
131
|
+
imported: z.number().int().nonnegative(),
|
|
132
|
+
upserted: z.number().int().nonnegative()
|
|
133
|
+
});
|
|
134
|
+
const LeadPoolAutoProcessResponseSchema = z.object({
|
|
135
|
+
success: z.boolean(),
|
|
136
|
+
clientIds: z.array(z.number().int().positive()),
|
|
137
|
+
steps: z.array(z.record(z.string(), z.unknown())),
|
|
138
|
+
startedAt: z.string(),
|
|
139
|
+
finishedAt: z.string()
|
|
140
|
+
});
|
|
141
|
+
const AffiliateCampaignLaunchResponseSchema = z
|
|
142
|
+
.object({
|
|
143
|
+
status: z.string().min(1),
|
|
144
|
+
dryRun: z.boolean(),
|
|
145
|
+
product: z.object({
|
|
146
|
+
domain: z.string().min(1),
|
|
147
|
+
affiliateLink: z.string().url()
|
|
148
|
+
}),
|
|
149
|
+
audience: z.object({
|
|
150
|
+
sourceType: z.string().min(1),
|
|
151
|
+
linkedInUrl: z.string().url(),
|
|
152
|
+
maxResults: z.number().int().positive()
|
|
153
|
+
}),
|
|
154
|
+
extraction: z
|
|
155
|
+
.object({
|
|
156
|
+
provider: z.string().min(1),
|
|
157
|
+
agentId: z.string().min(1).optional(),
|
|
158
|
+
containerId: z.string().min(1).optional(),
|
|
159
|
+
eventTypes: z.array(z.string()).optional(),
|
|
160
|
+
monitorId: z.string().min(1).optional(),
|
|
161
|
+
postsDiscoveredCount: z.number().int().nonnegative().optional(),
|
|
162
|
+
engagementEventsCount: z.number().int().nonnegative().optional(),
|
|
163
|
+
peopleDiscoveredCount: z.number().int().nonnegative().optional(),
|
|
164
|
+
people: z.array(z.unknown()).optional(),
|
|
165
|
+
posts: z.array(z.unknown()).optional(),
|
|
166
|
+
run: z.unknown().nullable().optional(),
|
|
167
|
+
summary: z.unknown().nullable().optional(),
|
|
168
|
+
selectedSessionCookieSha256: z.string().nullable().optional(),
|
|
169
|
+
selectedSessionUserEmail: z.string().nullable().optional(),
|
|
170
|
+
selectedSessionUserHandle: z.string().nullable().optional()
|
|
171
|
+
})
|
|
172
|
+
.optional(),
|
|
173
|
+
previewUrl: z.string().min(1).nullable().optional(),
|
|
174
|
+
next: z.string().optional()
|
|
175
|
+
})
|
|
176
|
+
.passthrough();
|
|
127
177
|
const LeadPoolEmailFinderProcessResponseSchema = z.object({
|
|
128
178
|
success: z.boolean(),
|
|
129
179
|
provider: z.string().optional(),
|
|
@@ -141,6 +191,7 @@ const LeadPoolEmailFinderProcessResponseSchema = z.object({
|
|
|
141
191
|
const PhantombusterContainersSyncResponseSchema = z.object({
|
|
142
192
|
status: z.literal("ok"),
|
|
143
193
|
agentIds: z.array(z.string().min(1)),
|
|
194
|
+
containerIds: z.array(z.string().min(1)).optional(),
|
|
144
195
|
agents: z.array(z.object({
|
|
145
196
|
agentId: z.string().min(1),
|
|
146
197
|
fetched: z.number().int().nonnegative(),
|
|
@@ -340,21 +391,21 @@ const cliPacks = [
|
|
|
340
391
|
slug: "research",
|
|
341
392
|
title: "Research",
|
|
342
393
|
summary: "Scrape markets and enrich companies before outreach.",
|
|
343
|
-
commands: ["market:scrape", "companies:enrich"],
|
|
394
|
+
commands: ["market:scrape", "companies:enrich", "leads:process"],
|
|
344
395
|
installStatus: "included"
|
|
345
396
|
},
|
|
346
397
|
{
|
|
347
398
|
slug: "discovery",
|
|
348
399
|
title: "Discovery",
|
|
349
400
|
summary: "Find leads from product and market inputs.",
|
|
350
|
-
commands: ["leads:discover", "search:run", "search:status", "search:export", "search:count"],
|
|
401
|
+
commands: ["leads:discover", "search:run", "search:status", "search:export", "search:local-import", "search:count"],
|
|
351
402
|
installStatus: "included"
|
|
352
403
|
},
|
|
353
404
|
{
|
|
354
405
|
slug: "outreach",
|
|
355
406
|
title: "Outreach",
|
|
356
407
|
summary: "Prepare and sync qualified leads into downstream systems.",
|
|
357
|
-
commands: ["sync:outreach", "sync:crm"],
|
|
408
|
+
commands: ["affiliate:launch", "sync:outreach", "sync:crm"],
|
|
358
409
|
installStatus: "included"
|
|
359
410
|
}
|
|
360
411
|
];
|
|
@@ -368,6 +419,7 @@ const helpAliasByCommandName = new Map([
|
|
|
368
419
|
["salesnav:crawl", "search:run"],
|
|
369
420
|
["salesnav:crawl:status", "search:status"],
|
|
370
421
|
["salesnav:export", "search:export"],
|
|
422
|
+
["salesnav:local-import", "search:local-import"],
|
|
371
423
|
["salesnav:count", "search:count"]
|
|
372
424
|
]);
|
|
373
425
|
const helpVisibleCommandNames = new Set([
|
|
@@ -391,7 +443,9 @@ const helpVisibleCommandNames = new Set([
|
|
|
391
443
|
"leads:generate",
|
|
392
444
|
"leads:enrich",
|
|
393
445
|
"leads:score",
|
|
446
|
+
"leads:process",
|
|
394
447
|
"leads:pipeline",
|
|
448
|
+
"affiliate:launch",
|
|
395
449
|
"sync:crm",
|
|
396
450
|
"sync:outreach",
|
|
397
451
|
"linkedin-companies:backfill",
|
|
@@ -400,6 +454,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
400
454
|
"salesnav:crawl",
|
|
401
455
|
"salesnav:crawl:status",
|
|
402
456
|
"salesnav:export",
|
|
457
|
+
"salesnav:local-import",
|
|
403
458
|
"salesnav:count"
|
|
404
459
|
]);
|
|
405
460
|
function printOutput(value) {
|
|
@@ -1327,12 +1382,22 @@ async function listChromeExtensionTokenLogCandidates() {
|
|
|
1327
1382
|
if (!file.endsWith(".log")) {
|
|
1328
1383
|
continue;
|
|
1329
1384
|
}
|
|
1330
|
-
|
|
1385
|
+
const filePath = path.join(extensionDir, file);
|
|
1386
|
+
let mtimeMs = 0;
|
|
1387
|
+
try {
|
|
1388
|
+
mtimeMs = (await stat(filePath)).mtimeMs;
|
|
1389
|
+
}
|
|
1390
|
+
catch {
|
|
1391
|
+
mtimeMs = 0;
|
|
1392
|
+
}
|
|
1393
|
+
paths.push({ path: filePath, mtimeMs });
|
|
1331
1394
|
}
|
|
1332
1395
|
}
|
|
1333
1396
|
}
|
|
1334
1397
|
}
|
|
1335
|
-
return paths
|
|
1398
|
+
return paths
|
|
1399
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path))
|
|
1400
|
+
.map((entry) => entry.path);
|
|
1336
1401
|
}
|
|
1337
1402
|
async function readLocalLinkedInExtensionDirectLookupConfig() {
|
|
1338
1403
|
const candidates = await listChromeExtensionTokenLogCandidates();
|
|
@@ -1430,6 +1495,9 @@ async function readLinkedInDirectLookupConfig() {
|
|
|
1430
1495
|
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1431
1496
|
return envConfig;
|
|
1432
1497
|
}
|
|
1498
|
+
if (process.env.SALESPROMPTER_DISABLE_LINKEDIN_DIRECT_LOOKUP_AUTODISCOVERY === "1") {
|
|
1499
|
+
throw new Error("Missing LinkedIn direct lookup session. Set LINKEDIN_CSRF_TOKEN, LINKEDIN_X_LI_IDENTITY, and LINKEDIN_SALES_NAV_COOKIE, or connect the Salesprompter Chrome extension to sync your LinkedIn session to salesprompter.ai.");
|
|
1500
|
+
}
|
|
1433
1501
|
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
1434
1502
|
if (localExtensionConfig) {
|
|
1435
1503
|
cachedLinkedInDirectLookupConfig = localExtensionConfig;
|
|
@@ -5722,26 +5790,558 @@ async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
|
5722
5790
|
}), LinkedInCompanyBackfillStatusResponseSchema);
|
|
5723
5791
|
return value;
|
|
5724
5792
|
}
|
|
5725
|
-
|
|
5726
|
-
|
|
5793
|
+
class LocalSalesNavigatorRateLimitError extends Error {
|
|
5794
|
+
constructor(status, bodyPreview) {
|
|
5795
|
+
super(`Sales Navigator request stopped because LinkedIn returned HTTP ${status}. ` +
|
|
5796
|
+
"Use LinkedIn directly in a supported browser and wait before trying again. " +
|
|
5797
|
+
`Response preview: ${bodyPreview}`);
|
|
5798
|
+
this.name = "LocalSalesNavigatorRateLimitError";
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
function stripShellQuotes(value) {
|
|
5802
|
+
const trimmed = value.trim();
|
|
5803
|
+
if (trimmed.length >= 2) {
|
|
5804
|
+
const quote = trimmed[0];
|
|
5805
|
+
if ((quote === "'" || quote === '"') && trimmed.at(-1) === quote) {
|
|
5806
|
+
return trimmed.slice(1, -1);
|
|
5807
|
+
}
|
|
5808
|
+
}
|
|
5809
|
+
return trimmed;
|
|
5810
|
+
}
|
|
5811
|
+
function parseCurlHeaderValue(value) {
|
|
5812
|
+
const separatorIndex = value.indexOf(":");
|
|
5813
|
+
if (separatorIndex <= 0) {
|
|
5814
|
+
return null;
|
|
5815
|
+
}
|
|
5816
|
+
const name = value.slice(0, separatorIndex).trim().toLowerCase();
|
|
5817
|
+
const headerValue = value.slice(separatorIndex + 1).trim();
|
|
5818
|
+
if (!name || name === "cookie") {
|
|
5819
|
+
return null;
|
|
5820
|
+
}
|
|
5821
|
+
return [name, headerValue];
|
|
5822
|
+
}
|
|
5823
|
+
function parseSalesNavigatorCurlRequest(curlText) {
|
|
5824
|
+
const urlMatch = curlText.match(/\bcurl\s+(?:--location\s+|-L\s+)?(['"])(https?:\/\/[\s\S]*?)\1/);
|
|
5825
|
+
const fallbackUrlMatch = curlText.match(/\bcurl\s+(https?:\/\/\S+)/);
|
|
5826
|
+
const url = stripShellQuotes(urlMatch?.[2] ?? fallbackUrlMatch?.[1] ?? "");
|
|
5827
|
+
if (!url) {
|
|
5828
|
+
throw new Error("Could not find a URL in the curl request.");
|
|
5829
|
+
}
|
|
5830
|
+
const headers = {};
|
|
5831
|
+
for (const match of curlText.matchAll(/(?:-H|--header)\s+(['"])([\s\S]*?)\1/g)) {
|
|
5832
|
+
const parsed = parseCurlHeaderValue(match[2] ?? "");
|
|
5833
|
+
if (parsed) {
|
|
5834
|
+
headers[parsed[0]] = parsed[1];
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
const cookieMatch = curlText.match(/(?:-b|--cookie)\s+(['"])([\s\S]*?)\1/) ??
|
|
5838
|
+
curlText.match(/(?:-b|--cookie)\s+([^\s\\]+)/);
|
|
5839
|
+
const cookie = stripShellQuotes(cookieMatch?.[2] ?? cookieMatch?.[1] ?? "");
|
|
5840
|
+
if (cookie) {
|
|
5841
|
+
headers.cookie = cookie;
|
|
5842
|
+
}
|
|
5843
|
+
return { url, headers };
|
|
5844
|
+
}
|
|
5845
|
+
function extractRawUrlSearchParam(url, key) {
|
|
5846
|
+
const search = url.search.startsWith("?") ? url.search.slice(1) : url.search;
|
|
5847
|
+
for (const part of search.split("&")) {
|
|
5848
|
+
const separatorIndex = part.indexOf("=");
|
|
5849
|
+
const name = separatorIndex >= 0 ? part.slice(0, separatorIndex) : part;
|
|
5850
|
+
if (decodeURIComponent(name.replace(/\+/g, " ")) === key) {
|
|
5851
|
+
const value = separatorIndex >= 0 ? part.slice(separatorIndex + 1) : "";
|
|
5852
|
+
return value.length > 0 ? value : null;
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
return null;
|
|
5856
|
+
}
|
|
5857
|
+
function decodeRawSalesNavigatorQueryParam(rawQuery) {
|
|
5858
|
+
try {
|
|
5859
|
+
return decodeURIComponent(rawQuery.replace(/\+/g, "%20"));
|
|
5860
|
+
}
|
|
5861
|
+
catch {
|
|
5862
|
+
return rawQuery;
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
function normalizeSalesNavigatorApiQueryValue(rawQuery) {
|
|
5866
|
+
const decoded = decodeRawSalesNavigatorQueryParam(rawQuery);
|
|
5867
|
+
const withRecentSearchId = decoded.replace(/recentSearchParam:\((?![^)]*\bid:)([^)]*)\)/, (_match, inner) => `recentSearchParam:(id:${Date.now()},${inner})`);
|
|
5868
|
+
return withRecentSearchId.includes("spellCorrectionEnabled:")
|
|
5869
|
+
? withRecentSearchId
|
|
5870
|
+
: withRecentSearchId.replace(/^\(/, "(spellCorrectionEnabled:true,");
|
|
5871
|
+
}
|
|
5872
|
+
function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
5873
|
+
const source = new URL(searchUrl);
|
|
5874
|
+
if (source.pathname.includes("/sales-api/salesApiLeadSearch")) {
|
|
5875
|
+
return {
|
|
5876
|
+
url: source.toString(),
|
|
5877
|
+
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config)
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
if (!source.pathname.includes("/sales/search/people")) {
|
|
5881
|
+
throw new Error("Automatic Sales Navigator import requires a /sales/search/people URL.");
|
|
5882
|
+
}
|
|
5883
|
+
const rawQuery = extractRawUrlSearchParam(source, "query");
|
|
5884
|
+
if (!rawQuery) {
|
|
5885
|
+
throw new Error("Sales Navigator search URL is missing the query parameter.");
|
|
5886
|
+
}
|
|
5887
|
+
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
5888
|
+
`${source.protocol}//${source.host}`;
|
|
5889
|
+
const rawSessionId = extractRawUrlSearchParam(source, "sessionId") ?? encodeURIComponent(generateLinkedInSessionId());
|
|
5890
|
+
const apiQuery = normalizeSalesNavigatorApiQueryValue(rawQuery);
|
|
5891
|
+
const normalizedCount = Math.max(1, Math.min(100, Math.trunc(count)));
|
|
5892
|
+
const apiUrl = `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiLeadSearch` +
|
|
5893
|
+
`?q=searchQuery&query=${apiQuery}&start=0&count=${normalizedCount}` +
|
|
5894
|
+
`&trackingParam=(sessionId:${rawSessionId})` +
|
|
5895
|
+
"&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14";
|
|
5896
|
+
return {
|
|
5897
|
+
url: apiUrl,
|
|
5898
|
+
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config)
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
function withSalesNavigatorPaging(url, start, count) {
|
|
5902
|
+
const normalizedStart = String(Math.max(0, Math.trunc(start)));
|
|
5903
|
+
const normalizedCount = String(Math.max(1, Math.min(100, Math.trunc(count))));
|
|
5904
|
+
const hashIndex = url.indexOf("#");
|
|
5905
|
+
const withoutHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
5906
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
5907
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
5908
|
+
if (queryIndex < 0) {
|
|
5909
|
+
return `${withoutHash}?start=${normalizedStart}&count=${normalizedCount}${hash}`;
|
|
5910
|
+
}
|
|
5911
|
+
const base = withoutHash.slice(0, queryIndex);
|
|
5912
|
+
const search = withoutHash.slice(queryIndex + 1);
|
|
5913
|
+
const params = search
|
|
5914
|
+
.split("&")
|
|
5915
|
+
.filter((part) => {
|
|
5916
|
+
const key = part.split("=", 1)[0]?.toLowerCase();
|
|
5917
|
+
return key !== "start" && key !== "count";
|
|
5918
|
+
});
|
|
5919
|
+
params.push(`start=${normalizedStart}`, `count=${normalizedCount}`);
|
|
5920
|
+
return `${base}?${params.join("&")}${hash}`;
|
|
5921
|
+
}
|
|
5922
|
+
function buildLinkedInSalesNavigatorLocalImportHeaders(config) {
|
|
5923
|
+
return {
|
|
5924
|
+
accept: "*/*",
|
|
5925
|
+
"csrf-token": config.csrfToken,
|
|
5926
|
+
cookie: config.cookie,
|
|
5927
|
+
"user-agent": config.userAgent,
|
|
5928
|
+
"x-li-identity": config.identity,
|
|
5929
|
+
"x-restli-protocol-version": "2.0.0"
|
|
5930
|
+
};
|
|
5931
|
+
}
|
|
5932
|
+
function getNestedValue(record, pathParts) {
|
|
5933
|
+
let current = record;
|
|
5934
|
+
for (const part of pathParts) {
|
|
5935
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) {
|
|
5936
|
+
return undefined;
|
|
5937
|
+
}
|
|
5938
|
+
current = current[part];
|
|
5939
|
+
}
|
|
5940
|
+
return current;
|
|
5941
|
+
}
|
|
5942
|
+
function firstLocalString(record, paths) {
|
|
5943
|
+
for (const pathName of paths) {
|
|
5944
|
+
const value = getNestedValue(record, pathName.split("."));
|
|
5945
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
5946
|
+
return value.trim();
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
return null;
|
|
5950
|
+
}
|
|
5951
|
+
function flattenSalesNavigatorRecord(value) {
|
|
5952
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5953
|
+
return {};
|
|
5954
|
+
}
|
|
5955
|
+
const record = value;
|
|
5956
|
+
const entity = (record.entityResult ?? record.lead ?? record.profile ?? record.person ?? record);
|
|
5957
|
+
return typeof entity === "object" && entity !== null && !Array.isArray(entity) ? { ...record, ...entity } : record;
|
|
5958
|
+
}
|
|
5959
|
+
function deriveLocalSalesNavigatorProfileUrl(record) {
|
|
5960
|
+
const direct = firstLocalString(record, [
|
|
5961
|
+
"profileUrl",
|
|
5962
|
+
"defaultProfileUrl",
|
|
5963
|
+
"linkedInProfileUrl",
|
|
5964
|
+
"navigationUrl",
|
|
5965
|
+
"url",
|
|
5966
|
+
"profilePicture.profileUrl"
|
|
5967
|
+
]);
|
|
5968
|
+
if (direct) {
|
|
5969
|
+
try {
|
|
5970
|
+
const parsed = new URL(direct, "https://www.linkedin.com");
|
|
5971
|
+
return parsed.toString();
|
|
5972
|
+
}
|
|
5973
|
+
catch {
|
|
5974
|
+
return direct;
|
|
5975
|
+
}
|
|
5976
|
+
}
|
|
5977
|
+
const publicIdentifier = firstLocalString(record, [
|
|
5978
|
+
"publicIdentifier",
|
|
5979
|
+
"public_id",
|
|
5980
|
+
"miniProfile.publicIdentifier",
|
|
5981
|
+
"member.publicIdentifier"
|
|
5982
|
+
]);
|
|
5983
|
+
if (publicIdentifier) {
|
|
5984
|
+
return `https://www.linkedin.com/in/${encodeURIComponent(publicIdentifier)}`;
|
|
5985
|
+
}
|
|
5986
|
+
const urn = firstLocalString(record, ["entityUrn", "profileUrn", "objectUrn", "memberUrn", "urn"]);
|
|
5987
|
+
const urnId = urn?.match(/fs_salesProfile:\(([^,)]+)/i)?.[1] ?? urn?.match(/(?:profile|member|lead):([^,)]+)/i)?.[1];
|
|
5988
|
+
return urnId ? `https://www.linkedin.com/sales/lead/${encodeURIComponent(urnId)}` : null;
|
|
5989
|
+
}
|
|
5990
|
+
function firstLocalArrayRecord(record, key) {
|
|
5991
|
+
const value = record[key];
|
|
5992
|
+
if (!Array.isArray(value)) {
|
|
5993
|
+
return {};
|
|
5994
|
+
}
|
|
5995
|
+
const first = value.find((item) => typeof item === "object" && item !== null && !Array.isArray(item));
|
|
5996
|
+
return first ? first : {};
|
|
5997
|
+
}
|
|
5998
|
+
function firstLocalNestedRecord(record, key) {
|
|
5999
|
+
const value = record[key];
|
|
6000
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
6001
|
+
}
|
|
6002
|
+
function deriveLocalCompanyId(record, currentPosition, company) {
|
|
6003
|
+
const direct = firstLocalString(record, ["companyId", "currentCompanyId", "company.id"]);
|
|
6004
|
+
if (direct) {
|
|
6005
|
+
return direct;
|
|
6006
|
+
}
|
|
6007
|
+
const urn = firstLocalString(currentPosition, ["companyUrn"]) ?? firstLocalString(company, ["entityUrn"]);
|
|
6008
|
+
return urn?.match(/:(\d+)$/)?.[1] ?? null;
|
|
6009
|
+
}
|
|
6010
|
+
function formatSalesNavigatorTenure(value) {
|
|
6011
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6012
|
+
return null;
|
|
6013
|
+
}
|
|
6014
|
+
const record = value;
|
|
6015
|
+
const years = typeof record.numYears === "number" && Number.isFinite(record.numYears) ? `${record.numYears}y` : null;
|
|
6016
|
+
const months = typeof record.numMonths === "number" && Number.isFinite(record.numMonths) ? `${record.numMonths}m` : null;
|
|
6017
|
+
return [years, months].filter(Boolean).join(" ") || null;
|
|
6018
|
+
}
|
|
6019
|
+
function buildLinkedInImageUrl(value) {
|
|
6020
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6021
|
+
return null;
|
|
6022
|
+
}
|
|
6023
|
+
const record = value;
|
|
6024
|
+
const rootUrl = typeof record.rootUrl === "string" ? record.rootUrl : null;
|
|
6025
|
+
if (!rootUrl || !Array.isArray(record.artifacts)) {
|
|
6026
|
+
return null;
|
|
6027
|
+
}
|
|
6028
|
+
const artifact = record.artifacts
|
|
6029
|
+
.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
|
|
6030
|
+
.sort((left, right) => Number(right.width ?? 0) - Number(left.width ?? 0))[0];
|
|
6031
|
+
const pathSegment = typeof artifact?.fileIdentifyingUrlPathSegment === "string" ? artifact.fileIdentifyingUrlPathSegment : null;
|
|
6032
|
+
return pathSegment ? `${rootUrl}${pathSegment}` : null;
|
|
6033
|
+
}
|
|
6034
|
+
function deriveSharedConnectionsCount(record) {
|
|
6035
|
+
const badges = Array.isArray(record.spotlightBadges) ? record.spotlightBadges : [];
|
|
6036
|
+
for (const badge of badges) {
|
|
6037
|
+
if (typeof badge !== "object" || badge === null || Array.isArray(badge)) {
|
|
6038
|
+
continue;
|
|
6039
|
+
}
|
|
6040
|
+
const displayValue = firstLocalString(badge, ["displayValue"]);
|
|
6041
|
+
const match = displayValue?.match(/(\d+)\s+mutual connection/i);
|
|
6042
|
+
if (match) {
|
|
6043
|
+
return Number(match[1]);
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
return null;
|
|
6047
|
+
}
|
|
6048
|
+
function deriveLocalSalesNavigatorFullName(record) {
|
|
6049
|
+
const direct = firstLocalString(record, [
|
|
6050
|
+
"fullName",
|
|
6051
|
+
"name",
|
|
6052
|
+
"title.text",
|
|
6053
|
+
"profileName",
|
|
6054
|
+
"miniProfile.fullName"
|
|
6055
|
+
]);
|
|
6056
|
+
if (direct) {
|
|
6057
|
+
return direct;
|
|
6058
|
+
}
|
|
6059
|
+
const firstName = firstLocalString(record, ["firstName", "miniProfile.firstName"]);
|
|
6060
|
+
const lastName = firstLocalString(record, ["lastName", "miniProfile.lastName"]);
|
|
6061
|
+
return [firstName, lastName].filter(Boolean).join(" ").trim() || null;
|
|
6062
|
+
}
|
|
6063
|
+
function collectPotentialSalesNavigatorRows(value, rows = []) {
|
|
6064
|
+
if (Array.isArray(value)) {
|
|
6065
|
+
for (const item of value) {
|
|
6066
|
+
collectPotentialSalesNavigatorRows(item, rows);
|
|
6067
|
+
}
|
|
6068
|
+
return rows;
|
|
6069
|
+
}
|
|
6070
|
+
if (typeof value !== "object" || value === null) {
|
|
6071
|
+
return rows;
|
|
6072
|
+
}
|
|
6073
|
+
const record = flattenSalesNavigatorRecord(value);
|
|
6074
|
+
if (deriveLocalSalesNavigatorProfileUrl(record) &&
|
|
6075
|
+
(deriveLocalSalesNavigatorFullName(record) ||
|
|
6076
|
+
firstLocalString(record, ["occupation", "title", "headline", "currentPositions.0.title"]))) {
|
|
6077
|
+
rows.push(record);
|
|
6078
|
+
}
|
|
6079
|
+
for (const nested of Object.values(value)) {
|
|
6080
|
+
if (typeof nested === "object" && nested !== null) {
|
|
6081
|
+
collectPotentialSalesNavigatorRows(nested, rows);
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
return rows;
|
|
6085
|
+
}
|
|
6086
|
+
function normalizeLocalSalesNavigatorPeople(responseBody, queryUrl) {
|
|
6087
|
+
const seen = new Set();
|
|
6088
|
+
const rows = [];
|
|
6089
|
+
for (const record of collectPotentialSalesNavigatorRows(responseBody)) {
|
|
6090
|
+
const currentPosition = firstLocalArrayRecord(record, "currentPositions");
|
|
6091
|
+
const company = firstLocalNestedRecord(currentPosition, "companyUrnResolutionResult");
|
|
6092
|
+
const profileUrl = deriveLocalSalesNavigatorProfileUrl(record);
|
|
6093
|
+
if (!profileUrl || seen.has(profileUrl)) {
|
|
6094
|
+
continue;
|
|
6095
|
+
}
|
|
6096
|
+
seen.add(profileUrl);
|
|
6097
|
+
const fullName = deriveLocalSalesNavigatorFullName(record);
|
|
6098
|
+
const companyId = deriveLocalCompanyId(record, currentPosition, company);
|
|
6099
|
+
rows.push({
|
|
6100
|
+
profileUrl,
|
|
6101
|
+
defaultProfileUrl: profileUrl,
|
|
6102
|
+
linkedInProfileUrl: profileUrl.includes("/in/") ? profileUrl : null,
|
|
6103
|
+
fullName,
|
|
6104
|
+
firstName: firstLocalString(record, ["firstName", "miniProfile.firstName"]),
|
|
6105
|
+
lastName: firstLocalString(record, ["lastName", "miniProfile.lastName"]),
|
|
6106
|
+
title: firstLocalString(record, ["occupation", "title", "headline"]) ?? firstLocalString(currentPosition, ["title"]),
|
|
6107
|
+
companyName: firstLocalString(record, [
|
|
6108
|
+
"companyName",
|
|
6109
|
+
"currentCompanyName"
|
|
6110
|
+
]) ?? firstLocalString(currentPosition, ["companyName"]) ?? firstLocalString(company, ["name"]),
|
|
6111
|
+
companyId,
|
|
6112
|
+
companyUrl: firstLocalString(record, ["companyUrl", "company.url"]) ?? (companyId ? `https://www.linkedin.com/sales/company/${companyId}` : null),
|
|
6113
|
+
regularCompanyUrl: companyId ? `https://www.linkedin.com/company/${companyId}` : null,
|
|
6114
|
+
location: firstLocalString(record, ["location", "geoRegion", "locationName"]),
|
|
6115
|
+
industry: firstLocalString(record, ["industry"]) ?? firstLocalString(company, ["industry"]),
|
|
6116
|
+
summary: firstLocalString(record, ["summary", "description"]),
|
|
6117
|
+
titleDescription: firstLocalString(record, ["titleDescription"]) ?? firstLocalString(currentPosition, ["description"]),
|
|
6118
|
+
companyLocation: firstLocalString(record, ["companyLocation"]) ?? firstLocalString(company, ["location"]),
|
|
6119
|
+
durationInRole: firstLocalString(record, ["durationInRole"]) ?? formatSalesNavigatorTenure(currentPosition.tenureAtPosition),
|
|
6120
|
+
durationInCompany: firstLocalString(record, ["durationInCompany"]) ?? formatSalesNavigatorTenure(currentPosition.tenureAtCompany),
|
|
6121
|
+
connectionDegree: firstLocalString(record, ["connectionDegree"]) ?? (record.degree != null ? String(record.degree) : null),
|
|
6122
|
+
sharedConnectionsCount: deriveSharedConnectionsCount(record),
|
|
6123
|
+
profileImageUrl: firstLocalString(record, ["profileImageUrl", "pictureUrl", "profilePicture.displayImage"]) ?? buildLinkedInImageUrl(record.profilePictureDisplayImage),
|
|
6124
|
+
vmid: firstLocalString(record, ["vmid", "memberId", "profileId"]),
|
|
6125
|
+
isPremium: typeof record.premium === "boolean" ? record.premium : null,
|
|
6126
|
+
isOpenLink: typeof record.openLink === "boolean" ? record.openLink : null,
|
|
6127
|
+
query: queryUrl,
|
|
6128
|
+
timestamp: new Date().toISOString(),
|
|
6129
|
+
rawLocalSalesNavigatorResult: record
|
|
6130
|
+
});
|
|
6131
|
+
}
|
|
6132
|
+
return rows;
|
|
6133
|
+
}
|
|
6134
|
+
function extractLocalSalesNavigatorTotalResults(responseBody) {
|
|
6135
|
+
if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
|
|
6136
|
+
return null;
|
|
6137
|
+
}
|
|
6138
|
+
const record = responseBody;
|
|
6139
|
+
const candidates = [record.paging, record.metadata, record.searchResultsMetadata, record];
|
|
6140
|
+
for (const candidate of candidates) {
|
|
6141
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) {
|
|
6142
|
+
continue;
|
|
6143
|
+
}
|
|
6144
|
+
for (const key of ["total", "totalResults", "resultCount", "count"]) {
|
|
6145
|
+
const value = candidate[key];
|
|
6146
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6147
|
+
return Math.trunc(value);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
return null;
|
|
6152
|
+
}
|
|
6153
|
+
function randomIntegerBetween(min, max) {
|
|
6154
|
+
const lower = Math.max(0, Math.trunc(min));
|
|
6155
|
+
const upper = Math.max(lower, Math.trunc(max));
|
|
6156
|
+
return lower + Math.floor(Math.random() * (upper - lower + 1));
|
|
6157
|
+
}
|
|
6158
|
+
function isSalesNavigatorRateLimitStatus(status, text) {
|
|
6159
|
+
return (status === 429 ||
|
|
6160
|
+
status === 999 ||
|
|
6161
|
+
/too many requests|rate limit|unusual activity|automated activity|temporarily restricted/i.test(text));
|
|
6162
|
+
}
|
|
6163
|
+
function isRetryableLocalSalesNavigatorStatus(status) {
|
|
6164
|
+
return status === 408 || status === 425 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
6165
|
+
}
|
|
6166
|
+
async function waitWithFullJitter(baseDelayMs, maxDelayMs, attempt) {
|
|
6167
|
+
const exponentialCap = Math.min(Math.max(0, Math.trunc(maxDelayMs)), Math.max(0, Math.trunc(baseDelayMs)) * 2 ** Math.max(0, attempt));
|
|
6168
|
+
const delayMs = randomIntegerBetween(0, exponentialCap);
|
|
6169
|
+
if (delayMs > 0) {
|
|
6170
|
+
await delay(delayMs);
|
|
6171
|
+
}
|
|
6172
|
+
return delayMs;
|
|
6173
|
+
}
|
|
6174
|
+
async function fetchLocalSalesNavigatorRequest(parsedRequest, retryOptions) {
|
|
6175
|
+
let retryCount = 0;
|
|
6176
|
+
let retryDelayMs = 0;
|
|
6177
|
+
for (let attempt = 0;; attempt += 1) {
|
|
6178
|
+
try {
|
|
6179
|
+
const response = await fetch(parsedRequest.url, {
|
|
6180
|
+
method: "GET",
|
|
6181
|
+
headers: parsedRequest.headers
|
|
6182
|
+
});
|
|
6183
|
+
const text = await response.text();
|
|
6184
|
+
if (!response.ok) {
|
|
6185
|
+
const preview = text.slice(0, 300);
|
|
6186
|
+
if (isSalesNavigatorRateLimitStatus(response.status, text)) {
|
|
6187
|
+
throw new LocalSalesNavigatorRateLimitError(response.status, preview);
|
|
6188
|
+
}
|
|
6189
|
+
if (isRetryableLocalSalesNavigatorStatus(response.status) && attempt < retryOptions.maxRetries) {
|
|
6190
|
+
retryCount += 1;
|
|
6191
|
+
retryDelayMs += await waitWithFullJitter(retryOptions.retryBaseDelayMs, retryOptions.retryMaxDelayMs, attempt);
|
|
6192
|
+
continue;
|
|
6193
|
+
}
|
|
6194
|
+
throw new Error(`Sales Navigator request failed with HTTP ${response.status}: ${preview}`);
|
|
6195
|
+
}
|
|
6196
|
+
try {
|
|
6197
|
+
return { body: JSON.parse(text), retryCount, retryDelayMs };
|
|
6198
|
+
}
|
|
6199
|
+
catch {
|
|
6200
|
+
throw new Error("Sales Navigator response was not valid JSON.");
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
catch (error) {
|
|
6204
|
+
if (error instanceof LocalSalesNavigatorRateLimitError) {
|
|
6205
|
+
throw error;
|
|
6206
|
+
}
|
|
6207
|
+
if (attempt < retryOptions.maxRetries) {
|
|
6208
|
+
retryCount += 1;
|
|
6209
|
+
retryDelayMs += await waitWithFullJitter(retryOptions.retryBaseDelayMs, retryOptions.retryMaxDelayMs, attempt);
|
|
6210
|
+
continue;
|
|
6211
|
+
}
|
|
6212
|
+
throw error;
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
|
|
6217
|
+
const people = [];
|
|
6218
|
+
const seen = new Set();
|
|
6219
|
+
let totalResults = null;
|
|
6220
|
+
let fetchedPages = 0;
|
|
6221
|
+
let totalDelayMs = 0;
|
|
6222
|
+
let retryCount = 0;
|
|
6223
|
+
const pageSize = Math.max(1, Math.min(100, Math.trunc(options.pageSize)));
|
|
6224
|
+
const requestedProfiles = options.requestedProfiles == null ? null : Math.max(1, Math.trunc(options.requestedProfiles));
|
|
6225
|
+
const pageDelayMinMs = Math.max(0, Math.trunc(options.pageDelayMinMs));
|
|
6226
|
+
const pageDelayMaxMs = Math.max(pageDelayMinMs, Math.trunc(options.pageDelayMaxMs));
|
|
6227
|
+
for (let start = 0;; start += pageSize) {
|
|
6228
|
+
const remainingRequested = requestedProfiles == null ? pageSize : requestedProfiles - people.length;
|
|
6229
|
+
if (remainingRequested <= 0) {
|
|
6230
|
+
break;
|
|
6231
|
+
}
|
|
6232
|
+
const currentCount = Math.min(pageSize, remainingRequested);
|
|
6233
|
+
const pageRequest = {
|
|
6234
|
+
...parsedRequest,
|
|
6235
|
+
url: withSalesNavigatorPaging(parsedRequest.url, start, currentCount)
|
|
6236
|
+
};
|
|
6237
|
+
const response = await fetchLocalSalesNavigatorRequest(pageRequest, options.retry);
|
|
6238
|
+
retryCount += response.retryCount;
|
|
6239
|
+
totalDelayMs += response.retryDelayMs;
|
|
6240
|
+
const responseBody = response.body;
|
|
6241
|
+
fetchedPages += 1;
|
|
6242
|
+
totalResults = totalResults ?? extractLocalSalesNavigatorTotalResults(responseBody);
|
|
6243
|
+
const pagePeople = normalizeLocalSalesNavigatorPeople(responseBody, pageRequest.url);
|
|
6244
|
+
let added = 0;
|
|
6245
|
+
for (const person of pagePeople) {
|
|
6246
|
+
if (seen.has(person.profileUrl)) {
|
|
6247
|
+
continue;
|
|
6248
|
+
}
|
|
6249
|
+
seen.add(person.profileUrl);
|
|
6250
|
+
people.push(person);
|
|
6251
|
+
added += 1;
|
|
6252
|
+
if (requestedProfiles != null && people.length >= requestedProfiles) {
|
|
6253
|
+
break;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
const reportedTotalReached = totalResults != null && start + currentCount >= totalResults;
|
|
6257
|
+
if (pagePeople.length === 0 || added === 0 || reportedTotalReached) {
|
|
6258
|
+
break;
|
|
6259
|
+
}
|
|
6260
|
+
const pageDelayMs = randomIntegerBetween(pageDelayMinMs, pageDelayMaxMs);
|
|
6261
|
+
if (pageDelayMs > 0) {
|
|
6262
|
+
totalDelayMs += pageDelayMs;
|
|
6263
|
+
await delay(pageDelayMs);
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
return {
|
|
6267
|
+
people,
|
|
6268
|
+
totalResults,
|
|
6269
|
+
fetchedPages,
|
|
6270
|
+
sourceQueryUrl: parsedRequest.url,
|
|
6271
|
+
pacing: {
|
|
6272
|
+
delayMinMs: pageDelayMinMs,
|
|
6273
|
+
delayMaxMs: pageDelayMaxMs,
|
|
6274
|
+
totalDelayMs,
|
|
6275
|
+
retryCount
|
|
6276
|
+
}
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
async function importLocalSalesNavigatorPeopleViaApp(session, payload) {
|
|
6280
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/local-import`, {
|
|
6281
|
+
method: "POST",
|
|
6282
|
+
headers: {
|
|
6283
|
+
"Content-Type": "application/json",
|
|
6284
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6285
|
+
},
|
|
6286
|
+
body: JSON.stringify({
|
|
6287
|
+
sourceQueryUrl: payload.sourceQueryUrl,
|
|
6288
|
+
slicedQueryUrl: payload.sourceQueryUrl,
|
|
6289
|
+
appliedFilters: [],
|
|
6290
|
+
maxResultsPerSearch: payload.maxResultsPerSearch,
|
|
6291
|
+
numberOfProfiles: payload.numberOfProfiles,
|
|
6292
|
+
slicePreset: payload.slicePreset,
|
|
6293
|
+
totalResults: payload.totalResults,
|
|
6294
|
+
fetchedPages: payload.fetchedPages,
|
|
6295
|
+
people: payload.people,
|
|
6296
|
+
rawPayload: {
|
|
6297
|
+
workflow: "salesnav:local-import",
|
|
6298
|
+
localPeopleCount: payload.people.length,
|
|
6299
|
+
fetchedPages: payload.fetchedPages,
|
|
6300
|
+
pacing: payload.pacing
|
|
6301
|
+
}
|
|
6302
|
+
})
|
|
6303
|
+
}), SalesNavigatorLocalImportResponseSchema);
|
|
6304
|
+
return value;
|
|
6305
|
+
}
|
|
6306
|
+
async function runLeadPoolAutoProcessViaApp(session, payload) {
|
|
6307
|
+
const url = new URL('/api/leadpool/auto-process', session.apiBaseUrl);
|
|
6308
|
+
if (payload.clientId)
|
|
6309
|
+
url.searchParams.set('clientId', String(payload.clientId));
|
|
6310
|
+
url.searchParams.set('companyLimit', String(payload.companyLimit));
|
|
6311
|
+
url.searchParams.set('contactCountryLimit', String(payload.contactCountryLimit));
|
|
6312
|
+
url.searchParams.set('contactNameLimit', String(payload.contactNameLimit));
|
|
6313
|
+
url.searchParams.set('hunterLimit', String(payload.hunterLimit));
|
|
6314
|
+
url.searchParams.set('hunterConcurrency', String(payload.hunterConcurrency));
|
|
6315
|
+
url.searchParams.set('refresh', payload.refresh ? 'true' : 'false');
|
|
6316
|
+
url.searchParams.set('qualifiedLeads', payload.qualifiedLeads ? 'true' : 'false');
|
|
6317
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(url.toString(), {
|
|
6318
|
+
method: "POST",
|
|
6319
|
+
headers: {
|
|
6320
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6321
|
+
}
|
|
6322
|
+
}), LeadPoolAutoProcessResponseSchema);
|
|
6323
|
+
return value;
|
|
6324
|
+
}
|
|
6325
|
+
async function launchAffiliateCampaignViaApp(session, payload) {
|
|
6326
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/affiliate-campaigns`, {
|
|
5727
6327
|
method: "POST",
|
|
5728
6328
|
headers: {
|
|
5729
6329
|
"Content-Type": "application/json",
|
|
5730
6330
|
Authorization: `Bearer ${currentSession.accessToken}`
|
|
5731
6331
|
},
|
|
5732
6332
|
body: JSON.stringify(payload)
|
|
5733
|
-
}),
|
|
6333
|
+
}), AffiliateCampaignLaunchResponseSchema);
|
|
5734
6334
|
return value;
|
|
5735
6335
|
}
|
|
5736
6336
|
async function runLeadPoolEmailFinderViaApp(session, payload) {
|
|
5737
|
-
const url = new URL(
|
|
5738
|
-
url.searchParams.set(
|
|
5739
|
-
url.searchParams.set(
|
|
6337
|
+
const url = new URL('/api/leadpool/process-hunter-emails', session.apiBaseUrl);
|
|
6338
|
+
url.searchParams.set('provider', payload.provider);
|
|
6339
|
+
url.searchParams.set('limit', String(payload.limit));
|
|
5740
6340
|
if (payload.concurrency != null) {
|
|
5741
|
-
url.searchParams.set(
|
|
6341
|
+
url.searchParams.set('concurrency', String(payload.concurrency));
|
|
5742
6342
|
}
|
|
5743
6343
|
if (payload.retryNotFoundAfterDays != null) {
|
|
5744
|
-
url.searchParams.set(
|
|
6344
|
+
url.searchParams.set('retryNotFoundAfterDays', String(payload.retryNotFoundAfterDays));
|
|
5745
6345
|
}
|
|
5746
6346
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(url.toString(), {
|
|
5747
6347
|
method: "POST",
|
|
@@ -5751,6 +6351,17 @@ async function runLeadPoolEmailFinderViaApp(session, payload) {
|
|
|
5751
6351
|
}), LeadPoolEmailFinderProcessResponseSchema);
|
|
5752
6352
|
return value;
|
|
5753
6353
|
}
|
|
6354
|
+
async function syncPhantombusterContainersViaApp(session, payload) {
|
|
6355
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/phantombuster/containers/sync`, {
|
|
6356
|
+
method: "POST",
|
|
6357
|
+
headers: {
|
|
6358
|
+
"Content-Type": "application/json",
|
|
6359
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6360
|
+
},
|
|
6361
|
+
body: JSON.stringify(payload)
|
|
6362
|
+
}), PhantombusterContainersSyncResponseSchema);
|
|
6363
|
+
return value;
|
|
6364
|
+
}
|
|
5754
6365
|
function serializeSalesNavigatorFiltersForApi(filters) {
|
|
5755
6366
|
return filters.map((filter) => ({
|
|
5756
6367
|
type: filter.type,
|
|
@@ -5909,7 +6520,8 @@ async function startSalesNavigatorExport(session, payload, traceId) {
|
|
|
5909
6520
|
const sessionOverridePayload = exportSession
|
|
5910
6521
|
? {
|
|
5911
6522
|
sessionCookie: exportSession.sessionCookie,
|
|
5912
|
-
selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256
|
|
6523
|
+
selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256,
|
|
6524
|
+
selectedSessionUserAgent: exportSession.selectedSessionUserAgent
|
|
5913
6525
|
}
|
|
5914
6526
|
: {};
|
|
5915
6527
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
|
|
@@ -6738,9 +7350,6 @@ function hashSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
|
|
|
6738
7350
|
}
|
|
6739
7351
|
return createHash("sha256").update(liAt).digest("hex");
|
|
6740
7352
|
}
|
|
6741
|
-
function normalizeSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
|
|
6742
|
-
return extractLiAtCookieValue(sessionCookie) ?? sessionCookie;
|
|
6743
|
-
}
|
|
6744
7353
|
function cacheSalesNavigatorExportSessionOverride(queryUrl, value) {
|
|
6745
7354
|
cachedSalesNavigatorExportSessionOverride = {
|
|
6746
7355
|
queryUrl,
|
|
@@ -6760,8 +7369,9 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6760
7369
|
const selectedSessionCookieSha256 = hashSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie);
|
|
6761
7370
|
if (process.env.SALESPROMPTER_SALESNAV_EXPORT_SKIP_EXTENSION_PREFLIGHT === "1") {
|
|
6762
7371
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6763
|
-
sessionCookie:
|
|
7372
|
+
sessionCookie: localExtensionConfig.cookie,
|
|
6764
7373
|
selectedSessionCookieSha256,
|
|
7374
|
+
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
6765
7375
|
source: "chrome_extension"
|
|
6766
7376
|
});
|
|
6767
7377
|
}
|
|
@@ -6778,8 +7388,9 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6778
7388
|
});
|
|
6779
7389
|
if (probe.status === "ok") {
|
|
6780
7390
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6781
|
-
sessionCookie:
|
|
7391
|
+
sessionCookie: localExtensionConfig.cookie,
|
|
6782
7392
|
selectedSessionCookieSha256,
|
|
7393
|
+
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
6783
7394
|
source: "chrome_extension"
|
|
6784
7395
|
});
|
|
6785
7396
|
}
|
|
@@ -6791,9 +7402,10 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6791
7402
|
});
|
|
6792
7403
|
if (claimed?.sessionCookie) {
|
|
6793
7404
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6794
|
-
sessionCookie:
|
|
7405
|
+
sessionCookie: claimed.sessionCookie,
|
|
6795
7406
|
selectedSessionCookieSha256: claimed.sessionCookieSha256 ??
|
|
6796
7407
|
hashSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
|
|
7408
|
+
selectedSessionUserAgent: null,
|
|
6797
7409
|
source: "session_vault"
|
|
6798
7410
|
});
|
|
6799
7411
|
}
|
|
@@ -9172,7 +9784,7 @@ program
|
|
|
9172
9784
|
const titleLimit = options.titleLimit === undefined
|
|
9173
9785
|
? undefined
|
|
9174
9786
|
: z.coerce.number().int().min(1).max(1000).parse(options.titleLimit);
|
|
9175
|
-
const maxResultsPerSearch = z.coerce.number().int().min(1).max(
|
|
9787
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(100000).parse(options.maxResultsPerSearch);
|
|
9176
9788
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9177
9789
|
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
9178
9790
|
const maxSlicesPerTitle = z.coerce.number().int().min(1).max(10000).parse(options.maxSlicesPerTitle);
|
|
@@ -10013,6 +10625,7 @@ program
|
|
|
10013
10625
|
.alias("pb:containers:sync")
|
|
10014
10626
|
.description("Fetch Phantombuster containers for configured agents and store them in Neon.")
|
|
10015
10627
|
.option("--agent-id <id>", "Phantombuster agent id to sync. Repeat to sync multiple agents.", collectStringOptionValue, [])
|
|
10628
|
+
.option("--container-id <id>", "Specific Phantombuster container id to sync. Repeat to sync multiple containers.", collectStringOptionValue, [])
|
|
10016
10629
|
.option("--limit <number>", "Maximum containers to fetch per Phantombuster page", "100")
|
|
10017
10630
|
.option("--max-pages <number>", "Maximum Phantombuster pages to fetch per agent", "50")
|
|
10018
10631
|
.option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
|
|
@@ -10022,6 +10635,7 @@ program
|
|
|
10022
10635
|
.option("--out <path>", "Optional local JSON output path")
|
|
10023
10636
|
.action(async (options) => {
|
|
10024
10637
|
const agentIds = z.array(z.string().min(1)).parse(options.agentId);
|
|
10638
|
+
const containerIds = z.array(z.string().min(1)).parse(options.containerId);
|
|
10025
10639
|
const limit = z.coerce.number().int().min(1).max(500).parse(options.limit);
|
|
10026
10640
|
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
10027
10641
|
const mode = z.enum(["all", "finalized"]).parse(options.mode);
|
|
@@ -10031,6 +10645,7 @@ program
|
|
|
10031
10645
|
const session = await requireAuthSession();
|
|
10032
10646
|
const result = await syncPhantombusterContainersViaApp(session, {
|
|
10033
10647
|
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
10648
|
+
containerIds: containerIds.length > 0 ? containerIds : undefined,
|
|
10034
10649
|
limit,
|
|
10035
10650
|
maxPages,
|
|
10036
10651
|
mode,
|
|
@@ -10122,6 +10737,100 @@ program
|
|
|
10122
10737
|
}
|
|
10123
10738
|
printOutput(payload);
|
|
10124
10739
|
});
|
|
10740
|
+
program
|
|
10741
|
+
.command("salesnav:local-import")
|
|
10742
|
+
.alias("search:local-import")
|
|
10743
|
+
.description("Run a saved Sales Navigator API request locally and store the people in Salesprompter.")
|
|
10744
|
+
.option("--curl-file <path>", "Path to a saved curl request copied from the Sales Navigator network panel")
|
|
10745
|
+
.option("--query-url <url>", "Sales Navigator people search URL. Uses the locally captured extension session automatically.")
|
|
10746
|
+
.option("--max-results-per-search <number>", "Maximum expected results for this local request", "100")
|
|
10747
|
+
.option("--number-of-profiles <number>", "Total profiles to import, or 'all' for the full reported result set", "all")
|
|
10748
|
+
.option("--page-size <number>", "Profiles requested per local Sales Navigator API page", "25")
|
|
10749
|
+
.option("--page-delay-min-ms <number>", "Minimum randomized delay between Sales Navigator result pages", "1500")
|
|
10750
|
+
.option("--page-delay-max-ms <number>", "Maximum randomized delay between Sales Navigator result pages", "6000")
|
|
10751
|
+
.option("--max-retries <number>", "Bounded retries for transient network/5xx failures. Rate limits still stop immediately.", "2")
|
|
10752
|
+
.option("--retry-base-delay-ms <number>", "Base delay for full-jitter retry backoff", "2000")
|
|
10753
|
+
.option("--retry-max-delay-ms <number>", "Maximum delay for full-jitter retry backoff", "30000")
|
|
10754
|
+
.option("--slice-preset <name>", "Slice preset label stored with the import run", "local-salesnav")
|
|
10755
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
10756
|
+
.option("--dry-run", "Run and normalize the local request without writing to Salesprompter", false)
|
|
10757
|
+
.action(async (options) => {
|
|
10758
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(100000).parse(options.maxResultsPerSearch);
|
|
10759
|
+
const numberOfProfilesOption = String(options.numberOfProfiles ?? "all").trim().toLowerCase();
|
|
10760
|
+
const requestedProfiles = numberOfProfilesOption === "all" ? null : z.coerce.number().int().min(1).max(100000).parse(options.numberOfProfiles);
|
|
10761
|
+
const pageSize = z.coerce.number().int().min(1).max(100).parse(options.pageSize);
|
|
10762
|
+
const pageDelayMinMs = z.coerce.number().int().min(0).max(300000).parse(options.pageDelayMinMs);
|
|
10763
|
+
const pageDelayMaxMs = z.coerce.number().int().min(0).max(300000).parse(options.pageDelayMaxMs);
|
|
10764
|
+
if (pageDelayMaxMs < pageDelayMinMs) {
|
|
10765
|
+
throw new Error("--page-delay-max-ms must be greater than or equal to --page-delay-min-ms.");
|
|
10766
|
+
}
|
|
10767
|
+
const retry = {
|
|
10768
|
+
maxRetries: z.coerce.number().int().min(0).max(5).parse(options.maxRetries),
|
|
10769
|
+
retryBaseDelayMs: z.coerce.number().int().min(0).max(300000).parse(options.retryBaseDelayMs),
|
|
10770
|
+
retryMaxDelayMs: z.coerce.number().int().min(0).max(300000).parse(options.retryMaxDelayMs)
|
|
10771
|
+
};
|
|
10772
|
+
const curlFile = typeof options.curlFile === "string" && options.curlFile.trim().length > 0 ? options.curlFile.trim() : null;
|
|
10773
|
+
const queryUrl = typeof options.queryUrl === "string" && options.queryUrl.trim().length > 0 ? z.string().url().parse(options.queryUrl.trim()) : null;
|
|
10774
|
+
if (Boolean(curlFile) === Boolean(queryUrl)) {
|
|
10775
|
+
throw new Error("Provide exactly one of --curl-file or --query-url.");
|
|
10776
|
+
}
|
|
10777
|
+
const parsedRequest = curlFile
|
|
10778
|
+
? parseSalesNavigatorCurlRequest(await readFile(curlFile, "utf8"))
|
|
10779
|
+
: buildSalesNavigatorApiRequestFromSearchUrl(queryUrl, await readLinkedInDirectLookupConfig(), pageSize);
|
|
10780
|
+
const localFetch = await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
|
|
10781
|
+
requestedProfiles,
|
|
10782
|
+
pageSize,
|
|
10783
|
+
pageDelayMinMs,
|
|
10784
|
+
pageDelayMaxMs,
|
|
10785
|
+
retry
|
|
10786
|
+
});
|
|
10787
|
+
const people = localFetch.people;
|
|
10788
|
+
const totalResults = localFetch.totalResults;
|
|
10789
|
+
const dryRun = Boolean(options.dryRun);
|
|
10790
|
+
if (people.length === 0) {
|
|
10791
|
+
throw new Error("The local Sales Navigator response did not contain any importable people rows.");
|
|
10792
|
+
}
|
|
10793
|
+
const basePayload = {
|
|
10794
|
+
status: "ok",
|
|
10795
|
+
dryRun,
|
|
10796
|
+
sourceQueryUrl: localFetch.sourceQueryUrl,
|
|
10797
|
+
totalResults,
|
|
10798
|
+
discovered: people.length,
|
|
10799
|
+
fetchedPages: localFetch.fetchedPages,
|
|
10800
|
+
requestedProfiles: requestedProfiles ?? "all",
|
|
10801
|
+
pacing: localFetch.pacing,
|
|
10802
|
+
people
|
|
10803
|
+
};
|
|
10804
|
+
if (dryRun) {
|
|
10805
|
+
if (options.out) {
|
|
10806
|
+
await writeJsonFile(options.out, basePayload);
|
|
10807
|
+
}
|
|
10808
|
+
printOutput(basePayload);
|
|
10809
|
+
return;
|
|
10810
|
+
}
|
|
10811
|
+
const session = await requireAuthSession();
|
|
10812
|
+
const imported = await importLocalSalesNavigatorPeopleViaApp(session, {
|
|
10813
|
+
sourceQueryUrl: localFetch.sourceQueryUrl,
|
|
10814
|
+
people,
|
|
10815
|
+
totalResults,
|
|
10816
|
+
fetchedPages: localFetch.fetchedPages,
|
|
10817
|
+
pacing: localFetch.pacing,
|
|
10818
|
+
maxResultsPerSearch,
|
|
10819
|
+
numberOfProfiles: requestedProfiles ?? people.length,
|
|
10820
|
+
slicePreset: options.slicePreset
|
|
10821
|
+
});
|
|
10822
|
+
const payload = {
|
|
10823
|
+
...basePayload,
|
|
10824
|
+
runId: imported.runId,
|
|
10825
|
+
containerId: imported.containerId,
|
|
10826
|
+
imported: imported.imported,
|
|
10827
|
+
upserted: imported.upserted
|
|
10828
|
+
};
|
|
10829
|
+
if (options.out) {
|
|
10830
|
+
await writeJsonFile(options.out, payload);
|
|
10831
|
+
}
|
|
10832
|
+
printOutput(payload);
|
|
10833
|
+
});
|
|
10125
10834
|
program
|
|
10126
10835
|
.command("salesnav:count")
|
|
10127
10836
|
.alias("search:count")
|
|
@@ -10784,6 +11493,54 @@ program
|
|
|
10784
11493
|
execution
|
|
10785
11494
|
});
|
|
10786
11495
|
});
|
|
11496
|
+
program
|
|
11497
|
+
.command("affiliate:launch")
|
|
11498
|
+
.description("Turn an affiliate link and LinkedIn audience URL into a ready-to-review outreach campaign.")
|
|
11499
|
+
.requiredOption("--affiliate-link <url>", "Affiliate landing URL, including the tracking/referral parameter")
|
|
11500
|
+
.requiredOption("--linkedin-url <url>", "LinkedIn content/search URL for the audience")
|
|
11501
|
+
.option("--max-results <number>", "Maximum audience profiles to extract", "1000")
|
|
11502
|
+
.option("--dry-run", "Validate inputs and show the planned campaign without launching extraction", false)
|
|
11503
|
+
.action(async (options) => {
|
|
11504
|
+
const session = await requireAuthSession();
|
|
11505
|
+
const result = await launchAffiliateCampaignViaApp(session, {
|
|
11506
|
+
affiliateLink: z.string().url().parse(options.affiliateLink),
|
|
11507
|
+
linkedInUrl: z.string().url().parse(options.linkedinUrl),
|
|
11508
|
+
maxResults: z.coerce.number().int().min(1).max(1000).parse(options.maxResults),
|
|
11509
|
+
dryRun: Boolean(options.dryRun)
|
|
11510
|
+
});
|
|
11511
|
+
printOutput(result);
|
|
11512
|
+
});
|
|
11513
|
+
program
|
|
11514
|
+
.command("leads:process")
|
|
11515
|
+
.description("Automatically process the lead pool: companies, contact cleanup, email finding, and qualified leads.")
|
|
11516
|
+
.option("--client-id <number>", "Limit processing to one Salesprompter clientId")
|
|
11517
|
+
.option("--company-limit <number>", "Max companies to start per enrichment batch", "10")
|
|
11518
|
+
.option("--contact-country-limit <number>", "Max contact country-code rows to process", "500")
|
|
11519
|
+
.option("--contact-name-limit <number>", "Max contact name rows to clean", "250")
|
|
11520
|
+
.option("--hunter-limit <number>", "Max Hunter email-finder rows to process", "500")
|
|
11521
|
+
.option("--hunter-concurrency <number>", "Hunter email-finder concurrency", "6")
|
|
11522
|
+
.option("--refresh", "Run a full lead-pool refresh after processing", false)
|
|
11523
|
+
.option("--no-qualified-leads", "Skip rebuilding the qualified lead snapshot")
|
|
11524
|
+
.action(async (options) => {
|
|
11525
|
+
const session = await requireAuthSession();
|
|
11526
|
+
const clientId = options.clientId == null || String(options.clientId).trim() === ''
|
|
11527
|
+
? null
|
|
11528
|
+
: parsePositiveClientIdValue(options.clientId);
|
|
11529
|
+
const result = await runLeadPoolAutoProcessViaApp(session, {
|
|
11530
|
+
clientId,
|
|
11531
|
+
companyLimit: z.coerce.number().int().min(1).max(50).parse(options.companyLimit),
|
|
11532
|
+
contactCountryLimit: z.coerce.number().int().min(1).max(5000).parse(options.contactCountryLimit),
|
|
11533
|
+
contactNameLimit: z.coerce.number().int().min(1).max(250).parse(options.contactNameLimit),
|
|
11534
|
+
hunterLimit: z.coerce.number().int().min(1).max(10000).parse(options.hunterLimit),
|
|
11535
|
+
hunterConcurrency: z.coerce.number().int().min(1).max(12).parse(options.hunterConcurrency),
|
|
11536
|
+
refresh: Boolean(options.refresh),
|
|
11537
|
+
qualifiedLeads: options.qualifiedLeads !== false,
|
|
11538
|
+
});
|
|
11539
|
+
printOutput({
|
|
11540
|
+
status: result.success ? "ok" : "partial",
|
|
11541
|
+
...result,
|
|
11542
|
+
});
|
|
11543
|
+
});
|
|
10787
11544
|
program
|
|
10788
11545
|
.command("contacts:process-emails")
|
|
10789
11546
|
.alias("contacts:resolve-emails")
|
package/package.json
CHANGED