gtm-now 0.9.0 → 0.10.0
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 +689 -378
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10339,7 +10339,7 @@ var init_harvest = __esm({
|
|
|
10339
10339
|
HarvestApiAdapter = class {
|
|
10340
10340
|
name = "harvest";
|
|
10341
10341
|
capabilities = ["extract_linkedin"];
|
|
10342
|
-
notes = "Best for: LinkedIn profile scraping, post search, engagement mining, activity signals. Called
|
|
10342
|
+
notes = "Best for: LinkedIn profile scraping, post search, engagement mining, activity signals. Called by gtm_extract for LinkedIn types.";
|
|
10343
10343
|
client;
|
|
10344
10344
|
credentialsCached = null;
|
|
10345
10345
|
constructor(apiKey) {
|
|
@@ -10621,7 +10621,7 @@ var init_prospect = __esm({
|
|
|
10621
10621
|
prospectTools = [
|
|
10622
10622
|
{
|
|
10623
10623
|
name: "gtm_find_companies",
|
|
10624
|
-
description:
|
|
10624
|
+
description: "Discover companies matching ICP criteria via DiscoLike, Apollo, or StoreLeads. Supports lookalike discovery, ICP text, tech stack filtering. Use gtm_tool_help for full parameter details.",
|
|
10625
10625
|
inputSchema: {
|
|
10626
10626
|
type: "object",
|
|
10627
10627
|
properties: {
|
|
@@ -10714,7 +10714,12 @@ var init_prospect = __esm({
|
|
|
10714
10714
|
output: {
|
|
10715
10715
|
type: "string",
|
|
10716
10716
|
enum: ["file", "inline"],
|
|
10717
|
-
description: 'Output mode. "file" writes results to JSONL file (auto when >
|
|
10717
|
+
description: 'Output mode. "file" writes results to JSONL file (auto when >20 results). "inline" returns in response.'
|
|
10718
|
+
},
|
|
10719
|
+
detail: {
|
|
10720
|
+
type: "string",
|
|
10721
|
+
enum: ["summary", "full"],
|
|
10722
|
+
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
10718
10723
|
}
|
|
10719
10724
|
},
|
|
10720
10725
|
required: []
|
|
@@ -10722,7 +10727,7 @@ var init_prospect = __esm({
|
|
|
10722
10727
|
},
|
|
10723
10728
|
{
|
|
10724
10729
|
name: "gtm_find_people",
|
|
10725
|
-
description: 'Find contacts
|
|
10730
|
+
description: 'Find contacts by job title, company, or domain. Returns name, title, email, LinkedIn URL. Slim by default; pass fields: "all" for full payload.',
|
|
10726
10731
|
inputSchema: {
|
|
10727
10732
|
type: "object",
|
|
10728
10733
|
properties: {
|
|
@@ -10821,7 +10826,12 @@ var init_prospect = __esm({
|
|
|
10821
10826
|
output: {
|
|
10822
10827
|
type: "string",
|
|
10823
10828
|
enum: ["file", "inline"],
|
|
10824
|
-
description: 'Output mode. "file" writes results to JSONL file (auto when >
|
|
10829
|
+
description: 'Output mode. "file" writes results to JSONL file (auto when >20 results). "inline" returns in response.'
|
|
10830
|
+
},
|
|
10831
|
+
detail: {
|
|
10832
|
+
type: "string",
|
|
10833
|
+
enum: ["summary", "full"],
|
|
10834
|
+
description: 'Response detail level. "summary" (default) returns slim results. "full" includes applied_filters and all metadata.'
|
|
10825
10835
|
}
|
|
10826
10836
|
},
|
|
10827
10837
|
required: []
|
|
@@ -10839,7 +10849,7 @@ var init_enrich = __esm({
|
|
|
10839
10849
|
enrichTools = [
|
|
10840
10850
|
{
|
|
10841
10851
|
name: "gtm_enrich",
|
|
10842
|
-
description: "Email
|
|
10852
|
+
description: "Email/phone enrichment via waterfall \u2014 tries providers in order, stops on first match. Always dry_run first for batches > 5.",
|
|
10843
10853
|
inputSchema: {
|
|
10844
10854
|
type: "object",
|
|
10845
10855
|
properties: {
|
|
@@ -10874,7 +10884,12 @@ var init_enrich = __esm({
|
|
|
10874
10884
|
description: "Provider order override (uses client default if omitted)"
|
|
10875
10885
|
},
|
|
10876
10886
|
verify: { type: "boolean", default: true },
|
|
10877
|
-
dry_run: { type: "boolean", default: false }
|
|
10887
|
+
dry_run: { type: "boolean", default: false },
|
|
10888
|
+
detail: {
|
|
10889
|
+
type: "string",
|
|
10890
|
+
enum: ["summary", "full"],
|
|
10891
|
+
description: 'Response detail level. "summary" (default) returns email/phone/providers per contact. "full" includes waterfall attempt details (provider, duration, errors) per contact.'
|
|
10892
|
+
}
|
|
10878
10893
|
},
|
|
10879
10894
|
required: ["contacts"]
|
|
10880
10895
|
}
|
|
@@ -10905,30 +10920,78 @@ var init_extract = __esm({
|
|
|
10905
10920
|
extractTools = [
|
|
10906
10921
|
{
|
|
10907
10922
|
name: "gtm_extract",
|
|
10908
|
-
description: "Extract
|
|
10923
|
+
description: "Extract data from LinkedIn (profiles, posts, engagement, search) or web pages. HarvestAPI charges per request.",
|
|
10909
10924
|
inputSchema: {
|
|
10910
10925
|
type: "object",
|
|
10926
|
+
required: ["type"],
|
|
10911
10927
|
properties: {
|
|
10928
|
+
type: {
|
|
10929
|
+
type: "string",
|
|
10930
|
+
enum: [
|
|
10931
|
+
"linkedin_profile",
|
|
10932
|
+
"linkedin_posts",
|
|
10933
|
+
"linkedin_engagement",
|
|
10934
|
+
"linkedin_search",
|
|
10935
|
+
"linkedin_company_posts",
|
|
10936
|
+
"web_page"
|
|
10937
|
+
],
|
|
10938
|
+
description: "Content type to extract"
|
|
10939
|
+
},
|
|
10912
10940
|
url: {
|
|
10913
10941
|
type: "string",
|
|
10914
|
-
description: "URL to extract data from (
|
|
10942
|
+
description: "URL to extract data from (required for web_page)"
|
|
10915
10943
|
},
|
|
10916
|
-
|
|
10944
|
+
linkedin_url: {
|
|
10945
|
+
type: "string",
|
|
10946
|
+
description: "LinkedIn profile URL (required for linkedin_profile)"
|
|
10947
|
+
},
|
|
10948
|
+
find_email: {
|
|
10949
|
+
type: "boolean",
|
|
10950
|
+
description: "Find email via SMTP verification \u2014 uses extra credits (linkedin_profile only)"
|
|
10951
|
+
},
|
|
10952
|
+
profile: {
|
|
10953
|
+
type: "string",
|
|
10954
|
+
description: "LinkedIn URL or publicIdentifier (required for linkedin_posts)"
|
|
10955
|
+
},
|
|
10956
|
+
company: {
|
|
10957
|
+
type: "string",
|
|
10958
|
+
description: "Company LinkedIn URL or universalName (required for linkedin_company_posts)"
|
|
10959
|
+
},
|
|
10960
|
+
posted_within: {
|
|
10961
|
+
type: "string",
|
|
10962
|
+
enum: ["24h", "week", "month"],
|
|
10963
|
+
description: "Filter posts by recency (linkedin_posts, linkedin_search, linkedin_company_posts)"
|
|
10964
|
+
},
|
|
10965
|
+
post_url: {
|
|
10966
|
+
type: "string",
|
|
10967
|
+
description: "LinkedIn post URL (required for linkedin_engagement)"
|
|
10968
|
+
},
|
|
10969
|
+
engagement_type: {
|
|
10917
10970
|
type: "string",
|
|
10918
|
-
enum: ["
|
|
10919
|
-
description: "
|
|
10971
|
+
enum: ["comments", "reactions", "both"],
|
|
10972
|
+
description: "Type of engagement to retrieve (default: both)"
|
|
10973
|
+
},
|
|
10974
|
+
query: {
|
|
10975
|
+
type: "string",
|
|
10976
|
+
description: "Keyword search (required for linkedin_search)"
|
|
10977
|
+
},
|
|
10978
|
+
sort: {
|
|
10979
|
+
type: "string",
|
|
10980
|
+
enum: ["relevance", "date"],
|
|
10981
|
+
description: "Sort order for linkedin_search"
|
|
10920
10982
|
},
|
|
10983
|
+
limit: { type: "number", description: "Max results to return (max 100)" },
|
|
10921
10984
|
fields: {
|
|
10922
10985
|
type: "array",
|
|
10923
10986
|
items: { type: "string" },
|
|
10924
10987
|
description: 'Specific fields to extract (e.g., ["name", "title", "company", "about"]). Extracts all available fields if omitted.'
|
|
10925
10988
|
},
|
|
10926
|
-
|
|
10989
|
+
detail: {
|
|
10927
10990
|
type: "string",
|
|
10928
|
-
|
|
10991
|
+
enum: ["summary", "full"],
|
|
10992
|
+
description: 'Response detail level. "summary" (default) returns counts and key fields only \u2014 truncates post text, omits individual engagement items. "full" returns complete data including all post content and engager lists.'
|
|
10929
10993
|
}
|
|
10930
|
-
}
|
|
10931
|
-
required: ["url"]
|
|
10994
|
+
}
|
|
10932
10995
|
}
|
|
10933
10996
|
}
|
|
10934
10997
|
];
|
|
@@ -10964,6 +11027,11 @@ var init_fetch = __esm({
|
|
|
10964
11027
|
params: {
|
|
10965
11028
|
type: "object",
|
|
10966
11029
|
description: "URL query parameters"
|
|
11030
|
+
},
|
|
11031
|
+
detail: {
|
|
11032
|
+
type: "string",
|
|
11033
|
+
enum: ["summary", "full"],
|
|
11034
|
+
description: 'Response detail level. "summary" (default) omits response headers and truncates large bodies. "full" returns complete response including all headers.'
|
|
10967
11035
|
}
|
|
10968
11036
|
},
|
|
10969
11037
|
required: ["url"]
|
|
@@ -11017,7 +11085,7 @@ var init_campaign = __esm({
|
|
|
11017
11085
|
campaignTools = [
|
|
11018
11086
|
{
|
|
11019
11087
|
name: "gtm_campaign",
|
|
11020
|
-
description: "Manage cold email campaigns. List
|
|
11088
|
+
description: "Manage cold email and LinkedIn outreach campaigns. List, stats, add_leads, or create. Supports PlusVibe, Instantly, SmartLead, HeyReach.",
|
|
11021
11089
|
inputSchema: {
|
|
11022
11090
|
type: "object",
|
|
11023
11091
|
properties: {
|
|
@@ -11035,7 +11103,14 @@ var init_campaign = __esm({
|
|
|
11035
11103
|
items: {
|
|
11036
11104
|
type: "object",
|
|
11037
11105
|
properties: {
|
|
11038
|
-
email: {
|
|
11106
|
+
email: {
|
|
11107
|
+
type: "string",
|
|
11108
|
+
description: "Email address (required for email campaigns)"
|
|
11109
|
+
},
|
|
11110
|
+
linkedin_url: {
|
|
11111
|
+
type: "string",
|
|
11112
|
+
description: "LinkedIn profile URL (required for LinkedIn outreach campaigns)"
|
|
11113
|
+
},
|
|
11039
11114
|
first_name: { type: "string" },
|
|
11040
11115
|
last_name: { type: "string" },
|
|
11041
11116
|
company: { type: "string" },
|
|
@@ -11043,10 +11118,9 @@ var init_campaign = __esm({
|
|
|
11043
11118
|
type: "object",
|
|
11044
11119
|
description: "Custom merge fields for email personalization"
|
|
11045
11120
|
}
|
|
11046
|
-
}
|
|
11047
|
-
required: ["email"]
|
|
11121
|
+
}
|
|
11048
11122
|
},
|
|
11049
|
-
description: "Leads to add (required for add_leads action)"
|
|
11123
|
+
description: "Leads to add (required for add_leads action). Each lead needs at least email or linkedin_url."
|
|
11050
11124
|
},
|
|
11051
11125
|
name: {
|
|
11052
11126
|
type: "string",
|
|
@@ -11054,54 +11128,12 @@ var init_campaign = __esm({
|
|
|
11054
11128
|
},
|
|
11055
11129
|
provider: {
|
|
11056
11130
|
type: "string",
|
|
11057
|
-
description: "Provider override (
|
|
11058
|
-
}
|
|
11059
|
-
},
|
|
11060
|
-
required: ["action"]
|
|
11061
|
-
}
|
|
11062
|
-
}
|
|
11063
|
-
];
|
|
11064
|
-
}
|
|
11065
|
-
});
|
|
11066
|
-
|
|
11067
|
-
// src/tools/outreach.ts
|
|
11068
|
-
var outreachTools;
|
|
11069
|
-
var init_outreach = __esm({
|
|
11070
|
-
"src/tools/outreach.ts"() {
|
|
11071
|
-
"use strict";
|
|
11072
|
-
outreachTools = [
|
|
11073
|
-
{
|
|
11074
|
-
name: "gtm_outreach",
|
|
11075
|
-
description: "Manage LinkedIn outreach campaigns. List campaigns, get stats, or add leads to a LinkedIn outreach campaign. Leads require a LinkedIn profile URL.",
|
|
11076
|
-
inputSchema: {
|
|
11077
|
-
type: "object",
|
|
11078
|
-
properties: {
|
|
11079
|
-
action: {
|
|
11080
|
-
type: "string",
|
|
11081
|
-
enum: ["list", "stats", "add_leads"],
|
|
11082
|
-
description: "Action to perform: list (all campaigns), stats (campaign metrics), add_leads (push contacts)"
|
|
11083
|
-
},
|
|
11084
|
-
campaign_id: {
|
|
11085
|
-
type: "string",
|
|
11086
|
-
description: "Campaign ID (required for stats and add_leads)"
|
|
11131
|
+
description: "Provider override (auto-selects based on available campaign_email or outreach_linkedin provider if omitted)"
|
|
11087
11132
|
},
|
|
11088
|
-
|
|
11089
|
-
type: "array",
|
|
11090
|
-
items: {
|
|
11091
|
-
type: "object",
|
|
11092
|
-
properties: {
|
|
11093
|
-
linkedin_url: { type: "string" },
|
|
11094
|
-
first_name: { type: "string" },
|
|
11095
|
-
last_name: { type: "string" },
|
|
11096
|
-
company: { type: "string" }
|
|
11097
|
-
},
|
|
11098
|
-
required: ["linkedin_url"]
|
|
11099
|
-
},
|
|
11100
|
-
description: "Leads to add (required for add_leads action). Each lead must have a LinkedIn profile URL."
|
|
11101
|
-
},
|
|
11102
|
-
provider: {
|
|
11133
|
+
detail: {
|
|
11103
11134
|
type: "string",
|
|
11104
|
-
|
|
11135
|
+
enum: ["summary", "full"],
|
|
11136
|
+
description: 'Response detail level. "summary" (default) returns key stats only. "full" returns all campaign data.'
|
|
11105
11137
|
}
|
|
11106
11138
|
},
|
|
11107
11139
|
required: ["action"]
|
|
@@ -11177,7 +11209,7 @@ var init_status = __esm({
|
|
|
11177
11209
|
statusTools = [
|
|
11178
11210
|
{
|
|
11179
11211
|
name: "gtm_status",
|
|
11180
|
-
description: "Check provider health, credit balances, and active configuration.
|
|
11212
|
+
description: "Check provider health, credit balances, and active configuration.",
|
|
11181
11213
|
inputSchema: {
|
|
11182
11214
|
type: "object",
|
|
11183
11215
|
properties: {
|
|
@@ -11190,6 +11222,11 @@ var init_status = __esm({
|
|
|
11190
11222
|
provider: {
|
|
11191
11223
|
type: "string",
|
|
11192
11224
|
description: "Check a specific provider only (checks all configured providers if omitted)"
|
|
11225
|
+
},
|
|
11226
|
+
detail: {
|
|
11227
|
+
type: "string",
|
|
11228
|
+
enum: ["summary", "full"],
|
|
11229
|
+
description: 'Response detail level. "summary" (default) returns provider name, health, and credits only. "full" includes capabilities, notes, and unconfigured tool details.'
|
|
11193
11230
|
}
|
|
11194
11231
|
},
|
|
11195
11232
|
required: []
|
|
@@ -11237,71 +11274,125 @@ var init_configure = __esm({
|
|
|
11237
11274
|
}
|
|
11238
11275
|
});
|
|
11239
11276
|
|
|
11240
|
-
// src/tools/
|
|
11241
|
-
var
|
|
11242
|
-
var
|
|
11243
|
-
"src/tools/
|
|
11277
|
+
// src/tools/feedback.ts
|
|
11278
|
+
var feedbackTools;
|
|
11279
|
+
var init_feedback = __esm({
|
|
11280
|
+
"src/tools/feedback.ts"() {
|
|
11244
11281
|
"use strict";
|
|
11245
|
-
|
|
11282
|
+
feedbackTools = [
|
|
11246
11283
|
{
|
|
11247
|
-
name: "
|
|
11248
|
-
description: "
|
|
11284
|
+
name: "gtm_feedback",
|
|
11285
|
+
description: "Submit feedback, bug reports, feature requests, or provider requests. Use this to report issues with gtm-now or request new data providers and capabilities. Feedback is stored for the gtm-now maintainers to review.",
|
|
11249
11286
|
inputSchema: {
|
|
11250
11287
|
type: "object",
|
|
11251
|
-
required: ["action"],
|
|
11252
11288
|
properties: {
|
|
11253
|
-
|
|
11254
|
-
type: "string",
|
|
11255
|
-
enum: ["profile", "posts", "engagement", "company_posts", "search_posts"],
|
|
11256
|
-
description: "Action to perform"
|
|
11257
|
-
},
|
|
11258
|
-
linkedin_url: {
|
|
11259
|
-
type: "string",
|
|
11260
|
-
description: "LinkedIn profile URL (required for profile action)"
|
|
11261
|
-
},
|
|
11262
|
-
find_email: {
|
|
11263
|
-
type: "boolean",
|
|
11264
|
-
description: "Find email via SMTP verification \u2014 uses extra credits (profile action only)"
|
|
11265
|
-
},
|
|
11266
|
-
profile: {
|
|
11267
|
-
type: "string",
|
|
11268
|
-
description: "LinkedIn URL or publicIdentifier (required for posts action)"
|
|
11269
|
-
},
|
|
11270
|
-
company: {
|
|
11271
|
-
type: "string",
|
|
11272
|
-
description: "Company LinkedIn URL or universalName (required for company_posts action)"
|
|
11273
|
-
},
|
|
11274
|
-
posted_within: {
|
|
11275
|
-
type: "string",
|
|
11276
|
-
enum: ["24h", "week", "month"],
|
|
11277
|
-
description: "Filter posts by recency"
|
|
11278
|
-
},
|
|
11279
|
-
post_url: {
|
|
11280
|
-
type: "string",
|
|
11281
|
-
description: "LinkedIn post URL (required for engagement action)"
|
|
11282
|
-
},
|
|
11283
|
-
engagement_type: {
|
|
11289
|
+
type: {
|
|
11284
11290
|
type: "string",
|
|
11285
|
-
enum: ["
|
|
11286
|
-
description:
|
|
11291
|
+
enum: ["bug", "feature_request", "provider_request"],
|
|
11292
|
+
description: 'Type of feedback: "bug" for something broken, "feature_request" for new capabilities, "provider_request" for requesting a new data provider integration'
|
|
11287
11293
|
},
|
|
11288
|
-
|
|
11294
|
+
message: {
|
|
11289
11295
|
type: "string",
|
|
11290
|
-
description: "
|
|
11296
|
+
description: "Describe the issue, request, or suggestion in detail"
|
|
11291
11297
|
},
|
|
11292
|
-
|
|
11293
|
-
type: "
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
}
|
|
11298
|
+
context: {
|
|
11299
|
+
type: "object",
|
|
11300
|
+
description: "Optional context \u2014 tool name, error_code, args used, provider, etc. Include anything that helps reproduce or understand the feedback."
|
|
11301
|
+
}
|
|
11302
|
+
},
|
|
11303
|
+
required: ["type", "message"]
|
|
11299
11304
|
}
|
|
11300
11305
|
}
|
|
11301
11306
|
];
|
|
11302
11307
|
}
|
|
11303
11308
|
});
|
|
11304
11309
|
|
|
11310
|
+
// src/tools/descriptions.ts
|
|
11311
|
+
var TOOL_DESCRIPTIONS;
|
|
11312
|
+
var init_descriptions = __esm({
|
|
11313
|
+
"src/tools/descriptions.ts"() {
|
|
11314
|
+
"use strict";
|
|
11315
|
+
TOOL_DESCRIPTIONS = {
|
|
11316
|
+
gtm_find_companies: `Discover companies matching your ICP criteria. Powered by DiscoLike (60M+ companies from web/SSL crawl data) and optionally Apollo.
|
|
11317
|
+
|
|
11318
|
+
DiscoLike strengths:
|
|
11319
|
+
- Lookalike discovery: give it 1-10 seed domains, get similar companies
|
|
11320
|
+
- ICP text matching: describe your ideal customer in plain English
|
|
11321
|
+
- Tech stack filtering: find companies using specific technologies
|
|
11322
|
+
- Website content analysis: matches against actual site content
|
|
11323
|
+
- Best for: B2B companies, local businesses, professional services, healthcare
|
|
11324
|
+
|
|
11325
|
+
For DTC/e-commerce brands, use provider: "storeleads" \u2014 it has revenue estimates and category data that neither DiscoLike nor Apollo provides.
|
|
11326
|
+
|
|
11327
|
+
Tip: DiscoLike lookalike mode is extremely powerful. If you have 2-3 example companies, use the domains parameter to find hundreds of similar ones.
|
|
11328
|
+
|
|
11329
|
+
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.`,
|
|
11330
|
+
gtm_find_people: `Find contacts at companies by job title, company, or domain. Returns name, title, email (when available), phone, and LinkedIn URL.
|
|
11331
|
+
|
|
11332
|
+
Powered by DiscoLike Contacts, Apollo, or BetterContact depending on configuration.
|
|
11333
|
+
|
|
11334
|
+
DiscoLike strengths:
|
|
11335
|
+
- Domain-based search: pass company_domain or company_domains[] for multi-domain batch
|
|
11336
|
+
- Seniority filtering (owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)
|
|
11337
|
+
- Industry + skills filtering to narrow results without post-processing
|
|
11338
|
+
- email_validated flag for pre-verified emails only
|
|
11339
|
+
- Employee/revenue range filters for company-level targeting
|
|
11340
|
+
- Returns validated emails when available
|
|
11341
|
+
|
|
11342
|
+
Response is slim by default (name, title, email, company, company_domain, linkedin). Pass fields: "all" for full payload.
|
|
11343
|
+
|
|
11344
|
+
After finding people, use:
|
|
11345
|
+
- gtm_enrich to get verified email/phone if not returned (company_domain field is pipeline-compatible)
|
|
11346
|
+
- gtm_extract to check LinkedIn activity and engagement signals
|
|
11347
|
+
|
|
11348
|
+
Known limitations:
|
|
11349
|
+
- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other.`,
|
|
11350
|
+
gtm_enrich: `Email and phone enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
|
|
11351
|
+
|
|
11352
|
+
Always do a dry_run first for batches > 5 to preview cost.
|
|
11353
|
+
|
|
11354
|
+
Providers (in configured waterfall order):
|
|
11355
|
+
- Prospeo: email finder, good hit rate on LinkedIn-active professionals
|
|
11356
|
+
- BetterContact: email + phone, charges only on successful find
|
|
11357
|
+
|
|
11358
|
+
Input needs: first_name + last_name minimum. Adding company_domain dramatically improves hit rates. LinkedIn URL is the gold standard input.
|
|
11359
|
+
|
|
11360
|
+
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.`,
|
|
11361
|
+
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.",
|
|
11362
|
+
gtm_extract: `Extract structured data from any URL or platform. LinkedIn profiles, posts, engagement, search results, company posts, and web pages.
|
|
11363
|
+
|
|
11364
|
+
When to use:
|
|
11365
|
+
- Scrape a LinkedIn profile for headline, experience, skills, follower count
|
|
11366
|
+
- Check if someone is active on LinkedIn (recent posts, engagement levels)
|
|
11367
|
+
- Find people who comment on or react to specific posts (warm lead mining)
|
|
11368
|
+
- Search LinkedIn posts by keyword to find thought leaders
|
|
11369
|
+
- Monitor competitor or industry content for engagers
|
|
11370
|
+
- Scrape any web page for structured data
|
|
11371
|
+
|
|
11372
|
+
When NOT to use:
|
|
11373
|
+
- Finding people by job title at a company \u2014 use gtm_find_people instead
|
|
11374
|
+
- Email enrichment \u2014 use gtm_enrich instead
|
|
11375
|
+
|
|
11376
|
+
Pairs well with:
|
|
11377
|
+
- gtm_find_people \u2192 gtm_extract (find contacts, then check LinkedIn activity)
|
|
11378
|
+
- gtm_extract linkedin_engagement \u2192 gtm_enrich (find engagers, then get emails)
|
|
11379
|
+
- gtm_storeleads \u2192 gtm_find_people \u2192 gtm_extract (DTC brands \u2192 CEOs \u2192 activity check)
|
|
11380
|
+
|
|
11381
|
+
Note: HarvestAPI charges per request. For engagement mining, each post lookup counts as a request. Use limit to control costs.`,
|
|
11382
|
+
gtm_fetch: "Make an HTTP call to any API endpoint. Use this to pull data from services that don't have a dedicated provider.",
|
|
11383
|
+
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).",
|
|
11384
|
+
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.",
|
|
11385
|
+
gtm_crm: "Read and write CRM data. Find, create, or update people and companies. Create deals. Supports Attio and other CRM providers.",
|
|
11386
|
+
gtm_status: "Check provider health, credit balances, and active configuration. Shows which providers are configured, their capabilities, remaining credits, and the current client context.",
|
|
11387
|
+
gtm_configure: "Manage GTM configuration. Switch between clients, view active config, set waterfall order, list available providers.",
|
|
11388
|
+
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.",
|
|
11389
|
+
gtm_tool_help: "Get the full parameter schema for a specific GTM tool. Use this to see exactly what arguments a tool accepts before calling gtm_execute.",
|
|
11390
|
+
gtm_execute: "Gateway to execute any GTM tool by name. Use gtm_guide for workflow recipes, gtm_tool_help for parameter schemas, then this to execute.",
|
|
11391
|
+
gtm_feedback: "Submit feedback, bug reports, feature requests, or provider requests. Feedback is stored for the gtm-now maintainers to review."
|
|
11392
|
+
};
|
|
11393
|
+
}
|
|
11394
|
+
});
|
|
11395
|
+
|
|
11305
11396
|
// src/tools/index.ts
|
|
11306
11397
|
var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, toolsByName, toolCategories, workflowRecipes;
|
|
11307
11398
|
var init_tools = __esm({
|
|
@@ -11313,22 +11404,20 @@ var init_tools = __esm({
|
|
|
11313
11404
|
init_fetch();
|
|
11314
11405
|
init_ai();
|
|
11315
11406
|
init_campaign();
|
|
11316
|
-
init_outreach();
|
|
11317
11407
|
init_crm();
|
|
11318
11408
|
init_status();
|
|
11319
11409
|
init_configure();
|
|
11320
|
-
|
|
11410
|
+
init_feedback();
|
|
11321
11411
|
init_prospect();
|
|
11322
11412
|
init_enrich();
|
|
11323
11413
|
init_extract();
|
|
11324
11414
|
init_fetch();
|
|
11325
11415
|
init_ai();
|
|
11326
11416
|
init_campaign();
|
|
11327
|
-
init_outreach();
|
|
11328
11417
|
init_crm();
|
|
11329
11418
|
init_status();
|
|
11330
11419
|
init_configure();
|
|
11331
|
-
|
|
11420
|
+
init_descriptions();
|
|
11332
11421
|
capabilityTools = [
|
|
11333
11422
|
...prospectTools,
|
|
11334
11423
|
...enrichTools,
|
|
@@ -11336,11 +11425,10 @@ var init_tools = __esm({
|
|
|
11336
11425
|
...fetchTools,
|
|
11337
11426
|
...aiTools,
|
|
11338
11427
|
...campaignTools,
|
|
11339
|
-
...outreachTools,
|
|
11340
11428
|
...crmTools,
|
|
11341
11429
|
...statusTools,
|
|
11342
11430
|
...configureTools,
|
|
11343
|
-
...
|
|
11431
|
+
...feedbackTools
|
|
11344
11432
|
];
|
|
11345
11433
|
guideTool = {
|
|
11346
11434
|
name: "gtm_guide",
|
|
@@ -11406,11 +11494,9 @@ var init_tools = __esm({
|
|
|
11406
11494
|
fetch: fetchTools.map((t) => t.name),
|
|
11407
11495
|
ai: aiTools.map((t) => t.name),
|
|
11408
11496
|
campaign: campaignTools.map((t) => t.name),
|
|
11409
|
-
outreach: outreachTools.map((t) => t.name),
|
|
11410
11497
|
crm: crmTools.map((t) => t.name),
|
|
11411
11498
|
status: statusTools.map((t) => t.name),
|
|
11412
11499
|
configure: configureTools.map((t) => t.name),
|
|
11413
|
-
linkedin: linkedinTools.map((t) => t.name),
|
|
11414
11500
|
meta: metaTools.map((t) => t.name)
|
|
11415
11501
|
};
|
|
11416
11502
|
workflowRecipes = {
|
|
@@ -11425,28 +11511,28 @@ Start by identifying what kind of companies you're targeting:
|
|
|
11425
11511
|
\u2192 gtm_find_people({ company_domain: "...", job_title: "CEO" })
|
|
11426
11512
|
3. gtm_enrich (dry_run first) \u2014 get verified emails
|
|
11427
11513
|
\u2192 gtm_enrich({ contacts: [...], find: ["email"], dry_run: true })
|
|
11428
|
-
4. Optional:
|
|
11429
|
-
\u2192
|
|
11514
|
+
4. Optional: gtm_extract \u2014 check if they're active on LinkedIn
|
|
11515
|
+
\u2192 gtm_extract({ type: "linkedin_profile", linkedin_url: "..." })
|
|
11430
11516
|
|
|
11431
11517
|
## B2B / SaaS / Services with websites
|
|
11432
11518
|
1. gtm_find_companies \u2014 use DiscoLike ICP text or lookalikes
|
|
11433
11519
|
\u2192 gtm_find_companies({ icp_text: "...", limit: 100 })
|
|
11434
11520
|
2. gtm_find_people \u2014 find contacts by title
|
|
11435
11521
|
3. gtm_enrich \u2014 get verified emails
|
|
11436
|
-
4. gtm_campaign
|
|
11522
|
+
4. gtm_campaign \u2014 push to email or LinkedIn campaigns
|
|
11437
11523
|
|
|
11438
11524
|
## LinkedIn-native prospects
|
|
11439
11525
|
1. gtm_find_people \u2014 search by title + company
|
|
11440
|
-
2.
|
|
11441
|
-
\u2192
|
|
11526
|
+
2. gtm_extract \u2014 scrape profiles for activity signals
|
|
11527
|
+
\u2192 gtm_extract({ type: "linkedin_profile", linkedin_url: "..." })
|
|
11442
11528
|
3. gtm_enrich \u2014 get emails for active prospects
|
|
11443
|
-
4.
|
|
11529
|
+
4. gtm_campaign \u2014 push to LinkedIn or email campaign
|
|
11444
11530
|
|
|
11445
11531
|
## Engagement-based (warm leads)
|
|
11446
|
-
1.
|
|
11447
|
-
\u2192
|
|
11448
|
-
2.
|
|
11449
|
-
\u2192
|
|
11532
|
+
1. gtm_extract (linkedin_search) \u2014 find relevant content
|
|
11533
|
+
\u2192 gtm_extract({ type: "linkedin_search", query: "...", sort: "date" })
|
|
11534
|
+
2. gtm_extract (linkedin_engagement) \u2014 extract commenters/reactors
|
|
11535
|
+
\u2192 gtm_extract({ type: "linkedin_engagement", post_url: "...", engagement_type: "both" })
|
|
11450
11536
|
3. gtm_enrich \u2014 get emails
|
|
11451
11537
|
4. gtm_campaign \u2014 personalized outreach referencing their engagement
|
|
11452
11538
|
|
|
@@ -11502,10 +11588,10 @@ Always verify emails before pushing to campaigns. Bounced emails hurt sender rep
|
|
|
11502
11588
|
\u2192 gtm_ai({ action: "personalize", data: [...], prompt: "Write a LinkedIn connection request referencing their recent post" })
|
|
11503
11589
|
|
|
11504
11590
|
3. LIST CAMPAIGNS \u2014 Find or identify the target outreach campaign
|
|
11505
|
-
\u2192
|
|
11591
|
+
\u2192 gtm_campaign({ action: "list", provider: "heyreach" })
|
|
11506
11592
|
|
|
11507
11593
|
4. PUSH \u2014 Add leads to LinkedIn outreach campaign
|
|
11508
|
-
\u2192
|
|
11594
|
+
\u2192 gtm_campaign({ action: "add_leads", campaign_id: "...", provider: "heyreach", leads: [{ linkedin_url: "..." }] })
|
|
11509
11595
|
|
|
11510
11596
|
## Key principle
|
|
11511
11597
|
LinkedIn outreach requires LinkedIn profile URLs \u2014 not emails. Extract profile data first for personalization.`,
|
|
@@ -11537,7 +11623,7 @@ Call gtm_guide with one of these tasks:
|
|
|
11537
11623
|
|
|
11538
11624
|
New tools available:
|
|
11539
11625
|
- gtm_find_companies (provider: storeleads) \u2014 Search 1.2M+ e-commerce stores by category, revenue, platform
|
|
11540
|
-
-
|
|
11626
|
+
- gtm_extract \u2014 LinkedIn profiles, posts, engagement mining, search + web page scraping
|
|
11541
11627
|
|
|
11542
11628
|
For any task: always dry_run enrichment batches > 5 to preview cost before executing.`
|
|
11543
11629
|
};
|
|
@@ -11662,7 +11748,7 @@ var FILE_OUTPUT_THRESHOLD, DEFAULT_RESULTS_DIR;
|
|
|
11662
11748
|
var init_result_writer = __esm({
|
|
11663
11749
|
"src/utils/result-writer.ts"() {
|
|
11664
11750
|
"use strict";
|
|
11665
|
-
FILE_OUTPUT_THRESHOLD =
|
|
11751
|
+
FILE_OUTPUT_THRESHOLD = 20;
|
|
11666
11752
|
DEFAULT_RESULTS_DIR = path.join(os.homedir(), ".gtm", "results");
|
|
11667
11753
|
}
|
|
11668
11754
|
});
|
|
@@ -11907,10 +11993,46 @@ function isRateLimitError(error) {
|
|
|
11907
11993
|
const msg = error instanceof Error ? error.message : String(error);
|
|
11908
11994
|
return msg.includes("429") || msg.toLowerCase().includes("rate limit");
|
|
11909
11995
|
}
|
|
11996
|
+
function withTimeout(promise, ms, label) {
|
|
11997
|
+
return new Promise((resolve, reject) => {
|
|
11998
|
+
const timer = setTimeout(
|
|
11999
|
+
() => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
|
|
12000
|
+
ms
|
|
12001
|
+
);
|
|
12002
|
+
promise.then(
|
|
12003
|
+
(val) => {
|
|
12004
|
+
clearTimeout(timer);
|
|
12005
|
+
resolve(val);
|
|
12006
|
+
},
|
|
12007
|
+
(err) => {
|
|
12008
|
+
clearTimeout(timer);
|
|
12009
|
+
reject(err);
|
|
12010
|
+
}
|
|
12011
|
+
);
|
|
12012
|
+
});
|
|
12013
|
+
}
|
|
11910
12014
|
async function runWaterfall(contacts, config, registry) {
|
|
11911
12015
|
const results = [];
|
|
11912
12016
|
const exhausted = /* @__PURE__ */ new Set();
|
|
12017
|
+
const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
|
|
11913
12018
|
for (const contact of contacts) {
|
|
12019
|
+
if (Date.now() > batchDeadline) {
|
|
12020
|
+
results.push({
|
|
12021
|
+
contact,
|
|
12022
|
+
attempts: [
|
|
12023
|
+
{
|
|
12024
|
+
provider: "waterfall",
|
|
12025
|
+
capability: "enrich_email",
|
|
12026
|
+
found: false,
|
|
12027
|
+
charged: false,
|
|
12028
|
+
duration_ms: 0,
|
|
12029
|
+
error: "Batch timeout exceeded \u2014 skipped remaining contacts"
|
|
12030
|
+
}
|
|
12031
|
+
],
|
|
12032
|
+
charged: false
|
|
12033
|
+
});
|
|
12034
|
+
continue;
|
|
12035
|
+
}
|
|
11914
12036
|
const result = await enrichContact(contact, config, registry, exhausted);
|
|
11915
12037
|
results.push(result);
|
|
11916
12038
|
}
|
|
@@ -11999,11 +12121,20 @@ async function enrichField(contact, field, config, registry, exhausted) {
|
|
|
11999
12121
|
const start = Date.now();
|
|
12000
12122
|
try {
|
|
12001
12123
|
let value;
|
|
12124
|
+
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12002
12125
|
if (field === "email" && "enrichEmail" in provider) {
|
|
12003
|
-
const result = await
|
|
12126
|
+
const result = await withTimeout(
|
|
12127
|
+
provider.enrichEmail(contact),
|
|
12128
|
+
providerTimeoutMs,
|
|
12129
|
+
providerName
|
|
12130
|
+
);
|
|
12004
12131
|
value = result?.email;
|
|
12005
12132
|
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12006
|
-
const result = await
|
|
12133
|
+
const result = await withTimeout(
|
|
12134
|
+
provider.enrichPhone(contact),
|
|
12135
|
+
providerTimeoutMs,
|
|
12136
|
+
providerName
|
|
12137
|
+
);
|
|
12007
12138
|
value = result?.phone;
|
|
12008
12139
|
}
|
|
12009
12140
|
const found = !!value;
|
|
@@ -12079,7 +12210,7 @@ function dryRunWaterfall(contactsCount, config, registry) {
|
|
|
12079
12210
|
confirm: "Call gtm_enrich again without dry_run to execute."
|
|
12080
12211
|
};
|
|
12081
12212
|
}
|
|
12082
|
-
var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS;
|
|
12213
|
+
var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
|
|
12083
12214
|
var init_waterfall = __esm({
|
|
12084
12215
|
"src/providers/waterfall.ts"() {
|
|
12085
12216
|
"use strict";
|
|
@@ -12101,6 +12232,8 @@ var init_waterfall = __esm({
|
|
|
12101
12232
|
"not accessible with this api_key on a free plan"
|
|
12102
12233
|
];
|
|
12103
12234
|
WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
|
|
12235
|
+
DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
|
|
12236
|
+
DEFAULT_BATCH_TIMEOUT_MS = 3e5;
|
|
12104
12237
|
}
|
|
12105
12238
|
});
|
|
12106
12239
|
|
|
@@ -12109,6 +12242,19 @@ var enrich_exports = {};
|
|
|
12109
12242
|
__export(enrich_exports, {
|
|
12110
12243
|
handleEnrich: () => handleEnrich
|
|
12111
12244
|
});
|
|
12245
|
+
function slimEnrichResults(results) {
|
|
12246
|
+
return results.map((r) => {
|
|
12247
|
+
const slim = {};
|
|
12248
|
+
if (r.email) slim.email = r.email;
|
|
12249
|
+
if (r.phone) slim.phone = r.phone;
|
|
12250
|
+
if (r.email_status) slim.email_status = r.email_status;
|
|
12251
|
+
if (r.email_provider) slim.email_provider = r.email_provider;
|
|
12252
|
+
if (r.phone_provider) slim.phone_provider = r.phone_provider;
|
|
12253
|
+
slim.charged = r.charged;
|
|
12254
|
+
slim.name = `${r.contact.first_name} ${r.contact.last_name}`;
|
|
12255
|
+
return slim;
|
|
12256
|
+
});
|
|
12257
|
+
}
|
|
12112
12258
|
async function handleEnrich(name, args, ctx) {
|
|
12113
12259
|
if (name === "gtm_verify") {
|
|
12114
12260
|
return verifyEmail(args, ctx);
|
|
@@ -12152,11 +12298,12 @@ async function enrichContacts(args, ctx) {
|
|
|
12152
12298
|
}
|
|
12153
12299
|
const results = await runWaterfall(contacts, config, ctx.registry);
|
|
12154
12300
|
const found = results.filter((r) => r.email || r.phone).length;
|
|
12301
|
+
const detail = args.detail ?? "summary";
|
|
12155
12302
|
return {
|
|
12156
12303
|
total: results.length,
|
|
12157
12304
|
found,
|
|
12158
12305
|
missed: results.length - found,
|
|
12159
|
-
results
|
|
12306
|
+
results: detail === "full" ? results : slimEnrichResults(results)
|
|
12160
12307
|
};
|
|
12161
12308
|
}
|
|
12162
12309
|
async function verifyEmail(args, ctx) {
|
|
@@ -12185,45 +12332,156 @@ var extract_exports = {};
|
|
|
12185
12332
|
__export(extract_exports, {
|
|
12186
12333
|
handleExtract: () => handleExtract
|
|
12187
12334
|
});
|
|
12335
|
+
function slimPost(post) {
|
|
12336
|
+
const slim = {};
|
|
12337
|
+
for (const [key, value] of Object.entries(post)) {
|
|
12338
|
+
if (key === "text" && typeof value === "string") {
|
|
12339
|
+
slim.text = value.length > POST_TEXT_LIMIT ? value.slice(0, POST_TEXT_LIMIT) + "..." : value;
|
|
12340
|
+
} else if (key === "images" || key === "documents" || key === "media") {
|
|
12341
|
+
if (Array.isArray(value) && value.length > 0) slim[`${key}_count`] = value.length;
|
|
12342
|
+
} else {
|
|
12343
|
+
slim[key] = value;
|
|
12344
|
+
}
|
|
12345
|
+
}
|
|
12346
|
+
return slim;
|
|
12347
|
+
}
|
|
12348
|
+
function slimProfile(profile) {
|
|
12349
|
+
const PROFILE_FIELDS = [
|
|
12350
|
+
"firstName",
|
|
12351
|
+
"lastName",
|
|
12352
|
+
"headline",
|
|
12353
|
+
"location",
|
|
12354
|
+
"summary",
|
|
12355
|
+
"followerCount",
|
|
12356
|
+
"connectionCount",
|
|
12357
|
+
"email",
|
|
12358
|
+
"publicIdentifier",
|
|
12359
|
+
"industryName",
|
|
12360
|
+
"company"
|
|
12361
|
+
];
|
|
12362
|
+
const slim = {};
|
|
12363
|
+
for (const field of PROFILE_FIELDS) {
|
|
12364
|
+
if (field in profile && profile[field] != null) slim[field] = profile[field];
|
|
12365
|
+
}
|
|
12366
|
+
return slim;
|
|
12367
|
+
}
|
|
12188
12368
|
async function handleExtract(args, ctx) {
|
|
12369
|
+
const type = args.type;
|
|
12370
|
+
if (type.startsWith("linkedin_")) {
|
|
12371
|
+
return handleLinkedinExtract(type, args, ctx);
|
|
12372
|
+
}
|
|
12373
|
+
return handleWebExtract(args, ctx);
|
|
12374
|
+
}
|
|
12375
|
+
async function handleWebExtract(args, ctx) {
|
|
12189
12376
|
const target = {
|
|
12190
12377
|
url: args.url,
|
|
12191
|
-
type:
|
|
12378
|
+
type: "web_page",
|
|
12192
12379
|
fields: args.fields
|
|
12193
12380
|
};
|
|
12194
|
-
const capability = target.type === "web_page" ? "extract_web" : "extract_linkedin";
|
|
12195
12381
|
const providerName = args.provider || void 0;
|
|
12196
|
-
const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability(
|
|
12382
|
+
const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability("extract_web");
|
|
12197
12383
|
if (providers.length === 0) {
|
|
12198
12384
|
throw createGtmError(
|
|
12199
12385
|
"EXTRACTION_FAILED",
|
|
12200
|
-
|
|
12386
|
+
"No providers configured for extract_web. Add a provider with that capability."
|
|
12201
12387
|
);
|
|
12202
12388
|
}
|
|
12203
|
-
const
|
|
12204
|
-
if (provider.name === "harvest" && capability === "extract_linkedin") {
|
|
12205
|
-
return extractViaHarvest(provider, target);
|
|
12206
|
-
}
|
|
12207
|
-
const extractor = provider;
|
|
12389
|
+
const extractor = providers[0];
|
|
12208
12390
|
return extractor.extract(target);
|
|
12209
12391
|
}
|
|
12210
|
-
async function
|
|
12211
|
-
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
|
|
12392
|
+
async function handleLinkedinExtract(type, args, ctx) {
|
|
12393
|
+
const adapter = ctx.registry.get("harvest");
|
|
12394
|
+
if (!adapter) {
|
|
12395
|
+
throw Object.assign(
|
|
12396
|
+
new Error(
|
|
12397
|
+
"HarvestAPI provider not configured. Add harvest.api_key to your config.yaml or set GTM_HARVEST_API_KEY environment variable."
|
|
12398
|
+
),
|
|
12399
|
+
{ statusCode: 503 }
|
|
12400
|
+
);
|
|
12401
|
+
}
|
|
12402
|
+
const detail = args.detail ?? "summary";
|
|
12403
|
+
const isSummary = detail !== "full";
|
|
12404
|
+
switch (type) {
|
|
12405
|
+
case "linkedin_profile": {
|
|
12406
|
+
const url = args.linkedin_url || args.url;
|
|
12407
|
+
const profile = await adapter.getProfile(url, args.find_email);
|
|
12408
|
+
return {
|
|
12409
|
+
type: "linkedin_profile",
|
|
12410
|
+
provider: "harvest",
|
|
12411
|
+
profile: isSummary ? slimProfile(profile) : profile
|
|
12412
|
+
};
|
|
12413
|
+
}
|
|
12414
|
+
case "linkedin_posts": {
|
|
12415
|
+
const result = await adapter.getProfilePosts({
|
|
12416
|
+
profile: args.profile,
|
|
12417
|
+
postedLimit: args.posted_within
|
|
12418
|
+
});
|
|
12419
|
+
return {
|
|
12420
|
+
type: "linkedin_posts",
|
|
12421
|
+
provider: "harvest",
|
|
12422
|
+
count: result.elements.length,
|
|
12423
|
+
posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
|
|
12424
|
+
...detail === "full" && { pagination: result.pagination }
|
|
12425
|
+
};
|
|
12426
|
+
}
|
|
12427
|
+
case "linkedin_company_posts": {
|
|
12428
|
+
const result = await adapter.getCompanyPosts({
|
|
12429
|
+
company: args.company,
|
|
12430
|
+
postedLimit: args.posted_within
|
|
12431
|
+
});
|
|
12432
|
+
return {
|
|
12433
|
+
type: "linkedin_company_posts",
|
|
12434
|
+
provider: "harvest",
|
|
12435
|
+
count: result.elements.length,
|
|
12436
|
+
posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
|
|
12437
|
+
...detail === "full" && { pagination: result.pagination }
|
|
12438
|
+
};
|
|
12439
|
+
}
|
|
12440
|
+
case "linkedin_search": {
|
|
12441
|
+
const result = await adapter.searchPosts(args.query, {
|
|
12442
|
+
sortBy: args.sort,
|
|
12443
|
+
postedLimit: args.posted_within
|
|
12444
|
+
});
|
|
12445
|
+
return {
|
|
12446
|
+
type: "linkedin_search",
|
|
12447
|
+
provider: "harvest",
|
|
12448
|
+
count: result.elements.length,
|
|
12449
|
+
posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
|
|
12450
|
+
...detail === "full" && { pagination: result.pagination }
|
|
12451
|
+
};
|
|
12452
|
+
}
|
|
12453
|
+
case "linkedin_engagement": {
|
|
12454
|
+
const result = await adapter.getPostEngagement(
|
|
12455
|
+
args.post_url,
|
|
12456
|
+
args.engagement_type ?? "both"
|
|
12457
|
+
);
|
|
12458
|
+
if (isSummary) {
|
|
12459
|
+
return {
|
|
12460
|
+
type: "linkedin_engagement",
|
|
12461
|
+
provider: "harvest",
|
|
12462
|
+
comments_count: result.comments.length,
|
|
12463
|
+
reactions_count: result.reactions.length
|
|
12464
|
+
};
|
|
12465
|
+
}
|
|
12466
|
+
return {
|
|
12467
|
+
type: "linkedin_engagement",
|
|
12468
|
+
provider: "harvest",
|
|
12469
|
+
comments_count: result.comments.length,
|
|
12470
|
+
reactions_count: result.reactions.length,
|
|
12471
|
+
comments: result.comments,
|
|
12472
|
+
reactions: result.reactions
|
|
12473
|
+
};
|
|
12474
|
+
}
|
|
12475
|
+
default:
|
|
12476
|
+
throw new Error(`Unknown extract type: ${type}`);
|
|
12218
12477
|
}
|
|
12219
|
-
throw new Error(
|
|
12220
|
-
'LinkedIn post extraction is not supported by harvest. Use gtm_linkedin with action "posts" instead.'
|
|
12221
|
-
);
|
|
12222
12478
|
}
|
|
12479
|
+
var POST_TEXT_LIMIT;
|
|
12223
12480
|
var init_extract2 = __esm({
|
|
12224
12481
|
"src/handlers/extract.ts"() {
|
|
12225
12482
|
"use strict";
|
|
12226
12483
|
init_errors();
|
|
12484
|
+
POST_TEXT_LIMIT = 200;
|
|
12227
12485
|
}
|
|
12228
12486
|
});
|
|
12229
12487
|
|
|
@@ -12273,11 +12531,19 @@ async function handleFetch(args) {
|
|
|
12273
12531
|
} else {
|
|
12274
12532
|
responseBody = await response.text();
|
|
12275
12533
|
}
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
|
|
12280
|
-
|
|
12534
|
+
const detail = args.detail ?? "summary";
|
|
12535
|
+
if (detail === "full") {
|
|
12536
|
+
return {
|
|
12537
|
+
status: response.status,
|
|
12538
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
12539
|
+
body: responseBody
|
|
12540
|
+
};
|
|
12541
|
+
}
|
|
12542
|
+
let slimBody = responseBody;
|
|
12543
|
+
if (typeof responseBody === "string" && responseBody.length > BODY_TRUNCATE_LIMIT) {
|
|
12544
|
+
slimBody = responseBody.slice(0, BODY_TRUNCATE_LIMIT) + `... (truncated, ${responseBody.length} chars total)`;
|
|
12545
|
+
}
|
|
12546
|
+
return { status: response.status, body: slimBody };
|
|
12281
12547
|
} catch (error) {
|
|
12282
12548
|
logError("handler:gtm_fetch", error instanceof Error ? error : new Error(String(error)), {
|
|
12283
12549
|
url: fullUrl,
|
|
@@ -12288,7 +12554,7 @@ async function handleFetch(args) {
|
|
|
12288
12554
|
clearTimeout(timeout);
|
|
12289
12555
|
}
|
|
12290
12556
|
}
|
|
12291
|
-
var BLOCKED_PATTERNS, BLOCKED_HOSTS;
|
|
12557
|
+
var BLOCKED_PATTERNS, BLOCKED_HOSTS, BODY_TRUNCATE_LIMIT;
|
|
12292
12558
|
var init_fetch2 = __esm({
|
|
12293
12559
|
"src/handlers/fetch.ts"() {
|
|
12294
12560
|
"use strict";
|
|
@@ -12305,6 +12571,7 @@ var init_fetch2 = __esm({
|
|
|
12305
12571
|
/^fe80:/
|
|
12306
12572
|
];
|
|
12307
12573
|
BLOCKED_HOSTS = ["localhost", "0.0.0.0", "[::1]"];
|
|
12574
|
+
BODY_TRUNCATE_LIMIT = 2e3;
|
|
12308
12575
|
}
|
|
12309
12576
|
});
|
|
12310
12577
|
|
|
@@ -12426,36 +12693,95 @@ var campaign_exports = {};
|
|
|
12426
12693
|
__export(campaign_exports, {
|
|
12427
12694
|
handleCampaign: () => handleCampaign
|
|
12428
12695
|
});
|
|
12429
|
-
|
|
12430
|
-
const action = args.action;
|
|
12696
|
+
function resolveChannel(args, ctx) {
|
|
12431
12697
|
const providerName = args.provider || void 0;
|
|
12432
|
-
|
|
12433
|
-
|
|
12698
|
+
if (providerName) {
|
|
12699
|
+
const provider = ctx.registry.get(providerName);
|
|
12700
|
+
if (!provider) {
|
|
12701
|
+
throw createGtmError(
|
|
12702
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
12703
|
+
`Provider "${providerName}" not found. Check your config or use action: "list" without a provider to discover available campaigns.`
|
|
12704
|
+
);
|
|
12705
|
+
}
|
|
12706
|
+
if (provider.capabilities.includes("outreach_linkedin")) {
|
|
12707
|
+
return {
|
|
12708
|
+
type: "linkedin",
|
|
12709
|
+
manager: provider,
|
|
12710
|
+
providerName: provider.name
|
|
12711
|
+
};
|
|
12712
|
+
}
|
|
12713
|
+
if (provider.capabilities.includes("campaign_email")) {
|
|
12714
|
+
return {
|
|
12715
|
+
type: "email",
|
|
12716
|
+
manager: provider,
|
|
12717
|
+
providerName: provider.name
|
|
12718
|
+
};
|
|
12719
|
+
}
|
|
12434
12720
|
throw createGtmError(
|
|
12435
12721
|
"PROVIDER_NOT_CONFIGURED",
|
|
12436
|
-
"
|
|
12722
|
+
`Provider "${providerName}" does not support campaign_email or outreach_linkedin.`
|
|
12437
12723
|
);
|
|
12438
12724
|
}
|
|
12439
|
-
const
|
|
12440
|
-
|
|
12725
|
+
const emailProviders = ctx.registry.getForCapability("campaign_email");
|
|
12726
|
+
if (emailProviders.length > 0) {
|
|
12727
|
+
const provider = emailProviders[0];
|
|
12728
|
+
return {
|
|
12729
|
+
type: "email",
|
|
12730
|
+
manager: provider,
|
|
12731
|
+
providerName: provider.name
|
|
12732
|
+
};
|
|
12733
|
+
}
|
|
12734
|
+
const linkedinProviders = ctx.registry.getForCapability("outreach_linkedin");
|
|
12735
|
+
if (linkedinProviders.length > 0) {
|
|
12736
|
+
const provider = linkedinProviders[0];
|
|
12737
|
+
return {
|
|
12738
|
+
type: "linkedin",
|
|
12739
|
+
manager: provider,
|
|
12740
|
+
providerName: provider.name
|
|
12741
|
+
};
|
|
12742
|
+
}
|
|
12743
|
+
throw createGtmError(
|
|
12744
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
12745
|
+
"No providers configured for campaigns. Add a provider with campaign_email or outreach_linkedin capability."
|
|
12746
|
+
);
|
|
12747
|
+
}
|
|
12748
|
+
async function handleCampaign(args, ctx) {
|
|
12749
|
+
const action = args.action;
|
|
12750
|
+
const channel = resolveChannel(args, ctx);
|
|
12441
12751
|
switch (action) {
|
|
12442
12752
|
case "list":
|
|
12443
|
-
return { provider:
|
|
12753
|
+
return { provider: channel.providerName, campaigns: await channel.manager.listCampaigns() };
|
|
12444
12754
|
case "stats": {
|
|
12445
12755
|
const campaignId = args.campaign_id;
|
|
12446
12756
|
if (!campaignId)
|
|
12447
12757
|
throw createGtmError("QUERY_INVALID", "campaign_id is required for stats action");
|
|
12448
|
-
return manager.getCampaignStats(campaignId);
|
|
12758
|
+
return channel.manager.getCampaignStats(campaignId);
|
|
12449
12759
|
}
|
|
12450
12760
|
case "add_leads": {
|
|
12451
12761
|
const campaignId = args.campaign_id;
|
|
12452
|
-
const
|
|
12762
|
+
const rawLeads = args.leads;
|
|
12453
12763
|
if (!campaignId)
|
|
12454
12764
|
throw createGtmError("QUERY_INVALID", "campaign_id is required for add_leads action");
|
|
12455
|
-
if (!
|
|
12765
|
+
if (!rawLeads?.length)
|
|
12456
12766
|
throw createGtmError("QUERY_INVALID", "leads array is required for add_leads action");
|
|
12767
|
+
if (channel.type === "linkedin") {
|
|
12768
|
+
const leads2 = rawLeads.map((l) => ({
|
|
12769
|
+
linkedin_url: l.linkedin_url,
|
|
12770
|
+
first_name: l.first_name,
|
|
12771
|
+
last_name: l.last_name,
|
|
12772
|
+
company: l.company
|
|
12773
|
+
}));
|
|
12774
|
+
return channel.manager.addLeadsToCampaign(campaignId, leads2);
|
|
12775
|
+
}
|
|
12776
|
+
const leads = rawLeads.map((l) => ({
|
|
12777
|
+
email: l.email,
|
|
12778
|
+
first_name: l.first_name,
|
|
12779
|
+
last_name: l.last_name,
|
|
12780
|
+
company: l.company,
|
|
12781
|
+
custom_fields: l.custom_fields
|
|
12782
|
+
}));
|
|
12457
12783
|
try {
|
|
12458
|
-
return await manager.addLeadsToCampaign(campaignId, leads);
|
|
12784
|
+
return await channel.manager.addLeadsToCampaign(campaignId, leads);
|
|
12459
12785
|
} catch (error) {
|
|
12460
12786
|
if (error instanceof ApiClientError && (error.message.includes("Validation error") || error.statusCode === 400 || error.statusCode === 422)) {
|
|
12461
12787
|
throw createGtmError(
|
|
@@ -12468,15 +12794,21 @@ async function handleCampaign(args, ctx) {
|
|
|
12468
12794
|
}
|
|
12469
12795
|
}
|
|
12470
12796
|
case "create": {
|
|
12797
|
+
if (channel.type === "linkedin") {
|
|
12798
|
+
throw createGtmError(
|
|
12799
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
12800
|
+
"LinkedIn outreach providers do not support campaign creation via API. Create campaigns directly in the provider dashboard."
|
|
12801
|
+
);
|
|
12802
|
+
}
|
|
12471
12803
|
const name = args.name;
|
|
12472
12804
|
if (!name) throw createGtmError("QUERY_INVALID", "name is required for create action");
|
|
12473
|
-
if (!manager.createCampaign) {
|
|
12805
|
+
if (!channel.manager.createCampaign) {
|
|
12474
12806
|
throw createGtmError(
|
|
12475
12807
|
"PROVIDER_NOT_CONFIGURED",
|
|
12476
|
-
`Provider "${
|
|
12808
|
+
`Provider "${channel.providerName}" does not support campaign creation. Currently only PlusVibe supports this action.`
|
|
12477
12809
|
);
|
|
12478
12810
|
}
|
|
12479
|
-
return manager.createCampaign(name);
|
|
12811
|
+
return channel.manager.createCampaign(name);
|
|
12480
12812
|
}
|
|
12481
12813
|
default:
|
|
12482
12814
|
throw createGtmError("QUERY_INVALID", `Unknown campaign action: ${action}`);
|
|
@@ -12490,47 +12822,6 @@ var init_campaign2 = __esm({
|
|
|
12490
12822
|
}
|
|
12491
12823
|
});
|
|
12492
12824
|
|
|
12493
|
-
// src/handlers/outreach.ts
|
|
12494
|
-
var outreach_exports = {};
|
|
12495
|
-
__export(outreach_exports, {
|
|
12496
|
-
handleOutreach: () => handleOutreach
|
|
12497
|
-
});
|
|
12498
|
-
async function handleOutreach(args, ctx) {
|
|
12499
|
-
const action = args.action;
|
|
12500
|
-
const providerName = args.provider || void 0;
|
|
12501
|
-
const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability("outreach_linkedin");
|
|
12502
|
-
if (providers.length === 0) {
|
|
12503
|
-
throw new Error(
|
|
12504
|
-
"No providers configured for LinkedIn outreach. Add a provider with outreach_linkedin capability."
|
|
12505
|
-
);
|
|
12506
|
-
}
|
|
12507
|
-
const provider = providers[0];
|
|
12508
|
-
const manager = provider;
|
|
12509
|
-
switch (action) {
|
|
12510
|
-
case "list":
|
|
12511
|
-
return { provider: provider.name, campaigns: await manager.listCampaigns() };
|
|
12512
|
-
case "stats": {
|
|
12513
|
-
const campaignId = args.campaign_id;
|
|
12514
|
-
if (!campaignId) throw new Error("campaign_id is required for stats action");
|
|
12515
|
-
return manager.getCampaignStats(campaignId);
|
|
12516
|
-
}
|
|
12517
|
-
case "add_leads": {
|
|
12518
|
-
const campaignId = args.campaign_id;
|
|
12519
|
-
const leads = args.leads;
|
|
12520
|
-
if (!campaignId) throw new Error("campaign_id is required for add_leads action");
|
|
12521
|
-
if (!leads?.length) throw new Error("leads array is required for add_leads action");
|
|
12522
|
-
return manager.addLeadsToCampaign(campaignId, leads);
|
|
12523
|
-
}
|
|
12524
|
-
default:
|
|
12525
|
-
throw new Error(`Unknown outreach action: ${action}`);
|
|
12526
|
-
}
|
|
12527
|
-
}
|
|
12528
|
-
var init_outreach2 = __esm({
|
|
12529
|
-
"src/handlers/outreach.ts"() {
|
|
12530
|
-
"use strict";
|
|
12531
|
-
}
|
|
12532
|
-
});
|
|
12533
|
-
|
|
12534
12825
|
// src/handlers/crm.ts
|
|
12535
12826
|
var crm_exports = {};
|
|
12536
12827
|
__export(crm_exports, {
|
|
@@ -12631,6 +12922,17 @@ async function handleStatus(args, ctx) {
|
|
|
12631
12922
|
})
|
|
12632
12923
|
);
|
|
12633
12924
|
const client = ctx.session.getActiveContext();
|
|
12925
|
+
const detail = args.detail ?? "summary";
|
|
12926
|
+
if (detail !== "full") {
|
|
12927
|
+
return {
|
|
12928
|
+
client: client.clientName,
|
|
12929
|
+
providers: results.map(({ provider, healthy, credits }) => ({
|
|
12930
|
+
provider,
|
|
12931
|
+
healthy,
|
|
12932
|
+
...credits && { credits }
|
|
12933
|
+
}))
|
|
12934
|
+
};
|
|
12935
|
+
}
|
|
12634
12936
|
const configuredCapabilities = new Set(results.flatMap((r) => r.capabilities));
|
|
12635
12937
|
const unconfigured = TOOL_CAPABILITY_REQUIREMENTS.filter(
|
|
12636
12938
|
(req) => !configuredCapabilities.has(req.capability)
|
|
@@ -12649,7 +12951,7 @@ var init_status2 = __esm({
|
|
|
12649
12951
|
init_dist();
|
|
12650
12952
|
TOOL_CAPABILITY_REQUIREMENTS = [
|
|
12651
12953
|
{
|
|
12652
|
-
tool: "
|
|
12954
|
+
tool: "gtm_extract",
|
|
12653
12955
|
requires: "harvest",
|
|
12654
12956
|
capability: "extract_linkedin",
|
|
12655
12957
|
setup: "Set GTM_HARVEST_API_KEY or add harvest.api_key to config.yaml"
|
|
@@ -12679,10 +12981,10 @@ var init_status2 = __esm({
|
|
|
12679
12981
|
setup: "Set GTM_PLUSVIBE_API_KEY, GTM_INSTANTLY_API_KEY, or GTM_SMARTLEAD_API_KEY"
|
|
12680
12982
|
},
|
|
12681
12983
|
{
|
|
12682
|
-
tool: "
|
|
12984
|
+
tool: "gtm_campaign",
|
|
12683
12985
|
requires: "heyreach",
|
|
12684
12986
|
capability: "outreach_linkedin",
|
|
12685
|
-
setup: "Set GTM_HEYREACH_API_KEY"
|
|
12987
|
+
setup: "Set GTM_HEYREACH_API_KEY (LinkedIn outreach via gtm_campaign)"
|
|
12686
12988
|
},
|
|
12687
12989
|
{ tool: "gtm_crm", requires: "attio", capability: "crm_read", setup: "Set GTM_ATTIO_API_KEY" },
|
|
12688
12990
|
{
|
|
@@ -12753,90 +13055,59 @@ var init_configure2 = __esm({
|
|
|
12753
13055
|
}
|
|
12754
13056
|
});
|
|
12755
13057
|
|
|
12756
|
-
// src/handlers/
|
|
12757
|
-
var
|
|
12758
|
-
__export(
|
|
12759
|
-
|
|
13058
|
+
// src/handlers/feedback.ts
|
|
13059
|
+
var feedback_exports = {};
|
|
13060
|
+
__export(feedback_exports, {
|
|
13061
|
+
handleFeedback: () => handleFeedback
|
|
12760
13062
|
});
|
|
12761
|
-
async function
|
|
12762
|
-
const
|
|
12763
|
-
if (!
|
|
12764
|
-
throw
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
),
|
|
12768
|
-
{ statusCode: 503 }
|
|
13063
|
+
async function handleFeedback(args) {
|
|
13064
|
+
const input = args;
|
|
13065
|
+
if (!SUPABASE_KEY) {
|
|
13066
|
+
throw createGtmError(
|
|
13067
|
+
"PROVIDER_NOT_CONFIGURED",
|
|
13068
|
+
"Supabase not configured for feedback. Set GTM_SUPABASE_KEY or SUPABASE_SERVICE_ROLE_KEY env var."
|
|
12769
13069
|
);
|
|
12770
13070
|
}
|
|
12771
|
-
const
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
company: args.company,
|
|
12796
|
-
postedLimit: args.posted_within
|
|
12797
|
-
});
|
|
12798
|
-
return {
|
|
12799
|
-
action: "company_posts",
|
|
12800
|
-
provider: "harvest",
|
|
12801
|
-
count: result.elements.length,
|
|
12802
|
-
posts: result.elements,
|
|
12803
|
-
pagination: result.pagination
|
|
12804
|
-
};
|
|
12805
|
-
}
|
|
12806
|
-
case "search_posts": {
|
|
12807
|
-
const result = await adapter.searchPosts(args.query, {
|
|
12808
|
-
sortBy: args.sort,
|
|
12809
|
-
postedLimit: args.posted_within
|
|
12810
|
-
});
|
|
12811
|
-
return {
|
|
12812
|
-
action: "search_posts",
|
|
12813
|
-
provider: "harvest",
|
|
12814
|
-
count: result.elements.length,
|
|
12815
|
-
posts: result.elements,
|
|
12816
|
-
pagination: result.pagination
|
|
12817
|
-
};
|
|
12818
|
-
}
|
|
12819
|
-
case "engagement": {
|
|
12820
|
-
const result = await adapter.getPostEngagement(
|
|
12821
|
-
args.post_url,
|
|
12822
|
-
args.engagement_type ?? "both"
|
|
12823
|
-
);
|
|
12824
|
-
return {
|
|
12825
|
-
action: "engagement",
|
|
12826
|
-
provider: "harvest",
|
|
12827
|
-
comments_count: result.comments.length,
|
|
12828
|
-
reactions_count: result.reactions.length,
|
|
12829
|
-
comments: result.comments,
|
|
12830
|
-
reactions: result.reactions
|
|
12831
|
-
};
|
|
12832
|
-
}
|
|
12833
|
-
default:
|
|
12834
|
-
throw new Error(`Unknown linkedin action: ${action}`);
|
|
13071
|
+
const row = {
|
|
13072
|
+
type: input.type,
|
|
13073
|
+
message: input.message.trim(),
|
|
13074
|
+
context: input.context ?? {}
|
|
13075
|
+
};
|
|
13076
|
+
const response = await fetch(`${SUPABASE_URL}/rest/v1/gtm_feedback`, {
|
|
13077
|
+
method: "POST",
|
|
13078
|
+
headers: {
|
|
13079
|
+
"Content-Type": "application/json",
|
|
13080
|
+
apikey: SUPABASE_KEY,
|
|
13081
|
+
Authorization: `Bearer ${SUPABASE_KEY}`,
|
|
13082
|
+
Prefer: "return=representation"
|
|
13083
|
+
},
|
|
13084
|
+
body: JSON.stringify(row)
|
|
13085
|
+
});
|
|
13086
|
+
if (!response.ok) {
|
|
13087
|
+
const body = await response.text().catch(() => "unknown");
|
|
13088
|
+
throw createGtmError(
|
|
13089
|
+
"PROVIDER_REQUEST_FAILED",
|
|
13090
|
+
`Failed to submit feedback: ${response.status}`,
|
|
13091
|
+
{
|
|
13092
|
+
details: { status: response.status, body }
|
|
13093
|
+
}
|
|
13094
|
+
);
|
|
12835
13095
|
}
|
|
13096
|
+
const [created] = await response.json();
|
|
13097
|
+
return {
|
|
13098
|
+
submitted: true,
|
|
13099
|
+
id: created?.id,
|
|
13100
|
+
type: input.type,
|
|
13101
|
+
message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
|
|
13102
|
+
};
|
|
12836
13103
|
}
|
|
12837
|
-
var
|
|
12838
|
-
|
|
13104
|
+
var SUPABASE_URL, SUPABASE_KEY;
|
|
13105
|
+
var init_feedback2 = __esm({
|
|
13106
|
+
"src/handlers/feedback.ts"() {
|
|
12839
13107
|
"use strict";
|
|
13108
|
+
init_errors();
|
|
13109
|
+
SUPABASE_URL = process.env.GTM_SUPABASE_URL ?? process.env.SUPABASE_URL ?? "https://qvawbxpijxlwdkolmjrs.supabase.co";
|
|
13110
|
+
SUPABASE_KEY = process.env.GTM_SUPABASE_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
|
|
12840
13111
|
}
|
|
12841
13112
|
});
|
|
12842
13113
|
|
|
@@ -12850,13 +13121,34 @@ function isRateLimitError2(error) {
|
|
|
12850
13121
|
async function sleep(ms) {
|
|
12851
13122
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12852
13123
|
}
|
|
13124
|
+
function withHandlerTimeout(promise, ms) {
|
|
13125
|
+
return new Promise((resolve, reject) => {
|
|
13126
|
+
const timer = setTimeout(
|
|
13127
|
+
() => reject(
|
|
13128
|
+
Object.assign(new Error(`Tool call timed out after ${ms}ms`), { code: "HANDLER_TIMEOUT" })
|
|
13129
|
+
),
|
|
13130
|
+
ms
|
|
13131
|
+
);
|
|
13132
|
+
promise.then(
|
|
13133
|
+
(val) => {
|
|
13134
|
+
clearTimeout(timer);
|
|
13135
|
+
resolve(val);
|
|
13136
|
+
},
|
|
13137
|
+
(err) => {
|
|
13138
|
+
clearTimeout(timer);
|
|
13139
|
+
reject(err);
|
|
13140
|
+
}
|
|
13141
|
+
);
|
|
13142
|
+
});
|
|
13143
|
+
}
|
|
12853
13144
|
async function handleToolCall(name, args, ctx) {
|
|
12854
13145
|
let lastError;
|
|
12855
13146
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
12856
13147
|
try {
|
|
12857
|
-
const
|
|
13148
|
+
const timeoutMs = args._testTimeoutMs ?? HANDLER_TIMEOUT_MS;
|
|
13149
|
+
const result = await withHandlerTimeout(dispatch(name, args, ctx), timeoutMs);
|
|
12858
13150
|
return {
|
|
12859
|
-
content: [{ type: "text", text: JSON.stringify(result
|
|
13151
|
+
content: [{ type: "text", text: JSON.stringify(result) ?? "{}" }]
|
|
12860
13152
|
};
|
|
12861
13153
|
} catch (error) {
|
|
12862
13154
|
lastError = error;
|
|
@@ -12868,6 +13160,13 @@ async function handleToolCall(name, args, ctx) {
|
|
|
12868
13160
|
break;
|
|
12869
13161
|
}
|
|
12870
13162
|
}
|
|
13163
|
+
if (lastError instanceof Error && "code" in lastError && lastError.code === "HANDLER_TIMEOUT") {
|
|
13164
|
+
const wrapped2 = createGtmError("HANDLER_TIMEOUT", lastError.message);
|
|
13165
|
+
return {
|
|
13166
|
+
content: [{ type: "text", text: JSON.stringify(wrapped2) }],
|
|
13167
|
+
isError: true
|
|
13168
|
+
};
|
|
13169
|
+
}
|
|
12871
13170
|
if (isGtmError(lastError)) {
|
|
12872
13171
|
return {
|
|
12873
13172
|
content: [{ type: "text", text: JSON.stringify(lastError) }],
|
|
@@ -12915,10 +13214,6 @@ async function dispatch(name, args, ctx) {
|
|
|
12915
13214
|
const { handleCampaign: handleCampaign2 } = await Promise.resolve().then(() => (init_campaign2(), campaign_exports));
|
|
12916
13215
|
return handleCampaign2(args, ctx);
|
|
12917
13216
|
}
|
|
12918
|
-
case "gtm_outreach": {
|
|
12919
|
-
const { handleOutreach: handleOutreach2 } = await Promise.resolve().then(() => (init_outreach2(), outreach_exports));
|
|
12920
|
-
return handleOutreach2(args, ctx);
|
|
12921
|
-
}
|
|
12922
13217
|
case "gtm_crm": {
|
|
12923
13218
|
const { handleCrm: handleCrm2 } = await Promise.resolve().then(() => (init_crm2(), crm_exports));
|
|
12924
13219
|
return handleCrm2(args, ctx);
|
|
@@ -12931,9 +13226,9 @@ async function dispatch(name, args, ctx) {
|
|
|
12931
13226
|
const { handleConfigure: handleConfigure2 } = await Promise.resolve().then(() => (init_configure2(), configure_exports));
|
|
12932
13227
|
return handleConfigure2(args, ctx);
|
|
12933
13228
|
}
|
|
12934
|
-
case "
|
|
12935
|
-
const {
|
|
12936
|
-
return
|
|
13229
|
+
case "gtm_feedback": {
|
|
13230
|
+
const { handleFeedback: handleFeedback2 } = await Promise.resolve().then(() => (init_feedback2(), feedback_exports));
|
|
13231
|
+
return handleFeedback2(args);
|
|
12937
13232
|
}
|
|
12938
13233
|
// ─── Meta-Tools ───────────────────────────────────────
|
|
12939
13234
|
case "gtm_guide": {
|
|
@@ -12949,7 +13244,7 @@ async function dispatch(name, args, ctx) {
|
|
|
12949
13244
|
}
|
|
12950
13245
|
const result = {
|
|
12951
13246
|
name: tool.name,
|
|
12952
|
-
description: tool.description,
|
|
13247
|
+
description: TOOL_DESCRIPTIONS[tool.name] ?? tool.description,
|
|
12953
13248
|
inputSchema: tool.inputSchema
|
|
12954
13249
|
};
|
|
12955
13250
|
const toolKey = toolName.replace("gtm_", "");
|
|
@@ -12972,7 +13267,7 @@ async function dispatch(name, args, ctx) {
|
|
|
12972
13267
|
throw new Error(`Unknown tool: ${name}`);
|
|
12973
13268
|
}
|
|
12974
13269
|
}
|
|
12975
|
-
var MAX_RETRIES, BASE_DELAY_MS;
|
|
13270
|
+
var MAX_RETRIES, BASE_DELAY_MS, HANDLER_TIMEOUT_MS;
|
|
12976
13271
|
var init_handlers = __esm({
|
|
12977
13272
|
"src/handlers/index.ts"() {
|
|
12978
13273
|
"use strict";
|
|
@@ -12980,8 +13275,10 @@ var init_handlers = __esm({
|
|
|
12980
13275
|
init_errors();
|
|
12981
13276
|
init_manifests();
|
|
12982
13277
|
init_tools();
|
|
13278
|
+
init_descriptions();
|
|
12983
13279
|
MAX_RETRIES = 3;
|
|
12984
13280
|
BASE_DELAY_MS = 1e3;
|
|
13281
|
+
HANDLER_TIMEOUT_MS = 12e4;
|
|
12985
13282
|
}
|
|
12986
13283
|
});
|
|
12987
13284
|
|
|
@@ -13002,7 +13299,7 @@ function validateToolArgs(toolName, args) {
|
|
|
13002
13299
|
}
|
|
13003
13300
|
return { success: true, data: result.data };
|
|
13004
13301
|
}
|
|
13005
|
-
var contactInputSchema, campaignLeadSchema,
|
|
13302
|
+
var contactInputSchema, campaignLeadSchema, detailSchema, toolSchemas, TOOL_NAMES;
|
|
13006
13303
|
var init_validation = __esm({
|
|
13007
13304
|
"src/validation.ts"() {
|
|
13008
13305
|
"use strict";
|
|
@@ -13014,18 +13311,16 @@ var init_validation = __esm({
|
|
|
13014
13311
|
linkedin_url: z2.string().optional()
|
|
13015
13312
|
});
|
|
13016
13313
|
campaignLeadSchema = z2.object({
|
|
13017
|
-
email: z2.string().email("email must be a valid email address"),
|
|
13314
|
+
email: z2.string().email("email must be a valid email address").optional(),
|
|
13315
|
+
linkedin_url: z2.string().min(1, "linkedin_url is required").optional(),
|
|
13018
13316
|
first_name: z2.string().optional(),
|
|
13019
13317
|
last_name: z2.string().optional(),
|
|
13020
13318
|
company: z2.string().optional(),
|
|
13021
13319
|
custom_fields: z2.record(z2.string()).optional()
|
|
13320
|
+
}).refine((data) => !!data.email || !!data.linkedin_url, {
|
|
13321
|
+
message: "At least one of email or linkedin_url is required"
|
|
13022
13322
|
});
|
|
13023
|
-
|
|
13024
|
-
linkedin_url: z2.string().min(1, "linkedin_url is required"),
|
|
13025
|
-
first_name: z2.string().optional(),
|
|
13026
|
-
last_name: z2.string().optional(),
|
|
13027
|
-
company: z2.string().optional()
|
|
13028
|
-
});
|
|
13323
|
+
detailSchema = z2.enum(["summary", "full"]).optional();
|
|
13029
13324
|
toolSchemas = {
|
|
13030
13325
|
// 1. Company discovery
|
|
13031
13326
|
gtm_find_companies: z2.object({
|
|
@@ -13049,7 +13344,8 @@ var init_validation = __esm({
|
|
|
13049
13344
|
platform: z2.string().optional(),
|
|
13050
13345
|
min_revenue: z2.number().optional(),
|
|
13051
13346
|
max_revenue: z2.number().optional(),
|
|
13052
|
-
output: z2.enum(["file", "inline"]).optional()
|
|
13347
|
+
output: z2.enum(["file", "inline"]).optional(),
|
|
13348
|
+
detail: detailSchema
|
|
13053
13349
|
}),
|
|
13054
13350
|
// 2. People/contact search
|
|
13055
13351
|
gtm_find_people: z2.object({
|
|
@@ -13089,16 +13385,43 @@ var init_validation = __esm({
|
|
|
13089
13385
|
max_employees: z2.number().int().min(1).optional(),
|
|
13090
13386
|
revenue_range: z2.string().optional(),
|
|
13091
13387
|
fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
|
|
13092
|
-
output: z2.enum(["file", "inline"]).optional()
|
|
13388
|
+
output: z2.enum(["file", "inline"]).optional(),
|
|
13389
|
+
detail: detailSchema
|
|
13093
13390
|
}).refine(
|
|
13094
13391
|
(data) => !!(data.job_title || data.company_name || data.company_domain || data.company_domains && data.company_domains.length > 0 || data.seniority && data.seniority.length > 0 || data.filter_industry && data.filter_industry.length > 0 || data.skills && data.skills.length > 0 || data.location),
|
|
13095
13392
|
{
|
|
13096
13393
|
message: "At least one search filter required (job_title, company_name, company_domain, company_domains, seniority, filter_industry, skills, or location)"
|
|
13097
13394
|
}
|
|
13098
13395
|
),
|
|
13099
|
-
//
|
|
13100
|
-
|
|
13101
|
-
|
|
13396
|
+
// 3. Enrichment waterfall
|
|
13397
|
+
gtm_enrich: z2.object({
|
|
13398
|
+
contacts: z2.array(contactInputSchema).min(1, "contacts must contain at least 1 contact").max(100, "contacts must contain at most 100 contacts"),
|
|
13399
|
+
find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
|
|
13400
|
+
waterfall: z2.array(z2.string()).optional(),
|
|
13401
|
+
verify: z2.boolean().optional(),
|
|
13402
|
+
dry_run: z2.boolean().optional(),
|
|
13403
|
+
detail: detailSchema
|
|
13404
|
+
}),
|
|
13405
|
+
// 4. Email verification
|
|
13406
|
+
gtm_verify: z2.object({
|
|
13407
|
+
email: z2.string().email("email must be a valid email address")
|
|
13408
|
+
}),
|
|
13409
|
+
// 5. Extract/scrape — web pages + all LinkedIn intelligence
|
|
13410
|
+
gtm_extract: z2.object({
|
|
13411
|
+
type: z2.enum(
|
|
13412
|
+
[
|
|
13413
|
+
"linkedin_profile",
|
|
13414
|
+
"linkedin_posts",
|
|
13415
|
+
"linkedin_engagement",
|
|
13416
|
+
"linkedin_search",
|
|
13417
|
+
"linkedin_company_posts",
|
|
13418
|
+
"web_page"
|
|
13419
|
+
],
|
|
13420
|
+
{
|
|
13421
|
+
message: "type must be one of: linkedin_profile, linkedin_posts, linkedin_engagement, linkedin_search, linkedin_company_posts, web_page"
|
|
13422
|
+
}
|
|
13423
|
+
),
|
|
13424
|
+
url: z2.string().url("url must be a valid URL").optional(),
|
|
13102
13425
|
linkedin_url: z2.string().optional(),
|
|
13103
13426
|
find_email: z2.boolean().optional(),
|
|
13104
13427
|
profile: z2.string().optional(),
|
|
@@ -13108,53 +13431,38 @@ var init_validation = __esm({
|
|
|
13108
13431
|
engagement_type: z2.enum(["comments", "reactions", "both"]).optional(),
|
|
13109
13432
|
query: z2.string().optional(),
|
|
13110
13433
|
sort: z2.enum(["relevance", "date"]).optional(),
|
|
13111
|
-
limit: z2.number().int().min(1).max(100).optional()
|
|
13434
|
+
limit: z2.number().int().min(1).max(100).optional(),
|
|
13435
|
+
fields: z2.array(z2.string()).optional(),
|
|
13436
|
+
detail: detailSchema
|
|
13112
13437
|
}).refine(
|
|
13113
13438
|
(data) => {
|
|
13114
|
-
switch (data.
|
|
13115
|
-
case "
|
|
13116
|
-
return !!data.linkedin_url;
|
|
13117
|
-
case "
|
|
13439
|
+
switch (data.type) {
|
|
13440
|
+
case "linkedin_profile":
|
|
13441
|
+
return !!(data.linkedin_url || data.url);
|
|
13442
|
+
case "linkedin_posts":
|
|
13443
|
+
return !!data.profile;
|
|
13444
|
+
case "linkedin_engagement":
|
|
13118
13445
|
return !!data.post_url;
|
|
13119
|
-
case "
|
|
13446
|
+
case "linkedin_search":
|
|
13120
13447
|
return !!data.query;
|
|
13121
|
-
case "
|
|
13122
|
-
return !!data.profile;
|
|
13123
|
-
case "company_posts":
|
|
13448
|
+
case "linkedin_company_posts":
|
|
13124
13449
|
return !!data.company;
|
|
13450
|
+
case "web_page":
|
|
13451
|
+
return !!data.url;
|
|
13125
13452
|
default:
|
|
13126
13453
|
return true;
|
|
13127
13454
|
}
|
|
13128
13455
|
},
|
|
13129
|
-
{ message: "Missing required field for the specified
|
|
13456
|
+
{ message: "Missing required field for the specified type" }
|
|
13130
13457
|
),
|
|
13131
|
-
// 3. Enrichment waterfall
|
|
13132
|
-
gtm_enrich: z2.object({
|
|
13133
|
-
contacts: z2.array(contactInputSchema).min(1, "contacts must contain at least 1 contact").max(100, "contacts must contain at most 100 contacts"),
|
|
13134
|
-
find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
|
|
13135
|
-
waterfall: z2.array(z2.string()).optional(),
|
|
13136
|
-
verify: z2.boolean().optional(),
|
|
13137
|
-
dry_run: z2.boolean().optional()
|
|
13138
|
-
}),
|
|
13139
|
-
// 4. Email verification
|
|
13140
|
-
gtm_verify: z2.object({
|
|
13141
|
-
email: z2.string().email("email must be a valid email address")
|
|
13142
|
-
}),
|
|
13143
|
-
// 5. Extract/scrape
|
|
13144
|
-
gtm_extract: z2.object({
|
|
13145
|
-
url: z2.string().url("url must be a valid URL"),
|
|
13146
|
-
type: z2.enum(["linkedin_profile", "linkedin_post", "web_page"], {
|
|
13147
|
-
message: "type must be one of: linkedin_profile, linkedin_post, web_page"
|
|
13148
|
-
}),
|
|
13149
|
-
fields: z2.array(z2.string()).optional()
|
|
13150
|
-
}),
|
|
13151
13458
|
// 6. Raw HTTP fetch
|
|
13152
13459
|
gtm_fetch: z2.object({
|
|
13153
13460
|
url: z2.string().url("url must be a valid URL"),
|
|
13154
13461
|
method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
|
|
13155
13462
|
headers: z2.record(z2.string()).optional(),
|
|
13156
13463
|
body: z2.record(z2.unknown()).optional(),
|
|
13157
|
-
params: z2.record(z2.string()).optional()
|
|
13464
|
+
params: z2.record(z2.string()).optional(),
|
|
13465
|
+
detail: detailSchema
|
|
13158
13466
|
}),
|
|
13159
13467
|
// 7. AI data operations
|
|
13160
13468
|
gtm_ai: z2.object({
|
|
@@ -13167,7 +13475,7 @@ var init_validation = __esm({
|
|
|
13167
13475
|
prompt: z2.string().optional(),
|
|
13168
13476
|
model: z2.string().optional()
|
|
13169
13477
|
}),
|
|
13170
|
-
// 8.
|
|
13478
|
+
// 8. Campaign management (cold email + LinkedIn outreach)
|
|
13171
13479
|
gtm_campaign: z2.object({
|
|
13172
13480
|
action: z2.enum(["list", "stats", "add_leads", "create"], {
|
|
13173
13481
|
message: "action must be one of: list, stats, add_leads, create"
|
|
@@ -13175,15 +13483,8 @@ var init_validation = __esm({
|
|
|
13175
13483
|
campaign_id: z2.string().optional(),
|
|
13176
13484
|
leads: z2.array(campaignLeadSchema).optional(),
|
|
13177
13485
|
name: z2.string().optional(),
|
|
13178
|
-
provider: z2.string().optional()
|
|
13179
|
-
|
|
13180
|
-
// 9. LinkedIn outreach management
|
|
13181
|
-
gtm_outreach: z2.object({
|
|
13182
|
-
action: z2.enum(["list", "stats", "add_leads"], {
|
|
13183
|
-
message: "action must be one of: list, stats, add_leads"
|
|
13184
|
-
}),
|
|
13185
|
-
campaign_id: z2.string().optional(),
|
|
13186
|
-
leads: z2.array(outreachLeadSchema).optional()
|
|
13486
|
+
provider: z2.string().optional(),
|
|
13487
|
+
detail: detailSchema
|
|
13187
13488
|
}),
|
|
13188
13489
|
// 10. CRM read/write
|
|
13189
13490
|
gtm_crm: z2.object({
|
|
@@ -13196,7 +13497,8 @@ var init_validation = __esm({
|
|
|
13196
13497
|
}),
|
|
13197
13498
|
// 11. Provider status/credits
|
|
13198
13499
|
gtm_status: z2.object({
|
|
13199
|
-
provider: z2.string().optional()
|
|
13500
|
+
provider: z2.string().optional(),
|
|
13501
|
+
detail: detailSchema
|
|
13200
13502
|
}),
|
|
13201
13503
|
// 12. Configuration management
|
|
13202
13504
|
gtm_configure: z2.object({
|
|
@@ -13229,6 +13531,14 @@ var init_validation = __esm({
|
|
|
13229
13531
|
gtm_execute: z2.object({
|
|
13230
13532
|
tool: z2.string().min(1, "tool is required"),
|
|
13231
13533
|
arguments: z2.record(z2.unknown()).optional()
|
|
13534
|
+
}),
|
|
13535
|
+
// 16. Feedback
|
|
13536
|
+
gtm_feedback: z2.object({
|
|
13537
|
+
type: z2.enum(["bug", "feature_request", "provider_request"], {
|
|
13538
|
+
message: "type must be one of: bug, feature_request, provider_request"
|
|
13539
|
+
}),
|
|
13540
|
+
message: z2.string().min(1, "message is required"),
|
|
13541
|
+
context: z2.record(z2.unknown()).optional()
|
|
13232
13542
|
})
|
|
13233
13543
|
};
|
|
13234
13544
|
TOOL_NAMES = Object.keys(toolSchemas);
|
|
@@ -13299,15 +13609,16 @@ function createServer() {
|
|
|
13299
13609
|
isError: true
|
|
13300
13610
|
};
|
|
13301
13611
|
}
|
|
13612
|
+
const fullDescription = TOOL_DESCRIPTIONS[tool.name] ?? tool.description;
|
|
13302
13613
|
return {
|
|
13303
13614
|
content: [
|
|
13304
13615
|
{
|
|
13305
13616
|
type: "text",
|
|
13306
|
-
text: JSON.stringify(
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
)
|
|
13617
|
+
text: JSON.stringify({
|
|
13618
|
+
name: tool.name,
|
|
13619
|
+
description: fullDescription,
|
|
13620
|
+
inputSchema: tool.inputSchema
|
|
13621
|
+
})
|
|
13311
13622
|
}
|
|
13312
13623
|
]
|
|
13313
13624
|
};
|