gtm-now 0.10.7 → 0.10.9
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 -38
- 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
|
|
|
@@ -9630,7 +9737,8 @@ var init_prospeo2 = __esm({
|
|
|
9630
9737
|
return {
|
|
9631
9738
|
email: response.person.email.email,
|
|
9632
9739
|
quality: response.person.email.status === "verified" ? "95%" : "70%",
|
|
9633
|
-
provider: this.name
|
|
9740
|
+
provider: this.name,
|
|
9741
|
+
credits_consumed: 1
|
|
9634
9742
|
};
|
|
9635
9743
|
} catch (error) {
|
|
9636
9744
|
logError("prospeo:enrichEmail", error instanceof Error ? error : new Error(String(error)));
|
|
@@ -10549,6 +10657,195 @@ var init_exa = __esm({
|
|
|
10549
10657
|
}
|
|
10550
10658
|
});
|
|
10551
10659
|
|
|
10660
|
+
// src/providers/adapters/leadmagic.ts
|
|
10661
|
+
function mapEmailQuality(status) {
|
|
10662
|
+
switch (status) {
|
|
10663
|
+
case "valid":
|
|
10664
|
+
return "95%";
|
|
10665
|
+
case "valid_catch_all":
|
|
10666
|
+
return "80%";
|
|
10667
|
+
case "catch_all":
|
|
10668
|
+
return "60%";
|
|
10669
|
+
default:
|
|
10670
|
+
return "50%";
|
|
10671
|
+
}
|
|
10672
|
+
}
|
|
10673
|
+
function mapVerificationStatus(status) {
|
|
10674
|
+
switch (status) {
|
|
10675
|
+
case "valid":
|
|
10676
|
+
return "deliverable";
|
|
10677
|
+
case "valid_catch_all":
|
|
10678
|
+
return "catch_all_safe";
|
|
10679
|
+
case "catch_all":
|
|
10680
|
+
return "catch_all_not_safe";
|
|
10681
|
+
case "invalid":
|
|
10682
|
+
return "undeliverable";
|
|
10683
|
+
default:
|
|
10684
|
+
return "unknown";
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
var LeadMagicAdapter2;
|
|
10688
|
+
var init_leadmagic3 = __esm({
|
|
10689
|
+
"src/providers/adapters/leadmagic.ts"() {
|
|
10690
|
+
"use strict";
|
|
10691
|
+
init_dist();
|
|
10692
|
+
init_dist2();
|
|
10693
|
+
LeadMagicAdapter2 = class {
|
|
10694
|
+
name = "leadmagic";
|
|
10695
|
+
capabilities = [
|
|
10696
|
+
"enrich_email",
|
|
10697
|
+
"enrich_phone",
|
|
10698
|
+
"verify_email",
|
|
10699
|
+
"find_people"
|
|
10700
|
+
];
|
|
10701
|
+
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.";
|
|
10702
|
+
client;
|
|
10703
|
+
constructor(apiKey) {
|
|
10704
|
+
this.client = new LeadMagicClient({ apiKey });
|
|
10705
|
+
}
|
|
10706
|
+
async checkCredentials() {
|
|
10707
|
+
try {
|
|
10708
|
+
await this.client.validateEmail({ email: "test@example.com" });
|
|
10709
|
+
return true;
|
|
10710
|
+
} catch (error) {
|
|
10711
|
+
logError(
|
|
10712
|
+
"leadmagic:checkCredentials",
|
|
10713
|
+
error instanceof Error ? error : new Error(String(error))
|
|
10714
|
+
);
|
|
10715
|
+
return false;
|
|
10716
|
+
}
|
|
10717
|
+
}
|
|
10718
|
+
async checkCredits() {
|
|
10719
|
+
return null;
|
|
10720
|
+
}
|
|
10721
|
+
// ─── EmailEnricher ──────────────────────────────────────
|
|
10722
|
+
async enrichEmail(contact) {
|
|
10723
|
+
try {
|
|
10724
|
+
if (contact.first_name && contact.last_name && (contact.company_domain || contact.company)) {
|
|
10725
|
+
const response = await this.client.findEmail({
|
|
10726
|
+
first_name: contact.first_name,
|
|
10727
|
+
last_name: contact.last_name,
|
|
10728
|
+
domain: contact.company_domain,
|
|
10729
|
+
company_name: !contact.company_domain ? contact.company : void 0
|
|
10730
|
+
});
|
|
10731
|
+
if (response.email && response.status !== "not_found") {
|
|
10732
|
+
return {
|
|
10733
|
+
email: response.email,
|
|
10734
|
+
quality: mapEmailQuality(response.status),
|
|
10735
|
+
provider: this.name,
|
|
10736
|
+
credits_consumed: response.credits_consumed
|
|
10737
|
+
};
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
if (contact.linkedin_url) {
|
|
10741
|
+
const response = await this.client.profileToEmail({
|
|
10742
|
+
profile_url: contact.linkedin_url
|
|
10743
|
+
});
|
|
10744
|
+
if (response.email) {
|
|
10745
|
+
return {
|
|
10746
|
+
email: response.email,
|
|
10747
|
+
quality: "80%",
|
|
10748
|
+
// Profile-to-email doesn't return validation status
|
|
10749
|
+
provider: this.name,
|
|
10750
|
+
credits_consumed: response.credits_consumed
|
|
10751
|
+
};
|
|
10752
|
+
}
|
|
10753
|
+
}
|
|
10754
|
+
return null;
|
|
10755
|
+
} catch (error) {
|
|
10756
|
+
logError("leadmagic:enrichEmail", error instanceof Error ? error : new Error(String(error)));
|
|
10757
|
+
return null;
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
// ─── PhoneEnricher ──────────────────────────────────────
|
|
10761
|
+
async enrichPhone(contact) {
|
|
10762
|
+
try {
|
|
10763
|
+
const response = await this.client.findMobile({
|
|
10764
|
+
profile_url: contact.linkedin_url,
|
|
10765
|
+
work_email: void 0,
|
|
10766
|
+
// Could pass email if available
|
|
10767
|
+
personal_email: void 0
|
|
10768
|
+
});
|
|
10769
|
+
if (!response.mobile_number) return null;
|
|
10770
|
+
return {
|
|
10771
|
+
phone: response.mobile_number,
|
|
10772
|
+
provider: this.name
|
|
10773
|
+
};
|
|
10774
|
+
} catch (error) {
|
|
10775
|
+
logError("leadmagic:enrichPhone", error instanceof Error ? error : new Error(String(error)));
|
|
10776
|
+
return null;
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
// ─── EmailVerifier ──────────────────────────────────────
|
|
10780
|
+
async verifyEmail(email) {
|
|
10781
|
+
try {
|
|
10782
|
+
const response = await this.client.validateEmail({ email });
|
|
10783
|
+
return {
|
|
10784
|
+
email: response.email ?? email,
|
|
10785
|
+
valid: response.email_status === "valid" || response.email_status === "valid_catch_all",
|
|
10786
|
+
status: mapVerificationStatus(response.email_status),
|
|
10787
|
+
provider: this.name
|
|
10788
|
+
};
|
|
10789
|
+
} catch (error) {
|
|
10790
|
+
logError("leadmagic:verifyEmail", error instanceof Error ? error : new Error(String(error)));
|
|
10791
|
+
return {
|
|
10792
|
+
email,
|
|
10793
|
+
valid: false,
|
|
10794
|
+
status: "unknown",
|
|
10795
|
+
provider: this.name
|
|
10796
|
+
};
|
|
10797
|
+
}
|
|
10798
|
+
}
|
|
10799
|
+
// ─── PeopleFinder ───────────────────────────────────────
|
|
10800
|
+
async findPeople(query) {
|
|
10801
|
+
try {
|
|
10802
|
+
if (query.job_title && (query.company_domain || query.company_name)) {
|
|
10803
|
+
const response = await this.client.findRole({
|
|
10804
|
+
job_title: query.job_title,
|
|
10805
|
+
company_domain: query.company_domain,
|
|
10806
|
+
company_name: !query.company_domain ? query.company_name : void 0
|
|
10807
|
+
});
|
|
10808
|
+
if (response.first_name && response.last_name) {
|
|
10809
|
+
return [
|
|
10810
|
+
{
|
|
10811
|
+
first_name: response.first_name,
|
|
10812
|
+
last_name: response.last_name,
|
|
10813
|
+
job_title: response.job_title ?? void 0,
|
|
10814
|
+
company: response.company_name ?? void 0,
|
|
10815
|
+
company_domain: response.company_website ?? query.company_domain ?? void 0,
|
|
10816
|
+
linkedin_url: response.profile_url ?? void 0,
|
|
10817
|
+
sources: [this.name]
|
|
10818
|
+
}
|
|
10819
|
+
];
|
|
10820
|
+
}
|
|
10821
|
+
return [];
|
|
10822
|
+
}
|
|
10823
|
+
if (query.company_domain || query.company_name) {
|
|
10824
|
+
const response = await this.client.findEmployees({
|
|
10825
|
+
company_domain: query.company_domain,
|
|
10826
|
+
company_name: !query.company_domain ? query.company_name : void 0,
|
|
10827
|
+
limit: query.limit ?? 20
|
|
10828
|
+
});
|
|
10829
|
+
if (!response.data?.length) return [];
|
|
10830
|
+
return response.data.map((emp) => ({
|
|
10831
|
+
first_name: emp.first_name ?? "",
|
|
10832
|
+
last_name: emp.last_name ?? "",
|
|
10833
|
+
job_title: emp.title ?? void 0,
|
|
10834
|
+
company: emp.company_name ?? void 0,
|
|
10835
|
+
company_domain: emp.website ?? query.company_domain ?? void 0,
|
|
10836
|
+
sources: [this.name]
|
|
10837
|
+
}));
|
|
10838
|
+
}
|
|
10839
|
+
return [];
|
|
10840
|
+
} catch (error) {
|
|
10841
|
+
logError("leadmagic:findPeople", error instanceof Error ? error : new Error(String(error)));
|
|
10842
|
+
return [];
|
|
10843
|
+
}
|
|
10844
|
+
}
|
|
10845
|
+
};
|
|
10846
|
+
}
|
|
10847
|
+
});
|
|
10848
|
+
|
|
10552
10849
|
// src/providers/adapters/index.ts
|
|
10553
10850
|
var init_adapters = __esm({
|
|
10554
10851
|
"src/providers/adapters/index.ts"() {
|
|
@@ -10566,6 +10863,7 @@ var init_adapters = __esm({
|
|
|
10566
10863
|
init_storeleads();
|
|
10567
10864
|
init_harvest();
|
|
10568
10865
|
init_exa();
|
|
10866
|
+
init_leadmagic3();
|
|
10569
10867
|
}
|
|
10570
10868
|
});
|
|
10571
10869
|
|
|
@@ -10598,6 +10896,8 @@ function createAdapter(name, config) {
|
|
|
10598
10896
|
return config.api_key ? new HarvestApiAdapter(config.api_key) : null;
|
|
10599
10897
|
case "exa":
|
|
10600
10898
|
return config.api_key ? new ExaAdapter(config.api_key) : null;
|
|
10899
|
+
case "leadmagic":
|
|
10900
|
+
return config.api_key ? new LeadMagicAdapter2(config.api_key) : null;
|
|
10601
10901
|
default:
|
|
10602
10902
|
return null;
|
|
10603
10903
|
}
|
|
@@ -10893,6 +11193,10 @@ var init_prospect = __esm({
|
|
|
10893
11193
|
type: "string",
|
|
10894
11194
|
enum: ["summary", "full"],
|
|
10895
11195
|
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
11196
|
+
},
|
|
11197
|
+
review: {
|
|
11198
|
+
type: "boolean",
|
|
11199
|
+
description: "Create a viewer table with results. Returns table_id and URL for human review. Other tools can read from this table via table_id parameter."
|
|
10896
11200
|
}
|
|
10897
11201
|
},
|
|
10898
11202
|
required: []
|
|
@@ -11009,6 +11313,10 @@ var init_prospect = __esm({
|
|
|
11009
11313
|
type: "string",
|
|
11010
11314
|
enum: ["summary", "full"],
|
|
11011
11315
|
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
11316
|
+
},
|
|
11317
|
+
review: {
|
|
11318
|
+
type: "boolean",
|
|
11319
|
+
description: "Create a viewer table with results. Returns table_id and URL for human review. Other tools can read from this table via table_id parameter."
|
|
11012
11320
|
}
|
|
11013
11321
|
},
|
|
11014
11322
|
required: []
|
|
@@ -11066,9 +11374,17 @@ var init_enrich = __esm({
|
|
|
11066
11374
|
type: "string",
|
|
11067
11375
|
enum: ["summary", "full"],
|
|
11068
11376
|
description: 'Response detail level. "summary" (default) returns email/phone/providers per contact. "full" includes waterfall attempt details (provider, duration, errors) per contact.'
|
|
11377
|
+
},
|
|
11378
|
+
table_id: {
|
|
11379
|
+
type: "string",
|
|
11380
|
+
description: "Read contacts from a viewer table instead of passing inline. Fetches selected rows, enriches them, and writes results back to the table."
|
|
11381
|
+
},
|
|
11382
|
+
review: {
|
|
11383
|
+
type: "boolean",
|
|
11384
|
+
description: "Create a viewer table with enrichment results."
|
|
11069
11385
|
}
|
|
11070
11386
|
},
|
|
11071
|
-
required: [
|
|
11387
|
+
required: []
|
|
11072
11388
|
}
|
|
11073
11389
|
},
|
|
11074
11390
|
{
|
|
@@ -11311,6 +11627,10 @@ var init_campaign = __esm({
|
|
|
11311
11627
|
type: "string",
|
|
11312
11628
|
enum: ["summary", "full"],
|
|
11313
11629
|
description: 'Response detail level. "summary" (default) returns key stats only. "full" returns all campaign data.'
|
|
11630
|
+
},
|
|
11631
|
+
table_id: {
|
|
11632
|
+
type: "string",
|
|
11633
|
+
description: "Read leads from a viewer table instead of passing inline. Fetches selected rows and maps them to campaign leads."
|
|
11314
11634
|
}
|
|
11315
11635
|
},
|
|
11316
11636
|
required: ["action"]
|
|
@@ -11369,6 +11689,10 @@ var init_crm = __esm({
|
|
|
11369
11689
|
provider: {
|
|
11370
11690
|
type: "string",
|
|
11371
11691
|
description: "CRM provider override (uses first available if omitted)"
|
|
11692
|
+
},
|
|
11693
|
+
table_id: {
|
|
11694
|
+
type: "string",
|
|
11695
|
+
description: "Read contacts from a viewer table for bulk create/update. Fetches selected rows and maps them to CRM person records."
|
|
11372
11696
|
}
|
|
11373
11697
|
},
|
|
11374
11698
|
required: ["action"]
|
|
@@ -11507,12 +11831,13 @@ var init_review = __esm({
|
|
|
11507
11831
|
items: { type: "object" },
|
|
11508
11832
|
description: "Array of row objects (for create)"
|
|
11509
11833
|
},
|
|
11510
|
-
table_id: { type: "string", description: "Table ID (for update, get_selected)" },
|
|
11834
|
+
table_id: { type: "string", description: "Table ID (for update, get_selected, or optional client-provided ID on create)" },
|
|
11511
11835
|
rows: {
|
|
11512
11836
|
type: "array",
|
|
11513
11837
|
items: { type: "object" },
|
|
11514
11838
|
description: "Updated row objects with _row_id (for update)"
|
|
11515
|
-
}
|
|
11839
|
+
},
|
|
11840
|
+
ttl_seconds: { type: "number", description: "Optional TTL override in seconds (default: 24h). For create only." }
|
|
11516
11841
|
},
|
|
11517
11842
|
required: ["action"]
|
|
11518
11843
|
}
|
|
@@ -11542,6 +11867,8 @@ Tip: DiscoLike lookalike mode is extremely powerful. If you have 2-3 example com
|
|
|
11542
11867
|
|
|
11543
11868
|
Single domain lookup: passing just domain (no icp_text, domains, category, tech_stack, or country) does a direct company profile fetch with rich data (growth signals, engagement metrics, tech stack from vendors). This is for researching ONE specific company, not discovery.
|
|
11544
11869
|
|
|
11870
|
+
Pass review: true to push discovery results to a visual review table at view.gtmnow.com so the user can select companies before enrichment or outreach.
|
|
11871
|
+
|
|
11545
11872
|
Exa (provider: "exa") \u2014 semantic/neural search:
|
|
11546
11873
|
- Meaning-based ICP matching: "B2B SaaS companies selling to healthcare, not healthtech vendors" actually works
|
|
11547
11874
|
- Best for: nuanced ICP descriptions where keyword matching fails
|
|
@@ -11568,7 +11895,9 @@ After finding people, use:
|
|
|
11568
11895
|
- gtm_extract to check LinkedIn activity and engagement signals
|
|
11569
11896
|
|
|
11570
11897
|
Known limitations:
|
|
11571
|
-
- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other
|
|
11898
|
+
- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other.
|
|
11899
|
+
|
|
11900
|
+
Pass review: true to push results to a visual review table at view.gtmnow.com so the user can select rows before proceeding.`,
|
|
11572
11901
|
gtm_enrich: `Email enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
|
|
11573
11902
|
|
|
11574
11903
|
IMPORTANT: Defaults to email only. Phone enrichment costs extra per lookup \u2014 only include when the user explicitly asks for phone numbers.
|
|
@@ -11581,7 +11910,9 @@ Providers (in configured waterfall order):
|
|
|
11581
11910
|
|
|
11582
11911
|
Input needs: first_name + last_name minimum. Adding company_domain dramatically improves hit rates. LinkedIn URL is the gold standard input.
|
|
11583
11912
|
|
|
11584
|
-
Tip: If you got contacts from gtm_find_people with DiscoLike and they already have emails, you may not need enrichment \u2014 check the results first
|
|
11913
|
+
Tip: If you got contacts from gtm_find_people with DiscoLike and they already have emails, you may not need enrichment \u2014 check the results first.
|
|
11914
|
+
|
|
11915
|
+
Pass table_id to read contacts from an existing review table (selected rows only). Pass review: true to push enrichment results to a new review table.`,
|
|
11585
11916
|
gtm_verify: "Verify a single email address for deliverability. Returns verification status: deliverable, catch_all_safe, catch_all_not_safe, undeliverable, or unknown. Use this after enrichment or to validate an existing email.",
|
|
11586
11917
|
gtm_extract: `Extract structured data from any URL or platform. LinkedIn profiles, posts, engagement, search results, company posts, and web pages.
|
|
11587
11918
|
|
|
@@ -11605,8 +11936,8 @@ Pairs well with:
|
|
|
11605
11936
|
Note: HarvestAPI charges per request. For engagement mining, each post lookup counts as a request. Use limit to control costs.`,
|
|
11606
11937
|
gtm_fetch: "Make an HTTP call to any API endpoint. Use this to pull data from services that don't have a dedicated provider.",
|
|
11607
11938
|
gtm_ai: "AI-powered data operations: personalize outreach, classify leads, clean data, score prospects, or write copy. Provide an action, input data, and optional prompt. Uses your configured AI provider (Anthropic or OpenAI).",
|
|
11608
|
-
gtm_campaign: "Manage cold email AND LinkedIn outreach campaigns. List campaigns, get stats, add leads, or create a new campaign. Supports PlusVibe, Instantly, and Smartlead (email) and HeyReach (LinkedIn). For email campaigns, leads need an email. For LinkedIn campaigns, leads need a linkedin_url. Each lead must have at least one of email or linkedin_url.",
|
|
11609
|
-
gtm_crm: "Read and write CRM data. Find, create, or update people and companies. Create deals. Supports Attio and other CRM providers.",
|
|
11939
|
+
gtm_campaign: "Manage cold email AND LinkedIn outreach campaigns. List campaigns, get stats, add leads, or create a new campaign. Supports PlusVibe, Instantly, and Smartlead (email) and HeyReach (LinkedIn). For email campaigns, leads need an email. For LinkedIn campaigns, leads need a linkedin_url. Each lead must have at least one of email or linkedin_url. Pass table_id to read leads directly from a review table (selected rows only).",
|
|
11940
|
+
gtm_crm: "Read and write CRM data. Find, create, or update people and companies. Create deals. Supports Attio and other CRM providers. For the create action, pass table_id to bulk-create contacts from a review table (selected rows only).",
|
|
11610
11941
|
gtm_status: "Check provider health, credit balances, and active configuration. Shows which providers are configured, their capabilities, remaining credits, and the current client context.",
|
|
11611
11942
|
gtm_configure: "Manage GTM configuration. Switch between clients, view active config, set waterfall order, list available providers.",
|
|
11612
11943
|
gtm_guide: "CALL THIS FIRST before any multi-step GTM task. Returns step-by-step workflow recipes for common tasks like building a TAM list, running enrichment, or launching cold outreach. Always check the guide before starting work to ensure you follow the correct sequence and avoid wasting credits.",
|
|
@@ -11619,7 +11950,7 @@ Note: HarvestAPI charges per request. For engagement mining, each post lookup co
|
|
|
11619
11950
|
});
|
|
11620
11951
|
|
|
11621
11952
|
// src/tools/index.ts
|
|
11622
|
-
var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, toolsByName, toolCategories, workflowRecipes;
|
|
11953
|
+
var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, MCP_HIDDEN_TOOLS, mcpTools, toolsByName, toolCategories, workflowRecipes;
|
|
11623
11954
|
var init_tools = __esm({
|
|
11624
11955
|
"src/tools/index.ts"() {
|
|
11625
11956
|
"use strict";
|
|
@@ -11715,6 +12046,8 @@ var init_tools = __esm({
|
|
|
11715
12046
|
};
|
|
11716
12047
|
metaTools = [guideTool, toolHelpTool, executeTool];
|
|
11717
12048
|
tools = [...capabilityTools, ...metaTools];
|
|
12049
|
+
MCP_HIDDEN_TOOLS = /* @__PURE__ */ new Set(["gtm_execute", "gtm_guide", "gtm_tool_help"]);
|
|
12050
|
+
mcpTools = tools.filter((t) => !MCP_HIDDEN_TOOLS.has(t.name));
|
|
11718
12051
|
toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
11719
12052
|
toolCategories = {
|
|
11720
12053
|
prospect: prospectTools.map((t) => t.name),
|
|
@@ -11974,6 +12307,7 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
11974
12307
|
let phone;
|
|
11975
12308
|
let phoneProvider;
|
|
11976
12309
|
let charged = false;
|
|
12310
|
+
let creditsConsumed;
|
|
11977
12311
|
const cacheKey = getCacheKey(contact);
|
|
11978
12312
|
const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
|
|
11979
12313
|
for (const field of config.find) {
|
|
@@ -12004,6 +12338,9 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
12004
12338
|
email = fieldResult.value;
|
|
12005
12339
|
emailProvider = fieldResult.provider;
|
|
12006
12340
|
charged = true;
|
|
12341
|
+
if (fieldResult.credits_consumed) {
|
|
12342
|
+
creditsConsumed = (creditsConsumed ?? 0) + fieldResult.credits_consumed;
|
|
12343
|
+
}
|
|
12007
12344
|
} else if (field === "phone" && fieldResult.value) {
|
|
12008
12345
|
phone = fieldResult.value;
|
|
12009
12346
|
phoneProvider = fieldResult.provider;
|
|
@@ -12026,7 +12363,8 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
12026
12363
|
email_provider: emailProvider,
|
|
12027
12364
|
phone_provider: phoneProvider,
|
|
12028
12365
|
attempts,
|
|
12029
|
-
charged
|
|
12366
|
+
charged,
|
|
12367
|
+
...creditsConsumed !== void 0 && { credits_consumed: creditsConsumed }
|
|
12030
12368
|
};
|
|
12031
12369
|
}
|
|
12032
12370
|
async function enrichField(contact, field, config, registry, exhausted) {
|
|
@@ -12051,6 +12389,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12051
12389
|
try {
|
|
12052
12390
|
let value;
|
|
12053
12391
|
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12392
|
+
let creditsConsumed;
|
|
12054
12393
|
if (field === "email" && "enrichEmail" in provider) {
|
|
12055
12394
|
const result = await withTimeout(
|
|
12056
12395
|
provider.enrichEmail(contact),
|
|
@@ -12058,6 +12397,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12058
12397
|
providerName
|
|
12059
12398
|
);
|
|
12060
12399
|
value = result?.email;
|
|
12400
|
+
creditsConsumed = result?.credits_consumed;
|
|
12061
12401
|
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12062
12402
|
const result = await withTimeout(
|
|
12063
12403
|
provider.enrichPhone(contact),
|
|
@@ -12075,7 +12415,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12075
12415
|
duration_ms: Date.now() - start
|
|
12076
12416
|
});
|
|
12077
12417
|
if (found && config.stopOnFirst) {
|
|
12078
|
-
return { value, provider: providerName, attempts };
|
|
12418
|
+
return { value, provider: providerName, credits_consumed: creditsConsumed, attempts };
|
|
12079
12419
|
}
|
|
12080
12420
|
break;
|
|
12081
12421
|
} catch (error) {
|
|
@@ -12149,7 +12489,8 @@ var init_waterfall = __esm({
|
|
|
12149
12489
|
bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
|
|
12150
12490
|
prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12151
12491
|
apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
|
|
12152
|
-
hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" }
|
|
12492
|
+
hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12493
|
+
leadmagic: { charged_only_on_find: true, estimated_cost_per_hit: "$0.03" }
|
|
12153
12494
|
};
|
|
12154
12495
|
EXHAUSTION_PATTERNS = [
|
|
12155
12496
|
"insufficient credits",
|
|
@@ -12166,6 +12507,123 @@ var init_waterfall = __esm({
|
|
|
12166
12507
|
}
|
|
12167
12508
|
});
|
|
12168
12509
|
|
|
12510
|
+
// src/utils/table-bridge.ts
|
|
12511
|
+
function asString(value) {
|
|
12512
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
12513
|
+
}
|
|
12514
|
+
function splitName(full) {
|
|
12515
|
+
const idx = full.indexOf(" ");
|
|
12516
|
+
if (idx === -1) return { first_name: full, last_name: "" };
|
|
12517
|
+
return { first_name: full.slice(0, idx), last_name: full.slice(idx + 1) };
|
|
12518
|
+
}
|
|
12519
|
+
async function createReviewTable(title, data, options) {
|
|
12520
|
+
try {
|
|
12521
|
+
const rows = data.map((row, i) => ({
|
|
12522
|
+
_row_id: `r${i + 1}`,
|
|
12523
|
+
...row
|
|
12524
|
+
}));
|
|
12525
|
+
const body = { title, rows };
|
|
12526
|
+
if (options?.table_id !== void 0) body.table_id = options.table_id;
|
|
12527
|
+
if (options?.ttl_seconds !== void 0) body.ttl_seconds = options.ttl_seconds;
|
|
12528
|
+
const response = await fetch(`${VIEWER_URL}/api/tables`, {
|
|
12529
|
+
method: "POST",
|
|
12530
|
+
headers: { "Content-Type": "application/json" },
|
|
12531
|
+
body: JSON.stringify(body)
|
|
12532
|
+
});
|
|
12533
|
+
if (!response.ok) {
|
|
12534
|
+
logError("table-bridge.createReviewTable", new Error(`Viewer returned ${response.status}`), {
|
|
12535
|
+
title,
|
|
12536
|
+
status: response.status
|
|
12537
|
+
});
|
|
12538
|
+
return null;
|
|
12539
|
+
}
|
|
12540
|
+
return await response.json();
|
|
12541
|
+
} catch (error) {
|
|
12542
|
+
logError("table-bridge.createReviewTable", error, { title });
|
|
12543
|
+
return null;
|
|
12544
|
+
}
|
|
12545
|
+
}
|
|
12546
|
+
async function fetchTableRows(tableId) {
|
|
12547
|
+
try {
|
|
12548
|
+
const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}/selected`);
|
|
12549
|
+
if (!response.ok) {
|
|
12550
|
+
logError("table-bridge.fetchTableRows", new Error(`Viewer returned ${response.status}`), {
|
|
12551
|
+
tableId,
|
|
12552
|
+
status: response.status
|
|
12553
|
+
});
|
|
12554
|
+
return [];
|
|
12555
|
+
}
|
|
12556
|
+
const json = await response.json();
|
|
12557
|
+
return json.rows ?? [];
|
|
12558
|
+
} catch (error) {
|
|
12559
|
+
logError("table-bridge.fetchTableRows", error, { tableId });
|
|
12560
|
+
return [];
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
async function patchTableRows(tableId, rows) {
|
|
12564
|
+
try {
|
|
12565
|
+
const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}`, {
|
|
12566
|
+
method: "PATCH",
|
|
12567
|
+
headers: { "Content-Type": "application/json" },
|
|
12568
|
+
body: JSON.stringify({ rows })
|
|
12569
|
+
});
|
|
12570
|
+
if (!response.ok) {
|
|
12571
|
+
logError("table-bridge.patchTableRows", new Error(`Viewer returned ${response.status}`), {
|
|
12572
|
+
tableId,
|
|
12573
|
+
status: response.status
|
|
12574
|
+
});
|
|
12575
|
+
return false;
|
|
12576
|
+
}
|
|
12577
|
+
return true;
|
|
12578
|
+
} catch (error) {
|
|
12579
|
+
logError("table-bridge.patchTableRows", error, { tableId });
|
|
12580
|
+
return false;
|
|
12581
|
+
}
|
|
12582
|
+
}
|
|
12583
|
+
function mapRowsToContacts(rows) {
|
|
12584
|
+
return rows.map((row) => {
|
|
12585
|
+
let firstName = asString(row.first_name);
|
|
12586
|
+
let lastName = asString(row.last_name);
|
|
12587
|
+
if (firstName === void 0 || lastName === void 0) {
|
|
12588
|
+
const fullName = asString(row.name) ?? asString(row.full_name);
|
|
12589
|
+
if (fullName) {
|
|
12590
|
+
const parts = splitName(fullName);
|
|
12591
|
+
firstName ??= parts.first_name;
|
|
12592
|
+
lastName ??= parts.last_name;
|
|
12593
|
+
}
|
|
12594
|
+
}
|
|
12595
|
+
return {
|
|
12596
|
+
first_name: firstName ?? "",
|
|
12597
|
+
last_name: lastName ?? "",
|
|
12598
|
+
company: asString(row.company) ?? asString(row.company_name),
|
|
12599
|
+
company_domain: asString(row.company_domain) ?? asString(row.domain),
|
|
12600
|
+
linkedin_url: asString(row.linkedin_url) ?? asString(row.linkedin)
|
|
12601
|
+
};
|
|
12602
|
+
});
|
|
12603
|
+
}
|
|
12604
|
+
function mapRowsToCampaignLeads(rows) {
|
|
12605
|
+
const leads = [];
|
|
12606
|
+
for (const row of rows) {
|
|
12607
|
+
const email = asString(row.email);
|
|
12608
|
+
if (!email || email.includes("*")) continue;
|
|
12609
|
+
leads.push({
|
|
12610
|
+
email,
|
|
12611
|
+
first_name: asString(row.first_name),
|
|
12612
|
+
last_name: asString(row.last_name),
|
|
12613
|
+
company: asString(row.company) ?? asString(row.company_name)
|
|
12614
|
+
});
|
|
12615
|
+
}
|
|
12616
|
+
return leads;
|
|
12617
|
+
}
|
|
12618
|
+
var VIEWER_URL;
|
|
12619
|
+
var init_table_bridge = __esm({
|
|
12620
|
+
"src/utils/table-bridge.ts"() {
|
|
12621
|
+
"use strict";
|
|
12622
|
+
init_dist();
|
|
12623
|
+
VIEWER_URL = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
|
|
12624
|
+
}
|
|
12625
|
+
});
|
|
12626
|
+
|
|
12169
12627
|
// src/providers/types.ts
|
|
12170
12628
|
function applySlimFields(obj, fields) {
|
|
12171
12629
|
const result = {};
|
|
@@ -12398,6 +12856,27 @@ async function findCompanies(args, ctx) {
|
|
|
12398
12856
|
});
|
|
12399
12857
|
const offset = args.offset ?? 0;
|
|
12400
12858
|
const has_more = results.length >= effectiveLimit;
|
|
12859
|
+
const hints = [];
|
|
12860
|
+
if (results.length >= effectiveLimit) {
|
|
12861
|
+
hints.push(
|
|
12862
|
+
`Returned ${results.length} results (limit reached). Increase limit or use offset for more.`
|
|
12863
|
+
);
|
|
12864
|
+
}
|
|
12865
|
+
const shouldReview = args.review ?? false;
|
|
12866
|
+
let tableId;
|
|
12867
|
+
let tableUrl;
|
|
12868
|
+
if (shouldReview) {
|
|
12869
|
+
const tableResult = await createReviewTable(
|
|
12870
|
+
`Find Companies: ${query.icp_text ?? query.domain ?? "results"}`,
|
|
12871
|
+
slimmedResults
|
|
12872
|
+
);
|
|
12873
|
+
if (tableResult) {
|
|
12874
|
+
tableId = tableResult.table_id;
|
|
12875
|
+
tableUrl = tableResult.url;
|
|
12876
|
+
} else {
|
|
12877
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12401
12880
|
const applied_filters = {};
|
|
12402
12881
|
if (query.icp_text) applied_filters.icp_text = query.icp_text;
|
|
12403
12882
|
if (query.domain) applied_filters.domain = query.domain;
|
|
@@ -12452,9 +12931,11 @@ async function findCompanies(args, ctx) {
|
|
|
12452
12931
|
count: results.length,
|
|
12453
12932
|
has_more,
|
|
12454
12933
|
offset,
|
|
12934
|
+
...tableId && { table_id: tableId, url: tableUrl },
|
|
12455
12935
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12456
12936
|
companies: slimmedResults,
|
|
12457
|
-
...warnings?.length ? { warnings } : {}
|
|
12937
|
+
...warnings?.length ? { warnings } : {},
|
|
12938
|
+
...hints.length > 0 && { hints }
|
|
12458
12939
|
};
|
|
12459
12940
|
}
|
|
12460
12941
|
async function findPeople(args, ctx) {
|
|
@@ -12495,10 +12976,10 @@ async function findPeople(args, ctx) {
|
|
|
12495
12976
|
const dedupedResults = deduplicatePeople(results);
|
|
12496
12977
|
const shouldEnrich = args.enrich ?? false;
|
|
12497
12978
|
let enrichedCount = 0;
|
|
12979
|
+
let needsEnrichmentCount = 0;
|
|
12498
12980
|
if (shouldEnrich) {
|
|
12499
|
-
const needsEnrichment = dedupedResults.filter(
|
|
12500
|
-
|
|
12501
|
-
);
|
|
12981
|
+
const needsEnrichment = dedupedResults.filter((r) => !r.email || r.email.includes("*"));
|
|
12982
|
+
needsEnrichmentCount = needsEnrichment.length;
|
|
12502
12983
|
if (needsEnrichment.length > 0) {
|
|
12503
12984
|
try {
|
|
12504
12985
|
const contacts = needsEnrichment.map((r) => ({
|
|
@@ -12526,7 +13007,10 @@ async function findPeople(args, ctx) {
|
|
|
12526
13007
|
}
|
|
12527
13008
|
}
|
|
12528
13009
|
} catch (error) {
|
|
12529
|
-
logError(
|
|
13010
|
+
logError(
|
|
13011
|
+
"prospect:inlineEnrich",
|
|
13012
|
+
error instanceof Error ? error : new Error(String(error))
|
|
13013
|
+
);
|
|
12530
13014
|
}
|
|
12531
13015
|
}
|
|
12532
13016
|
}
|
|
@@ -12555,6 +13039,37 @@ async function findPeople(args, ctx) {
|
|
|
12555
13039
|
if (query.min_employees != null) applied_filters.min_employees = query.min_employees;
|
|
12556
13040
|
if (query.max_employees != null) applied_filters.max_employees = query.max_employees;
|
|
12557
13041
|
if (query.revenue_range) applied_filters.revenue_range = query.revenue_range;
|
|
13042
|
+
const hints = [];
|
|
13043
|
+
const maskedCount = dedupedResults.filter((r) => r.email?.includes("*")).length;
|
|
13044
|
+
if (maskedCount > 0) {
|
|
13045
|
+
hints.push(
|
|
13046
|
+
`${maskedCount} emails are masked by the provider. Pass enrich: true to resolve them.`
|
|
13047
|
+
);
|
|
13048
|
+
}
|
|
13049
|
+
if (dedupedResults.length >= effectiveLimit) {
|
|
13050
|
+
hints.push(
|
|
13051
|
+
`Returned ${dedupedResults.length} results (limit reached). Increase limit or use offset for more.`
|
|
13052
|
+
);
|
|
13053
|
+
}
|
|
13054
|
+
if (shouldEnrich && enrichedCount < needsEnrichmentCount) {
|
|
13055
|
+
const failed = needsEnrichmentCount - enrichedCount;
|
|
13056
|
+
hints.push(`${failed} contacts could not be enriched \u2014 missing LinkedIn URL or domain.`);
|
|
13057
|
+
}
|
|
13058
|
+
const shouldReview = args.review ?? false;
|
|
13059
|
+
let tableId;
|
|
13060
|
+
let tableUrl;
|
|
13061
|
+
if (shouldReview) {
|
|
13062
|
+
const tableResult = await createReviewTable(
|
|
13063
|
+
`Find People: ${query.job_title ?? query.company_domain ?? "results"}`,
|
|
13064
|
+
slimmedResults
|
|
13065
|
+
);
|
|
13066
|
+
if (tableResult) {
|
|
13067
|
+
tableId = tableResult.table_id;
|
|
13068
|
+
tableUrl = tableResult.url;
|
|
13069
|
+
} else {
|
|
13070
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
13071
|
+
}
|
|
13072
|
+
}
|
|
12558
13073
|
const providerUsed = provider.name;
|
|
12559
13074
|
const output = args.output;
|
|
12560
13075
|
if (output === "csv") {
|
|
@@ -12595,10 +13110,12 @@ async function findPeople(args, ctx) {
|
|
|
12595
13110
|
count: dedupedResults.length,
|
|
12596
13111
|
has_more,
|
|
12597
13112
|
offset,
|
|
13113
|
+
...tableId && { table_id: tableId, url: tableUrl },
|
|
12598
13114
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12599
13115
|
...shouldEnrich && { enriched: enrichedCount },
|
|
12600
13116
|
people: slimmedResults,
|
|
12601
|
-
...warnings?.length ? { warnings } : {}
|
|
13117
|
+
...warnings?.length ? { warnings } : {},
|
|
13118
|
+
...hints.length > 0 && { hints }
|
|
12602
13119
|
};
|
|
12603
13120
|
}
|
|
12604
13121
|
var init_prospect2 = __esm({
|
|
@@ -12607,6 +13124,7 @@ var init_prospect2 = __esm({
|
|
|
12607
13124
|
init_dist();
|
|
12608
13125
|
init_errors();
|
|
12609
13126
|
init_waterfall();
|
|
13127
|
+
init_table_bridge();
|
|
12610
13128
|
init_types3();
|
|
12611
13129
|
init_manifest_validation();
|
|
12612
13130
|
init_result_writer();
|
|
@@ -12639,10 +13157,29 @@ async function handleEnrich(name, args, ctx) {
|
|
|
12639
13157
|
return enrichContacts(args, ctx);
|
|
12640
13158
|
}
|
|
12641
13159
|
async function enrichContacts(args, ctx) {
|
|
12642
|
-
|
|
13160
|
+
let contacts = args.contacts;
|
|
12643
13161
|
const find = args.find ?? ["email"];
|
|
12644
13162
|
const verify = args.verify ?? true;
|
|
12645
13163
|
const dryRun = args.dry_run ?? false;
|
|
13164
|
+
const detail = args.detail ?? "summary";
|
|
13165
|
+
const tableId = args.table_id;
|
|
13166
|
+
let rowIds = [];
|
|
13167
|
+
if (tableId) {
|
|
13168
|
+
const tableRows = await fetchTableRows(tableId);
|
|
13169
|
+
if (tableRows.length === 0) {
|
|
13170
|
+
return {
|
|
13171
|
+
total: 0,
|
|
13172
|
+
found: 0,
|
|
13173
|
+
missed: 0,
|
|
13174
|
+
results: [],
|
|
13175
|
+
hints: [
|
|
13176
|
+
"No rows selected in table. User may not have reviewed yet, or may have deselected all."
|
|
13177
|
+
]
|
|
13178
|
+
};
|
|
13179
|
+
}
|
|
13180
|
+
rowIds = tableRows.map((r) => r._row_id);
|
|
13181
|
+
contacts = mapRowsToContacts(tableRows);
|
|
13182
|
+
}
|
|
12646
13183
|
const WATERFALL_FIELDS = ["email", "phone"];
|
|
12647
13184
|
const richFields = find.filter((f) => !WATERFALL_FIELDS.includes(f));
|
|
12648
13185
|
if (richFields.length > 0) {
|
|
@@ -12675,11 +13212,65 @@ async function enrichContacts(args, ctx) {
|
|
|
12675
13212
|
}
|
|
12676
13213
|
const results = await runWaterfall(contacts, config, ctx.registry);
|
|
12677
13214
|
const found = results.filter((r) => r.email || r.phone).length;
|
|
12678
|
-
|
|
13215
|
+
if (tableId) {
|
|
13216
|
+
const patchPayload = results.map((r, i) => ({
|
|
13217
|
+
_row_id: rowIds[i],
|
|
13218
|
+
...r.email && { email: r.email },
|
|
13219
|
+
...r.email_provider && { email_provider: r.email_provider },
|
|
13220
|
+
...r.email_status && { email_status: r.email_status }
|
|
13221
|
+
}));
|
|
13222
|
+
await patchTableRows(tableId, patchPayload);
|
|
13223
|
+
}
|
|
13224
|
+
const hints = [];
|
|
13225
|
+
const exhaustedAttempts = results.flatMap(
|
|
13226
|
+
(r) => r.attempts.filter((a) => a.error?.includes("exhausted"))
|
|
13227
|
+
);
|
|
13228
|
+
if (exhaustedAttempts.length > 0) {
|
|
13229
|
+
const provider = exhaustedAttempts[0].provider;
|
|
13230
|
+
hints.push(
|
|
13231
|
+
`${provider} credits exhausted mid-batch. Remaining contacts fell through to next provider.`
|
|
13232
|
+
);
|
|
13233
|
+
}
|
|
13234
|
+
const noLinkedin = contacts.filter((c) => !c.linkedin_url).length;
|
|
13235
|
+
if (noLinkedin > 0) {
|
|
13236
|
+
hints.push(`${noLinkedin} contacts had no LinkedIn URL \u2014 some providers skipped them.`);
|
|
13237
|
+
}
|
|
13238
|
+
const timedOut = results.filter(
|
|
13239
|
+
(r) => r.attempts.some((a) => a.error?.includes("Batch timeout"))
|
|
13240
|
+
).length;
|
|
13241
|
+
if (timedOut > 0) {
|
|
13242
|
+
hints.push(`Batch timed out with ${timedOut} contacts remaining. Re-run with those contacts.`);
|
|
13243
|
+
}
|
|
13244
|
+
const costMap = /* @__PURE__ */ new Map();
|
|
13245
|
+
for (const r of results) {
|
|
13246
|
+
if (r.credits_consumed && r.email_provider) {
|
|
13247
|
+
costMap.set(r.email_provider, (costMap.get(r.email_provider) ?? 0) + r.credits_consumed);
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13250
|
+
const cost = costMap.size > 0 ? Array.from(costMap.entries()).map(([provider, credits]) => ({ provider, credits })) : void 0;
|
|
13251
|
+
const shouldReview = args.review ?? false;
|
|
13252
|
+
let reviewTableId;
|
|
13253
|
+
let reviewUrl;
|
|
13254
|
+
if (shouldReview) {
|
|
13255
|
+
const tableResult = await createReviewTable(
|
|
13256
|
+
"Enrichment Results",
|
|
13257
|
+
detail === "full" ? results : slimEnrichResults(results)
|
|
13258
|
+
);
|
|
13259
|
+
if (tableResult) {
|
|
13260
|
+
reviewTableId = tableResult.table_id;
|
|
13261
|
+
reviewUrl = tableResult.url;
|
|
13262
|
+
} else {
|
|
13263
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
12679
13266
|
return {
|
|
12680
13267
|
total: results.length,
|
|
12681
13268
|
found,
|
|
12682
13269
|
missed: results.length - found,
|
|
13270
|
+
...tableId && { table_id: tableId },
|
|
13271
|
+
...reviewTableId && { table_id: reviewTableId, url: reviewUrl },
|
|
13272
|
+
...hints.length > 0 && { hints },
|
|
13273
|
+
...cost && { cost },
|
|
12683
13274
|
results: detail === "full" ? results : slimEnrichResults(results)
|
|
12684
13275
|
};
|
|
12685
13276
|
}
|
|
@@ -12701,6 +13292,7 @@ var init_enrich2 = __esm({
|
|
|
12701
13292
|
"use strict";
|
|
12702
13293
|
init_waterfall();
|
|
12703
13294
|
init_errors();
|
|
13295
|
+
init_table_bridge();
|
|
12704
13296
|
}
|
|
12705
13297
|
});
|
|
12706
13298
|
|
|
@@ -13136,11 +13728,38 @@ async function handleCampaign(args, ctx) {
|
|
|
13136
13728
|
}
|
|
13137
13729
|
case "add_leads": {
|
|
13138
13730
|
const campaignId = args.campaign_id;
|
|
13139
|
-
const
|
|
13731
|
+
const tableId = args.table_id;
|
|
13140
13732
|
if (!campaignId)
|
|
13141
13733
|
throw createGtmError("QUERY_INVALID", "campaign_id is required for add_leads action");
|
|
13734
|
+
const hints = [];
|
|
13735
|
+
let rawLeads = args.leads;
|
|
13736
|
+
if (tableId) {
|
|
13737
|
+
const tableRows = await fetchTableRows(tableId);
|
|
13738
|
+
if (tableRows.length === 0) {
|
|
13739
|
+
return {
|
|
13740
|
+
provider: channel.providerName,
|
|
13741
|
+
added: 0,
|
|
13742
|
+
hints: [
|
|
13743
|
+
"No rows selected in table. User may not have reviewed yet, or may have deselected all."
|
|
13744
|
+
]
|
|
13745
|
+
};
|
|
13746
|
+
}
|
|
13747
|
+
if (channel.type === "linkedin") {
|
|
13748
|
+
rawLeads = tableRows;
|
|
13749
|
+
} else {
|
|
13750
|
+
const mapped = mapRowsToCampaignLeads(tableRows);
|
|
13751
|
+
const skipped = tableRows.length - mapped.length;
|
|
13752
|
+
if (skipped > 0) {
|
|
13753
|
+
hints.push(`${skipped} rows skipped \u2014 missing email (required for campaign).`);
|
|
13754
|
+
}
|
|
13755
|
+
rawLeads = mapped;
|
|
13756
|
+
}
|
|
13757
|
+
}
|
|
13142
13758
|
if (!rawLeads?.length)
|
|
13143
|
-
throw createGtmError(
|
|
13759
|
+
throw createGtmError(
|
|
13760
|
+
"QUERY_INVALID",
|
|
13761
|
+
"leads array is required for add_leads action (or provide table_id)"
|
|
13762
|
+
);
|
|
13144
13763
|
if (channel.type === "linkedin") {
|
|
13145
13764
|
const leads2 = rawLeads.map((l) => ({
|
|
13146
13765
|
linkedin_url: l.linkedin_url,
|
|
@@ -13148,7 +13767,11 @@ async function handleCampaign(args, ctx) {
|
|
|
13148
13767
|
last_name: l.last_name,
|
|
13149
13768
|
company: l.company
|
|
13150
13769
|
}));
|
|
13151
|
-
|
|
13770
|
+
const result = await channel.manager.addLeadsToCampaign(campaignId, leads2);
|
|
13771
|
+
return {
|
|
13772
|
+
...result,
|
|
13773
|
+
...hints.length > 0 && { hints }
|
|
13774
|
+
};
|
|
13152
13775
|
}
|
|
13153
13776
|
const leads = rawLeads.map((l) => ({
|
|
13154
13777
|
email: l.email,
|
|
@@ -13158,7 +13781,11 @@ async function handleCampaign(args, ctx) {
|
|
|
13158
13781
|
custom_fields: l.custom_fields
|
|
13159
13782
|
}));
|
|
13160
13783
|
try {
|
|
13161
|
-
|
|
13784
|
+
const result = await channel.manager.addLeadsToCampaign(campaignId, leads);
|
|
13785
|
+
return {
|
|
13786
|
+
...result,
|
|
13787
|
+
...hints.length > 0 && { hints }
|
|
13788
|
+
};
|
|
13162
13789
|
} catch (error) {
|
|
13163
13790
|
if (error instanceof ApiClientError && (error.message.includes("Validation error") || error.statusCode === 400 || error.statusCode === 422)) {
|
|
13164
13791
|
throw createGtmError(
|
|
@@ -13196,6 +13823,7 @@ var init_campaign2 = __esm({
|
|
|
13196
13823
|
"use strict";
|
|
13197
13824
|
init_dist2();
|
|
13198
13825
|
init_errors();
|
|
13826
|
+
init_table_bridge();
|
|
13199
13827
|
}
|
|
13200
13828
|
});
|
|
13201
13829
|
|
|
@@ -13223,6 +13851,28 @@ async function handleCrm(args, ctx) {
|
|
|
13223
13851
|
return manager.findPerson(query);
|
|
13224
13852
|
}
|
|
13225
13853
|
case "create": {
|
|
13854
|
+
const tableId = args.table_id;
|
|
13855
|
+
if (tableId) {
|
|
13856
|
+
const rows = await fetchTableRows(tableId);
|
|
13857
|
+
if (rows.length === 0) {
|
|
13858
|
+
return {
|
|
13859
|
+
created: 0,
|
|
13860
|
+
hints: ["No selected rows found in the review table. Select rows in the viewer and retry."]
|
|
13861
|
+
};
|
|
13862
|
+
}
|
|
13863
|
+
const contacts = mapRowsToContacts(rows);
|
|
13864
|
+
const results = await Promise.allSettled(
|
|
13865
|
+
contacts.map((contact) => manager.createPerson(contact))
|
|
13866
|
+
);
|
|
13867
|
+
const created = results.filter((r) => r.status === "fulfilled").length;
|
|
13868
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
13869
|
+
return {
|
|
13870
|
+
created,
|
|
13871
|
+
failed,
|
|
13872
|
+
total: contacts.length,
|
|
13873
|
+
hints: failed > 0 ? [`${failed} contact(s) failed to create \u2014 check for duplicates or missing fields.`] : void 0
|
|
13874
|
+
};
|
|
13875
|
+
}
|
|
13226
13876
|
const data = args.data;
|
|
13227
13877
|
if (!data) throw new Error("data is required for create action");
|
|
13228
13878
|
return manager.createPerson(data);
|
|
@@ -13254,6 +13904,7 @@ async function handleCrm(args, ctx) {
|
|
|
13254
13904
|
var init_crm2 = __esm({
|
|
13255
13905
|
"src/handlers/crm.ts"() {
|
|
13256
13906
|
"use strict";
|
|
13907
|
+
init_table_bridge();
|
|
13257
13908
|
}
|
|
13258
13909
|
});
|
|
13259
13910
|
|
|
@@ -13509,14 +14160,21 @@ async function handleReview(args) {
|
|
|
13509
14160
|
async function createTable(args) {
|
|
13510
14161
|
const data = args.data;
|
|
13511
14162
|
const title = args.title ?? "Review Table";
|
|
14163
|
+
const tableId = args.table_id;
|
|
14164
|
+
const ttlSeconds = args.ttl_seconds;
|
|
13512
14165
|
const rows = data.map((row, i) => ({
|
|
13513
14166
|
...row,
|
|
13514
14167
|
_row_id: row._row_id ?? `r${i + 1}`
|
|
13515
14168
|
}));
|
|
13516
|
-
const response = await fetch(`${
|
|
14169
|
+
const response = await fetch(`${VIEWER_URL2}/api/tables`, {
|
|
13517
14170
|
method: "POST",
|
|
13518
14171
|
headers: { "Content-Type": "application/json" },
|
|
13519
|
-
body: JSON.stringify({
|
|
14172
|
+
body: JSON.stringify({
|
|
14173
|
+
title,
|
|
14174
|
+
rows,
|
|
14175
|
+
...tableId && { table_id: tableId },
|
|
14176
|
+
...ttlSeconds && { ttl_seconds: ttlSeconds }
|
|
14177
|
+
})
|
|
13520
14178
|
});
|
|
13521
14179
|
if (!response.ok) {
|
|
13522
14180
|
const body = await response.json().catch(() => ({}));
|
|
@@ -13528,7 +14186,7 @@ async function createTable(args) {
|
|
|
13528
14186
|
async function updateTable(args) {
|
|
13529
14187
|
const tableId = args.table_id;
|
|
13530
14188
|
const rows = args.rows;
|
|
13531
|
-
const response = await fetch(`${
|
|
14189
|
+
const response = await fetch(`${VIEWER_URL2}/api/tables/${tableId}`, {
|
|
13532
14190
|
method: "PATCH",
|
|
13533
14191
|
headers: { "Content-Type": "application/json" },
|
|
13534
14192
|
body: JSON.stringify({ rows })
|
|
@@ -13541,18 +14199,18 @@ async function updateTable(args) {
|
|
|
13541
14199
|
}
|
|
13542
14200
|
async function getSelected(args) {
|
|
13543
14201
|
const tableId = args.table_id;
|
|
13544
|
-
const response = await fetch(`${
|
|
14202
|
+
const response = await fetch(`${VIEWER_URL2}/api/tables/${tableId}/selected`);
|
|
13545
14203
|
if (!response.ok) {
|
|
13546
14204
|
const body = await response.json().catch(() => ({}));
|
|
13547
14205
|
throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
|
|
13548
14206
|
}
|
|
13549
14207
|
return response.json();
|
|
13550
14208
|
}
|
|
13551
|
-
var
|
|
14209
|
+
var VIEWER_URL2;
|
|
13552
14210
|
var init_review2 = __esm({
|
|
13553
14211
|
"src/handlers/review.ts"() {
|
|
13554
14212
|
"use strict";
|
|
13555
|
-
|
|
14213
|
+
VIEWER_URL2 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
|
|
13556
14214
|
}
|
|
13557
14215
|
});
|
|
13558
14216
|
|
|
@@ -13794,7 +14452,8 @@ var init_validation = __esm({
|
|
|
13794
14452
|
min_revenue: z2.number().optional(),
|
|
13795
14453
|
max_revenue: z2.number().optional(),
|
|
13796
14454
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
13797
|
-
detail: detailSchema
|
|
14455
|
+
detail: detailSchema,
|
|
14456
|
+
review: z2.boolean().optional()
|
|
13798
14457
|
}),
|
|
13799
14458
|
// 2. People/contact search
|
|
13800
14459
|
gtm_find_people: z2.object({
|
|
@@ -13834,6 +14493,7 @@ var init_validation = __esm({
|
|
|
13834
14493
|
max_employees: z2.number().int().min(1).optional(),
|
|
13835
14494
|
revenue_range: z2.string().optional(),
|
|
13836
14495
|
enrich: z2.boolean().optional(),
|
|
14496
|
+
review: z2.boolean().optional(),
|
|
13837
14497
|
fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
|
|
13838
14498
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
13839
14499
|
detail: detailSchema
|
|
@@ -13845,12 +14505,16 @@ var init_validation = __esm({
|
|
|
13845
14505
|
),
|
|
13846
14506
|
// 3. Enrichment waterfall
|
|
13847
14507
|
gtm_enrich: z2.object({
|
|
13848
|
-
contacts: z2.array(contactInputSchema).
|
|
14508
|
+
contacts: z2.array(contactInputSchema).max(100, "contacts must contain at most 100 contacts").optional(),
|
|
13849
14509
|
find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
|
|
13850
14510
|
waterfall: z2.array(z2.string()).optional(),
|
|
13851
14511
|
verify: z2.boolean().optional(),
|
|
13852
14512
|
dry_run: z2.boolean().optional(),
|
|
13853
|
-
detail: detailSchema
|
|
14513
|
+
detail: detailSchema,
|
|
14514
|
+
table_id: z2.string().optional(),
|
|
14515
|
+
review: z2.boolean().optional()
|
|
14516
|
+
}).refine((data) => !!(data.contacts?.length || data.table_id), {
|
|
14517
|
+
message: "At least one of contacts or table_id is required"
|
|
13854
14518
|
}),
|
|
13855
14519
|
// 4. Email verification
|
|
13856
14520
|
gtm_verify: z2.object({
|
|
@@ -13934,7 +14598,8 @@ var init_validation = __esm({
|
|
|
13934
14598
|
leads: z2.array(campaignLeadSchema).optional(),
|
|
13935
14599
|
name: z2.string().optional(),
|
|
13936
14600
|
provider: z2.string().optional(),
|
|
13937
|
-
detail: detailSchema
|
|
14601
|
+
detail: detailSchema,
|
|
14602
|
+
table_id: z2.string().optional()
|
|
13938
14603
|
}),
|
|
13939
14604
|
// 10. CRM read/write
|
|
13940
14605
|
gtm_crm: z2.object({
|
|
@@ -13943,7 +14608,8 @@ var init_validation = __esm({
|
|
|
13943
14608
|
}),
|
|
13944
14609
|
query: z2.record(z2.unknown()).optional(),
|
|
13945
14610
|
data: z2.record(z2.unknown()).optional(),
|
|
13946
|
-
id: z2.string().optional()
|
|
14611
|
+
id: z2.string().optional(),
|
|
14612
|
+
table_id: z2.string().optional()
|
|
13947
14613
|
}),
|
|
13948
14614
|
// 11. Provider status/credits
|
|
13949
14615
|
gtm_status: z2.object({
|
|
@@ -13996,7 +14662,9 @@ var init_validation = __esm({
|
|
|
13996
14662
|
z2.object({
|
|
13997
14663
|
action: z2.literal("create"),
|
|
13998
14664
|
title: z2.string().optional(),
|
|
13999
|
-
data: z2.array(z2.record(z2.unknown())).min(1, "data must contain at least 1 row")
|
|
14665
|
+
data: z2.array(z2.record(z2.unknown())).min(1, "data must contain at least 1 row"),
|
|
14666
|
+
table_id: z2.string().optional(),
|
|
14667
|
+
ttl_seconds: z2.number().optional()
|
|
14000
14668
|
}),
|
|
14001
14669
|
z2.object({
|
|
14002
14670
|
action: z2.literal("update"),
|
|
@@ -14035,7 +14703,7 @@ function createServer() {
|
|
|
14035
14703
|
{ capabilities: { tools: {} } }
|
|
14036
14704
|
);
|
|
14037
14705
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
14038
|
-
tools
|
|
14706
|
+
tools: mcpTools
|
|
14039
14707
|
}));
|
|
14040
14708
|
server.setRequestHandler(CallToolRequestSchema, async (request, _extra) => {
|
|
14041
14709
|
const { name, arguments: args = {} } = request.params;
|