salesprompter-cli 0.1.40 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/dist/cli.js +2965 -592
- package/dist/sales-navigator.js +519 -12
- package/package.json +3 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
3
|
import { access, appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
5
4
|
import { createRequire } from "node:module";
|
|
6
5
|
import os from "node:os";
|
|
@@ -9,9 +8,10 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
9
8
|
import { createInterface } from "node:readline/promises";
|
|
10
9
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11
10
|
import { createClient } from "@supabase/supabase-js";
|
|
11
|
+
import pg from "pg";
|
|
12
12
|
import { Command } from "commander";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
-
import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, requireAuthSession, shouldBypassAuth, verifySession, writeAuthSession } from "./auth.js";
|
|
14
|
+
import { clearAuthSession, loginWithBrowserConnect, loginWithDeviceFlow, loginWithToken, readAuthSession, requireAuthSession, shouldBypassAuth, verifySession, writeAuthSession } from "./auth.js";
|
|
15
15
|
import { buildBigQueryLeadLookupSql, executeBigQuerySql, normalizeBigQueryLeadRows, runBigQueryQuery, runBigQueryRows } from "./bigquery.js";
|
|
16
16
|
import { AccountProfileSchema, EnrichedLeadSchema, IcpSchema, LeadSchema, ScoredLeadSchema, SyncTargetSchema } from "./domain.js";
|
|
17
17
|
import { auditDomainDecisions, buildDomainfinderBacklogQueries, buildDomainfinderCandidatesSql, buildDomainfinderInputSql, buildDomainfinderWritebackSql, buildExistingDomainRepairSql, buildExistingDomainAuditQueries, compareDomainSelectionStrategies, selectBestDomains } from "./domainfinder.js";
|
|
@@ -26,11 +26,10 @@ import { InstantlySyncProvider } from "./instantly.js";
|
|
|
26
26
|
import { backfillLinkedInCompanies } from "./linkedin-companies.js";
|
|
27
27
|
import { parseLinkedInCompanyPage } from "./linkedin-companies.js";
|
|
28
28
|
import { crawlLinkedInProductCategory } from "./linkedin-products.js";
|
|
29
|
-
import { claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient,
|
|
30
|
-
import { extractLiAtCookieValue } from "./linkedin-session-contracts.js";
|
|
29
|
+
import { claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient, resolveConfiguredEnvValue } from "./linkedin-session.js";
|
|
31
30
|
import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
|
|
32
31
|
import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
|
|
33
|
-
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
32
|
+
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSearchUrl, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, executeSalesNavigatorAdaptiveCrawl, expandSalesNavigatorCrawlAttempt, SalesNavigatorCrawlStopError, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
34
33
|
import { buildSalesNavigatorHistoricalBackfillPlan, ensureSalesNavigatorPeopleCount, resolveSalesNavigatorHistoricalBackfillConfig, resolveSalesNavigatorHistoricalBackfillResumeState, resolveSalesNavigatorHistoricalBackfillOrgId, salesNavigatorHistoricalBackfillDefaults } from "./salesnav-backfill.js";
|
|
35
34
|
const require = createRequire(import.meta.url);
|
|
36
35
|
const { version: packageVersion } = require("../package.json");
|
|
@@ -129,69 +128,17 @@ const SalesNavigatorLocalImportResponseSchema = z.object({
|
|
|
129
128
|
runId: z.string().min(1),
|
|
130
129
|
containerId: z.string().min(1),
|
|
131
130
|
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
|
|
131
|
+
upserted: z.number().int().nonnegative(),
|
|
132
|
+
leadPoolRefresh: z
|
|
155
133
|
.object({
|
|
156
|
-
|
|
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()
|
|
134
|
+
scheduled: z.literal(true),
|
|
171
135
|
})
|
|
136
|
+
.nullable()
|
|
172
137
|
.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
138
|
});
|
|
191
139
|
const PhantombusterContainersSyncResponseSchema = z.object({
|
|
192
140
|
status: z.literal("ok"),
|
|
193
141
|
agentIds: z.array(z.string().min(1)),
|
|
194
|
-
containerIds: z.array(z.string().min(1)).optional(),
|
|
195
142
|
agents: z.array(z.object({
|
|
196
143
|
agentId: z.string().min(1),
|
|
197
144
|
fetched: z.number().int().nonnegative(),
|
|
@@ -203,13 +150,7 @@ const PhantombusterContainersSyncResponseSchema = z.object({
|
|
|
203
150
|
resultsSynced: z.number().int().nonnegative(),
|
|
204
151
|
outputsStored: z.number().int().nonnegative(),
|
|
205
152
|
resultObjectsStored: z.number().int().nonnegative(),
|
|
206
|
-
resultRowsStored: z.number().int().nonnegative()
|
|
207
|
-
leadListsProjected: z.number().int().nonnegative().optional(),
|
|
208
|
-
leadListContactsProjected: z.number().int().nonnegative().optional(),
|
|
209
|
-
contactsProjected: z.number().int().nonnegative().optional(),
|
|
210
|
-
leadPoolRows: z.number().int().nonnegative().nullable().optional(),
|
|
211
|
-
qualifiedContacts: z.number().int().nonnegative().nullable().optional(),
|
|
212
|
-
qualifiedCompanies: z.number().int().nonnegative().nullable().optional()
|
|
153
|
+
resultRowsStored: z.number().int().nonnegative()
|
|
213
154
|
});
|
|
214
155
|
const CliEmailEnrichmentCompaniesResponseSchema = z.object({
|
|
215
156
|
clientId: z.number().int().positive(),
|
|
@@ -379,6 +320,47 @@ const SalesNavigatorCrawlReportResponseSchema = z.object({
|
|
|
379
320
|
status: z.literal("ok"),
|
|
380
321
|
job: SalesNavigatorCrawlJobSummarySchema
|
|
381
322
|
});
|
|
323
|
+
const SalesNavigatorAccountRunStatusSchema = z.object({
|
|
324
|
+
runId: z.string().uuid(),
|
|
325
|
+
orgId: z.string().min(1),
|
|
326
|
+
sourceQueryUrl: z.string().url(),
|
|
327
|
+
status: z.string().min(1),
|
|
328
|
+
storedAccounts: z.number().int().nonnegative(),
|
|
329
|
+
reportedRootTotal: z.number().int().nonnegative().nullable(),
|
|
330
|
+
missingStoredFromRoot: z.number().int().nonnegative().nullable(),
|
|
331
|
+
lastError: z.string().nullable(),
|
|
332
|
+
updatedAt: z.string().datetime(),
|
|
333
|
+
sliceCounts: z.record(z.string(), z.number().int().nonnegative()),
|
|
334
|
+
workflow: z.object({
|
|
335
|
+
latestRunId: z.string().min(1).nullable(),
|
|
336
|
+
latestStartedAt: z.string().datetime().nullable(),
|
|
337
|
+
}).default({
|
|
338
|
+
latestRunId: null,
|
|
339
|
+
latestStartedAt: null,
|
|
340
|
+
}),
|
|
341
|
+
nextAction: z.enum(["complete", "cooldown", "resume_cli", "inspect"]),
|
|
342
|
+
cooldownUntil: z.string().datetime().nullable(),
|
|
343
|
+
problemSlices: z.array(z.object({
|
|
344
|
+
sliceId: z.string().uuid(),
|
|
345
|
+
status: z.string().min(1),
|
|
346
|
+
totalResults: z.number().int().nonnegative().nullable(),
|
|
347
|
+
storedAccounts: z.number().int().nonnegative(),
|
|
348
|
+
lastError: z.string().nullable(),
|
|
349
|
+
slicedQueryUrl: z.string().url(),
|
|
350
|
+
stopReason: z.string().nullable(),
|
|
351
|
+
nextStart: z.number().int().nonnegative().nullable(),
|
|
352
|
+
missingFromReported: z.number().int().nonnegative().nullable()
|
|
353
|
+
}))
|
|
354
|
+
});
|
|
355
|
+
const SalesNavigatorAccountRunStatusResponseSchema = z.object({
|
|
356
|
+
status: z.literal("ok"),
|
|
357
|
+
run: SalesNavigatorAccountRunStatusSchema
|
|
358
|
+
});
|
|
359
|
+
const SalesNavigatorAccountWorkflowStartResponseSchema = z.object({
|
|
360
|
+
status: z.literal("accepted"),
|
|
361
|
+
workflowRunId: z.string().min(1),
|
|
362
|
+
accountRunId: z.string().uuid()
|
|
363
|
+
});
|
|
382
364
|
const cliPacks = [
|
|
383
365
|
{
|
|
384
366
|
slug: "contacts",
|
|
@@ -391,21 +373,32 @@ const cliPacks = [
|
|
|
391
373
|
slug: "research",
|
|
392
374
|
title: "Research",
|
|
393
375
|
summary: "Scrape markets and enrich companies before outreach.",
|
|
394
|
-
commands: ["market:scrape", "companies:enrich"
|
|
376
|
+
commands: ["market:scrape", "companies:enrich"],
|
|
395
377
|
installStatus: "included"
|
|
396
378
|
},
|
|
397
379
|
{
|
|
398
380
|
slug: "discovery",
|
|
399
381
|
title: "Discovery",
|
|
400
382
|
summary: "Find leads from product and market inputs.",
|
|
401
|
-
commands: [
|
|
383
|
+
commands: [
|
|
384
|
+
"leads:discover",
|
|
385
|
+
"search:run",
|
|
386
|
+
"search:status",
|
|
387
|
+
"search:export",
|
|
388
|
+
"search:count",
|
|
389
|
+
"accounts:split",
|
|
390
|
+
"accounts:local-import",
|
|
391
|
+
"accounts:status",
|
|
392
|
+
"accounts:resume",
|
|
393
|
+
"accounts:repair"
|
|
394
|
+
],
|
|
402
395
|
installStatus: "included"
|
|
403
396
|
},
|
|
404
397
|
{
|
|
405
398
|
slug: "outreach",
|
|
406
399
|
title: "Outreach",
|
|
407
400
|
summary: "Prepare and sync qualified leads into downstream systems.",
|
|
408
|
-
commands: ["
|
|
401
|
+
commands: ["sync:outreach", "sync:crm"],
|
|
409
402
|
installStatus: "included"
|
|
410
403
|
}
|
|
411
404
|
];
|
|
@@ -419,8 +412,12 @@ const helpAliasByCommandName = new Map([
|
|
|
419
412
|
["salesnav:crawl", "search:run"],
|
|
420
413
|
["salesnav:crawl:status", "search:status"],
|
|
421
414
|
["salesnav:export", "search:export"],
|
|
422
|
-
["salesnav:
|
|
423
|
-
["salesnav:
|
|
415
|
+
["salesnav:count", "search:count"],
|
|
416
|
+
["salesnav:accounts:split", "accounts:split"],
|
|
417
|
+
["salesnav:accounts:local-import", "accounts:local-import"],
|
|
418
|
+
["salesnav:accounts:status", "accounts:status"],
|
|
419
|
+
["salesnav:accounts:resume", "accounts:resume"],
|
|
420
|
+
["salesnav:accounts:repair", "accounts:repair"]
|
|
424
421
|
]);
|
|
425
422
|
const helpVisibleCommandNames = new Set([
|
|
426
423
|
"setup",
|
|
@@ -443,9 +440,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
443
440
|
"leads:generate",
|
|
444
441
|
"leads:enrich",
|
|
445
442
|
"leads:score",
|
|
446
|
-
"leads:process",
|
|
447
443
|
"leads:pipeline",
|
|
448
|
-
"affiliate:launch",
|
|
449
444
|
"sync:crm",
|
|
450
445
|
"sync:outreach",
|
|
451
446
|
"linkedin-companies:backfill",
|
|
@@ -454,8 +449,12 @@ const helpVisibleCommandNames = new Set([
|
|
|
454
449
|
"salesnav:crawl",
|
|
455
450
|
"salesnav:crawl:status",
|
|
456
451
|
"salesnav:export",
|
|
457
|
-
"salesnav:
|
|
458
|
-
"salesnav:
|
|
452
|
+
"salesnav:count",
|
|
453
|
+
"salesnav:accounts:split",
|
|
454
|
+
"salesnav:accounts:local-import",
|
|
455
|
+
"salesnav:accounts:status",
|
|
456
|
+
"salesnav:accounts:resume",
|
|
457
|
+
"salesnav:accounts:repair"
|
|
459
458
|
]);
|
|
460
459
|
function printOutput(value) {
|
|
461
460
|
if (runtimeOutputOptions.quiet) {
|
|
@@ -1382,22 +1381,12 @@ async function listChromeExtensionTokenLogCandidates() {
|
|
|
1382
1381
|
if (!file.endsWith(".log")) {
|
|
1383
1382
|
continue;
|
|
1384
1383
|
}
|
|
1385
|
-
|
|
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 });
|
|
1384
|
+
paths.push(path.join(extensionDir, file));
|
|
1394
1385
|
}
|
|
1395
1386
|
}
|
|
1396
1387
|
}
|
|
1397
1388
|
}
|
|
1398
|
-
return paths
|
|
1399
|
-
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path))
|
|
1400
|
-
.map((entry) => entry.path);
|
|
1389
|
+
return paths.sort().reverse();
|
|
1401
1390
|
}
|
|
1402
1391
|
async function readLocalLinkedInExtensionDirectLookupConfig() {
|
|
1403
1392
|
const candidates = await listChromeExtensionTokenLogCandidates();
|
|
@@ -1436,6 +1425,16 @@ function readLinkedInDirectLookupEnvConfig() {
|
|
|
1436
1425
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
1437
1426
|
};
|
|
1438
1427
|
}
|
|
1428
|
+
function shouldDisableLinkedInDirectLookupAutodiscovery() {
|
|
1429
|
+
const value = process.env.SALESPROMPTER_DISABLE_LINKEDIN_DIRECT_LOOKUP_AUTODISCOVERY ??
|
|
1430
|
+
process.env.LINKEDIN_DISABLE_DIRECT_LOOKUP_AUTODISCOVERY ??
|
|
1431
|
+
"";
|
|
1432
|
+
return ["1", "true", "on", "yes"].includes(value.trim().toLowerCase());
|
|
1433
|
+
}
|
|
1434
|
+
function isExplicitLinkedInSessionVaultEnabled() {
|
|
1435
|
+
const value = process.env.SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS ?? "";
|
|
1436
|
+
return ["1", "true", "on", "yes"].includes(value.trim().toLowerCase());
|
|
1437
|
+
}
|
|
1439
1438
|
function extractStoredLinkedInLookupValue(metadata, key) {
|
|
1440
1439
|
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
1441
1440
|
return "";
|
|
@@ -1495,18 +1494,24 @@ async function readLinkedInDirectLookupConfig() {
|
|
|
1495
1494
|
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1496
1495
|
return envConfig;
|
|
1497
1496
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1497
|
+
try {
|
|
1498
|
+
const storedConfig = await readStoredLinkedInDirectLookupConfig();
|
|
1499
|
+
if (storedConfig) {
|
|
1500
|
+
cachedLinkedInDirectLookupConfig = storedConfig;
|
|
1501
|
+
return storedConfig;
|
|
1502
|
+
}
|
|
1500
1503
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
if (isExplicitLinkedInSessionVaultEnabled()) {
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1505
1508
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1509
|
+
if (!shouldDisableLinkedInDirectLookupAutodiscovery()) {
|
|
1510
|
+
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
1511
|
+
if (localExtensionConfig) {
|
|
1512
|
+
cachedLinkedInDirectLookupConfig = localExtensionConfig;
|
|
1513
|
+
return localExtensionConfig;
|
|
1514
|
+
}
|
|
1510
1515
|
}
|
|
1511
1516
|
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.");
|
|
1512
1517
|
}
|
|
@@ -1997,25 +2002,198 @@ function scoreLinkedInSalesApiElementMatch(contact, element) {
|
|
|
1997
2002
|
companyMatchCount
|
|
1998
2003
|
};
|
|
1999
2004
|
}
|
|
2000
|
-
function extractLinkedInCompanyEmployeeCountFromSalesApiElement(element) {
|
|
2005
|
+
function extractLinkedInCompanyEmployeeCountFromSalesApiElement(element, options = {}) {
|
|
2006
|
+
if (!element) {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
for (const key of ["employeeDisplayCount", "employeesOnLinkedInCount"]) {
|
|
2010
|
+
const value = element[key];
|
|
2011
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2012
|
+
return Math.max(0, Math.trunc(value));
|
|
2013
|
+
}
|
|
2014
|
+
if (typeof value === "string") {
|
|
2015
|
+
const parsed = parseLinkedInCompactNumber(value);
|
|
2016
|
+
if (parsed != null) {
|
|
2017
|
+
return parsed;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (options.allowTextFallback) {
|
|
2022
|
+
for (const value of collectNestedStrings(element)) {
|
|
2023
|
+
const match = value.match(/(\d[\d.,]*)\s+employees\b/i);
|
|
2024
|
+
if (match) {
|
|
2025
|
+
return Number(match[1].replace(/[.,]/g, ""));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
function extractLinkedInCompanyNumericTextFromSalesApiElement(element, directKeys, pattern) {
|
|
2001
2032
|
if (!element) {
|
|
2002
2033
|
return null;
|
|
2003
2034
|
}
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
typeof
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2035
|
+
for (const key of directKeys) {
|
|
2036
|
+
const value = element[key];
|
|
2037
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2038
|
+
return Math.max(0, Math.trunc(value));
|
|
2039
|
+
}
|
|
2040
|
+
if (typeof value === "string") {
|
|
2041
|
+
const parsed = parseLinkedInCompactNumber(value);
|
|
2042
|
+
if (parsed != null) {
|
|
2043
|
+
return parsed;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2010
2046
|
}
|
|
2011
2047
|
for (const value of collectNestedStrings(element)) {
|
|
2012
|
-
const match = value.match(
|
|
2013
|
-
if (match) {
|
|
2014
|
-
|
|
2048
|
+
const match = value.match(pattern);
|
|
2049
|
+
if (match?.[1]) {
|
|
2050
|
+
const parsed = parseLinkedInCompactNumber(match[1]);
|
|
2051
|
+
if (parsed != null) {
|
|
2052
|
+
return parsed;
|
|
2053
|
+
}
|
|
2015
2054
|
}
|
|
2016
2055
|
}
|
|
2017
2056
|
return null;
|
|
2018
2057
|
}
|
|
2058
|
+
function parseLinkedInCompactNumber(value) {
|
|
2059
|
+
const normalized = value.trim().replace(/,/g, "");
|
|
2060
|
+
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*([kmb])?\+?$/i);
|
|
2061
|
+
if (!match) {
|
|
2062
|
+
const digits = normalized.match(/\d[\d.]*/)?.[0];
|
|
2063
|
+
return digits ? Number(digits.replace(/\./g, "")) : null;
|
|
2064
|
+
}
|
|
2065
|
+
const amount = Number(match[1]);
|
|
2066
|
+
if (!Number.isFinite(amount)) {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
const suffix = match[2]?.toLowerCase();
|
|
2070
|
+
const multiplier = suffix === "b" ? 1_000_000_000 : suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
|
|
2071
|
+
return Math.max(0, Math.trunc(amount * multiplier));
|
|
2072
|
+
}
|
|
2073
|
+
function extractFirstNestedStringByKeys(element, keys) {
|
|
2074
|
+
if (!element) {
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
for (const key of keys) {
|
|
2078
|
+
const direct = element[key];
|
|
2079
|
+
if (typeof direct === "string" && normalizeLookupWhitespace(direct)) {
|
|
2080
|
+
return normalizeLookupWhitespace(direct);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
const normalizedKeys = new Set(keys.map((key) => key.toLowerCase()));
|
|
2084
|
+
const visit = (value, seen = new Set()) => {
|
|
2085
|
+
if (value == null || seen.has(value) || typeof value !== "object") {
|
|
2086
|
+
return null;
|
|
2087
|
+
}
|
|
2088
|
+
seen.add(value);
|
|
2089
|
+
if (Array.isArray(value)) {
|
|
2090
|
+
for (const item of value) {
|
|
2091
|
+
const found = visit(item, seen);
|
|
2092
|
+
if (found) {
|
|
2093
|
+
return found;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
const record = value;
|
|
2099
|
+
for (const [key, candidate] of Object.entries(record)) {
|
|
2100
|
+
if (normalizedKeys.has(key.toLowerCase()) &&
|
|
2101
|
+
typeof candidate === "string" &&
|
|
2102
|
+
normalizeLookupWhitespace(candidate)) {
|
|
2103
|
+
return normalizeLookupWhitespace(candidate);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
for (const candidate of Object.values(record)) {
|
|
2107
|
+
const found = visit(candidate, seen);
|
|
2108
|
+
if (found) {
|
|
2109
|
+
return found;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
};
|
|
2114
|
+
return visit(element);
|
|
2115
|
+
}
|
|
2116
|
+
function extractLinkedInCompanyIdFromUrl(value) {
|
|
2117
|
+
if (!value) {
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
const match = value.match(/\/(?:sales\/company|company)\/([^/?#]+)/i);
|
|
2121
|
+
return match?.[1] ? decodeURIComponent(match[1]) : null;
|
|
2122
|
+
}
|
|
2123
|
+
function extractLinkedInSalesCompanyIdFromUrn(value) {
|
|
2124
|
+
if (typeof value !== "string") {
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
const directMatch = value.match(/urn:li:fs_salesCompany:(\d+)\b/i);
|
|
2128
|
+
if (directMatch?.[1]) {
|
|
2129
|
+
return directMatch[1];
|
|
2130
|
+
}
|
|
2131
|
+
const tupleMatch = value.match(/urn:li:fs_salesCompany:\((\d+),/i);
|
|
2132
|
+
return tupleMatch?.[1] ?? null;
|
|
2133
|
+
}
|
|
2134
|
+
function normalizeLocalSalesNavigatorAccount(element, queryUrl) {
|
|
2135
|
+
const urnCompanyId = extractLinkedInSalesCompanyIdFromUrn(element.entityUrn);
|
|
2136
|
+
const explicitSalesNavCompanyUrl = extractLinkedInSalesNavCompanyUrlFromSalesApiElement(element);
|
|
2137
|
+
const explicitLinkedinCompanyUrl = extractLinkedInCompanyUrlFromSalesApiElement(element);
|
|
2138
|
+
const companyName = extractLinkedInCompanyNameFromSalesApiElement(element);
|
|
2139
|
+
const companyId = extractLinkedInCompanyIdFromUrl(explicitSalesNavCompanyUrl) ??
|
|
2140
|
+
extractLinkedInCompanyIdFromUrl(explicitLinkedinCompanyUrl) ??
|
|
2141
|
+
urnCompanyId;
|
|
2142
|
+
const salesNavCompanyUrl = explicitSalesNavCompanyUrl ??
|
|
2143
|
+
(companyId ? `https://www.linkedin.com/sales/company/${companyId}` : null);
|
|
2144
|
+
const linkedinCompanyUrl = explicitLinkedinCompanyUrl ??
|
|
2145
|
+
(companyId ? normalizeLinkedInCompanyPage(companyId) : null);
|
|
2146
|
+
const accountKey = (companyId ? `company-id:${companyId}` : null) ??
|
|
2147
|
+
salesNavCompanyUrl?.toLowerCase() ??
|
|
2148
|
+
linkedinCompanyUrl?.toLowerCase() ??
|
|
2149
|
+
(companyName ? `company-name:${normalizeLooseMatchText(companyName)}` : null);
|
|
2150
|
+
if (!accountKey) {
|
|
2151
|
+
return null;
|
|
2152
|
+
}
|
|
2153
|
+
return {
|
|
2154
|
+
accountKey,
|
|
2155
|
+
salesNavCompanyUrl,
|
|
2156
|
+
linkedinCompanyUrl,
|
|
2157
|
+
companyId,
|
|
2158
|
+
companyName,
|
|
2159
|
+
industry: extractFirstNestedStringByKeys(element, ["industry", "industryName"]),
|
|
2160
|
+
location: extractFirstNestedStringByKeys(element, ["location", "locationName", "geoRegion"]),
|
|
2161
|
+
headquarters: extractFirstNestedStringByKeys(element, ["headquarters", "headquartersLocation"]),
|
|
2162
|
+
employeeCount: extractLinkedInCompanyEmployeeCountFromSalesApiElement(element),
|
|
2163
|
+
followerCount: extractLinkedInCompanyNumericTextFromSalesApiElement(element, ["followerCount", "followersCount", "numFollowers"], /(\d[\d.,kmbKMB+]*)\s+followers?\b/i),
|
|
2164
|
+
companyType: extractFirstNestedStringByKeys(element, ["companyType", "type"]),
|
|
2165
|
+
description: extractFirstNestedStringByKeys(element, ["description", "summary", "tagline"]),
|
|
2166
|
+
website: extractFirstNestedStringByKeys(element, ["website", "websiteUrl", "companyWebsite"]),
|
|
2167
|
+
searchQueryUrl: queryUrl,
|
|
2168
|
+
scrapedAt: new Date().toISOString(),
|
|
2169
|
+
rawLocalSalesNavigatorAccountResult: element,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function normalizeLocalSalesNavigatorAccounts(responseBody, queryUrl) {
|
|
2173
|
+
if (typeof responseBody !== "object" ||
|
|
2174
|
+
responseBody === null ||
|
|
2175
|
+
Array.isArray(responseBody)) {
|
|
2176
|
+
return [];
|
|
2177
|
+
}
|
|
2178
|
+
const elements = responseBody.elements;
|
|
2179
|
+
if (!Array.isArray(elements)) {
|
|
2180
|
+
return [];
|
|
2181
|
+
}
|
|
2182
|
+
const accounts = [];
|
|
2183
|
+
const seen = new Set();
|
|
2184
|
+
for (const element of elements) {
|
|
2185
|
+
if (typeof element !== "object" || element === null || Array.isArray(element)) {
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
const account = normalizeLocalSalesNavigatorAccount(element, queryUrl);
|
|
2189
|
+
if (!account || seen.has(account.accountKey)) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
seen.add(account.accountKey);
|
|
2193
|
+
accounts.push(account);
|
|
2194
|
+
}
|
|
2195
|
+
return accounts;
|
|
2196
|
+
}
|
|
2019
2197
|
function buildLinkedInCompanyLookupVariants(params) {
|
|
2020
2198
|
const variants = [];
|
|
2021
2199
|
const seen = new Set();
|
|
@@ -2125,7 +2303,9 @@ async function resolveDirectLinkedInCompanyContexts(params) {
|
|
|
2125
2303
|
matchedCompanyUrl = companyUrl ?? matchedCompanyUrl;
|
|
2126
2304
|
matchedSalesNavCompanyUrl = salesNavCompanyUrl ?? matchedSalesNavCompanyUrl;
|
|
2127
2305
|
matchedCompanyName = companyName ?? matchedCompanyName;
|
|
2128
|
-
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first
|
|
2306
|
+
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first, {
|
|
2307
|
+
allowTextFallback: true
|
|
2308
|
+
});
|
|
2129
2309
|
addAlias(companyName);
|
|
2130
2310
|
addAlias(companyUrl ? normalizeLinkedInCompanyHandle(companyUrl)?.replace(/[-_]+/g, " ") : null);
|
|
2131
2311
|
addAlias(salesNavCompanyUrl ? normalizeLookupWhitespace(salesNavCompanyUrl.split("/sales/company/")[1]?.split(/[/?#]/)[0] ?? "") : null);
|
|
@@ -3320,7 +3500,9 @@ async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
|
3320
3500
|
matchedCompanyUrl = companyUrl;
|
|
3321
3501
|
matchedSalesNavCompanyUrl = salesNavCompanyUrl;
|
|
3322
3502
|
matchedCompanyName = extractLinkedInCompanyNameFromSalesApiElement(first);
|
|
3323
|
-
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first
|
|
3503
|
+
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first, {
|
|
3504
|
+
allowTextFallback: true
|
|
3505
|
+
});
|
|
3324
3506
|
break;
|
|
3325
3507
|
}
|
|
3326
3508
|
}
|
|
@@ -4233,6 +4415,11 @@ function resolveSessionOrgId(session) {
|
|
|
4233
4415
|
const orgId = session.user.orgId?.trim();
|
|
4234
4416
|
return orgId && orgId.length > 0 ? orgId : null;
|
|
4235
4417
|
}
|
|
4418
|
+
function resolveAccountSearchStorageOrgId(session, explicitOrgId) {
|
|
4419
|
+
return (compactOptionalText(explicitOrgId) ??
|
|
4420
|
+
compactOptionalText(process.env.SALESPROMPTER_ORG_ID) ??
|
|
4421
|
+
(session ? resolveSessionOrgId(session) : null));
|
|
4422
|
+
}
|
|
4236
4423
|
function normalizeCliApiBaseUrl(value) {
|
|
4237
4424
|
return value.trim().replace(/\/+$/, "");
|
|
4238
4425
|
}
|
|
@@ -5791,11 +5978,18 @@ async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
|
5791
5978
|
return value;
|
|
5792
5979
|
}
|
|
5793
5980
|
class LocalSalesNavigatorRateLimitError extends Error {
|
|
5794
|
-
|
|
5981
|
+
status;
|
|
5982
|
+
retryAfterMs;
|
|
5983
|
+
constructor(status, bodyPreview, retryAfterMs) {
|
|
5795
5984
|
super(`Sales Navigator request stopped because LinkedIn returned HTTP ${status}. ` +
|
|
5796
5985
|
"Use LinkedIn directly in a supported browser and wait before trying again. " +
|
|
5986
|
+
(retryAfterMs != null
|
|
5987
|
+
? `Retry after approximately ${Math.ceil(retryAfterMs / 1000)} seconds. `
|
|
5988
|
+
: "") +
|
|
5797
5989
|
`Response preview: ${bodyPreview}`);
|
|
5798
5990
|
this.name = "LocalSalesNavigatorRateLimitError";
|
|
5991
|
+
this.status = status;
|
|
5992
|
+
this.retryAfterMs = retryAfterMs;
|
|
5799
5993
|
}
|
|
5800
5994
|
}
|
|
5801
5995
|
function stripShellQuotes(value) {
|
|
@@ -5874,7 +6068,7 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
|
5874
6068
|
if (source.pathname.includes("/sales-api/salesApiLeadSearch")) {
|
|
5875
6069
|
return {
|
|
5876
6070
|
url: source.toString(),
|
|
5877
|
-
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config)
|
|
6071
|
+
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config),
|
|
5878
6072
|
};
|
|
5879
6073
|
}
|
|
5880
6074
|
if (!source.pathname.includes("/sales/search/people")) {
|
|
@@ -5886,7 +6080,8 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
|
5886
6080
|
}
|
|
5887
6081
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
5888
6082
|
`${source.protocol}//${source.host}`;
|
|
5889
|
-
const rawSessionId = extractRawUrlSearchParam(source, "sessionId") ??
|
|
6083
|
+
const rawSessionId = extractRawUrlSearchParam(source, "sessionId") ??
|
|
6084
|
+
encodeURIComponent(generateLinkedInSessionId());
|
|
5890
6085
|
const apiQuery = normalizeSalesNavigatorApiQueryValue(rawQuery);
|
|
5891
6086
|
const normalizedCount = Math.max(1, Math.min(100, Math.trunc(count)));
|
|
5892
6087
|
const apiUrl = `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiLeadSearch` +
|
|
@@ -5895,7 +6090,337 @@ function buildSalesNavigatorApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
|
5895
6090
|
"&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14";
|
|
5896
6091
|
return {
|
|
5897
6092
|
url: apiUrl,
|
|
5898
|
-
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config)
|
|
6093
|
+
headers: buildLinkedInSalesNavigatorLocalImportHeaders(config),
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
6096
|
+
function extractSalesNavigatorDecorationId(requestUrl, fallbackDecorationId) {
|
|
6097
|
+
try {
|
|
6098
|
+
const url = new URL(requestUrl);
|
|
6099
|
+
return (extractRawUrlSearchParam(url, "decorationId") ??
|
|
6100
|
+
fallbackDecorationId);
|
|
6101
|
+
}
|
|
6102
|
+
catch {
|
|
6103
|
+
return fallbackDecorationId;
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
function buildSalesNavigatorAccountApiUrlFromSearchUrl(searchUrl, count, options = {}) {
|
|
6107
|
+
const source = new URL(searchUrl);
|
|
6108
|
+
if (source.pathname.includes("/sales-api/salesApiAccountSearch")) {
|
|
6109
|
+
return source.toString();
|
|
6110
|
+
}
|
|
6111
|
+
if (!source.pathname.includes("/sales/search/company")) {
|
|
6112
|
+
throw new Error("Automatic Sales Navigator account splitting requires a /sales/search/company URL.");
|
|
6113
|
+
}
|
|
6114
|
+
const rawQuery = extractRawUrlSearchParam(source, "query");
|
|
6115
|
+
if (!rawQuery) {
|
|
6116
|
+
throw new Error("Sales Navigator account search URL is missing the query parameter.");
|
|
6117
|
+
}
|
|
6118
|
+
const baseUrl = compactOptionalText(options.baseUrl) ??
|
|
6119
|
+
compactOptionalText(process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL) ??
|
|
6120
|
+
`${source.protocol}//${source.host}`;
|
|
6121
|
+
const rawSessionId = extractRawUrlSearchParam(source, "sessionId") ??
|
|
6122
|
+
encodeURIComponent(generateLinkedInSessionId());
|
|
6123
|
+
const apiQuery = normalizeSalesNavigatorApiQueryValue(rawQuery);
|
|
6124
|
+
const normalizedCount = Math.max(1, Math.min(100, Math.trunc(count)));
|
|
6125
|
+
const decorationId = compactOptionalText(options.decorationId) ??
|
|
6126
|
+
"com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-14";
|
|
6127
|
+
return (`${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiAccountSearch` +
|
|
6128
|
+
`?q=searchQuery&query=${apiQuery}&start=0&count=${normalizedCount}` +
|
|
6129
|
+
`&trackingParam=(sessionId:${rawSessionId})` +
|
|
6130
|
+
`&decorationId=${decorationId}`);
|
|
6131
|
+
}
|
|
6132
|
+
function buildSalesNavigatorAccountApiRequestFromSearchUrl(searchUrl, config, count) {
|
|
6133
|
+
const source = new URL(searchUrl);
|
|
6134
|
+
if (source.pathname.includes("/sales-api/salesApiAccountSearch")) {
|
|
6135
|
+
return {
|
|
6136
|
+
url: source.toString(),
|
|
6137
|
+
headers: buildLinkedInSalesNavigatorAccountSearchHeaders(config),
|
|
6138
|
+
};
|
|
6139
|
+
}
|
|
6140
|
+
return {
|
|
6141
|
+
url: buildSalesNavigatorAccountApiUrlFromSearchUrl(searchUrl, count),
|
|
6142
|
+
headers: buildLinkedInSalesNavigatorAccountSearchHeaders(config),
|
|
6143
|
+
};
|
|
6144
|
+
}
|
|
6145
|
+
function buildSalesNavigatorAccountApiRequestFromSearchUrlWithTemplate(searchUrl, templateRequest, count) {
|
|
6146
|
+
const templateUrl = new URL(templateRequest.url);
|
|
6147
|
+
return {
|
|
6148
|
+
url: buildSalesNavigatorAccountApiUrlFromSearchUrl(searchUrl, count, {
|
|
6149
|
+
baseUrl: `${templateUrl.protocol}//${templateUrl.host}`,
|
|
6150
|
+
decorationId: extractSalesNavigatorDecorationId(templateRequest.url, "com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-14"),
|
|
6151
|
+
}),
|
|
6152
|
+
headers: { ...templateRequest.headers },
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
6155
|
+
const SALES_NAVIGATOR_PROFILE_LANGUAGE_VALUES = new Map([
|
|
6156
|
+
["de", { id: "de", text: "German", selectionType: "INCLUDED" }],
|
|
6157
|
+
["german", { id: "de", text: "German", selectionType: "INCLUDED" }],
|
|
6158
|
+
["en", { id: "en", text: "English", selectionType: "INCLUDED" }],
|
|
6159
|
+
["english", { id: "en", text: "English", selectionType: "INCLUDED" }],
|
|
6160
|
+
]);
|
|
6161
|
+
const SALES_NAVIGATOR_SENIORITY_VALUES = new Map([
|
|
6162
|
+
[
|
|
6163
|
+
"owner / partner",
|
|
6164
|
+
{ id: "320", text: "Owner / Partner", selectionType: "INCLUDED" },
|
|
6165
|
+
],
|
|
6166
|
+
["owner", { id: "320", text: "Owner / Partner", selectionType: "INCLUDED" }],
|
|
6167
|
+
[
|
|
6168
|
+
"partner",
|
|
6169
|
+
{ id: "320", text: "Owner / Partner", selectionType: "INCLUDED" },
|
|
6170
|
+
],
|
|
6171
|
+
["cxo", { id: "310", text: "CXO", selectionType: "INCLUDED" }],
|
|
6172
|
+
[
|
|
6173
|
+
"vice president",
|
|
6174
|
+
{ id: "300", text: "Vice President", selectionType: "INCLUDED" },
|
|
6175
|
+
],
|
|
6176
|
+
["vp", { id: "300", text: "Vice President", selectionType: "INCLUDED" }],
|
|
6177
|
+
["director", { id: "220", text: "Director", selectionType: "INCLUDED" }],
|
|
6178
|
+
[
|
|
6179
|
+
"experienced manager",
|
|
6180
|
+
{ id: "210", text: "Experienced Manager", selectionType: "INCLUDED" },
|
|
6181
|
+
],
|
|
6182
|
+
[
|
|
6183
|
+
"manager",
|
|
6184
|
+
{ id: "210", text: "Experienced Manager", selectionType: "INCLUDED" },
|
|
6185
|
+
],
|
|
6186
|
+
["senior", { id: "120", text: "Senior", selectionType: "INCLUDED" }],
|
|
6187
|
+
[
|
|
6188
|
+
"entry level",
|
|
6189
|
+
{ id: "110", text: "Entry Level", selectionType: "INCLUDED" },
|
|
6190
|
+
],
|
|
6191
|
+
["entry", { id: "110", text: "Entry Level", selectionType: "INCLUDED" }],
|
|
6192
|
+
]);
|
|
6193
|
+
const SALES_NAVIGATOR_FUNCTION_VALUES = new Map([
|
|
6194
|
+
[
|
|
6195
|
+
"human resources",
|
|
6196
|
+
{ id: "12", text: "Human Resources", selectionType: "INCLUDED" },
|
|
6197
|
+
],
|
|
6198
|
+
["hr", { id: "12", text: "Human Resources", selectionType: "INCLUDED" }],
|
|
6199
|
+
]);
|
|
6200
|
+
const SALES_NAVIGATOR_COMPANY_TYPE_VALUES = new Map([
|
|
6201
|
+
[
|
|
6202
|
+
"privately held",
|
|
6203
|
+
{ id: "P", text: "Privately Held", selectionType: "INCLUDED" },
|
|
6204
|
+
],
|
|
6205
|
+
["private", { id: "P", text: "Privately Held", selectionType: "INCLUDED" }],
|
|
6206
|
+
[
|
|
6207
|
+
"public company",
|
|
6208
|
+
{ id: "C", text: "Public Company", selectionType: "INCLUDED" },
|
|
6209
|
+
],
|
|
6210
|
+
["public", { id: "C", text: "Public Company", selectionType: "INCLUDED" }],
|
|
6211
|
+
]);
|
|
6212
|
+
const SALES_NAVIGATOR_COMPANY_HEADCOUNT_VALUES = new Map([
|
|
6213
|
+
["1-10", { id: "B", text: "1-10", selectionType: "INCLUDED" }],
|
|
6214
|
+
["11-50", { id: "C", text: "11-50", selectionType: "INCLUDED" }],
|
|
6215
|
+
["51-200", { id: "D", text: "51-200", selectionType: "INCLUDED" }],
|
|
6216
|
+
["201-500", { id: "E", text: "201-500", selectionType: "INCLUDED" }],
|
|
6217
|
+
["501-1000", { id: "F", text: "501-1000", selectionType: "INCLUDED" }],
|
|
6218
|
+
["1001-5000", { id: "G", text: "1001-5000", selectionType: "INCLUDED" }],
|
|
6219
|
+
["5001-10,000", { id: "H", text: "5001-10,000", selectionType: "INCLUDED" }],
|
|
6220
|
+
["5001-10000", { id: "H", text: "5001-10,000", selectionType: "INCLUDED" }],
|
|
6221
|
+
["10,000+", { id: "I", text: "10,000+", selectionType: "INCLUDED" }],
|
|
6222
|
+
["10000+", { id: "I", text: "10,000+", selectionType: "INCLUDED" }],
|
|
6223
|
+
]);
|
|
6224
|
+
const SALES_NAVIGATOR_TENURE_VALUES = new Map([
|
|
6225
|
+
[
|
|
6226
|
+
"less than 1 year",
|
|
6227
|
+
{ id: "1", text: "Less than 1 year", selectionType: "INCLUDED" },
|
|
6228
|
+
],
|
|
6229
|
+
[
|
|
6230
|
+
"1 to 2 years",
|
|
6231
|
+
{ id: "2", text: "1 to 2 years", selectionType: "INCLUDED" },
|
|
6232
|
+
],
|
|
6233
|
+
["1-2 years", { id: "2", text: "1 to 2 years", selectionType: "INCLUDED" }],
|
|
6234
|
+
[
|
|
6235
|
+
"3 to 5 years",
|
|
6236
|
+
{ id: "3", text: "3 to 5 years", selectionType: "INCLUDED" },
|
|
6237
|
+
],
|
|
6238
|
+
["3-5 years", { id: "3", text: "3 to 5 years", selectionType: "INCLUDED" }],
|
|
6239
|
+
[
|
|
6240
|
+
"6 to 10 years",
|
|
6241
|
+
{ id: "4", text: "6 to 10 years", selectionType: "INCLUDED" },
|
|
6242
|
+
],
|
|
6243
|
+
["6-10 years", { id: "4", text: "6 to 10 years", selectionType: "INCLUDED" }],
|
|
6244
|
+
[
|
|
6245
|
+
"more than 10 years",
|
|
6246
|
+
{ id: "5", text: "More than 10 years", selectionType: "INCLUDED" },
|
|
6247
|
+
],
|
|
6248
|
+
[
|
|
6249
|
+
"10+ years",
|
|
6250
|
+
{ id: "5", text: "More than 10 years", selectionType: "INCLUDED" },
|
|
6251
|
+
],
|
|
6252
|
+
]);
|
|
6253
|
+
const SALES_NAVIGATOR_REGION_VALUES = new Map([
|
|
6254
|
+
["europe", { id: "100506914", text: "Europe", selectionType: "INCLUDED" }],
|
|
6255
|
+
["germany", { id: "101282230", text: "Germany", selectionType: "INCLUDED" }],
|
|
6256
|
+
]);
|
|
6257
|
+
const SALES_NAVIGATOR_INDUSTRY_VALUES = new Map([
|
|
6258
|
+
[
|
|
6259
|
+
"architecture and planning",
|
|
6260
|
+
{ id: "50", text: "Architecture and Planning", selectionType: "INCLUDED" },
|
|
6261
|
+
],
|
|
6262
|
+
[
|
|
6263
|
+
"machinery manufacturing",
|
|
6264
|
+
{ id: "55", text: "Machinery Manufacturing", selectionType: "INCLUDED" },
|
|
6265
|
+
],
|
|
6266
|
+
]);
|
|
6267
|
+
function normalizeLeadParameterKey(value) {
|
|
6268
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6269
|
+
}
|
|
6270
|
+
function splitName(fullName) {
|
|
6271
|
+
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
|
6272
|
+
return {
|
|
6273
|
+
firstName: parts[0] ?? "",
|
|
6274
|
+
lastName: parts.slice(1).join(" "),
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
function buildGermanNameVariants(value) {
|
|
6278
|
+
const trimmed = value.trim();
|
|
6279
|
+
if (!trimmed) {
|
|
6280
|
+
return [];
|
|
6281
|
+
}
|
|
6282
|
+
const variants = new Set([trimmed]);
|
|
6283
|
+
const replacements = [
|
|
6284
|
+
[/ae/g, "ä"],
|
|
6285
|
+
[/Ae/g, "Ä"],
|
|
6286
|
+
[/oe/g, "ö"],
|
|
6287
|
+
[/Oe/g, "Ö"],
|
|
6288
|
+
[/ue/g, "ü"],
|
|
6289
|
+
[/Ue/g, "Ü"],
|
|
6290
|
+
[/ss/g, "ß"],
|
|
6291
|
+
];
|
|
6292
|
+
let current = trimmed;
|
|
6293
|
+
for (const [pattern, replacement] of replacements) {
|
|
6294
|
+
current = current.replace(pattern, replacement);
|
|
6295
|
+
}
|
|
6296
|
+
variants.add(current);
|
|
6297
|
+
return [...variants].filter((variant) => variant.length > 0);
|
|
6298
|
+
}
|
|
6299
|
+
function parseSalesNavigatorValueSpec(spec, lookup) {
|
|
6300
|
+
const trimmed = spec.trim();
|
|
6301
|
+
if (!trimmed) {
|
|
6302
|
+
throw new Error("Filter value cannot be empty.");
|
|
6303
|
+
}
|
|
6304
|
+
const mapped = lookup?.get(normalizeLeadParameterKey(trimmed));
|
|
6305
|
+
if (mapped) {
|
|
6306
|
+
return { ...mapped };
|
|
6307
|
+
}
|
|
6308
|
+
const pipeIndex = trimmed.indexOf("|");
|
|
6309
|
+
if (pipeIndex > 0 && trimmed.slice(pipeIndex + 1).trim()) {
|
|
6310
|
+
return {
|
|
6311
|
+
id: trimmed.slice(0, pipeIndex).trim(),
|
|
6312
|
+
text: trimmed.slice(pipeIndex + 1).trim(),
|
|
6313
|
+
selectionType: "INCLUDED",
|
|
6314
|
+
};
|
|
6315
|
+
}
|
|
6316
|
+
const parts = trimmed.split(":");
|
|
6317
|
+
if (parts.length >= 2 &&
|
|
6318
|
+
parts[0]?.trim() &&
|
|
6319
|
+
parts.slice(1).join(":").trim()) {
|
|
6320
|
+
return {
|
|
6321
|
+
id: parts[0]?.trim(),
|
|
6322
|
+
text: parts.slice(1).join(":").trim(),
|
|
6323
|
+
selectionType: "INCLUDED",
|
|
6324
|
+
};
|
|
6325
|
+
}
|
|
6326
|
+
return {
|
|
6327
|
+
text: trimmed,
|
|
6328
|
+
selectionType: "INCLUDED",
|
|
6329
|
+
};
|
|
6330
|
+
}
|
|
6331
|
+
function parseGenericLeadParameterFilter(spec) {
|
|
6332
|
+
const separatorIndex = spec.indexOf(":");
|
|
6333
|
+
if (separatorIndex <= 0) {
|
|
6334
|
+
throw new Error(`Invalid --filter value "${spec}". Use TYPE:text or TYPE:id:text.`);
|
|
6335
|
+
}
|
|
6336
|
+
const type = spec.slice(0, separatorIndex).trim().toUpperCase();
|
|
6337
|
+
const valueSpec = spec.slice(separatorIndex + 1).trim();
|
|
6338
|
+
if (!type || !valueSpec) {
|
|
6339
|
+
throw new Error(`Invalid --filter value "${spec}". Use TYPE:text or TYPE:id:text.`);
|
|
6340
|
+
}
|
|
6341
|
+
return {
|
|
6342
|
+
type,
|
|
6343
|
+
values: [parseSalesNavigatorValueSpec(valueSpec)],
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
function buildSingleValueLeadParameterFilter(type, value, lookup) {
|
|
6347
|
+
const trimmed = value?.trim();
|
|
6348
|
+
if (!trimmed) {
|
|
6349
|
+
return null;
|
|
6350
|
+
}
|
|
6351
|
+
return {
|
|
6352
|
+
type,
|
|
6353
|
+
values: [parseSalesNavigatorValueSpec(trimmed, lookup)],
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6356
|
+
function appendSalesNavigatorKeywords(searchUrl, keywords) {
|
|
6357
|
+
const trimmed = keywords?.trim();
|
|
6358
|
+
if (!trimmed) {
|
|
6359
|
+
return searchUrl;
|
|
6360
|
+
}
|
|
6361
|
+
const url = new URL(searchUrl);
|
|
6362
|
+
const query = url.searchParams.get("query");
|
|
6363
|
+
if (!query) {
|
|
6364
|
+
return searchUrl;
|
|
6365
|
+
}
|
|
6366
|
+
const withoutClosingParen = query.endsWith(")") ? query.slice(0, -1) : query;
|
|
6367
|
+
url.searchParams.set("query", `${withoutClosingParen},keywords:${trimmed})`);
|
|
6368
|
+
return url.toString();
|
|
6369
|
+
}
|
|
6370
|
+
function buildLeadParameterSearchUrl(filters, keywords) {
|
|
6371
|
+
const url = new URL(appendSalesNavigatorKeywords(buildSalesNavigatorPeopleSearchUrl(filters), keywords));
|
|
6372
|
+
const query = url.searchParams.get("query");
|
|
6373
|
+
if (query) {
|
|
6374
|
+
const inner = query.startsWith("(") && query.endsWith(")") ? query.slice(1, -1) : query;
|
|
6375
|
+
url.searchParams.set("query", `(spellCorrectionEnabled:true,recentSearchParam:(id:${Date.now()},doLogHistory:true),${inner})`);
|
|
6376
|
+
}
|
|
6377
|
+
url.searchParams.set("sessionId", generateLinkedInSessionId());
|
|
6378
|
+
url.searchParams.set("viewAllFilters", "true");
|
|
6379
|
+
return url.toString();
|
|
6380
|
+
}
|
|
6381
|
+
function normalizeGermanNameForMatch(value) {
|
|
6382
|
+
return value
|
|
6383
|
+
.trim()
|
|
6384
|
+
.toLowerCase()
|
|
6385
|
+
.replace(/ä/g, "ae")
|
|
6386
|
+
.replace(/ö/g, "oe")
|
|
6387
|
+
.replace(/ü/g, "ue")
|
|
6388
|
+
.replace(/ß/g, "ss")
|
|
6389
|
+
.replace(/\s+/g, " ");
|
|
6390
|
+
}
|
|
6391
|
+
function contactMatchesLocalLead(person, target) {
|
|
6392
|
+
const fullName = normalizeGermanNameForMatch(String(person.fullName ?? ""));
|
|
6393
|
+
const firstName = target.firstName
|
|
6394
|
+
? normalizeGermanNameForMatch(target.firstName)
|
|
6395
|
+
: null;
|
|
6396
|
+
const lastName = target.lastName
|
|
6397
|
+
? normalizeGermanNameForMatch(target.lastName)
|
|
6398
|
+
: null;
|
|
6399
|
+
const company = target.company?.trim().toLowerCase();
|
|
6400
|
+
const personCompany = String(person.companyName ?? "").toLowerCase();
|
|
6401
|
+
if (firstName && !fullName.includes(firstName)) {
|
|
6402
|
+
return false;
|
|
6403
|
+
}
|
|
6404
|
+
if (lastName && !fullName.includes(lastName)) {
|
|
6405
|
+
return false;
|
|
6406
|
+
}
|
|
6407
|
+
if (company && personCompany && !personCompany.includes(company)) {
|
|
6408
|
+
return false;
|
|
6409
|
+
}
|
|
6410
|
+
return true;
|
|
6411
|
+
}
|
|
6412
|
+
function summarizeMatchedLeadParameterResult(person) {
|
|
6413
|
+
if (!person) {
|
|
6414
|
+
return null;
|
|
6415
|
+
}
|
|
6416
|
+
return {
|
|
6417
|
+
fullName: typeof person.fullName === "string" ? person.fullName : null,
|
|
6418
|
+
title: typeof person.title === "string" ? person.title : null,
|
|
6419
|
+
companyName: typeof person.companyName === "string" ? person.companyName : null,
|
|
6420
|
+
profileUrl: typeof person.profileUrl === "string" ? person.profileUrl : null,
|
|
6421
|
+
linkedInProfileUrl: typeof person.linkedInProfileUrl === "string"
|
|
6422
|
+
? person.linkedInProfileUrl
|
|
6423
|
+
: null,
|
|
5899
6424
|
};
|
|
5900
6425
|
}
|
|
5901
6426
|
function withSalesNavigatorPaging(url, start, count) {
|
|
@@ -5910,9 +6435,7 @@ function withSalesNavigatorPaging(url, start, count) {
|
|
|
5910
6435
|
}
|
|
5911
6436
|
const base = withoutHash.slice(0, queryIndex);
|
|
5912
6437
|
const search = withoutHash.slice(queryIndex + 1);
|
|
5913
|
-
const params = search
|
|
5914
|
-
.split("&")
|
|
5915
|
-
.filter((part) => {
|
|
6438
|
+
const params = search.split("&").filter((part) => {
|
|
5916
6439
|
const key = part.split("=", 1)[0]?.toLowerCase();
|
|
5917
6440
|
return key !== "start" && key !== "count";
|
|
5918
6441
|
});
|
|
@@ -5926,13 +6449,28 @@ function buildLinkedInSalesNavigatorLocalImportHeaders(config) {
|
|
|
5926
6449
|
cookie: config.cookie,
|
|
5927
6450
|
"user-agent": config.userAgent,
|
|
5928
6451
|
"x-li-identity": config.identity,
|
|
5929
|
-
"x-restli-protocol-version": "2.0.0"
|
|
6452
|
+
"x-restli-protocol-version": "2.0.0",
|
|
6453
|
+
};
|
|
6454
|
+
}
|
|
6455
|
+
function buildLinkedInSalesNavigatorAccountSearchHeaders(config) {
|
|
6456
|
+
return {
|
|
6457
|
+
...buildLinkedInSalesNavigatorLocalImportHeaders(config),
|
|
6458
|
+
accept: "*/*",
|
|
6459
|
+
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
6460
|
+
referer: "https://www.linkedin.com/sales/search/company",
|
|
6461
|
+
"sec-fetch-dest": "empty",
|
|
6462
|
+
"sec-fetch-mode": "cors",
|
|
6463
|
+
"sec-fetch-site": "same-origin",
|
|
6464
|
+
"x-li-lang": "en_US",
|
|
6465
|
+
"x-li-page-instance": "urn:li:page:d_sales2_search_accounts;13Jvve6kRGCao+iP0wwAag==",
|
|
5930
6466
|
};
|
|
5931
6467
|
}
|
|
5932
6468
|
function getNestedValue(record, pathParts) {
|
|
5933
6469
|
let current = record;
|
|
5934
6470
|
for (const part of pathParts) {
|
|
5935
|
-
if (typeof current !== "object" ||
|
|
6471
|
+
if (typeof current !== "object" ||
|
|
6472
|
+
current === null ||
|
|
6473
|
+
Array.isArray(current)) {
|
|
5936
6474
|
return undefined;
|
|
5937
6475
|
}
|
|
5938
6476
|
current = current[part];
|
|
@@ -5953,8 +6491,14 @@ function flattenSalesNavigatorRecord(value) {
|
|
|
5953
6491
|
return {};
|
|
5954
6492
|
}
|
|
5955
6493
|
const record = value;
|
|
5956
|
-
const entity = (record.entityResult ??
|
|
5957
|
-
|
|
6494
|
+
const entity = (record.entityResult ??
|
|
6495
|
+
record.lead ??
|
|
6496
|
+
record.profile ??
|
|
6497
|
+
record.person ??
|
|
6498
|
+
record);
|
|
6499
|
+
return typeof entity === "object" && entity !== null && !Array.isArray(entity)
|
|
6500
|
+
? { ...record, ...entity }
|
|
6501
|
+
: record;
|
|
5958
6502
|
}
|
|
5959
6503
|
function deriveLocalSalesNavigatorProfileUrl(record) {
|
|
5960
6504
|
const direct = firstLocalString(record, [
|
|
@@ -5963,7 +6507,7 @@ function deriveLocalSalesNavigatorProfileUrl(record) {
|
|
|
5963
6507
|
"linkedInProfileUrl",
|
|
5964
6508
|
"navigationUrl",
|
|
5965
6509
|
"url",
|
|
5966
|
-
"profilePicture.profileUrl"
|
|
6510
|
+
"profilePicture.profileUrl",
|
|
5967
6511
|
]);
|
|
5968
6512
|
if (direct) {
|
|
5969
6513
|
try {
|
|
@@ -5978,14 +6522,23 @@ function deriveLocalSalesNavigatorProfileUrl(record) {
|
|
|
5978
6522
|
"publicIdentifier",
|
|
5979
6523
|
"public_id",
|
|
5980
6524
|
"miniProfile.publicIdentifier",
|
|
5981
|
-
"member.publicIdentifier"
|
|
6525
|
+
"member.publicIdentifier",
|
|
5982
6526
|
]);
|
|
5983
6527
|
if (publicIdentifier) {
|
|
5984
6528
|
return `https://www.linkedin.com/in/${encodeURIComponent(publicIdentifier)}`;
|
|
5985
6529
|
}
|
|
5986
|
-
const urn = firstLocalString(record, [
|
|
5987
|
-
|
|
5988
|
-
|
|
6530
|
+
const urn = firstLocalString(record, [
|
|
6531
|
+
"entityUrn",
|
|
6532
|
+
"profileUrn",
|
|
6533
|
+
"objectUrn",
|
|
6534
|
+
"memberUrn",
|
|
6535
|
+
"urn",
|
|
6536
|
+
]);
|
|
6537
|
+
const urnId = urn?.match(/fs_salesProfile:\(([^,)]+)/i)?.[1] ??
|
|
6538
|
+
urn?.match(/(?:profile|member|lead):([^,)]+)/i)?.[1];
|
|
6539
|
+
return urnId
|
|
6540
|
+
? `https://www.linkedin.com/sales/lead/${encodeURIComponent(urnId)}`
|
|
6541
|
+
: null;
|
|
5989
6542
|
}
|
|
5990
6543
|
function firstLocalArrayRecord(record, key) {
|
|
5991
6544
|
const value = record[key];
|
|
@@ -5997,14 +6550,21 @@ function firstLocalArrayRecord(record, key) {
|
|
|
5997
6550
|
}
|
|
5998
6551
|
function firstLocalNestedRecord(record, key) {
|
|
5999
6552
|
const value = record[key];
|
|
6000
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6553
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6554
|
+
? value
|
|
6555
|
+
: {};
|
|
6001
6556
|
}
|
|
6002
6557
|
function deriveLocalCompanyId(record, currentPosition, company) {
|
|
6003
|
-
const direct = firstLocalString(record, [
|
|
6558
|
+
const direct = firstLocalString(record, [
|
|
6559
|
+
"companyId",
|
|
6560
|
+
"currentCompanyId",
|
|
6561
|
+
"company.id",
|
|
6562
|
+
]);
|
|
6004
6563
|
if (direct) {
|
|
6005
6564
|
return direct;
|
|
6006
6565
|
}
|
|
6007
|
-
const urn = firstLocalString(currentPosition, ["companyUrn"]) ??
|
|
6566
|
+
const urn = firstLocalString(currentPosition, ["companyUrn"]) ??
|
|
6567
|
+
firstLocalString(company, ["entityUrn"]);
|
|
6008
6568
|
return urn?.match(/:(\d+)$/)?.[1] ?? null;
|
|
6009
6569
|
}
|
|
6010
6570
|
function formatSalesNavigatorTenure(value) {
|
|
@@ -6012,8 +6572,12 @@ function formatSalesNavigatorTenure(value) {
|
|
|
6012
6572
|
return null;
|
|
6013
6573
|
}
|
|
6014
6574
|
const record = value;
|
|
6015
|
-
const years = typeof record.numYears === "number" && Number.isFinite(record.numYears)
|
|
6016
|
-
|
|
6575
|
+
const years = typeof record.numYears === "number" && Number.isFinite(record.numYears)
|
|
6576
|
+
? `${record.numYears}y`
|
|
6577
|
+
: null;
|
|
6578
|
+
const months = typeof record.numMonths === "number" && Number.isFinite(record.numMonths)
|
|
6579
|
+
? `${record.numMonths}m`
|
|
6580
|
+
: null;
|
|
6017
6581
|
return [years, months].filter(Boolean).join(" ") || null;
|
|
6018
6582
|
}
|
|
6019
6583
|
function buildLinkedInImageUrl(value) {
|
|
@@ -6028,16 +6592,22 @@ function buildLinkedInImageUrl(value) {
|
|
|
6028
6592
|
const artifact = record.artifacts
|
|
6029
6593
|
.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
|
|
6030
6594
|
.sort((left, right) => Number(right.width ?? 0) - Number(left.width ?? 0))[0];
|
|
6031
|
-
const pathSegment = typeof artifact?.fileIdentifyingUrlPathSegment === "string"
|
|
6595
|
+
const pathSegment = typeof artifact?.fileIdentifyingUrlPathSegment === "string"
|
|
6596
|
+
? artifact.fileIdentifyingUrlPathSegment
|
|
6597
|
+
: null;
|
|
6032
6598
|
return pathSegment ? `${rootUrl}${pathSegment}` : null;
|
|
6033
6599
|
}
|
|
6034
6600
|
function deriveSharedConnectionsCount(record) {
|
|
6035
|
-
const badges = Array.isArray(record.spotlightBadges)
|
|
6601
|
+
const badges = Array.isArray(record.spotlightBadges)
|
|
6602
|
+
? record.spotlightBadges
|
|
6603
|
+
: [];
|
|
6036
6604
|
for (const badge of badges) {
|
|
6037
6605
|
if (typeof badge !== "object" || badge === null || Array.isArray(badge)) {
|
|
6038
6606
|
continue;
|
|
6039
6607
|
}
|
|
6040
|
-
const displayValue = firstLocalString(badge, [
|
|
6608
|
+
const displayValue = firstLocalString(badge, [
|
|
6609
|
+
"displayValue",
|
|
6610
|
+
]);
|
|
6041
6611
|
const match = displayValue?.match(/(\d+)\s+mutual connection/i);
|
|
6042
6612
|
if (match) {
|
|
6043
6613
|
return Number(match[1]);
|
|
@@ -6051,13 +6621,19 @@ function deriveLocalSalesNavigatorFullName(record) {
|
|
|
6051
6621
|
"name",
|
|
6052
6622
|
"title.text",
|
|
6053
6623
|
"profileName",
|
|
6054
|
-
"miniProfile.fullName"
|
|
6624
|
+
"miniProfile.fullName",
|
|
6055
6625
|
]);
|
|
6056
6626
|
if (direct) {
|
|
6057
6627
|
return direct;
|
|
6058
6628
|
}
|
|
6059
|
-
const firstName = firstLocalString(record, [
|
|
6060
|
-
|
|
6629
|
+
const firstName = firstLocalString(record, [
|
|
6630
|
+
"firstName",
|
|
6631
|
+
"miniProfile.firstName",
|
|
6632
|
+
]);
|
|
6633
|
+
const lastName = firstLocalString(record, [
|
|
6634
|
+
"lastName",
|
|
6635
|
+
"miniProfile.lastName",
|
|
6636
|
+
]);
|
|
6061
6637
|
return [firstName, lastName].filter(Boolean).join(" ").trim() || null;
|
|
6062
6638
|
}
|
|
6063
6639
|
function collectPotentialSalesNavigatorRows(value, rows = []) {
|
|
@@ -6073,7 +6649,12 @@ function collectPotentialSalesNavigatorRows(value, rows = []) {
|
|
|
6073
6649
|
const record = flattenSalesNavigatorRecord(value);
|
|
6074
6650
|
if (deriveLocalSalesNavigatorProfileUrl(record) &&
|
|
6075
6651
|
(deriveLocalSalesNavigatorFullName(record) ||
|
|
6076
|
-
firstLocalString(record, [
|
|
6652
|
+
firstLocalString(record, [
|
|
6653
|
+
"occupation",
|
|
6654
|
+
"title",
|
|
6655
|
+
"headline",
|
|
6656
|
+
"currentPositions.0.title",
|
|
6657
|
+
]))) {
|
|
6077
6658
|
rows.push(record);
|
|
6078
6659
|
}
|
|
6079
6660
|
for (const nested of Object.values(value)) {
|
|
@@ -6101,44 +6682,75 @@ function normalizeLocalSalesNavigatorPeople(responseBody, queryUrl) {
|
|
|
6101
6682
|
defaultProfileUrl: profileUrl,
|
|
6102
6683
|
linkedInProfileUrl: profileUrl.includes("/in/") ? profileUrl : null,
|
|
6103
6684
|
fullName,
|
|
6104
|
-
firstName: firstLocalString(record, [
|
|
6685
|
+
firstName: firstLocalString(record, [
|
|
6686
|
+
"firstName",
|
|
6687
|
+
"miniProfile.firstName",
|
|
6688
|
+
]),
|
|
6105
6689
|
lastName: firstLocalString(record, ["lastName", "miniProfile.lastName"]),
|
|
6106
|
-
title: firstLocalString(record, ["occupation", "title", "headline"]) ??
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
"
|
|
6110
|
-
|
|
6690
|
+
title: firstLocalString(record, ["occupation", "title", "headline"]) ??
|
|
6691
|
+
firstLocalString(currentPosition, ["title"]),
|
|
6692
|
+
companyName: firstLocalString(record, ["companyName", "currentCompanyName"]) ??
|
|
6693
|
+
firstLocalString(currentPosition, ["companyName"]) ??
|
|
6694
|
+
firstLocalString(company, ["name"]),
|
|
6111
6695
|
companyId,
|
|
6112
|
-
companyUrl: firstLocalString(record, ["companyUrl", "company.url"]) ??
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6696
|
+
companyUrl: firstLocalString(record, ["companyUrl", "company.url"]) ??
|
|
6697
|
+
(companyId
|
|
6698
|
+
? `https://www.linkedin.com/sales/company/${companyId}`
|
|
6699
|
+
: null),
|
|
6700
|
+
regularCompanyUrl: companyId
|
|
6701
|
+
? `https://www.linkedin.com/company/${companyId}`
|
|
6702
|
+
: null,
|
|
6703
|
+
location: firstLocalString(record, [
|
|
6704
|
+
"location",
|
|
6705
|
+
"geoRegion",
|
|
6706
|
+
"locationName",
|
|
6707
|
+
]),
|
|
6708
|
+
industry: firstLocalString(record, ["industry"]) ??
|
|
6709
|
+
firstLocalString(company, ["industry"]),
|
|
6116
6710
|
summary: firstLocalString(record, ["summary", "description"]),
|
|
6117
|
-
titleDescription: firstLocalString(record, ["titleDescription"]) ??
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6711
|
+
titleDescription: firstLocalString(record, ["titleDescription"]) ??
|
|
6712
|
+
firstLocalString(currentPosition, ["description"]),
|
|
6713
|
+
companyLocation: firstLocalString(record, ["companyLocation"]) ??
|
|
6714
|
+
firstLocalString(company, ["location"]),
|
|
6715
|
+
durationInRole: firstLocalString(record, ["durationInRole"]) ??
|
|
6716
|
+
formatSalesNavigatorTenure(currentPosition.tenureAtPosition),
|
|
6717
|
+
durationInCompany: firstLocalString(record, ["durationInCompany"]) ??
|
|
6718
|
+
formatSalesNavigatorTenure(currentPosition.tenureAtCompany),
|
|
6719
|
+
connectionDegree: firstLocalString(record, ["connectionDegree"]) ??
|
|
6720
|
+
(record.degree != null ? String(record.degree) : null),
|
|
6122
6721
|
sharedConnectionsCount: deriveSharedConnectionsCount(record),
|
|
6123
|
-
profileImageUrl: firstLocalString(record, [
|
|
6722
|
+
profileImageUrl: firstLocalString(record, [
|
|
6723
|
+
"profileImageUrl",
|
|
6724
|
+
"pictureUrl",
|
|
6725
|
+
"profilePicture.displayImage",
|
|
6726
|
+
]) ?? buildLinkedInImageUrl(record.profilePictureDisplayImage),
|
|
6124
6727
|
vmid: firstLocalString(record, ["vmid", "memberId", "profileId"]),
|
|
6125
6728
|
isPremium: typeof record.premium === "boolean" ? record.premium : null,
|
|
6126
6729
|
isOpenLink: typeof record.openLink === "boolean" ? record.openLink : null,
|
|
6127
6730
|
query: queryUrl,
|
|
6128
6731
|
timestamp: new Date().toISOString(),
|
|
6129
|
-
rawLocalSalesNavigatorResult: record
|
|
6732
|
+
rawLocalSalesNavigatorResult: record,
|
|
6130
6733
|
});
|
|
6131
6734
|
}
|
|
6132
6735
|
return rows;
|
|
6133
6736
|
}
|
|
6134
6737
|
function extractLocalSalesNavigatorTotalResults(responseBody) {
|
|
6135
|
-
if (typeof responseBody !== "object" ||
|
|
6738
|
+
if (typeof responseBody !== "object" ||
|
|
6739
|
+
responseBody === null ||
|
|
6740
|
+
Array.isArray(responseBody)) {
|
|
6136
6741
|
return null;
|
|
6137
6742
|
}
|
|
6138
6743
|
const record = responseBody;
|
|
6139
|
-
const candidates = [
|
|
6744
|
+
const candidates = [
|
|
6745
|
+
record.paging,
|
|
6746
|
+
record.metadata,
|
|
6747
|
+
record.searchResultsMetadata,
|
|
6748
|
+
record,
|
|
6749
|
+
];
|
|
6140
6750
|
for (const candidate of candidates) {
|
|
6141
|
-
if (typeof candidate !== "object" ||
|
|
6751
|
+
if (typeof candidate !== "object" ||
|
|
6752
|
+
candidate === null ||
|
|
6753
|
+
Array.isArray(candidate)) {
|
|
6142
6754
|
continue;
|
|
6143
6755
|
}
|
|
6144
6756
|
for (const key of ["total", "totalResults", "resultCount", "count"]) {
|
|
@@ -6161,7 +6773,27 @@ function isSalesNavigatorRateLimitStatus(status, text) {
|
|
|
6161
6773
|
/too many requests|rate limit|unusual activity|automated activity|temporarily restricted/i.test(text));
|
|
6162
6774
|
}
|
|
6163
6775
|
function isRetryableLocalSalesNavigatorStatus(status) {
|
|
6164
|
-
return status === 408 ||
|
|
6776
|
+
return (status === 408 ||
|
|
6777
|
+
status === 425 ||
|
|
6778
|
+
status === 500 ||
|
|
6779
|
+
status === 502 ||
|
|
6780
|
+
status === 503 ||
|
|
6781
|
+
status === 504);
|
|
6782
|
+
}
|
|
6783
|
+
function parseRetryAfterHeader(value) {
|
|
6784
|
+
const trimmed = value?.trim();
|
|
6785
|
+
if (!trimmed) {
|
|
6786
|
+
return null;
|
|
6787
|
+
}
|
|
6788
|
+
const seconds = Number(trimmed);
|
|
6789
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
6790
|
+
return Math.trunc(seconds * 1000);
|
|
6791
|
+
}
|
|
6792
|
+
const dateMs = Date.parse(trimmed);
|
|
6793
|
+
if (Number.isFinite(dateMs)) {
|
|
6794
|
+
return Math.max(0, dateMs - Date.now());
|
|
6795
|
+
}
|
|
6796
|
+
return null;
|
|
6165
6797
|
}
|
|
6166
6798
|
async function waitWithFullJitter(baseDelayMs, maxDelayMs, attempt) {
|
|
6167
6799
|
const exponentialCap = Math.min(Math.max(0, Math.trunc(maxDelayMs)), Math.max(0, Math.trunc(baseDelayMs)) * 2 ** Math.max(0, attempt));
|
|
@@ -6178,15 +6810,16 @@ async function fetchLocalSalesNavigatorRequest(parsedRequest, retryOptions) {
|
|
|
6178
6810
|
try {
|
|
6179
6811
|
const response = await fetch(parsedRequest.url, {
|
|
6180
6812
|
method: "GET",
|
|
6181
|
-
headers: parsedRequest.headers
|
|
6813
|
+
headers: parsedRequest.headers,
|
|
6182
6814
|
});
|
|
6183
6815
|
const text = await response.text();
|
|
6184
6816
|
if (!response.ok) {
|
|
6185
6817
|
const preview = text.slice(0, 300);
|
|
6186
6818
|
if (isSalesNavigatorRateLimitStatus(response.status, text)) {
|
|
6187
|
-
throw new LocalSalesNavigatorRateLimitError(response.status, preview);
|
|
6819
|
+
throw new LocalSalesNavigatorRateLimitError(response.status, preview, parseRetryAfterHeader(response.headers.get("retry-after")));
|
|
6188
6820
|
}
|
|
6189
|
-
if (isRetryableLocalSalesNavigatorStatus(response.status) &&
|
|
6821
|
+
if (isRetryableLocalSalesNavigatorStatus(response.status) &&
|
|
6822
|
+
attempt < retryOptions.maxRetries) {
|
|
6190
6823
|
retryCount += 1;
|
|
6191
6824
|
retryDelayMs += await waitWithFullJitter(retryOptions.retryBaseDelayMs, retryOptions.retryMaxDelayMs, attempt);
|
|
6192
6825
|
continue;
|
|
@@ -6221,7 +6854,9 @@ async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
|
|
|
6221
6854
|
let totalDelayMs = 0;
|
|
6222
6855
|
let retryCount = 0;
|
|
6223
6856
|
const pageSize = Math.max(1, Math.min(100, Math.trunc(options.pageSize)));
|
|
6224
|
-
const requestedProfiles = options.requestedProfiles == null
|
|
6857
|
+
const requestedProfiles = options.requestedProfiles == null
|
|
6858
|
+
? null
|
|
6859
|
+
: Math.max(1, Math.trunc(options.requestedProfiles));
|
|
6225
6860
|
const pageDelayMinMs = Math.max(0, Math.trunc(options.pageDelayMinMs));
|
|
6226
6861
|
const pageDelayMaxMs = Math.max(pageDelayMinMs, Math.trunc(options.pageDelayMaxMs));
|
|
6227
6862
|
for (let start = 0;; start += pageSize) {
|
|
@@ -6232,14 +6867,15 @@ async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
|
|
|
6232
6867
|
const currentCount = Math.min(pageSize, remainingRequested);
|
|
6233
6868
|
const pageRequest = {
|
|
6234
6869
|
...parsedRequest,
|
|
6235
|
-
url: withSalesNavigatorPaging(parsedRequest.url, start, currentCount)
|
|
6870
|
+
url: withSalesNavigatorPaging(parsedRequest.url, start, currentCount),
|
|
6236
6871
|
};
|
|
6237
6872
|
const response = await fetchLocalSalesNavigatorRequest(pageRequest, options.retry);
|
|
6238
6873
|
retryCount += response.retryCount;
|
|
6239
6874
|
totalDelayMs += response.retryDelayMs;
|
|
6240
6875
|
const responseBody = response.body;
|
|
6241
6876
|
fetchedPages += 1;
|
|
6242
|
-
totalResults =
|
|
6877
|
+
totalResults =
|
|
6878
|
+
totalResults ?? extractLocalSalesNavigatorTotalResults(responseBody);
|
|
6243
6879
|
const pagePeople = normalizeLocalSalesNavigatorPeople(responseBody, pageRequest.url);
|
|
6244
6880
|
let added = 0;
|
|
6245
6881
|
for (const person of pagePeople) {
|
|
@@ -6272,85 +6908,699 @@ async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
|
|
|
6272
6908
|
delayMinMs: pageDelayMinMs,
|
|
6273
6909
|
delayMaxMs: pageDelayMaxMs,
|
|
6274
6910
|
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}`
|
|
6911
|
+
retryCount,
|
|
6285
6912
|
},
|
|
6286
|
-
|
|
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;
|
|
6913
|
+
};
|
|
6305
6914
|
}
|
|
6306
|
-
async function
|
|
6307
|
-
const
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
const
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6915
|
+
async function fetchAllLocalSalesNavigatorAccounts(parsedRequest, options) {
|
|
6916
|
+
const accounts = [];
|
|
6917
|
+
const seen = new Set();
|
|
6918
|
+
let totalResults = null;
|
|
6919
|
+
let fetchedPages = 0;
|
|
6920
|
+
let totalDelayMs = 0;
|
|
6921
|
+
let retryCount = 0;
|
|
6922
|
+
const pageSize = Math.max(1, Math.min(100, Math.trunc(options.pageSize)));
|
|
6923
|
+
const requestedAccounts = options.requestedAccounts == null
|
|
6924
|
+
? null
|
|
6925
|
+
: Math.max(1, Math.trunc(options.requestedAccounts));
|
|
6926
|
+
const pageDelayMinMs = Math.max(0, Math.trunc(options.pageDelayMinMs));
|
|
6927
|
+
const pageDelayMaxMs = Math.max(pageDelayMinMs, Math.trunc(options.pageDelayMaxMs));
|
|
6928
|
+
const startOffset = Math.max(0, Math.trunc(options.startOffset ?? 0));
|
|
6929
|
+
let rateLimit;
|
|
6930
|
+
let pageError;
|
|
6931
|
+
let stopReason = null;
|
|
6932
|
+
let lastSuccessfulStart = null;
|
|
6933
|
+
let lastSuccessfulCount = 0;
|
|
6934
|
+
let lastSuccessfulAdded = 0;
|
|
6935
|
+
let stoppedAtStart = null;
|
|
6936
|
+
let stoppedAtCount = null;
|
|
6937
|
+
let nextStart = null;
|
|
6938
|
+
for (let start = startOffset;; start += pageSize) {
|
|
6939
|
+
const remainingRequested = requestedAccounts == null ? pageSize : requestedAccounts - accounts.length;
|
|
6940
|
+
if (remainingRequested <= 0) {
|
|
6941
|
+
stopReason = "requested_limit_reached";
|
|
6942
|
+
nextStart = start;
|
|
6943
|
+
break;
|
|
6321
6944
|
}
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6945
|
+
const currentCount = Math.min(pageSize, remainingRequested);
|
|
6946
|
+
const pageRequest = {
|
|
6947
|
+
...parsedRequest,
|
|
6948
|
+
url: withSalesNavigatorPaging(parsedRequest.url, start, currentCount),
|
|
6949
|
+
};
|
|
6950
|
+
let response;
|
|
6951
|
+
try {
|
|
6952
|
+
response = await fetchLocalSalesNavigatorRequest(pageRequest, options.retry);
|
|
6953
|
+
}
|
|
6954
|
+
catch (error) {
|
|
6955
|
+
if (error instanceof LocalSalesNavigatorRateLimitError &&
|
|
6956
|
+
accounts.length > 0) {
|
|
6957
|
+
rateLimit = {
|
|
6958
|
+
status: error.status,
|
|
6959
|
+
retryAfterMs: error.retryAfterMs,
|
|
6960
|
+
message: error.message,
|
|
6961
|
+
};
|
|
6962
|
+
stopReason = "rate_limit";
|
|
6963
|
+
stoppedAtStart = start;
|
|
6964
|
+
stoppedAtCount = currentCount;
|
|
6965
|
+
nextStart = start;
|
|
6966
|
+
break;
|
|
6967
|
+
}
|
|
6968
|
+
if (options.allowPartialPageErrors && accounts.length > 0) {
|
|
6969
|
+
pageError = {
|
|
6970
|
+
message: error instanceof Error ? error.message : String(error),
|
|
6971
|
+
};
|
|
6972
|
+
stopReason = "page_error";
|
|
6973
|
+
stoppedAtStart = start;
|
|
6974
|
+
stoppedAtCount = currentCount;
|
|
6975
|
+
nextStart = start;
|
|
6976
|
+
break;
|
|
6977
|
+
}
|
|
6978
|
+
throw error;
|
|
6979
|
+
}
|
|
6980
|
+
retryCount += response.retryCount;
|
|
6981
|
+
totalDelayMs += response.retryDelayMs;
|
|
6982
|
+
const responseBody = response.body;
|
|
6983
|
+
fetchedPages += 1;
|
|
6984
|
+
totalResults =
|
|
6985
|
+
totalResults ?? extractLocalSalesNavigatorTotalResults(responseBody);
|
|
6986
|
+
if (totalResults != null &&
|
|
6987
|
+
totalResults > Math.max(1, Math.trunc(options.maxAllowedResults))) {
|
|
6988
|
+
throw new SalesNavigatorSliceTooBroadError(`Account slice has ${totalResults} results, above ${options.maxAllowedResults}.`, { totalResults });
|
|
6989
|
+
}
|
|
6990
|
+
const pageAccounts = normalizeLocalSalesNavigatorAccounts(responseBody, pageRequest.url);
|
|
6991
|
+
let added = 0;
|
|
6992
|
+
for (const account of pageAccounts) {
|
|
6993
|
+
if (seen.has(account.accountKey)) {
|
|
6994
|
+
continue;
|
|
6995
|
+
}
|
|
6996
|
+
seen.add(account.accountKey);
|
|
6997
|
+
accounts.push(account);
|
|
6998
|
+
added += 1;
|
|
6999
|
+
if (requestedAccounts != null && accounts.length >= requestedAccounts) {
|
|
7000
|
+
break;
|
|
7001
|
+
}
|
|
7002
|
+
}
|
|
7003
|
+
if (added > 0) {
|
|
7004
|
+
lastSuccessfulStart = start;
|
|
7005
|
+
lastSuccessfulCount = currentCount;
|
|
7006
|
+
lastSuccessfulAdded = added;
|
|
7007
|
+
}
|
|
7008
|
+
if (requestedAccounts != null && accounts.length >= requestedAccounts) {
|
|
7009
|
+
stopReason = "requested_limit_reached";
|
|
7010
|
+
stoppedAtStart = start;
|
|
7011
|
+
stoppedAtCount = currentCount;
|
|
7012
|
+
nextStart = start + currentCount;
|
|
7013
|
+
break;
|
|
7014
|
+
}
|
|
7015
|
+
const reportedTotalReached = totalResults != null && start + currentCount >= totalResults;
|
|
7016
|
+
if (pageAccounts.length === 0) {
|
|
7017
|
+
stopReason = "empty_page";
|
|
7018
|
+
stoppedAtStart = start;
|
|
7019
|
+
stoppedAtCount = currentCount;
|
|
7020
|
+
nextStart = start;
|
|
7021
|
+
break;
|
|
7022
|
+
}
|
|
7023
|
+
if (added === 0) {
|
|
7024
|
+
stopReason = "duplicate_page";
|
|
7025
|
+
stoppedAtStart = start;
|
|
7026
|
+
stoppedAtCount = currentCount;
|
|
7027
|
+
nextStart = start;
|
|
7028
|
+
break;
|
|
7029
|
+
}
|
|
7030
|
+
if (reportedTotalReached) {
|
|
7031
|
+
stopReason = "reported_total_reached";
|
|
7032
|
+
stoppedAtStart = start;
|
|
7033
|
+
stoppedAtCount = currentCount;
|
|
7034
|
+
nextStart = start + currentCount;
|
|
7035
|
+
break;
|
|
7036
|
+
}
|
|
7037
|
+
const pageDelayMs = randomIntegerBetween(pageDelayMinMs, pageDelayMaxMs);
|
|
7038
|
+
if (pageDelayMs > 0) {
|
|
7039
|
+
totalDelayMs += pageDelayMs;
|
|
7040
|
+
await delay(pageDelayMs);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
const fetchedThrough = nextStart ?? (lastSuccessfulStart == null ? startOffset : lastSuccessfulStart + lastSuccessfulCount);
|
|
7044
|
+
const missingFromReported = totalResults == null ? null : Math.max(0, totalResults - fetchedThrough);
|
|
7045
|
+
return {
|
|
7046
|
+
accounts,
|
|
7047
|
+
totalResults,
|
|
7048
|
+
fetchedPages,
|
|
7049
|
+
sourceQueryUrl: parsedRequest.url,
|
|
7050
|
+
pacing: {
|
|
7051
|
+
startOffset,
|
|
7052
|
+
delayMinMs: pageDelayMinMs,
|
|
7053
|
+
delayMaxMs: pageDelayMaxMs,
|
|
7054
|
+
totalDelayMs,
|
|
7055
|
+
retryCount,
|
|
7056
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
7057
|
+
...(pageError ? { pageError } : {}),
|
|
7058
|
+
stop: {
|
|
7059
|
+
reason: stopReason ?? "reported_total_reached",
|
|
7060
|
+
startOffset,
|
|
7061
|
+
pageSize,
|
|
7062
|
+
lastSuccessfulStart,
|
|
7063
|
+
lastSuccessfulCount,
|
|
7064
|
+
lastSuccessfulAdded,
|
|
7065
|
+
stoppedAtStart,
|
|
7066
|
+
stoppedAtCount,
|
|
7067
|
+
nextStart,
|
|
7068
|
+
fetchedAccounts: accounts.length,
|
|
7069
|
+
reportedTotal: totalResults,
|
|
7070
|
+
missingFromReported,
|
|
7071
|
+
},
|
|
6331
7072
|
},
|
|
6332
|
-
|
|
6333
|
-
|
|
7073
|
+
};
|
|
7074
|
+
}
|
|
7075
|
+
function describeLocalSalesNavigatorAccountPartialError(fetchResult) {
|
|
7076
|
+
const rateLimit = fetchResult.pacing.rateLimit;
|
|
7077
|
+
if (rateLimit) {
|
|
7078
|
+
return rateLimit.message;
|
|
7079
|
+
}
|
|
7080
|
+
const pageError = fetchResult.pacing.pageError;
|
|
7081
|
+
if (pageError) {
|
|
7082
|
+
return pageError.message;
|
|
7083
|
+
}
|
|
7084
|
+
const stop = fetchResult.pacing.stop;
|
|
7085
|
+
if (stop.reason === "requested_limit_reached") {
|
|
7086
|
+
return null;
|
|
7087
|
+
}
|
|
7088
|
+
if (stop.missingFromReported != null && stop.missingFromReported > 0) {
|
|
7089
|
+
return `Account slice stopped at offset ${stop.stoppedAtStart ?? stop.nextStart ?? "unknown"} (${stop.reason}) after fetching ${stop.fetchedAccounts} of ${stop.reportedTotal} reported accounts.`;
|
|
7090
|
+
}
|
|
7091
|
+
return null;
|
|
7092
|
+
}
|
|
7093
|
+
async function importLocalSalesNavigatorPeopleViaApp(session, payload) {
|
|
7094
|
+
const { value } = await fetchCliJson(session, (currentSession) => {
|
|
7095
|
+
const workspaceClientId = currentSession.user.workspaceClientId?.trim() || null;
|
|
7096
|
+
return fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/local-import`, {
|
|
7097
|
+
method: "POST",
|
|
7098
|
+
headers: {
|
|
7099
|
+
"Content-Type": "application/json",
|
|
7100
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
7101
|
+
},
|
|
7102
|
+
body: JSON.stringify({
|
|
7103
|
+
sourceQueryUrl: payload.sourceQueryUrl,
|
|
7104
|
+
slicedQueryUrl: payload.sourceQueryUrl,
|
|
7105
|
+
appliedFilters: [],
|
|
7106
|
+
maxResultsPerSearch: payload.maxResultsPerSearch,
|
|
7107
|
+
numberOfProfiles: payload.numberOfProfiles,
|
|
7108
|
+
slicePreset: payload.slicePreset,
|
|
7109
|
+
totalResults: payload.totalResults,
|
|
7110
|
+
fetchedPages: payload.fetchedPages,
|
|
7111
|
+
people: payload.people,
|
|
7112
|
+
rawPayload: {
|
|
7113
|
+
clientId: workspaceClientId ?? undefined,
|
|
7114
|
+
client_id: workspaceClientId ?? undefined,
|
|
7115
|
+
leadList: workspaceClientId
|
|
7116
|
+
? { clientId: workspaceClientId }
|
|
7117
|
+
: undefined,
|
|
7118
|
+
workflow: "salesnav:local-import",
|
|
7119
|
+
localPeopleCount: payload.people.length,
|
|
7120
|
+
fetchedPages: payload.fetchedPages,
|
|
7121
|
+
pacing: payload.pacing,
|
|
7122
|
+
},
|
|
7123
|
+
}),
|
|
7124
|
+
});
|
|
7125
|
+
}, SalesNavigatorLocalImportResponseSchema);
|
|
6334
7126
|
return value;
|
|
6335
7127
|
}
|
|
6336
|
-
async function
|
|
6337
|
-
const
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
if (
|
|
6341
|
-
|
|
7128
|
+
async function createSalesNavigatorAccountSearchStore() {
|
|
7129
|
+
const databaseUrl = process.env.SALESPROMPTER_DATABASE_URL?.trim() ||
|
|
7130
|
+
process.env.DATABASE_URL?.trim() ||
|
|
7131
|
+
"";
|
|
7132
|
+
if (databaseUrl) {
|
|
7133
|
+
const client = new pg.Client({
|
|
7134
|
+
connectionString: databaseUrl,
|
|
7135
|
+
ssl: { rejectUnauthorized: false },
|
|
7136
|
+
});
|
|
7137
|
+
await client.connect();
|
|
7138
|
+
return { kind: "postgres", client };
|
|
7139
|
+
}
|
|
7140
|
+
const config = resolveSalesNavigatorSupabaseConfig(process.env);
|
|
7141
|
+
return {
|
|
7142
|
+
kind: "supabase",
|
|
7143
|
+
client: createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
|
|
7144
|
+
auth: {
|
|
7145
|
+
persistSession: false,
|
|
7146
|
+
autoRefreshToken: false,
|
|
7147
|
+
},
|
|
7148
|
+
}),
|
|
7149
|
+
};
|
|
7150
|
+
}
|
|
7151
|
+
async function closeSalesNavigatorAccountSearchStore(store) {
|
|
7152
|
+
if (store.kind === "postgres") {
|
|
7153
|
+
await store.client.end();
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
async function createSalesNavigatorAccountSearchRun(params) {
|
|
7157
|
+
if (params.store.kind === "postgres") {
|
|
7158
|
+
const result = await params.store.client.query(`INSERT INTO public.linkedin_sales_nav_account_search_runs (
|
|
7159
|
+
org_id, source_query_url, slice_preset, max_results_per_search, page_size, status, raw_payload
|
|
7160
|
+
)
|
|
7161
|
+
VALUES ($1, $2, $3, $4, $5, 'running', $6::jsonb)
|
|
7162
|
+
RETURNING id`, [
|
|
7163
|
+
params.orgId,
|
|
7164
|
+
params.sourceQueryUrl,
|
|
7165
|
+
params.slicePreset,
|
|
7166
|
+
params.maxResultsPerSearch,
|
|
7167
|
+
params.pageSize,
|
|
7168
|
+
JSON.stringify(params.rawPayload),
|
|
7169
|
+
]);
|
|
7170
|
+
const id = result.rows[0]?.id ?? "";
|
|
7171
|
+
if (!id) {
|
|
7172
|
+
throw new Error("Failed to create account search run in Neon: missing run id.");
|
|
7173
|
+
}
|
|
7174
|
+
return id;
|
|
7175
|
+
}
|
|
7176
|
+
const { data, error } = await params.store.client
|
|
7177
|
+
.from("linkedin_sales_nav_account_search_runs")
|
|
7178
|
+
.insert({
|
|
7179
|
+
org_id: params.orgId,
|
|
7180
|
+
source_query_url: params.sourceQueryUrl,
|
|
7181
|
+
slice_preset: params.slicePreset,
|
|
7182
|
+
max_results_per_search: params.maxResultsPerSearch,
|
|
7183
|
+
page_size: params.pageSize,
|
|
7184
|
+
status: "running",
|
|
7185
|
+
raw_payload: params.rawPayload,
|
|
7186
|
+
})
|
|
7187
|
+
.select("id")
|
|
7188
|
+
.single();
|
|
7189
|
+
if (error) {
|
|
7190
|
+
throw new Error(`Failed to create account search run in Neon: ${error.message}`);
|
|
7191
|
+
}
|
|
7192
|
+
const id = typeof data?.id === "string" ? data.id : "";
|
|
7193
|
+
if (!id) {
|
|
7194
|
+
throw new Error("Failed to create account search run in Neon: missing run id.");
|
|
7195
|
+
}
|
|
7196
|
+
return id;
|
|
7197
|
+
}
|
|
7198
|
+
async function updateSalesNavigatorAccountSearchRun(params) {
|
|
7199
|
+
if (params.store.kind === "postgres") {
|
|
7200
|
+
await params.store.client.query(`UPDATE public.linkedin_sales_nav_account_search_runs
|
|
7201
|
+
SET status = $2,
|
|
7202
|
+
total_slices = $3,
|
|
7203
|
+
stored_accounts = $4,
|
|
7204
|
+
attempted_slices = $5,
|
|
7205
|
+
split_events = $6,
|
|
7206
|
+
failed_slices = $7,
|
|
7207
|
+
unresolved_slices = $8,
|
|
7208
|
+
truncated = $9,
|
|
7209
|
+
finished_at = now(),
|
|
7210
|
+
last_error = $10,
|
|
7211
|
+
raw_payload = CASE
|
|
7212
|
+
WHEN $11::jsonb IS NULL THEN raw_payload
|
|
7213
|
+
ELSE COALESCE(raw_payload, '{}'::jsonb) || $11::jsonb
|
|
7214
|
+
END
|
|
7215
|
+
WHERE id = $1`, [
|
|
7216
|
+
params.runId,
|
|
7217
|
+
params.status,
|
|
7218
|
+
params.totalSlices,
|
|
7219
|
+
params.storedAccounts,
|
|
7220
|
+
params.attemptedSlices,
|
|
7221
|
+
params.splitEvents,
|
|
7222
|
+
params.failedSlices,
|
|
7223
|
+
params.unresolvedSlices,
|
|
7224
|
+
params.truncated,
|
|
7225
|
+
params.lastError ?? null,
|
|
7226
|
+
params.rawPayload ? JSON.stringify(params.rawPayload) : null,
|
|
7227
|
+
]);
|
|
7228
|
+
return;
|
|
6342
7229
|
}
|
|
6343
|
-
|
|
6344
|
-
|
|
7230
|
+
let mergedRawPayload = null;
|
|
7231
|
+
if (params.rawPayload) {
|
|
7232
|
+
const { data: existing, error: selectError } = await params.store.client
|
|
7233
|
+
.from("linkedin_sales_nav_account_search_runs")
|
|
7234
|
+
.select("raw_payload")
|
|
7235
|
+
.eq("id", params.runId)
|
|
7236
|
+
.maybeSingle();
|
|
7237
|
+
if (selectError) {
|
|
7238
|
+
throw new Error(`Failed to read account search run payload in Neon: ${selectError.message}`);
|
|
7239
|
+
}
|
|
7240
|
+
const existingPayload = typeof existing?.raw_payload === "object" &&
|
|
7241
|
+
existing.raw_payload !== null &&
|
|
7242
|
+
!Array.isArray(existing.raw_payload)
|
|
7243
|
+
? existing.raw_payload
|
|
7244
|
+
: {};
|
|
7245
|
+
mergedRawPayload = {
|
|
7246
|
+
...existingPayload,
|
|
7247
|
+
...params.rawPayload,
|
|
7248
|
+
};
|
|
6345
7249
|
}
|
|
6346
|
-
const {
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
7250
|
+
const { error } = await params.store.client
|
|
7251
|
+
.from("linkedin_sales_nav_account_search_runs")
|
|
7252
|
+
.update({
|
|
7253
|
+
status: params.status,
|
|
7254
|
+
total_slices: params.totalSlices,
|
|
7255
|
+
stored_accounts: params.storedAccounts,
|
|
7256
|
+
attempted_slices: params.attemptedSlices,
|
|
7257
|
+
split_events: params.splitEvents,
|
|
7258
|
+
failed_slices: params.failedSlices,
|
|
7259
|
+
unresolved_slices: params.unresolvedSlices,
|
|
7260
|
+
truncated: params.truncated,
|
|
7261
|
+
finished_at: new Date().toISOString(),
|
|
7262
|
+
last_error: params.lastError ?? null,
|
|
7263
|
+
...(mergedRawPayload
|
|
7264
|
+
? {
|
|
7265
|
+
raw_payload: mergedRawPayload,
|
|
7266
|
+
}
|
|
7267
|
+
: {}),
|
|
7268
|
+
})
|
|
7269
|
+
.eq("id", params.runId);
|
|
7270
|
+
if (error) {
|
|
7271
|
+
throw new Error(`Failed to update account search run in Neon: ${error.message}`);
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
async function upsertSalesNavigatorAccountSearchSlice(params) {
|
|
7275
|
+
if (params.store.kind === "postgres") {
|
|
7276
|
+
const result = await params.store.client.query(`INSERT INTO public.linkedin_sales_nav_account_search_slices (
|
|
7277
|
+
run_id, org_id, sliced_query_url, applied_filters, split_trail, depth, status,
|
|
7278
|
+
total_results, fetched_pages, stored_accounts, retry_count, retry_delay_ms, last_error, raw_payload
|
|
7279
|
+
)
|
|
7280
|
+
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb)
|
|
7281
|
+
ON CONFLICT (run_id, sliced_query_url)
|
|
7282
|
+
DO UPDATE SET
|
|
7283
|
+
applied_filters = excluded.applied_filters,
|
|
7284
|
+
split_trail = excluded.split_trail,
|
|
7285
|
+
depth = excluded.depth,
|
|
7286
|
+
status = excluded.status,
|
|
7287
|
+
total_results = excluded.total_results,
|
|
7288
|
+
fetched_pages = excluded.fetched_pages,
|
|
7289
|
+
stored_accounts = excluded.stored_accounts,
|
|
7290
|
+
retry_count = excluded.retry_count,
|
|
7291
|
+
retry_delay_ms = excluded.retry_delay_ms,
|
|
7292
|
+
last_error = excluded.last_error,
|
|
7293
|
+
raw_payload = excluded.raw_payload
|
|
7294
|
+
RETURNING id`, [
|
|
7295
|
+
params.runId,
|
|
7296
|
+
params.orgId,
|
|
7297
|
+
params.attempt.slicedQueryUrl,
|
|
7298
|
+
JSON.stringify(params.attempt.appliedFilters),
|
|
7299
|
+
JSON.stringify(params.attempt.splitTrail),
|
|
7300
|
+
params.attempt.depth,
|
|
7301
|
+
params.status,
|
|
7302
|
+
params.totalResults,
|
|
7303
|
+
params.fetchedPages,
|
|
7304
|
+
params.storedAccounts,
|
|
7305
|
+
params.retryCount,
|
|
7306
|
+
params.retryDelayMs,
|
|
7307
|
+
params.lastError ?? null,
|
|
7308
|
+
JSON.stringify(params.rawPayload),
|
|
7309
|
+
]);
|
|
7310
|
+
const id = result.rows[0]?.id ?? "";
|
|
7311
|
+
if (!id) {
|
|
7312
|
+
throw new Error("Failed to upsert account search slice in Neon: missing slice id.");
|
|
7313
|
+
}
|
|
7314
|
+
return id;
|
|
7315
|
+
}
|
|
7316
|
+
const { data, error } = await params.store.client
|
|
7317
|
+
.from("linkedin_sales_nav_account_search_slices")
|
|
7318
|
+
.upsert({
|
|
7319
|
+
run_id: params.runId,
|
|
7320
|
+
org_id: params.orgId,
|
|
7321
|
+
sliced_query_url: params.attempt.slicedQueryUrl,
|
|
7322
|
+
applied_filters: params.attempt.appliedFilters,
|
|
7323
|
+
split_trail: params.attempt.splitTrail,
|
|
7324
|
+
depth: params.attempt.depth,
|
|
7325
|
+
status: params.status,
|
|
7326
|
+
total_results: params.totalResults,
|
|
7327
|
+
fetched_pages: params.fetchedPages,
|
|
7328
|
+
stored_accounts: params.storedAccounts,
|
|
7329
|
+
retry_count: params.retryCount,
|
|
7330
|
+
retry_delay_ms: params.retryDelayMs,
|
|
7331
|
+
last_error: params.lastError ?? null,
|
|
7332
|
+
raw_payload: params.rawPayload,
|
|
7333
|
+
}, { onConflict: "run_id,sliced_query_url" })
|
|
7334
|
+
.select("id")
|
|
7335
|
+
.single();
|
|
7336
|
+
if (error) {
|
|
7337
|
+
throw new Error(`Failed to upsert account search slice in Neon: ${error.message}`);
|
|
7338
|
+
}
|
|
7339
|
+
const id = typeof data?.id === "string" ? data.id : "";
|
|
7340
|
+
if (!id) {
|
|
7341
|
+
throw new Error("Failed to upsert account search slice in Neon: missing slice id.");
|
|
7342
|
+
}
|
|
7343
|
+
return id;
|
|
7344
|
+
}
|
|
7345
|
+
function stripPostgresUnsafeStringBytes(value) {
|
|
7346
|
+
return value.replace(/\u0000/g, "");
|
|
7347
|
+
}
|
|
7348
|
+
function sanitizeForPostgresJson(value) {
|
|
7349
|
+
if (typeof value === "string") {
|
|
7350
|
+
return stripPostgresUnsafeStringBytes(value);
|
|
7351
|
+
}
|
|
7352
|
+
if (Array.isArray(value)) {
|
|
7353
|
+
return value.map((item) => sanitizeForPostgresJson(item));
|
|
7354
|
+
}
|
|
7355
|
+
if (typeof value === "object" && value !== null) {
|
|
7356
|
+
const sanitized = {};
|
|
7357
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
7358
|
+
sanitized[stripPostgresUnsafeStringBytes(key)] =
|
|
7359
|
+
sanitizeForPostgresJson(nested);
|
|
6350
7360
|
}
|
|
6351
|
-
|
|
7361
|
+
return sanitized;
|
|
7362
|
+
}
|
|
6352
7363
|
return value;
|
|
6353
7364
|
}
|
|
7365
|
+
function sanitizeOptionalPostgresText(value) {
|
|
7366
|
+
return typeof value === "string" ? stripPostgresUnsafeStringBytes(value) : null;
|
|
7367
|
+
}
|
|
7368
|
+
async function upsertSalesNavigatorAccounts(params) {
|
|
7369
|
+
if (params.accounts.length === 0) {
|
|
7370
|
+
return 0;
|
|
7371
|
+
}
|
|
7372
|
+
const rows = params.accounts.map((account) => ({
|
|
7373
|
+
org_id: params.orgId,
|
|
7374
|
+
last_run_id: params.runId,
|
|
7375
|
+
last_slice_id: params.sliceId,
|
|
7376
|
+
account_key: stripPostgresUnsafeStringBytes(account.accountKey),
|
|
7377
|
+
sales_nav_company_url: sanitizeOptionalPostgresText(account.salesNavCompanyUrl),
|
|
7378
|
+
linkedin_company_url: sanitizeOptionalPostgresText(account.linkedinCompanyUrl),
|
|
7379
|
+
company_id: sanitizeOptionalPostgresText(account.companyId),
|
|
7380
|
+
company_name: sanitizeOptionalPostgresText(account.companyName),
|
|
7381
|
+
industry: sanitizeOptionalPostgresText(account.industry),
|
|
7382
|
+
location: sanitizeOptionalPostgresText(account.location),
|
|
7383
|
+
headquarters: sanitizeOptionalPostgresText(account.headquarters),
|
|
7384
|
+
employee_count: typeof account.employeeCount === "number" ? account.employeeCount : null,
|
|
7385
|
+
follower_count: typeof account.followerCount === "number" ? account.followerCount : null,
|
|
7386
|
+
company_type: sanitizeOptionalPostgresText(account.companyType),
|
|
7387
|
+
description: sanitizeOptionalPostgresText(account.description),
|
|
7388
|
+
website: sanitizeOptionalPostgresText(account.website),
|
|
7389
|
+
search_query_url: sanitizeOptionalPostgresText(account.searchQueryUrl),
|
|
7390
|
+
scraped_at: typeof account.scrapedAt === "string"
|
|
7391
|
+
? stripPostgresUnsafeStringBytes(account.scrapedAt)
|
|
7392
|
+
: new Date().toISOString(),
|
|
7393
|
+
raw_payload: sanitizeForPostgresJson(typeof account.rawLocalSalesNavigatorAccountResult === "object" &&
|
|
7394
|
+
account.rawLocalSalesNavigatorAccountResult !== null
|
|
7395
|
+
? account.rawLocalSalesNavigatorAccountResult
|
|
7396
|
+
: account),
|
|
7397
|
+
}));
|
|
7398
|
+
if (params.store.kind === "postgres") {
|
|
7399
|
+
for (const row of rows) {
|
|
7400
|
+
await params.store.client.query(`INSERT INTO public.linkedin_sales_nav_accounts (
|
|
7401
|
+
org_id, last_run_id, last_slice_id, account_key, sales_nav_company_url,
|
|
7402
|
+
linkedin_company_url, company_id, company_name, industry, location, headquarters,
|
|
7403
|
+
employee_count, follower_count, company_type, description, website, search_query_url,
|
|
7404
|
+
scraped_at, raw_payload
|
|
7405
|
+
)
|
|
7406
|
+
VALUES (
|
|
7407
|
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16,
|
|
7408
|
+
$17, $18, $19::jsonb
|
|
7409
|
+
)
|
|
7410
|
+
ON CONFLICT (org_id, account_key)
|
|
7411
|
+
DO UPDATE SET
|
|
7412
|
+
last_run_id = excluded.last_run_id,
|
|
7413
|
+
last_slice_id = excluded.last_slice_id,
|
|
7414
|
+
sales_nav_company_url = excluded.sales_nav_company_url,
|
|
7415
|
+
linkedin_company_url = excluded.linkedin_company_url,
|
|
7416
|
+
company_id = excluded.company_id,
|
|
7417
|
+
company_name = excluded.company_name,
|
|
7418
|
+
industry = excluded.industry,
|
|
7419
|
+
location = excluded.location,
|
|
7420
|
+
headquarters = excluded.headquarters,
|
|
7421
|
+
employee_count = excluded.employee_count,
|
|
7422
|
+
follower_count = excluded.follower_count,
|
|
7423
|
+
company_type = excluded.company_type,
|
|
7424
|
+
description = excluded.description,
|
|
7425
|
+
website = excluded.website,
|
|
7426
|
+
search_query_url = excluded.search_query_url,
|
|
7427
|
+
scraped_at = excluded.scraped_at,
|
|
7428
|
+
raw_payload = excluded.raw_payload`, [
|
|
7429
|
+
row.org_id,
|
|
7430
|
+
row.last_run_id,
|
|
7431
|
+
row.last_slice_id,
|
|
7432
|
+
row.account_key,
|
|
7433
|
+
row.sales_nav_company_url,
|
|
7434
|
+
row.linkedin_company_url,
|
|
7435
|
+
row.company_id,
|
|
7436
|
+
row.company_name,
|
|
7437
|
+
row.industry,
|
|
7438
|
+
row.location,
|
|
7439
|
+
row.headquarters,
|
|
7440
|
+
row.employee_count,
|
|
7441
|
+
row.follower_count,
|
|
7442
|
+
row.company_type,
|
|
7443
|
+
row.description,
|
|
7444
|
+
row.website,
|
|
7445
|
+
row.search_query_url,
|
|
7446
|
+
row.scraped_at,
|
|
7447
|
+
JSON.stringify(row.raw_payload),
|
|
7448
|
+
]);
|
|
7449
|
+
}
|
|
7450
|
+
return rows.length;
|
|
7451
|
+
}
|
|
7452
|
+
const { error } = await params.store.client
|
|
7453
|
+
.from("linkedin_sales_nav_accounts")
|
|
7454
|
+
.upsert(rows, { onConflict: "org_id,account_key" });
|
|
7455
|
+
if (error) {
|
|
7456
|
+
throw new Error(`Failed to upsert Sales Navigator accounts in Neon: ${error.message}`);
|
|
7457
|
+
}
|
|
7458
|
+
return rows.length;
|
|
7459
|
+
}
|
|
7460
|
+
function asJsonObject(value) {
|
|
7461
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
7462
|
+
? value
|
|
7463
|
+
: {};
|
|
7464
|
+
}
|
|
7465
|
+
function asJsonArray(value) {
|
|
7466
|
+
return Array.isArray(value) ? value : [];
|
|
7467
|
+
}
|
|
7468
|
+
function asOptionalNumber(value) {
|
|
7469
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7470
|
+
}
|
|
7471
|
+
function resolveAccountRepairStartOffset(slice) {
|
|
7472
|
+
const rawPayload = asJsonObject(slice.rawPayload);
|
|
7473
|
+
const stop = asJsonObject(rawPayload.stop ?? asJsonObject(rawPayload.pacing).stop);
|
|
7474
|
+
const nextStart = asOptionalNumber(stop.nextStart) ??
|
|
7475
|
+
asOptionalNumber(stop.stoppedAtStart);
|
|
7476
|
+
if (nextStart != null) {
|
|
7477
|
+
return Math.max(0, Math.trunc(nextStart));
|
|
7478
|
+
}
|
|
7479
|
+
return Math.max(0, Math.trunc(slice.storedAccounts));
|
|
7480
|
+
}
|
|
7481
|
+
function repairSliceToCrawlAttempt(slice) {
|
|
7482
|
+
return {
|
|
7483
|
+
sourceQueryUrl: slice.sourceQueryUrl,
|
|
7484
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
7485
|
+
appliedFilters: slice.appliedFilters,
|
|
7486
|
+
depth: slice.depth,
|
|
7487
|
+
retryCount: slice.retryCount,
|
|
7488
|
+
maxResultsPerSearch: slice.maxResultsPerSearch,
|
|
7489
|
+
numberOfProfiles: slice.pageSize,
|
|
7490
|
+
slicePreset: slice.slicePreset,
|
|
7491
|
+
splitTrail: slice.splitTrail,
|
|
7492
|
+
};
|
|
7493
|
+
}
|
|
7494
|
+
async function listSalesNavigatorAccountSearchRepairSlices(params) {
|
|
7495
|
+
if (params.store.kind !== "postgres") {
|
|
7496
|
+
throw new Error("Account run repair requires direct Postgres access. Set DATABASE_URL or SALESPROMPTER_DATABASE_URL.");
|
|
7497
|
+
}
|
|
7498
|
+
const result = await params.store.client.query(`SELECT
|
|
7499
|
+
slices.id::text,
|
|
7500
|
+
slices.org_id,
|
|
7501
|
+
slices.run_id::text,
|
|
7502
|
+
runs.source_query_url,
|
|
7503
|
+
slices.sliced_query_url,
|
|
7504
|
+
slices.applied_filters,
|
|
7505
|
+
slices.split_trail,
|
|
7506
|
+
slices.depth,
|
|
7507
|
+
slices.status,
|
|
7508
|
+
slices.total_results,
|
|
7509
|
+
slices.fetched_pages,
|
|
7510
|
+
slices.stored_accounts,
|
|
7511
|
+
slices.retry_count,
|
|
7512
|
+
slices.retry_delay_ms,
|
|
7513
|
+
slices.last_error,
|
|
7514
|
+
slices.raw_payload,
|
|
7515
|
+
runs.slice_preset,
|
|
7516
|
+
runs.max_results_per_search,
|
|
7517
|
+
runs.page_size
|
|
7518
|
+
FROM public.linkedin_sales_nav_account_search_slices AS slices
|
|
7519
|
+
INNER JOIN public.linkedin_sales_nav_account_search_runs AS runs
|
|
7520
|
+
ON runs.id = slices.run_id
|
|
7521
|
+
WHERE slices.run_id = $1
|
|
7522
|
+
AND slices.status IN ('failed', 'unresolved')
|
|
7523
|
+
ORDER BY
|
|
7524
|
+
CASE WHEN slices.stored_accounts > 0 THEN 0 ELSE 1 END,
|
|
7525
|
+
slices.depth ASC,
|
|
7526
|
+
slices.updated_at ASC
|
|
7527
|
+
LIMIT $2`, [params.runId, params.limit]);
|
|
7528
|
+
return result.rows.map((row) => ({
|
|
7529
|
+
id: String(row.id),
|
|
7530
|
+
orgId: String(row.org_id),
|
|
7531
|
+
runId: String(row.run_id),
|
|
7532
|
+
sourceQueryUrl: String(row.source_query_url ?? row.sliced_query_url),
|
|
7533
|
+
slicedQueryUrl: String(row.sliced_query_url),
|
|
7534
|
+
appliedFilters: asJsonArray(row.applied_filters),
|
|
7535
|
+
splitTrail: asJsonArray(row.split_trail),
|
|
7536
|
+
depth: Number(row.depth ?? 0),
|
|
7537
|
+
status: row.status === "unresolved" ? "unresolved" : "failed",
|
|
7538
|
+
totalResults: typeof row.total_results === "number" ? row.total_results : null,
|
|
7539
|
+
fetchedPages: Number(row.fetched_pages ?? 0),
|
|
7540
|
+
storedAccounts: Number(row.stored_accounts ?? 0),
|
|
7541
|
+
retryCount: Number(row.retry_count ?? 0),
|
|
7542
|
+
retryDelayMs: Number(row.retry_delay_ms ?? 0),
|
|
7543
|
+
lastError: typeof row.last_error === "string" ? row.last_error : null,
|
|
7544
|
+
rawPayload: asJsonObject(row.raw_payload),
|
|
7545
|
+
slicePreset: String(row.slice_preset ?? "account-search-split"),
|
|
7546
|
+
maxResultsPerSearch: Number(row.max_results_per_search ?? 1000),
|
|
7547
|
+
pageSize: Number(row.page_size ?? 25),
|
|
7548
|
+
}));
|
|
7549
|
+
}
|
|
7550
|
+
async function summarizeSalesNavigatorAccountSearchRunFromSlices(params) {
|
|
7551
|
+
if (params.store.kind !== "postgres") {
|
|
7552
|
+
throw new Error("Account run summary requires direct Postgres access. Set DATABASE_URL or SALESPROMPTER_DATABASE_URL.");
|
|
7553
|
+
}
|
|
7554
|
+
const result = await params.store.client.query(`SELECT
|
|
7555
|
+
count(*) FILTER (WHERE status <> 'split')::int AS total_slices,
|
|
7556
|
+
COALESCE(sum(stored_accounts) FILTER (WHERE status <> 'split'), 0)::int AS stored_accounts,
|
|
7557
|
+
count(*) FILTER (WHERE status <> 'split')::int AS attempted_slices,
|
|
7558
|
+
count(*) FILTER (WHERE status = 'split')::int AS split_events,
|
|
7559
|
+
count(*) FILTER (WHERE status = 'failed')::int AS failed_slices,
|
|
7560
|
+
count(*) FILTER (WHERE status = 'unresolved')::int AS unresolved_slices,
|
|
7561
|
+
(
|
|
7562
|
+
SELECT last_error
|
|
7563
|
+
FROM public.linkedin_sales_nav_account_search_slices
|
|
7564
|
+
WHERE run_id = $1
|
|
7565
|
+
AND status IN ('failed', 'unresolved')
|
|
7566
|
+
ORDER BY updated_at DESC
|
|
7567
|
+
LIMIT 1
|
|
7568
|
+
) AS last_error
|
|
7569
|
+
FROM public.linkedin_sales_nav_account_search_slices
|
|
7570
|
+
WHERE run_id = $1`, [params.runId]);
|
|
7571
|
+
const row = result.rows[0] ?? {};
|
|
7572
|
+
const failedSlices = Number(row.failed_slices ?? 0);
|
|
7573
|
+
const unresolvedSlices = Number(row.unresolved_slices ?? 0);
|
|
7574
|
+
return {
|
|
7575
|
+
totalSlices: Number(row.total_slices ?? 0),
|
|
7576
|
+
storedAccounts: Number(row.stored_accounts ?? 0),
|
|
7577
|
+
attemptedSlices: Number(row.attempted_slices ?? 0),
|
|
7578
|
+
splitEvents: Number(row.split_events ?? 0),
|
|
7579
|
+
failedSlices,
|
|
7580
|
+
unresolvedSlices,
|
|
7581
|
+
truncated: failedSlices > 0 || unresolvedSlices > 0,
|
|
7582
|
+
lastError: typeof row.last_error === "string" ? row.last_error : null,
|
|
7583
|
+
};
|
|
7584
|
+
}
|
|
7585
|
+
async function resolveOptionalAppSyncSession() {
|
|
7586
|
+
const envToken = resolveNonInteractiveAuthToken(process.env);
|
|
7587
|
+
if (envToken) {
|
|
7588
|
+
return await loginWithToken(envToken, process.env.SALESPROMPTER_API_BASE_URL?.trim());
|
|
7589
|
+
}
|
|
7590
|
+
const session = await readAuthSession();
|
|
7591
|
+
if (!session) {
|
|
7592
|
+
return null;
|
|
7593
|
+
}
|
|
7594
|
+
if (session.expiresAt !== undefined &&
|
|
7595
|
+
Date.now() >= Date.parse(session.expiresAt)) {
|
|
7596
|
+
if (canPromptForInteractiveLogin()) {
|
|
7597
|
+
await ensureInteractiveAuthSession(session.apiBaseUrl);
|
|
7598
|
+
return await readAuthSession();
|
|
7599
|
+
}
|
|
7600
|
+
return null;
|
|
7601
|
+
}
|
|
7602
|
+
return session;
|
|
7603
|
+
}
|
|
6354
7604
|
async function syncPhantombusterContainersViaApp(session, payload) {
|
|
6355
7605
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/phantombuster/containers/sync`, {
|
|
6356
7606
|
method: "POST",
|
|
@@ -6514,16 +7764,6 @@ async function runSalesNavigatorExport(session, payload, traceId, logOptions = {
|
|
|
6514
7764
|
}
|
|
6515
7765
|
}
|
|
6516
7766
|
async function startSalesNavigatorExport(session, payload, traceId) {
|
|
6517
|
-
const exportSession = await resolveSalesNavigatorExportSessionOverride(payload.slicedQueryUrl, {
|
|
6518
|
-
source: "cli_salesnav_export_start"
|
|
6519
|
-
});
|
|
6520
|
-
const sessionOverridePayload = exportSession
|
|
6521
|
-
? {
|
|
6522
|
-
sessionCookie: exportSession.sessionCookie,
|
|
6523
|
-
selectedSessionCookieSha256: exportSession.selectedSessionCookieSha256,
|
|
6524
|
-
selectedSessionUserAgent: exportSession.selectedSessionUserAgent
|
|
6525
|
-
}
|
|
6526
|
-
: {};
|
|
6527
7767
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/export`, {
|
|
6528
7768
|
method: "POST",
|
|
6529
7769
|
signal: AbortSignal.timeout(SALES_NAVIGATOR_EXPORT_START_TIMEOUT_MS),
|
|
@@ -6534,8 +7774,7 @@ async function startSalesNavigatorExport(session, payload, traceId) {
|
|
|
6534
7774
|
},
|
|
6535
7775
|
body: JSON.stringify({
|
|
6536
7776
|
...payload,
|
|
6537
|
-
appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters)
|
|
6538
|
-
...sessionOverridePayload
|
|
7777
|
+
appliedFilters: serializeSalesNavigatorFiltersForApi(payload.appliedFilters)
|
|
6539
7778
|
})
|
|
6540
7779
|
}), SalesNavigatorExportStartResponseSchema);
|
|
6541
7780
|
}
|
|
@@ -7057,6 +8296,73 @@ async function getSalesNavigatorCrawlStatus(session, jobId, traceId) {
|
|
|
7057
8296
|
}
|
|
7058
8297
|
}), SalesNavigatorCrawlStatusResponseSchema);
|
|
7059
8298
|
}
|
|
8299
|
+
async function getSalesNavigatorAccountRunStatus(session, runId, options = {}, traceId) {
|
|
8300
|
+
const params = new URLSearchParams();
|
|
8301
|
+
if (options.cooldownSeconds != null) {
|
|
8302
|
+
params.set("cooldownSeconds", String(options.cooldownSeconds));
|
|
8303
|
+
}
|
|
8304
|
+
const query = params.toString();
|
|
8305
|
+
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/accounts/runs/${runId}${query ? `?${query}` : ""}`, {
|
|
8306
|
+
method: "GET",
|
|
8307
|
+
headers: {
|
|
8308
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
8309
|
+
...buildTraceHeaders(traceId)
|
|
8310
|
+
}
|
|
8311
|
+
}), SalesNavigatorAccountRunStatusResponseSchema);
|
|
8312
|
+
}
|
|
8313
|
+
async function startSalesNavigatorAccountRunWorkflow(session, runId, options = {}, traceId) {
|
|
8314
|
+
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/accounts/runs/${runId}/workflow`, {
|
|
8315
|
+
method: "POST",
|
|
8316
|
+
headers: {
|
|
8317
|
+
"Content-Type": "application/json",
|
|
8318
|
+
Authorization: `Bearer ${currentSession.accessToken}`,
|
|
8319
|
+
...buildTraceHeaders(traceId)
|
|
8320
|
+
},
|
|
8321
|
+
body: JSON.stringify({
|
|
8322
|
+
runId,
|
|
8323
|
+
...(options.cooldownSeconds != null
|
|
8324
|
+
? { cooldownSeconds: options.cooldownSeconds }
|
|
8325
|
+
: {})
|
|
8326
|
+
})
|
|
8327
|
+
}), SalesNavigatorAccountWorkflowStartResponseSchema);
|
|
8328
|
+
}
|
|
8329
|
+
function buildSalesNavigatorAccountRepairCommand(params) {
|
|
8330
|
+
const parts = ["salesprompter", "accounts:repair", "--run-id", params.runId];
|
|
8331
|
+
if (params.curlFile) {
|
|
8332
|
+
parts.push("--curl-file", params.curlFile);
|
|
8333
|
+
}
|
|
8334
|
+
else {
|
|
8335
|
+
parts.push("--curl-file", "<account-search-curl-file>");
|
|
8336
|
+
}
|
|
8337
|
+
return parts.join(" ");
|
|
8338
|
+
}
|
|
8339
|
+
async function tryStartSalesNavigatorAccountWorkflow(params) {
|
|
8340
|
+
const session = await resolveOptionalAppSyncSession();
|
|
8341
|
+
if (!session) {
|
|
8342
|
+
return {
|
|
8343
|
+
started: false,
|
|
8344
|
+
workflowRunId: null,
|
|
8345
|
+
error: "No saved Salesprompter app auth session is available.",
|
|
8346
|
+
};
|
|
8347
|
+
}
|
|
8348
|
+
try {
|
|
8349
|
+
const workflow = await startSalesNavigatorAccountRunWorkflow(session, params.runId, {
|
|
8350
|
+
cooldownSeconds: params.cooldownSeconds,
|
|
8351
|
+
});
|
|
8352
|
+
return {
|
|
8353
|
+
started: true,
|
|
8354
|
+
workflowRunId: workflow.value.workflowRunId,
|
|
8355
|
+
error: null,
|
|
8356
|
+
};
|
|
8357
|
+
}
|
|
8358
|
+
catch (error) {
|
|
8359
|
+
return {
|
|
8360
|
+
started: false,
|
|
8361
|
+
workflowRunId: null,
|
|
8362
|
+
error: error instanceof Error ? error.message : String(error),
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
}
|
|
7060
8366
|
async function claimNextSalesNavigatorCrawlSlice(session, jobId, traceId) {
|
|
7061
8367
|
return await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/salesnav/crawls/${jobId}/claim-next`, {
|
|
7062
8368
|
method: "POST",
|
|
@@ -7342,95 +8648,25 @@ function buildSalesNavigatorSliceFailureReport(slice, error, options) {
|
|
|
7342
8648
|
function formatSalesNavigatorSplitTrail(splitTrail) {
|
|
7343
8649
|
return splitTrail.map((entry) => `${entry.key}:${entry.value.text}`);
|
|
7344
8650
|
}
|
|
7345
|
-
let cachedSalesNavigatorExportSessionOverride = null;
|
|
7346
|
-
function hashSalesNavigatorSessionCookieForPhantombuster(sessionCookie) {
|
|
7347
|
-
const liAt = extractLiAtCookieValue(sessionCookie);
|
|
7348
|
-
if (!liAt) {
|
|
7349
|
-
return null;
|
|
7350
|
-
}
|
|
7351
|
-
return createHash("sha256").update(liAt).digest("hex");
|
|
7352
|
-
}
|
|
7353
|
-
function cacheSalesNavigatorExportSessionOverride(queryUrl, value) {
|
|
7354
|
-
cachedSalesNavigatorExportSessionOverride = {
|
|
7355
|
-
queryUrl,
|
|
7356
|
-
expiresAt: Date.now() + 120_000,
|
|
7357
|
-
value
|
|
7358
|
-
};
|
|
7359
|
-
return value;
|
|
7360
|
-
}
|
|
7361
|
-
async function resolveSalesNavigatorExportSessionOverride(queryUrl, options) {
|
|
7362
|
-
if (cachedSalesNavigatorExportSessionOverride &&
|
|
7363
|
-
cachedSalesNavigatorExportSessionOverride.queryUrl === queryUrl &&
|
|
7364
|
-
cachedSalesNavigatorExportSessionOverride.expiresAt > Date.now()) {
|
|
7365
|
-
return cachedSalesNavigatorExportSessionOverride.value;
|
|
7366
|
-
}
|
|
7367
|
-
const localExtensionConfig = await readLocalLinkedInExtensionDirectLookupConfig();
|
|
7368
|
-
if (localExtensionConfig?.cookie) {
|
|
7369
|
-
const selectedSessionCookieSha256 = hashSalesNavigatorSessionCookieForPhantombuster(localExtensionConfig.cookie);
|
|
7370
|
-
if (process.env.SALESPROMPTER_SALESNAV_EXPORT_SKIP_EXTENSION_PREFLIGHT === "1") {
|
|
7371
|
-
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
7372
|
-
sessionCookie: localExtensionConfig.cookie,
|
|
7373
|
-
selectedSessionCookieSha256,
|
|
7374
|
-
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
7375
|
-
source: "chrome_extension"
|
|
7376
|
-
});
|
|
7377
|
-
}
|
|
7378
|
-
const probe = await probeSalesNavigatorSearchSession(localExtensionConfig.cookie, queryUrl, {
|
|
7379
|
-
timeoutMs: 8000
|
|
7380
|
-
});
|
|
7381
|
-
await options.logger?.log("salesnav.export.session.chrome_extension.preflight", {
|
|
7382
|
-
source: options.source,
|
|
7383
|
-
queryUrl,
|
|
7384
|
-
status: probe.status,
|
|
7385
|
-
selectedSessionCookieSha256,
|
|
7386
|
-
finalUrl: probe.finalUrl,
|
|
7387
|
-
validationError: probe.validationError
|
|
7388
|
-
});
|
|
7389
|
-
if (probe.status === "ok") {
|
|
7390
|
-
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
7391
|
-
sessionCookie: localExtensionConfig.cookie,
|
|
7392
|
-
selectedSessionCookieSha256,
|
|
7393
|
-
selectedSessionUserAgent: localExtensionConfig.userAgent,
|
|
7394
|
-
source: "chrome_extension"
|
|
7395
|
-
});
|
|
7396
|
-
}
|
|
7397
|
-
}
|
|
7398
|
-
const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
|
|
7399
|
-
queryUrl,
|
|
7400
|
-
source: options.source,
|
|
7401
|
-
env: process.env
|
|
7402
|
-
});
|
|
7403
|
-
if (claimed?.sessionCookie) {
|
|
7404
|
-
return cacheSalesNavigatorExportSessionOverride(queryUrl, {
|
|
7405
|
-
sessionCookie: claimed.sessionCookie,
|
|
7406
|
-
selectedSessionCookieSha256: claimed.sessionCookieSha256 ??
|
|
7407
|
-
hashSalesNavigatorSessionCookieForPhantombuster(claimed.sessionCookie),
|
|
7408
|
-
selectedSessionUserAgent: null,
|
|
7409
|
-
source: "session_vault"
|
|
7410
|
-
});
|
|
7411
|
-
}
|
|
7412
|
-
if (localExtensionConfig?.cookie &&
|
|
7413
|
-
process.env.SALESPROMPTER_SALESNAV_EXPORT_REQUIRE_FRESH_SESSION === "1") {
|
|
7414
|
-
throw new Error("The local Salesprompter Chrome extension session cookie is not valid for Sales Navigator. Reconnect the extension, then retry the CLI command.");
|
|
7415
|
-
}
|
|
7416
|
-
if (process.env.SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS === "1") {
|
|
7417
|
-
throw new Error("No validated LinkedIn Sales Navigator session cookie is available from the CLI-managed session pool.");
|
|
7418
|
-
}
|
|
7419
|
-
return null;
|
|
7420
|
-
}
|
|
7421
8651
|
async function ensureSalesNavigatorSessionPoolReady(queryUrl, options) {
|
|
7422
8652
|
try {
|
|
7423
8653
|
await options.logger?.log("salesnav.session_pool.preflight.started", {
|
|
7424
8654
|
source: options.source,
|
|
7425
8655
|
queryUrl
|
|
7426
8656
|
});
|
|
7427
|
-
const claimed = await
|
|
8657
|
+
const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
|
|
8658
|
+
queryUrl,
|
|
8659
|
+
source: options.source,
|
|
8660
|
+
env: process.env
|
|
8661
|
+
});
|
|
7428
8662
|
await options.logger?.log("salesnav.session_pool.preflight.completed", {
|
|
7429
8663
|
source: options.source,
|
|
7430
8664
|
queryUrl,
|
|
7431
8665
|
status: claimed ? "ok" : "skipped",
|
|
7432
|
-
|
|
7433
|
-
|
|
8666
|
+
selectedSessionUserEmail: claimed?.userEmail ?? null,
|
|
8667
|
+
selectedSessionUserHandle: claimed?.userHandle ?? null,
|
|
8668
|
+
selectedSessionCookieSha256: claimed?.sessionCookieSha256 ?? null,
|
|
8669
|
+
selectedSessionLastIngestedSource: claimed?.lastIngestedSource ?? null
|
|
7434
8670
|
});
|
|
7435
8671
|
return {
|
|
7436
8672
|
ready: true
|
|
@@ -9357,6 +10593,12 @@ program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
9357
10593
|
commandName === "llm:ready" ||
|
|
9358
10594
|
commandName === "contacts:find-linkedin-urls" ||
|
|
9359
10595
|
commandName === "companies:find-linkedin-urls" ||
|
|
10596
|
+
commandName === "salesnav:accounts:split" ||
|
|
10597
|
+
commandName === "accounts:split" ||
|
|
10598
|
+
commandName === "salesnav:accounts:local-import" ||
|
|
10599
|
+
commandName === "accounts:local-import" ||
|
|
10600
|
+
commandName === "salesnav:accounts:repair" ||
|
|
10601
|
+
commandName === "accounts:repair" ||
|
|
9360
10602
|
commandName.startsWith("packs:") ||
|
|
9361
10603
|
((commandName === "list" || commandName === "add") && parentCommandName === "packs")) {
|
|
9362
10604
|
return;
|
|
@@ -9784,7 +11026,7 @@ program
|
|
|
9784
11026
|
const titleLimit = options.titleLimit === undefined
|
|
9785
11027
|
? undefined
|
|
9786
11028
|
: z.coerce.number().int().min(1).max(1000).parse(options.titleLimit);
|
|
9787
|
-
const maxResultsPerSearch = z.coerce.number().int().min(1).max(
|
|
11029
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
9788
11030
|
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
9789
11031
|
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
9790
11032
|
const maxSlicesPerTitle = z.coerce.number().int().min(1).max(10000).parse(options.maxSlicesPerTitle);
|
|
@@ -10329,62 +11571,1061 @@ program
|
|
|
10329
11571
|
printOutput(payload);
|
|
10330
11572
|
});
|
|
10331
11573
|
program
|
|
10332
|
-
.command("salesnav:
|
|
10333
|
-
.alias("
|
|
10334
|
-
.description("
|
|
10335
|
-
.
|
|
10336
|
-
.option("--
|
|
10337
|
-
.option("--
|
|
10338
|
-
.option("--
|
|
10339
|
-
.option("--
|
|
10340
|
-
.option("--
|
|
10341
|
-
.option("--max-split-depth <number>", "Maximum number of adaptive split dimensions to use", "
|
|
10342
|
-
.option("--max-slices <number>", "Safety cap for total
|
|
10343
|
-
.option("--
|
|
10344
|
-
.option("--
|
|
10345
|
-
.option("--
|
|
10346
|
-
.option("--
|
|
10347
|
-
.option("--
|
|
10348
|
-
.option("--
|
|
10349
|
-
.option("--
|
|
10350
|
-
.option("--
|
|
11574
|
+
.command("salesnav:accounts:split")
|
|
11575
|
+
.alias("accounts:split")
|
|
11576
|
+
.description("Split a Sales Navigator account search into account slices below the result cap.")
|
|
11577
|
+
.requiredOption("--query-url <url>", "LinkedIn Sales Navigator account search URL")
|
|
11578
|
+
.option("--curl-file <path>", "Path to a saved account-search curl request used as the local request template")
|
|
11579
|
+
.option("--org-id <id>", "Salesprompter workspace org id for Neon storage. Defaults to SALESPROMPTER_ORG_ID or saved auth session org.")
|
|
11580
|
+
.option("--max-results-per-search <number>", "Maximum account results allowed for each final slice", "1000")
|
|
11581
|
+
.option("--max-windowed-final-results <number>", "Bounded fallback fetch for an oversized final account slice when no split dimensions remain", "1000")
|
|
11582
|
+
.option("--no-windowed-final-slices", "Do not fetch pageable rows for oversized final account slices after adaptive splitting is exhausted")
|
|
11583
|
+
.option("--max-split-depth <number>", "Maximum number of adaptive account split dimensions to use", "3")
|
|
11584
|
+
.option("--max-slices <number>", "Safety cap for total account slices probed in this invocation", "1000")
|
|
11585
|
+
.option("--probe-count <number>", "Accounts to request while probing each slice count", "1")
|
|
11586
|
+
.option("--number-of-accounts <number>", "Maximum accounts to fetch per final slice, or 'all' for the full reported slice", "all")
|
|
11587
|
+
.option("--page-size <number>", "Accounts requested per Sales Navigator account-search API page", "25")
|
|
11588
|
+
.option("--page-delay-min-ms <number>", "Minimum randomized delay between account-search result pages", "1500")
|
|
11589
|
+
.option("--page-delay-max-ms <number>", "Maximum randomized delay between account-search result pages", "6000")
|
|
11590
|
+
.option("--max-retries <number>", "Bounded retries for transient LinkedIn probe failures", "1")
|
|
11591
|
+
.option("--retry-base-delay-ms <number>", "Base delay for full-jitter retry backoff", "1500")
|
|
11592
|
+
.option("--retry-max-delay-ms <number>", "Maximum delay for full-jitter retry backoff", "10000")
|
|
10351
11593
|
.option("--out <path>", "Optional local JSON output path")
|
|
10352
|
-
.option("--
|
|
10353
|
-
.option("--
|
|
11594
|
+
.option("--dry-run", "Preview static account split candidates without calling LinkedIn", false)
|
|
11595
|
+
.option("--plan-only", "Probe account-search counts and print adaptive final batches without storing rows", false)
|
|
11596
|
+
.option("--no-start-workflow", "Do not start the durable app workflow when the account run needs cooldown/repair")
|
|
11597
|
+
.option("--workflow-cooldown-seconds <number>", "Seconds the app workflow should wait after a rate-limit before asking the CLI to repair", "3600")
|
|
10354
11598
|
.action(async (options) => {
|
|
10355
|
-
const queryUrl = z.string().url().
|
|
10356
|
-
const
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
const
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
const
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
11599
|
+
const queryUrl = z.string().url().parse(options.queryUrl);
|
|
11600
|
+
const maxResultsPerSearch = z.coerce
|
|
11601
|
+
.number()
|
|
11602
|
+
.int()
|
|
11603
|
+
.min(1)
|
|
11604
|
+
.max(1000)
|
|
11605
|
+
.parse(options.maxResultsPerSearch);
|
|
11606
|
+
const maxWindowedFinalResults = z.coerce
|
|
11607
|
+
.number()
|
|
11608
|
+
.int()
|
|
11609
|
+
.min(maxResultsPerSearch)
|
|
11610
|
+
.max(1000)
|
|
11611
|
+
.parse(options.maxWindowedFinalResults);
|
|
11612
|
+
const maxSplitDepth = z.coerce
|
|
11613
|
+
.number()
|
|
11614
|
+
.int()
|
|
11615
|
+
.min(1)
|
|
11616
|
+
.max(DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS.length)
|
|
11617
|
+
.parse(options.maxSplitDepth);
|
|
11618
|
+
const maxSlices = z.coerce
|
|
11619
|
+
.number()
|
|
11620
|
+
.int()
|
|
11621
|
+
.min(1)
|
|
11622
|
+
.max(10000)
|
|
11623
|
+
.parse(options.maxSlices);
|
|
11624
|
+
const workflowCooldownSeconds = z.coerce
|
|
11625
|
+
.number()
|
|
11626
|
+
.int()
|
|
11627
|
+
.min(60)
|
|
11628
|
+
.max(86_400)
|
|
11629
|
+
.parse(options.workflowCooldownSeconds);
|
|
11630
|
+
const probeCount = z.coerce
|
|
11631
|
+
.number()
|
|
11632
|
+
.int()
|
|
11633
|
+
.min(1)
|
|
11634
|
+
.max(100)
|
|
11635
|
+
.parse(options.probeCount);
|
|
11636
|
+
const numberOfAccountsOption = String(options.numberOfAccounts ?? "all")
|
|
11637
|
+
.trim()
|
|
11638
|
+
.toLowerCase();
|
|
11639
|
+
const requestedAccounts = numberOfAccountsOption === "all"
|
|
11640
|
+
? null
|
|
11641
|
+
: z.coerce
|
|
11642
|
+
.number()
|
|
11643
|
+
.int()
|
|
11644
|
+
.min(1)
|
|
11645
|
+
.max(100000)
|
|
11646
|
+
.parse(options.numberOfAccounts);
|
|
11647
|
+
const pageSize = z.coerce
|
|
11648
|
+
.number()
|
|
11649
|
+
.int()
|
|
11650
|
+
.min(1)
|
|
11651
|
+
.max(100)
|
|
11652
|
+
.parse(options.pageSize);
|
|
11653
|
+
const pageDelayMinMs = z.coerce
|
|
11654
|
+
.number()
|
|
11655
|
+
.int()
|
|
11656
|
+
.min(0)
|
|
11657
|
+
.max(300000)
|
|
11658
|
+
.parse(options.pageDelayMinMs);
|
|
11659
|
+
const pageDelayMaxMs = z.coerce
|
|
11660
|
+
.number()
|
|
11661
|
+
.int()
|
|
11662
|
+
.min(0)
|
|
11663
|
+
.max(300000)
|
|
11664
|
+
.parse(options.pageDelayMaxMs);
|
|
11665
|
+
if (pageDelayMaxMs < pageDelayMinMs) {
|
|
11666
|
+
throw new Error("--page-delay-max-ms must be greater than or equal to --page-delay-min-ms.");
|
|
11667
|
+
}
|
|
11668
|
+
const retry = {
|
|
11669
|
+
maxRetries: z.coerce
|
|
11670
|
+
.number()
|
|
11671
|
+
.int()
|
|
11672
|
+
.min(0)
|
|
11673
|
+
.max(5)
|
|
11674
|
+
.parse(options.maxRetries),
|
|
11675
|
+
retryBaseDelayMs: z.coerce
|
|
11676
|
+
.number()
|
|
11677
|
+
.int()
|
|
11678
|
+
.min(0)
|
|
11679
|
+
.max(300000)
|
|
11680
|
+
.parse(options.retryBaseDelayMs),
|
|
11681
|
+
retryMaxDelayMs: z.coerce
|
|
11682
|
+
.number()
|
|
11683
|
+
.int()
|
|
11684
|
+
.min(0)
|
|
11685
|
+
.max(300000)
|
|
11686
|
+
.parse(options.retryMaxDelayMs),
|
|
11687
|
+
};
|
|
11688
|
+
const curlFile = typeof options.curlFile === "string" && options.curlFile.trim().length > 0
|
|
11689
|
+
? options.curlFile.trim()
|
|
11690
|
+
: null;
|
|
11691
|
+
const effectiveDryRun = Boolean(options.dryRun || shouldBypassAuth());
|
|
11692
|
+
const preview = buildSalesNavigatorCrawlPreview({
|
|
11693
|
+
sourceQueryUrl: queryUrl,
|
|
11694
|
+
dimensions: DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS,
|
|
11695
|
+
maxResultsPerSearch,
|
|
11696
|
+
numberOfProfiles: probeCount,
|
|
11697
|
+
slicePreset: "account-search-split",
|
|
11698
|
+
searchType: "account",
|
|
11699
|
+
});
|
|
11700
|
+
if (effectiveDryRun) {
|
|
11701
|
+
const payload = {
|
|
11702
|
+
status: "ok",
|
|
11703
|
+
dryRun: true,
|
|
11704
|
+
mode: "account-split-static-preview",
|
|
11705
|
+
finalBatchesAvailable: false,
|
|
11706
|
+
finalBatchesRequire: "Run with --plan-only or without --dry-run so the CLI can probe Sales Navigator result counts.",
|
|
11707
|
+
maxResultsPerSearch,
|
|
11708
|
+
maxWindowedFinalResults,
|
|
11709
|
+
windowedFinalSlices: options.windowedFinalSlices !== false,
|
|
11710
|
+
probeCount,
|
|
11711
|
+
requestedAccounts: requestedAccounts ?? "all",
|
|
11712
|
+
pageSize,
|
|
11713
|
+
sourceQueryUrl: queryUrl,
|
|
11714
|
+
rootQueryUrl: preview.root.slicedQueryUrl,
|
|
11715
|
+
rootAppliedFilters: preview.root.appliedFilters,
|
|
11716
|
+
dimensionOrder: preview.dimensions.map((dimension) => ({
|
|
11717
|
+
key: dimension.key,
|
|
11718
|
+
filterType: dimension.filterType,
|
|
11719
|
+
valueCount: dimension.values.length,
|
|
11720
|
+
})),
|
|
11721
|
+
firstSplitQueries: preview.firstSplit.map((attempt) => ({
|
|
11722
|
+
kind: "split-candidate",
|
|
11723
|
+
isFinalBatch: false,
|
|
11724
|
+
countStatus: "not_probed",
|
|
11725
|
+
warning: "This URL is a coarse split candidate, not a final batch. It may exceed maxResultsPerSearch until --plan-only probes counts and recursively splits it.",
|
|
11726
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
11727
|
+
appliedFilters: attempt.appliedFilters,
|
|
11728
|
+
splitTrail: attempt.splitTrail.map((entry) => ({
|
|
11729
|
+
key: entry.key,
|
|
11730
|
+
filterType: entry.filterType,
|
|
11731
|
+
valueText: entry.value.text,
|
|
11732
|
+
})),
|
|
11733
|
+
})),
|
|
11734
|
+
};
|
|
11735
|
+
if (options.out) {
|
|
11736
|
+
await writeJsonFile(options.out, payload);
|
|
11737
|
+
}
|
|
11738
|
+
printOutput(payload);
|
|
11739
|
+
return;
|
|
11740
|
+
}
|
|
11741
|
+
const templateRequest = curlFile
|
|
11742
|
+
? parseSalesNavigatorCurlRequest(await readFile(curlFile, "utf8"))
|
|
11743
|
+
: null;
|
|
11744
|
+
if (templateRequest &&
|
|
11745
|
+
!new URL(templateRequest.url).pathname.includes("/sales-api/salesApiAccountSearch")) {
|
|
11746
|
+
throw new Error("Account split --curl-file must contain a /sales-api/salesApiAccountSearch request.");
|
|
11747
|
+
}
|
|
11748
|
+
const config = templateRequest ? null : await readLinkedInDirectLookupConfig();
|
|
11749
|
+
const buildAccountApiRequest = (slicedQueryUrl) => templateRequest
|
|
11750
|
+
? buildSalesNavigatorAccountApiRequestFromSearchUrlWithTemplate(slicedQueryUrl, templateRequest, pageSize)
|
|
11751
|
+
: buildSalesNavigatorAccountApiRequestFromSearchUrl(slicedQueryUrl, config, pageSize);
|
|
11752
|
+
const probeAccountSlice = async (attempt) => {
|
|
11753
|
+
const parsedRequest = buildAccountApiRequest(attempt.slicedQueryUrl);
|
|
11754
|
+
const probeResult = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
11755
|
+
requestedAccounts: probeCount,
|
|
11756
|
+
pageSize,
|
|
11757
|
+
pageDelayMinMs,
|
|
11758
|
+
pageDelayMaxMs,
|
|
11759
|
+
maxAllowedResults: Number.MAX_SAFE_INTEGER,
|
|
11760
|
+
retry,
|
|
11761
|
+
});
|
|
11762
|
+
return { totalResults: probeResult.totalResults };
|
|
11763
|
+
};
|
|
11764
|
+
if (Boolean(options.planOnly)) {
|
|
11765
|
+
const crawl = await executeSalesNavigatorAdaptiveCrawl({
|
|
11766
|
+
sourceQueryUrl: queryUrl,
|
|
11767
|
+
dimensions: DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS,
|
|
11768
|
+
maxResultsPerSearch,
|
|
11769
|
+
numberOfProfiles: probeCount,
|
|
11770
|
+
slicePreset: "account-search-split",
|
|
11771
|
+
searchType: "account",
|
|
11772
|
+
maxSplitDepth,
|
|
11773
|
+
maxSlices,
|
|
11774
|
+
maxRetries: 0,
|
|
11775
|
+
includeResidualExclusionSlices: true,
|
|
11776
|
+
adaptiveSplitProbing: true,
|
|
11777
|
+
probeSlice: probeAccountSlice,
|
|
11778
|
+
exportSlice: async (attempt) => {
|
|
11779
|
+
const probeResult = await probeAccountSlice(attempt);
|
|
11780
|
+
if (probeResult.totalResults == null ||
|
|
11781
|
+
probeResult.totalResults > attempt.maxResultsPerSearch) {
|
|
11782
|
+
throw new SalesNavigatorSliceTooBroadError(`Account slice has ${probeResult.totalResults ?? "unknown"} results, above ${attempt.maxResultsPerSearch}.`, { totalResults: probeResult.totalResults });
|
|
11783
|
+
}
|
|
11784
|
+
return probeResult;
|
|
11785
|
+
},
|
|
11786
|
+
});
|
|
11787
|
+
const payload = {
|
|
11788
|
+
status: "ok",
|
|
11789
|
+
dryRun: false,
|
|
11790
|
+
planOnly: true,
|
|
11791
|
+
mode: "account-split-plan",
|
|
11792
|
+
maxResultsPerSearch,
|
|
11793
|
+
probeCount,
|
|
11794
|
+
pageSize,
|
|
11795
|
+
sourceQueryUrl: queryUrl,
|
|
11796
|
+
finalSlices: crawl.exported.map(({ attempt, result }) => ({
|
|
11797
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
11798
|
+
totalResults: result.totalResults,
|
|
11799
|
+
splitTrail: attempt.splitTrail.map((entry) => ({
|
|
11800
|
+
key: entry.key,
|
|
11801
|
+
filterType: entry.filterType,
|
|
11802
|
+
valueText: entry.value.text,
|
|
11803
|
+
})),
|
|
11804
|
+
})),
|
|
11805
|
+
splitEvents: crawl.splitEvents.map((event) => ({
|
|
11806
|
+
slicedQueryUrl: event.attempt.slicedQueryUrl,
|
|
11807
|
+
totalResults: event.totalResults,
|
|
11808
|
+
nextDimension: event.nextDimension,
|
|
11809
|
+
childCount: event.childCount,
|
|
11810
|
+
strategy: event.strategy ?? "fixed-order",
|
|
11811
|
+
probeSummary: event.probeSummary ?? [],
|
|
11812
|
+
})),
|
|
11813
|
+
unresolved: crawl.unresolved.map((failure) => ({
|
|
11814
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
11815
|
+
totalResults: failure.totalResults ?? null,
|
|
11816
|
+
splitTrail: failure.attempt.splitTrail.map((entry) => ({
|
|
11817
|
+
key: entry.key,
|
|
11818
|
+
filterType: entry.filterType,
|
|
11819
|
+
valueText: entry.value.text,
|
|
11820
|
+
})),
|
|
11821
|
+
error: failure.error,
|
|
11822
|
+
})),
|
|
11823
|
+
failed: crawl.failed.map((failure) => ({
|
|
11824
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
11825
|
+
totalResults: failure.totalResults ?? null,
|
|
11826
|
+
splitTrail: failure.attempt.splitTrail.map((entry) => ({
|
|
11827
|
+
key: entry.key,
|
|
11828
|
+
filterType: entry.filterType,
|
|
11829
|
+
valueText: entry.value.text,
|
|
11830
|
+
})),
|
|
11831
|
+
error: failure.error,
|
|
11832
|
+
})),
|
|
11833
|
+
attempted: crawl.attempted,
|
|
11834
|
+
truncated: crawl.truncated,
|
|
11835
|
+
remainingQueue: crawl.remainingQueue,
|
|
11836
|
+
};
|
|
11837
|
+
if (options.out) {
|
|
11838
|
+
await writeJsonFile(options.out, payload);
|
|
11839
|
+
}
|
|
11840
|
+
printOutput(payload);
|
|
11841
|
+
return;
|
|
11842
|
+
}
|
|
11843
|
+
const storedSession = await readAuthSession();
|
|
11844
|
+
const orgId = resolveAccountSearchStorageOrgId(storedSession, options.orgId);
|
|
11845
|
+
if (!orgId) {
|
|
11846
|
+
throw new Error("Account search storage requires --org-id, SALESPROMPTER_ORG_ID, or a saved Salesprompter workspace org. Run `salesprompter auth:login` once, or pass the workspace org id explicitly.");
|
|
11847
|
+
}
|
|
11848
|
+
const store = await createSalesNavigatorAccountSearchStore();
|
|
11849
|
+
const runId = await createSalesNavigatorAccountSearchRun({
|
|
11850
|
+
store,
|
|
11851
|
+
orgId,
|
|
11852
|
+
sourceQueryUrl: queryUrl,
|
|
11853
|
+
slicePreset: "account-search-split",
|
|
11854
|
+
maxResultsPerSearch,
|
|
11855
|
+
pageSize,
|
|
11856
|
+
rawPayload: {
|
|
11857
|
+
workflow: "salesnav:accounts:split",
|
|
11858
|
+
sourceQueryUrl: queryUrl,
|
|
11859
|
+
maxResultsPerSearch,
|
|
11860
|
+
maxWindowedFinalResults,
|
|
11861
|
+
windowedFinalSlices: options.windowedFinalSlices !== false,
|
|
11862
|
+
maxSplitDepth,
|
|
11863
|
+
maxSlices,
|
|
11864
|
+
probeCount,
|
|
11865
|
+
requestedAccounts: requestedAccounts ?? "all",
|
|
11866
|
+
pageSize,
|
|
11867
|
+
pageDelayMinMs,
|
|
11868
|
+
pageDelayMaxMs,
|
|
11869
|
+
workflowCooldownSeconds,
|
|
11870
|
+
},
|
|
11871
|
+
});
|
|
11872
|
+
let storedAccountCount = 0;
|
|
11873
|
+
let crawl;
|
|
11874
|
+
try {
|
|
11875
|
+
crawl = await executeSalesNavigatorAdaptiveCrawl({
|
|
11876
|
+
sourceQueryUrl: queryUrl,
|
|
11877
|
+
dimensions: DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS,
|
|
11878
|
+
maxResultsPerSearch,
|
|
11879
|
+
numberOfProfiles: probeCount,
|
|
11880
|
+
slicePreset: "account-search-split",
|
|
11881
|
+
searchType: "account",
|
|
11882
|
+
maxSplitDepth,
|
|
11883
|
+
maxSlices,
|
|
11884
|
+
maxRetries: 0,
|
|
11885
|
+
includeResidualExclusionSlices: true,
|
|
11886
|
+
adaptiveSplitProbing: true,
|
|
11887
|
+
probeSlice: async (attempt) => {
|
|
11888
|
+
const parsedRequest = buildAccountApiRequest(attempt.slicedQueryUrl);
|
|
11889
|
+
const probeResult = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
11890
|
+
requestedAccounts: probeCount,
|
|
11891
|
+
pageSize,
|
|
11892
|
+
pageDelayMinMs,
|
|
11893
|
+
pageDelayMaxMs,
|
|
11894
|
+
maxAllowedResults: Number.MAX_SAFE_INTEGER,
|
|
11895
|
+
retry,
|
|
11896
|
+
});
|
|
11897
|
+
return { totalResults: probeResult.totalResults };
|
|
11898
|
+
},
|
|
11899
|
+
exportSlice: async (attempt) => {
|
|
11900
|
+
const parsedRequest = buildAccountApiRequest(attempt.slicedQueryUrl);
|
|
11901
|
+
try {
|
|
11902
|
+
const fetchResult = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
11903
|
+
requestedAccounts,
|
|
11904
|
+
pageSize,
|
|
11905
|
+
pageDelayMinMs,
|
|
11906
|
+
pageDelayMaxMs,
|
|
11907
|
+
maxAllowedResults: attempt.maxResultsPerSearch,
|
|
11908
|
+
retry,
|
|
11909
|
+
});
|
|
11910
|
+
const stop = fetchResult.pacing.stop;
|
|
11911
|
+
const partialError = describeLocalSalesNavigatorAccountPartialError(fetchResult);
|
|
11912
|
+
const sliceId = await upsertSalesNavigatorAccountSearchSlice({
|
|
11913
|
+
store,
|
|
11914
|
+
runId,
|
|
11915
|
+
orgId,
|
|
11916
|
+
attempt,
|
|
11917
|
+
status: partialError ? "failed" : "finished",
|
|
11918
|
+
totalResults: fetchResult.totalResults,
|
|
11919
|
+
fetchedPages: fetchResult.fetchedPages,
|
|
11920
|
+
storedAccounts: fetchResult.accounts.length,
|
|
11921
|
+
retryCount: fetchResult.pacing.retryCount,
|
|
11922
|
+
retryDelayMs: fetchResult.pacing.totalDelayMs,
|
|
11923
|
+
lastError: partialError,
|
|
11924
|
+
rawPayload: {
|
|
11925
|
+
sourceQueryUrl: attempt.sourceQueryUrl,
|
|
11926
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
11927
|
+
pacing: fetchResult.pacing,
|
|
11928
|
+
stop,
|
|
11929
|
+
},
|
|
11930
|
+
});
|
|
11931
|
+
const storedAccounts = await upsertSalesNavigatorAccounts({
|
|
11932
|
+
store,
|
|
11933
|
+
orgId,
|
|
11934
|
+
runId,
|
|
11935
|
+
sliceId,
|
|
11936
|
+
accounts: fetchResult.accounts,
|
|
11937
|
+
});
|
|
11938
|
+
storedAccountCount += storedAccounts;
|
|
11939
|
+
if (fetchResult.pacing.rateLimit) {
|
|
11940
|
+
throw new SalesNavigatorCrawlStopError(fetchResult.pacing.rateLimit.message, {
|
|
11941
|
+
totalResults: fetchResult.totalResults,
|
|
11942
|
+
details: {
|
|
11943
|
+
stop,
|
|
11944
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
11945
|
+
},
|
|
11946
|
+
});
|
|
11947
|
+
}
|
|
11948
|
+
return {
|
|
11949
|
+
estimatedTotalResults: fetchResult.totalResults,
|
|
11950
|
+
fetchedPages: fetchResult.fetchedPages,
|
|
11951
|
+
storedAccounts,
|
|
11952
|
+
retryCount: fetchResult.pacing.retryCount,
|
|
11953
|
+
retryDelayMs: fetchResult.pacing.totalDelayMs,
|
|
11954
|
+
stop,
|
|
11955
|
+
truncated: Boolean(partialError),
|
|
11956
|
+
error: partialError,
|
|
11957
|
+
};
|
|
11958
|
+
}
|
|
11959
|
+
catch (error) {
|
|
11960
|
+
if (error instanceof SalesNavigatorSliceTooBroadError) {
|
|
11961
|
+
throw error;
|
|
11962
|
+
}
|
|
11963
|
+
if (error instanceof SalesNavigatorCrawlStopError) {
|
|
11964
|
+
throw error;
|
|
11965
|
+
}
|
|
11966
|
+
await upsertSalesNavigatorAccountSearchSlice({
|
|
11967
|
+
store,
|
|
11968
|
+
runId,
|
|
11969
|
+
orgId,
|
|
11970
|
+
attempt,
|
|
11971
|
+
status: "failed",
|
|
11972
|
+
totalResults: null,
|
|
11973
|
+
fetchedPages: 0,
|
|
11974
|
+
storedAccounts: 0,
|
|
11975
|
+
retryCount: 0,
|
|
11976
|
+
retryDelayMs: 0,
|
|
11977
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
11978
|
+
rawPayload: {
|
|
11979
|
+
sourceQueryUrl: attempt.sourceQueryUrl,
|
|
11980
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
11981
|
+
},
|
|
11982
|
+
});
|
|
11983
|
+
throw error;
|
|
11984
|
+
}
|
|
11985
|
+
},
|
|
11986
|
+
});
|
|
11987
|
+
}
|
|
11988
|
+
catch (error) {
|
|
11989
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
11990
|
+
await updateSalesNavigatorAccountSearchRun({
|
|
11991
|
+
store,
|
|
11992
|
+
runId,
|
|
11993
|
+
status: "failed",
|
|
11994
|
+
totalSlices: 0,
|
|
11995
|
+
storedAccounts: storedAccountCount,
|
|
11996
|
+
attemptedSlices: 0,
|
|
11997
|
+
splitEvents: 0,
|
|
11998
|
+
failedSlices: 1,
|
|
11999
|
+
unresolvedSlices: 0,
|
|
12000
|
+
truncated: false,
|
|
12001
|
+
lastError: errorMessage,
|
|
12002
|
+
});
|
|
12003
|
+
await closeSalesNavigatorAccountSearchStore(store);
|
|
12004
|
+
if (Boolean(options.startWorkflow) &&
|
|
12005
|
+
/\b(429|rate[-\s]?limit|too many requests)\b/i.test(errorMessage)) {
|
|
12006
|
+
const workflow = await tryStartSalesNavigatorAccountWorkflow({
|
|
12007
|
+
runId,
|
|
12008
|
+
cooldownSeconds: workflowCooldownSeconds,
|
|
12009
|
+
});
|
|
12010
|
+
printOutput({
|
|
12011
|
+
status: "rate_limited",
|
|
12012
|
+
mode: "account-split",
|
|
12013
|
+
runId,
|
|
12014
|
+
error: errorMessage,
|
|
12015
|
+
workflow: {
|
|
12016
|
+
...workflow,
|
|
12017
|
+
nextAction: "cooldown",
|
|
12018
|
+
cooldownSeconds: workflowCooldownSeconds,
|
|
12019
|
+
repairCommand: buildSalesNavigatorAccountRepairCommand({
|
|
12020
|
+
runId,
|
|
12021
|
+
curlFile,
|
|
12022
|
+
}),
|
|
12023
|
+
},
|
|
12024
|
+
});
|
|
12025
|
+
}
|
|
12026
|
+
throw error;
|
|
12027
|
+
}
|
|
12028
|
+
const windowedFinalSlices = [];
|
|
12029
|
+
const additionalFailed = [];
|
|
12030
|
+
const remainingUnresolved = [];
|
|
12031
|
+
for (const failure of crawl.unresolved) {
|
|
12032
|
+
const canFetchWindowedFinalSlice = options.windowedFinalSlices !== false &&
|
|
12033
|
+
failure.totalResults != null &&
|
|
12034
|
+
failure.totalResults <= maxWindowedFinalResults;
|
|
12035
|
+
if (canFetchWindowedFinalSlice) {
|
|
12036
|
+
try {
|
|
12037
|
+
const parsedRequest = buildAccountApiRequest(failure.attempt.slicedQueryUrl);
|
|
12038
|
+
const fetchResult = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
12039
|
+
requestedAccounts,
|
|
12040
|
+
pageSize,
|
|
12041
|
+
pageDelayMinMs,
|
|
12042
|
+
pageDelayMaxMs,
|
|
12043
|
+
maxAllowedResults: maxWindowedFinalResults,
|
|
12044
|
+
retry,
|
|
12045
|
+
allowPartialPageErrors: true,
|
|
12046
|
+
});
|
|
12047
|
+
const windowedError = describeLocalSalesNavigatorAccountPartialError(fetchResult);
|
|
12048
|
+
const sliceId = await upsertSalesNavigatorAccountSearchSlice({
|
|
12049
|
+
store,
|
|
12050
|
+
runId,
|
|
12051
|
+
orgId,
|
|
12052
|
+
attempt: failure.attempt,
|
|
12053
|
+
status: windowedError ? "failed" : "finished",
|
|
12054
|
+
totalResults: fetchResult.totalResults,
|
|
12055
|
+
fetchedPages: fetchResult.fetchedPages,
|
|
12056
|
+
storedAccounts: fetchResult.accounts.length,
|
|
12057
|
+
retryCount: fetchResult.pacing.retryCount,
|
|
12058
|
+
retryDelayMs: fetchResult.pacing.totalDelayMs,
|
|
12059
|
+
lastError: windowedError,
|
|
12060
|
+
rawPayload: {
|
|
12061
|
+
sourceQueryUrl: failure.attempt.sourceQueryUrl,
|
|
12062
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12063
|
+
oversizedFinalSlice: true,
|
|
12064
|
+
originalError: failure.error,
|
|
12065
|
+
pacing: fetchResult.pacing,
|
|
12066
|
+
stop: fetchResult.pacing.stop,
|
|
12067
|
+
},
|
|
12068
|
+
});
|
|
12069
|
+
const storedAccounts = await upsertSalesNavigatorAccounts({
|
|
12070
|
+
store,
|
|
12071
|
+
orgId,
|
|
12072
|
+
runId,
|
|
12073
|
+
sliceId,
|
|
12074
|
+
accounts: fetchResult.accounts,
|
|
12075
|
+
});
|
|
12076
|
+
storedAccountCount += storedAccounts;
|
|
12077
|
+
windowedFinalSlices.push({
|
|
12078
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12079
|
+
appliedFilters: failure.attempt.appliedFilters,
|
|
12080
|
+
splitTrail: failure.attempt.splitTrail.map((entry) => ({
|
|
12081
|
+
key: entry.key,
|
|
12082
|
+
filterType: entry.filterType,
|
|
12083
|
+
valueText: entry.value.text,
|
|
12084
|
+
})),
|
|
12085
|
+
estimatedTotalResults: fetchResult.totalResults,
|
|
12086
|
+
fetchedPages: fetchResult.fetchedPages,
|
|
12087
|
+
storedAccounts,
|
|
12088
|
+
retryCount: fetchResult.pacing.retryCount,
|
|
12089
|
+
retryDelayMs: fetchResult.pacing.totalDelayMs,
|
|
12090
|
+
windowedFinal: true,
|
|
12091
|
+
truncated: Boolean(windowedError),
|
|
12092
|
+
error: windowedError,
|
|
12093
|
+
stop: fetchResult.pacing.stop,
|
|
12094
|
+
});
|
|
12095
|
+
continue;
|
|
12096
|
+
}
|
|
12097
|
+
catch (error) {
|
|
12098
|
+
additionalFailed.push({
|
|
12099
|
+
attempt: failure.attempt,
|
|
12100
|
+
totalResults: failure.totalResults ?? null,
|
|
12101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
12102
|
+
});
|
|
12103
|
+
await upsertSalesNavigatorAccountSearchSlice({
|
|
12104
|
+
store,
|
|
12105
|
+
runId,
|
|
12106
|
+
orgId,
|
|
12107
|
+
attempt: failure.attempt,
|
|
12108
|
+
status: "failed",
|
|
12109
|
+
totalResults: failure.totalResults ?? null,
|
|
12110
|
+
fetchedPages: 0,
|
|
12111
|
+
storedAccounts: 0,
|
|
12112
|
+
retryCount: 0,
|
|
12113
|
+
retryDelayMs: 0,
|
|
12114
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
12115
|
+
rawPayload: {
|
|
12116
|
+
sourceQueryUrl: failure.attempt.sourceQueryUrl,
|
|
12117
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12118
|
+
oversizedFinalSlice: true,
|
|
12119
|
+
originalError: failure.error,
|
|
12120
|
+
},
|
|
12121
|
+
});
|
|
12122
|
+
continue;
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12125
|
+
remainingUnresolved.push(failure);
|
|
12126
|
+
}
|
|
12127
|
+
for (const failure of remainingUnresolved) {
|
|
12128
|
+
await upsertSalesNavigatorAccountSearchSlice({
|
|
12129
|
+
store,
|
|
12130
|
+
runId,
|
|
12131
|
+
orgId,
|
|
12132
|
+
attempt: failure.attempt,
|
|
12133
|
+
status: "unresolved",
|
|
12134
|
+
totalResults: failure.totalResults ?? null,
|
|
12135
|
+
fetchedPages: 0,
|
|
12136
|
+
storedAccounts: 0,
|
|
12137
|
+
retryCount: 0,
|
|
12138
|
+
retryDelayMs: 0,
|
|
12139
|
+
lastError: failure.error,
|
|
12140
|
+
rawPayload: {
|
|
12141
|
+
sourceQueryUrl: failure.attempt.sourceQueryUrl,
|
|
12142
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12143
|
+
},
|
|
12144
|
+
});
|
|
12145
|
+
}
|
|
12146
|
+
for (const event of crawl.splitEvents) {
|
|
12147
|
+
await upsertSalesNavigatorAccountSearchSlice({
|
|
12148
|
+
store,
|
|
12149
|
+
runId,
|
|
12150
|
+
orgId,
|
|
12151
|
+
attempt: event.attempt,
|
|
12152
|
+
status: "split",
|
|
12153
|
+
totalResults: event.totalResults,
|
|
12154
|
+
fetchedPages: 1,
|
|
12155
|
+
storedAccounts: 0,
|
|
12156
|
+
retryCount: 0,
|
|
12157
|
+
retryDelayMs: 0,
|
|
12158
|
+
rawPayload: {
|
|
12159
|
+
sourceQueryUrl: event.attempt.sourceQueryUrl,
|
|
12160
|
+
slicedQueryUrl: event.attempt.slicedQueryUrl,
|
|
12161
|
+
nextDimension: event.nextDimension,
|
|
12162
|
+
childCount: event.childCount,
|
|
12163
|
+
},
|
|
12164
|
+
});
|
|
12165
|
+
}
|
|
12166
|
+
const finalSlices = crawl.exported.map(({ attempt, result }) => ({
|
|
12167
|
+
slicedQueryUrl: attempt.slicedQueryUrl,
|
|
12168
|
+
appliedFilters: attempt.appliedFilters,
|
|
12169
|
+
splitTrail: attempt.splitTrail.map((entry) => ({
|
|
12170
|
+
key: entry.key,
|
|
12171
|
+
filterType: entry.filterType,
|
|
12172
|
+
valueText: entry.value.text,
|
|
12173
|
+
})),
|
|
12174
|
+
estimatedTotalResults: result.estimatedTotalResults,
|
|
12175
|
+
fetchedPages: result.fetchedPages,
|
|
12176
|
+
storedAccounts: result.storedAccounts,
|
|
12177
|
+
retryCount: result.retryCount,
|
|
12178
|
+
retryDelayMs: result.retryDelayMs,
|
|
12179
|
+
stop: result.stop,
|
|
12180
|
+
truncated: result.truncated,
|
|
12181
|
+
error: result.error,
|
|
12182
|
+
})).concat(windowedFinalSlices);
|
|
12183
|
+
const failed = crawl.failed.concat(additionalFailed);
|
|
12184
|
+
const rootReportedTotal = crawl.splitEvents.find((event) => event.attempt.depth === 0)?.totalResults ?? null;
|
|
12185
|
+
const finalSliceReportedTotalSum = finalSlices.reduce((sum, slice) => sum + (slice.estimatedTotalResults ?? 0), 0);
|
|
12186
|
+
const missingReportedFromFinalSlices = rootReportedTotal == null
|
|
12187
|
+
? null
|
|
12188
|
+
: Math.max(0, rootReportedTotal - finalSliceReportedTotalSum);
|
|
12189
|
+
const missingStoredFromRoot = rootReportedTotal == null
|
|
12190
|
+
? null
|
|
12191
|
+
: Math.max(0, rootReportedTotal - storedAccountCount);
|
|
12192
|
+
const truncated = crawl.truncated ||
|
|
12193
|
+
finalSlices.some((slice) => slice.truncated) ||
|
|
12194
|
+
(missingReportedFromFinalSlices != null && missingReportedFromFinalSlices > 0) ||
|
|
12195
|
+
(missingStoredFromRoot != null && missingStoredFromRoot > 0);
|
|
12196
|
+
const coverageError = missingReportedFromFinalSlices != null && missingReportedFromFinalSlices > 0
|
|
12197
|
+
? `Final account slices explain ${finalSliceReportedTotalSum} of ${rootReportedTotal} root reported results; missing reported total ${missingReportedFromFinalSlices}.`
|
|
12198
|
+
: missingStoredFromRoot != null && missingStoredFromRoot > 0
|
|
12199
|
+
? `Stored ${storedAccountCount} of ${rootReportedTotal} root reported accounts; missing stored total ${missingStoredFromRoot}.`
|
|
12200
|
+
: null;
|
|
12201
|
+
const payload = {
|
|
12202
|
+
status: "ok",
|
|
12203
|
+
dryRun: false,
|
|
12204
|
+
mode: "account-split",
|
|
12205
|
+
maxResultsPerSearch,
|
|
12206
|
+
probeCount,
|
|
12207
|
+
requestedAccounts: requestedAccounts ?? "all",
|
|
12208
|
+
pageSize,
|
|
12209
|
+
sourceQueryUrl: queryUrl,
|
|
12210
|
+
runId,
|
|
12211
|
+
finalSlices,
|
|
12212
|
+
splitEvents: crawl.splitEvents.map((event) => ({
|
|
12213
|
+
slicedQueryUrl: event.attempt.slicedQueryUrl,
|
|
12214
|
+
totalResults: event.totalResults,
|
|
12215
|
+
nextDimension: event.nextDimension,
|
|
12216
|
+
childCount: event.childCount,
|
|
12217
|
+
strategy: event.strategy ?? "fixed-order",
|
|
12218
|
+
probeSummary: event.probeSummary ?? [],
|
|
12219
|
+
})),
|
|
12220
|
+
unresolved: remainingUnresolved.map((failure) => ({
|
|
12221
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12222
|
+
splitTrail: failure.attempt.splitTrail.map((entry) => ({
|
|
12223
|
+
key: entry.key,
|
|
12224
|
+
filterType: entry.filterType,
|
|
12225
|
+
valueText: entry.value.text,
|
|
12226
|
+
})),
|
|
12227
|
+
totalResults: failure.totalResults ?? null,
|
|
12228
|
+
error: failure.error,
|
|
12229
|
+
})),
|
|
12230
|
+
failed: failed.map((failure) => ({
|
|
12231
|
+
slicedQueryUrl: failure.attempt.slicedQueryUrl,
|
|
12232
|
+
splitTrail: failure.attempt.splitTrail.map((entry) => ({
|
|
12233
|
+
key: entry.key,
|
|
12234
|
+
filterType: entry.filterType,
|
|
12235
|
+
valueText: entry.value.text,
|
|
12236
|
+
})),
|
|
12237
|
+
totalResults: failure.totalResults ?? null,
|
|
12238
|
+
error: failure.error,
|
|
12239
|
+
})),
|
|
12240
|
+
attempted: crawl.attempted,
|
|
12241
|
+
truncated,
|
|
12242
|
+
remainingQueue: crawl.remainingQueue,
|
|
12243
|
+
storedAccounts: storedAccountCount,
|
|
12244
|
+
reportedCoverage: {
|
|
12245
|
+
rootReportedTotal,
|
|
12246
|
+
finalSliceReportedTotalSum,
|
|
12247
|
+
missingReportedFromFinalSlices,
|
|
12248
|
+
storedAccounts: storedAccountCount,
|
|
12249
|
+
missingStoredFromRoot,
|
|
12250
|
+
},
|
|
12251
|
+
};
|
|
12252
|
+
await updateSalesNavigatorAccountSearchRun({
|
|
12253
|
+
store,
|
|
12254
|
+
runId,
|
|
12255
|
+
status: payload.unresolved.length > 0 || payload.failed.length > 0 || payload.truncated
|
|
12256
|
+
? "failed"
|
|
12257
|
+
: "finished",
|
|
12258
|
+
totalSlices: payload.finalSlices.length,
|
|
12259
|
+
storedAccounts: storedAccountCount,
|
|
12260
|
+
attemptedSlices: crawl.attempted,
|
|
12261
|
+
splitEvents: crawl.splitEvents.length,
|
|
12262
|
+
failedSlices: payload.failed.length +
|
|
12263
|
+
finalSlices.filter((slice) => slice.error).length,
|
|
12264
|
+
unresolvedSlices: payload.unresolved.length,
|
|
12265
|
+
truncated: payload.truncated,
|
|
12266
|
+
lastError: payload.unresolved[0]?.error ??
|
|
12267
|
+
payload.failed[0]?.error ??
|
|
12268
|
+
windowedFinalSlices.find((slice) => slice.error)?.error ??
|
|
12269
|
+
finalSlices.find((slice) => slice.error)?.error ??
|
|
12270
|
+
coverageError ??
|
|
12271
|
+
(payload.truncated ? "Account search split truncated before completion." : null),
|
|
12272
|
+
rawPayload: {
|
|
12273
|
+
reportedCoverage: payload.reportedCoverage,
|
|
12274
|
+
},
|
|
12275
|
+
});
|
|
12276
|
+
const needsWorkflow = payload.unresolved.length > 0 || payload.failed.length > 0 || payload.truncated;
|
|
12277
|
+
const workflow = needsWorkflow
|
|
12278
|
+
? Boolean(options.startWorkflow)
|
|
12279
|
+
? await tryStartSalesNavigatorAccountWorkflow({
|
|
12280
|
+
runId,
|
|
12281
|
+
cooldownSeconds: workflowCooldownSeconds,
|
|
12282
|
+
})
|
|
12283
|
+
: {
|
|
12284
|
+
started: false,
|
|
12285
|
+
workflowRunId: null,
|
|
12286
|
+
error: "Workflow start disabled by --no-start-workflow.",
|
|
12287
|
+
}
|
|
12288
|
+
: {
|
|
12289
|
+
started: false,
|
|
12290
|
+
workflowRunId: null,
|
|
12291
|
+
error: null,
|
|
12292
|
+
};
|
|
12293
|
+
const outputPayload = {
|
|
12294
|
+
...payload,
|
|
12295
|
+
workflow: {
|
|
12296
|
+
...workflow,
|
|
12297
|
+
nextAction: needsWorkflow ? "cooldown" : "complete",
|
|
12298
|
+
cooldownSeconds: needsWorkflow ? workflowCooldownSeconds : null,
|
|
12299
|
+
repairCommand: needsWorkflow
|
|
12300
|
+
? buildSalesNavigatorAccountRepairCommand({
|
|
12301
|
+
runId,
|
|
12302
|
+
curlFile,
|
|
12303
|
+
})
|
|
12304
|
+
: null,
|
|
12305
|
+
},
|
|
12306
|
+
};
|
|
12307
|
+
if (options.out) {
|
|
12308
|
+
await writeJsonFile(options.out, outputPayload);
|
|
12309
|
+
}
|
|
12310
|
+
printOutput(outputPayload);
|
|
12311
|
+
await closeSalesNavigatorAccountSearchStore(store);
|
|
12312
|
+
if (needsWorkflow) {
|
|
12313
|
+
throw new Error(`Account search split did not finish cleanly. finalSlices=${payload.finalSlices.length} unresolved=${payload.unresolved.length} failed=${payload.failed.length} truncated=${payload.truncated}`);
|
|
12314
|
+
}
|
|
12315
|
+
});
|
|
12316
|
+
program
|
|
12317
|
+
.command("salesnav:accounts:local-import")
|
|
12318
|
+
.alias("accounts:local-import")
|
|
12319
|
+
.description("Run a saved Sales Navigator account request locally and store the accounts in Neon.")
|
|
12320
|
+
.option("--curl-file <path>", "Path to a saved account-search curl request copied from the Sales Navigator network panel")
|
|
12321
|
+
.option("--query-url <url>", "Sales Navigator account search URL. Uses the locally captured extension session automatically.")
|
|
12322
|
+
.option("--org-id <id>", "Salesprompter workspace org id for Neon storage. Defaults to SALESPROMPTER_ORG_ID or saved auth session org.")
|
|
12323
|
+
.option("--max-results-per-search <number>", "Maximum expected account results for this local request", "1000")
|
|
12324
|
+
.option("--number-of-accounts <number>", "Total accounts to import, or 'all' for the full reported result set", "all")
|
|
12325
|
+
.option("--page-size <number>", "Accounts requested per local Sales Navigator API page", "25")
|
|
12326
|
+
.option("--start-offset <number>", "Account-search result offset to start from when resuming a local import", "0")
|
|
12327
|
+
.option("--page-delay-min-ms <number>", "Minimum randomized delay between account-search result pages", "20000")
|
|
12328
|
+
.option("--page-delay-max-ms <number>", "Maximum randomized delay between account-search result pages", "35000")
|
|
12329
|
+
.option("--max-retries <number>", "Bounded retries for transient network/5xx failures. Rate limits still stop immediately.", "2")
|
|
12330
|
+
.option("--retry-base-delay-ms <number>", "Base delay for full-jitter retry backoff", "2000")
|
|
12331
|
+
.option("--retry-max-delay-ms <number>", "Maximum delay for full-jitter retry backoff", "30000")
|
|
12332
|
+
.option("--slice-preset <name>", "Slice preset label stored with the import run", "local-account-search")
|
|
12333
|
+
.option("--allow-partial-page-errors", "Persist rows fetched before a later non-rate-limit page error", false)
|
|
12334
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
12335
|
+
.option("--dry-run", "Run and normalize the local account request without writing to Neon", false)
|
|
12336
|
+
.action(async (options) => {
|
|
12337
|
+
const maxResultsPerSearch = z.coerce
|
|
12338
|
+
.number()
|
|
12339
|
+
.int()
|
|
12340
|
+
.min(1)
|
|
12341
|
+
.max(1000)
|
|
12342
|
+
.parse(options.maxResultsPerSearch);
|
|
12343
|
+
const numberOfAccountsOption = String(options.numberOfAccounts ?? "all")
|
|
12344
|
+
.trim()
|
|
12345
|
+
.toLowerCase();
|
|
12346
|
+
const requestedAccounts = numberOfAccountsOption === "all"
|
|
12347
|
+
? null
|
|
12348
|
+
: z.coerce
|
|
12349
|
+
.number()
|
|
12350
|
+
.int()
|
|
12351
|
+
.min(1)
|
|
12352
|
+
.max(100000)
|
|
12353
|
+
.parse(options.numberOfAccounts);
|
|
12354
|
+
const pageSize = z.coerce
|
|
12355
|
+
.number()
|
|
12356
|
+
.int()
|
|
12357
|
+
.min(1)
|
|
12358
|
+
.max(100)
|
|
12359
|
+
.parse(options.pageSize);
|
|
12360
|
+
const startOffset = z.coerce
|
|
12361
|
+
.number()
|
|
12362
|
+
.int()
|
|
12363
|
+
.min(0)
|
|
12364
|
+
.max(100000)
|
|
12365
|
+
.parse(options.startOffset);
|
|
12366
|
+
const pageDelayMinMs = z.coerce
|
|
12367
|
+
.number()
|
|
12368
|
+
.int()
|
|
12369
|
+
.min(0)
|
|
12370
|
+
.max(300000)
|
|
12371
|
+
.parse(options.pageDelayMinMs);
|
|
12372
|
+
const pageDelayMaxMs = z.coerce
|
|
12373
|
+
.number()
|
|
12374
|
+
.int()
|
|
12375
|
+
.min(0)
|
|
12376
|
+
.max(300000)
|
|
12377
|
+
.parse(options.pageDelayMaxMs);
|
|
12378
|
+
if (pageDelayMaxMs < pageDelayMinMs) {
|
|
12379
|
+
throw new Error("--page-delay-max-ms must be greater than or equal to --page-delay-min-ms.");
|
|
12380
|
+
}
|
|
12381
|
+
const retry = {
|
|
12382
|
+
maxRetries: z.coerce
|
|
12383
|
+
.number()
|
|
12384
|
+
.int()
|
|
12385
|
+
.min(0)
|
|
12386
|
+
.max(5)
|
|
12387
|
+
.parse(options.maxRetries),
|
|
12388
|
+
retryBaseDelayMs: z.coerce
|
|
12389
|
+
.number()
|
|
12390
|
+
.int()
|
|
12391
|
+
.min(0)
|
|
12392
|
+
.max(300000)
|
|
12393
|
+
.parse(options.retryBaseDelayMs),
|
|
12394
|
+
retryMaxDelayMs: z.coerce
|
|
12395
|
+
.number()
|
|
12396
|
+
.int()
|
|
12397
|
+
.min(0)
|
|
12398
|
+
.max(300000)
|
|
12399
|
+
.parse(options.retryMaxDelayMs),
|
|
12400
|
+
};
|
|
12401
|
+
const curlFile = typeof options.curlFile === "string" && options.curlFile.trim().length > 0
|
|
12402
|
+
? options.curlFile.trim()
|
|
12403
|
+
: null;
|
|
12404
|
+
const queryUrl = typeof options.queryUrl === "string" && options.queryUrl.trim().length > 0
|
|
12405
|
+
? z.string().url().parse(options.queryUrl.trim())
|
|
12406
|
+
: null;
|
|
12407
|
+
if (Boolean(curlFile) === Boolean(queryUrl)) {
|
|
12408
|
+
throw new Error("Provide exactly one of --curl-file or --query-url.");
|
|
12409
|
+
}
|
|
12410
|
+
const parsedRequest = curlFile
|
|
12411
|
+
? parseSalesNavigatorCurlRequest(await readFile(curlFile, "utf8"))
|
|
12412
|
+
: buildSalesNavigatorAccountApiRequestFromSearchUrl(queryUrl, await readLinkedInDirectLookupConfig(), pageSize);
|
|
12413
|
+
if (!new URL(parsedRequest.url).pathname.includes("/sales-api/salesApiAccountSearch")) {
|
|
12414
|
+
throw new Error("Account local import requires a /sales-api/salesApiAccountSearch request.");
|
|
12415
|
+
}
|
|
12416
|
+
const localFetch = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
12417
|
+
requestedAccounts,
|
|
12418
|
+
startOffset,
|
|
12419
|
+
pageSize,
|
|
12420
|
+
pageDelayMinMs,
|
|
12421
|
+
pageDelayMaxMs,
|
|
12422
|
+
maxAllowedResults: maxResultsPerSearch,
|
|
12423
|
+
retry,
|
|
12424
|
+
allowPartialPageErrors: Boolean(options.allowPartialPageErrors),
|
|
12425
|
+
});
|
|
12426
|
+
const accounts = localFetch.accounts;
|
|
12427
|
+
const totalResults = localFetch.totalResults;
|
|
12428
|
+
const dryRun = Boolean(options.dryRun);
|
|
12429
|
+
const rateLimit = localFetch.pacing.rateLimit;
|
|
12430
|
+
const pageError = localFetch.pacing.pageError;
|
|
12431
|
+
const partialError = describeLocalSalesNavigatorAccountPartialError(localFetch);
|
|
12432
|
+
if (accounts.length === 0) {
|
|
12433
|
+
throw new Error("The local Sales Navigator account response did not contain any importable account rows.");
|
|
12434
|
+
}
|
|
12435
|
+
const sourceQueryUrl = queryUrl ?? localFetch.sourceQueryUrl;
|
|
12436
|
+
const basePayload = {
|
|
12437
|
+
status: "ok",
|
|
12438
|
+
dryRun,
|
|
12439
|
+
mode: "account-local-import",
|
|
12440
|
+
sourceQueryUrl,
|
|
12441
|
+
totalResults,
|
|
12442
|
+
discovered: accounts.length,
|
|
12443
|
+
fetchedPages: localFetch.fetchedPages,
|
|
12444
|
+
requestedAccounts: requestedAccounts ?? "all",
|
|
12445
|
+
maxResultsPerSearch,
|
|
12446
|
+
startOffset,
|
|
12447
|
+
pageSize,
|
|
12448
|
+
pacing: localFetch.pacing,
|
|
12449
|
+
rateLimit: rateLimit ?? null,
|
|
12450
|
+
pageError: pageError ?? null,
|
|
12451
|
+
stop: localFetch.pacing.stop,
|
|
12452
|
+
accounts,
|
|
12453
|
+
};
|
|
12454
|
+
if (dryRun) {
|
|
12455
|
+
if (options.out) {
|
|
12456
|
+
await writeJsonFile(options.out, basePayload);
|
|
12457
|
+
}
|
|
12458
|
+
printOutput(basePayload);
|
|
12459
|
+
return;
|
|
12460
|
+
}
|
|
12461
|
+
const storedSession = await readAuthSession();
|
|
12462
|
+
const orgId = resolveAccountSearchStorageOrgId(storedSession, options.orgId);
|
|
12463
|
+
if (!orgId) {
|
|
12464
|
+
throw new Error("Account local import storage requires --org-id, SALESPROMPTER_ORG_ID, or a saved Salesprompter workspace org. Run `salesprompter auth:login` once, or pass the workspace org id explicitly.");
|
|
12465
|
+
}
|
|
12466
|
+
const store = await createSalesNavigatorAccountSearchStore();
|
|
12467
|
+
const runId = await createSalesNavigatorAccountSearchRun({
|
|
12468
|
+
store,
|
|
12469
|
+
orgId,
|
|
12470
|
+
sourceQueryUrl,
|
|
12471
|
+
slicePreset: options.slicePreset,
|
|
12472
|
+
maxResultsPerSearch,
|
|
12473
|
+
pageSize,
|
|
12474
|
+
rawPayload: {
|
|
12475
|
+
workflow: "salesnav:accounts:local-import",
|
|
12476
|
+
sourceQueryUrl,
|
|
12477
|
+
requestUrl: localFetch.sourceQueryUrl,
|
|
12478
|
+
maxResultsPerSearch,
|
|
12479
|
+
requestedAccounts: requestedAccounts ?? "all",
|
|
12480
|
+
startOffset,
|
|
12481
|
+
pageSize,
|
|
12482
|
+
pageDelayMinMs,
|
|
12483
|
+
pageDelayMaxMs,
|
|
12484
|
+
rateLimit: rateLimit ?? null,
|
|
12485
|
+
pageError: pageError ?? null,
|
|
12486
|
+
stop: localFetch.pacing.stop,
|
|
12487
|
+
},
|
|
12488
|
+
});
|
|
12489
|
+
const attempt = {
|
|
12490
|
+
sourceQueryUrl,
|
|
12491
|
+
slicedQueryUrl: sourceQueryUrl,
|
|
12492
|
+
appliedFilters: [],
|
|
12493
|
+
depth: 0,
|
|
12494
|
+
retryCount: 0,
|
|
12495
|
+
maxResultsPerSearch,
|
|
12496
|
+
numberOfProfiles: requestedAccounts ?? accounts.length,
|
|
12497
|
+
slicePreset: options.slicePreset,
|
|
12498
|
+
splitTrail: [],
|
|
12499
|
+
};
|
|
12500
|
+
try {
|
|
12501
|
+
const sliceId = await upsertSalesNavigatorAccountSearchSlice({
|
|
12502
|
+
store,
|
|
12503
|
+
runId,
|
|
12504
|
+
orgId,
|
|
12505
|
+
attempt,
|
|
12506
|
+
status: partialError ? "failed" : "finished",
|
|
12507
|
+
totalResults,
|
|
12508
|
+
fetchedPages: localFetch.fetchedPages,
|
|
12509
|
+
storedAccounts: accounts.length,
|
|
12510
|
+
retryCount: localFetch.pacing.retryCount,
|
|
12511
|
+
retryDelayMs: localFetch.pacing.totalDelayMs,
|
|
12512
|
+
lastError: partialError,
|
|
12513
|
+
rawPayload: {
|
|
12514
|
+
sourceQueryUrl,
|
|
12515
|
+
requestUrl: localFetch.sourceQueryUrl,
|
|
12516
|
+
startOffset,
|
|
12517
|
+
pacing: localFetch.pacing,
|
|
12518
|
+
stop: localFetch.pacing.stop,
|
|
12519
|
+
},
|
|
12520
|
+
});
|
|
12521
|
+
const storedAccounts = await upsertSalesNavigatorAccounts({
|
|
12522
|
+
store,
|
|
12523
|
+
orgId,
|
|
12524
|
+
runId,
|
|
12525
|
+
sliceId,
|
|
12526
|
+
accounts,
|
|
12527
|
+
});
|
|
12528
|
+
await updateSalesNavigatorAccountSearchRun({
|
|
12529
|
+
store,
|
|
12530
|
+
runId,
|
|
12531
|
+
status: partialError ? "failed" : "finished",
|
|
12532
|
+
totalSlices: 1,
|
|
12533
|
+
storedAccounts,
|
|
12534
|
+
attemptedSlices: 1,
|
|
12535
|
+
splitEvents: 0,
|
|
12536
|
+
failedSlices: partialError ? 1 : 0,
|
|
12537
|
+
unresolvedSlices: 0,
|
|
12538
|
+
truncated: Boolean(partialError),
|
|
12539
|
+
lastError: partialError,
|
|
12540
|
+
});
|
|
12541
|
+
const payload = {
|
|
12542
|
+
...basePayload,
|
|
12543
|
+
runId,
|
|
12544
|
+
sliceId,
|
|
12545
|
+
storedAccounts,
|
|
12546
|
+
};
|
|
12547
|
+
if (options.out) {
|
|
12548
|
+
await writeJsonFile(options.out, payload);
|
|
12549
|
+
}
|
|
12550
|
+
printOutput(payload);
|
|
12551
|
+
}
|
|
12552
|
+
catch (error) {
|
|
12553
|
+
await updateSalesNavigatorAccountSearchRun({
|
|
12554
|
+
store,
|
|
12555
|
+
runId,
|
|
12556
|
+
status: "failed",
|
|
12557
|
+
totalSlices: 0,
|
|
12558
|
+
storedAccounts: 0,
|
|
12559
|
+
attemptedSlices: 1,
|
|
12560
|
+
splitEvents: 0,
|
|
12561
|
+
failedSlices: 1,
|
|
12562
|
+
unresolvedSlices: 0,
|
|
12563
|
+
truncated: false,
|
|
12564
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
12565
|
+
});
|
|
12566
|
+
throw error;
|
|
12567
|
+
}
|
|
12568
|
+
finally {
|
|
12569
|
+
await closeSalesNavigatorAccountSearchStore(store);
|
|
12570
|
+
}
|
|
12571
|
+
});
|
|
12572
|
+
program
|
|
12573
|
+
.command("salesnav:crawl")
|
|
12574
|
+
.alias("search:run")
|
|
12575
|
+
.description("Run a saved people search, split broad result sets when needed, and store the finished output.")
|
|
12576
|
+
.option("--query-url <url>", "Base LinkedIn Sales Navigator people search URL")
|
|
12577
|
+
.option("--job-id <id>", "Resume an existing crawl job by id")
|
|
12578
|
+
.option("--max-results-per-search <number>", "Maximum results allowed for a sliced search", "2500")
|
|
12579
|
+
.option("--number-of-profiles <number>", "Profiles to export per sliced query", "2500")
|
|
12580
|
+
.option("--slice-preset <name>", "Slice preset label stored with the export runs", "human-resources-crawl")
|
|
12581
|
+
.option("--client-id <number>", "Client id used to generate and store the legacy Neon lead list projection")
|
|
12582
|
+
.option("--max-split-depth <number>", "Maximum number of adaptive split dimensions to use", "6")
|
|
12583
|
+
.option("--max-slices <number>", "Safety cap for total claimed slices in this invocation", "1000")
|
|
12584
|
+
.option("--max-retries <number>", "Retries for non-splitting export failures", "3")
|
|
12585
|
+
.option("--probe-profiles <number>", "Profiles to scrape while probing whether a slice is still too broad", "100")
|
|
12586
|
+
.option("--agent-busy-wait-seconds <number>", "Seconds to wait before retrying when the export agent is already busy", "30")
|
|
12587
|
+
.option("--agent-busy-max-waits <number>", "How many busy-agent waits to tolerate before failing the slice", "20")
|
|
12588
|
+
.option("--idle-poll-seconds <number>", "Seconds to wait before polling durable crawl status when remote slices are still running", "10")
|
|
12589
|
+
.option("--idle-max-polls <number>", "How many no-claim status polls to tolerate before the crawl is considered stalled", "180")
|
|
12590
|
+
.option("--parallel-exports <number>", "How many Sales Navigator slices to export concurrently in this invocation", "3")
|
|
12591
|
+
.option("--allow-partial-success", "Exit 0 even when the durable crawl finishes with failures or remains non-terminal", false)
|
|
12592
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
12593
|
+
.option("--log-path <path>", "Optional JSONL log path with timestamps, trace id, and Sales Navigator slice metadata")
|
|
12594
|
+
.option("--dry-run", "Preview the adaptive crawl plan without exporting anything", false)
|
|
12595
|
+
.action(async (options) => {
|
|
12596
|
+
const queryUrl = z.string().url().optional().parse(options.queryUrl);
|
|
12597
|
+
const jobId = z.string().uuid().optional().parse(options.jobId);
|
|
12598
|
+
const maxResultsPerSearch = z.coerce.number().int().min(1).max(2500).parse(options.maxResultsPerSearch);
|
|
12599
|
+
const numberOfProfiles = z.coerce.number().int().min(1).max(2500).parse(options.numberOfProfiles);
|
|
12600
|
+
const clientId = parseOptionalSalesNavigatorClientId(options.clientId);
|
|
12601
|
+
const maxSplitDepth = z.coerce.number().int().min(1).max(6).parse(options.maxSplitDepth);
|
|
12602
|
+
const maxSlices = z.coerce.number().int().min(1).max(10000).parse(options.maxSlices);
|
|
12603
|
+
const maxRetries = z.coerce.number().int().min(0).max(5).parse(options.maxRetries);
|
|
12604
|
+
const probeProfiles = z.coerce.number().int().min(1).max(2500).parse(options.probeProfiles);
|
|
12605
|
+
const agentBusyWaitSeconds = z.coerce.number().int().min(1).max(300).parse(options.agentBusyWaitSeconds);
|
|
12606
|
+
const agentBusyMaxWaits = z.coerce.number().int().min(0).max(120).parse(options.agentBusyMaxWaits);
|
|
12607
|
+
const idlePollSeconds = z.coerce.number().int().min(0).max(300).parse(options.idlePollSeconds);
|
|
12608
|
+
const idleMaxPolls = z.coerce.number().int().min(0).max(10000).parse(options.idleMaxPolls);
|
|
12609
|
+
const parallelExports = z.coerce.number().int().min(1).max(10).parse(options.parallelExports);
|
|
12610
|
+
const phantomLaneLimit = resolveSalesNavigatorPhantomLaneLimit(process.env);
|
|
12611
|
+
const effectiveDryRun = Boolean(options.dryRun || shouldBypassAuth());
|
|
12612
|
+
const logger = await createWorkflowLogger({
|
|
12613
|
+
logPath: options.logPath ?? buildSalesNavigatorCrawlLogPath(jobId ?? queryUrl ?? "salesnav-crawl")
|
|
12614
|
+
});
|
|
12615
|
+
await logger.log("salesnav.crawl.command.started", {
|
|
12616
|
+
queryUrl: queryUrl ?? null,
|
|
12617
|
+
jobId: jobId ?? null,
|
|
12618
|
+
maxResultsPerSearch,
|
|
12619
|
+
numberOfProfiles,
|
|
12620
|
+
clientId,
|
|
12621
|
+
slicePreset: options.slicePreset,
|
|
12622
|
+
maxSplitDepth,
|
|
12623
|
+
maxSlices,
|
|
12624
|
+
maxRetries,
|
|
12625
|
+
probeProfiles,
|
|
12626
|
+
agentBusyWaitSeconds,
|
|
12627
|
+
agentBusyMaxWaits,
|
|
12628
|
+
idlePollSeconds,
|
|
10388
12629
|
idleMaxPolls,
|
|
10389
12630
|
parallelExports,
|
|
10390
12631
|
phantomLaneLimit,
|
|
@@ -10620,22 +12861,362 @@ program
|
|
|
10620
12861
|
recentEvents
|
|
10621
12862
|
});
|
|
10622
12863
|
});
|
|
12864
|
+
program
|
|
12865
|
+
.command("salesnav:accounts:repair")
|
|
12866
|
+
.alias("accounts:repair")
|
|
12867
|
+
.description("Locally resume failed account-search slices from their stored offsets.")
|
|
12868
|
+
.requiredOption("--run-id <id>", "Sales Navigator account-search run id")
|
|
12869
|
+
.requiredOption("--curl-file <path>", "Path to a saved account-search curl request used as the local request template")
|
|
12870
|
+
.option("--max-slices <number>", "Maximum failed slices to repair in this invocation", "100")
|
|
12871
|
+
.option("--number-of-accounts <number>", "Maximum accounts to fetch per repair slice, or 'all' for each slice remainder", "all")
|
|
12872
|
+
.option("--max-repair-results <number>", "Skip zero-progress failed slices above this reported result count; those still need splitting", "1000")
|
|
12873
|
+
.option("--page-size <number>", "Accounts requested per Sales Navigator account-search API page", "25")
|
|
12874
|
+
.option("--page-delay-min-ms <number>", "Minimum randomized delay between account-search result pages", "20000")
|
|
12875
|
+
.option("--page-delay-max-ms <number>", "Maximum randomized delay between account-search result pages", "35000")
|
|
12876
|
+
.option("--max-retries <number>", "Bounded retries for transient network/5xx failures. Rate limits still stop immediately.", "1")
|
|
12877
|
+
.option("--retry-base-delay-ms <number>", "Base delay for full-jitter retry backoff", "2000")
|
|
12878
|
+
.option("--retry-max-delay-ms <number>", "Maximum delay for full-jitter retry backoff", "30000")
|
|
12879
|
+
.option("--out <path>", "Optional local JSON output path")
|
|
12880
|
+
.action(async (options) => {
|
|
12881
|
+
const runId = z.string().uuid().parse(options.runId);
|
|
12882
|
+
const maxSlices = z.coerce
|
|
12883
|
+
.number()
|
|
12884
|
+
.int()
|
|
12885
|
+
.min(1)
|
|
12886
|
+
.max(1000)
|
|
12887
|
+
.parse(options.maxSlices);
|
|
12888
|
+
const numberOfAccountsOption = String(options.numberOfAccounts ?? "all")
|
|
12889
|
+
.trim()
|
|
12890
|
+
.toLowerCase();
|
|
12891
|
+
const requestedAccountsOption = numberOfAccountsOption === "all"
|
|
12892
|
+
? null
|
|
12893
|
+
: z.coerce
|
|
12894
|
+
.number()
|
|
12895
|
+
.int()
|
|
12896
|
+
.min(1)
|
|
12897
|
+
.max(100000)
|
|
12898
|
+
.parse(options.numberOfAccounts);
|
|
12899
|
+
const pageSize = z.coerce
|
|
12900
|
+
.number()
|
|
12901
|
+
.int()
|
|
12902
|
+
.min(1)
|
|
12903
|
+
.max(100)
|
|
12904
|
+
.parse(options.pageSize);
|
|
12905
|
+
const maxRepairResults = z.coerce
|
|
12906
|
+
.number()
|
|
12907
|
+
.int()
|
|
12908
|
+
.min(1)
|
|
12909
|
+
.max(100000)
|
|
12910
|
+
.parse(options.maxRepairResults);
|
|
12911
|
+
const pageDelayMinMs = z.coerce
|
|
12912
|
+
.number()
|
|
12913
|
+
.int()
|
|
12914
|
+
.min(0)
|
|
12915
|
+
.max(300000)
|
|
12916
|
+
.parse(options.pageDelayMinMs);
|
|
12917
|
+
const pageDelayMaxMs = z.coerce
|
|
12918
|
+
.number()
|
|
12919
|
+
.int()
|
|
12920
|
+
.min(0)
|
|
12921
|
+
.max(300000)
|
|
12922
|
+
.parse(options.pageDelayMaxMs);
|
|
12923
|
+
if (pageDelayMaxMs < pageDelayMinMs) {
|
|
12924
|
+
throw new Error("--page-delay-max-ms must be greater than or equal to --page-delay-min-ms.");
|
|
12925
|
+
}
|
|
12926
|
+
const retry = {
|
|
12927
|
+
maxRetries: z.coerce
|
|
12928
|
+
.number()
|
|
12929
|
+
.int()
|
|
12930
|
+
.min(0)
|
|
12931
|
+
.max(5)
|
|
12932
|
+
.parse(options.maxRetries),
|
|
12933
|
+
retryBaseDelayMs: z.coerce
|
|
12934
|
+
.number()
|
|
12935
|
+
.int()
|
|
12936
|
+
.min(0)
|
|
12937
|
+
.max(300000)
|
|
12938
|
+
.parse(options.retryBaseDelayMs),
|
|
12939
|
+
retryMaxDelayMs: z.coerce
|
|
12940
|
+
.number()
|
|
12941
|
+
.int()
|
|
12942
|
+
.min(0)
|
|
12943
|
+
.max(300000)
|
|
12944
|
+
.parse(options.retryMaxDelayMs),
|
|
12945
|
+
};
|
|
12946
|
+
const templateRequest = parseSalesNavigatorCurlRequest(await readFile(options.curlFile, "utf8"));
|
|
12947
|
+
if (!new URL(templateRequest.url).pathname.includes("/sales-api/salesApiAccountSearch")) {
|
|
12948
|
+
throw new Error("Account repair --curl-file must contain a /sales-api/salesApiAccountSearch request.");
|
|
12949
|
+
}
|
|
12950
|
+
const store = await createSalesNavigatorAccountSearchStore();
|
|
12951
|
+
const repaired = [];
|
|
12952
|
+
const skipped = [];
|
|
12953
|
+
let stoppedByRateLimit = null;
|
|
12954
|
+
try {
|
|
12955
|
+
const slices = await listSalesNavigatorAccountSearchRepairSlices({
|
|
12956
|
+
store,
|
|
12957
|
+
runId,
|
|
12958
|
+
limit: maxSlices,
|
|
12959
|
+
});
|
|
12960
|
+
for (const slice of slices) {
|
|
12961
|
+
const startOffset = resolveAccountRepairStartOffset(slice);
|
|
12962
|
+
const remainingFromReported = slice.totalResults == null
|
|
12963
|
+
? null
|
|
12964
|
+
: Math.max(0, slice.totalResults - startOffset);
|
|
12965
|
+
if (remainingFromReported === 0) {
|
|
12966
|
+
continue;
|
|
12967
|
+
}
|
|
12968
|
+
if (startOffset === 0 &&
|
|
12969
|
+
slice.storedAccounts === 0 &&
|
|
12970
|
+
slice.totalResults != null &&
|
|
12971
|
+
slice.totalResults > maxRepairResults) {
|
|
12972
|
+
skipped.push({
|
|
12973
|
+
sliceId: slice.id,
|
|
12974
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
12975
|
+
totalResults: slice.totalResults,
|
|
12976
|
+
storedAccounts: slice.storedAccounts,
|
|
12977
|
+
reason: `reported total ${slice.totalResults} is above max repair results ${maxRepairResults}`,
|
|
12978
|
+
});
|
|
12979
|
+
continue;
|
|
12980
|
+
}
|
|
12981
|
+
if (startOffset === 0 &&
|
|
12982
|
+
slice.storedAccounts === 0 &&
|
|
12983
|
+
!asJsonObject(slice.rawPayload).stop &&
|
|
12984
|
+
/\b(429|rate[-\s]?limit|too many requests)\b/i.test(slice.lastError ?? "")) {
|
|
12985
|
+
skipped.push({
|
|
12986
|
+
sliceId: slice.id,
|
|
12987
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
12988
|
+
totalResults: slice.totalResults,
|
|
12989
|
+
storedAccounts: slice.storedAccounts,
|
|
12990
|
+
reason: "zero-progress slice has no resume offset and the previous failure was rate limited",
|
|
12991
|
+
});
|
|
12992
|
+
continue;
|
|
12993
|
+
}
|
|
12994
|
+
const requestedAccounts = requestedAccountsOption == null
|
|
12995
|
+
? remainingFromReported
|
|
12996
|
+
: remainingFromReported == null
|
|
12997
|
+
? requestedAccountsOption
|
|
12998
|
+
: Math.min(requestedAccountsOption, remainingFromReported);
|
|
12999
|
+
const parsedRequest = buildSalesNavigatorAccountApiRequestFromSearchUrlWithTemplate(slice.slicedQueryUrl, templateRequest, pageSize);
|
|
13000
|
+
const fetchResult = await fetchAllLocalSalesNavigatorAccounts(parsedRequest, {
|
|
13001
|
+
requestedAccounts,
|
|
13002
|
+
startOffset,
|
|
13003
|
+
pageSize,
|
|
13004
|
+
pageDelayMinMs,
|
|
13005
|
+
pageDelayMaxMs,
|
|
13006
|
+
maxAllowedResults: 100000,
|
|
13007
|
+
retry,
|
|
13008
|
+
allowPartialPageErrors: true,
|
|
13009
|
+
});
|
|
13010
|
+
const rateLimit = fetchResult.pacing.rateLimit;
|
|
13011
|
+
const pageError = fetchResult.pacing.pageError;
|
|
13012
|
+
const partialError = describeLocalSalesNavigatorAccountPartialError(fetchResult);
|
|
13013
|
+
const cumulativeStoredAccounts = slice.storedAccounts + fetchResult.accounts.length;
|
|
13014
|
+
const attempt = repairSliceToCrawlAttempt({
|
|
13015
|
+
...slice,
|
|
13016
|
+
maxResultsPerSearch: fetchResult.totalResults ??
|
|
13017
|
+
slice.totalResults ??
|
|
13018
|
+
slice.maxResultsPerSearch,
|
|
13019
|
+
pageSize,
|
|
13020
|
+
});
|
|
13021
|
+
const sliceId = await upsertSalesNavigatorAccountSearchSlice({
|
|
13022
|
+
store,
|
|
13023
|
+
runId,
|
|
13024
|
+
orgId: slice.orgId,
|
|
13025
|
+
attempt,
|
|
13026
|
+
status: partialError ? "failed" : "finished",
|
|
13027
|
+
totalResults: fetchResult.totalResults ?? slice.totalResults,
|
|
13028
|
+
fetchedPages: slice.fetchedPages + fetchResult.fetchedPages,
|
|
13029
|
+
storedAccounts: cumulativeStoredAccounts,
|
|
13030
|
+
retryCount: slice.retryCount + fetchResult.pacing.retryCount,
|
|
13031
|
+
retryDelayMs: slice.retryDelayMs + fetchResult.pacing.totalDelayMs,
|
|
13032
|
+
lastError: partialError,
|
|
13033
|
+
rawPayload: {
|
|
13034
|
+
...slice.rawPayload,
|
|
13035
|
+
repair: {
|
|
13036
|
+
repairedAt: new Date().toISOString(),
|
|
13037
|
+
previousStoredAccounts: slice.storedAccounts,
|
|
13038
|
+
startOffset,
|
|
13039
|
+
fetchedAccounts: fetchResult.accounts.length,
|
|
13040
|
+
},
|
|
13041
|
+
pacing: fetchResult.pacing,
|
|
13042
|
+
stop: fetchResult.pacing.stop,
|
|
13043
|
+
},
|
|
13044
|
+
});
|
|
13045
|
+
const storedAccounts = await upsertSalesNavigatorAccounts({
|
|
13046
|
+
store,
|
|
13047
|
+
orgId: slice.orgId,
|
|
13048
|
+
runId,
|
|
13049
|
+
sliceId,
|
|
13050
|
+
accounts: fetchResult.accounts,
|
|
13051
|
+
});
|
|
13052
|
+
repaired.push({
|
|
13053
|
+
sliceId,
|
|
13054
|
+
slicedQueryUrl: slice.slicedQueryUrl,
|
|
13055
|
+
startOffset,
|
|
13056
|
+
totalResults: fetchResult.totalResults ?? slice.totalResults,
|
|
13057
|
+
fetchedPages: fetchResult.fetchedPages,
|
|
13058
|
+
fetchedAccounts: fetchResult.accounts.length,
|
|
13059
|
+
storedAccounts,
|
|
13060
|
+
cumulativeStoredAccounts,
|
|
13061
|
+
status: partialError ? "failed" : "finished",
|
|
13062
|
+
stop: fetchResult.pacing.stop,
|
|
13063
|
+
error: partialError,
|
|
13064
|
+
});
|
|
13065
|
+
if (rateLimit) {
|
|
13066
|
+
stoppedByRateLimit = {
|
|
13067
|
+
sliceId,
|
|
13068
|
+
message: rateLimit.message,
|
|
13069
|
+
};
|
|
13070
|
+
break;
|
|
13071
|
+
}
|
|
13072
|
+
}
|
|
13073
|
+
const summary = await summarizeSalesNavigatorAccountSearchRunFromSlices({
|
|
13074
|
+
store,
|
|
13075
|
+
runId,
|
|
13076
|
+
});
|
|
13077
|
+
await updateSalesNavigatorAccountSearchRun({
|
|
13078
|
+
store,
|
|
13079
|
+
runId,
|
|
13080
|
+
status: summary.truncated ? "failed" : "finished",
|
|
13081
|
+
totalSlices: summary.totalSlices,
|
|
13082
|
+
storedAccounts: summary.storedAccounts,
|
|
13083
|
+
attemptedSlices: summary.attemptedSlices,
|
|
13084
|
+
splitEvents: summary.splitEvents,
|
|
13085
|
+
failedSlices: summary.failedSlices,
|
|
13086
|
+
unresolvedSlices: summary.unresolvedSlices,
|
|
13087
|
+
truncated: summary.truncated,
|
|
13088
|
+
lastError: stoppedByRateLimit?.message ?? summary.lastError,
|
|
13089
|
+
rawPayload: {
|
|
13090
|
+
lastRepair: {
|
|
13091
|
+
repairedAt: new Date().toISOString(),
|
|
13092
|
+
repairedSlices: repaired.length,
|
|
13093
|
+
skippedSlices: skipped.length,
|
|
13094
|
+
stoppedByRateLimit,
|
|
13095
|
+
},
|
|
13096
|
+
},
|
|
13097
|
+
});
|
|
13098
|
+
const payload = {
|
|
13099
|
+
status: stoppedByRateLimit ? "rate_limited" : "ok",
|
|
13100
|
+
mode: "account-repair",
|
|
13101
|
+
runId,
|
|
13102
|
+
inspectedSlices: repaired.length + skipped.length,
|
|
13103
|
+
repaired,
|
|
13104
|
+
skipped,
|
|
13105
|
+
stoppedByRateLimit,
|
|
13106
|
+
run: summary,
|
|
13107
|
+
};
|
|
13108
|
+
if (options.out) {
|
|
13109
|
+
await writeJsonFile(options.out, payload);
|
|
13110
|
+
}
|
|
13111
|
+
printOutput(payload);
|
|
13112
|
+
if (stoppedByRateLimit) {
|
|
13113
|
+
throw new Error(stoppedByRateLimit.message);
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
finally {
|
|
13117
|
+
await closeSalesNavigatorAccountSearchStore(store);
|
|
13118
|
+
}
|
|
13119
|
+
});
|
|
13120
|
+
program
|
|
13121
|
+
.command("salesnav:accounts:status")
|
|
13122
|
+
.alias("accounts:status")
|
|
13123
|
+
.description("Return the current status of a Sales Navigator account-search run.")
|
|
13124
|
+
.requiredOption("--run-id <id>", "Sales Navigator account-search run id")
|
|
13125
|
+
.option("--cooldown-seconds <number>", "Seconds to wait after a rate-limit before resuming", "3600")
|
|
13126
|
+
.action(async (options) => {
|
|
13127
|
+
const runId = z.string().uuid().parse(options.runId);
|
|
13128
|
+
const cooldownSeconds = z.coerce
|
|
13129
|
+
.number()
|
|
13130
|
+
.int()
|
|
13131
|
+
.min(60)
|
|
13132
|
+
.max(86_400)
|
|
13133
|
+
.parse(options.cooldownSeconds);
|
|
13134
|
+
const session = await requireAuthSession();
|
|
13135
|
+
const status = await getSalesNavigatorAccountRunStatus(session, runId, {
|
|
13136
|
+
cooldownSeconds
|
|
13137
|
+
});
|
|
13138
|
+
printOutput({
|
|
13139
|
+
status: "ok",
|
|
13140
|
+
runId,
|
|
13141
|
+
run: status.value.run,
|
|
13142
|
+
workflow: {
|
|
13143
|
+
...status.value.run.workflow,
|
|
13144
|
+
repairCommand: status.value.run.nextAction === "complete"
|
|
13145
|
+
? null
|
|
13146
|
+
: buildSalesNavigatorAccountRepairCommand({ runId }),
|
|
13147
|
+
},
|
|
13148
|
+
stoppedSlices: status.value.run.problemSlices.map((slice) => ({
|
|
13149
|
+
sliceId: slice.sliceId,
|
|
13150
|
+
status: slice.status,
|
|
13151
|
+
totalResults: slice.totalResults,
|
|
13152
|
+
storedAccounts: slice.storedAccounts,
|
|
13153
|
+
stopReason: slice.stopReason,
|
|
13154
|
+
nextStart: slice.nextStart,
|
|
13155
|
+
missingFromReported: slice.missingFromReported,
|
|
13156
|
+
lastError: slice.lastError,
|
|
13157
|
+
slicedQueryUrl: slice.slicedQueryUrl
|
|
13158
|
+
}))
|
|
13159
|
+
});
|
|
13160
|
+
});
|
|
13161
|
+
program
|
|
13162
|
+
.command("salesnav:accounts:resume")
|
|
13163
|
+
.alias("accounts:resume")
|
|
13164
|
+
.description("Start the durable account-search coordinator for a previous account run.")
|
|
13165
|
+
.requiredOption("--run-id <id>", "Sales Navigator account-search run id")
|
|
13166
|
+
.option("--cooldown-seconds <number>", "Seconds to wait after a rate-limit before resuming", "3600")
|
|
13167
|
+
.option("--status-only", "Only inspect the run and print the next action without starting a Vercel workflow", false)
|
|
13168
|
+
.action(async (options) => {
|
|
13169
|
+
const runId = z.string().uuid().parse(options.runId);
|
|
13170
|
+
const cooldownSeconds = z.coerce
|
|
13171
|
+
.number()
|
|
13172
|
+
.int()
|
|
13173
|
+
.min(60)
|
|
13174
|
+
.max(86_400)
|
|
13175
|
+
.parse(options.cooldownSeconds);
|
|
13176
|
+
const session = await requireAuthSession();
|
|
13177
|
+
const status = await getSalesNavigatorAccountRunStatus(session, runId, {
|
|
13178
|
+
cooldownSeconds
|
|
13179
|
+
});
|
|
13180
|
+
if (options.statusOnly) {
|
|
13181
|
+
printOutput({
|
|
13182
|
+
status: "ok",
|
|
13183
|
+
runId,
|
|
13184
|
+
startedWorkflow: false,
|
|
13185
|
+
nextAction: status.value.run.nextAction,
|
|
13186
|
+
run: status.value.run,
|
|
13187
|
+
workflow: {
|
|
13188
|
+
...status.value.run.workflow,
|
|
13189
|
+
repairCommand: status.value.run.nextAction === "complete"
|
|
13190
|
+
? null
|
|
13191
|
+
: buildSalesNavigatorAccountRepairCommand({ runId }),
|
|
13192
|
+
},
|
|
13193
|
+
});
|
|
13194
|
+
return;
|
|
13195
|
+
}
|
|
13196
|
+
const workflow = await startSalesNavigatorAccountRunWorkflow(status.session, runId, { cooldownSeconds });
|
|
13197
|
+
printOutput({
|
|
13198
|
+
status: "accepted",
|
|
13199
|
+
runId,
|
|
13200
|
+
workflowRunId: workflow.value.workflowRunId,
|
|
13201
|
+
nextAction: status.value.run.nextAction,
|
|
13202
|
+
cooldownUntil: status.value.run.cooldownUntil,
|
|
13203
|
+
repairCommand: buildSalesNavigatorAccountRepairCommand({ runId }),
|
|
13204
|
+
problemSlices: status.value.run.problemSlices
|
|
13205
|
+
});
|
|
13206
|
+
});
|
|
10623
13207
|
program
|
|
10624
13208
|
.command("phantombuster:containers:sync")
|
|
10625
13209
|
.alias("pb:containers:sync")
|
|
10626
13210
|
.description("Fetch Phantombuster containers for configured agents and store them in Neon.")
|
|
10627
13211
|
.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, [])
|
|
10629
13212
|
.option("--limit <number>", "Maximum containers to fetch per Phantombuster page", "100")
|
|
10630
13213
|
.option("--max-pages <number>", "Maximum Phantombuster pages to fetch per agent", "50")
|
|
10631
13214
|
.option("--mode <mode>", "Phantombuster container mode: all or finalized", "all")
|
|
10632
13215
|
.option("--before-ended-at <iso>", "Only fetch containers that ended before this ISO timestamp")
|
|
10633
13216
|
.option("--metadata-only", "Store container metadata without fetching output and result objects", false)
|
|
10634
|
-
.option("--refresh-lead-pool", "After syncing results, rebuild the Neon lead_pool_new reporting table. This can take several minutes.", false)
|
|
10635
13217
|
.option("--out <path>", "Optional local JSON output path")
|
|
10636
13218
|
.action(async (options) => {
|
|
10637
13219
|
const agentIds = z.array(z.string().min(1)).parse(options.agentId);
|
|
10638
|
-
const containerIds = z.array(z.string().min(1)).parse(options.containerId);
|
|
10639
13220
|
const limit = z.coerce.number().int().min(1).max(500).parse(options.limit);
|
|
10640
13221
|
const maxPages = z.coerce.number().int().min(1).max(500).parse(options.maxPages);
|
|
10641
13222
|
const mode = z.enum(["all", "finalized"]).parse(options.mode);
|
|
@@ -10645,13 +13226,11 @@ program
|
|
|
10645
13226
|
const session = await requireAuthSession();
|
|
10646
13227
|
const result = await syncPhantombusterContainersViaApp(session, {
|
|
10647
13228
|
agentIds: agentIds.length > 0 ? agentIds : undefined,
|
|
10648
|
-
containerIds: containerIds.length > 0 ? containerIds : undefined,
|
|
10649
13229
|
limit,
|
|
10650
13230
|
maxPages,
|
|
10651
13231
|
mode,
|
|
10652
13232
|
beforeEndedAt,
|
|
10653
|
-
includeResults: !options.metadataOnly
|
|
10654
|
-
refreshLeadPool: Boolean(options.refreshLeadPool)
|
|
13233
|
+
includeResults: !options.metadataOnly
|
|
10655
13234
|
});
|
|
10656
13235
|
const payload = {
|
|
10657
13236
|
...result,
|
|
@@ -10737,100 +13316,6 @@ program
|
|
|
10737
13316
|
}
|
|
10738
13317
|
printOutput(payload);
|
|
10739
13318
|
});
|
|
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
|
-
});
|
|
10834
13319
|
program
|
|
10835
13320
|
.command("salesnav:count")
|
|
10836
13321
|
.alias("search:count")
|
|
@@ -11493,54 +13978,6 @@ program
|
|
|
11493
13978
|
execution
|
|
11494
13979
|
});
|
|
11495
13980
|
});
|
|
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
|
-
});
|
|
11544
13981
|
program
|
|
11545
13982
|
.command("contacts:process-emails")
|
|
11546
13983
|
.alias("contacts:resolve-emails")
|
|
@@ -11552,8 +13989,6 @@ program
|
|
|
11552
13989
|
.requiredOption("--out-dir <path>", "Output directory for artifacts")
|
|
11553
13990
|
.option("--trace-id <traceId>", "Trace id for this batch")
|
|
11554
13991
|
.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")
|
|
11557
13992
|
.option("--timeout-ms <number>", "Workflow trigger timeout in milliseconds", "30000")
|
|
11558
13993
|
.option("--company-cleaning <mode>", "Company cleaning mode for direct input: off, basic, or ai", "basic")
|
|
11559
13994
|
.option("--dry-run", "Preview the next batch without starting background processing", false)
|
|
@@ -11562,69 +13997,7 @@ program
|
|
|
11562
13997
|
const outDir = z.string().min(1).parse(options.outDir);
|
|
11563
13998
|
const timeoutMs = z.coerce.number().int().min(1000).max(300000).parse(options.timeoutMs);
|
|
11564
13999
|
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);
|
|
11567
14000
|
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
|
-
}
|
|
11628
14001
|
let clientId;
|
|
11629
14002
|
let queueRows;
|
|
11630
14003
|
let directReportRows = null;
|