salesprompter-cli 0.1.49 → 0.1.51

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.
package/README.md CHANGED
@@ -70,6 +70,11 @@ salesprompter leads:collect \
70
70
  --linkedin-url "$SALES_NAV_PEOPLE_URL" \
71
71
  --max-results 1000
72
72
 
73
+ # Enrich one or more stored imports: Sales Navigator company -> official domain -> Hunter email
74
+ # This updates workspace data only. It does not create or start outreach.
75
+ salesprompter leads:enrich-import \
76
+ --run-id "$CLI_IMPORT_RUN_ID"
77
+
73
78
  salesprompter affiliate:launch \
74
79
  --affiliate-link "https://example.com/?ref=you" \
75
80
  --linkedin-url "https://www.linkedin.com/sales/search/people?query=..." \
package/dist/cli.js CHANGED
@@ -258,6 +258,51 @@ const CliEmailEnrichmentCompaniesResponseSchema = z.object({
258
258
  linkedinCompanyPage: z.string().nullable()
259
259
  }))
260
260
  });
261
+ const CliImportCompanyCandidateSchema = z.object({
262
+ companyKey: z.string().min(1),
263
+ companyId: z.string().nullable(),
264
+ companyName: z.string().nullable(),
265
+ salesNavCompanyUrl: z.string().nullable(),
266
+ linkedinCompanyUrl: z.string().nullable(),
267
+ personCount: z.number().int().nonnegative(),
268
+ profileStatus: z.string().nullable(),
269
+ profileDomain: z.string().nullable()
270
+ });
271
+ const CliImportCompanyCandidatesResponseSchema = z.object({
272
+ companies: z.array(CliImportCompanyCandidateSchema)
273
+ });
274
+ const CliImportCompanySaveResponseSchema = z.object({
275
+ saved: z.number().int().nonnegative(),
276
+ enriched: z.number().int().nonnegative(),
277
+ domains: z.number().int().nonnegative(),
278
+ notFound: z.number().int().nonnegative(),
279
+ failed: z.number().int().nonnegative()
280
+ });
281
+ const CliImportHunterBatchResponseSchema = z.object({
282
+ selected: z.number().int().nonnegative(),
283
+ processed: z.number().int().nonnegative(),
284
+ found: z.number().int().nonnegative(),
285
+ notFound: z.number().int().nonnegative(),
286
+ failed: z.number().int().nonnegative(),
287
+ halted: z.boolean(),
288
+ error: z.string().nullable()
289
+ });
290
+ const CliImportEnrichmentStatusSchema = z.object({
291
+ runCount: z.coerce.number().int().nonnegative(),
292
+ memberships: z.coerce.number().int().nonnegative(),
293
+ uniquePeople: z.coerce.number().int().nonnegative(),
294
+ distinctCompanies: z.coerce.number().int().nonnegative(),
295
+ companiesEnriched: z.coerce.number().int().nonnegative(),
296
+ companiesNotFound: z.coerce.number().int().nonnegative(),
297
+ companiesFailed: z.coerce.number().int().nonnegative(),
298
+ domainsFound: z.coerce.number().int().nonnegative(),
299
+ peopleWithDomain: z.coerce.number().int().nonnegative(),
300
+ emailsFound: z.coerce.number().int().nonnegative(),
301
+ emailsNotFound: z.coerce.number().int().nonnegative(),
302
+ emailsFailed: z.coerce.number().int().nonnegative(),
303
+ emailsProcessing: z.coerce.number().int().nonnegative(),
304
+ emailsPending: z.coerce.number().int().nonnegative()
305
+ });
261
306
  const SalesNavigatorLaunchDiagnosticsSchema = z.object({
262
307
  orderedCandidateAgentIds: z.array(z.string().min(1)),
263
308
  runningAgentIds: z.array(z.string().min(1)),
@@ -479,6 +524,7 @@ const cliPacks = [
479
524
  commands: [
480
525
  "leads:discover",
481
526
  "leads:collect",
527
+ "leads:enrich-import",
482
528
  "search:run",
483
529
  "search:status",
484
530
  "search:export",
@@ -517,6 +563,7 @@ const helpAliasByCommandName = new Map([
517
563
  ["linkedin-products:scrape", "market:scrape"],
518
564
  ["salesnav:from-product-category", "leads:discover"],
519
565
  ["salesnav:people:collect", "leads:collect"],
566
+ ["salesnav:people:enrich", "leads:enrich-import"],
520
567
  ["salesnav:crawl", "search:run"],
521
568
  ["salesnav:crawl:status", "search:status"],
522
569
  ["salesnav:export", "search:export"],
@@ -562,6 +609,7 @@ const helpVisibleCommandNames = new Set([
562
609
  "linkedin-products:scrape",
563
610
  "salesnav:from-product-category",
564
611
  "salesnav:people:collect",
612
+ "salesnav:people:enrich",
565
613
  "salesnav:crawl",
566
614
  "salesnav:crawl:status",
567
615
  "salesnav:export",
@@ -6696,6 +6744,29 @@ async function enrichDirectEmailCompaniesViaApp(session, payload) {
6696
6744
  }), CliEmailEnrichmentCompaniesResponseSchema);
6697
6745
  return value;
6698
6746
  }
6747
+ async function runCliImportEnrichmentAction(session, body, schema) {
6748
+ const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/leads/imports/enrichment`, {
6749
+ method: "POST",
6750
+ headers: {
6751
+ "Content-Type": "application/json",
6752
+ Authorization: `Bearer ${currentSession.accessToken}`
6753
+ },
6754
+ body: JSON.stringify(body)
6755
+ }), schema);
6756
+ return value;
6757
+ }
6758
+ async function getCliImportCompanyCandidatesViaApp(session, input) {
6759
+ return runCliImportEnrichmentAction(session, { action: "company-candidates", ...input }, CliImportCompanyCandidatesResponseSchema);
6760
+ }
6761
+ async function saveCliImportCompanyProfilesViaApp(session, profiles) {
6762
+ return runCliImportEnrichmentAction(session, { action: "save-companies", profiles }, CliImportCompanySaveResponseSchema);
6763
+ }
6764
+ async function runCliImportHunterBatchViaApp(session, input) {
6765
+ return runCliImportEnrichmentAction(session, { action: "hunter", ...input }, CliImportHunterBatchResponseSchema);
6766
+ }
6767
+ async function getCliImportEnrichmentStatusViaApp(session, runIds) {
6768
+ return runCliImportEnrichmentAction(session, { action: "status", runIds }, CliImportEnrichmentStatusSchema);
6769
+ }
6699
6770
  async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
6700
6771
  const url = new URL('/api/cli/linkedin-companies/status', session.apiBaseUrl);
6701
6772
  url.searchParams.set('clientId', String(payload.clientId));
@@ -6726,6 +6797,16 @@ class LocalSalesNavigatorRateLimitError extends Error {
6726
6797
  this.retryAfterMs = retryAfterMs;
6727
6798
  }
6728
6799
  }
6800
+ class LocalSalesNavigatorHttpError extends Error {
6801
+ status;
6802
+ body;
6803
+ constructor(status, body, message) {
6804
+ super(message);
6805
+ this.name = "LocalSalesNavigatorHttpError";
6806
+ this.status = status;
6807
+ this.body = body;
6808
+ }
6809
+ }
6729
6810
  function stripShellQuotes(value) {
6730
6811
  const trimmed = value.trim();
6731
6812
  if (trimmed.length >= 2) {
@@ -7755,6 +7836,8 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
7755
7836
  status: "task",
7756
7837
  requestId: pending.requestId,
7757
7838
  url: pending.url,
7839
+ headers: pending.headers,
7840
+ requiredHeaders: pending.requiredHeaders,
7758
7841
  }
7759
7842
  : { status: "idle" });
7760
7843
  return;
@@ -7793,7 +7876,10 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
7793
7876
  return;
7794
7877
  }
7795
7878
  if (!Number.isFinite(status) || status < 200 || status >= 300) {
7796
- current.reject(new Error(`Sales Navigator browser relay failed with HTTP ${status}: ${bodyPreview}`));
7879
+ const recovery = status === 401 || status === 403
7880
+ ? " Refresh the Sales Navigator search, capture the current csrf-token from a native Lead Search request, and replay this task with the supplied headers."
7881
+ : "";
7882
+ current.reject(new LocalSalesNavigatorHttpError(status, body, `Sales Navigator browser relay failed with HTTP ${status}: ${bodyPreview}.${recovery}`));
7797
7883
  writeLocalAccountSearchRelayJson(response, 200, {
7798
7884
  status: "failed",
7799
7885
  });
@@ -7828,21 +7914,36 @@ async function createLocalAccountSearchBrowserRelay(requestedPort) {
7828
7914
  }
7829
7915
  return {
7830
7916
  port: address.port,
7831
- request(url) {
7917
+ request(request) {
7832
7918
  if (pending) {
7833
7919
  throw new Error("Account Search browser relay already has a pending task.");
7834
7920
  }
7835
- const parsedUrl = new URL(url);
7921
+ const parsedUrl = new URL(request.url);
7836
7922
  if (!/(^|\.)linkedin\.com$/i.test(parsedUrl.hostname) ||
7837
7923
  !(parsedUrl.pathname.includes("/sales-api/salesApiAccountSearch") ||
7838
- parsedUrl.pathname.includes("/sales-api/salesApiLeadSearch"))) {
7839
- throw new Error("Browser relay only accepts LinkedIn Account or Lead Search API URLs.");
7924
+ parsedUrl.pathname.includes("/sales-api/salesApiLeadSearch") ||
7925
+ parsedUrl.pathname.includes("/sales-api/salesApiCompanies"))) {
7926
+ throw new Error("Browser relay only accepts LinkedIn Account Search, Lead Search, or company-detail API URLs.");
7840
7927
  }
7928
+ const headers = Object.fromEntries(Object.entries(request.headers)
7929
+ .map(([name, value]) => [name.toLowerCase(), value])
7930
+ .filter(([name]) => ![
7931
+ "cookie",
7932
+ "host",
7933
+ "origin",
7934
+ "referer",
7935
+ "content-length",
7936
+ "user-agent",
7937
+ ].includes(name)));
7938
+ headers.accept ??= "*/*";
7939
+ headers["x-restli-protocol-version"] ??= "2.0.0";
7841
7940
  requestSequence += 1;
7842
7941
  return new Promise((resolve, reject) => {
7843
7942
  pending = {
7844
7943
  requestId: `sales-nav-search-${requestSequence}`,
7845
- url,
7944
+ url: request.url,
7945
+ headers,
7946
+ requiredHeaders: ["csrf-token"],
7846
7947
  resolve,
7847
7948
  reject,
7848
7949
  };
@@ -13281,7 +13382,7 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
13281
13382
  retryMaxDelayMs: 10_000
13282
13383
  },
13283
13384
  executeRequest: browserRelay
13284
- ? (request) => browserRelay.request(request.url)
13385
+ ? (request) => browserRelay.request(request)
13285
13386
  : undefined
13286
13387
  });
13287
13388
  }
@@ -13303,6 +13404,14 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
13303
13404
  dryRun: false,
13304
13405
  collected: collected.people.length,
13305
13406
  totalResults: collected.totalResults,
13407
+ collectionComplete: collected.totalResults != null &&
13408
+ collected.people.length >= collected.totalResults,
13409
+ truncated: collected.totalResults != null &&
13410
+ collected.totalResults > collected.people.length &&
13411
+ collected.people.length >= maxResults,
13412
+ remainingResults: collected.totalResults == null
13413
+ ? null
13414
+ : Math.max(0, collected.totalResults - collected.people.length),
13306
13415
  fetchedPages: collected.fetchedPages,
13307
13416
  runId: imported.runId,
13308
13417
  imported: imported.imported,
@@ -13314,6 +13423,657 @@ async function runSalesNavigatorPeopleCollectCommand(options) {
13314
13423
  await writeJsonFile(options.out, payload);
13315
13424
  return payload;
13316
13425
  }
13426
+ class CliImportCompanyRateLimitError extends Error {
13427
+ constructor(message) {
13428
+ super(message);
13429
+ this.name = "CliImportCompanyRateLimitError";
13430
+ }
13431
+ }
13432
+ function collectRunId(value, previous) {
13433
+ return [...previous, ...value.split(",")]
13434
+ .map((entry) => entry.trim())
13435
+ .filter(Boolean);
13436
+ }
13437
+ function normalizeCompanyWebsite(value) {
13438
+ const trimmed = value?.trim() ?? "";
13439
+ if (!trimmed)
13440
+ return null;
13441
+ try {
13442
+ return new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`).toString();
13443
+ }
13444
+ catch {
13445
+ return null;
13446
+ }
13447
+ }
13448
+ const CLI_IMPORT_COMPANY_DECORATION = "(entityUrn,name,employeeCount,employeeDisplayCount,employeeCountRange,account(saved,noteCount,listCount,crmStatus,starred),pictureInfo,companyPictureDisplayImage,companyBackgroundCoverImage,description,industry,location,headquarters,website,revenueRange,crmOpportunities,flagshipCompanyUrl,employeeGrowthPercentages,employees*~fs_salesProfile(entityUrn,firstName,lastName,fullName,pictureInfo,profilePictureDisplayImage),specialties,type,yearFounded)";
13449
+ class CliImportCompanySessionError extends Error {
13450
+ constructor(message) {
13451
+ super(message);
13452
+ this.name = "CliImportCompanySessionError";
13453
+ }
13454
+ }
13455
+ function buildCliImportAccountSearchUrl(companyName) {
13456
+ const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
13457
+ "https://www.linkedin.com";
13458
+ return (`${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiAccountSearch` +
13459
+ `?q=searchQuery&query=(spellCorrectionEnabled:true,keywords:${encodeLinkedInRestliValue(companyName)})` +
13460
+ "&start=0&count=25" +
13461
+ "&decorationId=com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-4");
13462
+ }
13463
+ function encodeLinkedInRestliValue(value) {
13464
+ return encodeURIComponent(value)
13465
+ .replace(/\(/g, "%28")
13466
+ .replace(/\)/g, "%29");
13467
+ }
13468
+ function buildCliImportCompanyDetailsUrl(companyIds) {
13469
+ const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
13470
+ "https://www.linkedin.com";
13471
+ return (`${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiCompanies` +
13472
+ `?ids=List(${companyIds.map((companyId) => encodeURIComponent(companyId)).join(",")})` +
13473
+ `&decoration=${encodeLinkedInRestliValue(CLI_IMPORT_COMPANY_DECORATION)}`);
13474
+ }
13475
+ function buildCliImportCompanyHeaders(config, companyId) {
13476
+ return {
13477
+ ...buildLinkedInSalesNavigatorAccountSearchHeaders(config),
13478
+ referer: companyId
13479
+ ? `https://www.linkedin.com/sales/company/${companyId}`
13480
+ : "https://www.linkedin.com/sales/search/company",
13481
+ };
13482
+ }
13483
+ async function fetchCliImportSalesNavigatorJson(params) {
13484
+ if (params.browserRelay) {
13485
+ try {
13486
+ const result = await params.browserRelay.request({
13487
+ url: params.url,
13488
+ headers: params.config
13489
+ ? buildCliImportCompanyHeaders(params.config, params.companyId)
13490
+ : {},
13491
+ });
13492
+ const body = cliImportCompanyRecord(result.body);
13493
+ return { status: 200, body, finalUrl: params.url };
13494
+ }
13495
+ catch (error) {
13496
+ if (error instanceof LocalSalesNavigatorRateLimitError) {
13497
+ throw new CliImportCompanyRateLimitError(error.message);
13498
+ }
13499
+ if (error instanceof LocalSalesNavigatorHttpError) {
13500
+ if (error.status === 401 || error.status === 403) {
13501
+ throw new CliImportCompanySessionError(error.message);
13502
+ }
13503
+ return {
13504
+ status: error.status,
13505
+ body: cliImportCompanyRecord(error.body),
13506
+ finalUrl: params.url,
13507
+ };
13508
+ }
13509
+ throw error;
13510
+ }
13511
+ }
13512
+ if (!params.config) {
13513
+ throw new Error("Company enrichment requires a LinkedIn session or --browser-relay-port.");
13514
+ }
13515
+ const controller = new AbortController();
13516
+ const timeout = setTimeout(() => controller.abort(), params.timeoutMs);
13517
+ try {
13518
+ const response = await fetch(params.url, {
13519
+ method: "GET",
13520
+ signal: controller.signal,
13521
+ headers: buildCliImportCompanyHeaders(params.config, params.companyId),
13522
+ });
13523
+ const finalUrl = response.url || params.url;
13524
+ if (response.status === 429 || response.status === 999) {
13525
+ throw new CliImportCompanyRateLimitError(`LinkedIn rate limited ${params.label} (${response.status}). Resume the same command after cooldown.`);
13526
+ }
13527
+ if (response.status === 401 ||
13528
+ response.status === 403 ||
13529
+ /\/uas\/login|\/checkpoint\/challenge|\/authwall/i.test(finalUrl)) {
13530
+ throw new CliImportCompanySessionError(`LinkedIn rejected ${params.label}. Refresh the Sales Navigator session, then rerun the same command.`);
13531
+ }
13532
+ const text = await response.text();
13533
+ if (/sign in to linkedin|authwall|checkpoint\/challenge/i.test(text)) {
13534
+ throw new CliImportCompanySessionError(`LinkedIn redirected ${params.label} to authentication. Refresh the Sales Navigator session, then rerun the same command.`);
13535
+ }
13536
+ let body = null;
13537
+ if (text.trim()) {
13538
+ try {
13539
+ const parsed = JSON.parse(text);
13540
+ body =
13541
+ typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
13542
+ ? parsed
13543
+ : null;
13544
+ }
13545
+ catch {
13546
+ if (response.ok) {
13547
+ throw new Error(`${params.label} returned a non-JSON response (${response.status}).`);
13548
+ }
13549
+ }
13550
+ }
13551
+ return { status: response.status, body, finalUrl };
13552
+ }
13553
+ catch (error) {
13554
+ if (error instanceof CliImportCompanyRateLimitError ||
13555
+ error instanceof CliImportCompanySessionError) {
13556
+ throw error;
13557
+ }
13558
+ if (error instanceof Error && error.name === "AbortError") {
13559
+ throw new Error(`${params.label} timed out after ${params.timeoutMs}ms.`);
13560
+ }
13561
+ throw error;
13562
+ }
13563
+ finally {
13564
+ clearTimeout(timeout);
13565
+ }
13566
+ }
13567
+ function exactCliImportAccountMatch(candidate, accounts) {
13568
+ const sourceName = candidate.companyName ?? "";
13569
+ const expectedNames = new Set([sourceName, aggressivelyCleanLookupCompanyName(sourceName)]
13570
+ .map((value) => normalizeLooseMatchText(value))
13571
+ .filter(Boolean));
13572
+ if (expectedNames.size === 0)
13573
+ return null;
13574
+ return (accounts.find((account) => {
13575
+ const companyName = typeof account.companyName === "string" ? account.companyName : "";
13576
+ const normalizedNames = [
13577
+ companyName,
13578
+ aggressivelyCleanLookupCompanyName(companyName),
13579
+ ].map((value) => normalizeLooseMatchText(value));
13580
+ return normalizedNames.some((value) => value && expectedNames.has(value));
13581
+ }) ?? null);
13582
+ }
13583
+ async function resolveCliImportCompanyIdentity(candidate, config, timeoutMs, browserRelay) {
13584
+ let resolvedCompanyId = candidate.companyId && /^\d+$/.test(candidate.companyId)
13585
+ ? candidate.companyId
13586
+ : null;
13587
+ let salesNavCompanyUrl = candidate.salesNavCompanyUrl;
13588
+ let linkedinCompanyUrl = candidate.linkedinCompanyUrl;
13589
+ let matchedCompanyName = null;
13590
+ let accountSearchUsed = false;
13591
+ let account = null;
13592
+ if (!resolvedCompanyId && candidate.companyName) {
13593
+ accountSearchUsed = true;
13594
+ const queryUrl = buildCliImportAccountSearchUrl(candidate.companyName);
13595
+ const response = await fetchCliImportSalesNavigatorJson({
13596
+ url: queryUrl,
13597
+ config,
13598
+ browserRelay,
13599
+ timeoutMs,
13600
+ label: `Sales Navigator Account Search for ${candidate.companyName}`,
13601
+ });
13602
+ if (response.status < 200 || response.status >= 300) {
13603
+ throw new Error(`Sales Navigator Account Search for ${candidate.companyName} returned ${response.status}.`);
13604
+ }
13605
+ if (response.status >= 200 && response.status < 300 && response.body) {
13606
+ const accounts = extractLocalSalesNavigatorElements(response.body)
13607
+ .map((element) => typeof element === "object" && element !== null && !Array.isArray(element)
13608
+ ? normalizeLocalSalesNavigatorAccount(element, queryUrl)
13609
+ : null)
13610
+ .filter((value) => value !== null);
13611
+ account = exactCliImportAccountMatch(candidate, accounts);
13612
+ }
13613
+ salesNavCompanyUrl =
13614
+ (typeof account?.salesNavCompanyUrl === "string"
13615
+ ? account.salesNavCompanyUrl
13616
+ : null) ?? salesNavCompanyUrl;
13617
+ linkedinCompanyUrl =
13618
+ (typeof account?.linkedinCompanyUrl === "string"
13619
+ ? account.linkedinCompanyUrl
13620
+ : null) ?? linkedinCompanyUrl;
13621
+ matchedCompanyName =
13622
+ typeof account?.companyName === "string" ? account.companyName : null;
13623
+ resolvedCompanyId =
13624
+ (typeof account?.companyId === "string" && /^\d+$/.test(account.companyId)
13625
+ ? account.companyId
13626
+ : null) ??
13627
+ extractLinkedInCompanyIdFromUrl(salesNavCompanyUrl) ??
13628
+ extractLinkedInCompanyIdFromUrl(linkedinCompanyUrl) ??
13629
+ null;
13630
+ }
13631
+ if (!linkedinCompanyUrl && resolvedCompanyId) {
13632
+ linkedinCompanyUrl = normalizeLinkedInCompanyPage(resolvedCompanyId);
13633
+ }
13634
+ if (!salesNavCompanyUrl && resolvedCompanyId) {
13635
+ salesNavCompanyUrl = `https://www.linkedin.com/sales/company/${resolvedCompanyId}`;
13636
+ }
13637
+ return {
13638
+ candidate,
13639
+ resolvedCompanyId,
13640
+ salesNavCompanyUrl,
13641
+ linkedinCompanyUrl,
13642
+ matchedCompanyName,
13643
+ accountSearchUsed,
13644
+ account,
13645
+ };
13646
+ }
13647
+ function cliImportCompanyProfileCommon(resolution) {
13648
+ return {
13649
+ companyKey: resolution.candidate.companyKey,
13650
+ sourceCompanyId: resolution.candidate.companyId,
13651
+ resolvedCompanyId: resolution.resolvedCompanyId,
13652
+ sourceCompanyName: resolution.candidate.companyName,
13653
+ companyName: resolution.matchedCompanyName ?? resolution.candidate.companyName,
13654
+ salesNavCompanyUrl: resolution.salesNavCompanyUrl,
13655
+ linkedinCompanyUrl: resolution.linkedinCompanyUrl,
13656
+ enrichmentSource: resolution.accountSearchUsed
13657
+ ? "sales_api_account_search+sales_api_company_detail"
13658
+ : "sales_api_company_detail",
13659
+ };
13660
+ }
13661
+ function formatCliImportCompanyLocation(value) {
13662
+ if (typeof value === "string")
13663
+ return normalizeLookupWhitespace(value) || null;
13664
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
13665
+ return null;
13666
+ }
13667
+ const record = value;
13668
+ const orderedKeys = [
13669
+ "line1",
13670
+ "line2",
13671
+ "city",
13672
+ "geographicArea",
13673
+ "postalCode",
13674
+ "country",
13675
+ ];
13676
+ return (orderedKeys
13677
+ .map((key) => typeof record[key] === "string"
13678
+ ? normalizeLookupWhitespace(record[key])
13679
+ : "")
13680
+ .filter(Boolean)
13681
+ .join(", ") || null);
13682
+ }
13683
+ function normalizeCliImportCompanyDomain(website) {
13684
+ if (!website)
13685
+ return null;
13686
+ try {
13687
+ const hostname = new URL(website).hostname.replace(/^www\./i, "").toLowerCase();
13688
+ return hostname && !/(^|\.)linkedin\.com$/i.test(hostname) ? hostname : null;
13689
+ }
13690
+ catch {
13691
+ return null;
13692
+ }
13693
+ }
13694
+ function compactCliImportCompanyString(value) {
13695
+ return typeof value === "string" ? normalizeLookupWhitespace(value) || null : null;
13696
+ }
13697
+ function cliImportCompanyRecord(value) {
13698
+ return typeof value === "object" && value !== null && !Array.isArray(value)
13699
+ ? value
13700
+ : null;
13701
+ }
13702
+ function buildCliImportCompanyProfile(resolution, detail, input) {
13703
+ const common = cliImportCompanyProfileCommon(resolution);
13704
+ if (!detail) {
13705
+ return {
13706
+ ...common,
13707
+ status: input.status === 404 || input.status === 410 ? "not_found" : "failed",
13708
+ error: input.error ??
13709
+ `Sales Navigator company detail returned ${input.status || "no result"}`,
13710
+ rawPayload: {
13711
+ accountSearchUsed: resolution.accountSearchUsed,
13712
+ httpStatus: input.status,
13713
+ },
13714
+ };
13715
+ }
13716
+ const accountRaw = cliImportCompanyRecord(resolution.account?.rawLocalSalesNavigatorAccountResult);
13717
+ const accountWebsite = compactCliImportCompanyString(resolution.account?.website);
13718
+ const website = normalizeCompanyWebsite(compactCliImportCompanyString(detail.website) ?? accountWebsite ?? undefined);
13719
+ const specialties = Array.isArray(detail.specialties)
13720
+ ? detail.specialties
13721
+ .filter((value) => typeof value === "string")
13722
+ .map((value) => normalizeLookupWhitespace(value))
13723
+ .filter(Boolean)
13724
+ .join(", ") || null
13725
+ : compactCliImportCompanyString(detail.specialties);
13726
+ const foundedCandidate = detail.yearFounded ?? detail.foundedYear;
13727
+ const founded = typeof foundedCandidate === "number" && Number.isInteger(foundedCandidate)
13728
+ ? foundedCandidate
13729
+ : typeof foundedCandidate === "string" && /^\d{4}$/.test(foundedCandidate)
13730
+ ? Number(foundedCandidate)
13731
+ : null;
13732
+ const revenueRange = cliImportCompanyRecord(detail.revenueRange);
13733
+ const employeeGrowthPercentages = Array.isArray(detail.employeeGrowthPercentages)
13734
+ ? detail.employeeGrowthPercentages.filter((value) => typeof value === "object" && value !== null && !Array.isArray(value))
13735
+ : [];
13736
+ const flagshipCompanyUrl = normalizeCompanyWebsite(compactCliImportCompanyString(detail.flagshipCompanyUrl) ?? undefined);
13737
+ const linkedinCompanyUrl = flagshipCompanyUrl && /(^|\.)linkedin\.com$/i.test(new URL(flagshipCompanyUrl).hostname)
13738
+ ? flagshipCompanyUrl
13739
+ : resolution.linkedinCompanyUrl;
13740
+ const accountEmployeeCount = typeof resolution.account?.employeeCount === "number"
13741
+ ? resolution.account.employeeCount
13742
+ : typeof detail.employeeDisplayCount === "number"
13743
+ ? Math.max(0, Math.trunc(detail.employeeDisplayCount))
13744
+ : typeof detail.employeeCount === "number"
13745
+ ? Math.max(0, Math.trunc(detail.employeeCount))
13746
+ : null;
13747
+ const followerCount = typeof resolution.account?.followerCount === "number"
13748
+ ? resolution.account.followerCount
13749
+ : null;
13750
+ return {
13751
+ ...common,
13752
+ companyName: compactCliImportCompanyString(detail.name) ?? common.companyName,
13753
+ linkedinCompanyUrl,
13754
+ website,
13755
+ domain: normalizeCliImportCompanyDomain(website),
13756
+ industry: compactCliImportCompanyString(detail.industry) ??
13757
+ compactCliImportCompanyString(resolution.account?.industry),
13758
+ companySize: extractFirstNestedStringByKeys(accountRaw, [
13759
+ "employeeCountRange",
13760
+ "companySize",
13761
+ ]) ?? compactCliImportCompanyString(detail.employeeCountRange),
13762
+ companyType: compactCliImportCompanyString(detail.type),
13763
+ location: formatCliImportCompanyLocation(detail.location) ??
13764
+ compactCliImportCompanyString(resolution.account?.location),
13765
+ headquarters: formatCliImportCompanyLocation(detail.headquarters) ??
13766
+ compactCliImportCompanyString(resolution.account?.headquarters),
13767
+ employeesOnLinkedIn: accountEmployeeCount,
13768
+ followerCount,
13769
+ description: compactCliImportCompanyString(detail.description) ??
13770
+ compactCliImportCompanyString(resolution.account?.description),
13771
+ tagline: null,
13772
+ specialties,
13773
+ founded,
13774
+ revenueRange,
13775
+ employeeGrowthPercentages,
13776
+ logoUrl: buildLinkedInImageUrl(detail.companyPictureDisplayImage) ??
13777
+ buildLinkedInImageUrl(detail.pictureInfo),
13778
+ bannerUrl: buildLinkedInImageUrl(detail.companyBackgroundCoverImage),
13779
+ flagshipCompanyUrl,
13780
+ status: "enriched",
13781
+ error: null,
13782
+ rawPayload: {
13783
+ accountSearchUsed: resolution.accountSearchUsed,
13784
+ httpStatus: input.status,
13785
+ detail,
13786
+ accountSearchResult: accountRaw,
13787
+ },
13788
+ };
13789
+ }
13790
+ async function runSalesNavigatorImportEnrichmentCommand(options) {
13791
+ if (options.companiesOnly && options.hunterOnly) {
13792
+ throw new Error("Use either --companies-only or --hunter-only, not both.");
13793
+ }
13794
+ const runIds = Array.from(new Set(options.runId.map((runId) => z.string().uuid().parse(runId))));
13795
+ const companyTimeoutMs = z.coerce.number().int().min(1000).max(120000).parse(options.companyTimeoutMs);
13796
+ const companyDelayMinMs = z.coerce.number().int().min(0).parse(options.companyDelayMinMs);
13797
+ const companyDelayMaxMs = z.coerce.number().int().min(companyDelayMinMs).parse(options.companyDelayMaxMs);
13798
+ const browserRelayPort = options.browserRelayPort == null
13799
+ ? null
13800
+ : z.coerce.number().int().min(0).max(65535).parse(options.browserRelayPort);
13801
+ const maxCompanies = z.coerce.number().int().min(0).max(1000).parse(options.maxCompanies);
13802
+ const companySaveBatchSize = z.coerce.number().int().min(1).max(200).parse(options.companySaveBatchSize);
13803
+ const hunterBatchSize = z.coerce.number().int().min(1).max(150).parse(options.hunterBatchSize);
13804
+ const maxHunterBatches = z.coerce.number().int().min(0).max(100000).parse(options.maxHunterBatches);
13805
+ const session = await requireAuthSession();
13806
+ const startedAt = new Date().toISOString();
13807
+ const initialStatus = await getCliImportEnrichmentStatusViaApp(session, runIds);
13808
+ if (options.dryRun) {
13809
+ const companyCandidates = options.hunterOnly
13810
+ ? []
13811
+ : (await getCliImportCompanyCandidatesViaApp(session, {
13812
+ runIds,
13813
+ limit: 1000,
13814
+ retry: options.retryCompanies
13815
+ })).companies.slice(0, maxCompanies || undefined);
13816
+ return {
13817
+ status: "ok",
13818
+ dryRun: true,
13819
+ runIds,
13820
+ initial: initialStatus,
13821
+ companyCandidates: companyCandidates.length,
13822
+ next: options.companiesOnly
13823
+ ? "company enrichment only"
13824
+ : options.hunterOnly
13825
+ ? "Hunter enrichment only"
13826
+ : "company profiles, verified domains, then Hunter"
13827
+ };
13828
+ }
13829
+ let companies = {
13830
+ selected: 0,
13831
+ saved: 0,
13832
+ enriched: 0,
13833
+ domains: 0,
13834
+ notFound: 0,
13835
+ failed: 0
13836
+ };
13837
+ if (!options.hunterOnly) {
13838
+ const companyCandidates = (await getCliImportCompanyCandidatesViaApp(session, {
13839
+ runIds,
13840
+ limit: 1000,
13841
+ retry: options.retryCompanies
13842
+ })).companies.slice(0, maxCompanies || undefined);
13843
+ companies.selected = companyCandidates.length;
13844
+ if (companyCandidates.length > 0) {
13845
+ const browserRelay = browserRelayPort == null
13846
+ ? null
13847
+ : await createLocalAccountSearchBrowserRelay(browserRelayPort);
13848
+ try {
13849
+ const config = browserRelay ? null : await readLinkedInDirectLookupConfig();
13850
+ let pendingProfiles = [];
13851
+ const flushCompanyProfiles = async () => {
13852
+ while (pendingProfiles.length > 0) {
13853
+ const batch = pendingProfiles.splice(0, companySaveBatchSize);
13854
+ const saved = await saveCliImportCompanyProfilesViaApp(session, batch);
13855
+ companies = {
13856
+ ...companies,
13857
+ saved: companies.saved + saved.saved,
13858
+ enriched: companies.enriched + saved.enriched,
13859
+ domains: companies.domains + saved.domains,
13860
+ notFound: companies.notFound + saved.notFound,
13861
+ failed: companies.failed + saved.failed
13862
+ };
13863
+ writeProgress(`Company enrichment: ${companies.saved}/${companyCandidates.length} saved, ${companies.domains} domains.`);
13864
+ }
13865
+ };
13866
+ const detailCache = new Map();
13867
+ const enrichResolvedCompanies = async (resolutions) => {
13868
+ const resolutionBatchSize = 20;
13869
+ for (let offset = 0; offset < resolutions.length; offset += resolutionBatchSize) {
13870
+ const resolutionBatch = resolutions.slice(offset, offset + resolutionBatchSize);
13871
+ const ids = Array.from(new Set(resolutionBatch
13872
+ .map((resolution) => resolution.resolvedCompanyId)
13873
+ .filter((value) => Boolean(value)))).filter((companyId) => !detailCache.has(companyId));
13874
+ if (ids.length > 0) {
13875
+ const response = await fetchCliImportSalesNavigatorJson({
13876
+ url: buildCliImportCompanyDetailsUrl(ids),
13877
+ config,
13878
+ browserRelay,
13879
+ timeoutMs: companyTimeoutMs,
13880
+ label: `Sales Navigator company details ${companies.saved + 1}-${companies.saved + resolutionBatch.length}`,
13881
+ companyId: ids[0] ?? null,
13882
+ });
13883
+ const results = cliImportCompanyRecord(response.body?.results) ?? {};
13884
+ const errors = cliImportCompanyRecord(response.body?.errors) ?? {};
13885
+ const statuses = cliImportCompanyRecord(response.body?.statuses) ?? {};
13886
+ for (const companyId of ids) {
13887
+ const detail = cliImportCompanyRecord(results[companyId]);
13888
+ const statusValue = statuses[companyId];
13889
+ const status = typeof statusValue === "number"
13890
+ ? statusValue
13891
+ : typeof statusValue === "string" && /^\d+$/.test(statusValue)
13892
+ ? Number(statusValue)
13893
+ : response.status;
13894
+ const errorValue = errors[companyId];
13895
+ detailCache.set(companyId, {
13896
+ detail,
13897
+ status,
13898
+ error: errorValue
13899
+ ? JSON.stringify(errorValue).slice(0, 3000)
13900
+ : response.status >= 200 && response.status < 300
13901
+ ? null
13902
+ : `Sales Navigator company details returned ${response.status}`,
13903
+ });
13904
+ }
13905
+ }
13906
+ for (const resolution of resolutionBatch) {
13907
+ const cached = detailCache.get(resolution.resolvedCompanyId ?? "");
13908
+ pendingProfiles.push(buildCliImportCompanyProfile(resolution, cached?.detail ?? null, {
13909
+ status: cached?.status ?? 500,
13910
+ error: cached?.error ?? "Sales Navigator returned no company detail",
13911
+ }));
13912
+ }
13913
+ await flushCompanyProfiles();
13914
+ if (offset + resolutionBatchSize < resolutions.length &&
13915
+ companyDelayMaxMs > 0) {
13916
+ await delay(randomIntegerBetween(companyDelayMinMs, companyDelayMaxMs));
13917
+ }
13918
+ }
13919
+ };
13920
+ const knownCandidates = companyCandidates.filter((candidate) => candidate.companyId && /^\d+$/.test(candidate.companyId));
13921
+ const knownResolutions = await Promise.all(knownCandidates.map((candidate) => resolveCliImportCompanyIdentity(candidate, config, companyTimeoutMs, browserRelay)));
13922
+ await enrichResolvedCompanies(knownResolutions);
13923
+ const knownByAlias = new Map();
13924
+ for (const candidate of knownCandidates) {
13925
+ for (const alias of [
13926
+ candidate.companyName ?? "",
13927
+ aggressivelyCleanLookupCompanyName(candidate.companyName ?? ""),
13928
+ ]) {
13929
+ const key = normalizeLooseMatchText(alias);
13930
+ if (!key)
13931
+ continue;
13932
+ const entries = knownByAlias.get(key) ?? [];
13933
+ entries.push(candidate);
13934
+ knownByAlias.set(key, entries);
13935
+ }
13936
+ }
13937
+ const unresolvedCandidates = [];
13938
+ const localAliasResolutions = [];
13939
+ for (const candidate of companyCandidates) {
13940
+ if (candidate.companyId && /^\d+$/.test(candidate.companyId))
13941
+ continue;
13942
+ const matches = new Map();
13943
+ for (const alias of [
13944
+ candidate.companyName ?? "",
13945
+ aggressivelyCleanLookupCompanyName(candidate.companyName ?? ""),
13946
+ ]) {
13947
+ const key = normalizeLooseMatchText(alias);
13948
+ for (const match of knownByAlias.get(key) ?? []) {
13949
+ if (match.companyId)
13950
+ matches.set(match.companyId, match);
13951
+ }
13952
+ }
13953
+ if (matches.size === 1) {
13954
+ const match = Array.from(matches.values())[0];
13955
+ localAliasResolutions.push({
13956
+ candidate,
13957
+ resolvedCompanyId: match.companyId,
13958
+ salesNavCompanyUrl: match.salesNavCompanyUrl ??
13959
+ `https://www.linkedin.com/sales/company/${match.companyId}`,
13960
+ linkedinCompanyUrl: match.linkedinCompanyUrl ??
13961
+ normalizeLinkedInCompanyPage(match.companyId),
13962
+ matchedCompanyName: match.companyName,
13963
+ accountSearchUsed: false,
13964
+ account: null,
13965
+ });
13966
+ }
13967
+ else {
13968
+ unresolvedCandidates.push(candidate);
13969
+ }
13970
+ }
13971
+ await enrichResolvedCompanies(localAliasResolutions);
13972
+ writeProgress(`Company identities: ${knownResolutions.length} imported IDs, ${localAliasResolutions.length} local aliases, ${unresolvedCandidates.length} Account Search candidates.`);
13973
+ const accountSearchGroupSize = 5;
13974
+ let accountSearches = 0;
13975
+ for (let offset = 0; offset < unresolvedCandidates.length; offset += accountSearchGroupSize) {
13976
+ const group = unresolvedCandidates.slice(offset, offset + accountSearchGroupSize);
13977
+ const resolvedGroup = [];
13978
+ for (let index = 0; index < group.length; index += 1) {
13979
+ const candidate = group[index];
13980
+ let resolution = null;
13981
+ try {
13982
+ resolution = await resolveCliImportCompanyIdentity(candidate, config, companyTimeoutMs, browserRelay);
13983
+ }
13984
+ catch (error) {
13985
+ if (error instanceof CliImportCompanyRateLimitError ||
13986
+ error instanceof CliImportCompanySessionError) {
13987
+ throw error;
13988
+ }
13989
+ pendingProfiles.push(buildCliImportCompanyProfile({
13990
+ candidate,
13991
+ resolvedCompanyId: null,
13992
+ salesNavCompanyUrl: candidate.salesNavCompanyUrl,
13993
+ linkedinCompanyUrl: candidate.linkedinCompanyUrl,
13994
+ matchedCompanyName: null,
13995
+ accountSearchUsed: true,
13996
+ account: null,
13997
+ }, null, {
13998
+ status: 500,
13999
+ error: error instanceof Error
14000
+ ? error.message
14001
+ : String(error),
14002
+ }));
14003
+ }
14004
+ accountSearches += 1;
14005
+ if (resolution?.resolvedCompanyId) {
14006
+ resolvedGroup.push(resolution);
14007
+ }
14008
+ else if (resolution) {
14009
+ pendingProfiles.push(buildCliImportCompanyProfile(resolution, null, {
14010
+ status: 404,
14011
+ error: "No exact LinkedIn company was found by Account Search",
14012
+ }));
14013
+ }
14014
+ if (index < group.length - 1 &&
14015
+ companyDelayMaxMs > 0) {
14016
+ await delay(randomIntegerBetween(companyDelayMinMs, companyDelayMaxMs));
14017
+ }
14018
+ }
14019
+ await flushCompanyProfiles();
14020
+ await enrichResolvedCompanies(resolvedGroup);
14021
+ writeProgress(`Account Search: ${Math.min(offset + group.length, unresolvedCandidates.length)}/${unresolvedCandidates.length} checked (${accountSearches} requests).`);
14022
+ }
14023
+ }
14024
+ finally {
14025
+ await browserRelay?.close();
14026
+ }
14027
+ }
14028
+ }
14029
+ let hunter = {
14030
+ batches: 0,
14031
+ selected: 0,
14032
+ processed: 0,
14033
+ found: 0,
14034
+ notFound: 0,
14035
+ failed: 0
14036
+ };
14037
+ if (!options.companiesOnly) {
14038
+ const retryBefore = options.retryEmails ? startedAt : null;
14039
+ while (maxHunterBatches === 0 || hunter.batches < maxHunterBatches) {
14040
+ const batch = await runCliImportHunterBatchViaApp(session, {
14041
+ runIds,
14042
+ limit: hunterBatchSize,
14043
+ retryBefore
14044
+ });
14045
+ if (batch.selected === 0)
14046
+ break;
14047
+ hunter = {
14048
+ batches: hunter.batches + 1,
14049
+ selected: hunter.selected + batch.selected,
14050
+ processed: hunter.processed + batch.processed,
14051
+ found: hunter.found + batch.found,
14052
+ notFound: hunter.notFound + batch.notFound,
14053
+ failed: hunter.failed + batch.failed
14054
+ };
14055
+ writeProgress(`Hunter enrichment: ${hunter.processed} checked, ${hunter.found} emails found.`);
14056
+ if (batch.halted) {
14057
+ throw new Error(`Hunter enrichment stopped: ${batch.error ?? "provider unavailable"}. Saved progress will be reused.`);
14058
+ }
14059
+ }
14060
+ }
14061
+ const finalStatus = await getCliImportEnrichmentStatusViaApp(session, runIds);
14062
+ const payload = {
14063
+ status: "ok",
14064
+ dryRun: false,
14065
+ runIds,
14066
+ companies,
14067
+ hunter,
14068
+ before: initialStatus,
14069
+ after: finalStatus,
14070
+ outreachStarted: false,
14071
+ workspaceUrl: `${session.apiBaseUrl}/leads/cli-imports?runId=${encodeURIComponent(runIds[0])}`
14072
+ };
14073
+ if (options.out)
14074
+ await writeJsonFile(options.out, payload);
14075
+ return payload;
14076
+ }
13317
14077
  async function runAffiliateLaunchCommand(options) {
13318
14078
  const affiliateLink = z.string().url().parse(options.affiliateLink);
13319
14079
  const linkedInUrl = z.string().url().parse(options.linkedinUrl);
@@ -13359,7 +14119,7 @@ async function runAffiliateLaunchCommand(options) {
13359
14119
  retryMaxDelayMs: 10_000,
13360
14120
  },
13361
14121
  executeRequest: browserRelay
13362
- ? (request) => browserRelay.request(request.url)
14122
+ ? (request) => browserRelay.request(request)
13363
14123
  : undefined,
13364
14124
  });
13365
14125
  }
