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.
@@ -8,6 +8,16 @@ export class SalesNavigatorSliceTooBroadError extends Error {
8
8
  this.details = options?.details;
9
9
  }
10
10
  }
11
+ export class SalesNavigatorCrawlStopError extends Error {
12
+ totalResults;
13
+ details;
14
+ constructor(message, options) {
15
+ super(message);
16
+ this.name = "SalesNavigatorCrawlStopError";
17
+ this.totalResults = options?.totalResults ?? null;
18
+ this.details = options?.details;
19
+ }
20
+ }
11
21
  export const DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS = [
12
22
  {
13
23
  type: "FUNCTION",
@@ -81,6 +91,7 @@ export const DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS = [
81
91
  },
82
92
  ];
83
93
  const LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL = "https://www.linkedin.com/sales/search/people";
94
+ const LINKEDIN_SALES_NAVIGATOR_ACCOUNT_SEARCH_URL = "https://www.linkedin.com/sales/search/company";
84
95
  export const DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS = [
85
96
  {
86
97
  type: "FUNCTION",
@@ -215,6 +226,46 @@ export const DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS = [
215
226
  ],
216
227
  },
217
228
  ];
229
+ export const DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS = [
230
+ {
231
+ key: "company-headcount",
232
+ filterType: "COMPANY_HEADCOUNT",
233
+ values: [
234
+ { id: "B", text: "1-10", selectionType: "INCLUDED" },
235
+ { id: "C", text: "11-50", selectionType: "INCLUDED" },
236
+ { id: "D", text: "51-200", selectionType: "INCLUDED" },
237
+ { id: "E", text: "201-500", selectionType: "INCLUDED" },
238
+ { id: "F", text: "501-1000", selectionType: "INCLUDED" },
239
+ { id: "G", text: "1001-5000", selectionType: "INCLUDED" },
240
+ { id: "H", text: "5001-10,000", selectionType: "INCLUDED" },
241
+ { id: "I", text: "10,000+", selectionType: "INCLUDED" },
242
+ ],
243
+ },
244
+ {
245
+ key: "number-of-followers",
246
+ filterType: "NUM_OF_FOLLOWERS",
247
+ values: [
248
+ { id: "NFR1", text: "1-10", selectionType: "INCLUDED" },
249
+ { id: "NFR2", text: "11-100", selectionType: "INCLUDED" },
250
+ { id: "NFR3", text: "101-1000", selectionType: "INCLUDED" },
251
+ { id: "NFR4", text: "1001-5000", selectionType: "INCLUDED" },
252
+ { id: "NFR5", text: "5001+", selectionType: "INCLUDED" },
253
+ ],
254
+ },
255
+ {
256
+ key: "company-type",
257
+ filterType: "COMPANY_TYPE",
258
+ values: [
259
+ { id: "P", text: "Privately Held", selectionType: "INCLUDED" },
260
+ { id: "C", text: "Public Company", selectionType: "INCLUDED" },
261
+ { text: "Partnership", selectionType: "INCLUDED" },
262
+ { text: "Self Owned", selectionType: "INCLUDED" },
263
+ { text: "Non Profit", selectionType: "INCLUDED" },
264
+ { text: "Educational Institution", selectionType: "INCLUDED" },
265
+ { text: "Government Agency", selectionType: "INCLUDED" },
266
+ ],
267
+ },
268
+ ];
218
269
  function ensureLinkedInSalesPeopleSearchUrl(url) {
219
270
  if (!/(^|\.)linkedin\.com$/i.test(url.hostname)) {
220
271
  throw new Error("Expected a linkedin.com Sales Navigator URL.");
@@ -226,6 +277,24 @@ function ensureLinkedInSalesPeopleSearchUrl(url) {
226
277
  throw new Error("Sales Navigator URL is missing the query parameter.");
227
278
  }
228
279
  }
280
+ function ensureLinkedInSalesAccountSearchUrl(url) {
281
+ if (!/(^|\.)linkedin\.com$/i.test(url.hostname)) {
282
+ throw new Error("Expected a linkedin.com Sales Navigator URL.");
283
+ }
284
+ if (!/^\/sales\/search\/company/i.test(url.pathname)) {
285
+ throw new Error("Expected a LinkedIn Sales Navigator account search URL.");
286
+ }
287
+ if (!url.searchParams.has("query")) {
288
+ throw new Error("Sales Navigator URL is missing the query parameter.");
289
+ }
290
+ }
291
+ function ensureLinkedInSalesSearchUrl(url, searchType) {
292
+ if (searchType === "account") {
293
+ ensureLinkedInSalesAccountSearchUrl(url);
294
+ return;
295
+ }
296
+ ensureLinkedInSalesPeopleSearchUrl(url);
297
+ }
229
298
  function splitTopLevelCommaSeparated(value) {
230
299
  const parts = [];
231
300
  let depth = 0;
@@ -357,19 +426,36 @@ function canonicalizeSalesNavigatorQueryValue(queryValue) {
357
426
  const { filtersContent } = findFiltersListBounds(queryValue);
358
427
  return `(filters:List(${filtersContent}))`;
359
428
  }
360
- function canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl) {
429
+ function canonicalizeSalesNavigatorSearchUrl(sourceQueryUrl, searchType = "people") {
361
430
  const url = new URL(sourceQueryUrl);
362
- ensureLinkedInSalesPeopleSearchUrl(url);
431
+ ensureLinkedInSalesSearchUrl(url, searchType);
363
432
  const queryValue = url.searchParams.get("query");
364
433
  if (!queryValue) {
365
434
  throw new Error("Sales Navigator URL is missing the query parameter.");
366
435
  }
367
- const canonicalUrl = new URL(LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL);
436
+ const canonicalUrl = new URL(searchType === "account"
437
+ ? LINKEDIN_SALES_NAVIGATOR_ACCOUNT_SEARCH_URL
438
+ : LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL);
368
439
  canonicalUrl.searchParams.set("query", canonicalizeSalesNavigatorQueryValue(queryValue));
369
440
  return canonicalUrl;
370
441
  }
371
442
  export function buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters) {
372
- const url = canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl);
443
+ const url = canonicalizeSalesNavigatorSearchUrl(sourceQueryUrl, "people");
444
+ const queryValue = url.searchParams.get("query");
445
+ if (!queryValue) {
446
+ throw new Error("Sales Navigator URL is missing the query parameter.");
447
+ }
448
+ const appliedFilters = dedupeSalesNavigatorFilters(filters);
449
+ const slicedQueryValue = applySalesNavigatorFiltersToQueryValue(queryValue, appliedFilters);
450
+ url.searchParams.set("query", slicedQueryValue);
451
+ return {
452
+ sourceQueryUrl: canonicalizeSalesNavigatorSearchUrl(sourceQueryUrl, "people").toString(),
453
+ slicedQueryUrl: url.toString(),
454
+ appliedFilters,
455
+ };
456
+ }
457
+ export function buildSalesNavigatorAccountQuery(sourceQueryUrl, filters) {
458
+ const url = canonicalizeSalesNavigatorSearchUrl(sourceQueryUrl, "account");
373
459
  const queryValue = url.searchParams.get("query");
374
460
  if (!queryValue) {
375
461
  throw new Error("Sales Navigator URL is missing the query parameter.");
@@ -378,7 +464,7 @@ export function buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters) {
378
464
  const slicedQueryValue = applySalesNavigatorFiltersToQueryValue(queryValue, appliedFilters);
379
465
  url.searchParams.set("query", slicedQueryValue);
380
466
  return {
381
- sourceQueryUrl: canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl).toString(),
467
+ sourceQueryUrl: canonicalizeSalesNavigatorSearchUrl(sourceQueryUrl, "account").toString(),
382
468
  slicedQueryUrl: url.toString(),
383
469
  appliedFilters,
384
470
  };
@@ -392,6 +478,15 @@ export function buildSalesNavigatorPeopleSearchUrl(filters) {
392
478
  url.searchParams.set("query", `(filters:List(${appliedFilters.map((filter) => buildFilterBlock(filter)).join(",")}))`);
393
479
  return url.toString();
394
480
  }
481
+ export function buildSalesNavigatorAccountSearchUrl(filters) {
482
+ const appliedFilters = dedupeSalesNavigatorFilters(filters);
483
+ if (appliedFilters.length === 0) {
484
+ throw new Error("Sales Navigator account search requires at least one filter.");
485
+ }
486
+ const url = new URL(LINKEDIN_SALES_NAVIGATOR_ACCOUNT_SEARCH_URL);
487
+ url.searchParams.set("query", `(filters:List(${appliedFilters.map((filter) => buildFilterBlock(filter)).join(",")}))`);
488
+ return url.toString();
489
+ }
395
490
  function inferSalesNavigatorFunctionFilters(options) {
396
491
  const titleHaystack = options.title.trim();
397
492
  const categoryHaystack = [options.categoryName, options.categorySlug]
@@ -507,8 +602,13 @@ export function deriveSalesNavigatorTitleQuerySeeds(options) {
507
602
  export function buildSalesNavigatorPeopleSlice(sourceQueryUrl, filters = DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS) {
508
603
  return buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters);
509
604
  }
605
+ export function buildSalesNavigatorAccountSlice(sourceQueryUrl, filters = []) {
606
+ return buildSalesNavigatorAccountQuery(sourceQueryUrl, filters);
607
+ }
510
608
  export function createSalesNavigatorCrawlSeed(options) {
511
- const sliced = buildSalesNavigatorPeopleQuery(options.sourceQueryUrl, options.baselineFilters ?? []);
609
+ const sliced = options.searchType === "account"
610
+ ? buildSalesNavigatorAccountQuery(options.sourceQueryUrl, options.baselineFilters ?? [])
611
+ : buildSalesNavigatorPeopleQuery(options.sourceQueryUrl, options.baselineFilters ?? []);
512
612
  return {
513
613
  ...sliced,
514
614
  depth: 0,
@@ -519,7 +619,7 @@ export function createSalesNavigatorCrawlSeed(options) {
519
619
  splitTrail: [],
520
620
  };
521
621
  }
522
- export function expandSalesNavigatorCrawlAttempt(attempt, dimension) {
622
+ export function expandSalesNavigatorCrawlAttempt(attempt, dimension, searchType = "people") {
523
623
  return dimension.values.map((value) => {
524
624
  const filters = dedupeSalesNavigatorFilters([
525
625
  ...attempt.appliedFilters.filter((filter) => filter.type !== dimension.filterType),
@@ -533,7 +633,9 @@ export function expandSalesNavigatorCrawlAttempt(attempt, dimension) {
533
633
  ],
534
634
  },
535
635
  ]);
536
- const sliced = buildSalesNavigatorPeopleQuery(attempt.sourceQueryUrl, filters);
636
+ const sliced = searchType === "account"
637
+ ? buildSalesNavigatorAccountQuery(attempt.sourceQueryUrl, filters)
638
+ : buildSalesNavigatorPeopleQuery(attempt.sourceQueryUrl, filters);
537
639
  return {
538
640
  ...sliced,
539
641
  depth: attempt.depth + 1,
@@ -555,12 +657,364 @@ export function expandSalesNavigatorCrawlAttempt(attempt, dimension) {
555
657
  };
556
658
  });
557
659
  }
660
+ export function buildSalesNavigatorResidualCrawlAttempt(attempt, dimension, searchType = "people") {
661
+ const residualValues = dimension.values.map((value) => ({
662
+ ...value,
663
+ selectionType: "EXCLUDED",
664
+ }));
665
+ const filters = dedupeSalesNavigatorFilters([
666
+ ...attempt.appliedFilters.filter((filter) => filter.type !== dimension.filterType),
667
+ {
668
+ type: dimension.filterType,
669
+ values: residualValues,
670
+ },
671
+ ]);
672
+ const sliced = searchType === "account"
673
+ ? buildSalesNavigatorAccountQuery(attempt.sourceQueryUrl, filters)
674
+ : buildSalesNavigatorPeopleQuery(attempt.sourceQueryUrl, filters);
675
+ return {
676
+ ...sliced,
677
+ depth: attempt.depth + 1,
678
+ retryCount: 0,
679
+ maxResultsPerSearch: attempt.maxResultsPerSearch,
680
+ numberOfProfiles: attempt.numberOfProfiles,
681
+ slicePreset: attempt.slicePreset,
682
+ splitTrail: [
683
+ ...attempt.splitTrail,
684
+ {
685
+ key: `${dimension.key}-residual`,
686
+ filterType: dimension.filterType,
687
+ value: {
688
+ text: `Not in ${dimension.key}`,
689
+ selectionType: "EXCLUDED",
690
+ },
691
+ },
692
+ ],
693
+ };
694
+ }
558
695
  export function buildSalesNavigatorCrawlPreview(options) {
559
696
  const root = createSalesNavigatorCrawlSeed(options);
560
697
  const dimensions = options.dimensions ?? DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS;
561
- const firstSplit = dimensions[0] ? expandSalesNavigatorCrawlAttempt(root, dimensions[0]) : [];
698
+ const firstSplit = dimensions[0]
699
+ ? expandSalesNavigatorCrawlAttempt(root, dimensions[0], options.searchType)
700
+ : [];
562
701
  return { root, firstSplit, dimensions };
563
702
  }
703
+ function salesNavigatorDimensionKeyFromTrailKey(key) {
704
+ return key.endsWith("-residual") ? key.slice(0, -"-residual".length) : key;
705
+ }
706
+ function getUnusedSalesNavigatorDimensions(attempt, dimensions) {
707
+ const usedKeys = new Set(attempt.splitTrail.map((entry) => salesNavigatorDimensionKeyFromTrailKey(entry.key)));
708
+ return dimensions.filter((dimension) => !usedKeys.has(dimension.key));
709
+ }
710
+ function buildSalesNavigatorAdaptiveChildren(attempt, dimension, options) {
711
+ const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, dimension, options.searchType);
712
+ const residualChildren = options.includeResidualExclusionSlices
713
+ ? [
714
+ buildSalesNavigatorResidualCrawlAttempt(attempt, dimension, options.searchType),
715
+ ]
716
+ : [];
717
+ return [...expandedChildren, ...residualChildren];
718
+ }
719
+ function getSalesNavigatorAttemptDimensionValues(attempt, dimension) {
720
+ const trailEntry = attempt.splitTrail.at(-1);
721
+ if (trailEntry?.key !== dimension.key) {
722
+ return null;
723
+ }
724
+ const filter = attempt.appliedFilters.find((entry) => entry.type === dimension.filterType);
725
+ if (!filter || filter.values.length !== 1) {
726
+ return null;
727
+ }
728
+ if (filter.values.some((value) => (value.selectionType ?? "INCLUDED") !== "INCLUDED")) {
729
+ return null;
730
+ }
731
+ return filter.values;
732
+ }
733
+ function buildSalesNavigatorCombinedCrawlAttempt(attempt, dimension, children, searchType = "people") {
734
+ const values = children.flatMap((child) => getSalesNavigatorAttemptDimensionValues(child.attempt, dimension) ?? []);
735
+ const filters = dedupeSalesNavigatorFilters([
736
+ ...attempt.appliedFilters.filter((filter) => filter.type !== dimension.filterType),
737
+ {
738
+ type: dimension.filterType,
739
+ values: values.map((value) => ({
740
+ ...value,
741
+ selectionType: value.selectionType ?? "INCLUDED",
742
+ })),
743
+ },
744
+ ]);
745
+ const sliced = searchType === "account"
746
+ ? buildSalesNavigatorAccountQuery(attempt.sourceQueryUrl, filters)
747
+ : buildSalesNavigatorPeopleQuery(attempt.sourceQueryUrl, filters);
748
+ const valueText = values.map((value) => value.text).join(" + ");
749
+ return {
750
+ ...sliced,
751
+ depth: attempt.depth + 1,
752
+ retryCount: 0,
753
+ maxResultsPerSearch: attempt.maxResultsPerSearch,
754
+ numberOfProfiles: attempt.numberOfProfiles,
755
+ slicePreset: attempt.slicePreset,
756
+ splitTrail: [
757
+ ...attempt.splitTrail,
758
+ {
759
+ key: dimension.key,
760
+ filterType: dimension.filterType,
761
+ value: {
762
+ text: valueText,
763
+ selectionType: "INCLUDED",
764
+ },
765
+ },
766
+ ],
767
+ };
768
+ }
769
+ async function coalesceSalesNavigatorUnderCapChildren(options) {
770
+ const packable = [];
771
+ const fixed = [];
772
+ for (const summary of options.summaries) {
773
+ const values = getSalesNavigatorAttemptDimensionValues(summary.attempt, options.dimension);
774
+ if (!values ||
775
+ summary.totalResults == null ||
776
+ summary.totalResults > summary.attempt.maxResultsPerSearch) {
777
+ fixed.push(summary);
778
+ continue;
779
+ }
780
+ packable.push(summary);
781
+ }
782
+ if (packable.length <= 1) {
783
+ return options.summaries;
784
+ }
785
+ const bins = [];
786
+ const ordered = [...packable].sort((left, right) => (right.totalResults ?? 0) - (left.totalResults ?? 0));
787
+ for (const item of ordered) {
788
+ let placed = false;
789
+ for (const bin of bins) {
790
+ const combinedAttempt = buildSalesNavigatorCombinedCrawlAttempt(options.parentAttempt, options.dimension, [...bin.children, item], options.searchType);
791
+ const combinedSummary = await probeSalesNavigatorAdaptiveAttempt(combinedAttempt, options.probeSlice, options.probeCache);
792
+ if (combinedSummary.totalResults != null &&
793
+ combinedSummary.totalResults <= combinedAttempt.maxResultsPerSearch) {
794
+ bin.children.push(item);
795
+ bin.summary = {
796
+ attempt: combinedAttempt,
797
+ totalResults: combinedSummary.totalResults,
798
+ };
799
+ placed = true;
800
+ break;
801
+ }
802
+ }
803
+ if (!placed) {
804
+ bins.push({
805
+ children: [item],
806
+ summary: item,
807
+ });
808
+ }
809
+ }
810
+ return [
811
+ ...bins.map((bin) => bin.summary),
812
+ ...fixed,
813
+ ];
814
+ }
815
+ function summarizeSalesNavigatorAdaptiveSplitCandidate(summaries, maxResultsPerSearch, estimate) {
816
+ const knownTotals = summaries
817
+ .map((summary) => summary.totalResults)
818
+ .filter((total) => typeof total === "number");
819
+ const knownChildren = knownTotals.length;
820
+ const oversizedChildren = knownTotals.filter((total) => total > maxResultsPerSearch).length;
821
+ const largestChildTotal = knownTotals.length > 0 ? Math.max(...knownTotals) : null;
822
+ const underCapTotals = knownTotals.filter((total) => total <= maxResultsPerSearch);
823
+ const utilization = underCapTotals.length === 0
824
+ ? 0
825
+ : underCapTotals.reduce((sum, total) => sum + total / maxResultsPerSearch, 0) / underCapTotals.length;
826
+ const largestPenalty = largestChildTotal == null
827
+ ? maxResultsPerSearch * 2
828
+ : Math.max(0, largestChildTotal - maxResultsPerSearch) /
829
+ maxResultsPerSearch;
830
+ return {
831
+ knownChildren,
832
+ oversizedChildren,
833
+ largestChildTotal,
834
+ estimatedFinalBatches: estimate.estimatedFinalBatches,
835
+ unresolvedChildren: estimate.unresolvedChildren,
836
+ score: estimate.unresolvedChildren * 1_000_000 +
837
+ estimate.estimatedFinalBatches * 1_000 +
838
+ oversizedChildren * 100 +
839
+ largestPenalty * 10 -
840
+ utilization,
841
+ };
842
+ }
843
+ async function estimateSalesNavigatorMinimumFinalBatches(options) {
844
+ const remainingDimensions = getUnusedSalesNavigatorDimensions(options.attempt, options.dimensions);
845
+ if (remainingDimensions.length === 0 ||
846
+ options.attempt.depth >= options.maxSplitDepth) {
847
+ return {
848
+ estimatedFinalBatches: 1,
849
+ unresolvedChildren: 1,
850
+ largestChildTotal: null,
851
+ };
852
+ }
853
+ const candidateEstimates = [];
854
+ for (const dimension of remainingDimensions) {
855
+ const rawChildren = buildSalesNavigatorAdaptiveChildren(options.attempt, dimension, options);
856
+ const rawSummaries = [];
857
+ for (const child of rawChildren) {
858
+ rawSummaries.push({
859
+ attempt: child,
860
+ ...(await probeSalesNavigatorAdaptiveAttempt(child, options.probeSlice, options.probeCache)),
861
+ });
862
+ }
863
+ const summaries = await coalesceSalesNavigatorUnderCapChildren({
864
+ parentAttempt: options.attempt,
865
+ dimension,
866
+ summaries: rawSummaries,
867
+ searchType: options.searchType,
868
+ probeSlice: options.probeSlice,
869
+ probeCache: options.probeCache,
870
+ });
871
+ let estimatedFinalBatches = 0;
872
+ let unresolvedChildren = 0;
873
+ let largestChildTotal = null;
874
+ for (const summary of summaries) {
875
+ if (summary.totalResults != null) {
876
+ largestChildTotal =
877
+ largestChildTotal == null
878
+ ? summary.totalResults
879
+ : Math.max(largestChildTotal, summary.totalResults);
880
+ }
881
+ if (summary.totalResults != null &&
882
+ summary.totalResults <= summary.attempt.maxResultsPerSearch) {
883
+ estimatedFinalBatches += 1;
884
+ continue;
885
+ }
886
+ const childEstimate = await estimateSalesNavigatorMinimumFinalBatches({
887
+ ...options,
888
+ attempt: summary.attempt,
889
+ });
890
+ estimatedFinalBatches += childEstimate.estimatedFinalBatches;
891
+ unresolvedChildren += childEstimate.unresolvedChildren;
892
+ if (childEstimate.largestChildTotal != null) {
893
+ largestChildTotal =
894
+ largestChildTotal == null
895
+ ? childEstimate.largestChildTotal
896
+ : Math.max(largestChildTotal, childEstimate.largestChildTotal);
897
+ }
898
+ }
899
+ candidateEstimates.push({
900
+ estimatedFinalBatches,
901
+ unresolvedChildren,
902
+ largestChildTotal,
903
+ });
904
+ }
905
+ candidateEstimates.sort((left, right) => {
906
+ if (left.unresolvedChildren !== right.unresolvedChildren) {
907
+ return left.unresolvedChildren - right.unresolvedChildren;
908
+ }
909
+ if (left.estimatedFinalBatches !== right.estimatedFinalBatches) {
910
+ return left.estimatedFinalBatches - right.estimatedFinalBatches;
911
+ }
912
+ return dimensionsSortValue(right.largestChildTotal) -
913
+ dimensionsSortValue(left.largestChildTotal);
914
+ });
915
+ return candidateEstimates[0] ?? {
916
+ estimatedFinalBatches: 1,
917
+ unresolvedChildren: 1,
918
+ largestChildTotal: null,
919
+ };
920
+ }
921
+ async function probeSalesNavigatorAdaptiveAttempt(attempt, probeSlice, probeCache) {
922
+ let cached = probeCache.get(attempt.slicedQueryUrl);
923
+ if (!cached) {
924
+ cached = probeSlice(attempt);
925
+ probeCache.set(attempt.slicedQueryUrl, cached);
926
+ }
927
+ return await cached;
928
+ }
929
+ async function chooseSalesNavigatorAdaptiveSplitCandidate(options) {
930
+ const candidates = [];
931
+ const probeCache = new Map();
932
+ for (const dimension of options.dimensions) {
933
+ const children = buildSalesNavigatorAdaptiveChildren(options.attempt, dimension, options).filter((child) => !options.seenQueryUrls.has(child.slicedQueryUrl));
934
+ if (children.length === 0) {
935
+ continue;
936
+ }
937
+ const summaries = [];
938
+ for (const child of children) {
939
+ summaries.push({
940
+ attempt: child,
941
+ ...(await probeSalesNavigatorAdaptiveAttempt(child, options.probeSlice, probeCache)),
942
+ });
943
+ }
944
+ const coalescedSummaries = await coalesceSalesNavigatorUnderCapChildren({
945
+ parentAttempt: options.attempt,
946
+ dimension,
947
+ summaries,
948
+ searchType: options.searchType,
949
+ probeSlice: options.probeSlice,
950
+ probeCache,
951
+ });
952
+ const coalescedChildren = coalescedSummaries.map((summary) => summary.attempt);
953
+ let estimatedFinalBatches = 0;
954
+ let unresolvedChildren = 0;
955
+ let largestChildTotal = null;
956
+ for (const summary of coalescedSummaries) {
957
+ if (summary.totalResults != null) {
958
+ largestChildTotal =
959
+ largestChildTotal == null
960
+ ? summary.totalResults
961
+ : Math.max(largestChildTotal, summary.totalResults);
962
+ }
963
+ if (summary.totalResults != null &&
964
+ summary.totalResults <= summary.attempt.maxResultsPerSearch) {
965
+ estimatedFinalBatches += 1;
966
+ continue;
967
+ }
968
+ const childEstimate = await estimateSalesNavigatorMinimumFinalBatches({
969
+ attempt: summary.attempt,
970
+ dimensions: options.dimensions,
971
+ maxSplitDepth: options.maxSplitDepth,
972
+ searchType: options.searchType,
973
+ includeResidualExclusionSlices: options.includeResidualExclusionSlices,
974
+ probeSlice: options.probeSlice,
975
+ probeCache,
976
+ });
977
+ estimatedFinalBatches += childEstimate.estimatedFinalBatches;
978
+ unresolvedChildren += childEstimate.unresolvedChildren;
979
+ if (childEstimate.largestChildTotal != null) {
980
+ largestChildTotal =
981
+ largestChildTotal == null
982
+ ? childEstimate.largestChildTotal
983
+ : Math.max(largestChildTotal, childEstimate.largestChildTotal);
984
+ }
985
+ }
986
+ const score = summarizeSalesNavigatorAdaptiveSplitCandidate(coalescedSummaries, options.attempt.maxResultsPerSearch, {
987
+ estimatedFinalBatches,
988
+ unresolvedChildren,
989
+ });
990
+ const largestTotal = largestChildTotal == null
991
+ ? score.largestChildTotal
992
+ : score.largestChildTotal == null
993
+ ? largestChildTotal
994
+ : Math.max(largestChildTotal, score.largestChildTotal);
995
+ candidates.push({
996
+ dimension,
997
+ children: coalescedChildren,
998
+ summaries: coalescedSummaries,
999
+ ...score,
1000
+ largestChildTotal: largestTotal,
1001
+ });
1002
+ }
1003
+ candidates.sort((left, right) => {
1004
+ if (left.score !== right.score) {
1005
+ return left.score - right.score;
1006
+ }
1007
+ if (left.oversizedChildren !== right.oversizedChildren) {
1008
+ return left.oversizedChildren - right.oversizedChildren;
1009
+ }
1010
+ return dimensionsSortValue(right.largestChildTotal) -
1011
+ dimensionsSortValue(left.largestChildTotal);
1012
+ });
1013
+ return candidates[0] ?? null;
1014
+ }
1015
+ function dimensionsSortValue(value) {
1016
+ return value ?? -1;
1017
+ }
564
1018
  export async function executeSalesNavigatorAdaptiveCrawl(options) {
565
1019
  const dimensions = options.dimensions ?? DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS;
566
1020
  const maxSplitDepth = Math.min(options.maxSplitDepth ?? dimensions.length, dimensions.length);
@@ -577,6 +1031,7 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
577
1031
  maxResultsPerSearch: options.maxResultsPerSearch,
578
1032
  numberOfProfiles: options.numberOfProfiles,
579
1033
  slicePreset: options.slicePreset,
1034
+ searchType: options.searchType,
580
1035
  }),
581
1036
  ];
582
1037
  const seenQueryUrls = new Set(queue.map((attempt) => attempt.slicedQueryUrl));
@@ -597,9 +1052,18 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
597
1052
  exported.push({ attempt, result });
598
1053
  }
599
1054
  catch (error) {
1055
+ if (error instanceof SalesNavigatorCrawlStopError) {
1056
+ failed.push({
1057
+ attempt,
1058
+ error: error.message,
1059
+ totalResults: error.totalResults,
1060
+ });
1061
+ truncated = true;
1062
+ break;
1063
+ }
600
1064
  if (error instanceof SalesNavigatorSliceTooBroadError) {
601
- const nextDimension = dimensions[attempt.depth];
602
- if (!nextDimension || attempt.depth >= maxSplitDepth) {
1065
+ const remainingDimensions = getUnusedSalesNavigatorDimensions(attempt, dimensions);
1066
+ if (remainingDimensions.length === 0 || attempt.depth >= maxSplitDepth) {
603
1067
  unresolved.push({
604
1068
  attempt,
605
1069
  error: error.message,
@@ -607,7 +1071,48 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
607
1071
  });
608
1072
  continue;
609
1073
  }
610
- const children = expandSalesNavigatorCrawlAttempt(attempt, nextDimension).filter((child) => {
1074
+ let nextDimension = remainingDimensions[0];
1075
+ let children = [];
1076
+ let probeSummary;
1077
+ let strategy = "fixed-order";
1078
+ if (options.adaptiveSplitProbing && options.probeSlice) {
1079
+ const candidate = await chooseSalesNavigatorAdaptiveSplitCandidate({
1080
+ attempt,
1081
+ dimensions: remainingDimensions,
1082
+ searchType: options.searchType,
1083
+ includeResidualExclusionSlices: options.includeResidualExclusionSlices,
1084
+ seenQueryUrls,
1085
+ maxSplitDepth,
1086
+ probeSlice: options.probeSlice,
1087
+ });
1088
+ if (candidate) {
1089
+ nextDimension = candidate.dimension;
1090
+ children = candidate.children;
1091
+ strategy = "adaptive-probe";
1092
+ probeSummary = [
1093
+ {
1094
+ dimension: candidate.dimension.key,
1095
+ childCount: candidate.children.length,
1096
+ knownChildren: candidate.knownChildren,
1097
+ oversizedChildren: candidate.oversizedChildren,
1098
+ largestChildTotal: candidate.largestChildTotal,
1099
+ estimatedFinalBatches: candidate.estimatedFinalBatches,
1100
+ unresolvedChildren: candidate.unresolvedChildren,
1101
+ score: candidate.score,
1102
+ },
1103
+ ];
1104
+ }
1105
+ }
1106
+ if (children.length === 0) {
1107
+ const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, nextDimension, options.searchType);
1108
+ const residualChildren = options.includeResidualExclusionSlices
1109
+ ? [
1110
+ buildSalesNavigatorResidualCrawlAttempt(attempt, nextDimension, options.searchType),
1111
+ ]
1112
+ : [];
1113
+ children = [...expandedChildren, ...residualChildren];
1114
+ }
1115
+ children = children.filter((child) => {
611
1116
  if (seenQueryUrls.has(child.slicedQueryUrl)) {
612
1117
  return false;
613
1118
  }
@@ -627,6 +1132,8 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
627
1132
  totalResults: error.totalResults,
628
1133
  nextDimension: nextDimension.key,
629
1134
  childCount: children.length,
1135
+ strategy,
1136
+ probeSummary,
630
1137
  });
631
1138
  queue.push(...children);
632
1139
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
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",
@@ -66,8 +66,10 @@
66
66
  "dependencies": {
67
67
  "@google-cloud/bigquery": "^8.1.1",
68
68
  "@supabase/supabase-js": "^2.99.1",
69
+ "@types/pg": "^8.20.0",
69
70
  "cheerio": "^1.2.0",
70
71
  "commander": "^14.0.1",
72
+ "pg": "^8.21.0",
71
73
  "zod": "^4.1.5"
72
74
  },
73
75
  "overrides": {