salesprompter-cli 0.1.17 → 0.1.18

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.
@@ -0,0 +1,475 @@
1
+ export class SalesNavigatorSliceTooBroadError extends Error {
2
+ totalResults;
3
+ details;
4
+ constructor(message, options) {
5
+ super(message);
6
+ this.name = "SalesNavigatorSliceTooBroadError";
7
+ this.totalResults = options?.totalResults ?? null;
8
+ this.details = options?.details;
9
+ }
10
+ }
11
+ export const DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS = [
12
+ {
13
+ type: "FUNCTION",
14
+ values: [
15
+ {
16
+ id: "12",
17
+ text: "Human Resources",
18
+ selectionType: "INCLUDED",
19
+ },
20
+ ],
21
+ },
22
+ {
23
+ type: "SENIORITY_LEVEL",
24
+ values: [
25
+ {
26
+ id: "220",
27
+ text: "Director",
28
+ selectionType: "INCLUDED",
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ type: "YEARS_AT_CURRENT_COMPANY",
34
+ values: [
35
+ {
36
+ id: "3",
37
+ text: "3 to 5 years",
38
+ selectionType: "INCLUDED",
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ type: "YEARS_IN_CURRENT_POSITION",
44
+ values: [
45
+ {
46
+ id: "3",
47
+ text: "3 to 5 years",
48
+ selectionType: "INCLUDED",
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ type: "COMPANY_HEADCOUNT",
54
+ values: [
55
+ {
56
+ id: "D",
57
+ text: "51-200",
58
+ selectionType: "INCLUDED",
59
+ },
60
+ ],
61
+ },
62
+ {
63
+ type: "COMPANY_TYPE",
64
+ values: [
65
+ {
66
+ id: "P",
67
+ text: "Privately Held",
68
+ selectionType: "INCLUDED",
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ type: "YEARS_OF_EXPERIENCE",
74
+ values: [
75
+ {
76
+ id: "5",
77
+ text: "More than 10 years",
78
+ selectionType: "INCLUDED",
79
+ },
80
+ ],
81
+ },
82
+ ];
83
+ export const DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS = [
84
+ {
85
+ type: "FUNCTION",
86
+ values: [
87
+ {
88
+ id: "12",
89
+ text: "Human Resources",
90
+ selectionType: "INCLUDED",
91
+ },
92
+ ],
93
+ },
94
+ ];
95
+ export const DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS = [
96
+ {
97
+ key: "company-headcount",
98
+ filterType: "COMPANY_HEADCOUNT",
99
+ 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" },
108
+ ],
109
+ },
110
+ {
111
+ key: "company-type",
112
+ filterType: "COMPANY_TYPE",
113
+ values: [
114
+ { text: "Privately Held", selectionType: "INCLUDED" },
115
+ { text: "Public Company", selectionType: "INCLUDED" },
116
+ { text: "Partnership", selectionType: "INCLUDED" },
117
+ { text: "Self Owned", selectionType: "INCLUDED" },
118
+ { text: "Non Profit", selectionType: "INCLUDED" },
119
+ { text: "Educational Institution", selectionType: "INCLUDED" },
120
+ { text: "Government Agency", selectionType: "INCLUDED" },
121
+ ],
122
+ },
123
+ {
124
+ key: "seniority-level",
125
+ filterType: "SENIORITY_LEVEL",
126
+ 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" },
135
+ ],
136
+ },
137
+ {
138
+ key: "years-at-company",
139
+ filterType: "YEARS_AT_CURRENT_COMPANY",
140
+ 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" },
146
+ ],
147
+ },
148
+ {
149
+ key: "years-in-position",
150
+ filterType: "YEARS_IN_CURRENT_POSITION",
151
+ 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" },
157
+ ],
158
+ },
159
+ {
160
+ key: "years-of-experience",
161
+ filterType: "YEARS_OF_EXPERIENCE",
162
+ 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" },
168
+ ],
169
+ },
170
+ ];
171
+ function ensureLinkedInSalesPeopleSearchUrl(url) {
172
+ if (!/(^|\.)linkedin\.com$/i.test(url.hostname)) {
173
+ throw new Error("Expected a linkedin.com Sales Navigator URL.");
174
+ }
175
+ if (!/^\/sales\/search\/people/i.test(url.pathname)) {
176
+ throw new Error("Expected a LinkedIn Sales Navigator people search URL.");
177
+ }
178
+ if (!url.searchParams.has("query")) {
179
+ throw new Error("Sales Navigator URL is missing the query parameter.");
180
+ }
181
+ }
182
+ function splitTopLevelCommaSeparated(value) {
183
+ const parts = [];
184
+ let depth = 0;
185
+ let start = 0;
186
+ for (let index = 0; index < value.length; index += 1) {
187
+ const char = value[index];
188
+ if (char === "(") {
189
+ depth += 1;
190
+ continue;
191
+ }
192
+ if (char === ")") {
193
+ depth = Math.max(0, depth - 1);
194
+ continue;
195
+ }
196
+ if (char === "," && depth === 0) {
197
+ const part = value.slice(start, index).trim();
198
+ if (part.length > 0) {
199
+ parts.push(part);
200
+ }
201
+ start = index + 1;
202
+ }
203
+ }
204
+ const tail = value.slice(start).trim();
205
+ if (tail.length > 0) {
206
+ parts.push(tail);
207
+ }
208
+ return parts;
209
+ }
210
+ function findFiltersListBounds(queryValue) {
211
+ const marker = "filters:List(";
212
+ const markerIndex = queryValue.indexOf(marker);
213
+ if (markerIndex === -1) {
214
+ throw new Error("Sales Navigator query is missing filters:List(...).");
215
+ }
216
+ let depth = 1;
217
+ let endIndex = -1;
218
+ for (let index = markerIndex + marker.length; index < queryValue.length; index += 1) {
219
+ const char = queryValue[index];
220
+ if (char === "(") {
221
+ depth += 1;
222
+ }
223
+ else if (char === ")") {
224
+ depth -= 1;
225
+ if (depth === 0) {
226
+ endIndex = index;
227
+ break;
228
+ }
229
+ }
230
+ }
231
+ if (endIndex === -1) {
232
+ throw new Error("Could not parse filters list from Sales Navigator query.");
233
+ }
234
+ return {
235
+ before: queryValue.slice(0, markerIndex),
236
+ filtersContent: queryValue.slice(markerIndex + marker.length, endIndex),
237
+ after: queryValue.slice(endIndex + 1),
238
+ };
239
+ }
240
+ function extractFilterType(block) {
241
+ const match = block.match(/^\(type:([A-Z_]+)/);
242
+ return match?.[1] ?? null;
243
+ }
244
+ function buildFilterBlock(filter) {
245
+ const encodedValues = filter.values
246
+ .map((value) => {
247
+ const selectionType = value.selectionType ?? "INCLUDED";
248
+ const segments = [];
249
+ if (typeof value.id === "string" && value.id.trim().length > 0) {
250
+ segments.push(`id:${value.id}`);
251
+ }
252
+ segments.push(`text:${encodeURIComponent(value.text)}`);
253
+ segments.push(`selectionType:${selectionType}`);
254
+ return `(${segments.join(",")})`;
255
+ })
256
+ .join(",");
257
+ return `(type:${filter.type},values:List(${encodedValues}))`;
258
+ }
259
+ function dedupeSalesNavigatorFilters(filters) {
260
+ const seen = new Set();
261
+ const deduped = [];
262
+ for (const filter of filters) {
263
+ const values = filter.values.filter((value) => {
264
+ const key = [
265
+ filter.type,
266
+ value.selectionType ?? "INCLUDED",
267
+ value.text.trim().toLowerCase(),
268
+ value.id?.trim().toLowerCase() ?? "",
269
+ ].join("|");
270
+ if (seen.has(key)) {
271
+ return false;
272
+ }
273
+ seen.add(key);
274
+ return true;
275
+ });
276
+ if (values.length > 0) {
277
+ deduped.push({
278
+ type: filter.type,
279
+ values,
280
+ });
281
+ }
282
+ }
283
+ return deduped;
284
+ }
285
+ export function applySalesNavigatorFiltersToQueryValue(queryValue, filters) {
286
+ const { before, filtersContent, after } = findFiltersListBounds(queryValue);
287
+ const existingBlocks = splitTopLevelCommaSeparated(filtersContent);
288
+ const replacements = new Map(filters.map((filter) => [filter.type, filter]));
289
+ const consumedTypes = new Set();
290
+ const nextBlocks = existingBlocks.map((block) => {
291
+ const type = extractFilterType(block);
292
+ if (!type) {
293
+ return block;
294
+ }
295
+ const replacement = replacements.get(type);
296
+ if (!replacement) {
297
+ return block;
298
+ }
299
+ consumedTypes.add(type);
300
+ return buildFilterBlock(replacement);
301
+ });
302
+ for (const filter of filters) {
303
+ if (!consumedTypes.has(filter.type)) {
304
+ nextBlocks.push(buildFilterBlock(filter));
305
+ }
306
+ }
307
+ return `${before}filters:List(${nextBlocks.join(",")})${after}`;
308
+ }
309
+ export function buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters) {
310
+ const url = new URL(sourceQueryUrl);
311
+ ensureLinkedInSalesPeopleSearchUrl(url);
312
+ const queryValue = url.searchParams.get("query");
313
+ if (!queryValue) {
314
+ throw new Error("Sales Navigator URL is missing the query parameter.");
315
+ }
316
+ const appliedFilters = dedupeSalesNavigatorFilters(filters);
317
+ const slicedQueryValue = applySalesNavigatorFiltersToQueryValue(queryValue, appliedFilters);
318
+ url.searchParams.set("query", slicedQueryValue);
319
+ return {
320
+ sourceQueryUrl,
321
+ slicedQueryUrl: url.toString(),
322
+ appliedFilters,
323
+ };
324
+ }
325
+ export function buildSalesNavigatorPeopleSlice(sourceQueryUrl, filters = DEFAULT_SALES_NAVIGATOR_PEOPLE_SLICE_FILTERS) {
326
+ return buildSalesNavigatorPeopleQuery(sourceQueryUrl, filters);
327
+ }
328
+ export function createSalesNavigatorCrawlSeed(options) {
329
+ const sliced = buildSalesNavigatorPeopleQuery(options.sourceQueryUrl, options.baselineFilters ?? DEFAULT_SALES_NAVIGATOR_CRAWL_BASELINE_FILTERS);
330
+ return {
331
+ ...sliced,
332
+ depth: 0,
333
+ retryCount: 0,
334
+ maxResultsPerSearch: options.maxResultsPerSearch,
335
+ numberOfProfiles: options.numberOfProfiles,
336
+ slicePreset: options.slicePreset,
337
+ splitTrail: [],
338
+ };
339
+ }
340
+ export function expandSalesNavigatorCrawlAttempt(attempt, dimension) {
341
+ return dimension.values.map((value) => {
342
+ const filters = dedupeSalesNavigatorFilters([
343
+ ...attempt.appliedFilters.filter((filter) => filter.type !== dimension.filterType),
344
+ {
345
+ type: dimension.filterType,
346
+ values: [
347
+ {
348
+ ...value,
349
+ selectionType: value.selectionType ?? "INCLUDED",
350
+ },
351
+ ],
352
+ },
353
+ ]);
354
+ const sliced = buildSalesNavigatorPeopleQuery(attempt.sourceQueryUrl, filters);
355
+ return {
356
+ ...sliced,
357
+ depth: attempt.depth + 1,
358
+ retryCount: 0,
359
+ maxResultsPerSearch: attempt.maxResultsPerSearch,
360
+ numberOfProfiles: attempt.numberOfProfiles,
361
+ slicePreset: attempt.slicePreset,
362
+ splitTrail: [
363
+ ...attempt.splitTrail,
364
+ {
365
+ key: dimension.key,
366
+ filterType: dimension.filterType,
367
+ value: {
368
+ ...value,
369
+ selectionType: value.selectionType ?? "INCLUDED",
370
+ },
371
+ },
372
+ ],
373
+ };
374
+ });
375
+ }
376
+ export function buildSalesNavigatorCrawlPreview(options) {
377
+ const root = createSalesNavigatorCrawlSeed(options);
378
+ const dimensions = options.dimensions ?? DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS;
379
+ const firstSplit = dimensions[0] ? expandSalesNavigatorCrawlAttempt(root, dimensions[0]) : [];
380
+ return { root, firstSplit, dimensions };
381
+ }
382
+ export async function executeSalesNavigatorAdaptiveCrawl(options) {
383
+ const dimensions = options.dimensions ?? DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS;
384
+ const maxSplitDepth = Math.min(options.maxSplitDepth ?? dimensions.length, dimensions.length);
385
+ const maxSlices = options.maxSlices ?? 1000;
386
+ const maxRetries = options.maxRetries ?? 1;
387
+ const exported = [];
388
+ const splitEvents = [];
389
+ const failed = [];
390
+ const unresolved = [];
391
+ const queue = [
392
+ createSalesNavigatorCrawlSeed({
393
+ sourceQueryUrl: options.sourceQueryUrl,
394
+ baselineFilters: options.baselineFilters,
395
+ maxResultsPerSearch: options.maxResultsPerSearch,
396
+ numberOfProfiles: options.numberOfProfiles,
397
+ slicePreset: options.slicePreset,
398
+ }),
399
+ ];
400
+ const seenQueryUrls = new Set(queue.map((attempt) => attempt.slicedQueryUrl));
401
+ let attempted = 0;
402
+ let truncated = false;
403
+ while (queue.length > 0) {
404
+ if (attempted >= maxSlices) {
405
+ truncated = true;
406
+ break;
407
+ }
408
+ const attempt = queue.shift();
409
+ if (!attempt) {
410
+ break;
411
+ }
412
+ attempted += 1;
413
+ try {
414
+ const result = await options.exportSlice(attempt);
415
+ exported.push({ attempt, result });
416
+ }
417
+ catch (error) {
418
+ if (error instanceof SalesNavigatorSliceTooBroadError) {
419
+ const nextDimension = dimensions[attempt.depth];
420
+ if (!nextDimension || attempt.depth >= maxSplitDepth) {
421
+ unresolved.push({
422
+ attempt,
423
+ error: error.message,
424
+ totalResults: error.totalResults,
425
+ });
426
+ continue;
427
+ }
428
+ const children = expandSalesNavigatorCrawlAttempt(attempt, nextDimension).filter((child) => {
429
+ if (seenQueryUrls.has(child.slicedQueryUrl)) {
430
+ return false;
431
+ }
432
+ seenQueryUrls.add(child.slicedQueryUrl);
433
+ return true;
434
+ });
435
+ if (children.length === 0) {
436
+ unresolved.push({
437
+ attempt,
438
+ error: error.message,
439
+ totalResults: error.totalResults,
440
+ });
441
+ continue;
442
+ }
443
+ splitEvents.push({
444
+ attempt,
445
+ totalResults: error.totalResults,
446
+ nextDimension: nextDimension.key,
447
+ childCount: children.length,
448
+ });
449
+ queue.push(...children);
450
+ continue;
451
+ }
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ if (attempt.retryCount < maxRetries) {
454
+ queue.push({
455
+ ...attempt,
456
+ retryCount: attempt.retryCount + 1,
457
+ });
458
+ continue;
459
+ }
460
+ failed.push({
461
+ attempt,
462
+ error: message,
463
+ });
464
+ }
465
+ }
466
+ return {
467
+ exported,
468
+ splitEvents,
469
+ failed,
470
+ unresolved,
471
+ attempted,
472
+ truncated,
473
+ remainingQueue: queue.length,
474
+ };
475
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "JSON-first sales prospecting CLI for ICP definition, lead generation, enrichment, scoring, and CRM/outreach sync.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "license": "MIT",
52
52
  "dependencies": {
53
+ "cheerio": "^1.2.0",
53
54
  "commander": "^14.0.1",
54
55
  "zod": "^4.1.5"
55
56
  },