salesprompter-cli 0.1.38 → 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 +866 -13
- 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,9 +124,74 @@ 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();
|
|
177
|
+
const LeadPoolEmailFinderProcessResponseSchema = z.object({
|
|
178
|
+
success: z.boolean(),
|
|
179
|
+
provider: z.string().optional(),
|
|
180
|
+
selected: z.number().int().nonnegative(),
|
|
181
|
+
found: z.number().int().nonnegative(),
|
|
182
|
+
notFound: z.number().int().nonnegative(),
|
|
183
|
+
failed: z.number().int().nonnegative(),
|
|
184
|
+
durationMs: z.number().nonnegative(),
|
|
185
|
+
launched: z.boolean().optional(),
|
|
186
|
+
agentId: z.string().nullable().optional(),
|
|
187
|
+
containerId: z.string().nullable().optional(),
|
|
188
|
+
csvInputUrl: z.string().nullable().optional(),
|
|
189
|
+
processedAt: z.string()
|
|
190
|
+
});
|
|
127
191
|
const PhantombusterContainersSyncResponseSchema = z.object({
|
|
128
192
|
status: z.literal("ok"),
|
|
129
193
|
agentIds: z.array(z.string().min(1)),
|
|
194
|
+
containerIds: z.array(z.string().min(1)).optional(),
|
|
130
195
|
agents: z.array(z.object({
|
|
131
196
|
agentId: z.string().min(1),
|
|
132
197
|
fetched: z.number().int().nonnegative(),
|
|
@@ -326,21 +391,21 @@ const cliPacks = [
|
|
|
326
391
|
slug: "research",
|
|
327
392
|
title: "Research",
|
|
328
393
|
summary: "Scrape markets and enrich companies before outreach.",
|
|
329
|
-
commands: ["market:scrape", "companies:enrich"],
|
|
394
|
+
commands: ["market:scrape", "companies:enrich", "leads:process"],
|
|
330
395
|
installStatus: "included"
|
|
331
396
|
},
|
|
332
397
|
{
|
|
333
398
|
slug: "discovery",
|
|
334
399
|
title: "Discovery",
|
|
335
400
|
summary: "Find leads from product and market inputs.",
|
|
336
|
-
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"],
|
|
337
402
|
installStatus: "included"
|
|
338
403
|
},
|
|
339
404
|
{
|
|
340
405
|
slug: "outreach",
|
|
341
406
|
title: "Outreach",
|
|
342
407
|
summary: "Prepare and sync qualified leads into downstream systems.",
|
|
343
|
-
commands: ["sync:outreach", "sync:crm"],
|
|
408
|
+
commands: ["affiliate:launch", "sync:outreach", "sync:crm"],
|
|
344
409
|
installStatus: "included"
|
|
345
410
|
}
|
|
346
411
|
];
|
|
@@ -354,6 +419,7 @@ const helpAliasByCommandName = new Map([
|
|
|
354
419
|
["salesnav:crawl", "search:run"],
|
|
355
420
|
["salesnav:crawl:status", "search:status"],
|
|
356
421
|
["salesnav:export", "search:export"],
|
|
422
|
+
["salesnav:local-import", "search:local-import"],
|
|
357
423
|
["salesnav:count", "search:count"]
|
|
358
424
|
]);
|
|
359
425
|
const helpVisibleCommandNames = new Set([
|
|
@@ -377,7 +443,9 @@ const helpVisibleCommandNames = new Set([
|
|
|
377
443
|
"leads:generate",
|
|
378
444
|
"leads:enrich",
|
|
379
445
|
"leads:score",
|
|
446
|
+
"leads:process",
|
|
380
447
|
"leads:pipeline",
|
|
448
|
+
"affiliate:launch",
|
|
381
449
|
"sync:crm",
|
|
382
450
|
"sync:outreach",
|
|
383
451
|
"linkedin-companies:backfill",
|
|
@@ -386,6 +454,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
386
454
|
"salesnav:crawl",
|
|
387
455
|
"salesnav:crawl:status",
|
|
388
456
|
"salesnav:export",
|
|
457
|
+
"salesnav:local-import",
|
|
389
458
|
"salesnav:count"
|
|
390
459
|
]);
|
|
391
460
|
function printOutput(value) {
|
|
@@ -1313,12 +1382,22 @@ async function listChromeExtensionTokenLogCandidates() {
|
|
|
1313
1382
|
if (!file.endsWith(".log")) {
|
|
1314
1383
|
continue;
|
|
1315
1384
|
}
|
|
1316
|
-
|
|
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 });
|
|
1317
1394
|
}
|
|
1318
1395
|
}
|
|
1319
1396
|
}
|
|
1320
1397
|
}
|
|
1321
|
-
return paths
|
|
1398
|
+
return paths
|
|
1399
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path))
|
|
1400
|
+
.map((entry) => entry.path);
|
|
1322
1401
|
}
|
|
1323
1402
|
async function readLocalLinkedInExtensionDirectLookupConfig() {
|
|
1324
1403
|
const candidates = await listChromeExtensionTokenLogCandidates();
|
|
@@ -1416,6 +1495,9 @@ async function readLinkedInDirectLookupConfig() {
|
|
|
1416
1495
|
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1417
1496
|
return envConfig;
|
|
1418
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
|
+
}
|
|
1419
1501
|
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
1420
1502
|
if (localExtensionConfig) {
|
|
1421
1503
|
cachedLinkedInDirectLookupConfig = localExtensionConfig;
|
|
@@ -5708,6 +5790,567 @@ async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
|
5708
5790
|
}), LinkedInCompanyBackfillStatusResponseSchema);
|
|
5709
5791
|
return value;
|
|
5710
5792
|
}
|
|
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`, {
|
|
6327
|
+
method: "POST",
|
|
6328
|
+
headers: {
|
|
6329
|
+
"Content-Type": "application/json",
|
|
6330
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6331
|
+
},
|
|
6332
|
+
body: JSON.stringify(payload)
|
|
6333
|
+
}), AffiliateCampaignLaunchResponseSchema);
|
|
6334
|
+
return value;
|
|
6335
|
+
}
|
|
6336
|
+
async function runLeadPoolEmailFinderViaApp(session, payload) {
|
|
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));
|
|
6340
|
+
if (payload.concurrency != null) {
|
|
6341
|
+
url.searchParams.set('concurrency', String(payload.concurrency));
|
|
6342
|
+
}
|
|
6343
|
+
if (payload.retryNotFoundAfterDays != null) {
|
|
6344
|
+
url.searchParams.set('retryNotFoundAfterDays', String(payload.retryNotFoundAfterDays));
|
|
6345
|
+
}
|
|
6346
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(url.toString(), {
|
|
6347
|
+
method: "POST",
|
|
6348
|
+
headers: {
|
|
6349
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
6350
|
+
}
|
|
6351
|
+
}), LeadPoolEmailFinderProcessResponseSchema);
|
|
6352
|
+
return value;
|
|
6353
|
+
}
|
|
5711
6354
|
async function syncPhantombusterContainersViaApp(session, payload) {
|
|
5712
6355
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/phantombuster/containers/sync`, {
|
|
5713
6356
|
method: "POST",
|
|
@@ -5877,7 +6520,8 @@ async function startSalesNavigatorExport(session, payload, traceId) {
|
|
|
5877
6520
|
const sessionOverridePayload = exportSession
|
|
5878
6521
|
? {
|
|
5879
6522
|
sessionCookie: exportSession.sessionCookie,
|
|
5880
|
-
selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256
|
|
6523
|
+
selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256,
|
|
6524
|
+
selectedSessionUserAgent: exportSession.selectedSessionUserAgent
|
|
5881
6525
|
}
|
|
5882
6526
|
: {};
|
|
5883
6527
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
|
|
@@ -6706,9 +7350,6 @@ function hashSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
|
|
|
6706
7350
|
}
|
|
6707
7351
|
return createHash("sha256").update(liAt).digest("hex");
|
|
6708
7352
|
}
|
|
6709
|
-
function normalizeSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
|
|
6710
|
-
return extractLiAtCookieValue(sessionCookie) ?? sessionCookie;
|
|
6711
|
-
}
|
|
6712
7353
|
function cacheSalesNavigatorExportSessionOverride(queryUrl, value) {
|
|
6713
7354
|
cachedSalesNavigatorExportSessionOverride = {
|
|
6714
7355
|
queryUrl,
|
|
@@ -6728,8 +7369,9 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6728
7369
|
const selectedSessionCookieSha256 = hashSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie);
|
|
6729
7370
|
if (process.env.SALESPROMPTER_SALESNAV_EXPORT_SKIP_EXTENSION_PREFLIGHT === "1") {
|
|
6730
7371
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6731
|
-
sessionCookie:
|
|
7372
|
+
sessionCookie: localExtensionConfig.cookie,
|
|
6732
7373
|
selectedSessionCookieSha256,
|
|
7374
|
+
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
6733
7375
|
source: "chrome_extension"
|
|
6734
7376
|
});
|
|
6735
7377
|
}
|
|
@@ -6746,8 +7388,9 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6746
7388
|
});
|
|
6747
7389
|
if (probe.status === "ok") {
|
|
6748
7390
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6749
|
-
sessionCookie:
|
|
7391
|
+
sessionCookie: localExtensionConfig.cookie,
|
|
6750
7392
|
selectedSessionCookieSha256,
|
|
7393
|
+
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
6751
7394
|
source: "chrome_extension"
|
|
6752
7395
|
});
|
|
6753
7396
|
}
|
|
@@ -6759,9 +7402,10 @@ async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
|
6759
7402
|
});
|
|
6760
7403
|
if (claimed?.sessionCookie) {
|
|
6761
7404
|
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
6762
|
-
sessionCookie:
|
|
7405
|
+
sessionCookie: claimed.sessionCookie,
|
|
6763
7406
|
selectedSessionCookieSha256: claimed.sessionCookieSha256 ??
|
|
6764
7407
|
hashSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
|
|
7408
|
+
selectedSessionUserAgent: null,
|
|
6765
7409
|
source: "session_vault"
|
|
6766
7410
|
});
|
|
6767
7411
|
}
|
|
@@ -9140,7 +9784,7 @@ program
|
|
|
9140
9784
|
const titleLimit = options.titleLimit === undefined
|
|
9141
9785
|
? undefined
|
|
9142
9786
|
: z.coerce.number().int().min(1).max(1000).parse(options.titleLimit);
|
|
9143
|
-
const maxResultsPerSearch = z.coerce.number().int().min(1).max(
|
|
9787
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(100000).parse(options.maxResultsPerSearch);
|
|
9144
9788
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9145
9789
|
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
9146
9790
|
const maxSlicesPerTitle = z.coerce.number().int().min(1).max(10000).parse(options.maxSlicesPerTitle);
|
|
@@ -9981,6 +10625,7 @@ program
|
|
|
9981
10625
|
.alias("pb:containers:sync")
|
|
9982
10626
|
.description("Fetch Phantombuster containers for configured agents and store them in Neon.")
|
|
9983
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, [])
|
|
9984
10629
|
.option("--limit <number>", "Maximum containers to fetch per Phantombuster page", "100")
|
|
9985
10630
|
.option("--max-pages <number>", "Maximum Phantombuster pages to fetch per agent", "50")
|
|
9986
10631
|
.option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
|
|
@@ -9990,6 +10635,7 @@ program
|
|
|
9990
10635
|
.option("--out <path>", "Optional local JSON output path")
|
|
9991
10636
|
.action(async (options) => {
|
|
9992
10637
|
const agentIds = z.array(z.string().min(1)).parse(options.agentId);
|
|
10638
|
+
const containerIds = z.array(z.string().min(1)).parse(options.containerId);
|
|
9993
10639
|
const limit = z.coerce.number().int().min(1).max(500).parse(options.limit);
|
|
9994
10640
|
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
9995
10641
|
const mode = z.enum(["all", "finalized"]).parse(options.mode);
|
|
@@ -9999,6 +10645,7 @@ program
|
|
|
9999
10645
|
const session = await requireAuthSession();
|
|
10000
10646
|
const result = await syncPhantombusterContainersViaApp(session, {
|
|
10001
10647
|
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
10648
|
+
containerIds: containerIds.length > 0 ? containerIds : undefined,
|
|
10002
10649
|
limit,
|
|
10003
10650
|
maxPages,
|
|
10004
10651
|
mode,
|
|
@@ -10090,6 +10737,100 @@ program
|
|
|
10090
10737
|
}
|
|
10091
10738
|
printOutput(payload);
|
|
10092
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
|
+
});
|
|
10093
10834
|
program
|
|
10094
10835
|
.command("salesnav:count")
|
|
10095
10836
|
.alias("search:count")
|
|
@@ -10752,6 +11493,54 @@ program
|
|
|
10752
11493
|
execution
|
|
10753
11494
|
});
|
|
10754
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
|
+
});
|
|
10755
11544
|
program
|
|
10756
11545
|
.command("contacts:process-emails")
|
|
10757
11546
|
.alias("contacts:resolve-emails")
|
|
@@ -10763,6 +11552,8 @@ program
|
|
|
10763
11552
|
.requiredOption("--out-dir <path>", "Output directory for artifacts")
|
|
10764
11553
|
.option("--trace-id <traceId>", "Trace id for this batch")
|
|
10765
11554
|
.option("--endpoint-url <url>", "Override the enrichment workflow endpoint")
|
|
11555
|
+
.option("--provider <provider>", "Email finder provider for workspace queue: hunter or phantombuster", "hunter")
|
|
11556
|
+
.option("--retry-not-found-after-days <number>", "Retry stale Email not found rows after this many days", "1")
|
|
10766
11557
|
.option("--timeout-ms <number>", "Workflow trigger timeout in milliseconds", "30000")
|
|
10767
11558
|
.option("--company-cleaning <mode>", "Company cleaning mode for direct input: off, basic, or ai", "basic")
|
|
10768
11559
|
.option("--dry-run", "Preview the next batch without starting background processing", false)
|
|
@@ -10771,7 +11562,69 @@ program
|
|
|
10771
11562
|
const outDir = z.string().min(1).parse(options.outDir);
|
|
10772
11563
|
const timeoutMs = z.coerce.number().int().min(1000).max(300000).parse(options.timeoutMs);
|
|
10773
11564
|
const usingDirectInput = typeof options.in === "string" && options.in.trim().length > 0;
|
|
11565
|
+
const provider = z.enum(["hunter", "phantombuster"]).parse(String(options.provider ?? "hunter").trim().toLowerCase());
|
|
11566
|
+
const retryNotFoundAfterDays = z.coerce.number().int().min(0).max(30).parse(options.retryNotFoundAfterDays);
|
|
10774
11567
|
const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
|
|
11568
|
+
if (provider === "phantombuster" && !usingDirectInput) {
|
|
11569
|
+
await mkdir(outDir, { recursive: true });
|
|
11570
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-email-phantombuster-${Date.now()}`);
|
|
11571
|
+
const requestPath = `${outDir}/email-enrichment-request-phantombuster.json`;
|
|
11572
|
+
const responsePath = `${outDir}/email-enrichment-response-phantombuster.json`;
|
|
11573
|
+
const requestPayload = {
|
|
11574
|
+
traceId,
|
|
11575
|
+
provider,
|
|
11576
|
+
limit,
|
|
11577
|
+
retryNotFoundAfterDays
|
|
11578
|
+
};
|
|
11579
|
+
await writeJsonFile(requestPath, requestPayload);
|
|
11580
|
+
if (options.dryRun) {
|
|
11581
|
+
printOutput({
|
|
11582
|
+
status: "ok",
|
|
11583
|
+
provider,
|
|
11584
|
+
limit,
|
|
11585
|
+
traceId,
|
|
11586
|
+
dryRun: true,
|
|
11587
|
+
mode: "workspace-queue",
|
|
11588
|
+
contactsQueued: null,
|
|
11589
|
+
started: false,
|
|
11590
|
+
artifacts: {
|
|
11591
|
+
inputSqlPath: null,
|
|
11592
|
+
inputRowsPath: null,
|
|
11593
|
+
directReportPath: null,
|
|
11594
|
+
requestPath,
|
|
11595
|
+
responsePath: null
|
|
11596
|
+
}
|
|
11597
|
+
});
|
|
11598
|
+
return;
|
|
11599
|
+
}
|
|
11600
|
+
const session = await requireAuthSession();
|
|
11601
|
+
const result = await runLeadPoolEmailFinderViaApp(session, {
|
|
11602
|
+
provider,
|
|
11603
|
+
limit,
|
|
11604
|
+
retryNotFoundAfterDays
|
|
11605
|
+
});
|
|
11606
|
+
await writeJsonFile(responsePath, result);
|
|
11607
|
+
printOutput({
|
|
11608
|
+
status: result.success ? "ok" : "partial",
|
|
11609
|
+
provider,
|
|
11610
|
+
limit,
|
|
11611
|
+
traceId,
|
|
11612
|
+
dryRun: false,
|
|
11613
|
+
mode: "workspace-queue",
|
|
11614
|
+
contactsQueued: result.selected,
|
|
11615
|
+
started: Boolean(result.launched),
|
|
11616
|
+
agentId: result.agentId ?? null,
|
|
11617
|
+
containerId: result.containerId ?? null,
|
|
11618
|
+
artifacts: {
|
|
11619
|
+
inputSqlPath: null,
|
|
11620
|
+
inputRowsPath: null,
|
|
11621
|
+
directReportPath: null,
|
|
11622
|
+
requestPath,
|
|
11623
|
+
responsePath
|
|
11624
|
+
}
|
|
11625
|
+
});
|
|
11626
|
+
return;
|
|
11627
|
+
}
|
|
10775
11628
|
let clientId;
|
|
10776
11629
|
let queueRows;
|
|
10777
11630
|
let directReportRows = null;
|
package/package.json
CHANGED