@@ -13415,6 +14175,28 @@ program
13415
14175
  .action(async (options) => {
13416
14176
  printOutput(await runSalesNavigatorPeopleCollectCommand(options));
13417
14177
  });
14178
+ program
14179
+ .command("salesnav:people:enrich")
14180
+ .alias("leads:enrich-import")
14181
+ .description("Enrich stored CLI imports with company profiles, verified domains, and Hunter emails.")
14182
+ .requiredOption("--run-id <uuid>", "Workspace import run id; repeat the option or pass comma-separated ids", collectRunId, [])
14183
+ .option("--browser-relay-port <number>", "Use the signed-in browser for Account Search and company-detail requests")
14184
+ .option("--company-timeout-ms <number>", "Timeout for each company lookup", "20000")
14185
+ .option("--company-delay-min-ms <number>", "Minimum delay between LinkedIn company lookups", "1500")
14186
+ .option("--company-delay-max-ms <number>", "Maximum delay between LinkedIn company lookups", "3000")
14187
+ .option("--max-companies <number>", "Maximum companies for this invocation; 0 processes all eligible companies", "0")
14188
+ .option("--company-save-batch-size <number>", "Company rows saved per request", "50")
14189
+ .option("--hunter-batch-size <number>", "Hunter contacts checked per request", "100")
14190
+ .option("--max-hunter-batches <number>", "Maximum Hunter batches; 0 continues until no eligible people remain", "0")
14191
+ .option("--companies-only", "Stop after company profiles and domains", false)
14192
+ .option("--hunter-only", "Skip company lookup and use already stored domains", false)
14193
+ .option("--retry-companies", "Retry previously saved company failures", false)
14194
+ .option("--retry-emails", "Retry previously checked unresolved emails once", false)
14195
+ .option("--dry-run", "Show scope without LinkedIn or Hunter requests", false)
14196
+ .option("--out <path>", "Optional JSON result path")
14197
+ .action(async (options) => {
14198
+ printOutput(await runSalesNavigatorImportEnrichmentCommand(options));
14199
+ });
13418
14200
  addAffiliateAudienceOptions(program
13419
14201
  .command("affiliate:launch")
13420
14202
  .description("Build an affiliate audience from an affiliate link and LinkedIn search URL."))
