salesprompter-cli 0.1.18 → 0.1.20

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.
@@ -80,6 +80,7 @@ export const DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS = [
80
80
  ],
81
81
  },
82
82
  ];
83
+ const LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL = "https://www.linkedin.com/sales/search/people";
83
84
  export const DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS = [
84
85
  {
85
86
  type: "FUNCTION",
@@ -92,27 +93,74 @@ export const DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS = [
92
93
  ],
93
94
  },
94
95
  ];
96
+ const SALES_NAVIGATOR_FUNCTION_HINTS = [
97
+ {
98
+ name: "Human Resources",
99
+ filterValue: { id: "12", text: "Human Resources", selectionType: "INCLUDED" },
100
+ patterns: [/(^|[^a-z])(human resources|hr\b|people\b|recruit|talent acquisition|talent management)/i],
101
+ },
102
+ {
103
+ name: "Finance",
104
+ filterValue: { text: "Finance", selectionType: "INCLUDED" },
105
+ patterns: [/(^|[^a-z])(finance|accounting|accounts payable|accounts receivable|controller|payroll)/i],
106
+ },
107
+ {
108
+ name: "Information Technology",
109
+ filterValue: { text: "Information Technology", selectionType: "INCLUDED" },
110
+ patterns: [/(^|[^a-z])(information technology|it\b|devops|infrastructure|security|help desk|service desk)/i],
111
+ },
112
+ {
113
+ name: "Engineering",
114
+ filterValue: { text: "Engineering", selectionType: "INCLUDED" },
115
+ patterns: [/(^|[^a-z])(engineering|developer|software|platform engineer|technical lead|cto\b)/i],
116
+ },
117
+ {
118
+ name: "Sales",
119
+ filterValue: { text: "Sales", selectionType: "INCLUDED" },
120
+ patterns: [/(^|[^a-z])(sales|revenue|account executive|business development|sales enablement)/i],
121
+ },
122
+ {
123
+ name: "Marketing",
124
+ filterValue: { text: "Marketing", selectionType: "INCLUDED" },
125
+ patterns: [/(^|[^a-z])(marketing|demand generation|growth|brand|campaign)/i],
126
+ },
127
+ {
128
+ name: "Operations",
129
+ filterValue: { text: "Operations", selectionType: "INCLUDED" },
130
+ patterns: [/(^|[^a-z])(operations|ops\b|supply chain|procurement|vendor management)/i],
131
+ },
132
+ {
133
+ name: "Legal",
134
+ filterValue: { text: "Legal", selectionType: "INCLUDED" },
135
+ patterns: [/(^|[^a-z])(legal|compliance|general counsel|privacy)/i],
136
+ },
137
+ {
138
+ name: "Support",
139
+ filterValue: { text: "Support", selectionType: "INCLUDED" },
140
+ patterns: [/(^|[^a-z])(support|customer service|customer success|help center)/i],
141
+ },
142
+ ];
95
143
  export const DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS = [
96
144
  {
97
145
  key: "company-headcount",
98
146
  filterType: "COMPANY_HEADCOUNT",
99
147
  values: [
100
- { text: "1-10", selectionType: "INCLUDED" },
101
- { text: "11-50", selectionType: "INCLUDED" },
102
- { text: "51-200", selectionType: "INCLUDED" },
103
- { text: "201-500", selectionType: "INCLUDED" },
104
- { text: "501-1000", selectionType: "INCLUDED" },
105
- { text: "1001-5000", selectionType: "INCLUDED" },
106
- { text: "5001-10,000", selectionType: "INCLUDED" },
107
- { text: "10,001+", selectionType: "INCLUDED" },
148
+ { id: "D", text: "51-200", selectionType: "INCLUDED" },
149
+ { id: "C", text: "11-50", selectionType: "INCLUDED" },
150
+ { id: "E", text: "201-500", selectionType: "INCLUDED" },
151
+ { id: "B", text: "1-10", selectionType: "INCLUDED" },
152
+ { id: "F", text: "501-1000", selectionType: "INCLUDED" },
153
+ { id: "G", text: "1001-5000", selectionType: "INCLUDED" },
154
+ { id: "H", text: "5001-10,000", selectionType: "INCLUDED" },
155
+ { id: "I", text: "10,000+", selectionType: "INCLUDED" },
108
156
  ],
109
157
  },
110
158
  {
111
159
  key: "company-type",
112
160
  filterType: "COMPANY_TYPE",
113
161
  values: [
114
- { text: "Privately Held", selectionType: "INCLUDED" },
115
- { text: "Public Company", selectionType: "INCLUDED" },
162
+ { id: "P", text: "Privately Held", selectionType: "INCLUDED" },
163
+ { id: "C", text: "Public Company", selectionType: "INCLUDED" },
116
164
  { text: "Partnership", selectionType: "INCLUDED" },
117
165
  { text: "Self Owned", selectionType: "INCLUDED" },
118
166
  { text: "Non Profit", selectionType: "INCLUDED" },
@@ -124,47 +172,46 @@ export const DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS = [
124
172
  key: "seniority-level",
125
173
  filterType: "SENIORITY_LEVEL",
126
174
  values: [
127
- { text: "Owner", selectionType: "INCLUDED" },
128
- { text: "Partner", selectionType: "INCLUDED" },
129
- { text: "CXO", selectionType: "INCLUDED" },
130
- { text: "VP", selectionType: "INCLUDED" },
131
- { text: "Director", selectionType: "INCLUDED" },
132
- { text: "Manager", selectionType: "INCLUDED" },
133
- { text: "Senior", selectionType: "INCLUDED" },
134
- { text: "Entry", selectionType: "INCLUDED" },
175
+ { id: "320", text: "Owner / Partner", selectionType: "INCLUDED" },
176
+ { id: "310", text: "CXO", selectionType: "INCLUDED" },
177
+ { id: "300", text: "Vice President", selectionType: "INCLUDED" },
178
+ { id: "220", text: "Director", selectionType: "INCLUDED" },
179
+ { id: "210", text: "Experienced Manager", selectionType: "INCLUDED" },
180
+ { id: "120", text: "Senior", selectionType: "INCLUDED" },
181
+ { id: "110", text: "Entry Level", selectionType: "INCLUDED" },
135
182
  ],
136
183
  },
137
184
  {
138
185
  key: "years-at-company",
139
186
  filterType: "YEARS_AT_CURRENT_COMPANY",
140
187
  values: [
141
- { text: "Less than 1 year", selectionType: "INCLUDED" },
142
- { text: "1 to 2 years", selectionType: "INCLUDED" },
143
- { text: "3 to 5 years", selectionType: "INCLUDED" },
144
- { text: "6 to 10 years", selectionType: "INCLUDED" },
145
- { text: "More than 10 years", selectionType: "INCLUDED" },
188
+ { id: "1", text: "Less than 1 year", selectionType: "INCLUDED" },
189
+ { id: "2", text: "1 to 2 years", selectionType: "INCLUDED" },
190
+ { id: "3", text: "3 to 5 years", selectionType: "INCLUDED" },
191
+ { id: "4", text: "6 to 10 years", selectionType: "INCLUDED" },
192
+ { id: "5", text: "More than 10 years", selectionType: "INCLUDED" },
146
193
  ],
147
194
  },
148
195
  {
149
196
  key: "years-in-position",
150
197
  filterType: "YEARS_IN_CURRENT_POSITION",
151
198
  values: [
152
- { text: "Less than 1 year", selectionType: "INCLUDED" },
153
- { text: "1 to 2 years", selectionType: "INCLUDED" },
154
- { text: "3 to 5 years", selectionType: "INCLUDED" },
155
- { text: "6 to 10 years", selectionType: "INCLUDED" },
156
- { text: "More than 10 years", selectionType: "INCLUDED" },
199
+ { id: "1", text: "Less than 1 year", selectionType: "INCLUDED" },
200
+ { id: "2", text: "1 to 2 years", selectionType: "INCLUDED" },
201
+ { id: "3", text: "3 to 5 years", selectionType: "INCLUDED" },
202
+ { id: "4", text: "6 to 10 years", selectionType: "INCLUDED" },
203
+ { id: "5", text: "More than 10 years", selectionType: "INCLUDED" },
157
204
  ],
158
205
  },
159
206
  {
160
207
  key: "years-of-experience",
161
208
  filterType: "YEARS_OF_EXPERIENCE",
162
209
  values: [
163
- { text: "Less than 1 year", selectionType: "INCLUDED" },
164
- { text: "1 to 2 years", selectionType: "INCLUDED" },
165
- { text: "3 to 5 years", selectionType: "INCLUDED" },
166
- { text: "6 to 10 years", selectionType: "INCLUDED" },
167
- { text: "More than 10 years", selectionType: "INCLUDED" },
210
+ { id: "1", text: "Less than 1 year", selectionType: "INCLUDED" },
211
+ { id: "2", text: "1 to 2 years", selectionType: "INCLUDED" },
212
+ { id: "3", text: "3 to 5 years", selectionType: "INCLUDED" },
213
+ { id: "4", text: "6 to 10 years", selectionType: "INCLUDED" },
214
+ { id: "5", text: "More than 10 years", selectionType: "INCLUDED" },
168
215
  ],
169
216
  },
170
217
  ];
@@ -306,27 +353,151 @@ export function applySalesNavigatorFiltersToQueryValue(queryValue, filters) {
306
353
  }
307
354
  return `${before}filters:List(${nextBlocks.join(",")})${after}`;
308
355
  }
309
- export function buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters) {
356
+ function canonicalizeSalesNavigatorQueryValue(queryValue) {
357
+ const { filtersContent } = findFiltersListBounds(queryValue);
358
+ return `(filters:List(${filtersContent}))`;
359
+ }
360
+ function canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl) {
310
361
  const url = new URL(sourceQueryUrl);
311
362
  ensureLinkedInSalesPeopleSearchUrl(url);
312
363
  const queryValue = url.searchParams.get("query");
313
364
  if (!queryValue) {
314
365
  throw new Error("Sales Navigator URL is missing the query parameter.");
315
366
  }
367
+ const canonicalUrl = new URL(LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL);
368
+ canonicalUrl.searchParams.set("query", canonicalizeSalesNavigatorQueryValue(queryValue));
369
+ return canonicalUrl;
370
+ }
371
+ export function buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters) {
372
+ const url = canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl);
373
+ const queryValue = url.searchParams.get("query");
374
+ if (!queryValue) {
375
+ throw new Error("Sales Navigator URL is missing the query parameter.");
376
+ }
316
377
  const appliedFilters = dedupeSalesNavigatorFilters(filters);
