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.
Files changed (3) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.js +149 -27
  3. 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 imported company ids fall back to exact-name Account Search automatically.
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, { action: "company-candidates", ...input }, CliImportCompanyCandidatesResponseSchema);
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 queryUrl = buildCliImportAccountSearchUrl(candidate.companyName);
13595
- const response = await fetchCliImportSalesNavigatorJson({
13596
- url: queryUrl,
13597
- config,
13598
- browserRelay,
13599
- timeoutMs,
13600
- label: `Sales Navigator Account Search for ${candidate.companyName}`,
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
- if (response.status < 200 || response.status >= 300) {
13603
- throw new Error(`Sales Navigator Account Search for ${candidate.companyName} returned ${response.status}.`);
13604
- }
13605
- if (response.status >= 200 && response.status < 300 && response.body) {
13606
- const accounts = extractLocalSalesNavigatorElements(response.body)
13607
- .map((element) => typeof element === "object" && element !== null && !Array.isArray(element)
13608
- ? normalizeLocalSalesNavigatorAccount(element, queryUrl)
13609
- : null)
13610
- .filter((value) => value !== null);
13611
- account = exactCliImportAccountMatch(candidate, accounts);
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
- retry: options.retryCompanies
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
- retry: options.retryCompanies
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 ? startedAt : null;
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 already stored domains", false)
14249
- .option("--retry-companies", "Retry previously saved company failures", false)
14250
- .option("--retry-emails", "Retry previously checked unresolved emails once", false)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Sales workflow CLI for guided lead generation, enrichment, scoring, and sync.",
5
5
  "author": "Daniel Sinewe <hello@danielsinewe.com>",
6
6
  "type": "module",