salesprompter-cli 0.1.53 → 0.1.55
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 +13 -1
- package/dist/cli.js +149 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,8 @@ salesprompter leads:collect \
|
|
|
72
72
|
|
|
73
73
|
# Enrich one or more stored imports: Sales Navigator company -> official domain -> Hunter email
|
|
74
74
|
# This updates workspace data only. It does not create or start outreach.
|
|
75
|
-
# Stale
|
|
75
|
+
# Stale ids and raw legal-name misses use conservative exact-name recovery.
|
|
76
|
+
# Hunter prefers official domains and can fall back to the exact company name.
|
|
76
77
|
salesprompter leads:enrich-import \
|
|
77
78
|
--run-id "$CLI_IMPORT_RUN_ID"
|
|
78
79
|
|
|
@@ -109,6 +110,17 @@ salesprompter packs:list
|
|
|
109
110
|
salesprompter --help
|
|
110
111
|
```
|
|
111
112
|
|
|
113
|
+
Download stored workspace leads without starting a new Sales Navigator scrape:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
salesprompter leads:download --out ./salesprompter-leads.csv
|
|
117
|
+
salesprompter leads:download --run-id <uuid> --emails-only --out ./run-leads.csv
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Use `--search <value>` to match a person, title, or company. The command uses the
|
|
121
|
+
active Salesprompter workspace and downloads the same filtered data available in
|
|
122
|
+
the app's CLI imports view.
|
|
123
|
+
|
|
112
124
|
## Output modes
|
|
113
125
|
|
|
114
126
|
- `--json` for machine-readable output
|
package/dist/cli.js
CHANGED
|
@@ -284,6 +284,8 @@ const CliImportHunterBatchResponseSchema = z.object({
|
|
|
284
284
|
found: z.number().int().nonnegative(),
|
|
285
285
|
notFound: z.number().int().nonnegative(),
|
|
286
286
|
failed: z.number().int().nonnegative(),
|
|
287
|
+
domainLookups: z.number().int().nonnegative().default(0),
|
|
288
|
+
companyLookups: z.number().int().nonnegative().default(0),
|
|
287
289
|
halted: z.boolean(),
|
|
288
290
|
error: z.string().nullable()
|
|
289
291
|
});
|
|
@@ -524,6 +526,7 @@ const cliPacks = [
|
|
|
524
526
|
commands: [
|
|
525
527
|
"leads:discover",
|
|
526
528
|
"leads:collect",
|
|
529
|
+
"leads:download",
|
|
527
530
|
"leads:enrich-import",
|
|
528
531
|
"search:run",
|
|
529
532
|
"search:status",
|
|
@@ -585,6 +588,7 @@ const helpVisibleCommandNames = new Set([
|
|
|
585
588
|
"wizard",
|
|
586
589
|
"auth:whoami",
|
|
587
590
|
"llm:ready",
|
|
591
|
+
"leads:download",
|
|
588
592
|
"contacts:find-linkedin-urls",
|
|
589
593
|
"companies:find-linkedin-urls",
|
|
590
594
|
"contacts:process-emails",
|
|
@@ -6755,8 +6759,65 @@ async function runCliImportEnrichmentAction(session, body, schema) {
|
|
|
6755
6759
|
}), schema);
|
|
6756
6760
|
return value;
|
|
6757
6761
|
}
|
|
6762
|
+
function parseDownloadFilename(contentDisposition) {
|
|
6763
|
+
const match = contentDisposition?.match(/filename="?([^";]+)"?/i);
|
|
6764
|
+
return match?.[1]?.trim() || `salesprompter-leads-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
6765
|
+
}
|
|
6766
|
+
async function runLeadsDownloadCommand(options) {
|
|
6767
|
+
const session = await requireAuthSession();
|
|
6768
|
+
const runId = options.runId ? z.string().uuid().parse(options.runId) : null;
|
|
6769
|
+
const search = options.search?.trim() || null;
|
|
6770
|
+
const { value } = await withRefreshableAuthSession(session, async (currentSession) => {
|
|
6771
|
+
const url = new URL('/api/cli/leads/imports/export', currentSession.apiBaseUrl);
|
|
6772
|
+
if (runId)
|
|
6773
|
+
url.searchParams.set('runId', runId);
|
|
6774
|
+
if (search)
|
|
6775
|
+
url.searchParams.set('q', search);
|
|
6776
|
+
if (options.emailsOnly)
|
|
6777
|
+
url.searchParams.set('emailsOnly', '1');
|
|
6778
|
+
const response = await fetch(url, {
|
|
6779
|
+
headers: { Authorization: `Bearer ${currentSession.accessToken}` },
|
|
6780
|
+
});
|
|
6781
|
+
if (!response.ok) {
|
|
6782
|
+
const raw = await response.text();
|
|
6783
|
+
let message = `lead download failed (${response.status})`;
|
|
6784
|
+
try {
|
|
6785
|
+
const payload = JSON.parse(raw);
|
|
6786
|
+
if (typeof payload.error === 'string')
|
|
6787
|
+
message = payload.error;
|
|
6788
|
+
}
|
|
6789
|
+
catch {
|
|
6790
|
+
if (raw.trim())
|
|
6791
|
+
message = raw.trim();
|
|
6792
|
+
}
|
|
6793
|
+
throw new CliApiRequestError(message, { statusCode: response.status });
|
|
6794
|
+
}
|
|
6795
|
+
const countHeader = response.headers.get('x-salesprompter-lead-count');
|
|
6796
|
+
return {
|
|
6797
|
+
bytes: Buffer.from(await response.arrayBuffer()),
|
|
6798
|
+
count: countHeader == null ? null : Number.parseInt(countHeader, 10),
|
|
6799
|
+
filename: parseDownloadFilename(response.headers.get('content-disposition')),
|
|
6800
|
+
};
|
|
6801
|
+
});
|
|
6802
|
+
const outputPath = path.resolve(options.out?.trim() || value.filename);
|
|
6803
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
6804
|
+
await writeFile(outputPath, value.bytes);
|
|
6805
|
+
return {
|
|
6806
|
+
status: 'ok',
|
|
6807
|
+
output: outputPath,
|
|
6808
|
+
leads: Number.isFinite(value.count) ? value.count : null,
|
|
6809
|
+
bytes: value.bytes.length,
|
|
6810
|
+
runId,
|
|
6811
|
+
search,
|
|
6812
|
+
emailsOnly: options.emailsOnly,
|
|
6813
|
+
};
|
|
6814
|
+
}
|
|
6758
6815
|
async function getCliImportCompanyCandidatesViaApp(session, input) {
|
|
6759
|
-
return runCliImportEnrichmentAction(session, {
|
|
6816
|
+
return runCliImportEnrichmentAction(session, {
|
|
6817
|
+
action: "company-candidates",
|
|
6818
|
+
...input,
|
|
6819
|
+
retry: input.retryMode === "all",
|
|
6820
|
+
}, CliImportCompanyCandidatesResponseSchema);
|
|
6760
6821
|
}
|
|
6761
6822
|
async function saveCliImportCompanyProfilesViaApp(session, profiles) {
|
|
6762
6823
|
return runCliImportEnrichmentAction(session, { action: "save-companies", profiles }, CliImportCompanySaveResponseSchema);
|
|
@@ -13446,6 +13507,7 @@ function normalizeCompanyWebsite(value) {
|
|
|
13446
13507
|
}
|
|
13447
13508
|
}
|
|
13448
13509
|
const CLI_IMPORT_COMPANY_DECORATION = "(entityUrn,name,employeeCount,employeeDisplayCount,employeeCountRange,account(saved,noteCount,listCount,crmStatus,starred),pictureInfo,companyPictureDisplayImage,companyBackgroundCoverImage,description,industry,location,headquarters,website,revenueRange,crmOpportunities,flagshipCompanyUrl,employeeGrowthPercentages,employees*~fs_salesProfile(entityUrn,firstName,lastName,fullName,pictureInfo,profilePictureDisplayImage),specialties,type,yearFounded)";
|
|
13510
|
+
const CLI_IMPORT_COMPANY_RESOLUTION_VERSION = "cleaned-search-v1";
|
|
13449
13511
|
class CliImportCompanySessionError extends Error {
|
|
13450
13512
|
constructor(message) {
|
|
13451
13513
|
super(message);
|
|
@@ -13591,24 +13653,39 @@ async function resolveCliImportCompanyIdentity(candidate, config, timeoutMs, bro
|
|
|
13591
13653
|
let account = null;
|
|
13592
13654
|
if (!resolvedCompanyId && candidate.companyName) {
|
|
13593
13655
|
accountSearchUsed = true;
|
|
13594
|
-
const
|
|
13595
|
-
const
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13656
|
+
const seenSearchNames = new Set();
|
|
13657
|
+
const searchNames = [
|
|
13658
|
+
candidate.companyName,
|
|
13659
|
+
aggressivelyCleanLookupCompanyName(candidate.companyName),
|
|
13660
|
+
].filter((value) => {
|
|
13661
|
+
const key = normalizeLookupWhitespace(value).toLowerCase();
|
|
13662
|
+
if (!key || seenSearchNames.has(key))
|
|
13663
|
+
return false;
|
|
13664
|
+
seenSearchNames.add(key);
|
|
13665
|
+
return true;
|
|
13601
13666
|
});
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
:
|
|
13610
|
-
|
|
13611
|
-
|
|
13667
|
+
for (const searchName of searchNames) {
|
|
13668
|
+
const queryUrl = buildCliImportAccountSearchUrl(searchName);
|
|
13669
|
+
const response = await fetchCliImportSalesNavigatorJson({
|
|
13670
|
+
url: queryUrl,
|
|
13671
|
+
config,
|
|
13672
|
+
browserRelay,
|
|
13673
|
+
timeoutMs,
|
|
13674
|
+
label: `Sales Navigator Account Search for ${searchName}`,
|
|
13675
|
+
});
|
|
13676
|
+
if (response.status < 200 || response.status >= 300) {
|
|
13677
|
+
throw new Error(`Sales Navigator Account Search for ${searchName} returned ${response.status}.`);
|
|
13678
|
+
}
|
|
13679
|
+
if (response.body) {
|
|
13680
|
+
const accounts = extractLocalSalesNavigatorElements(response.body)
|
|
13681
|
+
.map((element) => typeof element === "object" && element !== null && !Array.isArray(element)
|
|
13682
|
+
? normalizeLocalSalesNavigatorAccount(element, queryUrl)
|
|
13683
|
+
: null)
|
|
13684
|
+
.filter((value) => value !== null);
|
|
13685
|
+
account = exactCliImportAccountMatch(candidate, accounts);
|
|
13686
|
+
}
|
|
13687
|
+
if (account)
|
|
13688
|
+
break;
|
|
13612
13689
|
}
|
|
13613
13690
|
salesNavCompanyUrl =
|
|
13614
13691
|
(typeof account?.salesNavCompanyUrl === "string"
|
|
@@ -13708,6 +13785,7 @@ function buildCliImportCompanyProfile(resolution, detail, input) {
|
|
|
13708
13785
|
error: input.error ??
|
|
13709
13786
|
`Sales Navigator company detail returned ${input.status || "no result"}`,
|
|
13710
13787
|
rawPayload: {
|
|
13788
|
+
resolutionVersion: CLI_IMPORT_COMPANY_RESOLUTION_VERSION,
|
|
13711
13789
|
accountSearchUsed: resolution.accountSearchUsed,
|
|
13712
13790
|
httpStatus: input.status,
|
|
13713
13791
|
},
|
|
@@ -13780,6 +13858,7 @@ function buildCliImportCompanyProfile(resolution, detail, input) {
|
|
|
13780
13858
|
status: "enriched",
|
|
13781
13859
|
error: null,
|
|
13782
13860
|
rawPayload: {
|
|
13861
|
+
resolutionVersion: CLI_IMPORT_COMPANY_RESOLUTION_VERSION,
|
|
13783
13862
|
accountSearchUsed: resolution.accountSearchUsed,
|
|
13784
13863
|
httpStatus: input.status,
|
|
13785
13864
|
detail,
|
|
@@ -13791,6 +13870,12 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
13791
13870
|
if (options.companiesOnly && options.hunterOnly) {
|
|
13792
13871
|
throw new Error("Use either --companies-only or --hunter-only, not both.");
|
|
13793
13872
|
}
|
|
13873
|
+
if (options.retryCompanies && options.retryNotFoundCompanies) {
|
|
13874
|
+
throw new Error("Use either --retry-companies or --retry-not-found-companies, not both.");
|
|
13875
|
+
}
|
|
13876
|
+
if (options.retryEmails && options.retryFailedEmails) {
|
|
13877
|
+
throw new Error("Use either --retry-emails or --retry-failed-emails, not both.");
|
|
13878
|
+
}
|
|
13794
13879
|
const runIds = Array.from(new Set(options.runId.map((runId) => z.string().uuid().parse(runId))));
|
|
13795
13880
|
const companyTimeoutMs = z.coerce.number().int().min(1000).max(120000).parse(options.companyTimeoutMs);
|
|
13796
13881
|
const companyDelayMinMs = z.coerce.number().int().min(0).parse(options.companyDelayMinMs);
|
|
@@ -13805,13 +13890,24 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
13805
13890
|
const session = await requireAuthSession();
|
|
13806
13891
|
const startedAt = new Date().toISOString();
|
|
13807
13892
|
const initialStatus = await getCliImportEnrichmentStatusViaApp(session, runIds);
|
|
13893
|
+
const companyRetryMode = options.retryCompanies
|
|
13894
|
+
? "all"
|
|
13895
|
+
: options.retryNotFoundCompanies
|
|
13896
|
+
? "not_found"
|
|
13897
|
+
: "default";
|
|
13898
|
+
const retryStatuses = options.retryEmails
|
|
13899
|
+
? ["not_found", "failed"]
|
|
13900
|
+
: options.retryFailedEmails
|
|
13901
|
+
? ["failed"]
|
|
13902
|
+
: [];
|
|
13808
13903
|
if (options.dryRun) {
|
|
13809
13904
|
const companyCandidates = options.hunterOnly
|
|
13810
13905
|
? []
|
|
13811
13906
|
: (await getCliImportCompanyCandidatesViaApp(session, {
|
|
13812
13907
|
runIds,
|
|
13813
13908
|
limit: 1000,
|
|
13814
|
-
|
|
13909
|
+
retryMode: companyRetryMode,
|
|
13910
|
+
resolutionVersion: CLI_IMPORT_COMPANY_RESOLUTION_VERSION,
|
|
13815
13911
|
})).companies.slice(0, maxCompanies || undefined);
|
|
13816
13912
|
return {
|
|
13817
13913
|
status: "ok",
|
|
@@ -13819,6 +13915,9 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
13819
13915
|
runIds,
|
|
13820
13916
|
initial: initialStatus,
|
|
13821
13917
|
companyCandidates: companyCandidates.length,
|
|
13918
|
+
companyRetryMode,
|
|
13919
|
+
hunterCompanyFallback: options.hunterCompanyFallback,
|
|
13920
|
+
retryEmailStatuses: retryStatuses,
|
|
13822
13921
|
next: options.companiesOnly
|
|
13823
13922
|
? "company enrichment only"
|
|
13824
13923
|
: options.hunterOnly
|
|
@@ -13838,7 +13937,8 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
13838
13937
|
const companyCandidates = (await getCliImportCompanyCandidatesViaApp(session, {
|
|
13839
13938
|
runIds,
|
|
13840
13939
|
limit: 1000,
|
|
13841
|
-
|
|
13940
|
+
retryMode: companyRetryMode,
|
|
13941
|
+
resolutionVersion: CLI_IMPORT_COMPANY_RESOLUTION_VERSION,
|
|
13842
13942
|
})).companies.slice(0, maxCompanies || undefined);
|
|
13843
13943
|
companies.selected = companyCandidates.length;
|
|
13844
13944
|
if (companyCandidates.length > 0) {
|
|
@@ -14088,15 +14188,21 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
14088
14188
|
processed: 0,
|
|
14089
14189
|
found: 0,
|
|
14090
14190
|
notFound: 0,
|
|
14091
|
-
failed: 0
|
|
14191
|
+
failed: 0,
|
|
14192
|
+
domainLookups: 0,
|
|
14193
|
+
companyLookups: 0,
|
|
14092
14194
|
};
|
|
14093
14195
|
if (!options.companiesOnly) {
|
|
14094
|
-
const retryBefore = options.retryEmails
|
|
14196
|
+
const retryBefore = options.retryEmails || options.retryFailedEmails
|
|
14197
|
+
? startedAt
|
|
14198
|
+
: null;
|
|
14095
14199
|
while (maxHunterBatches === 0 || hunter.batches < maxHunterBatches) {
|
|
14096
14200
|
const batch = await runCliImportHunterBatchViaApp(session, {
|
|
14097
14201
|
runIds,
|
|
14098
14202
|
limit: hunterBatchSize,
|
|
14099
|
-
retryBefore
|
|
14203
|
+
retryBefore,
|
|
14204
|
+
allowCompanyFallback: options.hunterCompanyFallback,
|
|
14205
|
+
retryStatuses,
|
|
14100
14206
|
});
|
|
14101
14207
|
if (batch.selected === 0)
|
|
14102
14208
|
break;
|
|
@@ -14106,7 +14212,9 @@ async function runSalesNavigatorImportEnrichmentCommand(options) {
|
|
|
14106
14212
|
processed: hunter.processed + batch.processed,
|
|
14107
14213
|
found: hunter.found + batch.found,
|
|
14108
14214
|
notFound: hunter.notFound + batch.notFound,
|
|
14109
|
-
failed: hunter.failed + batch.failed
|
|
14215
|
+
failed: hunter.failed + batch.failed,
|
|
14216
|
+
domainLookups: hunter.domainLookups + batch.domainLookups,
|
|
14217
|
+
companyLookups: hunter.companyLookups + batch.companyLookups,
|
|
14110
14218
|
};
|
|
14111
14219
|
writeProgress(`Hunter enrichment: ${hunter.processed} checked, ${hunter.found} emails found.`);
|
|
14112
14220
|
if (batch.halted) {
|
|
@@ -14231,6 +14339,17 @@ program
|
|
|
14231
14339
|
.action(async (options) => {
|
|
14232
14340
|
printOutput(await runSalesNavigatorPeopleCollectCommand(options));
|
|
14233
14341
|
});
|
|
14342
|
+
program
|
|
14343
|
+
.command("leads:download")
|
|
14344
|
+
.alias("leads:export-csv")
|
|
14345
|
+
.description("Download stored leads from the active Salesprompter workspace as CSV.")
|
|
14346
|
+
.option("--run-id <uuid>", "Limit the CSV to one CLI import run")
|
|
14347
|
+
.option("--search <value>", "Filter by person, title, or company")
|
|
14348
|
+
.option("--emails-only", "Include only leads with a found email", false)
|
|
14349
|
+
.option("--out <path>", "CSV output path; defaults to a dated filename")
|
|
14350
|
+
.action(async (options) => {
|
|
14351
|
+
printOutput(await runLeadsDownloadCommand(options));
|
|
14352
|
+
});
|
|
14234
14353
|
program
|
|
14235
14354
|
.command("salesnav:people:enrich")
|
|
14236
14355
|
.alias("leads:enrich-import")
|
|
@@ -14245,9 +14364,12 @@ program
|
|
|
14245
14364
|
.option("--hunter-batch-size <number>", "Hunter contacts checked per request", "100")
|
|
14246
14365
|
.option("--max-hunter-batches <number>", "Maximum Hunter batches; 0 continues until no eligible people remain", "0")
|
|
14247
14366
|
.option("--companies-only", "Stop after company profiles and domains", false)
|
|
14248
|
-
.option("--hunter-only", "Skip company lookup and use
|
|
14249
|
-
.option("--retry-companies", "
|
|
14250
|
-
.option("--retry-
|
|
14367
|
+
.option("--hunter-only", "Skip company lookup and use stored domains or company-name fallback", false)
|
|
14368
|
+
.option("--retry-companies", "Refresh every previously saved company profile", false)
|
|
14369
|
+
.option("--retry-not-found-companies", "Retry only previous exact company misses with the current resolver", false)
|
|
14370
|
+
.option("--retry-emails", "Retry previous Hunter misses and failures once", false)
|
|
14371
|
+
.option("--retry-failed-emails", "Retry only previous Hunter failures once", false)
|
|
14372
|
+
.option("--no-hunter-company-fallback", "Require a verified domain and skip Hunter company-name fallback")
|
|
14251
14373
|
.option("--dry-run", "Show scope without LinkedIn or Hunter requests", false)
|
|
14252
14374
|
.option("--out <path>", "Optional JSON result path")
|
|
14253
14375
|
.action(async (options) => {
|
package/package.json
CHANGED