317
378
  const slicedQueryValue = applySalesNavigatorFiltersToQueryValue(queryValue, appliedFilters);
318
379
  url.searchParams.set("query", slicedQueryValue);
319
380
  return {
320
- sourceQueryUrl,
381
+ sourceQueryUrl: canonicalizeSalesNavigatorPeopleSearchUrl(sourceQueryUrl).toString(),
321
382
  slicedQueryUrl: url.toString(),
322
383
  appliedFilters,
323
384
  };
324
385
  }
386
+ export function buildSalesNavigatorPeopleSearchUrl(filters) {
387
+ const appliedFilters = dedupeSalesNavigatorFilters(filters);
388
+ if (appliedFilters.length === 0) {
389
+ throw new Error("Sales Navigator people search requires at least one filter.");
390
+ }
391
+ const url = new URL(LINKEDIN_SALES_NAVIGATOR_PEOPLE_SEARCH_URL);
392
+ url.searchParams.set("query", `(filters:List(${appliedFilters.map((filter) => buildFilterBlock(filter)).join(",")}))`);
393
+ return url.toString();
394
+ }
395
+ function inferSalesNavigatorFunctionFilters(options) {
396
+ const haystack = [options.title, options.categoryName, options.categorySlug]
397
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
398
+ .join(" ");
399
+ for (const hint of SALES_NAVIGATOR_FUNCTION_HINTS) {
400
+ if (hint.patterns.some((pattern) => pattern.test(haystack))) {
401
+ return [
402
+ {
403
+ type: "FUNCTION",
404
+ values: [hint.filterValue]
405
+ }
406
+ ];
407
+ }
408
+ }
409
+ return [];
410
+ }
411
+ function normalizeSalesNavigatorTitle(title) {
412
+ return title
413
+ .replace(/[“”"']+/g, "")
414
+ .replace(/\s*[|/]+\s*/g, " / ")
415
+ .replace(/\s*&\s*/g, " & ")
416
+ .replace(/[–—]+/g, "-")
417
+ .replace(/\s+/g, " ")
418
+ .replace(/^[\s\-:;,]+|[\s\-:;,]+$/g, "")
419
+ .trim();
420
+ }
421
+ function canonicalizeSalesNavigatorTitleKey(title) {
422
+ return normalizeSalesNavigatorTitle(title)
423
+ .toLowerCase()
424
+ .replace(/&/g, " and ")
425
+ .replace(/[^a-z0-9]+/g, " ")
426
+ .replace(/\s+/g, " ")
427
+ .trim();
428
+ }
429
+ export function deriveSalesNavigatorTitleQuerySeeds(options) {
430
+ const rankedTitles = new Map();
431
+ let firstSeenIndex = 0;
432
+ for (const item of options.items) {
433
+ const sourceProduct = options.sourceProductUrl === item.productUrl;
434
+ for (const intendedRole of item.intendedRoles) {
435
+ const title = normalizeSalesNavigatorTitle(intendedRole);
436
+ if (title.length === 0) {
437
+ continue;
438
+ }
439
+ const normalizedTitle = canonicalizeSalesNavigatorTitleKey(title);
440
+ const existing = rankedTitles.get(normalizedTitle);
441
+ if (existing) {
442
+ existing.matchedProductCount += 1;
443
+ existing.sourceProduct = existing.sourceProduct || sourceProduct;
444
+ continue;
445
+ }
446
+ rankedTitles.set(normalizedTitle, {
447
+ title,
448
+ matchedProductCount: 1,
449
+ sourceProduct,
450
+ firstSeenIndex,
451
+ categoryName: item.category?.name,
452
+ categorySlug: item.category?.slug
453
+ });
454
+ firstSeenIndex += 1;
455
+ }
456
+ }
457
+ const orderedTitles = [...rankedTitles.values()].sort((left, right) => {
458
+ if (left.sourceProduct !== right.sourceProduct) {
459
+ return left.sourceProduct ? -1 : 1;
460
+ }
461
+ if (left.sourceProduct && right.sourceProduct && left.firstSeenIndex !== right.firstSeenIndex) {
462
+ return left.firstSeenIndex - right.firstSeenIndex;
463
+ }
464
+ if (left.matchedProductCount !== right.matchedProductCount) {
465
+ return right.matchedProductCount - left.matchedProductCount;
466
+ }
467
+ if (left.firstSeenIndex !== right.firstSeenIndex) {
468
+ return left.firstSeenIndex - right.firstSeenIndex;
469
+ }
470
+ return left.title.localeCompare(right.title);
471
+ });
472
+ const limitedTitles = options.titleLimit && options.titleLimit > 0
473
+ ? orderedTitles.slice(0, options.titleLimit)
474
+ : orderedTitles;
475
+ return limitedTitles.map((entry) => {
476
+ const appliedFilters = dedupeSalesNavigatorFilters([
477
+ {
478
+ type: "CURRENT_TITLE",
479
+ values: [{ text: entry.title, selectionType: "INCLUDED" }]
480
+ },
481
+ ...inferSalesNavigatorFunctionFilters({
482
+ title: entry.title,
483
+ categoryName: entry.categoryName,
484
+ categorySlug: entry.categorySlug
485
+ })
486
+ ]);
487
+ return {
488
+ title: entry.title,
489
+ queryUrl: buildSalesNavigatorPeopleSearchUrl(appliedFilters),
490
+ appliedFilters,
491
+ matchedProductCount: entry.matchedProductCount,
492
+ sourceProduct: entry.sourceProduct
493
+ };
494
+ });
495
+ }
325
496
  export function buildSalesNavigatorPeopleSlice(sourceQueryUrl, filters = DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS) {
326
497
  return buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters);
327
498
  }
328
499
  export function createSalesNavigatorCrawlSeed(options) {
329
- const sliced = buildSalesNavigatorPeopleQuery(options.sourceQueryUrl, options.baselineFilters ?? DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS);
500
+ const sliced = buildSalesNavigatorPeopleQuery(options.sourceQueryUrl, options.baselineFilters ?? []);
330
501
  return {
331
502
  ...sliced,
332
503
  depth: 0,