salesprompter-cli 0.1.58 → 0.1.60

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 +2 -1
  2. package/dist/cli.js +144 -87
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,7 +66,8 @@ salesprompter companies:resolve-linkedin-urls --in ./companies.txt --out ./compa
66
66
  salesprompter contacts:resolve-emails --in ./contacts.tsv --out-dir ./email-run --dry-run
67
67
 
68
68
  # Collect a Sales Navigator people search into the active workspace
69
- # Oversized or pagination-short searches are split by company headcount and deduplicated.
69
+ # Normal people searches use 2,500-result slices; Connections-of searches use 1,000.
70
+ # Oversized searches split by company headcount and deduplicate by canonical profile URL.
70
71
  salesprompter leads:collect \
71
72
  --linkedin-url "$SALES_NAV_PEOPLE_URL"
72
73
 
package/dist/cli.js CHANGED
@@ -8019,7 +8019,7 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
8019
8019
  },
8020
8020
  };
8021
8021
  }
8022
- const LOCAL_ACCOUNT_SEARCH_COLLECTOR_RESULT_WINDOW = 1600;
8022
+ const LOCAL_ACCOUNT_SEARCH_COLLECTOR_RESULT_WINDOW = 1000;
8023
8023
  const LOCAL_ACCOUNT_SEARCH_COLLECTOR_COVERAGE_PASS_VERSION = 3;
8024
8024
  const LOCAL_ACCOUNT_SEARCH_COLLECTOR_COVERAGE_PASS = "coverage-complete";
8025
8025
  const LOCAL_ACCOUNT_SEARCH_COLLECTOR_PASSES = [
@@ -8724,6 +8724,7 @@ async function fetchAllLocalSalesNavigatorPeople(parsedRequest, options) {
8724
8724
  },
8725
8725
  };
8726
8726
  }
