salesprompter-cli 0.1.43 → 0.1.44

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.
@@ -10,6 +10,10 @@ const REQUEST_TIMEOUT_MS = 20_000;
10
10
  const MAX_COOKIE_VALIDATION_ATTEMPTS = 8;
11
11
  const COOKIE_VALIDATION_TIMEOUT_MS = 75_000;
12
12
  const COOKIE_PROBE_TIMEOUT_MS = 8_000;
13
+ const DEFAULT_SUPABASE_SESSION_REQUEST_TIMEOUT_MS = 2_500;
14
+ const LINKEDIN_SESSION_COOKIE_TABLE = "salesprompter_linkedin_session_cookies";
15
+ const LINKEDIN_SESSION_COOKIE_CHECK_TABLE = "salesprompter_linkedin_session_cookie_checks";
16
+ const CLAIM_LINKEDIN_SESSION_COOKIE_RPC = "salesprompter_claim_active_linkedin_session_cookie_excluding";
13
17
  const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_EMAILS = ["hello@danielsinewe.com"];
14
18
  const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_HANDLES = ["danielsinewe"];
15
19
  const ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS = 72;
@@ -213,14 +217,26 @@ export function createLinkedInSessionSupabaseClient(env = process.env) {
213
217
  if (!config) {
214
218
  return null;
215
219
  }
220
+ const configuredTimeoutMs = Number(env.SALESPROMPTER_LINKEDIN_SESSION_SUPABASE_TIMEOUT_MS);
221
+ const timeoutMs = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs >= 500
222
+ ? Math.min(30_000, Math.trunc(configuredTimeoutMs))
223
+ : DEFAULT_SUPABASE_SESSION_REQUEST_TIMEOUT_MS;
224
+ const timedFetch = (input, init) => {
225
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
226
+ const signal = init?.signal
227
+ ? AbortSignal.any([init.signal, timeoutSignal])
228
+ : timeoutSignal;
229
+ return fetch(input, { ...init, signal });
230
+ };
216
231
  return createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
217
- auth: { persistSession: false }
232
+ auth: { persistSession: false },
233
+ global: { fetch: timedFetch }
218
234
  });
219
235
  }
