gtm-now 0.10.8 → 0.10.10
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 +445 -365
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -205,8 +205,7 @@ 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"
|
|
209
|
-
GTM_LEADMAGIC_API_KEY: "leadmagic"
|
|
208
|
+
GTM_EXA_API_KEY: "exa"
|
|
210
209
|
};
|
|
211
210
|
ENV_WORKSPACE_MAP = {
|
|
212
211
|
GTM_PLUSVIBE_WORKSPACE_ID: "plusvibe"
|
|
@@ -8739,107 +8738,10 @@ var init_client25 = __esm({
|
|
|
8739
8738
|
});
|
|
8740
8739
|
|
|
8741
8740
|
// ../integrations/dist/leadmagic.js
|
|
8742
|
-
var LeadMagicClient;
|
|
8743
8741
|
var init_leadmagic2 = __esm({
|
|
8744
8742
|
"../integrations/dist/leadmagic.js"() {
|
|
8745
8743
|
"use strict";
|
|
8746
8744
|
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
8745
|
}
|
|
8844
8746
|
});
|
|
8845
8747
|
|
|
@@ -9737,7 +9639,8 @@ var init_prospeo2 = __esm({
|
|
|
9737
9639
|
return {
|
|
9738
9640
|
email: response.person.email.email,
|
|
9739
9641
|
quality: response.person.email.status === "verified" ? "95%" : "70%",
|
|
9740
|
-
provider: this.name
|
|
9642
|
+
provider: this.name,
|
|
9643
|
+
credits_consumed: 1
|
|
9741
9644
|
};
|
|
9742
9645
|
} catch (error) {
|
|
9743
9646
|
logError("prospeo:enrichEmail", error instanceof Error ? error : new Error(String(error)));
|
|
@@ -10656,193 +10559,6 @@ var init_exa = __esm({
|
|
|
10656
10559
|
}
|
|
10657
10560
|
});
|
|
10658
10561
|
|
|
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
|
-
|
|
10846
10562
|
// src/providers/adapters/index.ts
|
|
10847
10563
|
var init_adapters = __esm({
|
|
10848
10564
|
"src/providers/adapters/index.ts"() {
|
|
@@ -10860,7 +10576,6 @@ var init_adapters = __esm({
|
|
|
10860
10576
|
init_storeleads();
|
|
10861
10577
|
init_harvest();
|
|
10862
10578
|
init_exa();
|
|
10863
|
-
init_leadmagic3();
|
|
10864
10579
|
}
|
|
10865
10580
|
});
|
|
10866
10581
|
|
|
@@ -10893,8 +10608,6 @@ function createAdapter(name, config) {
|
|
|
10893
10608
|
return config.api_key ? new HarvestApiAdapter(config.api_key) : null;
|
|
10894
10609
|
case "exa":
|
|
10895
10610
|
return config.api_key ? new ExaAdapter(config.api_key) : null;
|
|
10896
|
-
case "leadmagic":
|
|
10897
|
-
return config.api_key ? new LeadMagicAdapter2(config.api_key) : null;
|
|
10898
10611
|
default:
|
|
10899
10612
|
return null;
|
|
10900
10613
|
}
|
|
@@ -11190,6 +10903,10 @@ var init_prospect = __esm({
|
|
|
11190
10903
|
type: "string",
|
|
11191
10904
|
enum: ["summary", "full"],
|
|
11192
10905
|
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
10906
|
+
},
|
|
10907
|
+
review: {
|
|
10908
|
+
type: "boolean",
|
|
10909
|
+
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."
|
|
11193
10910
|
}
|
|
11194
10911
|
},
|
|
11195
10912
|
required: []
|
|
@@ -11306,6 +11023,10 @@ var init_prospect = __esm({
|
|
|
11306
11023
|
type: "string",
|
|
11307
11024
|
enum: ["summary", "full"],
|
|
11308
11025
|
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
11026
|
+
},
|
|
11027
|
+
review: {
|
|
11028
|
+
type: "boolean",
|
|
11029
|
+
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."
|
|
11309
11030
|
}
|
|
11310
11031
|
},
|
|
11311
11032
|
required: []
|
|
@@ -11363,9 +11084,17 @@ var init_enrich = __esm({
|
|
|
11363
11084
|
type: "string",
|
|
11364
11085
|
enum: ["summary", "full"],
|
|
11365
11086
|
description: 'Response detail level. "summary" (default) returns email/phone/providers per contact. "full" includes waterfall attempt details (provider, duration, errors) per contact.'
|
|
11087
|
+
},
|
|
11088
|
+
table_id: {
|
|
11089
|
+
type: "string",
|
|
11090
|
+
description: "Read contacts from a viewer table instead of passing inline. Fetches selected rows, enriches them, and writes results back to the table."
|
|
11091
|
+
},
|
|
11092
|
+
review: {
|
|
11093
|
+
type: "boolean",
|
|
11094
|
+
description: "Create a viewer table with enrichment results."
|
|
11366
11095
|
}
|
|
11367
11096
|
},
|
|
11368
|
-
required: [
|
|
11097
|
+
required: []
|
|
11369
11098
|
}
|
|
11370
11099
|
},
|
|
11371
11100
|
{
|
|
@@ -11608,6 +11337,10 @@ var init_campaign = __esm({
|
|
|
11608
11337
|
type: "string",
|
|
11609
11338
|
enum: ["summary", "full"],
|
|
11610
11339
|
description: 'Response detail level. "summary" (default) returns key stats only. "full" returns all campaign data.'
|
|
11340
|
+
},
|
|
11341
|
+
table_id: {
|
|
11342
|
+
type: "string",
|
|
11343
|
+
description: "Read leads from a viewer table instead of passing inline. Fetches selected rows and maps them to campaign leads."
|
|
11611
11344
|
}
|
|
11612
11345
|
},
|
|
11613
11346
|
required: ["action"]
|
|
@@ -11666,6 +11399,10 @@ var init_crm = __esm({
|
|
|
11666
11399
|
provider: {
|
|
11667
11400
|
type: "string",
|
|
11668
11401
|
description: "CRM provider override (uses first available if omitted)"
|
|
11402
|
+
},
|
|
11403
|
+
table_id: {
|
|
11404
|
+
type: "string",
|
|
11405
|
+
description: "Read contacts from a viewer table for bulk create/update. Fetches selected rows and maps them to CRM person records."
|
|
11669
11406
|
}
|
|
11670
11407
|
},
|
|
11671
11408
|
required: ["action"]
|
|
@@ -11804,11 +11541,18 @@ var init_review = __esm({
|
|
|
11804
11541
|
items: { type: "object" },
|
|
11805
11542
|
description: "Array of row objects (for create)"
|
|
11806
11543
|
},
|
|
11807
|
-
table_id: {
|
|
11544
|
+
table_id: {
|
|
11545
|
+
type: "string",
|
|
11546
|
+
description: "Table ID (for update, get_selected, or optional client-provided ID on create)"
|
|
11547
|
+
},
|
|
11808
11548
|
rows: {
|
|
11809
11549
|
type: "array",
|
|
11810
11550
|
items: { type: "object" },
|
|
11811
11551
|
description: "Updated row objects with _row_id (for update)"
|
|
11552
|
+
},
|
|
11553
|
+
ttl_seconds: {
|
|
11554
|
+
type: "number",
|
|
11555
|
+
description: "Optional TTL override in seconds (default: 24h). For create only."
|
|
11812
11556
|
}
|
|
11813
11557
|
},
|
|
11814
11558
|
required: ["action"]
|
|
@@ -11839,6 +11583,8 @@ Tip: DiscoLike lookalike mode is extremely powerful. If you have 2-3 example com
|
|
|
11839
11583
|
|
|
11840
11584
|
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.
|
|
11841
11585
|
|
|
11586
|
+
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.
|
|
11587
|
+
|
|
11842
11588
|
Exa (provider: "exa") \u2014 semantic/neural search:
|
|
11843
11589
|
- Meaning-based ICP matching: "B2B SaaS companies selling to healthcare, not healthtech vendors" actually works
|
|
11844
11590
|
- Best for: nuanced ICP descriptions where keyword matching fails
|
|
@@ -11865,7 +11611,9 @@ After finding people, use:
|
|
|
11865
11611
|
- gtm_extract to check LinkedIn activity and engagement signals
|
|
11866
11612
|
|
|
11867
11613
|
Known limitations:
|
|
11868
|
-
- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other
|
|
11614
|
+
- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other.
|
|
11615
|
+
|
|
11616
|
+
Pass review: true to push results to a visual review table at view.gtmnow.com so the user can select rows before proceeding.`,
|
|
11869
11617
|
gtm_enrich: `Email enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
|
|
11870
11618
|
|
|
11871
11619
|
IMPORTANT: Defaults to email only. Phone enrichment costs extra per lookup \u2014 only include when the user explicitly asks for phone numbers.
|
|
@@ -11878,7 +11626,9 @@ Providers (in configured waterfall order):
|
|
|
11878
11626
|
|
|
11879
11627
|
Input needs: first_name + last_name minimum. Adding company_domain dramatically improves hit rates. LinkedIn URL is the gold standard input.
|
|
11880
11628
|
|
|
11881
|
-
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
|
|
11629
|
+
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.
|
|
11630
|
+
|
|
11631
|
+
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.`,
|
|
11882
11632
|
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.",
|
|
11883
11633
|
gtm_extract: `Extract structured data from any URL or platform. LinkedIn profiles, posts, engagement, search results, company posts, and web pages.
|
|
11884
11634
|
|
|
@@ -11902,8 +11652,8 @@ Pairs well with:
|
|
|
11902
11652
|
Note: HarvestAPI charges per request. For engagement mining, each post lookup counts as a request. Use limit to control costs.`,
|
|
11903
11653
|
gtm_fetch: "Make an HTTP call to any API endpoint. Use this to pull data from services that don't have a dedicated provider.",
|
|
11904
11654
|
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).",
|
|
11905
|
-
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.",
|
|
11906
|
-
gtm_crm: "Read and write CRM data. Find, create, or update people and companies. Create deals. Supports Attio and other CRM providers.",
|
|
11655
|
+
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).",
|
|
11656
|
+
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).",
|
|
11907
11657
|
gtm_status: "Check provider health, credit balances, and active configuration. Shows which providers are configured, their capabilities, remaining credits, and the current client context.",
|
|
11908
11658
|
gtm_configure: "Manage GTM configuration. Switch between clients, view active config, set waterfall order, list available providers.",
|
|
11909
11659
|
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.",
|
|
@@ -11916,7 +11666,7 @@ Note: HarvestAPI charges per request. For engagement mining, each post lookup co
|
|
|
11916
11666
|
});
|
|
11917
11667
|
|
|
11918
11668
|
// src/tools/index.ts
|
|
11919
|
-
var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, toolsByName, toolCategories, workflowRecipes;
|
|
11669
|
+
var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, MCP_HIDDEN_TOOLS, mcpTools, toolsByName, toolCategories, workflowRecipes;
|
|
11920
11670
|
var init_tools = __esm({
|
|
11921
11671
|
"src/tools/index.ts"() {
|
|
11922
11672
|
"use strict";
|
|
@@ -12012,6 +11762,8 @@ var init_tools = __esm({
|
|
|
12012
11762
|
};
|
|
12013
11763
|
metaTools = [guideTool, toolHelpTool, executeTool];
|
|
12014
11764
|
tools = [...capabilityTools, ...metaTools];
|
|
11765
|
+
MCP_HIDDEN_TOOLS = /* @__PURE__ */ new Set(["gtm_execute", "gtm_guide", "gtm_tool_help"]);
|
|
11766
|
+
mcpTools = tools.filter((t) => !MCP_HIDDEN_TOOLS.has(t.name));
|
|
12015
11767
|
toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
12016
11768
|
toolCategories = {
|
|
12017
11769
|
prospect: prospectTools.map((t) => t.name),
|
|
@@ -12271,6 +12023,7 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
12271
12023
|
let phone;
|
|
12272
12024
|
let phoneProvider;
|
|
12273
12025
|
let charged = false;
|
|
12026
|
+
let creditsConsumed;
|
|
12274
12027
|
const cacheKey = getCacheKey(contact);
|
|
12275
12028
|
const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
|
|
12276
12029
|
for (const field of config.find) {
|
|
@@ -12301,6 +12054,9 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
12301
12054
|
email = fieldResult.value;
|
|
12302
12055
|
emailProvider = fieldResult.provider;
|
|
12303
12056
|
charged = true;
|
|
12057
|
+
if (fieldResult.credits_consumed) {
|
|
12058
|
+
creditsConsumed = (creditsConsumed ?? 0) + fieldResult.credits_consumed;
|
|
12059
|
+
}
|
|
12304
12060
|
} else if (field === "phone" && fieldResult.value) {
|
|
12305
12061
|
phone = fieldResult.value;
|
|
12306
12062
|
phoneProvider = fieldResult.provider;
|
|
@@ -12323,7 +12079,8 @@ async function enrichContact(contact, config, registry, exhausted) {
|
|
|
12323
12079
|
email_provider: emailProvider,
|
|
12324
12080
|
phone_provider: phoneProvider,
|
|
12325
12081
|
attempts,
|
|
12326
|
-
charged
|
|
12082
|
+
charged,
|
|
12083
|
+
...creditsConsumed !== void 0 && { credits_consumed: creditsConsumed }
|
|
12327
12084
|
};
|
|
12328
12085
|
}
|
|
12329
12086
|
async function enrichField(contact, field, config, registry, exhausted) {
|
|
@@ -12348,6 +12105,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12348
12105
|
try {
|
|
12349
12106
|
let value;
|
|
12350
12107
|
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12108
|
+
let creditsConsumed;
|
|
12351
12109
|
if (field === "email" && "enrichEmail" in provider) {
|
|
12352
12110
|
const result = await withTimeout(
|
|
12353
12111
|
provider.enrichEmail(contact),
|
|
@@ -12355,6 +12113,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12355
12113
|
providerName
|
|
12356
12114
|
);
|
|
12357
12115
|
value = result?.email;
|
|
12116
|
+
creditsConsumed = result?.credits_consumed;
|
|
12358
12117
|
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12359
12118
|
const result = await withTimeout(
|
|
12360
12119
|
provider.enrichPhone(contact),
|
|
@@ -12372,7 +12131,7 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
12372
12131
|
duration_ms: Date.now() - start
|
|
12373
12132
|
});
|
|
12374
12133
|
if (found && config.stopOnFirst) {
|
|
12375
|
-
return { value, provider: providerName, attempts };
|
|
12134
|
+
return { value, provider: providerName, credits_consumed: creditsConsumed, attempts };
|
|
12376
12135
|
}
|
|
12377
12136
|
break;
|
|
12378
12137
|
} catch (error) {
|
|
@@ -12464,6 +12223,123 @@ var init_waterfall = __esm({
|
|
|
12464
12223
|
}
|
|
12465
12224
|
});
|
|
12466
12225
|
|
|
12226
|
+
// src/utils/table-bridge.ts
|
|
12227
|
+
function asString(value) {
|
|
12228
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
12229
|
+
}
|
|
12230
|
+
function splitName(full) {
|
|
12231
|
+
const idx = full.indexOf(" ");
|
|
12232
|
+
if (idx === -1) return { first_name: full, last_name: "" };
|
|
12233
|
+
return { first_name: full.slice(0, idx), last_name: full.slice(idx + 1) };
|
|
12234
|
+
}
|
|
12235
|
+
async function createReviewTable(title, data, options) {
|
|
12236
|
+
try {
|
|
12237
|
+
const rows = data.map((row, i) => ({
|
|
12238
|
+
_row_id: `r${i + 1}`,
|
|
12239
|
+
...row
|
|
12240
|
+
}));
|
|
12241
|
+
const body = { title, rows };
|
|
12242
|
+
if (options?.table_id !== void 0) body.table_id = options.table_id;
|
|
12243
|
+
if (options?.ttl_seconds !== void 0) body.ttl_seconds = options.ttl_seconds;
|
|
12244
|
+
const response = await fetch(`${VIEWER_URL}/api/tables`, {
|
|
12245
|
+
method: "POST",
|
|
12246
|
+
headers: { "Content-Type": "application/json" },
|
|
12247
|
+
body: JSON.stringify(body)
|
|
12248
|
+
});
|
|
12249
|
+
if (!response.ok) {
|
|
12250
|
+
logError("table-bridge.createReviewTable", new Error(`Viewer returned ${response.status}`), {
|
|
12251
|
+
title,
|
|
12252
|
+
status: response.status
|
|
12253
|
+
});
|
|
12254
|
+
return null;
|
|
12255
|
+
}
|
|
12256
|
+
return await response.json();
|
|
12257
|
+
} catch (error) {
|
|
12258
|
+
logError("table-bridge.createReviewTable", error, { title });
|
|
12259
|
+
return null;
|
|
12260
|
+
}
|
|
12261
|
+
}
|
|
12262
|
+
async function fetchTableRows(tableId) {
|
|
12263
|
+
try {
|
|
12264
|
+
const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}/selected`);
|
|
12265
|
+
if (!response.ok) {
|
|
12266
|
+
logError("table-bridge.fetchTableRows", new Error(`Viewer returned ${response.status}`), {
|
|
12267
|
+
tableId,
|
|
12268
|
+
status: response.status
|
|
12269
|
+
});
|
|
12270
|
+
return [];
|
|
12271
|
+
}
|
|
12272
|
+
const json = await response.json();
|
|
12273
|
+
return json.rows ?? [];
|
|
12274
|
+
} catch (error) {
|
|
12275
|
+
logError("table-bridge.fetchTableRows", error, { tableId });
|
|
12276
|
+
return [];
|
|
12277
|
+
}
|
|
12278
|
+
}
|
|
12279
|
+
async function patchTableRows(tableId, rows) {
|
|
12280
|
+
try {
|
|
12281
|
+
const response = await fetch(`${VIEWER_URL}/api/tables/${tableId}`, {
|
|
12282
|
+
method: "PATCH",
|
|
12283
|
+
headers: { "Content-Type": "application/json" },
|
|
12284
|
+
body: JSON.stringify({ rows })
|
|
12285
|
+
});
|
|
12286
|
+
if (!response.ok) {
|
|
12287
|
+
logError("table-bridge.patchTableRows", new Error(`Viewer returned ${response.status}`), {
|
|
12288
|
+
tableId,
|
|
12289
|
+
status: response.status
|
|
12290
|
+
});
|
|
12291
|
+
return false;
|
|
12292
|
+
}
|
|
12293
|
+
return true;
|
|
12294
|
+
} catch (error) {
|
|
12295
|
+
logError("table-bridge.patchTableRows", error, { tableId });
|
|
12296
|
+
return false;
|
|
12297
|
+
}
|
|
12298
|
+
}
|
|
12299
|
+
function mapRowsToContacts(rows) {
|
|
12300
|
+
return rows.map((row) => {
|
|
12301
|
+
let firstName = asString(row.first_name);
|
|
12302
|
+
let lastName = asString(row.last_name);
|
|
12303
|
+
if (firstName === void 0 || lastName === void 0) {
|
|
12304
|
+
const fullName = asString(row.name) ?? asString(row.full_name);
|
|
12305
|
+
if (fullName) {
|
|
12306
|
+
const parts = splitName(fullName);
|
|
12307
|
+
firstName ??= parts.first_name;
|
|
12308
|
+
lastName ??= parts.last_name;
|
|
12309
|
+
}
|
|
12310
|
+
}
|
|
12311
|
+
return {
|
|
12312
|
+
first_name: firstName ?? "",
|
|
12313
|
+
last_name: lastName ?? "",
|
|
12314
|
+
company: asString(row.company) ?? asString(row.company_name),
|
|
12315
|
+
company_domain: asString(row.company_domain) ?? asString(row.domain),
|
|
12316
|
+
linkedin_url: asString(row.linkedin_url) ?? asString(row.linkedin)
|
|
12317
|
+
};
|
|
12318
|
+
});
|
|
12319
|
+
}
|
|
12320
|
+
function mapRowsToCampaignLeads(rows) {
|
|
12321
|
+
const leads = [];
|
|
12322
|
+
for (const row of rows) {
|
|
12323
|
+
const email = asString(row.email);
|
|
12324
|
+
if (!email || email.includes("*")) continue;
|
|
12325
|
+
leads.push({
|
|
12326
|
+
email,
|
|
12327
|
+
first_name: asString(row.first_name),
|
|
12328
|
+
last_name: asString(row.last_name),
|
|
12329
|
+
company: asString(row.company) ?? asString(row.company_name)
|
|
12330
|
+
});
|
|
12331
|
+
}
|
|
12332
|
+
return leads;
|
|
12333
|
+
}
|
|
12334
|
+
var VIEWER_URL;
|
|
12335
|
+
var init_table_bridge = __esm({
|
|
12336
|
+
"src/utils/table-bridge.ts"() {
|
|
12337
|
+
"use strict";
|
|
12338
|
+
init_dist();
|
|
12339
|
+
VIEWER_URL = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
|
|
12340
|
+
}
|
|
12341
|
+
});
|
|
12342
|
+
|
|
12467
12343
|
// src/providers/types.ts
|
|
12468
12344
|
function applySlimFields(obj, fields) {
|
|
12469
12345
|
const result = {};
|
|
@@ -12696,6 +12572,27 @@ async function findCompanies(args, ctx) {
|
|
|
12696
12572
|
});
|
|
12697
12573
|
const offset = args.offset ?? 0;
|
|
12698
12574
|
const has_more = results.length >= effectiveLimit;
|
|
12575
|
+
const hints = [];
|
|
12576
|
+
if (results.length >= effectiveLimit) {
|
|
12577
|
+
hints.push(
|
|
12578
|
+
`Returned ${results.length} results (limit reached). Increase limit or use offset for more.`
|
|
12579
|
+
);
|
|
12580
|
+
}
|
|
12581
|
+
const shouldReview = args.review ?? false;
|
|
12582
|
+
let tableId;
|
|
12583
|
+
let tableUrl;
|
|
12584
|
+
if (shouldReview) {
|
|
12585
|
+
const tableResult = await createReviewTable(
|
|
12586
|
+
`Find Companies: ${query.icp_text ?? query.domain ?? "results"}`,
|
|
12587
|
+
slimmedResults
|
|
12588
|
+
);
|
|
12589
|
+
if (tableResult) {
|
|
12590
|
+
tableId = tableResult.table_id;
|
|
12591
|
+
tableUrl = tableResult.url;
|
|
12592
|
+
} else {
|
|
12593
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
12594
|
+
}
|
|
12595
|
+
}
|
|
12699
12596
|
const applied_filters = {};
|
|
12700
12597
|
if (query.icp_text) applied_filters.icp_text = query.icp_text;
|
|
12701
12598
|
if (query.domain) applied_filters.domain = query.domain;
|
|
@@ -12750,9 +12647,11 @@ async function findCompanies(args, ctx) {
|
|
|
12750
12647
|
count: results.length,
|
|
12751
12648
|
has_more,
|
|
12752
12649
|
offset,
|
|
12650
|
+
...tableId && { table_id: tableId, url: tableUrl },
|
|
12753
12651
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12754
12652
|
companies: slimmedResults,
|
|
12755
|
-
...warnings?.length ? { warnings } : {}
|
|
12653
|
+
...warnings?.length ? { warnings } : {},
|
|
12654
|
+
...hints.length > 0 && { hints }
|
|
12756
12655
|
};
|
|
12757
12656
|
}
|
|
12758
12657
|
async function findPeople(args, ctx) {
|
|
@@ -12793,10 +12692,10 @@ async function findPeople(args, ctx) {
|
|
|
12793
12692
|
const dedupedResults = deduplicatePeople(results);
|
|
12794
12693
|
const shouldEnrich = args.enrich ?? false;
|
|
12795
12694
|
let enrichedCount = 0;
|
|
12695
|
+
let needsEnrichmentCount = 0;
|
|
12796
12696
|
if (shouldEnrich) {
|
|
12797
|
-
const needsEnrichment = dedupedResults.filter(
|
|
12798
|
-
|
|
12799
|
-
);
|
|
12697
|
+
const needsEnrichment = dedupedResults.filter((r) => !r.email || r.email.includes("*"));
|
|
12698
|
+
needsEnrichmentCount = needsEnrichment.length;
|
|
12800
12699
|
if (needsEnrichment.length > 0) {
|
|
12801
12700
|
try {
|
|
12802
12701
|
const contacts = needsEnrichment.map((r) => ({
|
|
@@ -12824,7 +12723,10 @@ async function findPeople(args, ctx) {
|
|
|
12824
12723
|
}
|
|
12825
12724
|
}
|
|
12826
12725
|
} catch (error) {
|
|
12827
|
-
logError(
|
|
12726
|
+
logError(
|
|
12727
|
+
"prospect:inlineEnrich",
|
|
12728
|
+
error instanceof Error ? error : new Error(String(error))
|
|
12729
|
+
);
|
|
12828
12730
|
}
|
|
12829
12731
|
}
|
|
12830
12732
|
}
|
|
@@ -12853,6 +12755,37 @@ async function findPeople(args, ctx) {
|
|
|
12853
12755
|
if (query.min_employees != null) applied_filters.min_employees = query.min_employees;
|
|
12854
12756
|
if (query.max_employees != null) applied_filters.max_employees = query.max_employees;
|
|
12855
12757
|
if (query.revenue_range) applied_filters.revenue_range = query.revenue_range;
|
|
12758
|
+
const hints = [];
|
|
12759
|
+
const maskedCount = dedupedResults.filter((r) => r.email?.includes("*")).length;
|
|
12760
|
+
if (maskedCount > 0) {
|
|
12761
|
+
hints.push(
|
|
12762
|
+
`${maskedCount} emails are masked by the provider. Pass enrich: true to resolve them.`
|
|
12763
|
+
);
|
|
12764
|
+
}
|
|
12765
|
+
if (dedupedResults.length >= effectiveLimit) {
|
|
12766
|
+
hints.push(
|
|
12767
|
+
`Returned ${dedupedResults.length} results (limit reached). Increase limit or use offset for more.`
|
|
12768
|
+
);
|
|
12769
|
+
}
|
|
12770
|
+
if (shouldEnrich && enrichedCount < needsEnrichmentCount) {
|
|
12771
|
+
const failed = needsEnrichmentCount - enrichedCount;
|
|
12772
|
+
hints.push(`${failed} contacts could not be enriched \u2014 missing LinkedIn URL or domain.`);
|
|
12773
|
+
}
|
|
12774
|
+
const shouldReview = args.review ?? false;
|
|
12775
|
+
let tableId;
|
|
12776
|
+
let tableUrl;
|
|
12777
|
+
if (shouldReview) {
|
|
12778
|
+
const tableResult = await createReviewTable(
|
|
12779
|
+
`Find People: ${query.job_title ?? query.company_domain ?? "results"}`,
|
|
12780
|
+
slimmedResults
|
|
12781
|
+
);
|
|
12782
|
+
if (tableResult) {
|
|
12783
|
+
tableId = tableResult.table_id;
|
|
12784
|
+
tableUrl = tableResult.url;
|
|
12785
|
+
} else {
|
|
12786
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
12787
|
+
}
|
|
12788
|
+
}
|
|
12856
12789
|
const providerUsed = provider.name;
|
|
12857
12790
|
const output = args.output;
|
|
12858
12791
|
if (output === "csv") {
|
|
@@ -12893,10 +12826,12 @@ async function findPeople(args, ctx) {
|
|
|
12893
12826
|
count: dedupedResults.length,
|
|
12894
12827
|
has_more,
|
|
12895
12828
|
offset,
|
|
12829
|
+
...tableId && { table_id: tableId, url: tableUrl },
|
|
12896
12830
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12897
12831
|
...shouldEnrich && { enriched: enrichedCount },
|
|
12898
12832
|
people: slimmedResults,
|
|
12899
|
-
...warnings?.length ? { warnings } : {}
|
|
12833
|
+
...warnings?.length ? { warnings } : {},
|
|
12834
|
+
...hints.length > 0 && { hints }
|
|
12900
12835
|
};
|
|
12901
12836
|
}
|
|
12902
12837
|
var init_prospect2 = __esm({
|
|
@@ -12905,6 +12840,7 @@ var init_prospect2 = __esm({
|
|
|
12905
12840
|
init_dist();
|
|
12906
12841
|
init_errors();
|
|
12907
12842
|
init_waterfall();
|
|
12843
|
+
init_table_bridge();
|
|
12908
12844
|
init_types3();
|
|
12909
12845
|
init_manifest_validation();
|
|
12910
12846
|
init_result_writer();
|
|
@@ -12937,10 +12873,29 @@ async function handleEnrich(name, args, ctx) {
|
|
|
12937
12873
|
return enrichContacts(args, ctx);
|
|
12938
12874
|
}
|
|
12939
12875
|
async function enrichContacts(args, ctx) {
|
|
12940
|
-
|
|
12876
|
+
let contacts = args.contacts;
|
|
12941
12877
|
const find = args.find ?? ["email"];
|
|
12942
12878
|
const verify = args.verify ?? true;
|
|
12943
12879
|
const dryRun = args.dry_run ?? false;
|
|
12880
|
+
const detail = args.detail ?? "summary";
|
|
12881
|
+
const tableId = args.table_id;
|
|
12882
|
+
let rowIds = [];
|
|
12883
|
+
if (tableId) {
|
|
12884
|
+
const tableRows = await fetchTableRows(tableId);
|
|
12885
|
+
if (tableRows.length === 0) {
|
|
12886
|
+
return {
|
|
12887
|
+
total: 0,
|
|
12888
|
+
found: 0,
|
|
12889
|
+
missed: 0,
|
|
12890
|
+
results: [],
|
|
12891
|
+
hints: [
|
|
12892
|
+
"No rows selected in table. User may not have reviewed yet, or may have deselected all."
|
|
12893
|
+
]
|
|
12894
|
+
};
|
|
12895
|
+
}
|
|
12896
|
+
rowIds = tableRows.map((r) => r._row_id);
|
|
12897
|
+
contacts = mapRowsToContacts(tableRows);
|
|
12898
|
+
}
|
|
12944
12899
|
const WATERFALL_FIELDS = ["email", "phone"];
|
|
12945
12900
|
const richFields = find.filter((f) => !WATERFALL_FIELDS.includes(f));
|
|
12946
12901
|
if (richFields.length > 0) {
|
|
@@ -12973,11 +12928,65 @@ async function enrichContacts(args, ctx) {
|
|
|
12973
12928
|
}
|
|
12974
12929
|
const results = await runWaterfall(contacts, config, ctx.registry);
|
|
12975
12930
|
const found = results.filter((r) => r.email || r.phone).length;
|
|
12976
|
-
|
|
12931
|
+
if (tableId) {
|
|
12932
|
+
const patchPayload = results.map((r, i) => ({
|
|
12933
|
+
_row_id: rowIds[i],
|
|
12934
|
+
...r.email && { email: r.email },
|
|
12935
|
+
...r.email_provider && { email_provider: r.email_provider },
|
|
12936
|
+
...r.email_status && { email_status: r.email_status }
|
|
12937
|
+
}));
|
|
12938
|
+
await patchTableRows(tableId, patchPayload);
|
|
12939
|
+
}
|
|
12940
|
+
const hints = [];
|
|
12941
|
+
const exhaustedAttempts = results.flatMap(
|
|
12942
|
+
(r) => r.attempts.filter((a) => a.error?.includes("exhausted"))
|
|
12943
|
+
);
|
|
12944
|
+
if (exhaustedAttempts.length > 0) {
|
|
12945
|
+
const provider = exhaustedAttempts[0].provider;
|
|
12946
|
+
hints.push(
|
|
12947
|
+
`${provider} credits exhausted mid-batch. Remaining contacts fell through to next provider.`
|
|
12948
|
+
);
|
|
12949
|
+
}
|
|
12950
|
+
const noLinkedin = contacts.filter((c) => !c.linkedin_url).length;
|
|
12951
|
+
if (noLinkedin > 0) {
|
|
12952
|
+
hints.push(`${noLinkedin} contacts had no LinkedIn URL \u2014 some providers skipped them.`);
|
|
12953
|
+
}
|
|
12954
|
+
const timedOut = results.filter(
|
|
12955
|
+
(r) => r.attempts.some((a) => a.error?.includes("Batch timeout"))
|
|
12956
|
+
).length;
|
|
12957
|
+
if (timedOut > 0) {
|
|
12958
|
+
hints.push(`Batch timed out with ${timedOut} contacts remaining. Re-run with those contacts.`);
|
|
12959
|
+
}
|
|
12960
|
+
const costMap = /* @__PURE__ */ new Map();
|
|
12961
|
+
for (const r of results) {
|
|
12962
|
+
if (r.credits_consumed && r.email_provider) {
|
|
12963
|
+
costMap.set(r.email_provider, (costMap.get(r.email_provider) ?? 0) + r.credits_consumed);
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
const cost = costMap.size > 0 ? Array.from(costMap.entries()).map(([provider, credits]) => ({ provider, credits })) : void 0;
|
|
12967
|
+
const shouldReview = args.review ?? false;
|
|
12968
|
+
let reviewTableId;
|
|
12969
|
+
let reviewUrl;
|
|
12970
|
+
if (shouldReview) {
|
|
12971
|
+
const tableResult = await createReviewTable(
|
|
12972
|
+
"Enrichment Results",
|
|
12973
|
+
detail === "full" ? results : slimEnrichResults(results)
|
|
12974
|
+
);
|
|
12975
|
+
if (tableResult) {
|
|
12976
|
+
reviewTableId = tableResult.table_id;
|
|
12977
|
+
reviewUrl = tableResult.url;
|
|
12978
|
+
} else {
|
|
12979
|
+
hints.push("Review table creation failed \u2014 results returned inline only.");
|
|
12980
|
+
}
|
|
12981
|
+
}
|
|
12977
12982
|
return {
|
|
12978
12983
|
total: results.length,
|
|
12979
12984
|
found,
|
|
12980
12985
|
missed: results.length - found,
|
|
12986
|
+
...tableId && { table_id: tableId },
|
|
12987
|
+
...reviewTableId && { table_id: reviewTableId, url: reviewUrl },
|
|
12988
|
+
...hints.length > 0 && { hints },
|
|
12989
|
+
...cost && { cost },
|
|
12981
12990
|
results: detail === "full" ? results : slimEnrichResults(results)
|
|
12982
12991
|
};
|
|
12983
12992
|
}
|
|
@@ -12999,6 +13008,7 @@ var init_enrich2 = __esm({
|
|
|
12999
13008
|
"use strict";
|
|
13000
13009
|
init_waterfall();
|
|
13001
13010
|
init_errors();
|
|
13011
|
+
init_table_bridge();
|
|
13002
13012
|
}
|
|
13003
13013
|
});
|
|
13004
13014
|
|
|
@@ -13434,11 +13444,38 @@ async function handleCampaign(args, ctx) {
|
|
|
13434
13444
|
}
|
|
13435
13445
|
case "add_leads": {
|
|
13436
13446
|
const campaignId = args.campaign_id;
|
|
13437
|
-
const
|
|
13447
|
+
const tableId = args.table_id;
|
|
13438
13448
|
if (!campaignId)
|
|
13439
13449
|
throw createGtmError("QUERY_INVALID", "campaign_id is required for add_leads action");
|
|
13450
|
+
const hints = [];
|
|
13451
|
+
let rawLeads = args.leads;
|
|
13452
|
+
if (tableId) {
|
|
13453
|
+
const tableRows = await fetchTableRows(tableId);
|
|
13454
|
+
if (tableRows.length === 0) {
|
|
13455
|
+
return {
|
|
13456
|
+
provider: channel.providerName,
|
|
13457
|
+
added: 0,
|
|
13458
|
+
hints: [
|
|
13459
|
+
"No rows selected in table. User may not have reviewed yet, or may have deselected all."
|
|
13460
|
+
]
|
|
13461
|
+
};
|
|
13462
|
+
}
|
|
13463
|
+
if (channel.type === "linkedin") {
|
|
13464
|
+
rawLeads = tableRows;
|
|
13465
|
+
} else {
|
|
13466
|
+
const mapped = mapRowsToCampaignLeads(tableRows);
|
|
13467
|
+
const skipped = tableRows.length - mapped.length;
|
|
13468
|
+
if (skipped > 0) {
|
|
13469
|
+
hints.push(`${skipped} rows skipped \u2014 missing email (required for campaign).`);
|
|
13470
|
+
}
|
|
13471
|
+
rawLeads = mapped;
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13440
13474
|
if (!rawLeads?.length)
|
|
13441
|
-
throw createGtmError(
|
|
13475
|
+
throw createGtmError(
|
|
13476
|
+
"QUERY_INVALID",
|
|
13477
|
+
"leads array is required for add_leads action (or provide table_id)"
|
|
13478
|
+
);
|
|
13442
13479
|
if (channel.type === "linkedin") {
|
|
13443
13480
|
const leads2 = rawLeads.map((l) => ({
|
|
13444
13481
|
linkedin_url: l.linkedin_url,
|
|
@@ -13446,7 +13483,11 @@ async function handleCampaign(args, ctx) {
|
|
|
13446
13483
|
last_name: l.last_name,
|
|
13447
13484
|
company: l.company
|
|
13448
13485
|
}));
|
|
13449
|
-
|
|
13486
|
+
const result = await channel.manager.addLeadsToCampaign(campaignId, leads2);
|
|
13487
|
+
return {
|
|
13488
|
+
...result,
|
|
13489
|
+
...hints.length > 0 && { hints }
|
|
13490
|
+
};
|
|
13450
13491
|
}
|
|
13451
13492
|
const leads = rawLeads.map((l) => ({
|
|
13452
13493
|
email: l.email,
|
|
@@ -13456,7 +13497,11 @@ async function handleCampaign(args, ctx) {
|
|
|
13456
13497
|
custom_fields: l.custom_fields
|
|
13457
13498
|
}));
|
|
13458
13499
|
try {
|
|
13459
|
-
|
|
13500
|
+
const result = await channel.manager.addLeadsToCampaign(campaignId, leads);
|
|
13501
|
+
return {
|
|
13502
|
+
...result,
|
|
13503
|
+
...hints.length > 0 && { hints }
|
|
13504
|
+
};
|
|
13460
13505
|
} catch (error) {
|
|
13461
13506
|
if (error instanceof ApiClientError && (error.message.includes("Validation error") || error.statusCode === 400 || error.statusCode === 422)) {
|
|
13462
13507
|
throw createGtmError(
|
|
@@ -13494,6 +13539,7 @@ var init_campaign2 = __esm({
|
|
|
13494
13539
|
"use strict";
|
|
13495
13540
|
init_dist2();
|
|
13496
13541
|
init_errors();
|
|
13542
|
+
init_table_bridge();
|
|
13497
13543
|
}
|
|
13498
13544
|
});
|
|
13499
13545
|
|
|
@@ -13521,6 +13567,30 @@ async function handleCrm(args, ctx) {
|
|
|
13521
13567
|
return manager.findPerson(query);
|
|
13522
13568
|
}
|
|
13523
13569
|
case "create": {
|
|
13570
|
+
const tableId = args.table_id;
|
|
13571
|
+
if (tableId) {
|
|
13572
|
+
const rows = await fetchTableRows(tableId);
|
|
13573
|
+
if (rows.length === 0) {
|
|
13574
|
+
return {
|
|
13575
|
+
created: 0,
|
|
13576
|
+
hints: [
|
|
13577
|
+
"No selected rows found in the review table. Select rows in the viewer and retry."
|
|
13578
|
+
]
|
|
13579
|
+
};
|
|
13580
|
+
}
|
|
13581
|
+
const contacts = mapRowsToContacts(rows);
|
|
13582
|
+
const results = await Promise.allSettled(
|
|
13583
|
+
contacts.map((contact) => manager.createPerson(contact))
|
|
13584
|
+
);
|
|
13585
|
+
const created = results.filter((r) => r.status === "fulfilled").length;
|
|
13586
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
13587
|
+
return {
|
|
13588
|
+
created,
|
|
13589
|
+
failed,
|
|
13590
|
+
total: contacts.length,
|
|
13591
|
+
hints: failed > 0 ? [`${failed} contact(s) failed to create \u2014 check for duplicates or missing fields.`] : void 0
|
|
13592
|
+
};
|
|
13593
|
+
}
|
|
13524
13594
|
const data = args.data;
|
|
13525
13595
|
if (!data) throw new Error("data is required for create action");
|
|
13526
13596
|
return manager.createPerson(data);
|
|
@@ -13535,7 +13605,10 @@ async function handleCrm(args, ctx) {
|
|
|
13535
13605
|
case "find_company": {
|
|
13536
13606
|
const query = args.query;
|
|
13537
13607
|
const name = args.name ?? query?.name;
|
|
13538
|
-
if (!name)
|
|
13608
|
+
if (!name)
|
|
13609
|
+
throw new Error(
|
|
13610
|
+
'name is required for find_company action (pass as name="X" or query={"name":"X"})'
|
|
13611
|
+
);
|
|
13539
13612
|
if (!manager.findCompany) throw new Error("Provider does not support company lookup");
|
|
13540
13613
|
return manager.findCompany(name);
|
|
13541
13614
|
}
|
|
@@ -13552,6 +13625,7 @@ async function handleCrm(args, ctx) {
|
|
|
13552
13625
|
var init_crm2 = __esm({
|
|
13553
13626
|
"src/handlers/crm.ts"() {
|
|
13554
13627
|
"use strict";
|
|
13628
|
+
init_table_bridge();
|
|
13555
13629
|
}
|
|
13556
13630
|
});
|
|
13557
13631
|
|
|
@@ -13736,53 +13810,42 @@ __export(feedback_exports, {
|
|
|
13736
13810
|
handleFeedback: () => handleFeedback
|
|
13737
13811
|
});
|
|
13738
13812
|
async function handleFeedback(args) {
|
|
13739
|
-
const
|
|
13740
|
-
|
|
13741
|
-
|
|
13742
|
-
|
|
13743
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13751
|
-
const response = await fetch(`${SUPABASE_URL}/rest/v1/gtm_feedback`, {
|
|
13752
|
-
method: "POST",
|
|
13753
|
-
headers: {
|
|
13754
|
-
"Content-Type": "application/json",
|
|
13755
|
-
apikey: SUPABASE_KEY,
|
|
13756
|
-
Authorization: `Bearer ${SUPABASE_KEY}`,
|
|
13757
|
-
Prefer: "return=representation"
|
|
13758
|
-
},
|
|
13759
|
-
body: JSON.stringify(row)
|
|
13760
|
-
});
|
|
13761
|
-
if (!response.ok) {
|
|
13762
|
-
const body = await response.text().catch(() => "unknown");
|
|
13763
|
-
throw createGtmError(
|
|
13764
|
-
"PROVIDER_REQUEST_FAILED",
|
|
13765
|
-
`Failed to submit feedback: ${response.status}`,
|
|
13766
|
-
{
|
|
13813
|
+
const type = args.type;
|
|
13814
|
+
const message = args.message.trim();
|
|
13815
|
+
const context = args.context ?? {};
|
|
13816
|
+
try {
|
|
13817
|
+
const response = await fetch(`${VIEWER_URL2}/api/feedback`, {
|
|
13818
|
+
method: "POST",
|
|
13819
|
+
headers: { "Content-Type": "application/json" },
|
|
13820
|
+
body: JSON.stringify({ type, message, context })
|
|
13821
|
+
});
|
|
13822
|
+
if (!response.ok) {
|
|
13823
|
+
const body = await response.text().catch(() => "unknown");
|
|
13824
|
+
throw createGtmError("PROVIDER_REQUEST_FAILED", `Failed to submit feedback: ${response.status}`, {
|
|
13767
13825
|
details: { status: response.status, body }
|
|
13768
|
-
}
|
|
13769
|
-
|
|
13826
|
+
});
|
|
13827
|
+
}
|
|
13828
|
+
const data = await response.json();
|
|
13829
|
+
const created = data[0];
|
|
13830
|
+
return {
|
|
13831
|
+
submitted: true,
|
|
13832
|
+
id: created?.id,
|
|
13833
|
+
type,
|
|
13834
|
+
message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
|
|
13835
|
+
};
|
|
13836
|
+
} catch (error) {
|
|
13837
|
+
if (error && typeof error === "object" && "error_code" in error) throw error;
|
|
13838
|
+
logError("feedback:submit", error instanceof Error ? error : new Error(String(error)));
|
|
13839
|
+
throw createGtmError("PROVIDER_REQUEST_FAILED", "Failed to submit feedback. Please try again later.");
|
|
13770
13840
|
}
|
|
13771
|
-
const [created] = await response.json();
|
|
13772
|
-
return {
|
|
13773
|
-
submitted: true,
|
|
13774
|
-
id: created?.id,
|
|
13775
|
-
type: input.type,
|
|
13776
|
-
message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
|
|
13777
|
-
};
|
|
13778
13841
|
}
|
|
13779
|
-
var
|
|
13842
|
+
var VIEWER_URL2;
|
|
13780
13843
|
var init_feedback2 = __esm({
|
|
13781
13844
|
"src/handlers/feedback.ts"() {
|
|
13782
13845
|
"use strict";
|
|
13846
|
+
init_dist();
|
|
13783
13847
|
init_errors();
|
|
13784
|
-
|
|
13785
|
-
SUPABASE_KEY = process.env.GTM_SUPABASE_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
|
|
13848
|
+
VIEWER_URL2 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
|
|
13786
13849
|
}
|
|
13787
13850
|
});
|
|
13788
13851
|
|
|
@@ -13807,14 +13870,21 @@ async function handleReview(args) {
|
|
|
13807
13870
|
async function createTable(args) {
|
|
13808
13871
|
const data = args.data;
|
|
13809
13872
|
const title = args.title ?? "Review Table";
|
|
13873
|
+
const tableId = args.table_id;
|
|
13874
|
+
const ttlSeconds = args.ttl_seconds;
|
|
13810
13875
|
const rows = data.map((row, i) => ({
|
|
13811
13876
|
...row,
|
|
13812
13877
|
_row_id: row._row_id ?? `r${i + 1}`
|
|
13813
13878
|
}));
|
|
13814
|
-
const response = await fetch(`${
|
|
13879
|
+
const response = await fetch(`${VIEWER_URL3}/api/tables`, {
|
|
13815
13880
|
method: "POST",
|
|
13816
13881
|
headers: { "Content-Type": "application/json" },
|
|
13817
|
-
body: JSON.stringify({
|
|
13882
|
+
body: JSON.stringify({
|
|
13883
|
+
title,
|
|
13884
|
+
rows,
|
|
13885
|
+
...tableId && { table_id: tableId },
|
|
13886
|
+
...ttlSeconds && { ttl_seconds: ttlSeconds }
|
|
13887
|
+
})
|
|
13818
13888
|
});
|
|
13819
13889
|
if (!response.ok) {
|
|
13820
13890
|
const body = await response.json().catch(() => ({}));
|
|
@@ -13826,7 +13896,7 @@ async function createTable(args) {
|
|
|
13826
13896
|
async function updateTable(args) {
|
|
13827
13897
|
const tableId = args.table_id;
|
|
13828
13898
|
const rows = args.rows;
|
|
13829
|
-
const response = await fetch(`${
|
|
13899
|
+
const response = await fetch(`${VIEWER_URL3}/api/tables/${tableId}`, {
|
|
13830
13900
|
method: "PATCH",
|
|
13831
13901
|
headers: { "Content-Type": "application/json" },
|
|
13832
13902
|
body: JSON.stringify({ rows })
|
|
@@ -13839,18 +13909,18 @@ async function updateTable(args) {
|
|
|
13839
13909
|
}
|
|
13840
13910
|
async function getSelected(args) {
|
|
13841
13911
|
const tableId = args.table_id;
|
|
13842
|
-
const response = await fetch(`${
|
|
13912
|
+
const response = await fetch(`${VIEWER_URL3}/api/tables/${tableId}/selected`);
|
|
13843
13913
|
if (!response.ok) {
|
|
13844
13914
|
const body = await response.json().catch(() => ({}));
|
|
13845
13915
|
throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
|
|
13846
13916
|
}
|
|
13847
13917
|
return response.json();
|
|
13848
13918
|
}
|
|
13849
|
-
var
|
|
13919
|
+
var VIEWER_URL3;
|
|
13850
13920
|
var init_review2 = __esm({
|
|
13851
13921
|
"src/handlers/review.ts"() {
|
|
13852
13922
|
"use strict";
|
|
13853
|
-
|
|
13923
|
+
VIEWER_URL3 = process.env.GTM_VIEWER_URL ?? "https://gtm-viewer-production.up.railway.app";
|
|
13854
13924
|
}
|
|
13855
13925
|
});
|
|
13856
13926
|
|
|
@@ -14092,7 +14162,8 @@ var init_validation = __esm({
|
|
|
14092
14162
|
min_revenue: z2.number().optional(),
|
|
14093
14163
|
max_revenue: z2.number().optional(),
|
|
14094
14164
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
14095
|
-
detail: detailSchema
|
|
14165
|
+
detail: detailSchema,
|
|
14166
|
+
review: z2.boolean().optional()
|
|
14096
14167
|
}),
|
|
14097
14168
|
// 2. People/contact search
|
|
14098
14169
|
gtm_find_people: z2.object({
|
|
@@ -14132,6 +14203,7 @@ var init_validation = __esm({
|
|
|
14132
14203
|
max_employees: z2.number().int().min(1).optional(),
|
|
14133
14204
|
revenue_range: z2.string().optional(),
|
|
14134
14205
|
enrich: z2.boolean().optional(),
|
|
14206
|
+
review: z2.boolean().optional(),
|
|
14135
14207
|
fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
|
|
14136
14208
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
14137
14209
|
detail: detailSchema
|
|
@@ -14143,12 +14215,16 @@ var init_validation = __esm({
|
|
|
14143
14215
|
),
|
|
14144
14216
|
// 3. Enrichment waterfall
|
|
14145
14217
|
gtm_enrich: z2.object({
|
|
14146
|
-
contacts: z2.array(contactInputSchema).
|
|
14218
|
+
contacts: z2.array(contactInputSchema).max(100, "contacts must contain at most 100 contacts").optional(),
|
|
14147
14219
|
find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
|
|
14148
14220
|
waterfall: z2.array(z2.string()).optional(),
|
|
14149
14221
|
verify: z2.boolean().optional(),
|
|
14150
14222
|
dry_run: z2.boolean().optional(),
|
|
14151
|
-
detail: detailSchema
|
|
14223
|
+
detail: detailSchema,
|
|
14224
|
+
table_id: z2.string().optional(),
|
|
14225
|
+
review: z2.boolean().optional()
|
|
14226
|
+
}).refine((data) => !!(data.contacts?.length || data.table_id), {
|
|
14227
|
+
message: "At least one of contacts or table_id is required"
|
|
14152
14228
|
}),
|
|
14153
14229
|
// 4. Email verification
|
|
14154
14230
|
gtm_verify: z2.object({
|
|
@@ -14232,7 +14308,8 @@ var init_validation = __esm({
|
|
|
14232
14308
|
leads: z2.array(campaignLeadSchema).optional(),
|
|
14233
14309
|
name: z2.string().optional(),
|
|
14234
14310
|
provider: z2.string().optional(),
|
|
14235
|
-
detail: detailSchema
|
|
14311
|
+
detail: detailSchema,
|
|
14312
|
+
table_id: z2.string().optional()
|
|
14236
14313
|
}),
|
|
14237
14314
|
// 10. CRM read/write
|
|
14238
14315
|
gtm_crm: z2.object({
|
|
@@ -14241,7 +14318,8 @@ var init_validation = __esm({
|
|
|
14241
14318
|
}),
|
|
14242
14319
|
query: z2.record(z2.unknown()).optional(),
|
|
14243
14320
|
data: z2.record(z2.unknown()).optional(),
|
|
14244
|
-
id: z2.string().optional()
|
|
14321
|
+
id: z2.string().optional(),
|
|
14322
|
+
table_id: z2.string().optional()
|
|
14245
14323
|
}),
|
|
14246
14324
|
// 11. Provider status/credits
|
|
14247
14325
|
gtm_status: z2.object({
|
|
@@ -14294,7 +14372,9 @@ var init_validation = __esm({
|
|
|
14294
14372
|
z2.object({
|
|
14295
14373
|
action: z2.literal("create"),
|
|
14296
14374
|
title: z2.string().optional(),
|
|
14297
|
-
data: z2.array(z2.record(z2.unknown())).min(1, "data must contain at least 1 row")
|
|
14375
|
+
data: z2.array(z2.record(z2.unknown())).min(1, "data must contain at least 1 row"),
|
|
14376
|
+
table_id: z2.string().optional(),
|
|
14377
|
+
ttl_seconds: z2.number().optional()
|
|
14298
14378
|
}),
|
|
14299
14379
|
z2.object({
|
|
14300
14380
|
action: z2.literal("update"),
|
|
@@ -14333,7 +14413,7 @@ function createServer() {
|
|
|
14333
14413
|
{ capabilities: { tools: {} } }
|
|
14334
14414
|
);
|
|
14335
14415
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
14336
|
-
tools
|
|
14416
|
+
tools: mcpTools
|
|
14337
14417
|
}));
|
|
14338
14418
|
server.setRequestHandler(CallToolRequestSchema, async (request, _extra) => {
|
|
14339
14419
|
const { name, arguments: args = {} } = request.params;
|