salesprompter-cli 0.1.25 → 0.1.27
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 +20 -6
- package/dist/cli.js +1189 -35
- package/dist/hunter-emailfinder.js +252 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -19,11 +19,13 @@ import { buildDeelLeadPoolContactSql, buildDeelOutreachExportSql, buildDeelOutre
|
|
|
19
19
|
import { buildDirectPathLeadExportSql, normalizeDirectPathRows, segmentDirectPathRows } from "./direct-path.js";
|
|
20
20
|
import { AccountLeadProvider, DryRunSyncProvider, HeuristicCompanyProvider, HeuristicEnrichmentProvider, HeuristicPeopleSearchProvider, HeuristicScoringProvider, RoutedSyncProvider } from "./engine.js";
|
|
21
21
|
import { analyzeHistoricalQueries } from "./historical-queries.js";
|
|
22
|
+
import { buildHunterEmailfinderQueueSql, buildHunterEmailfinderTriggerPayload, directEmailEnrichmentInputRowArraySchema, hunterEmailfinderQueueRowArraySchema, parseDirectEmailEnrichmentInput, readHunterEmailfinderConfig, resolveDirectEmailEnrichmentClientId, triggerHunterEmailfinderWorkflow } from "./hunter-emailfinder.js";
|
|
22
23
|
import { buildHistoricalVendorIcp, buildVendorIcp } from "./icp-templates.js";
|
|
23
24
|
import { InstantlySyncProvider } from "./instantly.js";
|
|
24
25
|
import { backfillLinkedInCompanies } from "./linkedin-companies.js";
|
|
26
|
+
import { parseLinkedInCompanyPage } from "./linkedin-companies.js";
|
|
25
27
|
import { crawlLinkedInProductCategory } from "./linkedin-products.js";
|
|
26
|
-
import { claimValidatedSalesNavigatorSessionCookieForCli } from "./linkedin-session.js";
|
|
28
|
+
import { claimValidatedSalesNavigatorSessionCookieForCli, createLinkedInSessionSupabaseClient } from "./linkedin-session.js";
|
|
27
29
|
import { buildLeadlistsFunnelQueries } from "./leadlists-funnel.js";
|
|
28
30
|
import { readJsonFile, splitCsv, writeJsonFile, writeTextFile } from "./io.js";
|
|
29
31
|
import { buildSalesNavigatorCrawlPreview, createSalesNavigatorCrawlSeed, DEFAULT_SALES_NAVIGATOR_CRAWL_DIMENSIONS, buildSalesNavigatorPeopleSlice, deriveSalesNavigatorTitleQuerySeeds, expandSalesNavigatorCrawlAttempt, SalesNavigatorSliceTooBroadError } from "./sales-navigator.js";
|
|
@@ -78,6 +80,16 @@ const LinkedInCompanyBackfillStatusResponseSchema = z.object({
|
|
|
78
80
|
processed: z.boolean(),
|
|
79
81
|
remaining: z.number().int().nonnegative()
|
|
80
82
|
});
|
|
83
|
+
const CliEmailEnrichmentCompaniesResponseSchema = z.object({
|
|
84
|
+
clientId: z.number().int().positive(),
|
|
85
|
+
companies: z.array(z.object({
|
|
86
|
+
companyId: z.string().min(1),
|
|
87
|
+
companyName: z.string().min(1),
|
|
88
|
+
companyNameCleaned: z.string().nullable().optional(),
|
|
89
|
+
domain: z.string().nullable(),
|
|
90
|
+
linkedinCompanyPage: z.string().nullable()
|
|
91
|
+
}))
|
|
92
|
+
});
|
|
81
93
|
const SalesNavigatorLaunchDiagnosticsSchema = z.object({
|
|
82
94
|
orderedCandidateAgentIds: z.array(z.string().min(1)),
|
|
83
95
|
runningAgentIds: z.array(z.string().min(1)),
|
|
@@ -236,8 +248,39 @@ const SalesNavigatorCrawlReportResponseSchema = z.object({
|
|
|
236
248
|
status: z.literal("ok"),
|
|
237
249
|
job: SalesNavigatorCrawlJobSummarySchema
|
|
238
250
|
});
|
|
251
|
+
const cliPacks = [
|
|
252
|
+
{
|
|
253
|
+
slug: "contacts",
|
|
254
|
+
title: "Contacts",
|
|
255
|
+
summary: "Resolve profile URLs and work from pasted contact lists.",
|
|
256
|
+
commands: ["contacts:resolve-profiles", "contacts:resolve-emails"],
|
|
257
|
+
installStatus: "included"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
slug: "research",
|
|
261
|
+
title: "Research",
|
|
262
|
+
summary: "Scrape markets and enrich companies before outreach.",
|
|
263
|
+
commands: ["market:scrape", "companies:enrich"],
|
|
264
|
+
installStatus: "included"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
slug: "discovery",
|
|
268
|
+
title: "Discovery",
|
|
269
|
+
summary: "Find leads from product and market inputs.",
|
|
270
|
+
commands: ["leads:discover", "search:run", "search:status", "search:export", "search:count"],
|
|
271
|
+
installStatus: "included"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
slug: "outreach",
|
|
275
|
+
title: "Outreach",
|
|
276
|
+
summary: "Prepare and sync qualified leads into downstream systems.",
|
|
277
|
+
commands: ["sync:outreach", "sync:crm"],
|
|
278
|
+
installStatus: "included"
|
|
279
|
+
}
|
|
280
|
+
];
|
|
239
281
|
const helpAliasByCommandName = new Map([
|
|
240
282
|
["contacts:find-linkedin-urls", "contacts:resolve-profiles"],
|
|
283
|
+
["contacts:process-emails", "contacts:resolve-emails"],
|
|
241
284
|
["linkedin-companies:backfill", "companies:enrich"],
|
|
242
285
|
["linkedin-products:scrape", "market:scrape"],
|
|
243
286
|
["salesnav:from-product-category", "leads:discover"],
|
|
@@ -247,11 +290,17 @@ const helpAliasByCommandName = new Map([
|
|
|
247
290
|
["salesnav:count", "search:count"]
|
|
248
291
|
]);
|
|
249
292
|
const helpVisibleCommandNames = new Set([
|
|
293
|
+
"setup",
|
|
294
|
+
"doctor",
|
|
295
|
+
"packs:list",
|
|
296
|
+
"packs:add",
|
|
297
|
+
"upgrade",
|
|
250
298
|
"auth:login",
|
|
251
299
|
"wizard",
|
|
252
300
|
"auth:whoami",
|
|
253
301
|
"llm:ready",
|
|
254
302
|
"contacts:find-linkedin-urls",
|
|
303
|
+
"contacts:process-emails",
|
|
255
304
|
"auth:logout",
|
|
256
305
|
"account:resolve",
|
|
257
306
|
"icp:define",
|
|
@@ -498,6 +547,296 @@ function normalizeLookupCompanyForSearch(value) {
|
|
|
498
547
|
.replace(/\s+/g, " ")
|
|
499
548
|
.trim();
|
|
500
549
|
}
|
|
550
|
+
function normalizeLookupCompanyForCleaning(value) {
|
|
551
|
+
return normalizeLookupWhitespace(value)
|
|
552
|
+
.replace(/[+]/g, " ")
|
|
553
|
+
.replace(/\s+/g, " ")
|
|
554
|
+
.trim();
|
|
555
|
+
}
|
|
556
|
+
function stripLookupCompanyLegalSuffixes(value) {
|
|
557
|
+
let cleaned = normalizeLookupCompanyForCleaning(value);
|
|
558
|
+
if (!cleaned) {
|
|
559
|
+
return "";
|
|
560
|
+
}
|
|
561
|
+
cleaned = cleaned.replace(/\s*\([^)]*\)\s*$/g, "").trim();
|
|
562
|
+
const suffixes = [
|
|
563
|
+
"gmbh & co. kgaa",
|
|
564
|
+
"gmbh & co kgaa",
|
|
565
|
+
"gmbh & co. kg",
|
|
566
|
+
"gmbh & co kg",
|
|
567
|
+
"ug (haftungsbeschrankt)",
|
|
568
|
+
"ug (haftungsbeschränkt)",
|
|
569
|
+
"wirtschaftsprufungsgesellschaft",
|
|
570
|
+
"wirtschaftsprüfungsgesellschaft",
|
|
571
|
+
"gesellschaft",
|
|
572
|
+
"corporation",
|
|
573
|
+
"holdings",
|
|
574
|
+
"holding",
|
|
575
|
+
"international",
|
|
576
|
+
"services",
|
|
577
|
+
"service",
|
|
578
|
+
"solutions",
|
|
579
|
+
"systems",
|
|
580
|
+
"consulting",
|
|
581
|
+
"management",
|
|
582
|
+
"technologies",
|
|
583
|
+
"technology",
|
|
584
|
+
"digital",
|
|
585
|
+
"software",
|
|
586
|
+
"global",
|
|
587
|
+
"worldwide",
|
|
588
|
+
"deutschland",
|
|
589
|
+
"germany",
|
|
590
|
+
"america",
|
|
591
|
+
"europe",
|
|
592
|
+
"company",
|
|
593
|
+
"verlag",
|
|
594
|
+
"servtec",
|
|
595
|
+
"group",
|
|
596
|
+
"gruppe",
|
|
597
|
+
"tech",
|
|
598
|
+
"gmbh",
|
|
599
|
+
"kgaa",
|
|
600
|
+
"mbh",
|
|
601
|
+
"ohg",
|
|
602
|
+
"kg",
|
|
603
|
+
"ag",
|
|
604
|
+
"ug",
|
|
605
|
+
"se",
|
|
606
|
+
"sa",
|
|
607
|
+
"s.a.",
|
|
608
|
+
"sarl",
|
|
609
|
+
"s.r.l.",
|
|
610
|
+
"srl",
|
|
611
|
+
"llc",
|
|
612
|
+
"ltd",
|
|
613
|
+
"limited",
|
|
614
|
+
"inc.",
|
|
615
|
+
"inc",
|
|
616
|
+
"corp.",
|
|
617
|
+
"corp",
|
|
618
|
+
"co.",
|
|
619
|
+
"co",
|
|
620
|
+
"plc",
|
|
621
|
+
"bv",
|
|
622
|
+
"b.v.",
|
|
623
|
+
"nv",
|
|
624
|
+
"n.v.",
|
|
625
|
+
"oyj",
|
|
626
|
+
"oy",
|
|
627
|
+
"aps",
|
|
628
|
+
"a/s",
|
|
629
|
+
"ab",
|
|
630
|
+
"as"
|
|
631
|
+
];
|
|
632
|
+
const escapedSuffixes = suffixes
|
|
633
|
+
.map((suffix) => suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
634
|
+
.sort((left, right) => right.length - left.length)
|
|
635
|
+
.join("|");
|
|
636
|
+
const separatorPattern = String.raw `(?:\s*[,|/]\s*|\s+[-–]\s+|\s+)`;
|
|
637
|
+
const trailingSuffixPattern = new RegExp(`${separatorPattern}(?:${escapedSuffixes})\\s*$`, "i");
|
|
638
|
+
const suffixBeforeSeparatorPattern = new RegExp(`\\s+(?:${escapedSuffixes})(?=\\s*[-–|/])`, "ig");
|
|
639
|
+
for (let index = 0; index < 6; index += 1) {
|
|
640
|
+
const next = cleaned
|
|
641
|
+
.replace(suffixBeforeSeparatorPattern, "")
|
|
642
|
+
.replace(trailingSuffixPattern, "")
|
|
643
|
+
.trim();
|
|
644
|
+
if (next === cleaned) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
cleaned = next;
|
|
648
|
+
}
|
|
649
|
+
return normalizeLookupWhitespace(cleaned);
|
|
650
|
+
}
|
|
651
|
+
function aggressivelyCleanLookupCompanyName(value) {
|
|
652
|
+
let cleaned = stripLookupCompanyLegalSuffixes(value);
|
|
653
|
+
if (!cleaned) {
|
|
654
|
+
return "";
|
|
655
|
+
}
|
|
656
|
+
const suffixesToRemove = [
|
|
657
|
+
"group",
|
|
658
|
+
"gruppe",
|
|
659
|
+
"international",
|
|
660
|
+
"global",
|
|
661
|
+
"worldwide",
|
|
662
|
+
"services",
|
|
663
|
+
"service",
|
|
664
|
+
"servtec",
|
|
665
|
+
"solutions",
|
|
666
|
+
"systems",
|
|
667
|
+
"consulting",
|
|
668
|
+
"management",
|
|
669
|
+
"technologies",
|
|
670
|
+
"technology",
|
|
671
|
+
"digital",
|
|
672
|
+
"tech",
|
|
673
|
+
"it",
|
|
674
|
+
"software",
|
|
675
|
+
"europe",
|
|
676
|
+
"deutschland",
|
|
677
|
+
"germany",
|
|
678
|
+
"usa",
|
|
679
|
+
"uk",
|
|
680
|
+
"america",
|
|
681
|
+
"holdings",
|
|
682
|
+
"holding",
|
|
683
|
+
"corporation",
|
|
684
|
+
"company",
|
|
685
|
+
"verlag",
|
|
686
|
+
"gesellschaft",
|
|
687
|
+
"wirtschaftsprüfungsgesellschaft",
|
|
688
|
+
"wirtschaftsprufungsgesellschaft"
|
|
689
|
+
];
|
|
690
|
+
for (const suffix of suffixesToRemove) {
|
|
691
|
+
const pattern = new RegExp(`(?:\\s+|\\s*[-–|/]\\s*)${suffix}\\s*$`, "i");
|
|
692
|
+
cleaned = cleaned.replace(pattern, "").trim();
|
|
693
|
+
}
|
|
694
|
+
const segments = cleaned
|
|
695
|
+
.split(/\s*[-–|/]\s*/)
|
|
696
|
+
.map((segment) => normalizeLookupWhitespace(segment))
|
|
697
|
+
.filter(Boolean);
|
|
698
|
+
if (segments.length > 1) {
|
|
699
|
+
const descriptorPattern = /^(robotic|solutions?|systems?|services?|service|consulting|management|technologies?|technology|digital|tech|software|logistics|distribution|distributions|trading|handel|messtechnik|produktionstechnik|umwelttechnik|reinigungstechnik|pflanzenschutz|betonwerk|export|import|export import|vertrieb|wholesale|retail|agro|center|centre|center)$/i;
|
|
700
|
+
const trailingSegments = segments.slice(1);
|
|
701
|
+
const shouldCollapseToFirstSegment = trailingSegments.every((segment) => segment
|
|
702
|
+
.split(/\s+/)
|
|
703
|
+
.filter(Boolean)
|
|
704
|
+
.every((word) => descriptorPattern.test(word)));
|
|
705
|
+
if (shouldCollapseToFirstSegment) {
|
|
706
|
+
cleaned = segments[0] ?? cleaned;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const words = cleaned.split(/\s+/).filter(Boolean);
|
|
710
|
+
if (words.length === 2) {
|
|
711
|
+
const [firstWord, secondWord] = words;
|
|
712
|
+
if (/^(servtec|digital|tech|solutions|systems|services|consulting|management|technologies)$/i.test(secondWord ?? "")) {
|
|
713
|
+
cleaned = firstWord ?? cleaned;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const descriptorTailPattern = /^(robotic|solutions?|systems?|services?|service|consulting|management|technologies?|technology|digital|tech|software|logistics|distribution|distributions|trading|handel)$/i;
|
|
717
|
+
const cleanedWords = cleaned.split(/\s+/).filter(Boolean);
|
|
718
|
+
if (cleanedWords.length >= 2 &&
|
|
719
|
+
cleanedWords.slice(1).every((word) => descriptorTailPattern.test(word))) {
|
|
720
|
+
cleaned = cleanedWords[0] ?? cleaned;
|
|
721
|
+
}
|
|
722
|
+
return normalizeLookupWhitespace(cleaned);
|
|
723
|
+
}
|
|
724
|
+
function resolveCompanyCleaningMode(value) {
|
|
725
|
+
const normalized = String(value ?? "basic").trim().toLowerCase();
|
|
726
|
+
if (normalized === "off" || normalized === "raw" || normalized === "none") {
|
|
727
|
+
return "off";
|
|
728
|
+
}
|
|
729
|
+
if (normalized === "ai" || normalized === "openai") {
|
|
730
|
+
return "ai";
|
|
731
|
+
}
|
|
732
|
+
return "basic";
|
|
733
|
+
}
|
|
734
|
+
function readOpenAiCompanyCleanerConfig() {
|
|
735
|
+
const apiKey = process.env.SALESPROMPTER_OPENAI_API_KEY?.trim() ||
|
|
736
|
+
process.env.OPENAI_API_KEY?.trim() ||
|
|
737
|
+
"";
|
|
738
|
+
if (!apiKey) {
|
|
739
|
+
throw new Error("Missing OpenAI API key for AI company cleaning. Set OPENAI_API_KEY or SALESPROMPTER_OPENAI_API_KEY.");
|
|
740
|
+
}
|
|
741
|
+
const baseUrl = process.env.SALESPROMPTER_OPENAI_BASE_URL?.trim().replace(/\/+$/, "") || "https://api.openai.com/v1";
|
|
742
|
+
const promptId = process.env.SALESPROMPTER_COMPANY_CLEANER_PROMPT_ID?.trim() ||
|
|
743
|
+
"pmpt_69c164c582a48196b288a8909de87c700694068fbd1b0e55";
|
|
744
|
+
const promptVersion = process.env.SALESPROMPTER_COMPANY_CLEANER_PROMPT_VERSION?.trim() || "2";
|
|
745
|
+
const maxOutputTokens = z.coerce
|
|
746
|
+
.number()
|
|
747
|
+
.int()
|
|
748
|
+
.min(256)
|
|
749
|
+
.max(32768)
|
|
750
|
+
.parse(process.env.SALESPROMPTER_COMPANY_CLEANER_MAX_OUTPUT_TOKENS ?? "4096");
|
|
751
|
+
return {
|
|
752
|
+
apiKey,
|
|
753
|
+
baseUrl,
|
|
754
|
+
promptId,
|
|
755
|
+
promptVersion,
|
|
756
|
+
maxOutputTokens
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
async function cleanCompanyNamesWithOpenAi(companyNames, config) {
|
|
760
|
+
if (companyNames.length === 0) {
|
|
761
|
+
return new Map();
|
|
762
|
+
}
|
|
763
|
+
const response = await fetch(`${config.baseUrl}/responses`, {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: {
|
|
766
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
767
|
+
"Content-Type": "application/json"
|
|
768
|
+
},
|
|
769
|
+
body: JSON.stringify({
|
|
770
|
+
prompt: {
|
|
771
|
+
id: config.promptId,
|
|
772
|
+
version: config.promptVersion
|
|
773
|
+
},
|
|
774
|
+
input: [
|
|
775
|
+
{
|
|
776
|
+
role: "user",
|
|
777
|
+
content: JSON.stringify(companyNames)
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
text: {
|
|
781
|
+
format: {
|
|
782
|
+
type: "text"
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
reasoning: {},
|
|
786
|
+
max_output_tokens: config.maxOutputTokens,
|
|
787
|
+
store: true
|
|
788
|
+
})
|
|
789
|
+
});
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
const details = await response.text();
|
|
792
|
+
throw new Error(`OpenAI company cleaning failed with ${response.status}: ${details}`);
|
|
793
|
+
}
|
|
794
|
+
const payload = (await response.json());
|
|
795
|
+
const incomplete = payload.status === "incomplete" || payload.incomplete_details;
|
|
796
|
+
if (incomplete) {
|
|
797
|
+
const reason = payload.incomplete_details?.reason || payload.status || "unknown";
|
|
798
|
+
throw new Error(`OpenAI company cleaning returned an incomplete response (${reason}).`);
|
|
799
|
+
}
|
|
800
|
+
const rawText = payload.output_text ??
|
|
801
|
+
payload.output?.flatMap((item) => item.content ?? []).map((item) => item.text ?? "").join("") ??
|
|
802
|
+
"";
|
|
803
|
+
if (!rawText.trim()) {
|
|
804
|
+
throw new Error("OpenAI company cleaning returned no text.");
|
|
805
|
+
}
|
|
806
|
+
const parsed = z
|
|
807
|
+
.array(z.object({
|
|
808
|
+
companyName_input: z.string(),
|
|
809
|
+
companyName_cleaned: z.string().nullish()
|
|
810
|
+
}))
|
|
811
|
+
.parse(JSON.parse(rawText));
|
|
812
|
+
const cleanedByInput = new Map();
|
|
813
|
+
for (const item of parsed) {
|
|
814
|
+
cleanedByInput.set(normalizeLookupCompanyForCleaning(item.companyName_input), aggressivelyCleanLookupCompanyName(item.companyName_cleaned ?? item.companyName_input));
|
|
815
|
+
}
|
|
816
|
+
return cleanedByInput;
|
|
817
|
+
}
|
|
818
|
+
async function buildCompanyNameCleaningMap(rows, mode) {
|
|
819
|
+
const uniqueCompanyNames = Array.from(new Set(rows
|
|
820
|
+
.map((row) => normalizeLookupCompanyForCleaning(row.companyName))
|
|
821
|
+
.filter((value) => value.length > 0)));
|
|
822
|
+
if (uniqueCompanyNames.length === 0) {
|
|
823
|
+
return new Map();
|
|
824
|
+
}
|
|
825
|
+
const cleanedByInput = new Map();
|
|
826
|
+
for (const companyName of uniqueCompanyNames) {
|
|
827
|
+
cleanedByInput.set(companyName, aggressivelyCleanLookupCompanyName(companyName));
|
|
828
|
+
}
|
|
829
|
+
if (mode !== "ai") {
|
|
830
|
+
return cleanedByInput;
|
|
831
|
+
}
|
|
832
|
+
const aiCleanedByInput = await cleanCompanyNamesWithOpenAi(uniqueCompanyNames, readOpenAiCompanyCleanerConfig());
|
|
833
|
+
for (const [input, cleaned] of aiCleanedByInput.entries()) {
|
|
834
|
+
if (cleaned) {
|
|
835
|
+
cleanedByInput.set(input, cleaned);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return cleanedByInput;
|
|
839
|
+
}
|
|
501
840
|
function splitLookupFullName(fullName) {
|
|
502
841
|
const parts = normalizeLookupWhitespace(fullName).split(" ").filter(Boolean);
|
|
503
842
|
return {
|
|
@@ -581,12 +920,12 @@ function parseLinkedInUrlLookupInput(content) {
|
|
|
581
920
|
}))
|
|
582
921
|
.filter((row) => row.fullName.length > 0 || row.companyName.length > 0);
|
|
583
922
|
}
|
|
584
|
-
function toLinkedInUrlLookupContacts(rows) {
|
|
923
|
+
function toLinkedInUrlLookupContacts(rows, cleanedCompanyMap = new Map()) {
|
|
585
924
|
return rows.flatMap((row, index) => {
|
|
586
925
|
const contactId = String(index + 1);
|
|
587
926
|
const syntheticEmail = row.email?.trim() || buildSyntheticLookupEmail(contactId);
|
|
588
927
|
const rawCompanyName = normalizeLookupWhitespace(row.companyName);
|
|
589
|
-
const cleanedCompanyName = normalizeLookupCompanyForSearch(rawCompanyName);
|
|
928
|
+
const cleanedCompanyName = normalizeLookupCompanyForSearch(cleanedCompanyMap.get(normalizeLookupCompanyForCleaning(rawCompanyName)) ?? rawCompanyName);
|
|
590
929
|
const rawFullName = normalizeLookupWhitespace(row.fullName);
|
|
591
930
|
if (looksLikeLookupCompanyRow(rawFullName, rawCompanyName)) {
|
|
592
931
|
return [
|
|
@@ -652,45 +991,265 @@ function deriveCsrfTokenFromCookie(cookie) {
|
|
|
652
991
|
const match = cookie.match(/JSESSIONID="?([^";]+)"?/i);
|
|
653
992
|
return match?.[1]?.trim() || "";
|
|
654
993
|
}
|
|
655
|
-
function
|
|
656
|
-
const csrfToken = process.env.SALESPROMPTER_LINKEDIN_CSRF_TOKEN?.trim() ||
|
|
657
|
-
process.env.LINKEDIN_CSRF_TOKEN?.trim() ||
|
|
658
|
-
deriveCsrfTokenFromCookie(process.env.SALESPROMPTER_LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
659
|
-
process.env.LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
660
|
-
"");
|
|
661
|
-
const identity = process.env.SALESPROMPTER_LINKEDIN_X_LI_IDENTITY?.trim() ||
|
|
662
|
-
process.env.LINKEDIN_X_LI_IDENTITY?.trim() ||
|
|
663
|
-
"";
|
|
994
|
+
function readLinkedInDirectLookupEnvConfig() {
|
|
664
995
|
const cookie = process.env.SALESPROMPTER_LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
665
996
|
process.env.LINKEDIN_SALES_NAV_COOKIE?.trim() ||
|
|
666
997
|
"";
|
|
667
|
-
const
|
|
668
|
-
|
|
998
|
+
const identity = process.env.SALESPROMPTER_LINKEDIN_X_LI_IDENTITY?.trim() ||
|
|
999
|
+
process.env.LINKEDIN_X_LI_IDENTITY?.trim() ||
|
|
1000
|
+
"";
|
|
1001
|
+
const csrfToken = process.env.SALESPROMPTER_LINKEDIN_CSRF_TOKEN?.trim() ||
|
|
1002
|
+
process.env.LINKEDIN_CSRF_TOKEN?.trim() ||
|
|
1003
|
+
deriveCsrfTokenFromCookie(cookie);
|
|
669
1004
|
if (!csrfToken || !identity || !cookie) {
|
|
670
|
-
|
|
1005
|
+
return null;
|
|
671
1006
|
}
|
|
672
1007
|
return {
|
|
673
1008
|
csrfToken,
|
|
674
1009
|
identity,
|
|
675
1010
|
cookie,
|
|
1011
|
+
userAgent: process.env.SALESPROMPTER_LINKEDIN_USER_AGENT?.trim() ||
|
|
1012
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function extractStoredLinkedInLookupValue(metadata, key) {
|
|
1016
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
1017
|
+
return "";
|
|
1018
|
+
}
|
|
1019
|
+
const record = metadata;
|
|
1020
|
+
const linkedInSession = record.linkedInSession && typeof record.linkedInSession === "object" && !Array.isArray(record.linkedInSession)
|
|
1021
|
+
? record.linkedInSession
|
|
1022
|
+
: null;
|
|
1023
|
+
const value = linkedInSession?.[key];
|
|
1024
|
+
return typeof value === "string" ? value.trim() : "";
|
|
1025
|
+
}
|
|
1026
|
+
async function readStoredLinkedInDirectLookupConfig() {
|
|
1027
|
+
const supabase = createLinkedInSessionSupabaseClient();
|
|
1028
|
+
if (!supabase) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
const claimed = await claimValidatedSalesNavigatorSessionCookieForCli({
|
|
1032
|
+
queryUrl: "https://www.linkedin.com/sales/search/people",
|
|
1033
|
+
source: "cli_direct_lookup",
|
|
1034
|
+
supabase
|
|
1035
|
+
});
|
|
1036
|
+
if (!claimed) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const lookupResult = await supabase
|
|
1040
|
+
.from("linkedin_session_cookies")
|
|
1041
|
+
.select("metadata")
|
|
1042
|
+
.eq("session_cookie_sha256", claimed.sessionCookieSha256)
|
|
1043
|
+
.maybeSingle();
|
|
1044
|
+
if (lookupResult.error && !/linkedin_session_cookies/.test(lookupResult.error.message)) {
|
|
1045
|
+
throw new Error(`Failed to read stored LinkedIn session metadata: ${lookupResult.error.message}`);
|
|
1046
|
+
}
|
|
1047
|
+
const metadata = lookupResult.data?.metadata;
|
|
1048
|
+
const identity = extractStoredLinkedInLookupValue(metadata, "linkedInIdentity");
|
|
1049
|
+
const csrfToken = extractStoredLinkedInLookupValue(metadata, "csrfToken") ||
|
|
1050
|
+
deriveCsrfTokenFromCookie(claimed.sessionCookie);
|
|
1051
|
+
const userAgent = process.env.SALESPROMPTER_LINKEDIN_USER_AGENT?.trim() ||
|
|
1052
|
+
extractStoredLinkedInLookupValue(metadata, "userAgent") ||
|
|
1053
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";
|
|
1054
|
+
if (!identity || !csrfToken) {
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
csrfToken,
|
|
1059
|
+
identity,
|
|
1060
|
+
cookie: claimed.sessionCookie,
|
|
676
1061
|
userAgent
|
|
677
1062
|
};
|
|
678
1063
|
}
|
|
1064
|
+
let cachedLinkedInDirectLookupConfig = null;
|
|
1065
|
+
async function readLinkedInDirectLookupConfig() {
|
|
1066
|
+
if (cachedLinkedInDirectLookupConfig) {
|
|
1067
|
+
return cachedLinkedInDirectLookupConfig;
|
|
1068
|
+
}
|
|
1069
|
+
const envConfig = readLinkedInDirectLookupEnvConfig();
|
|
1070
|
+
if (envConfig) {
|
|
1071
|
+
cachedLinkedInDirectLookupConfig = envConfig;
|
|
1072
|
+
return envConfig;
|
|
1073
|
+
}
|
|
1074
|
+
const storedConfig = await readStoredLinkedInDirectLookupConfig();
|
|
1075
|
+
if (storedConfig) {
|
|
1076
|
+
cachedLinkedInDirectLookupConfig = storedConfig;
|
|
1077
|
+
return storedConfig;
|
|
1078
|
+
}
|
|
1079
|
+
throw new Error("Missing LinkedIn direct lookup session. Set LINKEDIN_CSRF_TOKEN, LINKEDIN_X_LI_IDENTITY, and LINKEDIN_SALES_NAV_COOKIE, or connect the Salesprompter Chrome extension to sync your LinkedIn session to salesprompter.ai.");
|
|
1080
|
+
}
|
|
679
1081
|
function generateLinkedInSessionId() {
|
|
680
1082
|
return Array.from({ length: 22 }, () => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(Math.floor(Math.random() * 62))).join("");
|
|
681
1083
|
}
|
|
682
|
-
function buildLinkedInSalesApiUrl(
|
|
1084
|
+
function buildLinkedInSalesApiUrl(params) {
|
|
1085
|
+
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
1086
|
+
"https://www.linkedin.com";
|
|
1087
|
+
const encodedFirstName = encodeURIComponent(params.firstName);
|
|
1088
|
+
const encodedLastName = encodeURIComponent(params.lastName);
|
|
1089
|
+
const encodedCompanyName = encodeURIComponent(params.companyName);
|
|
1090
|
+
const filters = params.searchMode === "current_company"
|
|
1091
|
+
? `(type:FIRST_NAME,values:List((text:${encodedFirstName},selectionType:INCLUDED))),(type:LAST_NAME,values:List((text:${encodedLastName},selectionType:INCLUDED))),(type:CURRENT_COMPANY,values:List((text:${encodedCompanyName},selectionType:INCLUDED)))`
|
|
1092
|
+
: `(type:FIRST_NAME,values:List((text:${encodedFirstName},selectionType:INCLUDED))),(type:LAST_NAME,values:List((text:${encodedLastName},selectionType:INCLUDED)))`;
|
|
1093
|
+
const keywordsSegment = params.searchMode === "keywords" ? `,keywords:${encodedCompanyName}` : "";
|
|
1094
|
+
return `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiLeadSearch?q=searchQuery&query=(recentSearchParam:(id:${Date.now()},doLogHistory:true),filters:List(${filters})${keywordsSegment})&start=0&count=25&trackingParam=(sessionId:${generateLinkedInSessionId()})&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14`;
|
|
1095
|
+
}
|
|
1096
|
+
function buildLinkedInAccountSearchApiUrl(companyName) {
|
|
683
1097
|
const baseUrl = process.env.SALESPROMPTER_LINKEDIN_SALES_API_BASE_URL?.trim() ||
|
|
684
1098
|
"https://www.linkedin.com";
|
|
685
|
-
|
|
1099
|
+
const encodedCompanyName = encodeURIComponent(companyName);
|
|
1100
|
+
return `${baseUrl.replace(/\/+$/, "")}/sales-api/salesApiAccountSearch?q=searchQuery&query=(recentSearchParam:(id:${Date.now()},doLogHistory:true),spellCorrectionEnabled:true,keywords:${encodedCompanyName})&start=0&count=10&trackingParam=(sessionId:${generateLinkedInSessionId()})&decorationId=com.linkedin.sales.deco.desktop.searchv2.AccountSearchResult-14`;
|
|
1101
|
+
}
|
|
1102
|
+
function buildLinkedInLookupSearchVariants(contact) {
|
|
1103
|
+
const variants = [];
|
|
1104
|
+
const seen = new Set();
|
|
1105
|
+
const companyCandidates = [
|
|
1106
|
+
normalizeLookupWhitespace(contact.companyName),
|
|
1107
|
+
normalizeLookupWhitespace(contact.companyNameOriginal)
|
|
1108
|
+
].filter(Boolean);
|
|
1109
|
+
for (const companyName of companyCandidates) {
|
|
1110
|
+
for (const searchMode of ["current_company", "keywords"]) {
|
|
1111
|
+
const key = [
|
|
1112
|
+
contact.firstName.trim().toLowerCase(),
|
|
1113
|
+
contact.lastName.trim().toLowerCase(),
|
|
1114
|
+
companyName.toLowerCase(),
|
|
1115
|
+
searchMode
|
|
1116
|
+
].join("|");
|
|
1117
|
+
if (seen.has(key)) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
seen.add(key);
|
|
1121
|
+
variants.push({
|
|
1122
|
+
firstName: contact.firstName,
|
|
1123
|
+
lastName: contact.lastName,
|
|
1124
|
+
companyName,
|
|
1125
|
+
searchMode
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return variants;
|
|
686
1130
|
}
|
|
687
1131
|
function extractLinkedInProfileUrlFromSalesApiElement(element) {
|
|
688
1132
|
const entityUrn = typeof element?.entityUrn === "string" ? element.entityUrn : "";
|
|
689
1133
|
const salesIdMatch = entityUrn.match(/\(([^,]+),/);
|
|
690
1134
|
return salesIdMatch ? `https://www.linkedin.com/in/${salesIdMatch[1]}` : null;
|
|
691
1135
|
}
|
|
1136
|
+
function collectNestedStrings(value, seen = new Set()) {
|
|
1137
|
+
if (value == null || seen.has(value)) {
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1140
|
+
if (typeof value === "string") {
|
|
1141
|
+
return [value];
|
|
1142
|
+
}
|
|
1143
|
+
if (typeof value !== "object") {
|
|
1144
|
+
return [];
|
|
1145
|
+
}
|
|
1146
|
+
seen.add(value);
|
|
1147
|
+
if (Array.isArray(value)) {
|
|
1148
|
+
return value.flatMap((entry) => collectNestedStrings(entry, seen));
|
|
1149
|
+
}
|
|
1150
|
+
return Object.values(value).flatMap((entry) => collectNestedStrings(entry, seen));
|
|
1151
|
+
}
|
|
1152
|
+
function extractLinkedInCompanyUrlFromSalesApiElement(element) {
|
|
1153
|
+
if (!element) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
const explicitUrlCandidates = [
|
|
1157
|
+
typeof element.companyPageUrl === "string" ? element.companyPageUrl : null,
|
|
1158
|
+
typeof element.regularCompanyUrl === "string" ? element.regularCompanyUrl : null,
|
|
1159
|
+
typeof element.url === "string" ? element.url : null
|
|
1160
|
+
].filter(Boolean);
|
|
1161
|
+
const nestedUrlMatch = collectNestedStrings(element).find((value) => /https:\/\/www\.linkedin\.com\/company\/[^/?#]+/i.test(value));
|
|
1162
|
+
if (nestedUrlMatch) {
|
|
1163
|
+
return nestedUrlMatch.match(/https:\/\/www\.linkedin\.com\/company\/[^/?#]+/i)?.[0] ?? null;
|
|
1164
|
+
}
|
|
1165
|
+
for (const candidate of explicitUrlCandidates) {
|
|
1166
|
+
const normalized = normalizeLinkedInCompanyHandle(candidate);
|
|
1167
|
+
if (normalized) {
|
|
1168
|
+
return normalizeLinkedInCompanyPage(normalized);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const entityUrn = typeof element.entityUrn === "string" ? element.entityUrn : "";
|
|
1172
|
+
const idMatch = entityUrn.match(/\(([^,]+),/);
|
|
1173
|
+
const companyId = idMatch?.[1]?.trim() ?? "";
|
|
1174
|
+
if (/^\d+$/.test(companyId)) {
|
|
1175
|
+
return normalizeLinkedInCompanyPage(companyId);
|
|
1176
|
+
}
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
function extractLinkedInCompanyNameFromSalesApiElement(element) {
|
|
1180
|
+
if (!element) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
const directCandidates = [
|
|
1184
|
+
typeof element.companyName === "string" ? element.companyName : null,
|
|
1185
|
+
typeof element.name === "string" ? element.name : null,
|
|
1186
|
+
typeof element.title === "string" ? element.title : null
|
|
1187
|
+
].filter(Boolean);
|
|
1188
|
+
for (const candidate of directCandidates) {
|
|
1189
|
+
const normalized = normalizeLookupWhitespace(candidate);
|
|
1190
|
+
if (normalized) {
|
|
1191
|
+
return normalized;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const nestedTitle = element.title;
|
|
1195
|
+
if (nestedTitle && typeof nestedTitle === "object" && "text" in nestedTitle) {
|
|
1196
|
+
const text = typeof nestedTitle.text === "string" ? normalizeLookupWhitespace(nestedTitle.text) : "";
|
|
1197
|
+
if (text) {
|
|
1198
|
+
return text;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
function extractLinkedInCompanyEmployeeCountFromSalesApiElement(element) {
|
|
1204
|
+
if (!element) {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
const numericCandidates = [
|
|
1208
|
+
typeof element.employeeCount === "number" ? element.employeeCount : null,
|
|
1209
|
+
typeof element.employeesOnLinkedInCount === "number" ? element.employeesOnLinkedInCount : null
|
|
1210
|
+
].filter((value) => Number.isFinite(value));
|
|
1211
|
+
if (numericCandidates.length > 0) {
|
|
1212
|
+
return Math.max(0, Math.trunc(numericCandidates[0] ?? 0));
|
|
1213
|
+
}
|
|
1214
|
+
for (const value of collectNestedStrings(element)) {
|
|
1215
|
+
const match = value.match(/(\d[\d.,]*)\s+employees\b/i);
|
|
1216
|
+
if (match) {
|
|
1217
|
+
return Number(match[1].replace(/[.,]/g, ""));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return null;
|
|
1221
|
+
}
|
|
1222
|
+
function buildLinkedInCompanyLookupVariants(params) {
|
|
1223
|
+
const variants = [];
|
|
1224
|
+
const seen = new Set();
|
|
1225
|
+
const rawCandidates = [
|
|
1226
|
+
normalizeLookupWhitespace(params.companyName),
|
|
1227
|
+
normalizeLookupWhitespace(params.companyNameOriginal)
|
|
1228
|
+
].filter(Boolean);
|
|
1229
|
+
const normalizedCandidates = rawCandidates.flatMap((candidate) => {
|
|
1230
|
+
const aggressive = aggressivelyCleanLookupCompanyName(candidate);
|
|
1231
|
+
const searchable = normalizeLookupCompanyForSearch(candidate);
|
|
1232
|
+
const dePunctuated = normalizeLookupWhitespace(candidate.replace(/[^\p{L}\p{N}]+/gu, " "));
|
|
1233
|
+
return [candidate, aggressive, searchable, dePunctuated];
|
|
1234
|
+
});
|
|
1235
|
+
for (const companyName of normalizedCandidates) {
|
|
1236
|
+
if (!companyName) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const key = companyName.toLowerCase();
|
|
1240
|
+
if (seen.has(key)) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
seen.add(key);
|
|
1244
|
+
variants.push({
|
|
1245
|
+
contact_id: params.contactId,
|
|
1246
|
+
companyName
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
return variants;
|
|
1250
|
+
}
|
|
692
1251
|
async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
693
|
-
const config = readLinkedInDirectLookupConfig();
|
|
1252
|
+
const config = await readLinkedInDirectLookupConfig();
|
|
694
1253
|
const groupedContacts = new Map();
|
|
695
1254
|
for (const contact of params.contacts) {
|
|
696
1255
|
const key = contact.email?.trim().toLowerCase() || `contact:${contact.contact_id}`;
|
|
@@ -722,23 +1281,119 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
722
1281
|
let matchedUrl = null;
|
|
723
1282
|
let lastError = null;
|
|
724
1283
|
for (const candidate of variations) {
|
|
1284
|
+
for (const searchVariant of buildLinkedInLookupSearchVariants(candidate)) {
|
|
1285
|
+
const controller = new AbortController();
|
|
1286
|
+
const timeout = setTimeout(controller.abort.bind(controller), Math.min(params.timeoutMs, 20_000));
|
|
1287
|
+
try {
|
|
1288
|
+
const response = await fetch(buildLinkedInSalesApiUrl(searchVariant), {
|
|
1289
|
+
method: "GET",
|
|
1290
|
+
signal: controller.signal,
|
|
1291
|
+
headers: {
|
|
1292
|
+
accept: "*/*",
|
|
1293
|
+
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
1294
|
+
"csrf-token": config.csrfToken,
|
|
1295
|
+
referer: "https://www.linkedin.com/sales/search/people",
|
|
1296
|
+
"sec-fetch-dest": "empty",
|
|
1297
|
+
"sec-fetch-mode": "cors",
|
|
1298
|
+
"sec-fetch-site": "same-origin",
|
|
1299
|
+
"user-agent": config.userAgent,
|
|
1300
|
+
"x-li-identity": config.identity,
|
|
1301
|
+
"x-li-lang": "en_US",
|
|
1302
|
+
"x-restli-protocol-version": "2.0.0",
|
|
1303
|
+
cookie: config.cookie
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
if (response.status === 429) {
|
|
1307
|
+
rateLimited = true;
|
|
1308
|
+
lastError = "LinkedIn rate limit";
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
if (!response.ok) {
|
|
1312
|
+
lastError = `LinkedIn returned ${response.status}`;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const data = (await response.json());
|
|
1316
|
+
const profilesFound = data.paging?.total ?? 0;
|
|
1317
|
+
if (profilesFound > 0) {
|
|
1318
|
+
matchedUrl = extractLinkedInProfileUrlFromSalesApiElement(data.elements?.[0]) ?? null;
|
|
1319
|
+
if (matchedUrl) {
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
lastError = error instanceof Error ? error.message : "Unknown direct lookup error";
|
|
1326
|
+
}
|
|
1327
|
+
finally {
|
|
1328
|
+
clearTimeout(timeout);
|
|
1329
|
+
}
|
|
1330
|
+
if (matchedUrl || rateLimited) {
|
|
1331
|
+
break;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (matchedUrl || rateLimited) {
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
results.push({
|
|
1339
|
+
contact_id: primary.contact_id,
|
|
1340
|
+
linkedin_url: matchedUrl,
|
|
1341
|
+
error: matchedUrl ? null : lastError
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
success: true,
|
|
1346
|
+
contacts: results
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
async function invokeLinkedInCompanyEnrichmentDirect(params) {
|
|
1350
|
+
const config = await readLinkedInDirectLookupConfig();
|
|
1351
|
+
const primaryContacts = new Map();
|
|
1352
|
+
for (const contact of params.contacts) {
|
|
1353
|
+
const existing = primaryContacts.get(contact.contact_id);
|
|
1354
|
+
if (!existing || existing.isVariation) {
|
|
1355
|
+
primaryContacts.set(contact.contact_id, contact);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const results = [];
|
|
1359
|
+
let rateLimited = false;
|
|
1360
|
+
for (const contact of primaryContacts.values()) {
|
|
1361
|
+
if (rateLimited) {
|
|
1362
|
+
results.push({
|
|
1363
|
+
contact_id: contact.contact_id,
|
|
1364
|
+
linkedin_company_url: null,
|
|
1365
|
+
error: "LinkedIn rate limit"
|
|
1366
|
+
});
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const variants = buildLinkedInCompanyLookupVariants({
|
|
1370
|
+
contactId: contact.contact_id,
|
|
1371
|
+
companyName: contact.companyName,
|
|
1372
|
+
companyNameOriginal: contact.companyNameOriginal
|
|
1373
|
+
});
|
|
1374
|
+
let matchedCompanyUrl = null;
|
|
1375
|
+
let matchedCompanyName = null;
|
|
1376
|
+
let matchedCompanyEmployeeCount = null;
|
|
1377
|
+
let lastError = null;
|
|
1378
|
+
for (const variant of variants) {
|
|
725
1379
|
const controller = new AbortController();
|
|
726
1380
|
const timeout = setTimeout(controller.abort.bind(controller), Math.min(params.timeoutMs, 20_000));
|
|
727
1381
|
try {
|
|
728
|
-
const response = await fetch(
|
|
1382
|
+
const response = await fetch(buildLinkedInAccountSearchApiUrl(variant.companyName), {
|
|
729
1383
|
method: "GET",
|
|
730
1384
|
signal: controller.signal,
|
|
731
1385
|
headers: {
|
|
732
1386
|
accept: "*/*",
|
|
733
1387
|
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
734
1388
|
"csrf-token": config.csrfToken,
|
|
735
|
-
referer: "https://www.linkedin.com/sales/search/
|
|
1389
|
+
referer: "https://www.linkedin.com/sales/search/company",
|
|
736
1390
|
"sec-fetch-dest": "empty",
|
|
737
1391
|
"sec-fetch-mode": "cors",
|
|
738
1392
|
"sec-fetch-site": "same-origin",
|
|
739
1393
|
"user-agent": config.userAgent,
|
|
740
1394
|
"x-li-identity": config.identity,
|
|
741
1395
|
"x-li-lang": "en_US",
|
|
1396
|
+
"x-li-page-instance": "urn:li:page:d_sales2_search_accounts;13Jvve6kRGCao+iP0wwAag==",
|
|
742
1397
|
"x-restli-protocol-version": "2.0.0",
|
|
743
1398
|
cookie: config.cookie
|
|
744
1399
|
}
|
|
@@ -749,29 +1404,35 @@ async function invokeLinkedInUrlEnrichmentDirect(params) {
|
|
|
749
1404
|
break;
|
|
750
1405
|
}
|
|
751
1406
|
if (!response.ok) {
|
|
752
|
-
lastError = `LinkedIn returned ${response.status}`;
|
|
1407
|
+
lastError = `LinkedIn company search returned ${response.status}`;
|
|
753
1408
|
continue;
|
|
754
1409
|
}
|
|
755
1410
|
const data = (await response.json());
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1411
|
+
const first = data.elements?.[0];
|
|
1412
|
+
const companyUrl = extractLinkedInCompanyUrlFromSalesApiElement(first);
|
|
1413
|
+
if (companyUrl) {
|
|
1414
|
+
matchedCompanyUrl = companyUrl;
|
|
1415
|
+
matchedCompanyName = extractLinkedInCompanyNameFromSalesApiElement(first);
|
|
1416
|
+
matchedCompanyEmployeeCount = extractLinkedInCompanyEmployeeCountFromSalesApiElement(first);
|
|
1417
|
+
break;
|
|
762
1418
|
}
|
|
763
1419
|
}
|
|
764
1420
|
catch (error) {
|
|
765
|
-
lastError = error instanceof Error ? error.message : "Unknown direct lookup error";
|
|
1421
|
+
lastError = error instanceof Error ? error.message : "Unknown direct company lookup error";
|
|
766
1422
|
}
|
|
767
1423
|
finally {
|
|
768
1424
|
clearTimeout(timeout);
|
|
769
1425
|
}
|
|
1426
|
+
if (matchedCompanyUrl || rateLimited) {
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
770
1429
|
}
|
|
771
1430
|
results.push({
|
|
772
|
-
contact_id:
|
|
773
|
-
|
|
774
|
-
|
|
1431
|
+
contact_id: contact.contact_id,
|
|
1432
|
+
linkedin_company_url: matchedCompanyUrl,
|
|
1433
|
+
matched_company_name: matchedCompanyName,
|
|
1434
|
+
matched_company_employee_count: matchedCompanyEmployeeCount,
|
|
1435
|
+
error: matchedCompanyUrl ? null : lastError
|
|
775
1436
|
});
|
|
776
1437
|
}
|
|
777
1438
|
return {
|
|
@@ -869,13 +1530,15 @@ async function fetchSalesNavLookupCandidates(params) {
|
|
|
869
1530
|
fullName: row.full_name == null ? null : String(row.full_name),
|
|
870
1531
|
companyName: row.company_name == null ? null : String(row.company_name),
|
|
871
1532
|
title: row.title == null ? null : String(row.title),
|
|
1533
|
+
companyUrl: row.company_url == null ? null : String(row.company_url),
|
|
1534
|
+
regularCompanyUrl: row.regular_company_url == null ? null : String(row.regular_company_url),
|
|
872
1535
|
salesNavProfileUrl: row.sales_nav_profile_url == null ? null : String(row.sales_nav_profile_url),
|
|
873
1536
|
linkedInProfileUrl: row.linkedin_profile_url == null ? null : String(row.linkedin_profile_url)
|
|
874
1537
|
}));
|
|
875
1538
|
const fetchRows = async (operator, value) => {
|
|
876
1539
|
let query = supabase
|
|
877
1540
|
.from("linkedin_sales_nav_people")
|
|
878
|
-
.select("org_id,full_name,company_name,title,sales_nav_profile_url,linkedin_profile_url")
|
|
1541
|
+
.select("org_id,full_name,company_name,title,company_url,regular_company_url,sales_nav_profile_url,linkedin_profile_url")
|
|
879
1542
|
.limit(10);
|
|
880
1543
|
if (params.orgId?.trim()) {
|
|
881
1544
|
query = query.eq("org_id", params.orgId.trim());
|
|
@@ -932,18 +1595,31 @@ async function resolveLinkedInUrlsFromSalesNavRows(params) {
|
|
|
932
1595
|
});
|
|
933
1596
|
const best = ranked[0]?.candidate;
|
|
934
1597
|
const linkedinUrl = best?.linkedInProfileUrl ?? best?.salesNavProfileUrl ?? null;
|
|
1598
|
+
const linkedinCompanyUrl = (() => {
|
|
1599
|
+
const handle = normalizeLinkedInCompanyHandle(best?.regularCompanyUrl ?? "") ??
|
|
1600
|
+
normalizeLinkedInCompanyHandle(best?.companyUrl ?? "");
|
|
1601
|
+
if (handle) {
|
|
1602
|
+
return normalizeLinkedInCompanyPage(handle);
|
|
1603
|
+
}
|
|
1604
|
+
const numericCompanyUrl = typeof best?.companyUrl === "string" ? best.companyUrl.trim() : "";
|
|
1605
|
+
return numericCompanyUrl.length > 0 ? numericCompanyUrl : null;
|
|
1606
|
+
})();
|
|
935
1607
|
results.push({
|
|
936
1608
|
clientId: row.clientId,
|
|
937
1609
|
fullName: row.fullName,
|
|
938
1610
|
companyName: row.companyName,
|
|
939
1611
|
linkedinUrl,
|
|
1612
|
+
linkedinCompanyUrl,
|
|
940
1613
|
found: Boolean(linkedinUrl),
|
|
1614
|
+
companyFound: Boolean(linkedinCompanyUrl),
|
|
941
1615
|
contactId: String(index + 1),
|
|
942
1616
|
source: linkedinUrl ? "salesnav-supabase" : null,
|
|
1617
|
+
companySource: linkedinCompanyUrl ? "salesnav-supabase" : null,
|
|
943
1618
|
matchedFullName: best?.fullName ?? null,
|
|
944
1619
|
matchedCompanyName: best?.companyName ?? null,
|
|
945
1620
|
matchedTitle: best?.title ?? null,
|
|
946
|
-
matchedOrgId: best?.orgId ?? null
|
|
1621
|
+
matchedOrgId: best?.orgId ?? null,
|
|
1622
|
+
matchedCompanyEmployeeCount: null
|
|
947
1623
|
});
|
|
948
1624
|
}
|
|
949
1625
|
return results;
|
|
@@ -1021,6 +1697,199 @@ function normalizeLinkedInCompanyHandle(value) {
|
|
|
1021
1697
|
function normalizeLinkedInCompanyPage(handle) {
|
|
1022
1698
|
return `https://www.linkedin.com/company/${handle}`;
|
|
1023
1699
|
}
|
|
1700
|
+
function rewriteLinkedInUrlForConfiguredBase(url, env = process.env) {
|
|
1701
|
+
const overrideBase = env.SALESPROMPTER_LINKEDIN_BASE_URL?.trim();
|
|
1702
|
+
if (!overrideBase) {
|
|
1703
|
+
return url;
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
const source = new URL(url);
|
|
1707
|
+
if (!/(^|\.)linkedin\.com$/i.test(source.hostname)) {
|
|
1708
|
+
return url;
|
|
1709
|
+
}
|
|
1710
|
+
const targetBase = new URL(overrideBase);
|
|
1711
|
+
targetBase.pathname = source.pathname;
|
|
1712
|
+
targetBase.search = source.search;
|
|
1713
|
+
targetBase.hash = source.hash;
|
|
1714
|
+
return targetBase.toString();
|
|
1715
|
+
}
|
|
1716
|
+
catch {
|
|
1717
|
+
return url;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function extractDirectLookupNameParts(contact) {
|
|
1721
|
+
return {
|
|
1722
|
+
firstName: normalizeLookupWhitespace(contact.firstName),
|
|
1723
|
+
lastName: normalizeLookupWhitespace(contact.lastName)
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
async function resolveDomainFromLinkedInCompanyUrl(params) {
|
|
1727
|
+
const config = await readLinkedInDirectLookupConfig();
|
|
1728
|
+
const targetUrl = rewriteLinkedInUrlForConfiguredBase(params.companyUrl);
|
|
1729
|
+
const controller = new AbortController();
|
|
1730
|
+
const timeout = setTimeout(() => controller.abort(), Math.min(params.timeoutMs, 20_000));
|
|
1731
|
+
try {
|
|
1732
|
+
const response = await fetch(targetUrl, {
|
|
1733
|
+
method: "GET",
|
|
1734
|
+
signal: controller.signal,
|
|
1735
|
+
headers: {
|
|
1736
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
1737
|
+
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
|
|
1738
|
+
"user-agent": config.userAgent,
|
|
1739
|
+
cookie: config.cookie
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
if (!response.ok) {
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
const html = await response.text();
|
|
1746
|
+
const parsed = parseLinkedInCompanyPage(html, params.companyUrl);
|
|
1747
|
+
return parsed.domain ?? null;
|
|
1748
|
+
}
|
|
1749
|
+
catch (error) {
|
|
1750
|
+
if (error.name === "AbortError") {
|
|
1751
|
+
throw new Error(`LinkedIn company page lookup timed out after ${params.timeoutMs}ms.`);
|
|
1752
|
+
}
|
|
1753
|
+
return null;
|
|
1754
|
+
}
|
|
1755
|
+
finally {
|
|
1756
|
+
clearTimeout(timeout);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
async function buildDirectEmailEnrichmentBatch(params) {
|
|
1760
|
+
const parsedInput = directEmailEnrichmentInputRowArraySchema.parse(parseDirectEmailEnrichmentInput(params.rawInput));
|
|
1761
|
+
if (parsedInput.length === 0) {
|
|
1762
|
+
throw new Error("No contact rows found. Provide TSV/CSV/JSON input via --in.");
|
|
1763
|
+
}
|
|
1764
|
+
const clientId = resolveDirectEmailEnrichmentClientId(parsedInput, params.clientIdOption);
|
|
1765
|
+
const lookupRows = parsedInput.map((row) => ({
|
|
1766
|
+
clientId: String(clientId),
|
|
1767
|
+
fullName: row.fullName,
|
|
1768
|
+
companyName: row.companyName
|
|
1769
|
+
}));
|
|
1770
|
+
const cleanedCompanyMap = await buildCompanyNameCleaningMap(lookupRows, params.companyCleaningMode);
|
|
1771
|
+
const contacts = toLinkedInUrlLookupContacts(lookupRows, cleanedCompanyMap);
|
|
1772
|
+
let companyEnrichmentByName = new Map();
|
|
1773
|
+
if (!shouldBypassAuth()) {
|
|
1774
|
+
const session = await requireAuthSession().catch(() => null);
|
|
1775
|
+
if (session) {
|
|
1776
|
+
const uniqueCompanies = Array.from(new Map(contacts
|
|
1777
|
+
.filter((contact) => !contact.isVariation)
|
|
1778
|
+
.map((contact) => {
|
|
1779
|
+
const cleanedName = cleanedCompanyMap.get(normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName)) ??
|
|
1780
|
+
normalizeLookupWhitespace(contact.companyNameOriginal ?? contact.companyName);
|
|
1781
|
+
return [
|
|
1782
|
+
normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName),
|
|
1783
|
+
{
|
|
1784
|
+
companyId: contact.contact_id,
|
|
1785
|
+
companyName: contact.companyNameOriginal ?? contact.companyName,
|
|
1786
|
+
companyNameCleaned: cleanedName || null
|
|
1787
|
+
}
|
|
1788
|
+
];
|
|
1789
|
+
})).values());
|
|
1790
|
+
if (uniqueCompanies.length > 0) {
|
|
1791
|
+
const enrichedCompanies = await enrichDirectEmailCompaniesViaApp(session, {
|
|
1792
|
+
clientId,
|
|
1793
|
+
companies: uniqueCompanies
|
|
1794
|
+
});
|
|
1795
|
+
companyEnrichmentByName = new Map(enrichedCompanies.companies.map((company) => [
|
|
1796
|
+
normalizeLookupCompanyForCleaning(company.companyName),
|
|
1797
|
+
{
|
|
1798
|
+
domain: company.domain ?? null,
|
|
1799
|
+
linkedinCompanyPage: company.linkedinCompanyPage ?? null
|
|
1800
|
+
}
|
|
1801
|
+
]));
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const [profileResult, companyResult] = await Promise.all([
|
|
1806
|
+
invokeLinkedInUrlEnrichmentDirect({
|
|
1807
|
+
contacts,
|
|
1808
|
+
timeoutMs: params.timeoutMs
|
|
1809
|
+
}),
|
|
1810
|
+
companyEnrichmentByName.size > 0
|
|
1811
|
+
? Promise.resolve({
|
|
1812
|
+
success: true,
|
|
1813
|
+
contacts: contacts
|
|
1814
|
+
.filter((contact) => !contact.isVariation)
|
|
1815
|
+
.map((contact) => ({
|
|
1816
|
+
contact_id: contact.contact_id,
|
|
1817
|
+
linkedin_company_url: companyEnrichmentByName.get(normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName))?.linkedinCompanyPage ?? null,
|
|
1818
|
+
matched_company_name: null,
|
|
1819
|
+
matched_company_employee_count: null,
|
|
1820
|
+
error: null
|
|
1821
|
+
}))
|
|
1822
|
+
})
|
|
1823
|
+
: invokeLinkedInCompanyEnrichmentDirect({
|
|
1824
|
+
contacts,
|
|
1825
|
+
timeoutMs: params.timeoutMs
|
|
1826
|
+
})
|
|
1827
|
+
]);
|
|
1828
|
+
const profileByContactId = new Map(profileResult.contacts.map((row) => [row.contact_id, row.linkedin_url ?? null]));
|
|
1829
|
+
const companyByContactId = new Map(companyResult.contacts.map((row) => [row.contact_id, row.linkedin_company_url ?? null]));
|
|
1830
|
+
const reportRows = [];
|
|
1831
|
+
const queueRows = [];
|
|
1832
|
+
for (let index = 0; index < contacts.length; index += 1) {
|
|
1833
|
+
const contact = contacts[index];
|
|
1834
|
+
if (contact.isVariation) {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
const names = extractDirectLookupNameParts(contact);
|
|
1838
|
+
const appCompanyEnrichment = companyEnrichmentByName.get(normalizeLookupCompanyForCleaning(contact.companyNameOriginal ?? contact.companyName)) ?? { domain: null, linkedinCompanyPage: null };
|
|
1839
|
+
const linkedinCompanyUrl = appCompanyEnrichment.linkedinCompanyPage ??
|
|
1840
|
+
companyByContactId.get(contact.contact_id) ??
|
|
1841
|
+
null;
|
|
1842
|
+
const domain = appCompanyEnrichment.domain ??
|
|
1843
|
+
(linkedinCompanyUrl
|
|
1844
|
+
? await resolveDomainFromLinkedInCompanyUrl({
|
|
1845
|
+
companyUrl: linkedinCompanyUrl,
|
|
1846
|
+
timeoutMs: params.timeoutMs
|
|
1847
|
+
})
|
|
1848
|
+
: null);
|
|
1849
|
+
let skippedReason = null;
|
|
1850
|
+
if (!names.firstName || !names.lastName) {
|
|
1851
|
+
skippedReason = "missing-name";
|
|
1852
|
+
}
|
|
1853
|
+
else if (!domain) {
|
|
1854
|
+
skippedReason = "missing-domain";
|
|
1855
|
+
}
|
|
1856
|
+
const companyHandle = linkedinCompanyUrl ? normalizeLinkedInCompanyHandle(linkedinCompanyUrl) : null;
|
|
1857
|
+
const numericCompanyId = companyHandle && /^\d+$/.test(companyHandle) ? companyHandle : String(index + 1);
|
|
1858
|
+
const reportRow = {
|
|
1859
|
+
contactId: contact.contact_id,
|
|
1860
|
+
clientId: String(clientId),
|
|
1861
|
+
companyId: numericCompanyId,
|
|
1862
|
+
companyName: contact.companyNameOriginal ?? contact.companyName,
|
|
1863
|
+
fullName: normalizeLookupWhitespace(`${names.firstName} ${names.lastName}`),
|
|
1864
|
+
firstName: names.firstName,
|
|
1865
|
+
lastName: names.lastName,
|
|
1866
|
+
linkedinUrl: profileByContactId.get(contact.contact_id) ?? null,
|
|
1867
|
+
linkedinCompanyUrl,
|
|
1868
|
+
domain,
|
|
1869
|
+
ready: skippedReason == null,
|
|
1870
|
+
skippedReason
|
|
1871
|
+
};
|
|
1872
|
+
reportRows.push(reportRow);
|
|
1873
|
+
if (!reportRow.ready || !domain) {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
queueRows.push({
|
|
1877
|
+
clientId: String(clientId),
|
|
1878
|
+
contactId: contact.contact_id,
|
|
1879
|
+
companyId: numericCompanyId,
|
|
1880
|
+
firstName_cleaned: names.firstName,
|
|
1881
|
+
lastName_cleaned: names.lastName,
|
|
1882
|
+
domain,
|
|
1883
|
+
companyScore: null,
|
|
1884
|
+
seniorityId: null
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
return {
|
|
1888
|
+
clientId,
|
|
1889
|
+
queueRows,
|
|
1890
|
+
reportRows
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1024
1893
|
function titleCaseSlug(value) {
|
|
1025
1894
|
return value
|
|
1026
1895
|
.split(/[-_]/)
|
|
@@ -2105,6 +2974,17 @@ async function launchLinkedInCompaniesBackfill(session, payload) {
|
|
|
2105
2974
|
}), LinkedInCompanyBackfillLaunchResponseSchema);
|
|
2106
2975
|
return value;
|
|
2107
2976
|
}
|
|
2977
|
+
async function enrichDirectEmailCompaniesViaApp(session, payload) {
|
|
2978
|
+
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/email-enrichment/companies`, {
|
|
2979
|
+
method: "POST",
|
|
2980
|
+
headers: {
|
|
2981
|
+
"Content-Type": "application/json",
|
|
2982
|
+
Authorization: `Bearer ${currentSession.accessToken}`
|
|
2983
|
+
},
|
|
2984
|
+
body: JSON.stringify(payload)
|
|
2985
|
+
}), CliEmailEnrichmentCompaniesResponseSchema);
|
|
2986
|
+
return value;
|
|
2987
|
+
}
|
|
2108
2988
|
async function fetchLinkedInCompaniesBackfillStatus(session, payload) {
|
|
2109
2989
|
const { value } = await fetchCliJson(session, (currentSession) => fetch(`${currentSession.apiBaseUrl}/api/cli/linkedin-companies/status?clientId=${encodeURIComponent(String(payload.clientId))}&containerId=${encodeURIComponent(payload.containerId)}`, {
|
|
2110
2990
|
method: "GET",
|
|
@@ -3667,12 +4547,127 @@ program.configureHelp({
|
|
|
3667
4547
|
});
|
|
3668
4548
|
program.addHelpText("after", `
|
|
3669
4549
|
LLM operator tips:
|
|
4550
|
+
- Install with: curl -fsSL https://docs.salesprompter.ai/install.sh | bash
|
|
4551
|
+
- First-run setup: salesprompter setup
|
|
4552
|
+
- Run a quick health check: salesprompter doctor
|
|
4553
|
+
- New here? Create your account at https://salesprompter.ai/sign-up, then run: salesprompter auth:login
|
|
3670
4554
|
- Prefer non-interactive auth: set SALESPROMPTER_TOKEN (+ optional SALESPROMPTER_API_BASE_URL).
|
|
3671
4555
|
- Use machine output for tools: add --json.
|
|
3672
4556
|
- One-shot leads flow: leads:pipeline --icp <path> --domain <company.com> --count <n>.
|
|
3673
4557
|
- Preview contact enrichment first: contacts:resolve-profiles --in <contacts.tsv> --dry-run.
|
|
3674
4558
|
- For bigger runs, start with a small sample before processing the full file.
|
|
3675
4559
|
`);
|
|
4560
|
+
program
|
|
4561
|
+
.command("setup")
|
|
4562
|
+
.description("Run the fastest first-run setup path for the Salesprompter CLI.")
|
|
4563
|
+
.option("--api-url <url>", "Salesprompter API base URL, defaults to SALESPROMPTER_API_BASE_URL or salesprompter.ai")
|
|
4564
|
+
.option("--timeout-seconds <number>", "Auth login timeout in seconds when setup needs to sign in", "180")
|
|
4565
|
+
.action(async (options) => {
|
|
4566
|
+
const timeoutSeconds = z.coerce.number().int().min(30).max(1800).parse(options.timeoutSeconds);
|
|
4567
|
+
printOutput({
|
|
4568
|
+
status: "ok",
|
|
4569
|
+
command: "setup",
|
|
4570
|
+
next: [
|
|
4571
|
+
"salesprompter auth:login",
|
|
4572
|
+
"salesprompter auth:whoami --verify",
|
|
4573
|
+
"salesprompter wizard"
|
|
4574
|
+
],
|
|
4575
|
+
docs: "https://docs.salesprompter.ai/quickstart"
|
|
4576
|
+
});
|
|
4577
|
+
if (process.stdin.isTTY && process.stdout.isTTY && !runtimeOutputOptions.json && !runtimeOutputOptions.quiet) {
|
|
4578
|
+
await runWizard({
|
|
4579
|
+
apiUrl: options.apiUrl,
|
|
4580
|
+
timeoutSeconds
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
program
|
|
4585
|
+
.command("doctor")
|
|
4586
|
+
.description("Check local CLI prerequisites, auth state, and optional enrichment setup.")
|
|
4587
|
+
.action(async () => {
|
|
4588
|
+
const nodeMajor = Number(process.versions.node.split(".")[0] ?? "0");
|
|
4589
|
+
const readiness = await resolveLlmAuthReadiness();
|
|
4590
|
+
const hasOpenAiKey = Boolean(process.env.OPENAI_API_KEY || process.env.SALESPROMPTER_OPENAI_API_KEY);
|
|
4591
|
+
const hasLinkedInSession = Boolean(process.env.LINKEDIN_CSRF_TOKEN ||
|
|
4592
|
+
process.env.SALESPROMPTER_LINKEDIN_CSRF_TOKEN ||
|
|
4593
|
+
process.env.LINKEDIN_X_LI_IDENTITY ||
|
|
4594
|
+
process.env.SALESPROMPTER_LINKEDIN_X_LI_IDENTITY ||
|
|
4595
|
+
process.env.LINKEDIN_SALES_NAV_COOKIE ||
|
|
4596
|
+
process.env.SALESPROMPTER_LINKEDIN_SALES_NAV_COOKIE);
|
|
4597
|
+
printOutput({
|
|
4598
|
+
status: "ok",
|
|
4599
|
+
command: "doctor",
|
|
4600
|
+
checks: {
|
|
4601
|
+
node: {
|
|
4602
|
+
ok: nodeMajor >= 20,
|
|
4603
|
+
current: process.versions.node,
|
|
4604
|
+
required: ">=20.0.0"
|
|
4605
|
+
},
|
|
4606
|
+
auth: {
|
|
4607
|
+
ok: readiness.ready,
|
|
4608
|
+
mode: readiness.mode,
|
|
4609
|
+
apiBaseUrl: readiness.apiBaseUrl,
|
|
4610
|
+
reason: readiness.reason ?? null
|
|
4611
|
+
},
|
|
4612
|
+
companyCleaningAi: {
|
|
4613
|
+
ok: hasOpenAiKey,
|
|
4614
|
+
envVarPresent: hasOpenAiKey
|
|
4615
|
+
},
|
|
4616
|
+
linkedInSession: {
|
|
4617
|
+
ok: hasLinkedInSession,
|
|
4618
|
+
envVarPresent: hasLinkedInSession
|
|
4619
|
+
}
|
|
4620
|
+
},
|
|
4621
|
+
recommended: [
|
|
4622
|
+
readiness.ready ? null : "salesprompter auth:login",
|
|
4623
|
+
hasOpenAiKey ? null : "Set OPENAI_API_KEY to enable --company-cleaning ai",
|
|
4624
|
+
hasLinkedInSession ? null : "Set LINKEDIN_CSRF_TOKEN, LINKEDIN_X_LI_IDENTITY, and LINKEDIN_SALES_NAV_COOKIE for direct LinkedIn lookup"
|
|
4625
|
+
].filter(Boolean)
|
|
4626
|
+
});
|
|
4627
|
+
});
|
|
4628
|
+
program
|
|
4629
|
+
.command("packs:list")
|
|
4630
|
+
.description("Show the product capability packs included in the CLI.")
|
|
4631
|
+
.action(() => {
|
|
4632
|
+
printOutput({
|
|
4633
|
+
status: "ok",
|
|
4634
|
+
packs: cliPacks
|
|
4635
|
+
});
|
|
4636
|
+
});
|
|
4637
|
+
program
|
|
4638
|
+
.command("packs:add")
|
|
4639
|
+
.description("Explain how to unlock a capability pack in the Salesprompter CLI.")
|
|
4640
|
+
.argument("<pack>", "Pack slug, for example contacts, research, discovery, or outreach")
|
|
4641
|
+
.action((pack) => {
|
|
4642
|
+
const normalized = String(pack).trim().toLowerCase();
|
|
4643
|
+
const match = cliPacks.find((entry) => entry.slug === normalized);
|
|
4644
|
+
if (!match) {
|
|
4645
|
+
throw new Error(`Unknown pack "${pack}". Run "salesprompter packs:list" to see the supported packs.`);
|
|
4646
|
+
}
|
|
4647
|
+
printOutput({
|
|
4648
|
+
status: "ok",
|
|
4649
|
+
pack: match.slug,
|
|
4650
|
+
title: match.title,
|
|
4651
|
+
available: true,
|
|
4652
|
+
installStatus: match.installStatus,
|
|
4653
|
+
commands: match.commands,
|
|
4654
|
+
message: `The ${match.title} pack is already included. Start with: salesprompter ${match.commands[0]}`
|
|
4655
|
+
});
|
|
4656
|
+
});
|
|
4657
|
+
program
|
|
4658
|
+
.command("upgrade")
|
|
4659
|
+
.description("Show the recommended upgrade command for the current installation.")
|
|
4660
|
+
.action(() => {
|
|
4661
|
+
printOutput({
|
|
4662
|
+
status: "ok",
|
|
4663
|
+
command: "upgrade",
|
|
4664
|
+
recommended: {
|
|
4665
|
+
npmGlobal: "npm i -g salesprompter-cli@latest",
|
|
4666
|
+
npx: "npx -y salesprompter-cli@latest",
|
|
4667
|
+
docs: "https://docs.salesprompter.ai/quickstart"
|
|
4668
|
+
}
|
|
4669
|
+
});
|
|
4670
|
+
});
|
|
3676
4671
|
program
|
|
3677
4672
|
.command("auth:login")
|
|
3678
4673
|
.description("Authenticate CLI with a Salesprompter app token, or device flow if the app supports it.")
|
|
@@ -3773,6 +4768,7 @@ program
|
|
|
3773
4768
|
.option("--org-id <id>", "Optional Sales Nav workspace org id for the lookup-first pass.")
|
|
3774
4769
|
.option("--external-user-id <id>", "Deprecated compatibility option. Ignored by the direct CLI lookup.")
|
|
3775
4770
|
.option("--timeout-ms <number>", "Lookup timeout in milliseconds", "30000")
|
|
4771
|
+
.option("--company-cleaning <mode>", "Company cleaning mode: off, basic, or ai", "basic")
|
|
3776
4772
|
.option("--dry-run", "Preview the normalized payload without calling LinkedIn", false)
|
|
3777
4773
|
.action(async (options) => {
|
|
3778
4774
|
const timeoutMs = z.coerce.number().int().min(1000).max(300000).parse(options.timeoutMs);
|
|
@@ -3791,12 +4787,15 @@ program
|
|
|
3791
4787
|
sessionOrgId = "";
|
|
3792
4788
|
}
|
|
3793
4789
|
}
|
|
3794
|
-
const
|
|
4790
|
+
const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
|
|
4791
|
+
const cleanedCompanyMap = await buildCompanyNameCleaningMap(rows, companyCleaningMode);
|
|
4792
|
+
const contacts = toLinkedInUrlLookupContacts(rows, cleanedCompanyMap);
|
|
3795
4793
|
if (options.dryRun) {
|
|
3796
4794
|
const payload = {
|
|
3797
4795
|
status: "ok",
|
|
3798
4796
|
dryRun: true,
|
|
3799
4797
|
orgId: String(options.orgId ?? "").trim() || null,
|
|
4798
|
+
companyCleaningMode,
|
|
3800
4799
|
contacts: contacts.length,
|
|
3801
4800
|
sample: contacts.slice(0, 5)
|
|
3802
4801
|
};
|
|
@@ -3831,11 +4830,41 @@ program
|
|
|
3831
4830
|
}
|
|
3832
4831
|
}
|
|
3833
4832
|
}
|
|
4833
|
+
try {
|
|
4834
|
+
const companyResult = await invokeLinkedInCompanyEnrichmentDirect({
|
|
4835
|
+
contacts,
|
|
4836
|
+
timeoutMs
|
|
4837
|
+
});
|
|
4838
|
+
const companyByContactId = new Map(companyResult.contacts.map((contact) => [
|
|
4839
|
+
contact.contact_id,
|
|
4840
|
+
{
|
|
4841
|
+
linkedinCompanyUrl: contact.linkedin_company_url ?? null,
|
|
4842
|
+
matchedCompanyName: contact.matched_company_name ?? null,
|
|
4843
|
+
matchedCompanyEmployeeCount: contact.matched_company_employee_count ?? null
|
|
4844
|
+
}
|
|
4845
|
+
]));
|
|
4846
|
+
for (const row of enrichedRows) {
|
|
4847
|
+
const company = companyByContactId.get(row.contactId);
|
|
4848
|
+
if (!company || row.linkedinCompanyUrl) {
|
|
4849
|
+
continue;
|
|
4850
|
+
}
|
|
4851
|
+
row.linkedinCompanyUrl = company.linkedinCompanyUrl;
|
|
4852
|
+
row.companyFound = Boolean(company.linkedinCompanyUrl);
|
|
4853
|
+
row.companySource = company.linkedinCompanyUrl ? "linkedin-direct" : row.companySource ?? null;
|
|
4854
|
+
row.matchedCompanyName = company.matchedCompanyName ?? row.matchedCompanyName ?? null;
|
|
4855
|
+
row.matchedCompanyEmployeeCount =
|
|
4856
|
+
company.matchedCompanyEmployeeCount ?? row.matchedCompanyEmployeeCount ?? null;
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
catch (error) {
|
|
4860
|
+
writeProgress(`Skipping separate company enrichment: ${error instanceof Error ? error.message : String(error)}`);
|
|
4861
|
+
}
|
|
3834
4862
|
const payload = {
|
|
3835
4863
|
status: "ok",
|
|
3836
4864
|
orgId: String(options.orgId ?? "").trim() || null,
|
|
3837
4865
|
requested: rows.length,
|
|
3838
4866
|
found: enrichedRows.filter((row) => row.found).length,
|
|
4867
|
+
companiesFound: enrichedRows.filter((row) => row.companyFound).length,
|
|
3839
4868
|
directAttempted,
|
|
3840
4869
|
rows: enrichedRows
|
|
3841
4870
|
};
|
|
@@ -3854,10 +4883,18 @@ program
|
|
|
3854
4883
|
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
3855
4884
|
applyGlobalOutputOptions(actionCommand);
|
|
3856
4885
|
const commandName = actionCommand.name();
|
|
4886
|
+
const parentCommandName = actionCommand.parent && typeof actionCommand.parent.name === "function"
|
|
4887
|
+
? actionCommand.parent.name()
|
|
4888
|
+
: "";
|
|
3857
4889
|
if (commandName.startsWith("auth:") ||
|
|
4890
|
+
commandName === "setup" ||
|
|
4891
|
+
commandName === "doctor" ||
|
|
4892
|
+
commandName === "upgrade" ||
|
|
3858
4893
|
commandName === "wizard" ||
|
|
3859
4894
|
commandName === "llm:ready" ||
|
|
3860
|
-
commandName === "contacts:find-linkedin-urls"
|
|
4895
|
+
commandName === "contacts:find-linkedin-urls" ||
|
|
4896
|
+
commandName.startsWith("packs:") ||
|
|
4897
|
+
((commandName === "list" || commandName === "add") && parentCommandName === "packs")) {
|
|
3861
4898
|
return;
|
|
3862
4899
|
}
|
|
3863
4900
|
const commandOptions = actionCommand.opts();
|
|
@@ -5818,6 +6855,123 @@ program
|
|
|
5818
6855
|
execution
|
|
5819
6856
|
});
|
|
5820
6857
|
});
|
|
6858
|
+
program
|
|
6859
|
+
.command("contacts:process-emails")
|
|
6860
|
+
.alias("contacts:resolve-emails")
|
|
6861
|
+
.alias("hunter-emailfinder:run:bq")
|
|
6862
|
+
.description("Process the next batch of contact email enrichment for a workspace.")
|
|
6863
|
+
.option("--client-id <number>", "Queue clientId to process")
|
|
6864
|
+
.option("--in <path>", "Optional TSV/CSV/JSON input path with company and contact rows")
|
|
6865
|
+
.option("--limit <number>", "Max queue rows to fetch", "500")
|
|
6866
|
+
.requiredOption("--out-dir <path>", "Output directory for artifacts")
|
|
6867
|
+
.option("--trace-id <traceId>", "Trace id for this batch")
|
|
6868
|
+
.option("--endpoint-url <url>", "Override the enrichment workflow endpoint")
|
|
6869
|
+
.option("--timeout-ms <number>", "Workflow trigger timeout in milliseconds", "30000")
|
|
6870
|
+
.option("--company-cleaning <mode>", "Company cleaning mode for direct input: off, basic, or ai", "basic")
|
|
6871
|
+
.option("--dry-run", "Preview the next batch without starting background processing", false)
|
|
6872
|
+
.action(async (options) => {
|
|
6873
|
+
const limit = z.coerce.number().int().min(1).max(50000).parse(options.limit);
|
|
6874
|
+
const outDir = z.string().min(1).parse(options.outDir);
|
|
6875
|
+
const timeoutMs = z.coerce.number().int().min(1000).max(300000).parse(options.timeoutMs);
|
|
6876
|
+
const usingDirectInput = typeof options.in === "string" && options.in.trim().length > 0;
|
|
6877
|
+
const companyCleaningMode = resolveCompanyCleaningMode(String(options.companyCleaning ?? process.env.SALESPROMPTER_COMPANY_CLEANING_MODE ?? "basic"));
|
|
6878
|
+
let clientId;
|
|
6879
|
+
let queueRows;
|
|
6880
|
+
let directReportRows = null;
|
|
6881
|
+
let queueSqlPath = null;
|
|
6882
|
+
let directReportPath = null;
|
|
6883
|
+
if (usingDirectInput) {
|
|
6884
|
+
const rawInput = await readFile(options.in, "utf8");
|
|
6885
|
+
const directBatch = await buildDirectEmailEnrichmentBatch({
|
|
6886
|
+
rawInput,
|
|
6887
|
+
clientIdOption: options.clientId,
|
|
6888
|
+
timeoutMs,
|
|
6889
|
+
companyCleaningMode
|
|
6890
|
+
});
|
|
6891
|
+
clientId = directBatch.clientId;
|
|
6892
|
+
queueRows = directBatch.queueRows.slice(0, limit);
|
|
6893
|
+
directReportRows = directBatch.reportRows;
|
|
6894
|
+
}
|
|
6895
|
+
else {
|
|
6896
|
+
clientId = z.coerce.number().int().positive().parse(options.clientId);
|
|
6897
|
+
queueSqlPath = `${outDir}/email-enrichment-input-${clientId}.sql`;
|
|
6898
|
+
const queueSql = buildHunterEmailfinderQueueSql(clientId, limit);
|
|
6899
|
+
await writeTextFile(queueSqlPath, `${queueSql}\n`);
|
|
6900
|
+
const rawQueueRows = await runBigQueryRows(queueSql, { maxRows: limit });
|
|
6901
|
+
queueRows = hunterEmailfinderQueueRowArraySchema.parse(rawQueueRows);
|
|
6902
|
+
}
|
|
6903
|
+
const traceId = z.string().min(1).parse(options.traceId ?? `salesprompter-cli-email-${clientId}-${Date.now()}`);
|
|
6904
|
+
const queueRowsPath = `${outDir}/email-enrichment-input-${clientId}.json`;
|
|
6905
|
+
const requestPath = `${outDir}/email-enrichment-request-${clientId}.json`;
|
|
6906
|
+
const responsePath = `${outDir}/email-enrichment-response-${clientId}.json`;
|
|
6907
|
+
await writeJsonFile(queueRowsPath, queueRows);
|
|
6908
|
+
if (directReportRows) {
|
|
6909
|
+
directReportPath = `${outDir}/email-enrichment-direct-${clientId}.json`;
|
|
6910
|
+
await writeJsonFile(directReportPath, {
|
|
6911
|
+
clientId,
|
|
6912
|
+
companyCleaningMode,
|
|
6913
|
+
rows: directReportRows
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
6916
|
+
const payload = buildHunterEmailfinderTriggerPayload({
|
|
6917
|
+
traceId,
|
|
6918
|
+
clientId: String(clientId),
|
|
6919
|
+
queueRows
|
|
6920
|
+
});
|
|
6921
|
+
await writeJsonFile(requestPath, {
|
|
6922
|
+
traceId,
|
|
6923
|
+
clientId,
|
|
6924
|
+
queueRows: queueRows.length,
|
|
6925
|
+
payload
|
|
6926
|
+
});
|
|
6927
|
+
let trigger = null;
|
|
6928
|
+
if (!options.dryRun && queueRows.length > 0) {
|
|
6929
|
+
const config = readHunterEmailfinderConfig(process.env, options.endpointUrl);
|
|
6930
|
+
const result = await triggerHunterEmailfinderWorkflow({
|
|
6931
|
+
config,
|
|
6932
|
+
externalUserId: `email-enrichment-client-${clientId}`,
|
|
6933
|
+
timeoutMs,
|
|
6934
|
+
payload
|
|
6935
|
+
});
|
|
6936
|
+
trigger = {
|
|
6937
|
+
triggered: result.response.ok,
|
|
6938
|
+
status: result.response.status
|
|
6939
|
+
};
|
|
6940
|
+
await writeJsonFile(responsePath, {
|
|
6941
|
+
traceId,
|
|
6942
|
+
clientId,
|
|
6943
|
+
status: result.response.status,
|
|
6944
|
+
ok: result.response.ok,
|
|
6945
|
+
endpoint: result.endpoint,
|
|
6946
|
+
body: result.parsedBody
|
|
6947
|
+
});
|
|
6948
|
+
if (!result.response.ok) {
|
|
6949
|
+
throw new Error(`Email enrichment workflow returned ${result.response.status}: ${result.bodyText || "No response body"}`);
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
printOutput({
|
|
6953
|
+
status: "ok",
|
|
6954
|
+
clientId,
|
|
6955
|
+
limit,
|
|
6956
|
+
traceId,
|
|
6957
|
+
dryRun: Boolean(options.dryRun),
|
|
6958
|
+
mode: usingDirectInput ? "direct-input" : "workspace-queue",
|
|
6959
|
+
companyCleaningMode: usingDirectInput ? companyCleaningMode : null,
|
|
6960
|
+
contactsRead: directReportRows?.length ?? null,
|
|
6961
|
+
contactsQueued: queueRows.length,
|
|
6962
|
+
started: Boolean(trigger?.triggered),
|
|
6963
|
+
profilesFound: directReportRows ? directReportRows.filter((row) => row.linkedinUrl).length : null,
|
|
6964
|
+
companiesFound: directReportRows ? directReportRows.filter((row) => row.linkedinCompanyUrl).length : null,
|
|
6965
|
+
domainsFound: directReportRows ? directReportRows.filter((row) => row.domain).length : null,
|
|
6966
|
+
artifacts: {
|
|
6967
|
+
inputSqlPath: queueSqlPath,
|
|
6968
|
+
inputRowsPath: queueRowsPath,
|
|
6969
|
+
directReportPath,
|
|
6970
|
+
requestPath,
|
|
6971
|
+
responsePath: trigger ? responsePath : null
|
|
6972
|
+
}
|
|
6973
|
+
});
|
|
6974
|
+
});
|
|
5821
6975
|
async function main() {
|
|
5822
6976
|
if (shouldAutoRunWizard(process.argv)) {
|
|
5823
6977
|
await runWizard();
|