@@ -14464,7 +15246,7 @@ program
14464
15246
  };
14465
15247
  };
14466
15248
  const executeRequest = (request) => browserRelay
14467
- ? browserRelay.request(request.url)
15249
+ ? browserRelay.request(request)
14468
15250
  : fetchLocalSalesNavigatorRequest(request, retry);
14469
15251
  try {
14470
15252
  let requestsThisRun = 0;
@@ -135,8 +135,20 @@ export function parseLinkedInCompanyPage(html, requestUrl) {
135
135
  normalizeWhitespace($("h1").first().text()) ||
136
136
  normalizeWhitespace($('meta[property="og:title"]').attr("content")) ||
137
137
  undefined;
138
- const website = normalizeWhitespace(String(jsonLd?.url ?? "")) ||
139
- normalizeWhitespace(definitions.get("website")) ||
138
+ const jsonLdUrl = normalizeWhitespace(String(jsonLd?.url ?? ""));
139
+ const jsonLdWebsite = (() => {
140
+ if (!jsonLdUrl)
141
+ return undefined;
142
+ try {
143
+ const hostname = new URL(jsonLdUrl).hostname;
144
+ return /(^|\.)linkedin\.com$/i.test(hostname) ? undefined : jsonLdUrl;
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ })();
150
+ const website = normalizeWhitespace(definitions.get("website")) ||
151
+ jsonLdWebsite ||
140
152
  undefined;
141
153
  const description = normalizeWhitespace(String(jsonLd?.description ?? "")) ||
142
154
  normalizeWhitespace($('meta[name="description"]').attr("content")) ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesprompter-cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.51",
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",