220
236
  export async function recordLinkedInSessionCookieAudit(supabase, input) {
221
237
  const checkedAt = input.checkedAt || new Date().toISOString();
222
238
  const updateResult = await supabase
223
- .from("linkedin_session_cookies")
239
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
224
240
  .update({
225
241
  is_active: input.isActive,
226
242
  inactive_reason: input.inactiveReason ?? null,
@@ -238,7 +254,7 @@ export async function recordLinkedInSessionCookieAudit(supabase, input) {
238
254
  if (updateResult.error) {
239
255
  throw new Error(`Failed to update LinkedIn session cookie status: ${updateResult.error.message}`);
240
256
  }
241
- const insertResult = await supabase.from("linkedin_session_cookie_checks").insert({
257
+ const insertResult = await supabase.from(LINKEDIN_SESSION_COOKIE_CHECK_TABLE).insert({
242
258
  session_cookie_sha256: input.sessionCookieSha256,
243
259
  checked_at: checkedAt,
244
260
  source: input.source,
@@ -262,8 +278,8 @@ function normalizeCandidateSelectionError(error) {
262
278
  return error instanceof Error ? error.message : String(error);
263
279
  }
264
280
  async function listLinkedInSessionCookieCandidates(supabase, options) {
265
- const query = supabase
266
- .from("linkedin_session_cookies")
281
+ let query = supabase
282
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
267
283
  .select([
268
284
  "session_cookie_sha256",
269
285
  "session_cookie_ciphertext",
@@ -279,17 +295,28 @@ async function listLinkedInSessionCookieCandidates(supabase, options) {
279
295
  ].join(","))
280
296
  .eq("last_product_class", "sales_navigator")
281
297
  .eq("last_sales_navigator_status", "ok")
282
- .not("last_validation_at", "is", null)
283
- .gte("last_validation_at", lookbackThresholdIso(ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS))
284
- .order("last_selected_at", { ascending: true, nullsFirst: true })
285
- .order("last_validation_at", { ascending: false })
286
- .limit(options.limit ?? MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES);
298
+ .not("last_validation_at", "is", null);
299
+ if (!options.includeStale) {
300
+ query = query.gte("last_validation_at", lookbackThresholdIso(ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS));
301
+ }
302
+ query = options.includeStale
303
+ ? query
304
+ .order("last_validation_at", { ascending: false })
305
+ .order("last_selected_at", { ascending: true, nullsFirst: true })
306
+ .limit(options.limit ?? MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES)
307
+ : query
308
+ .order("last_selected_at", { ascending: true, nullsFirst: true })
309
+ .order("last_validation_at", { ascending: false })
310
+ .limit(options.limit ?? MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES);
287
311
  const filteredQuery = options.activeOnly
288
312
  ? query.eq("is_active", true)
289
313
  : query
290
314
  .eq("is_active", false)
291
315
  .like("inactive_reason", `${RECOVERABLE_LINKEDIN_SESSION_INACTIVE_REASON_PREFIX}%`);
292
- const result = await filteredQuery;
316
+ const executableQuery = typeof filteredQuery.retry === "function"
317
+ ? filteredQuery.retry(false)
318
+ : filteredQuery;
319
+ const result = await executableQuery;
293
320
  if (result.error) {
294
321
  throw new Error(`Failed to query LinkedIn session cookie candidates: ${result.error.message}`);
295
322
  }
@@ -368,7 +395,8 @@ async function buildClaimedLinkedInSessionCookieFromRow(supabase, row, source, e
368
395
  async function claimLinkedInSessionCookieFromCandidates(supabase, source, exclusions, options) {
369
396
  const candidates = await listLinkedInSessionCookieCandidates(supabase, {
370
397
  activeOnly: options.activeOnly,
371
- exclusions
398
+ exclusions,
399
+ includeStale: options.includeStale
372
400
  });
373
401
  for (const candidate of candidates) {
374
402
  const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
@@ -381,6 +409,27 @@ async function claimLinkedInSessionCookieFromCandidates(supabase, source, exclus
381
409
  }
382
410
  return null;
383
411
  }
412
+ async function claimStaleActiveLinkedInSessionCookie(supabase, source, exclusions, env = process.env, seenCookieHashes) {
413
+ const candidates = await listLinkedInSessionCookieCandidates(supabase, {
414
+ activeOnly: true,
415
+ exclusions,
416
+ includeStale: true,
417
+ limit: 64
418
+ });
419
+ for (const candidate of candidates) {
420
+ if (seenCookieHashes?.has(candidate.session_cookie_sha256)) {
421
+ continue;
422
+ }
423
+ const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
424
+ claimStrategy: "stale_active_fallback",
425
+ env
426
+ });
427
+ if (claimed) {
428
+ return claimed;
429
+ }
430
+ }
431
+ return null;
432
+ }
384
433
  export async function claimActiveLinkedInSessionCookie(supabase, source, env = process.env) {
385
434
  if (typeof supabase.rpc !== "function") {
386
435
  throw new Error("Supabase client does not support RPC");
@@ -390,7 +439,7 @@ export async function claimActiveLinkedInSessionCookie(supabase, source, env = p
390
439
  for (let attempt = 0; attempt < 10; attempt += 1) {
391
440
  try {
392
441
  const result = await supabase
393
- .rpc("claim_active_linkedin_session_cookie_excluding", {
442
+ .rpc(CLAIM_LINKEDIN_SESSION_COOKIE_RPC, {
394
443
  selection_source: source,
395
444
  excluded_user_emails: exclusions.userEmails,
396
445
  excluded_user_handles: exclusions.userHandles
@@ -427,8 +476,8 @@ export async function claimActiveLinkedInSessionCookie(supabase, source, env = p
427
476
  return directFallback;
428
477
  }
429
478
  if (lastClaimError &&
430
- /claim_active_linkedin_session_cookie_excluding/i.test(lastClaimError)) {
431
- throw new Error("LinkedIn session exclusion rotation RPC is unavailable. Apply the Salesprompter app migration that adds claim_active_linkedin_session_cookie_excluding before running Sales Navigator exports.");
479
+ /salesprompter_claim_active_linkedin_session_cookie_excluding/i.test(lastClaimError)) {
480
+ throw new Error("LinkedIn session exclusion rotation RPC is unavailable. Apply the Salesprompter migration that adds salesprompter_claim_active_linkedin_session_cookie_excluding before running Sales Navigator exports.");
432
481
  }
433
482
  if (lastClaimError) {
434
483
  throw new Error(`Failed to claim LinkedIn session cookie: ${lastClaimError}`);
@@ -455,8 +504,8 @@ async function claimRecoverableInactiveLinkedInSessionCookie(supabase, source, e
455
504
  return null;
456
505
  }
457
506
  async function describeLinkedInSessionPool(supabase, exclusions) {
458
- const result = await supabase
459
- .from("linkedin_session_cookies")
507
+ const poolQuery = supabase
508
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
460
509
  .select([
461
510
  "last_user_email",
462
511
  "last_user_handle",
@@ -467,6 +516,9 @@ async function describeLinkedInSessionPool(supabase, exclusions) {
467
516
  "last_product_class"
468
517
  ].join(","))
469
518
  .eq("last_product_class", "sales_navigator");
519
+ const result = await (typeof poolQuery.retry === "function"
520
+ ? poolQuery.retry(false)
521
+ : poolQuery);
470
522
  if (result.error) {
471
523
  throw new Error(`Failed to inspect LinkedIn session pool: ${result.error.message}`);
472
524
  }
@@ -682,6 +734,37 @@ export async function probeSalesNavigatorSearchSession(sessionCookie, queryUrl,
682
734
  }
683
735
  };
684
736
  }
737
+ export async function claimLinkedInSessionCookieForCli(options) {
738
+ const env = options.env ?? process.env;
739
+ const supabase = options.supabase ?? createLinkedInSessionSupabaseClient(env);
740
+ if (!supabase) {
741
+ return null;
742
+ }
743
+ const exclusions = resolveCliLinkedInSessionExclusions(env);
744
+ const seenCookieHashes = new Set();
745
+ let claimErrorMessage = null;
746
+ let claimedSession = null;
747
+ try {
748
+ claimedSession = await claimActiveLinkedInSessionCookie(supabase, options.source, env);
749
+ }
750
+ catch (error) {
751
+ claimErrorMessage = normalizeCandidateSelectionError(error);
752
+ }
753
+ if (claimedSession) {
754
+ seenCookieHashes.add(claimedSession.sessionCookieSha256);
755
+ return claimedSession;
756
+ }
757
+ claimedSession = await claimRecoverableInactiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
758
+ if (claimedSession) {
759
+ return claimedSession;
760
+ }
761
+ claimedSession = await claimStaleActiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
762
+ if (claimedSession) {
763
+ return claimedSession;
764
+ }
765
+ const poolDiagnostics = await buildLinkedInSessionPoolDiagnosticsMessage(supabase, exclusions);
766
+ throw new Error(`${claimErrorMessage ?? "No non-excluded decryptable LinkedIn session cookies available"}${poolDiagnostics}`);
767
+ }
685
768
  export async function claimValidatedSalesNavigatorSessionCookieForCli(options) {
686
769
  const env = options.env ?? process.env;
687
770
  const supabase = options.supabase ?? createLinkedInSessionSupabaseClient(env);
@@ -708,6 +791,9 @@ export async function claimValidatedSalesNavigatorSessionCookieForCli(options) {
708
791
  if (!claimedSession) {
709
792
  claimedSession = await claimRecoverableInactiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
710
793
  }
794
+ if (!claimedSession) {
795
+ claimedSession = await claimStaleActiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
796
+ }
711
797
  if (!claimedSession) {
712
798
  lastFailureMessage =
713
799
  claimErrorMessage ?? "No non-excluded decryptable active LinkedIn session cookies available";
@@ -230,39 +230,47 @@ export const DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS = [
230
230
  {
231
231
  key: "company-headcount",
232
232
  filterType: "COMPANY_HEADCOUNT",
233
+ supportsResidualExclusion: true,
233
234
  values: [
234
235
  { id: "B", text: "1-10", selectionType: "INCLUDED" },
235
236
  { id: "C", text: "11-50", selectionType: "INCLUDED" },
236
237
  { id: "D", text: "51-200", selectionType: "INCLUDED" },
237
238
  { 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" },
239
+ { id: "F", text: "501-1,000", selectionType: "INCLUDED" },
240
+ { id: "G", text: "1,001-5,000", selectionType: "INCLUDED" },
241
+ { id: "H", text: "5,001-10,000", selectionType: "INCLUDED" },
242
+ { id: "I", text: "10,001+", selectionType: "INCLUDED" },
242
243
  ],
243
244
  },
244
245
  {
245
246
  key: "number-of-followers",
246
247
  filterType: "NUM_OF_FOLLOWERS",
248
+ supportsResidualExclusion: true,
247
249
  values: [
248
- { id: "NFR1", text: "1-10", selectionType: "INCLUDED" },
249
- { id: "NFR2", text: "11-100", selectionType: "INCLUDED" },
250
+ { id: "NFR1", text: "1-50", selectionType: "INCLUDED" },
251
+ { id: "NFR2", text: "51-100", selectionType: "INCLUDED" },
250
252
  { id: "NFR3", text: "101-1000", selectionType: "INCLUDED" },
251
253
  { id: "NFR4", text: "1001-5000", selectionType: "INCLUDED" },
252
254
  { id: "NFR5", text: "5001+", selectionType: "INCLUDED" },
253
255
  ],
254
256
  },
255
257
  {
256
- key: "company-type",
257
- filterType: "COMPANY_TYPE",
258
+ key: "annual-revenue",
259
+ filterType: "ANNUAL_REVENUE",
260
+ supportsResidualExclusion: false,
261
+ combinableValues: false,
258
262
  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" },
263
+ { text: "USD 0-0.5M", rangeValue: { min: 0, max: 0.5 }, selectedSubFilter: "USD" },
264
+ { text: "USD 0.5-1M", rangeValue: { min: 0.5, max: 1 }, selectedSubFilter: "USD" },
265
+ { text: "USD 1-2.5M", rangeValue: { min: 1, max: 2.5 }, selectedSubFilter: "USD" },
266
+ { text: "USD 2.5-5M", rangeValue: { min: 2.5, max: 5 }, selectedSubFilter: "USD" },
267
+ { text: "USD 5-10M", rangeValue: { min: 5, max: 10 }, selectedSubFilter: "USD" },
268
+ { text: "USD 10-20M", rangeValue: { min: 10, max: 20 }, selectedSubFilter: "USD" },
269
+ { text: "USD 20-50M", rangeValue: { min: 20, max: 50 }, selectedSubFilter: "USD" },
270
+ { text: "USD 50-100M", rangeValue: { min: 50, max: 100 }, selectedSubFilter: "USD" },
271
+ { text: "USD 100-500M", rangeValue: { min: 100, max: 500 }, selectedSubFilter: "USD" },
272
+ { text: "USD 500M-1B", rangeValue: { min: 500, max: 1000 }, selectedSubFilter: "USD" },
273
+ { text: "USD 1B+", rangeValue: { min: 1000, max: 1001 }, selectedSubFilter: "USD" },
266
274
  ],
267
275
  },
268
276
  ];
@@ -358,6 +366,17 @@ function extractFilterType(block) {
358
366
  return match?.[1] ?? null;
359
367
  }
360
368
  function buildFilterBlock(filter) {
369
+ const rangeValues = filter.values.filter((value) => value.rangeValue);
370
+ if (rangeValues.length > 0) {
371
+ if (rangeValues.length !== 1 || filter.values.length !== 1) {
372
+ throw new Error(`Sales Navigator range filter ${filter.type} requires exactly one range.`);
373
+ }
374
+ const value = rangeValues[0];
375
+ const range = value.rangeValue;
376
+ const selectedSubFilter = value.selectedSubFilter?.trim();
377
+ return (`(type:${filter.type},rangeValue:(min:${range.min},max:${range.max})` +
378
+ `${selectedSubFilter ? `,selectedSubFilter:${encodeURIComponent(selectedSubFilter)}` : ""})`);
379
+ }
361
380
  const encodedValues = filter.values
362
381
  .map((value) => {
363
382
  const selectionType = value.selectionType ?? "INCLUDED";
@@ -382,6 +401,9 @@ function dedupeSalesNavigatorFilters(filters) {
382
401
  value.selectionType ?? "INCLUDED",
383
402
  value.text.trim().toLowerCase(),
384
403
  value.id?.trim().toLowerCase() ?? "",
404
+ value.rangeValue?.min ?? "",
405
+ value.rangeValue?.max ?? "",
406
+ value.selectedSubFilter?.trim().toLowerCase() ?? "",
385
407
  ].join("|");
386
408
  if (seen.has(key)) {
387
409
  return false;
@@ -710,6 +732,7 @@ function getUnusedSalesNavigatorDimensions(attempt, dimensions) {
710
732
  function buildSalesNavigatorAdaptiveChildren(attempt, dimension, options) {
711
733
  const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, dimension, options.searchType);
712
734
  const residualChildren = options.includeResidualExclusionSlices
735
+ && dimension.supportsResidualExclusion !== false
713
736
  ? [
714
737
  buildSalesNavigatorResidualCrawlAttempt(attempt, dimension, options.searchType),
715
738
  ]
@@ -767,6 +790,9 @@ function buildSalesNavigatorCombinedCrawlAttempt(attempt, dimension, children, s
767
790
  };
768
791
  }
769
792
  async function coalesceSalesNavigatorUnderCapChildren(options) {
793
+ if (options.dimension.combinableValues === false) {
794
+ return options.summaries;
795
+ }
770
796
  const packable = [];
771
797
  const fixed = [];
772
798
  for (const summary of options.summaries) {
@@ -1106,6 +1132,7 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
1106
1132
  if (children.length === 0) {
1107
1133
  const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, nextDimension, options.searchType);
1108
1134
  const residualChildren = options.includeResidualExclusionSlices
1135
+ && nextDimension.supportsResidualExclusion !== false
1109
1136
  ? [
1110
1137
  buildSalesNavigatorResidualCrawlAttempt(attempt, nextDimension, options.searchType),
1111
1138
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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",