8727
+ const LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT = 5;
8727
8728
  function canonicalLocalSalesNavigatorLeadKey(person) {
8728
8729
  try {
8729
8730
  const url = new URL(person.profileUrl);
@@ -8752,111 +8753,161 @@ async function fetchCompleteLocalSalesNavigatorPeople(sourceQueryUrl, requestHea
8752
8753
  const dimensions = localPeopleSplitDimensions(root.sourceQueryUrl);
8753
8754
  const queue = [root];
8754
8755
  const queuedUrls = new Set([root.slicedQueryUrl]);
8756
+ const rootRecoveryDimensionKeys = new Set();
8755
8757
  const peopleByKey = new Map();
8756
8758
  const slices = [];
8757
8759
  let rootTotalResults = null;
8758
8760
  let fetchedPages = 0;
8759
8761
  let totalDelayMs = 0;
8760
8762
  let retryCount = 0;
8761
- while (queue.length > 0) {
8762
- const attempt = queue.shift();
8763
- const parsedRequest = {
8764
- url: buildSalesNavigatorLeadApiUrlFromSearchUrl(attempt.slicedQueryUrl, options.pageSize),
8765
- headers: requestHeaders,
8766
- };
8767
- let collected = null;
8768
- let tooBroadTotal = null;
8769
- let probeFetchedPages = 0;
8770
- try {
8771
- collected = await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
8772
- requestedProfiles: options.maxResultsPerSearch,
8773
- maxAllowedResults: options.maxResultsPerSearch,
8774
- pageSize: options.pageSize,
8775
- pageDelayMinMs: options.pageDelayMinMs,
8776
- pageDelayMaxMs: options.pageDelayMaxMs,
8777
- retry: options.retry,
8778
- executeRequest: options.executeRequest,
8779
- });
8780
- fetchedPages += collected.fetchedPages;
8781
- totalDelayMs += collected.pacing.totalDelayMs;
8782
- retryCount += collected.pacing.retryCount;
8783
- if (attempt.depth === 0) {
8784
- rootTotalResults = collected.totalResults;
8785
- }
8786
- }
8787
- catch (error) {
8788
- if (!(error instanceof SalesNavigatorSliceTooBroadError)) {
8789
- throw error;
8763
+ while (true) {
8764
+ while (queue.length > 0) {
8765
+ const attempt = queue.shift();
8766
+ const parsedRequest = {
8767
+ url: buildSalesNavigatorLeadApiUrlFromSearchUrl(attempt.slicedQueryUrl, options.pageSize),
8768
+ headers: requestHeaders,
8769
+ };
8770
+ let collected = null;
8771
+ let tooBroadTotal = null;
8772
+ let probeFetchedPages = 0;
8773
+ try {
8774
+ collected = await fetchAllLocalSalesNavigatorPeople(parsedRequest, {
8775
+ requestedProfiles: options.maxResultsPerSearch,
8776
+ maxAllowedResults: options.maxResultsPerSearch,
8777
+ pageSize: options.pageSize,
8778
+ pageDelayMinMs: options.pageDelayMinMs,
8779
+ pageDelayMaxMs: options.pageDelayMaxMs,
8780
+ retry: options.retry,
8781
+ executeRequest: options.executeRequest,
8782
+ });
8783
+ fetchedPages += collected.fetchedPages;
8784
+ totalDelayMs += collected.pacing.totalDelayMs;
8785
+ retryCount += collected.pacing.retryCount;
8786
+ if (attempt.depth === 0) {
8787
+ rootTotalResults = collected.totalResults;
8788
+ }
8790
8789
  }
8791
- tooBroadTotal = error.totalResults;
8792
- const details = error.details && typeof error.details === "object"
8793
- ? error.details
8794
- : {};
8795
- probeFetchedPages = Number(details.fetchedPages ?? 1);
8796
- fetchedPages += probeFetchedPages;
8797
- retryCount += Number(details.retryCount ?? 0);
8798
- totalDelayMs += Number(details.totalDelayMs ?? 0);
8799
- if (attempt.depth === 0) {
8800
- rootTotalResults = tooBroadTotal;
8790
+ catch (error) {
8791
+ if (!(error instanceof SalesNavigatorSliceTooBroadError)) {
8792
+ throw error;
8793
+ }
8794
+ tooBroadTotal = error.totalResults;
8795
+ const details = error.details && typeof error.details === "object"
8796
+ ? error.details
8797
+ : {};
8798
+ probeFetchedPages = Number(details.fetchedPages ?? 1);
8799
+ fetchedPages += probeFetchedPages;
8800
+ retryCount += Number(details.retryCount ?? 0);
8801
+ totalDelayMs += Number(details.totalDelayMs ?? 0);
8802
+ if (attempt.depth === 0) {
8803
+ rootTotalResults = tooBroadTotal;
8804
+ }
8801
8805
  }
8802
- }
8803
- const needsSplit = tooBroadTotal != null ||
8804
- (collected?.totalResults == null &&
8805
- collected != null &&
8806
- collected.people.length >= options.maxResultsPerSearch) ||
8807
- (collected?.totalResults != null &&
8808
- collected.people.length < collected.totalResults);
8809
- if (needsSplit) {
8810
- const usedDimensionKeys = new Set(attempt.splitTrail.map((entry) => entry.key.replace(/-residual$/, "")));
8811
- const nextDimension = dimensions.find((dimension) => !usedDimensionKeys.has(dimension.key));
8812
- if (!nextDimension) {
8813
- throw new Error(`Sales Navigator search still exceeds the ${options.maxResultsPerSearch}-person window after all safe split dimensions were used. Narrow the source search further.`);
8806
+ const needsSplit = tooBroadTotal != null ||
8807
+ (collected?.totalResults == null &&
8808
+ collected != null &&
8809
+ collected.people.length >= options.maxResultsPerSearch) ||
8810
+ (attempt.depth === 0 &&
8811
+ collected?.totalResults != null &&
8812
+ collected.people.length < collected.totalResults &&
8813
+ collected.totalResults - collected.people.length >
8814
+ LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT);
8815
+ if (needsSplit) {
8816
+ const usedDimensionKeys = new Set(attempt.splitTrail.map((entry) => entry.key.replace(/-residual$/, "")));
8817
+ const nextDimension = dimensions.find((dimension) => !usedDimensionKeys.has(dimension.key));
8818
+ if (!nextDimension) {
8819
+ throw new Error(`Sales Navigator search could not be completed within the ${options.maxResultsPerSearch}-person window after all safe split dimensions were used. Narrow the source search further.`);
8820
+ }
8821
+ if (attempt.depth === 0) {
8822
+ rootRecoveryDimensionKeys.add(nextDimension.key);
8823
+ }
8824
+ const children = expandSalesNavigatorCrawlAttempt(attempt, nextDimension, "people");
8825
+ if (nextDimension.supportsResidualExclusion !== false) {
8826
+ children.push(buildSalesNavigatorResidualCrawlAttempt(attempt, nextDimension, "people"));
8827
+ }
8828
+ for (const child of children) {
8829
+ if (queuedUrls.has(child.slicedQueryUrl))
8830
+ continue;
8831
+ queuedUrls.add(child.slicedQueryUrl);
8832
+ queue.push(child);
8833
+ }
8834
+ slices.push({
8835
+ depth: attempt.depth,
8836
+ splitTrail: attempt.splitTrail,
8837
+ totalResults: tooBroadTotal ?? collected?.totalResults ?? null,
8838
+ collected: 0,
8839
+ fetchedPages: probeFetchedPages || collected?.fetchedPages || 0,
8840
+ status: "split",
8841
+ splitDimension: nextDimension.key,
8842
+ });
8843
+ continue;
8814
8844
  }
8815
- const children = expandSalesNavigatorCrawlAttempt(attempt, nextDimension, "people");
8816
- if (nextDimension.supportsResidualExclusion !== false) {
8817
- children.push(buildSalesNavigatorResidualCrawlAttempt(attempt, nextDimension, "people"));
8845
+ if (!collected) {
8846
+ throw new Error("Sales Navigator slice did not return a collection result.");
8818
8847
  }
8819
- for (const child of children) {
8820
- if (queuedUrls.has(child.slicedQueryUrl))
8821
- continue;
8822
- queuedUrls.add(child.slicedQueryUrl);
8823
- queue.push(child);
8848
+ for (const person of collected.people) {
8849
+ peopleByKey.set(canonicalLocalSalesNavigatorLeadKey(person), person);
8824
8850
  }
8825
8851
  slices.push({
8826
8852
  depth: attempt.depth,
8827
8853
  splitTrail: attempt.splitTrail,
8828
- totalResults: tooBroadTotal ?? collected?.totalResults ?? null,
8829
- collected: 0,
8830
- fetchedPages: probeFetchedPages || collected?.fetchedPages || 0,
8831
- status: "split",
8832
- splitDimension: nextDimension.key,
8854
+ totalResults: collected.totalResults,
8855
+ collected: collected.people.length,
8856
+ fetchedPages: collected.fetchedPages,
8857
+ status: "collected",
8858
+ splitDimension: null,
8833
8859
  });
8834
- continue;
8835
8860
  }
8836
- if (!collected) {
8837
- throw new Error("Sales Navigator slice did not return a collection result.");
8861
+ const currentReportedCountDrift = rootTotalResults == null
8862
+ ? 0
8863
+ : Math.max(0, rootTotalResults - peopleByKey.size);
8864
+ const currentReportedCountDriftAccepted = rootTotalResults != null &&
8865
+ rootTotalResults <= options.maxResultsPerSearch &&
8866
+ currentReportedCountDrift <=
8867
+ LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT;
8868
+ if (rootTotalResults == null ||
8869
+ peopleByKey.size >= rootTotalResults ||
8870
+ currentReportedCountDriftAccepted) {
8871
+ break;
8838
8872
  }
8839
- for (const person of collected.people) {
8840
- peopleByKey.set(canonicalLocalSalesNavigatorLeadKey(person), person);
8873
+ const nextRecoveryDimension = dimensions.find((dimension) => !rootRecoveryDimensionKeys.has(dimension.key));
8874
+ if (!nextRecoveryDimension) {
8875
+ break;
8876
+ }
8877
+ rootRecoveryDimensionKeys.add(nextRecoveryDimension.key);
8878
+ const recoveryChildren = expandSalesNavigatorCrawlAttempt(root, nextRecoveryDimension, "people");
8879
+ if (nextRecoveryDimension.supportsResidualExclusion !== false) {
8880
+ recoveryChildren.push(buildSalesNavigatorResidualCrawlAttempt(root, nextRecoveryDimension, "people"));
8881
+ }
8882
+ for (const child of recoveryChildren) {
8883
+ if (queuedUrls.has(child.slicedQueryUrl))
8884
+ continue;
8885
+ queuedUrls.add(child.slicedQueryUrl);
8886
+ queue.push(child);
8841
8887
  }
8842
8888
  slices.push({
8843
- depth: attempt.depth,
8844
- splitTrail: attempt.splitTrail,
8845
- totalResults: collected.totalResults,
8846
- collected: collected.people.length,
8847
- fetchedPages: collected.fetchedPages,
8848
- status: "collected",
8849
- splitDimension: null,
8889
+ depth: 0,
8890
+ splitTrail: [],
8891
+ totalResults: rootTotalResults,
8892
+ collected: 0,
8893
+ fetchedPages: 0,
8894
+ status: "split",
8895
+ splitDimension: nextRecoveryDimension.key,
8850
8896
  });
8851
8897
  }
8852
8898
  const people = [...peopleByKey.values()];
8853
- if (rootTotalResults != null && people.length < rootTotalResults) {
8899
+ const reportedCountDrift = rootTotalResults == null ? 0 : Math.max(0, rootTotalResults - people.length);
8900
+ const acceptedReportedCountDrift = rootTotalResults != null &&
8901
+ rootTotalResults <= options.maxResultsPerSearch &&
8902
+ reportedCountDrift <= LOCAL_SALES_NAVIGATOR_REPORTED_COUNT_DRIFT_LIMIT;
8903
+ if (reportedCountDrift > 0 && !acceptedReportedCountDrift) {
8854
8904
  throw new Error(`Adaptive collection found ${people.length} unique people but the root search reported ${rootTotalResults}. No import was written because coverage is incomplete.`);
8855
8905
  }
8856
8906
  return {
8857
8907
  people,
8858
8908
  totalResults: rootTotalResults,
8859
8909
  rootTotalResults,
8910
+ reportedCountDrift,
8860
8911
  fetchedPages,
8861
8912
  sourceQueryUrl,
8862
8913
  collectionMode: "adaptive",
@@ -13645,6 +13696,7 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
13645
13696
  ? {
13646
13697
  collectionMode: adaptiveCollection.collectionMode,
13647
13698
  accessibleResultLimit,
13699
+ reportedCountDrift: adaptiveCollection.reportedCountDrift,
13648
13700
  sliceCount: adaptiveCollection.slices.filter((slice) => slice.status === "collected").length,
13649
13701
  slices: adaptiveCollection.slices,
13650
13702
  }
@@ -13658,14 +13710,19 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
13658
13710
  dryRun: false,
13659
13711
  collected: collected.people.length,
13660
13712
  totalResults: collected.totalResults,
13661
- collectionComplete: collected.totalResults != null &&
13662
- collected.people.length >= collected.totalResults,
13663
- truncated: collected.totalResults != null &&
13713
+ collectionComplete: adaptiveCollection != null ||
13714
+ (collected.totalResults != null &&
13715
+ collected.people.length >= collected.totalResults),
13716
+ truncated: adaptiveCollection == null &&
13717
+ collected.totalResults != null &&
13664
13718
  collected.totalResults > collected.people.length &&
13665
13719
  collected.people.length >= maxResults,
13666
- remainingResults: collected.totalResults == null
13667
- ? null
13668
- : Math.max(0, collected.totalResults - collected.people.length),
13720
+ remainingResults: adaptiveCollection != null
13721
+ ? 0
13722
+ : collected.totalResults == null
13723
+ ? null
13724
+ : Math.max(0, collected.totalResults - collected.people.length),
13725
+ reportedCountDrift: adaptiveCollection?.reportedCountDrift ?? 0,
13669
13726
  fetchedPages: collected.fetchedPages,
13670
13727
  accessibleResultLimit,
13671
13728
  collectionMode: adaptiveCollection ? "adaptive" : "bounded",
@@ -15563,10 +15620,10 @@ program
15563
15620
  if (normalizedCheckpointUrl !== normalizedSourceQueryUrl) {
15564
15621
  throw new Error("The checkpoint belongs to a different Sales Navigator account search.");
15565
15622
  }
15566
- if (checkpoint.pageSize !== pageSize || checkpoint.cap > cap) {
15567
- throw new Error(`Checkpoint paging does not match this invocation. Use --max-results-per-search ${checkpoint.cap} --page-size ${checkpoint.pageSize}.`);
15623
+ if (checkpoint.pageSize !== pageSize) {
15624
+ throw new Error(`Checkpoint page size does not match this invocation. Use --page-size ${checkpoint.pageSize}.`);
15568
15625
  }
15569
- if (checkpoint.cap < cap) {
15626
+ if (checkpoint.cap !== cap) {
15570
15627
  checkpoint.cap = cap;
15571
15628
  }
15572
15629
  checkpoint.delayMinMs = delayMinMs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
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",