gtm-now 0.10.6 → 0.10.8
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/dist/index.js +706 -332
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -205,7 +205,8 @@ var init_config = __esm({
|
|
|
205
205
|
GTM_SMARTLEAD_API_KEY: "smartlead",
|
|
206
206
|
GTM_HARVEST_API_KEY: "harvest",
|
|
207
207
|
GTM_STORELEADS_API_KEY: "storeleads",
|
|
208
|
-
GTM_EXA_API_KEY: "exa"
|
|
208
|
+
GTM_EXA_API_KEY: "exa",
|
|
209
|
+
GTM_LEADMAGIC_API_KEY: "leadmagic"
|
|
209
210
|
};
|
|
210
211
|
ENV_WORKSPACE_MAP = {
|
|
211
212
|
GTM_PLUSVIBE_WORKSPACE_ID: "plusvibe"
|
|
@@ -8737,6 +8738,111 @@ var init_client25 = __esm({
|
|
|
8737
8738
|
}
|
|
8738
8739
|
});
|
|
8739
8740
|
|
|
8741
|
+
// ../integrations/dist/leadmagic.js
|
|
8742
|
+
var LeadMagicClient;
|
|
8743
|
+
var init_leadmagic2 = __esm({
|
|
8744
|
+
"../integrations/dist/leadmagic.js"() {
|
|
8745
|
+
"use strict";
|
|
8746
|
+
init_base_client();
|
|
8747
|
+
LeadMagicClient = class extends BaseApiClient {
|
|
8748
|
+
constructor(config) {
|
|
8749
|
+
super({
|
|
8750
|
+
name: "LeadMagic",
|
|
8751
|
+
baseUrl: config.baseUrl ?? "https://api.leadmagic.io",
|
|
8752
|
+
headers: { "X-API-Key": config.apiKey },
|
|
8753
|
+
timeoutMs: config.timeoutMs
|
|
8754
|
+
});
|
|
8755
|
+
}
|
|
8756
|
+
// ─── People ──────────────────────────────────────────────────────────────────
|
|
8757
|
+
async findEmail(params) {
|
|
8758
|
+
return this.request("/v1/people/email-finder", {
|
|
8759
|
+
method: "POST",
|
|
8760
|
+
body: JSON.stringify(params)
|
|
8761
|
+
});
|
|
8762
|
+
}
|
|
8763
|
+
async validateEmail(params) {
|
|
8764
|
+
return this.request("/v1/people/email-validation", {
|
|
8765
|
+
method: "POST",
|
|
8766
|
+
body: JSON.stringify(params)
|
|
8767
|
+
});
|
|
8768
|
+
}
|
|
8769
|
+
async findMobile(params) {
|
|
8770
|
+
return this.request("/v1/people/mobile-finder", {
|
|
8771
|
+
method: "POST",
|
|
8772
|
+
body: JSON.stringify(params)
|
|
8773
|
+
});
|
|
8774
|
+
}
|
|
8775
|
+
async searchProfile(params) {
|
|
8776
|
+
return this.request("/v1/people/profile-search", {
|
|
8777
|
+
method: "POST",
|
|
8778
|
+
body: JSON.stringify(params)
|
|
8779
|
+
});
|
|
8780
|
+
}
|
|
8781
|
+
async findEmployees(params) {
|
|
8782
|
+
return this.request("/v1/people/employee-finder", {
|
|
8783
|
+
method: "POST",
|
|
8784
|
+
body: JSON.stringify(params)
|
|
8785
|
+
});
|
|
8786
|
+
}
|
|
8787
|
+
async findRole(params) {
|
|
8788
|
+
return this.request("/v1/people/role-finder", {
|
|
8789
|
+
method: "POST",
|
|
8790
|
+
body: JSON.stringify(params)
|
|
8791
|
+
});
|
|
8792
|
+
}
|
|
8793
|
+
async emailToProfile(params) {
|
|
8794
|
+
return this.request("/v1/people/b2b-profile", {
|
|
8795
|
+
method: "POST",
|
|
8796
|
+
body: JSON.stringify(params)
|
|
8797
|
+
});
|
|
8798
|
+
}
|
|
8799
|
+
async profileToEmail(params) {
|
|
8800
|
+
return this.request("/v1/people/b2b-profile-email", {
|
|
8801
|
+
method: "POST",
|
|
8802
|
+
body: JSON.stringify(params)
|
|
8803
|
+
});
|
|
8804
|
+
}
|
|
8805
|
+
async findPersonalEmail(params) {
|
|
8806
|
+
return this.request("/v1/people/personal-email-finder", {
|
|
8807
|
+
method: "POST",
|
|
8808
|
+
body: JSON.stringify(params)
|
|
8809
|
+
});
|
|
8810
|
+
}
|
|
8811
|
+
async detectJobChange(params) {
|
|
8812
|
+
return this.request("/v1/people/job-change-detector", {
|
|
8813
|
+
method: "POST",
|
|
8814
|
+
body: JSON.stringify(params)
|
|
8815
|
+
});
|
|
8816
|
+
}
|
|
8817
|
+
// ─── Companies ───────────────────────────────────────────────────────────────
|
|
8818
|
+
async searchCompany(params) {
|
|
8819
|
+
return this.request("/v1/companies/company-search", {
|
|
8820
|
+
method: "POST",
|
|
8821
|
+
body: JSON.stringify(params)
|
|
8822
|
+
});
|
|
8823
|
+
}
|
|
8824
|
+
async searchCompetitors(params) {
|
|
8825
|
+
return this.request("/v1/companies/competitors-search", {
|
|
8826
|
+
method: "POST",
|
|
8827
|
+
body: JSON.stringify(params)
|
|
8828
|
+
});
|
|
8829
|
+
}
|
|
8830
|
+
async getCompanyFunding(params) {
|
|
8831
|
+
return this.request("/v1/companies/company-funding", {
|
|
8832
|
+
method: "POST",
|
|
8833
|
+
body: JSON.stringify(params)
|
|
8834
|
+
});
|
|
8835
|
+
}
|
|
8836
|
+
async getTechnographics(params) {
|
|
8837
|
+
return this.request("/v1/companies/technographics", {
|
|
8838
|
+
method: "POST",
|
|
8839
|
+
body: JSON.stringify(params)
|
|
8840
|
+
});
|
|
8841
|
+
}
|
|
8842
|
+
};
|
|
8843
|
+
}
|
|
8844
|
+
});
|
|
8845
|
+
|
|
8740
8846
|
// ../integrations/dist/index.js
|
|
8741
8847
|
var init_dist2 = __esm({
|
|
8742
8848
|
"../integrations/dist/index.js"() {
|
|
@@ -8778,6 +8884,7 @@ var init_dist2 = __esm({
|
|
|
8778
8884
|
init_factory();
|
|
8779
8885
|
init_client24();
|
|
8780
8886
|
init_client25();
|
|
8887
|
+
init_leadmagic2();
|
|
8781
8888
|
}
|
|
8782
8889
|
});
|
|
8783
8890
|
|
|
@@ -9481,13 +9588,14 @@ function mapSeniority(values) {
|
|
|
9481
9588
|
function isApiClientError(error, statusCode) {
|
|
9482
9589
|
return error instanceof Error && "statusCode" in error && error.statusCode === statusCode;
|
|
9483
9590
|
}
|
|
9484
|
-
var PROSPEO_PAGE_SIZE, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
|
|
9591
|
+
var PROSPEO_PAGE_SIZE, MAX_PAGES, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
|
|
9485
9592
|
var init_prospeo2 = __esm({
|
|
9486
9593
|
"src/providers/adapters/prospeo.ts"() {
|
|
9487
9594
|
"use strict";
|
|
9488
9595
|
init_dist();
|
|
9489
9596
|
init_dist2();
|
|
9490
9597
|
PROSPEO_PAGE_SIZE = 25;
|
|
9598
|
+
MAX_PAGES = 40;
|
|
9491
9599
|
SENIORITY_TO_PROSPEO = {
|
|
9492
9600
|
c_suite: "C-Suite",
|
|
9493
9601
|
founder: "Founder/Owner",
|
|
@@ -9577,24 +9685,33 @@ var init_prospeo2 = __esm({
|
|
|
9577
9685
|
{ statusCode: 400 }
|
|
9578
9686
|
);
|
|
9579
9687
|
}
|
|
9580
|
-
const
|
|
9688
|
+
const requestedLimit = query.limit ?? PROSPEO_PAGE_SIZE;
|
|
9689
|
+
const startPage = query.offset ? Math.floor(query.offset / PROSPEO_PAGE_SIZE) + 1 : 1;
|
|
9581
9690
|
try {
|
|
9582
|
-
const
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9691
|
+
const allResults = [];
|
|
9692
|
+
let page = startPage;
|
|
9693
|
+
while (allResults.length < requestedLimit && page < startPage + MAX_PAGES) {
|
|
9694
|
+
const response = await this.client.searchPeople({ page, filters });
|
|
9695
|
+
if (response.error || !response.results?.length) break;
|
|
9696
|
+
const mapped = response.results.map((r) => ({
|
|
9697
|
+
first_name: r.person?.first_name ?? "",
|
|
9698
|
+
last_name: r.person?.last_name ?? "",
|
|
9699
|
+
job_title: r.person?.current_job_title ?? void 0,
|
|
9700
|
+
company: r.company?.name ?? void 0,
|
|
9701
|
+
company_domain: r.company?.domain ?? void 0,
|
|
9702
|
+
email: r.person?.email?.email ?? void 0,
|
|
9703
|
+
phone: r.person?.mobile?.mobile ?? void 0,
|
|
9704
|
+
linkedin_url: r.person?.linkedin_url ?? void 0,
|
|
9705
|
+
seniority: r.person?.job_history?.[0]?.seniority ?? void 0,
|
|
9706
|
+
department: r.person?.job_history?.[0]?.departments?.[0] ?? void 0,
|
|
9707
|
+
location: r.person?.location ? [r.person.location.city, r.person.location.state, r.person.location.country].filter(Boolean).join(", ") : void 0,
|
|
9708
|
+
sources: [this.name]
|
|
9709
|
+
}));
|
|
9710
|
+
allResults.push(...mapped);
|
|
9711
|
+
if (response.results.length < PROSPEO_PAGE_SIZE) break;
|
|
9712
|
+
page++;
|
|
9713
|
+
}
|
|
9714
|
+
return allResults.slice(0, requestedLimit);
|
|
9598
9715
|
} catch (error) {
|
|
9599
9716
|
if (isApiClientError(error, 400)) {
|
|
9600
9717
|
const body = error.responseBody;
|
|
@@ -10539,6 +10656,193 @@ var init_exa = __esm({
|
|
|
10539
10656
|
}
|
|
10540
10657
|
});
|
|
10541
10658
|
|
|
10659
|
+
// src/providers/adapters/leadmagic.ts
|
|
10660
|
+
function mapEmailQuality(status) {
|
|
10661
|
+
switch (status) {
|
|
10662
|
+
case "valid":
|
|
10663
|
+
return "95%";
|
|
10664
|
+
case "valid_catch_all":
|
|
10665
|
+
return "80%";
|
|
10666
|
+
case "catch_all":
|
|
10667
|
+
return "60%";
|
|
10668
|
+
default:
|
|
10669
|
+
return "50%";
|
|
10670
|
+
}
|
|
10671
|
+
}
|
|
10672
|
+
function mapVerificationStatus(status) {
|
|
10673
|
+
switch (status) {
|
|
10674
|
+
case "valid":
|
|
10675
|
+
return "deliverable";
|
|
10676
|
+
case "valid_catch_all":
|
|
10677
|
+
return "catch_all_safe";
|
|
10678
|
+
case "catch_all":
|
|
10679
|
+
return "catch_all_not_safe";
|
|
10680
|
+
case "invalid":
|
|
10681
|
+
return "undeliverable";
|
|
10682
|
+
default:
|
|
10683
|
+
return "unknown";
|
|
10684
|
+
}
|
|
10685
|
+
}
|
|
10686
|
+
var LeadMagicAdapter2;
|
|
10687
|
+
var init_leadmagic3 = __esm({
|
|
10688
|
+
"src/providers/adapters/leadmagic.ts"() {
|
|
10689
|
+
"use strict";
|
|
10690
|
+
init_dist();
|
|
10691
|
+
init_dist2();
|
|
10692
|
+
LeadMagicAdapter2 = class {
|
|
10693
|
+
name = "leadmagic";
|
|
10694
|
+
capabilities = [
|
|
10695
|
+
"enrich_email",
|
|
10696
|
+
"enrich_phone",
|
|
10697
|
+
"verify_email",
|
|
10698
|
+
"find_people"
|
|
10699
|
+
];
|
|
10700
|
+
notes = "Best for: email enrichment (name+domain), email validation (0.05 credits), mobile finder (needs LinkedIn URL, 5 credits), employee lists (0.05/employee). Also supports: profile search, company search, technographics, funding, competitors, job change detection. Pay-on-find pricing \u2014 free when no result.";
|
|
10701
|
+
client;
|
|
10702
|
+
constructor(apiKey) {
|
|
10703
|
+
this.client = new LeadMagicClient({ apiKey });
|
|
10704
|
+
}
|
|
10705
|
+
async checkCredentials() {
|
|
10706
|
+
try {
|
|
10707
|
+
await this.client.validateEmail({ email: "test@example.com" });
|
|
10708
|
+
return true;
|
|
10709
|
+
} catch (error) {
|
|
10710
|
+
logError(
|
|
10711
|
+
"leadmagic:checkCredentials",
|
|
10712
|
+
error instanceof Error ? error : new Error(String(error))
|
|
10713
|
+
);
|
|
10714
|
+
return false;
|
|
10715
|
+
}
|
|
10716
|
+
}
|
|
10717
|
+
async checkCredits() {
|
|
10718
|
+
return null;
|
|
10719
|
+
}
|
|
10720
|
+
// ─── EmailEnricher ──────────────────────────────────────
|
|
10721
|
+
async enrichEmail(contact) {
|
|
10722
|
+
try {
|
|
10723
|
+
if (contact.first_name && contact.last_name && (contact.company_domain || contact.company)) {
|
|
10724
|
+
const response = await this.client.findEmail({
|
|
10725
|
+
first_name: contact.first_name,
|
|
10726
|
+
last_name: contact.last_name,
|
|
10727
|
+
domain: contact.company_domain,
|
|
10728
|
+
company_name: !contact.company_domain ? contact.company : void 0
|
|
10729
|
+
});
|
|
10730
|
+
if (response.email && response.status !== "not_found") {
|
|
10731
|
+
return {
|
|
10732
|
+
email: response.email,
|
|
10733
|
+
quality: mapEmailQuality(response.status),
|
|
10734
|
+
provider: this.name
|
|
10735
|
+
};
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
if (contact.linkedin_url) {
|
|
10739
|
+
const response = await this.client.profileToEmail({
|
|
10740
|
+
profile_url: contact.linkedin_url
|
|
10741
|
+
});
|
|
10742
|
+
if (response.email) {
|
|
10743
|
+
return {
|
|
10744
|
+
email: response.email,
|
|
10745
|
+
quality: "80%",
|
|
10746
|
+
// Profile-to-email doesn't return validation status
|
|
10747
|
+
provider: this.name
|
|
10748
|
+
};
|
|
10749
|
+
}
|
|
10750
|
+
}
|
|
10751
|
+
return null;
|
|
10752
|
+
} catch (error) {
|
|
10753
|
+
logError("leadmagic:enrichEmail", error instanceof Error ? error : new Error(String(error)));
|
|
10754
|
+
return null;
|
|
10755
|
+
}
|
|
10756
|
+
}
|
|
10757
|
+
// ─── PhoneEnricher ──────────────────────────────────────
|
|
10758
|
+
async enrichPhone(contact) {
|
|
10759
|
+
try {
|
|
10760
|
+
const response = await this.client.findMobile({
|
|
10761
|
+
profile_url: contact.linkedin_url,
|
|
10762
|
+
work_email: void 0,
|
|
10763
|
+
// Could pass email if available
|
|
10764
|
+
personal_email: void 0
|
|
10765
|
+
});
|
|
10766
|
+
if (!response.mobile_number) return null;
|
|
10767
|
+
return {
|
|
10768
|
+
phone: response.mobile_number,
|
|
10769
|
+
provider: this.name
|
|
10770
|
+
};
|
|
10771
|
+
} catch (error) {
|
|
10772
|
+
logError("leadmagic:enrichPhone", error instanceof Error ? error : new Error(String(error)));
|
|
10773
|
+
return null;
|
|
10774
|
+
}
|
|
10775
|
+
}
|
|
10776
|
+
// ─── EmailVerifier ──────────────────────────────────────
|
|
10777
|
+
async verifyEmail(email) {
|
|
10778
|
+
try {
|
|
10779
|
+
const response = await this.client.validateEmail({ email });
|
|
10780
|
+
return {
|
|
10781
|
+
email: response.email ?? email,
|
|
10782
|
+
valid: response.email_status === "valid" || response.email_status === "valid_catch_all",
|
|
10783
|
+
status: mapVerificationStatus(response.email_status),
|
|
10784
|
+
provider: this.name
|
|
10785
|
+
};
|
|
10786
|
+
} catch (error) {
|
|
10787
|
+
logError("leadmagic:verifyEmail", error instanceof Error ? error : new Error(String(error)));
|
|
10788
|
+
return {
|
|
10789
|
+
email,
|
|
10790
|
+
valid: false,
|
|
10791
|
+
status: "unknown",
|
|
10792
|
+
provider: this.name
|
|
10793
|
+
};
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
// ─── PeopleFinder ───────────────────────────────────────
|
|
10797
|
+
async findPeople(query) {
|
|
10798
|
+
try {
|
|
10799
|
+
if (query.job_title && (query.company_domain || query.company_name)) {
|
|
10800
|
+
const response = await this.client.findRole({
|
|
10801
|
+
job_title: query.job_title,
|
|
10802
|
+
company_domain: query.company_domain,
|
|
10803
|
+
company_name: !query.company_domain ? query.company_name : void 0
|
|
10804
|
+
});
|
|
10805
|
+
if (response.first_name && response.last_name) {
|
|
10806
|
+
return [
|
|
10807
|
+
{
|
|
10808
|
+
first_name: response.first_name,
|
|
10809
|
+
last_name: response.last_name,
|
|
10810
|
+
job_title: response.job_title ?? void 0,
|
|
10811
|
+
company: response.company_name ?? void 0,
|
|
10812
|
+
company_domain: response.company_website ?? query.company_domain ?? void 0,
|
|
10813
|
+
linkedin_url: response.profile_url ?? void 0,
|
|
10814
|
+
sources: [this.name]
|
|
10815
|
+
}
|
|
10816
|
+
];
|
|
10817
|
+
}
|
|
10818
|
+
return [];
|
|
10819
|
+
}
|
|
10820
|
+
if (query.company_domain || query.company_name) {
|
|
10821
|
+
const response = await this.client.findEmployees({
|
|
10822
|
+
company_domain: query.company_domain,
|
|
10823
|
+
company_name: !query.company_domain ? query.company_name : void 0,
|
|
10824
|
+
limit: query.limit ?? 20
|
|
10825
|
+
});
|
|
10826
|
+
if (!response.data?.length) return [];
|
|
10827
|
+
return response.data.map((emp) => ({
|
|
10828
|
+
first_name: emp.first_name ?? "",
|
|
10829
|
+
last_name: emp.last_name ?? "",
|
|
10830
|
+
job_title: emp.title ?? void 0,
|
|
10831
|
+
company: emp.company_name ?? void 0,
|
|
10832
|
+
company_domain: emp.website ?? query.company_domain ?? void 0,
|
|
10833
|
+
sources: [this.name]
|
|
10834
|
+
}));
|
|
10835
|
+
}
|
|
10836
|
+
return [];
|
|
10837
|
+
} catch (error) {
|
|
10838
|
+
logError("leadmagic:findPeople", error instanceof Error ? error : new Error(String(error)));
|
|
10839
|
+
return [];
|
|
10840
|
+
}
|
|
10841
|
+
}
|
|
10842
|
+
};
|
|
10843
|
+
}
|
|
10844
|
+
});
|
|
10845
|
+
|
|
10542
10846
|
// src/providers/adapters/index.ts
|
|
10543
10847
|
var init_adapters = __esm({
|
|
10544
10848
|
"src/providers/adapters/index.ts"() {
|
|
@@ -10556,6 +10860,7 @@ var init_adapters = __esm({
|
|
|
10556
10860
|
init_storeleads();
|
|
10557
10861
|
init_harvest();
|
|
10558
10862
|
init_exa();
|
|
10863
|
+
init_leadmagic3();
|
|
10559
10864
|
}
|
|
10560
10865
|
});
|
|
10561
10866
|
|
|
@@ -10588,6 +10893,8 @@ function createAdapter(name, config) {
|
|
|
10588
10893
|
return config.api_key ? new HarvestApiAdapter(config.api_key) : null;
|
|
10589
10894
|
case "exa":
|
|
10590
10895
|
return config.api_key ? new ExaAdapter(config.api_key) : null;
|
|
10896
|
+
case "leadmagic":
|
|
10897
|
+
return config.api_key ? new LeadMagicAdapter2(config.api_key) : null;
|
|
10591
10898
|
default:
|
|
10592
10899
|
return null;
|
|
10593
10900
|
}
|
|
@@ -10890,7 +11197,7 @@ var init_prospect = __esm({
|
|
|
10890
11197
|
},
|
|
10891
11198
|
{
|
|
10892
11199
|
name: "gtm_find_people",
|
|
10893
|
-
description: 'Find contacts by job title, company, or domain. Returns name, title, email, LinkedIn URL. Slim by default; pass fields: "all" for full payload.',
|
|
11200
|
+
description: 'Find contacts by job title, company, or domain. Returns name, title, email, LinkedIn URL. Slim by default; pass fields: "all" for full payload. Pass enrich: true to resolve full emails inline (avoids a separate gtm_enrich call).',
|
|
10894
11201
|
inputSchema: {
|
|
10895
11202
|
type: "object",
|
|
10896
11203
|
properties: {
|
|
@@ -10979,6 +11286,10 @@ var init_prospect = __esm({
|
|
|
10979
11286
|
type: "string",
|
|
10980
11287
|
description: 'Company revenue range as "min,max" (e.g., "1000000,10000000")'
|
|
10981
11288
|
},
|
|
11289
|
+
enrich: {
|
|
11290
|
+
type: "boolean",
|
|
11291
|
+
description: "Resolve full email addresses inline. Some providers return masked emails (e.g., j***@company.com) in search results. Pass enrich: true to run waterfall enrichment on those contacts automatically, so you get real emails without a separate gtm_enrich call."
|
|
11292
|
+
},
|
|
10982
11293
|
fields: {
|
|
10983
11294
|
description: 'Response fields. Defaults to slim (name, title, email, company, company_domain, linkedin). Pass "all" for full payload including skills, phone, seniority, department, connections. Or pass an array of specific field names.',
|
|
10984
11295
|
oneOf: [
|
|
@@ -11872,61 +12183,342 @@ For any task: always dry_run enrichment batches > 5 to preview cost before execu
|
|
|
11872
12183
|
}
|
|
11873
12184
|
});
|
|
11874
12185
|
|
|
11875
|
-
// src/providers/
|
|
11876
|
-
function
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
if (field in obj) {
|
|
11880
|
-
result[field] = obj[field];
|
|
11881
|
-
}
|
|
11882
|
-
}
|
|
11883
|
-
return result;
|
|
12186
|
+
// src/providers/waterfall.ts
|
|
12187
|
+
function getCacheKey(contact) {
|
|
12188
|
+
if (!contact.linkedin_url) return null;
|
|
12189
|
+
return contact.linkedin_url.toLowerCase().trim();
|
|
11884
12190
|
}
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
"industry",
|
|
11893
|
-
"employees",
|
|
11894
|
-
"country"
|
|
11895
|
-
];
|
|
11896
|
-
SLIM_PERSON_FIELDS = [
|
|
11897
|
-
"first_name",
|
|
11898
|
-
"last_name",
|
|
11899
|
-
"job_title",
|
|
11900
|
-
"email",
|
|
11901
|
-
"company",
|
|
11902
|
-
"company_domain",
|
|
11903
|
-
"linkedin_url"
|
|
11904
|
-
];
|
|
12191
|
+
function isExhaustedError(error) {
|
|
12192
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
12193
|
+
return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
|
|
12194
|
+
}
|
|
12195
|
+
function isRateLimitError(error) {
|
|
12196
|
+
if (error instanceof Error && "status" in error) {
|
|
12197
|
+
return error.status === 429;
|
|
11905
12198
|
}
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
function
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
12199
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
12200
|
+
return msg.includes("429") || msg.toLowerCase().includes("rate limit");
|
|
12201
|
+
}
|
|
12202
|
+
function withTimeout(promise, ms, label) {
|
|
12203
|
+
return new Promise((resolve, reject) => {
|
|
12204
|
+
const timer = setTimeout(
|
|
12205
|
+
() => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
|
|
12206
|
+
ms
|
|
12207
|
+
);
|
|
12208
|
+
promise.then(
|
|
12209
|
+
(val) => {
|
|
12210
|
+
clearTimeout(timer);
|
|
12211
|
+
resolve(val);
|
|
12212
|
+
},
|
|
12213
|
+
(err) => {
|
|
12214
|
+
clearTimeout(timer);
|
|
12215
|
+
reject(err);
|
|
12216
|
+
}
|
|
12217
|
+
);
|
|
12218
|
+
});
|
|
12219
|
+
}
|
|
12220
|
+
async function runWaterfall(contacts, config, registry) {
|
|
12221
|
+
const exhausted = /* @__PURE__ */ new Set();
|
|
12222
|
+
const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
|
|
12223
|
+
const concurrency = config.concurrency ?? 1;
|
|
12224
|
+
if (concurrency <= 1) {
|
|
12225
|
+
const results2 = [];
|
|
12226
|
+
for (const contact of contacts) {
|
|
12227
|
+
if (Date.now() > batchDeadline) {
|
|
12228
|
+
results2.push(makeTimeoutResult(contact));
|
|
12229
|
+
continue;
|
|
12230
|
+
}
|
|
12231
|
+
const result = await enrichContact(contact, config, registry, exhausted);
|
|
12232
|
+
results2.push(result);
|
|
12233
|
+
}
|
|
12234
|
+
return results2;
|
|
12235
|
+
}
|
|
12236
|
+
const results = new Array(contacts.length);
|
|
12237
|
+
let nextIndex = 0;
|
|
12238
|
+
async function worker() {
|
|
12239
|
+
while (nextIndex < contacts.length) {
|
|
12240
|
+
const i = nextIndex++;
|
|
12241
|
+
if (Date.now() > batchDeadline) {
|
|
12242
|
+
results[i] = makeTimeoutResult(contacts[i]);
|
|
12243
|
+
continue;
|
|
12244
|
+
}
|
|
12245
|
+
results[i] = await enrichContact(contacts[i], config, registry, exhausted);
|
|
11921
12246
|
}
|
|
11922
12247
|
}
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
12248
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, contacts.length) }, () => worker()));
|
|
12249
|
+
return results;
|
|
12250
|
+
}
|
|
12251
|
+
function makeTimeoutResult(contact) {
|
|
12252
|
+
return {
|
|
12253
|
+
contact,
|
|
12254
|
+
attempts: [
|
|
12255
|
+
{
|
|
12256
|
+
provider: "waterfall",
|
|
12257
|
+
capability: "enrich_email",
|
|
12258
|
+
found: false,
|
|
12259
|
+
charged: false,
|
|
12260
|
+
duration_ms: 0,
|
|
12261
|
+
error: "Batch timeout exceeded \u2014 skipped remaining contacts"
|
|
12262
|
+
}
|
|
12263
|
+
],
|
|
12264
|
+
charged: false
|
|
12265
|
+
};
|
|
12266
|
+
}
|
|
12267
|
+
async function enrichContact(contact, config, registry, exhausted) {
|
|
12268
|
+
const attempts = [];
|
|
12269
|
+
let email;
|
|
12270
|
+
let emailProvider;
|
|
12271
|
+
let phone;
|
|
12272
|
+
let phoneProvider;
|
|
12273
|
+
let charged = false;
|
|
12274
|
+
const cacheKey = getCacheKey(contact);
|
|
12275
|
+
const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
|
|
12276
|
+
for (const field of config.find) {
|
|
12277
|
+
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12278
|
+
const cachedValue = cached?.[field];
|
|
12279
|
+
if (cachedValue) {
|
|
12280
|
+
if (field === "email") {
|
|
12281
|
+
email = cachedValue;
|
|
12282
|
+
emailProvider = cached.provider;
|
|
12283
|
+
}
|
|
12284
|
+
if (field === "phone") {
|
|
12285
|
+
phone = cachedValue;
|
|
12286
|
+
phoneProvider = cached.provider;
|
|
12287
|
+
}
|
|
12288
|
+
attempts.push({
|
|
12289
|
+
provider: cached.provider,
|
|
12290
|
+
capability,
|
|
12291
|
+
found: true,
|
|
12292
|
+
charged: false,
|
|
12293
|
+
duration_ms: 0,
|
|
12294
|
+
cached: true
|
|
12295
|
+
});
|
|
12296
|
+
continue;
|
|
12297
|
+
}
|
|
12298
|
+
const fieldResult = await enrichField(contact, field, config, registry, exhausted);
|
|
12299
|
+
attempts.push(...fieldResult.attempts);
|
|
12300
|
+
if (field === "email" && fieldResult.value) {
|
|
12301
|
+
email = fieldResult.value;
|
|
12302
|
+
emailProvider = fieldResult.provider;
|
|
12303
|
+
charged = true;
|
|
12304
|
+
} else if (field === "phone" && fieldResult.value) {
|
|
12305
|
+
phone = fieldResult.value;
|
|
12306
|
+
phoneProvider = fieldResult.provider;
|
|
12307
|
+
charged = true;
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
if (cacheKey && (email || phone)) {
|
|
12311
|
+
const existing = enrichmentCache.get(cacheKey);
|
|
12312
|
+
enrichmentCache.set(cacheKey, {
|
|
12313
|
+
email: email ?? existing?.email,
|
|
12314
|
+
phone: phone ?? existing?.phone,
|
|
12315
|
+
provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
|
|
12316
|
+
timestamp: Date.now()
|
|
12317
|
+
});
|
|
12318
|
+
}
|
|
12319
|
+
return {
|
|
12320
|
+
contact,
|
|
12321
|
+
email,
|
|
12322
|
+
phone,
|
|
12323
|
+
email_provider: emailProvider,
|
|
12324
|
+
phone_provider: phoneProvider,
|
|
12325
|
+
attempts,
|
|
12326
|
+
charged
|
|
12327
|
+
};
|
|
12328
|
+
}
|
|
12329
|
+
async function enrichField(contact, field, config, registry, exhausted) {
|
|
12330
|
+
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12331
|
+
const attempts = [];
|
|
12332
|
+
for (const providerName of config.providers) {
|
|
12333
|
+
if (exhausted.has(providerName)) {
|
|
12334
|
+
attempts.push({
|
|
12335
|
+
provider: providerName,
|
|
12336
|
+
capability,
|
|
12337
|
+
found: false,
|
|
12338
|
+
charged: false,
|
|
12339
|
+
duration_ms: 0,
|
|
12340
|
+
error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
|
|
12341
|
+
});
|
|
12342
|
+
continue;
|
|
12343
|
+
}
|
|
12344
|
+
const provider = registry.get(providerName);
|
|
12345
|
+
if (!provider || !provider.capabilities.includes(capability)) continue;
|
|
12346
|
+
for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
|
|
12347
|
+
const start = Date.now();
|
|
12348
|
+
try {
|
|
12349
|
+
let value;
|
|
12350
|
+
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12351
|
+
if (field === "email" && "enrichEmail" in provider) {
|
|
12352
|
+
const result = await withTimeout(
|
|
12353
|
+
provider.enrichEmail(contact),
|
|
12354
|
+
providerTimeoutMs,
|
|
12355
|
+
providerName
|
|
12356
|
+
);
|
|
12357
|
+
value = result?.email;
|
|
12358
|
+
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12359
|
+
const result = await withTimeout(
|
|
12360
|
+
provider.enrichPhone(contact),
|
|
12361
|
+
providerTimeoutMs,
|
|
12362
|
+
providerName
|
|
12363
|
+
);
|
|
12364
|
+
value = result?.phone;
|
|
12365
|
+
}
|
|
12366
|
+
const found = !!value;
|
|
12367
|
+
attempts.push({
|
|
12368
|
+
provider: providerName,
|
|
12369
|
+
capability,
|
|
12370
|
+
found,
|
|
12371
|
+
charged: found,
|
|
12372
|
+
duration_ms: Date.now() - start
|
|
12373
|
+
});
|
|
12374
|
+
if (found && config.stopOnFirst) {
|
|
12375
|
+
return { value, provider: providerName, attempts };
|
|
12376
|
+
}
|
|
12377
|
+
break;
|
|
12378
|
+
} catch (error) {
|
|
12379
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
12380
|
+
if (isExhaustedError(error)) {
|
|
12381
|
+
exhausted.add(providerName);
|
|
12382
|
+
attempts.push({
|
|
12383
|
+
provider: providerName,
|
|
12384
|
+
capability,
|
|
12385
|
+
found: false,
|
|
12386
|
+
charged: false,
|
|
12387
|
+
duration_ms: Date.now() - start,
|
|
12388
|
+
error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
|
|
12389
|
+
});
|
|
12390
|
+
break;
|
|
12391
|
+
}
|
|
12392
|
+
if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
|
|
12393
|
+
const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
|
|
12394
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
12395
|
+
continue;
|
|
12396
|
+
}
|
|
12397
|
+
attempts.push({
|
|
12398
|
+
provider: providerName,
|
|
12399
|
+
capability,
|
|
12400
|
+
found: false,
|
|
12401
|
+
charged: false,
|
|
12402
|
+
duration_ms: Date.now() - start,
|
|
12403
|
+
error: errorMsg
|
|
12404
|
+
});
|
|
12405
|
+
logError(
|
|
12406
|
+
`waterfall:${providerName}`,
|
|
12407
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
12408
|
+
{ contact: `${contact.first_name} ${contact.last_name}` }
|
|
12409
|
+
);
|
|
12410
|
+
break;
|
|
12411
|
+
}
|
|
12412
|
+
}
|
|
12413
|
+
}
|
|
12414
|
+
return { attempts };
|
|
12415
|
+
}
|
|
12416
|
+
function dryRunWaterfall(contactsCount, config, registry) {
|
|
12417
|
+
const activeProviders = config.providers.filter((name) => registry.get(name));
|
|
12418
|
+
const costs = {};
|
|
12419
|
+
for (const name of activeProviders) {
|
|
12420
|
+
const pricing = DEFAULT_PRICING[name];
|
|
12421
|
+
if (pricing?.estimated_cost_per_hit) {
|
|
12422
|
+
costs[name] = pricing.estimated_cost_per_hit;
|
|
12423
|
+
} else if (pricing?.estimated_cost_per_lookup) {
|
|
12424
|
+
costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
|
|
12425
|
+
} else {
|
|
12426
|
+
costs[name] = "unknown";
|
|
12427
|
+
}
|
|
12428
|
+
}
|
|
12429
|
+
return {
|
|
12430
|
+
dry_run: true,
|
|
12431
|
+
contacts_count: contactsCount,
|
|
12432
|
+
waterfall: activeProviders,
|
|
12433
|
+
pricing_model: "pay-on-find",
|
|
12434
|
+
estimated_cost_per_hit: costs,
|
|
12435
|
+
note: "Charged only when contact info is found. No result = no charge.",
|
|
12436
|
+
confirm: "Call gtm_enrich again without dry_run to execute."
|
|
12437
|
+
};
|
|
12438
|
+
}
|
|
12439
|
+
var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
|
|
12440
|
+
var init_waterfall = __esm({
|
|
12441
|
+
"src/providers/waterfall.ts"() {
|
|
12442
|
+
"use strict";
|
|
12443
|
+
init_dist();
|
|
12444
|
+
enrichmentCache = /* @__PURE__ */ new Map();
|
|
12445
|
+
DEFAULT_PRICING = {
|
|
12446
|
+
bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
|
|
12447
|
+
prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12448
|
+
apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
|
|
12449
|
+
hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12450
|
+
leadmagic: { charged_only_on_find: true, estimated_cost_per_hit: "$0.03" }
|
|
12451
|
+
};
|
|
12452
|
+
EXHAUSTION_PATTERNS = [
|
|
12453
|
+
"insufficient credits",
|
|
12454
|
+
"no credits",
|
|
12455
|
+
"0 credits",
|
|
12456
|
+
"plan limit",
|
|
12457
|
+
"quota exceeded",
|
|
12458
|
+
"upgrade your plan",
|
|
12459
|
+
"not accessible with this api_key on a free plan"
|
|
12460
|
+
];
|
|
12461
|
+
WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
|
|
12462
|
+
DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
|
|
12463
|
+
DEFAULT_BATCH_TIMEOUT_MS = 3e5;
|
|
12464
|
+
}
|
|
12465
|
+
});
|
|
12466
|
+
|
|
12467
|
+
// src/providers/types.ts
|
|
12468
|
+
function applySlimFields(obj, fields) {
|
|
12469
|
+
const result = {};
|
|
12470
|
+
for (const field of fields) {
|
|
12471
|
+
if (field in obj) {
|
|
12472
|
+
result[field] = obj[field];
|
|
12473
|
+
}
|
|
12474
|
+
}
|
|
12475
|
+
return result;
|
|
12476
|
+
}
|
|
12477
|
+
var SLIM_COMPANY_FIELDS, SLIM_PERSON_FIELDS;
|
|
12478
|
+
var init_types3 = __esm({
|
|
12479
|
+
"src/providers/types.ts"() {
|
|
12480
|
+
"use strict";
|
|
12481
|
+
SLIM_COMPANY_FIELDS = [
|
|
12482
|
+
"domain",
|
|
12483
|
+
"name",
|
|
12484
|
+
"industry",
|
|
12485
|
+
"employees",
|
|
12486
|
+
"country"
|
|
12487
|
+
];
|
|
12488
|
+
SLIM_PERSON_FIELDS = [
|
|
12489
|
+
"first_name",
|
|
12490
|
+
"last_name",
|
|
12491
|
+
"job_title",
|
|
12492
|
+
"email",
|
|
12493
|
+
"company",
|
|
12494
|
+
"company_domain",
|
|
12495
|
+
"linkedin_url"
|
|
12496
|
+
];
|
|
12497
|
+
}
|
|
12498
|
+
});
|
|
12499
|
+
|
|
12500
|
+
// src/providers/manifest-validation.ts
|
|
12501
|
+
function validateParams(provider, tool, args) {
|
|
12502
|
+
const manifest = getToolManifest(provider, tool);
|
|
12503
|
+
if (!manifest) return [];
|
|
12504
|
+
const supported = new Set(manifest.supported_params);
|
|
12505
|
+
const warnings = [];
|
|
12506
|
+
for (const [key, value] of Object.entries(args)) {
|
|
12507
|
+
if (value === void 0 || value === null) continue;
|
|
12508
|
+
if (controlSet.has(key)) continue;
|
|
12509
|
+
if (!supported.has(key)) {
|
|
12510
|
+
const alternatives = Object.entries(MANIFESTS).filter(([_, m]) => m[tool]?.supported_params.includes(key)).map(([name]) => name);
|
|
12511
|
+
const suggestion = alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : "";
|
|
12512
|
+
warnings.push(`${key} is not supported by ${provider}.${suggestion}`);
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
return warnings;
|
|
12516
|
+
}
|
|
12517
|
+
function clampLimit(provider, tool, limit) {
|
|
12518
|
+
const manifest = getToolManifest(provider, tool);
|
|
12519
|
+
if (!manifest) return limit ?? 50;
|
|
12520
|
+
if (limit === void 0) return manifest.default_limit;
|
|
12521
|
+
return Math.min(limit, manifest.max_limit);
|
|
11930
12522
|
}
|
|
11931
12523
|
var controlSet;
|
|
11932
12524
|
var init_manifest_validation = __esm({
|
|
@@ -12199,6 +12791,43 @@ async function findPeople(args, ctx) {
|
|
|
12199
12791
|
};
|
|
12200
12792
|
const results = await finder.findPeople(query);
|
|
12201
12793
|
const dedupedResults = deduplicatePeople(results);
|
|
12794
|
+
const shouldEnrich = args.enrich ?? false;
|
|
12795
|
+
let enrichedCount = 0;
|
|
12796
|
+
if (shouldEnrich) {
|
|
12797
|
+
const needsEnrichment = dedupedResults.filter(
|
|
12798
|
+
(r) => !r.email || r.email.includes("*")
|
|
12799
|
+
);
|
|
12800
|
+
if (needsEnrichment.length > 0) {
|
|
12801
|
+
try {
|
|
12802
|
+
const contacts = needsEnrichment.map((r) => ({
|
|
12803
|
+
first_name: r.first_name,
|
|
12804
|
+
last_name: r.last_name,
|
|
12805
|
+
company: r.company,
|
|
12806
|
+
company_domain: r.company_domain,
|
|
12807
|
+
linkedin_url: r.linkedin_url
|
|
12808
|
+
}));
|
|
12809
|
+
const clientConfig = ctx.session.getActiveContext().config;
|
|
12810
|
+
const waterfallProviders = clientConfig.waterfall?.email ?? ["prospeo"];
|
|
12811
|
+
const waterfallConfig = {
|
|
12812
|
+
providers: waterfallProviders,
|
|
12813
|
+
find: ["email"],
|
|
12814
|
+
verify: false,
|
|
12815
|
+
stopOnFirst: true,
|
|
12816
|
+
concurrency: 5
|
|
12817
|
+
};
|
|
12818
|
+
const enrichResults = await runWaterfall(contacts, waterfallConfig, ctx.registry);
|
|
12819
|
+
for (let i = 0; i < needsEnrichment.length; i++) {
|
|
12820
|
+
const enriched = enrichResults[i];
|
|
12821
|
+
if (enriched?.email) {
|
|
12822
|
+
needsEnrichment[i].email = enriched.email;
|
|
12823
|
+
enrichedCount++;
|
|
12824
|
+
}
|
|
12825
|
+
}
|
|
12826
|
+
} catch (error) {
|
|
12827
|
+
logError("prospect:inlineEnrich", error instanceof Error ? error : new Error(String(error)));
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12830
|
+
}
|
|
12202
12831
|
const resultsWithAlias = dedupedResults.map((r) => ({
|
|
12203
12832
|
...r,
|
|
12204
12833
|
company_domain: r.company_domain ?? r.company
|
|
@@ -12265,6 +12894,7 @@ async function findPeople(args, ctx) {
|
|
|
12265
12894
|
has_more,
|
|
12266
12895
|
offset,
|
|
12267
12896
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12897
|
+
...shouldEnrich && { enriched: enrichedCount },
|
|
12268
12898
|
people: slimmedResults,
|
|
12269
12899
|
...warnings?.length ? { warnings } : {}
|
|
12270
12900
|
};
|
|
@@ -12272,7 +12902,9 @@ async function findPeople(args, ctx) {
|
|
|
12272
12902
|
var init_prospect2 = __esm({
|
|
12273
12903
|
"src/handlers/prospect.ts"() {
|
|
12274
12904
|
"use strict";
|
|
12905
|
+
init_dist();
|
|
12275
12906
|
init_errors();
|
|
12907
|
+
init_waterfall();
|
|
12276
12908
|
init_types3();
|
|
12277
12909
|
init_manifest_validation();
|
|
12278
12910
|
init_result_writer();
|
|
@@ -12280,266 +12912,6 @@ var init_prospect2 = __esm({
|
|
|
12280
12912
|
}
|
|
12281
12913
|
});
|
|
12282
12914
|
|
|
12283
|
-
// src/providers/waterfall.ts
|
|
12284
|
-
function getCacheKey(contact) {
|
|
12285
|
-
if (!contact.linkedin_url) return null;
|
|
12286
|
-
return contact.linkedin_url.toLowerCase().trim();
|
|
12287
|
-
}
|
|
12288
|
-
function isExhaustedError(error) {
|
|
12289
|
-
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
12290
|
-
return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
|
|
12291
|
-
}
|
|
12292
|
-
function isRateLimitError(error) {
|
|
12293
|
-
if (error instanceof Error && "status" in error) {
|
|
12294
|
-
return error.status === 429;
|
|
12295
|
-
}
|
|
12296
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
12297
|
-
return msg.includes("429") || msg.toLowerCase().includes("rate limit");
|
|
12298
|
-
}
|
|
12299
|
-
function withTimeout(promise, ms, label) {
|
|
12300
|
-
return new Promise((resolve, reject) => {
|
|
12301
|
-
const timer = setTimeout(
|
|
12302
|
-
() => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
|
|
12303
|
-
ms
|
|
12304
|
-
);
|
|
12305
|
-
promise.then(
|
|
12306
|
-
(val) => {
|
|
12307
|
-
clearTimeout(timer);
|
|
12308
|
-
resolve(val);
|
|
12309
|
-
},
|
|
12310
|
-
(err) => {
|
|
12311
|
-
clearTimeout(timer);
|
|
12312
|
-
reject(err);
|
|
12313
|
-
}
|
|
12314
|
-
);
|
|
12315
|
-
});
|
|
12316
|
-
}
|
|
12317
|
-
async function runWaterfall(contacts, config, registry) {
|
|
12318
|
-
const results = [];
|
|
12319
|
-
const exhausted = /* @__PURE__ */ new Set();
|
|
12320
|
-
const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
|
|
12321
|
-
for (const contact of contacts) {
|
|
12322
|
-
if (Date.now() > batchDeadline) {
|
|
12323
|
-
results.push({
|
|
12324
|
-
contact,
|
|
12325
|
-
attempts: [
|
|
12326
|
-
{
|
|
12327
|
-
provider: "waterfall",
|
|
12328
|
-
capability: "enrich_email",
|
|
12329
|
-
found: false,
|
|
12330
|
-
charged: false,
|
|
12331
|
-
duration_ms: 0,
|
|
12332
|
-
error: "Batch timeout exceeded \u2014 skipped remaining contacts"
|
|
12333
|
-
}
|
|
12334
|
-
],
|
|
12335
|
-
charged: false
|
|
12336
|
-
});
|
|
12337
|
-
continue;
|
|
12338
|
-
}
|
|
12339
|
-
const result = await enrichContact(contact, config, registry, exhausted);
|
|
12340
|
-
results.push(result);
|
|
12341
|
-
}
|
|
12342
|
-
return results;
|
|
12343
|
-
}
|
|
12344
|
-
async function enrichContact(contact, config, registry, exhausted) {
|
|
12345
|
-
const attempts = [];
|
|
12346
|
-
let email;
|
|
12347
|
-
let emailProvider;
|
|
12348
|
-
let phone;
|
|
12349
|
-
let phoneProvider;
|
|
12350
|
-
let charged = false;
|
|
12351
|
-
const cacheKey = getCacheKey(contact);
|
|
12352
|
-
const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
|
|
12353
|
-
for (const field of config.find) {
|
|
12354
|
-
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12355
|
-
const cachedValue = cached?.[field];
|
|
12356
|
-
if (cachedValue) {
|
|
12357
|
-
if (field === "email") {
|
|
12358
|
-
email = cachedValue;
|
|
12359
|
-
emailProvider = cached.provider;
|
|
12360
|
-
}
|
|
12361
|
-
if (field === "phone") {
|
|
12362
|
-
phone = cachedValue;
|
|
12363
|
-
phoneProvider = cached.provider;
|
|
12364
|
-
}
|
|
12365
|
-
attempts.push({
|
|
12366
|
-
provider: cached.provider,
|
|
12367
|
-
capability,
|
|
12368
|
-
found: true,
|
|
12369
|
-
charged: false,
|
|
12370
|
-
duration_ms: 0,
|
|
12371
|
-
cached: true
|
|
12372
|
-
});
|
|
12373
|
-
continue;
|
|
12374
|
-
}
|
|
12375
|
-
const fieldResult = await enrichField(contact, field, config, registry, exhausted);
|
|
12376
|
-
attempts.push(...fieldResult.attempts);
|
|
12377
|
-
if (field === "email" && fieldResult.value) {
|
|
12378
|
-
email = fieldResult.value;
|
|
12379
|
-
emailProvider = fieldResult.provider;
|
|
12380
|
-
charged = true;
|
|
12381
|
-
} else if (field === "phone" && fieldResult.value) {
|
|
12382
|
-
phone = fieldResult.value;
|
|
12383
|
-
phoneProvider = fieldResult.provider;
|
|
12384
|
-
charged = true;
|
|
12385
|
-
}
|
|
12386
|
-
}
|
|
12387
|
-
if (cacheKey && (email || phone)) {
|
|
12388
|
-
const existing = enrichmentCache.get(cacheKey);
|
|
12389
|
-
enrichmentCache.set(cacheKey, {
|
|
12390
|
-
email: email ?? existing?.email,
|
|
12391
|
-
phone: phone ?? existing?.phone,
|
|
12392
|
-
provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
|
|
12393
|
-
timestamp: Date.now()
|
|
12394
|
-
});
|
|
12395
|
-
}
|
|
12396
|
-
return {
|
|
12397
|
-
contact,
|
|
12398
|
-
email,
|
|
12399
|
-
phone,
|
|
12400
|
-
email_provider: emailProvider,
|
|
12401
|
-
phone_provider: phoneProvider,
|
|
12402
|
-
attempts,
|
|
12403
|
-
charged
|
|
12404
|
-
};
|
|
12405
|
-
}
|
|
12406
|
-
async function enrichField(contact, field, config, registry, exhausted) {
|
|
12407
|
-
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12408
|
-
const attempts = [];
|
|
12409
|
-
for (const providerName of config.providers) {
|
|
12410
|
-
if (exhausted.has(providerName)) {
|
|
12411
|
-
attempts.push({
|
|
12412
|
-
provider: providerName,
|
|
12413
|
-
capability,
|
|
12414
|
-
found: false,
|
|
12415
|
-
charged: false,
|
|
12416
|
-
duration_ms: 0,
|
|
12417
|
-
error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
|
|
12418
|
-
});
|
|
12419
|
-
continue;
|
|
12420
|
-
}
|
|
12421
|
-
const provider = registry.get(providerName);
|
|
12422
|
-
if (!provider || !provider.capabilities.includes(capability)) continue;
|
|
12423
|
-
for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
|
|
12424
|
-
const start = Date.now();
|
|
12425
|
-
try {
|
|
12426
|
-
let value;
|
|
12427
|
-
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12428
|
-
if (field === "email" && "enrichEmail" in provider) {
|
|
12429
|
-
const result = await withTimeout(
|
|
12430
|
-
provider.enrichEmail(contact),
|
|
12431
|
-
providerTimeoutMs,
|
|
12432
|
-
providerName
|
|
12433
|
-
);
|
|
12434
|
-
value = result?.email;
|
|
12435
|
-
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12436
|
-
const result = await withTimeout(
|
|
12437
|
-
provider.enrichPhone(contact),
|
|
12438
|
-
providerTimeoutMs,
|
|
12439
|
-
providerName
|
|
12440
|
-
);
|
|
12441
|
-
value = result?.phone;
|
|
12442
|
-
}
|
|
12443
|
-
const found = !!value;
|
|
12444
|
-
attempts.push({
|
|
12445
|
-
provider: providerName,
|
|
12446
|
-
capability,
|
|
12447
|
-
found,
|
|
12448
|
-
charged: found,
|
|
12449
|
-
duration_ms: Date.now() - start
|
|
12450
|
-
});
|
|
12451
|
-
if (found && config.stopOnFirst) {
|
|
12452
|
-
return { value, provider: providerName, attempts };
|
|
12453
|
-
}
|
|
12454
|
-
break;
|
|
12455
|
-
} catch (error) {
|
|
12456
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
12457
|
-
if (isExhaustedError(error)) {
|
|
12458
|
-
exhausted.add(providerName);
|
|
12459
|
-
attempts.push({
|
|
12460
|
-
provider: providerName,
|
|
12461
|
-
capability,
|
|
12462
|
-
found: false,
|
|
12463
|
-
charged: false,
|
|
12464
|
-
duration_ms: Date.now() - start,
|
|
12465
|
-
error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
|
|
12466
|
-
});
|
|
12467
|
-
break;
|
|
12468
|
-
}
|
|
12469
|
-
if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
|
|
12470
|
-
const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
|
|
12471
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
12472
|
-
continue;
|
|
12473
|
-
}
|
|
12474
|
-
attempts.push({
|
|
12475
|
-
provider: providerName,
|
|
12476
|
-
capability,
|
|
12477
|
-
found: false,
|
|
12478
|
-
charged: false,
|
|
12479
|
-
duration_ms: Date.now() - start,
|
|
12480
|
-
error: errorMsg
|
|
12481
|
-
});
|
|
12482
|
-
logError(
|
|
12483
|
-
`waterfall:${providerName}`,
|
|
12484
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
12485
|
-
{ contact: `${contact.first_name} ${contact.last_name}` }
|
|
12486
|
-
);
|
|
12487
|
-
break;
|
|
12488
|
-
}
|
|
12489
|
-
}
|
|
12490
|
-
}
|
|
12491
|
-
return { attempts };
|
|
12492
|
-
}
|
|
12493
|
-
function dryRunWaterfall(contactsCount, config, registry) {
|
|
12494
|
-
const activeProviders = config.providers.filter((name) => registry.get(name));
|
|
12495
|
-
const costs = {};
|
|
12496
|
-
for (const name of activeProviders) {
|
|
12497
|
-
const pricing = DEFAULT_PRICING[name];
|
|
12498
|
-
if (pricing?.estimated_cost_per_hit) {
|
|
12499
|
-
costs[name] = pricing.estimated_cost_per_hit;
|
|
12500
|
-
} else if (pricing?.estimated_cost_per_lookup) {
|
|
12501
|
-
costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
|
|
12502
|
-
} else {
|
|
12503
|
-
costs[name] = "unknown";
|
|
12504
|
-
}
|
|
12505
|
-
}
|
|
12506
|
-
return {
|
|
12507
|
-
dry_run: true,
|
|
12508
|
-
contacts_count: contactsCount,
|
|
12509
|
-
waterfall: activeProviders,
|
|
12510
|
-
pricing_model: "pay-on-find",
|
|
12511
|
-
estimated_cost_per_hit: costs,
|
|
12512
|
-
note: "Charged only when contact info is found. No result = no charge.",
|
|
12513
|
-
confirm: "Call gtm_enrich again without dry_run to execute."
|
|
12514
|
-
};
|
|
12515
|
-
}
|
|
12516
|
-
var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
|
|
12517
|
-
var init_waterfall = __esm({
|
|
12518
|
-
"src/providers/waterfall.ts"() {
|
|
12519
|
-
"use strict";
|
|
12520
|
-
init_dist();
|
|
12521
|
-
enrichmentCache = /* @__PURE__ */ new Map();
|
|
12522
|
-
DEFAULT_PRICING = {
|
|
12523
|
-
bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
|
|
12524
|
-
prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12525
|
-
apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
|
|
12526
|
-
hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" }
|
|
12527
|
-
};
|
|
12528
|
-
EXHAUSTION_PATTERNS = [
|
|
12529
|
-
"insufficient credits",
|
|
12530
|
-
"no credits",
|
|
12531
|
-
"0 credits",
|
|
12532
|
-
"plan limit",
|
|
12533
|
-
"quota exceeded",
|
|
12534
|
-
"upgrade your plan",
|
|
12535
|
-
"not accessible with this api_key on a free plan"
|
|
12536
|
-
];
|
|
12537
|
-
WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
|
|
12538
|
-
DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
|
|
12539
|
-
DEFAULT_BATCH_TIMEOUT_MS = 3e5;
|
|
12540
|
-
}
|
|
12541
|
-
});
|
|
12542
|
-
|
|
12543
12915
|
// src/handlers/enrich.ts
|
|
12544
12916
|
var enrich_exports = {};
|
|
12545
12917
|
__export(enrich_exports, {
|
|
@@ -13448,7 +13820,8 @@ async function createTable(args) {
|
|
|
13448
13820
|
const body = await response.json().catch(() => ({}));
|
|
13449
13821
|
throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
|
|
13450
13822
|
}
|
|
13451
|
-
|
|
13823
|
+
const result = await response.json();
|
|
13824
|
+
return { ...result, row_ids: rows.map((r) => r._row_id) };
|
|
13452
13825
|
}
|
|
13453
13826
|
async function updateTable(args) {
|
|
13454
13827
|
const tableId = args.table_id;
|
|
@@ -13758,6 +14131,7 @@ var init_validation = __esm({
|
|
|
13758
14131
|
min_employees: z2.number().int().min(0).optional(),
|
|
13759
14132
|
max_employees: z2.number().int().min(1).optional(),
|
|
13760
14133
|
revenue_range: z2.string().optional(),
|
|
14134
|
+
enrich: z2.boolean().optional(),
|
|
13761
14135
|
fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
|
|
13762
14136
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
13763
14137
|
detail: detailSchema
|