gtm-now 0.9.0 → 0.10.1

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 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 directly by gtm_linkedin tool.";
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: 'Discover companies matching your ICP criteria. Powered by DiscoLike (60M+ companies from web/SSL crawl data) and optionally Apollo.\n\nDiscoLike strengths:\n- Lookalike discovery: give it 1-10 seed domains, get similar companies\n- ICP text matching: describe your ideal customer in plain English\n- Tech stack filtering: find companies using specific technologies\n- Website content analysis: matches against actual site content\n- Best for: B2B companies, local businesses, professional services, healthcare\n\nFor DTC/e-commerce brands, use `provider: "storeleads"` \u2014 it has revenue estimates and category data that neither DiscoLike nor Apollo provides.\n\nTip: DiscoLike lookalike mode is extremely powerful. If you have 2-3 example companies, use the domains parameter to find hundreds of similar ones.\n\nSingle 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.',
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: {
@@ -10713,8 +10713,13 @@ var init_prospect = __esm({
10713
10713
  },
10714
10714
  output: {
10715
10715
  type: "string",
10716
- enum: ["file", "inline"],
10717
- description: 'Output mode. "file" writes results to JSONL file (auto when >100 results). "inline" returns in response.'
10716
+ enum: ["file", "inline", "csv"],
10717
+ description: 'Output mode. "file" writes JSONL (auto when >20 results). "csv" writes a CSV file for spreadsheet review. "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 at companies by job title, company, or domain. Returns name, title, email (when available), phone, and LinkedIn URL.\n\nPowered by DiscoLike Contacts, Apollo, or BetterContact depending on configuration.\n\nDiscoLike strengths:\n- Domain-based search: pass company_domain or company_domains[] for multi-domain batch\n- Seniority filtering (owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)\n- Industry + skills filtering to narrow results without post-processing\n- email_validated flag for pre-verified emails only\n- Employee/revenue range filters for company-level targeting\n- Returns validated emails when available\n\nResponse is slim by default (name, title, email, company, company_domain, linkedin). Pass fields: "all" for full payload.\n\nAfter finding people, use:\n- gtm_enrich to get verified email/phone if not returned (company_domain field is pipeline-compatible)\n- gtm_linkedin to check LinkedIn activity and engagement signals\n\nKnown limitations:\n- department + seniority filters combined may return 0 results (Disco API uses strict AND). Use one or the other.',
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: {
@@ -10820,8 +10825,13 @@ var init_prospect = __esm({
10820
10825
  },
10821
10826
  output: {
10822
10827
  type: "string",
10823
- enum: ["file", "inline"],
10824
- description: 'Output mode. "file" writes results to JSONL file (auto when >100 results). "inline" returns in response.'
10828
+ enum: ["file", "inline", "csv"],
10829
+ description: 'Output mode. "file" writes JSONL (auto when >20 results). "csv" writes a CSV file for spreadsheet review. "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 and phone enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.\n\nAlways do a dry_run first for batches > 5 to preview cost.\n\nProviders (in configured waterfall order):\n- Prospeo: email finder, good hit rate on LinkedIn-active professionals\n- BetterContact: email + phone, charges only on successful find\n\nInput needs: first_name + last_name minimum. Adding company_domain dramatically improves hit rates. LinkedIn URL is the gold standard input.\n\nTip: 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.",
10852
+ description: 'Email enrichment via waterfall \u2014 tries providers in order, stops on first match. Always dry_run first for batches > 5. Defaults to email only. Phone costs extra \u2014 only add find: ["email", "phone"] when the user explicitly requests phone numbers.',
10843
10853
  inputSchema: {
10844
10854
  type: "object",
10845
10855
  properties: {
@@ -10866,7 +10876,7 @@ var init_enrich = __esm({
10866
10876
  enum: ["email", "phone", "title", "company", "funding", "tech_stack"]
10867
10877
  },
10868
10878
  default: ["email"],
10869
- description: "Fields to find. Currently supported: email, phone. Additional fields (title, company, funding, tech_stack) are accepted but return an informative error until Phase 2."
10879
+ description: 'Fields to find. Defaults to ["email"] only. Add "phone" ONLY when explicitly requested \u2014 phone lookups cost extra. Additional fields (title, company, funding, tech_stack) return an informative error until Phase 2.'
10870
10880
  },
10871
10881
  waterfall: {
10872
10882
  type: "array",
@@ -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 structured data from a URL. Supports LinkedIn profiles, LinkedIn posts, and web pages. Optionally specify which fields to extract. Provider is auto-selected based on URL type (LinkedIn vs web).",
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 (LinkedIn profile/post URL or any web page)"
10942
+ description: "URL to extract data from (required for web_page)"
10915
10943
  },
10916
- type: {
10944
+ linkedin_url: {
10917
10945
  type: "string",
10918
- enum: ["linkedin_profile", "linkedin_post", "web_page"],
10919
- description: "Content type (auto-detected from URL if omitted)"
10946
+ description: "LinkedIn profile URL (required for linkedin_profile)"
10920
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: {
10970
+ type: "string",
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"
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
- provider: {
10989
+ detail: {
10927
10990
  type: "string",
10928
- description: "Provider override (auto-selected based on URL type if omitted)"
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 campaigns, get stats, add leads, or create a new campaign. Supports PlusVibe, Instantly, and Smartlead providers.",
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: { type: "string" },
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 (uses first available campaign_email provider if omitted)"
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)"
11087
- },
11088
- leads: {
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."
11131
+ description: "Provider override (auto-selects based on available campaign_email or outreach_linkedin provider if omitted)"
11101
11132
  },
11102
- provider: {
11133
+ detail: {
11103
11134
  type: "string",
11104
- description: "Provider override (uses first available outreach_linkedin provider if omitted)"
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. Shows which providers are configured, their capabilities, remaining credits, and the current client context.",
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,127 @@ var init_configure = __esm({
11237
11274
  }
11238
11275
  });
11239
11276
 
11240
- // src/tools/linkedin.ts
11241
- var linkedinTools;
11242
- var init_linkedin = __esm({
11243
- "src/tools/linkedin.ts"() {
11277
+ // src/tools/feedback.ts
11278
+ var feedbackTools;
11279
+ var init_feedback = __esm({
11280
+ "src/tools/feedback.ts"() {
11244
11281
  "use strict";
11245
- linkedinTools = [
11282
+ feedbackTools = [
11246
11283
  {
11247
- name: "gtm_linkedin",
11248
- description: "LinkedIn intelligence \u2014 profile scraping, post search, and engagement mining. Powered by HarvestAPI.\n\nWhen to use:\n- Scrape a LinkedIn profile for headline, experience, skills, follower count\n- Check if someone is active on LinkedIn (recent posts, engagement levels)\n- Find people who comment on or react to specific posts (warm lead mining)\n- Search LinkedIn posts by keyword to find thought leaders\n- Monitor competitor or industry content for engagers\n\nWhen NOT to use:\n- Finding people by job title at a company \u2014 use gtm_find_people instead\n- Email enrichment \u2014 use gtm_enrich instead\n- Non-LinkedIn web scraping \u2014 use gtm_extract instead\n\nPairs well with:\n- gtm_find_people \u2192 gtm_linkedin (find contacts, then check LinkedIn activity)\n- gtm_linkedin engagement \u2192 gtm_enrich (find engagers, then get emails)\n- gtm_storeleads \u2192 gtm_find_people \u2192 gtm_linkedin (DTC brands \u2192 CEOs \u2192 activity check)\n\nNote: HarvestAPI charges per request. For engagement mining, each post lookup counts as a request. Use limit to control costs.",
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
- action: {
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: {
11284
- type: "string",
11285
- enum: ["comments", "reactions", "both"],
11286
- description: "Type of engagement to retrieve (default: both)"
11287
- },
11288
- query: {
11289
+ type: {
11289
11290
  type: "string",
11290
- description: "Keyword search (required for search_posts action)"
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'
11291
11293
  },
11292
- sort: {
11294
+ message: {
11293
11295
  type: "string",
11294
- enum: ["relevance", "date"],
11295
- description: "Sort order for search_posts"
11296
+ description: "Describe the issue, request, or suggestion in detail"
11296
11297
  },
11297
- limit: { type: "number", description: "Max results to return (max 100)" }
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 enrichment via waterfall \u2014 tries multiple providers in order, stops on first match. Supports 1-100 contacts per call.
11351
+
11352
+ IMPORTANT: Defaults to email only. Phone enrichment costs extra per lookup \u2014 only include when the user explicitly asks for phone numbers.
11353
+
11354
+ Always do a dry_run first for batches > 5 to preview cost.
11355
+
11356
+ Providers (in configured waterfall order):
11357
+ - Prospeo: email finder, good hit rate on LinkedIn-active professionals
11358
+ - BetterContact: email + phone, charges only on successful find
11359
+
11360
+ Input needs: first_name + last_name minimum. Adding company_domain dramatically improves hit rates. LinkedIn URL is the gold standard input.
11361
+
11362
+ 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.`,
11363
+ 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.",
11364
+ gtm_extract: `Extract structured data from any URL or platform. LinkedIn profiles, posts, engagement, search results, company posts, and web pages.
11365
+
11366
+ When to use:
11367
+ - Scrape a LinkedIn profile for headline, experience, skills, follower count
11368
+ - Check if someone is active on LinkedIn (recent posts, engagement levels)
11369
+ - Find people who comment on or react to specific posts (warm lead mining)
11370
+ - Search LinkedIn posts by keyword to find thought leaders
11371
+ - Monitor competitor or industry content for engagers
11372
+ - Scrape any web page for structured data
11373
+
11374
+ When NOT to use:
11375
+ - Finding people by job title at a company \u2014 use gtm_find_people instead
11376
+ - Email enrichment \u2014 use gtm_enrich instead
11377
+
11378
+ Pairs well with:
11379
+ - gtm_find_people \u2192 gtm_extract (find contacts, then check LinkedIn activity)
11380
+ - gtm_extract linkedin_engagement \u2192 gtm_enrich (find engagers, then get emails)
11381
+ - gtm_storeleads \u2192 gtm_find_people \u2192 gtm_extract (DTC brands \u2192 CEOs \u2192 activity check)
11382
+
11383
+ Note: HarvestAPI charges per request. For engagement mining, each post lookup counts as a request. Use limit to control costs.`,
11384
+ gtm_fetch: "Make an HTTP call to any API endpoint. Use this to pull data from services that don't have a dedicated provider.",
11385
+ 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).",
11386
+ 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.",
11387
+ gtm_crm: "Read and write CRM data. Find, create, or update people and companies. Create deals. Supports Attio and other CRM providers.",
11388
+ gtm_status: "Check provider health, credit balances, and active configuration. Shows which providers are configured, their capabilities, remaining credits, and the current client context.",
11389
+ gtm_configure: "Manage GTM configuration. Switch between clients, view active config, set waterfall order, list available providers.",
11390
+ 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.",
11391
+ 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.",
11392
+ 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.",
11393
+ gtm_feedback: "Submit feedback, bug reports, feature requests, or provider requests. Feedback is stored for the gtm-now maintainers to review."
11394
+ };
11395
+ }
11396
+ });
11397
+
11305
11398
  // src/tools/index.ts
11306
11399
  var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, toolsByName, toolCategories, workflowRecipes;
11307
11400
  var init_tools = __esm({
@@ -11313,22 +11406,20 @@ var init_tools = __esm({
11313
11406
  init_fetch();
11314
11407
  init_ai();
11315
11408
  init_campaign();
11316
- init_outreach();
11317
11409
  init_crm();
11318
11410
  init_status();
11319
11411
  init_configure();
11320
- init_linkedin();
11412
+ init_feedback();
11321
11413
  init_prospect();
11322
11414
  init_enrich();
11323
11415
  init_extract();
11324
11416
  init_fetch();
11325
11417
  init_ai();
11326
11418
  init_campaign();
11327
- init_outreach();
11328
11419
  init_crm();
11329
11420
  init_status();
11330
11421
  init_configure();
11331
- init_linkedin();
11422
+ init_descriptions();
11332
11423
  capabilityTools = [
11333
11424
  ...prospectTools,
11334
11425
  ...enrichTools,
@@ -11336,11 +11427,10 @@ var init_tools = __esm({
11336
11427
  ...fetchTools,
11337
11428
  ...aiTools,
11338
11429
  ...campaignTools,
11339
- ...outreachTools,
11340
11430
  ...crmTools,
11341
11431
  ...statusTools,
11342
11432
  ...configureTools,
11343
- ...linkedinTools
11433
+ ...feedbackTools
11344
11434
  ];
11345
11435
  guideTool = {
11346
11436
  name: "gtm_guide",
@@ -11356,6 +11446,7 @@ var init_tools = __esm({
11356
11446
  "cold_outreach",
11357
11447
  "linkedin_outreach",
11358
11448
  "crm_sync",
11449
+ "export_to_webhook",
11359
11450
  "list_tasks"
11360
11451
  ],
11361
11452
  description: "The workflow to get guidance for"
@@ -11406,11 +11497,9 @@ var init_tools = __esm({
11406
11497
  fetch: fetchTools.map((t) => t.name),
11407
11498
  ai: aiTools.map((t) => t.name),
11408
11499
  campaign: campaignTools.map((t) => t.name),
11409
- outreach: outreachTools.map((t) => t.name),
11410
11500
  crm: crmTools.map((t) => t.name),
11411
11501
  status: statusTools.map((t) => t.name),
11412
11502
  configure: configureTools.map((t) => t.name),
11413
- linkedin: linkedinTools.map((t) => t.name),
11414
11503
  meta: metaTools.map((t) => t.name)
11415
11504
  };
11416
11505
  workflowRecipes = {
@@ -11425,31 +11514,37 @@ Start by identifying what kind of companies you're targeting:
11425
11514
  \u2192 gtm_find_people({ company_domain: "...", job_title: "CEO" })
11426
11515
  3. gtm_enrich (dry_run first) \u2014 get verified emails
11427
11516
  \u2192 gtm_enrich({ contacts: [...], find: ["email"], dry_run: true })
11428
- 4. Optional: gtm_linkedin \u2014 check if they're active on LinkedIn
11429
- \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
11517
+ 4. Optional: gtm_extract \u2014 check if they're active on LinkedIn
11518
+ \u2192 gtm_extract({ type: "linkedin_profile", linkedin_url: "..." })
11430
11519
 
11431
11520
  ## B2B / SaaS / Services with websites
11432
11521
  1. gtm_find_companies \u2014 use DiscoLike ICP text or lookalikes
11433
11522
  \u2192 gtm_find_companies({ icp_text: "...", limit: 100 })
11434
11523
  2. gtm_find_people \u2014 find contacts by title
11435
11524
  3. gtm_enrich \u2014 get verified emails
11436
- 4. gtm_campaign or gtm_outreach \u2014 push to campaigns
11525
+ 4. gtm_campaign \u2014 push to email or LinkedIn campaigns
11437
11526
 
11438
11527
  ## LinkedIn-native prospects
11439
11528
  1. gtm_find_people \u2014 search by title + company
11440
- 2. gtm_linkedin \u2014 scrape profiles for activity signals
11441
- \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
11529
+ 2. gtm_extract \u2014 scrape profiles for activity signals
11530
+ \u2192 gtm_extract({ type: "linkedin_profile", linkedin_url: "..." })
11442
11531
  3. gtm_enrich \u2014 get emails for active prospects
11443
- 4. gtm_outreach \u2014 LinkedIn campaign, or gtm_campaign for email
11532
+ 4. gtm_campaign \u2014 push to LinkedIn or email campaign
11444
11533
 
11445
11534
  ## Engagement-based (warm leads)
11446
- 1. gtm_linkedin (search_posts) \u2014 find relevant content
11447
- \u2192 gtm_linkedin({ action: "search_posts", query: "...", sort: "date" })
11448
- 2. gtm_linkedin (engagement) \u2014 extract commenters/reactors
11449
- \u2192 gtm_linkedin({ action: "engagement", post_url: "...", engagement_type: "both" })
11535
+ 1. gtm_extract (linkedin_search) \u2014 find relevant content
11536
+ \u2192 gtm_extract({ type: "linkedin_search", query: "...", sort: "date" })
11537
+ 2. gtm_extract (linkedin_engagement) \u2014 extract commenters/reactors
11538
+ \u2192 gtm_extract({ type: "linkedin_engagement", post_url: "...", engagement_type: "both" })
11450
11539
  3. gtm_enrich \u2014 get emails
11451
11540
  4. gtm_campaign \u2014 personalized outreach referencing their engagement
11452
11541
 
11542
+ ## Review before enriching
11543
+ After finding companies, review the list before spending enrichment credits:
11544
+ - Use output: 'csv' to write results to a spreadsheet for visual review
11545
+ - Or export to Clay/webhook via gtm_fetch POST for team review (see export_to_webhook guide)
11546
+ - Use gtm_ai with action: 'classify' to auto-filter competitors or bad fits
11547
+
11453
11548
  ## Key principle
11454
11549
  Always dry_run enrichment first. Credits are spent per successful find \u2014 previewing cost avoids surprises.`,
11455
11550
  enrich_list: `# Enrich a Contact List \u2014 Workflow
@@ -11502,10 +11597,10 @@ Always verify emails before pushing to campaigns. Bounced emails hurt sender rep
11502
11597
  \u2192 gtm_ai({ action: "personalize", data: [...], prompt: "Write a LinkedIn connection request referencing their recent post" })
11503
11598
 
11504
11599
  3. LIST CAMPAIGNS \u2014 Find or identify the target outreach campaign
11505
- \u2192 gtm_outreach({ action: "list" })
11600
+ \u2192 gtm_campaign({ action: "list", provider: "heyreach" })
11506
11601
 
11507
11602
  4. PUSH \u2014 Add leads to LinkedIn outreach campaign
11508
- \u2192 gtm_outreach({ action: "add_leads", campaign_id: "...", leads: [{ linkedin_url: "..." }] })
11603
+ \u2192 gtm_campaign({ action: "add_leads", campaign_id: "...", provider: "heyreach", leads: [{ linkedin_url: "..." }] })
11509
11604
 
11510
11605
  ## Key principle
11511
11606
  LinkedIn outreach requires LinkedIn profile URLs \u2014 not emails. Extract profile data first for personalization.`,
@@ -11525,6 +11620,26 @@ LinkedIn outreach requires LinkedIn profile URLs \u2014 not emails. Extract prof
11525
11620
 
11526
11621
  ## Key principle
11527
11622
  Always search before creating to avoid duplicates. Most CRMs have built-in email dedup but better to check first.`,
11623
+ export_to_webhook: `# Export Data to Webhook \u2014 Workflow
11624
+
11625
+ Push GTM data to Clay, Make, Zapier, or any webhook endpoint for visual review or further processing.
11626
+
11627
+ ## Steps
11628
+ 1. FIND \u2014 Get your companies or contacts
11629
+ \u2192 gtm_find_companies({ icp_text: "...", limit: 50 })
11630
+ \u2192 gtm_find_people({ company_domain: "...", job_title: "CEO" })
11631
+
11632
+ 2. EXPORT \u2014 Push to webhook via gtm_fetch
11633
+ \u2192 gtm_fetch({ url: "https://your-webhook-url", method: "POST", body: { results: [...] } })
11634
+
11635
+ ## Clay Integration
11636
+ Clay table webhooks accept JSON directly:
11637
+ \u2192 gtm_fetch({ url: "https://api.clay.com/v3/sources/webhook/...", method: "POST", body: { first_name: "...", last_name: "...", company: "...", ... } })
11638
+
11639
+ Send one record per request, or batch them depending on the webhook's format.
11640
+
11641
+ ## Key principle
11642
+ Review data visually before spending enrichment credits. Export \u2192 review \u2192 enrich only the good leads.`,
11528
11643
  list_tasks: `# Available GTM Workflows
11529
11644
 
11530
11645
  Call gtm_guide with one of these tasks:
@@ -11534,10 +11649,11 @@ Call gtm_guide with one of these tasks:
11534
11649
  - cold_outreach \u2014 Enrich + personalize + push to cold email campaign
11535
11650
  - linkedin_outreach \u2014 Extract profiles + personalize + push to LinkedIn campaign
11536
11651
  - crm_sync \u2014 Find/create/update people and deals in CRM
11652
+ - export_to_webhook \u2014 Push data to Clay, Make, Zapier, or any webhook for review
11537
11653
 
11538
11654
  New tools available:
11539
11655
  - gtm_find_companies (provider: storeleads) \u2014 Search 1.2M+ e-commerce stores by category, revenue, platform
11540
- - gtm_linkedin \u2014 LinkedIn profile scraping, post search, engagement mining
11656
+ - gtm_extract \u2014 LinkedIn profiles, posts, engagement mining, search + web page scraping
11541
11657
 
11542
11658
  For any task: always dry_run enrichment batches > 5 to preview cost before executing.`
11543
11659
  };
@@ -11617,6 +11733,7 @@ import * as os from "os";
11617
11733
  function shouldWriteToFile(count, output) {
11618
11734
  if (output === "file") return true;
11619
11735
  if (output === "inline") return false;
11736
+ if (output === "csv") return false;
11620
11737
  return count > FILE_OUTPUT_THRESHOLD;
11621
11738
  }
11622
11739
  function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR) {
@@ -11634,6 +11751,40 @@ function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR)
11634
11751
  }
11635
11752
  return filePath;
11636
11753
  }
11754
+ function writeResultsCsv(tool, results, resultsDir = DEFAULT_RESULTS_DIR) {
11755
+ fs.mkdirSync(resultsDir, { recursive: true });
11756
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
11757
+ const filename = `${tool}-${timestamp}.csv`;
11758
+ const filePath = path.join(resultsDir, filename);
11759
+ if (results.length === 0) {
11760
+ fs.writeFileSync(filePath, "", "utf-8");
11761
+ return filePath;
11762
+ }
11763
+ const headers = Object.keys(results[0]);
11764
+ const headerLine = headers.map(escapeCsvField).join(",");
11765
+ const dataLines = results.map(
11766
+ (row) => headers.map((h) => escapeCsvField(formatCsvValue(row[h]))).join(",")
11767
+ );
11768
+ const content = [headerLine, ...dataLines].join("\n") + "\n";
11769
+ fs.writeFileSync(filePath, content, "utf-8");
11770
+ try {
11771
+ cleanOldResults(7, resultsDir);
11772
+ } catch {
11773
+ }
11774
+ return filePath;
11775
+ }
11776
+ function formatCsvValue(value) {
11777
+ if (value === null || value === void 0) return "";
11778
+ if (Array.isArray(value)) return value.join(";");
11779
+ if (typeof value === "object") return JSON.stringify(value);
11780
+ return String(value);
11781
+ }
11782
+ function escapeCsvField(value) {
11783
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
11784
+ return `"${value.replace(/"/g, '""')}"`;
11785
+ }
11786
+ return value;
11787
+ }
11637
11788
  function cleanOldResults(maxAgeDays, resultsDir = DEFAULT_RESULTS_DIR) {
11638
11789
  let entries;
11639
11790
  try {
@@ -11662,7 +11813,7 @@ var FILE_OUTPUT_THRESHOLD, DEFAULT_RESULTS_DIR;
11662
11813
  var init_result_writer = __esm({
11663
11814
  "src/utils/result-writer.ts"() {
11664
11815
  "use strict";
11665
- FILE_OUTPUT_THRESHOLD = 100;
11816
+ FILE_OUTPUT_THRESHOLD = 20;
11666
11817
  DEFAULT_RESULTS_DIR = path.join(os.homedir(), ".gtm", "results");
11667
11818
  }
11668
11819
  });
@@ -11757,6 +11908,19 @@ async function findCompanies(args, ctx) {
11757
11908
  if (query.max_revenue != null) applied_filters.max_revenue = query.max_revenue;
11758
11909
  const providerUsed = provider.name;
11759
11910
  const output = args.output;
11911
+ if (output === "csv") {
11912
+ const filePath = writeResultsCsv("find_companies", slimmedResults);
11913
+ return {
11914
+ provider: providerUsed,
11915
+ count: results.length,
11916
+ has_more,
11917
+ output: "csv",
11918
+ file: filePath,
11919
+ preview: slimmedResults.slice(0, 3),
11920
+ columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],
11921
+ ...warnings?.length ? { warnings } : {}
11922
+ };
11923
+ }
11760
11924
  if (shouldWriteToFile(results.length, output)) {
11761
11925
  const meta = {
11762
11926
  query: applied_filters,
@@ -11850,6 +12014,19 @@ async function findPeople(args, ctx) {
11850
12014
  if (query.revenue_range) applied_filters.revenue_range = query.revenue_range;
11851
12015
  const providerUsed = provider.name;
11852
12016
  const output = args.output;
12017
+ if (output === "csv") {
12018
+ const filePath = writeResultsCsv("find_people", slimmedResults);
12019
+ return {
12020
+ provider: providerUsed,
12021
+ count: dedupedResults.length,
12022
+ has_more,
12023
+ output: "csv",
12024
+ file: filePath,
12025
+ preview: slimmedResults.slice(0, 3),
12026
+ columns: slimmedResults.length > 0 ? Object.keys(slimmedResults[0]) : [],
12027
+ ...warnings?.length ? { warnings } : {}
12028
+ };
12029
+ }
11853
12030
  if (shouldWriteToFile(dedupedResults.length, output)) {
11854
12031
  const meta = {
11855
12032
  query: applied_filters,
@@ -11907,10 +12084,46 @@ function isRateLimitError(error) {
11907
12084
  const msg = error instanceof Error ? error.message : String(error);
11908
12085
  return msg.includes("429") || msg.toLowerCase().includes("rate limit");
11909
12086
  }
12087
+ function withTimeout(promise, ms, label) {
12088
+ return new Promise((resolve, reject) => {
12089
+ const timer = setTimeout(
12090
+ () => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
12091
+ ms
12092
+ );
12093
+ promise.then(
12094
+ (val) => {
12095
+ clearTimeout(timer);
12096
+ resolve(val);
12097
+ },
12098
+ (err) => {
12099
+ clearTimeout(timer);
12100
+ reject(err);
12101
+ }
12102
+ );
12103
+ });
12104
+ }
11910
12105
  async function runWaterfall(contacts, config, registry) {
11911
12106
  const results = [];
11912
12107
  const exhausted = /* @__PURE__ */ new Set();
12108
+ const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
11913
12109
  for (const contact of contacts) {
12110
+ if (Date.now() > batchDeadline) {
12111
+ results.push({
12112
+ contact,
12113
+ attempts: [
12114
+ {
12115
+ provider: "waterfall",
12116
+ capability: "enrich_email",
12117
+ found: false,
12118
+ charged: false,
12119
+ duration_ms: 0,
12120
+ error: "Batch timeout exceeded \u2014 skipped remaining contacts"
12121
+ }
12122
+ ],
12123
+ charged: false
12124
+ });
12125
+ continue;
12126
+ }
11914
12127
  const result = await enrichContact(contact, config, registry, exhausted);
11915
12128
  results.push(result);
11916
12129
  }
@@ -11999,11 +12212,20 @@ async function enrichField(contact, field, config, registry, exhausted) {
11999
12212
  const start = Date.now();
12000
12213
  try {
12001
12214
  let value;
12215
+ const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
12002
12216
  if (field === "email" && "enrichEmail" in provider) {
12003
- const result = await provider.enrichEmail(contact);
12217
+ const result = await withTimeout(
12218
+ provider.enrichEmail(contact),
12219
+ providerTimeoutMs,
12220
+ providerName
12221
+ );
12004
12222
  value = result?.email;
12005
12223
  } else if (field === "phone" && "enrichPhone" in provider) {
12006
- const result = await provider.enrichPhone(contact);
12224
+ const result = await withTimeout(
12225
+ provider.enrichPhone(contact),
12226
+ providerTimeoutMs,
12227
+ providerName
12228
+ );
12007
12229
  value = result?.phone;
12008
12230
  }
12009
12231
  const found = !!value;
@@ -12079,7 +12301,7 @@ function dryRunWaterfall(contactsCount, config, registry) {
12079
12301
  confirm: "Call gtm_enrich again without dry_run to execute."
12080
12302
  };
12081
12303
  }
12082
- var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS;
12304
+ var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
12083
12305
  var init_waterfall = __esm({
12084
12306
  "src/providers/waterfall.ts"() {
12085
12307
  "use strict";
@@ -12101,6 +12323,8 @@ var init_waterfall = __esm({
12101
12323
  "not accessible with this api_key on a free plan"
12102
12324
  ];
12103
12325
  WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
12326
+ DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
12327
+ DEFAULT_BATCH_TIMEOUT_MS = 3e5;
12104
12328
  }
12105
12329
  });
12106
12330
 
@@ -12109,6 +12333,19 @@ var enrich_exports = {};
12109
12333
  __export(enrich_exports, {
12110
12334
  handleEnrich: () => handleEnrich
12111
12335
  });
12336
+ function slimEnrichResults(results) {
12337
+ return results.map((r) => {
12338
+ const slim = {};
12339
+ if (r.email) slim.email = r.email;
12340
+ if (r.phone) slim.phone = r.phone;
12341
+ if (r.email_status) slim.email_status = r.email_status;
12342
+ if (r.email_provider) slim.email_provider = r.email_provider;
12343
+ if (r.phone_provider) slim.phone_provider = r.phone_provider;
12344
+ slim.charged = r.charged;
12345
+ slim.name = `${r.contact.first_name} ${r.contact.last_name}`;
12346
+ return slim;
12347
+ });
12348
+ }
12112
12349
  async function handleEnrich(name, args, ctx) {
12113
12350
  if (name === "gtm_verify") {
12114
12351
  return verifyEmail(args, ctx);
@@ -12152,11 +12389,12 @@ async function enrichContacts(args, ctx) {
12152
12389
  }
12153
12390
  const results = await runWaterfall(contacts, config, ctx.registry);
12154
12391
  const found = results.filter((r) => r.email || r.phone).length;
12392
+ const detail = args.detail ?? "summary";
12155
12393
  return {
12156
12394
  total: results.length,
12157
12395
  found,
12158
12396
  missed: results.length - found,
12159
- results
12397
+ results: detail === "full" ? results : slimEnrichResults(results)
12160
12398
  };
12161
12399
  }
12162
12400
  async function verifyEmail(args, ctx) {
@@ -12185,45 +12423,156 @@ var extract_exports = {};
12185
12423
  __export(extract_exports, {
12186
12424
  handleExtract: () => handleExtract
12187
12425
  });
12426
+ function slimPost(post) {
12427
+ const slim = {};
12428
+ for (const [key, value] of Object.entries(post)) {
12429
+ if (key === "text" && typeof value === "string") {
12430
+ slim.text = value.length > POST_TEXT_LIMIT ? value.slice(0, POST_TEXT_LIMIT) + "..." : value;
12431
+ } else if (key === "images" || key === "documents" || key === "media") {
12432
+ if (Array.isArray(value) && value.length > 0) slim[`${key}_count`] = value.length;
12433
+ } else {
12434
+ slim[key] = value;
12435
+ }
12436
+ }
12437
+ return slim;
12438
+ }
12439
+ function slimProfile(profile) {
12440
+ const PROFILE_FIELDS = [
12441
+ "firstName",
12442
+ "lastName",
12443
+ "headline",
12444
+ "location",
12445
+ "summary",
12446
+ "followerCount",
12447
+ "connectionCount",
12448
+ "email",
12449
+ "publicIdentifier",
12450
+ "industryName",
12451
+ "company"
12452
+ ];
12453
+ const slim = {};
12454
+ for (const field of PROFILE_FIELDS) {
12455
+ if (field in profile && profile[field] != null) slim[field] = profile[field];
12456
+ }
12457
+ return slim;
12458
+ }
12188
12459
  async function handleExtract(args, ctx) {
12460
+ const type = args.type;
12461
+ if (type.startsWith("linkedin_")) {
12462
+ return handleLinkedinExtract(type, args, ctx);
12463
+ }
12464
+ return handleWebExtract(args, ctx);
12465
+ }
12466
+ async function handleWebExtract(args, ctx) {
12189
12467
  const target = {
12190
12468
  url: args.url,
12191
- type: args.type,
12469
+ type: "web_page",
12192
12470
  fields: args.fields
12193
12471
  };
12194
- const capability = target.type === "web_page" ? "extract_web" : "extract_linkedin";
12195
12472
  const providerName = args.provider || void 0;
12196
- const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability(capability);
12473
+ const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability("extract_web");
12197
12474
  if (providers.length === 0) {
12198
12475
  throw createGtmError(
12199
12476
  "EXTRACTION_FAILED",
12200
- `No providers configured for ${capability}. Add a provider with that capability.`
12477
+ "No providers configured for extract_web. Add a provider with that capability."
12201
12478
  );
12202
12479
  }
12203
- const provider = providers[0];
12204
- if (provider.name === "harvest" && capability === "extract_linkedin") {
12205
- return extractViaHarvest(provider, target);
12206
- }
12207
- const extractor = provider;
12480
+ const extractor = providers[0];
12208
12481
  return extractor.extract(target);
12209
12482
  }
12210
- async function extractViaHarvest(harvest, target) {
12211
- if (target.type === "linkedin_profile") {
12212
- const profile = await harvest.getProfile(target.url, true);
12213
- return {
12214
- data: profile,
12215
- source: target.url,
12216
- provider: "harvest"
12217
- };
12483
+ async function handleLinkedinExtract(type, args, ctx) {
12484
+ const adapter = ctx.registry.get("harvest");
12485
+ if (!adapter) {
12486
+ throw Object.assign(
12487
+ new Error(
12488
+ "HarvestAPI provider not configured. Add harvest.api_key to your config.yaml or set GTM_HARVEST_API_KEY environment variable."
12489
+ ),
12490
+ { statusCode: 503 }
12491
+ );
12492
+ }
12493
+ const detail = args.detail ?? "summary";
12494
+ const isSummary = detail !== "full";
12495
+ switch (type) {
12496
+ case "linkedin_profile": {
12497
+ const url = args.linkedin_url || args.url;
12498
+ const profile = await adapter.getProfile(url, args.find_email);
12499
+ return {
12500
+ type: "linkedin_profile",
12501
+ provider: "harvest",
12502
+ profile: isSummary ? slimProfile(profile) : profile
12503
+ };
12504
+ }
12505
+ case "linkedin_posts": {
12506
+ const result = await adapter.getProfilePosts({
12507
+ profile: args.profile,
12508
+ postedLimit: args.posted_within
12509
+ });
12510
+ return {
12511
+ type: "linkedin_posts",
12512
+ provider: "harvest",
12513
+ count: result.elements.length,
12514
+ posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
12515
+ ...detail === "full" && { pagination: result.pagination }
12516
+ };
12517
+ }
12518
+ case "linkedin_company_posts": {
12519
+ const result = await adapter.getCompanyPosts({
12520
+ company: args.company,
12521
+ postedLimit: args.posted_within
12522
+ });
12523
+ return {
12524
+ type: "linkedin_company_posts",
12525
+ provider: "harvest",
12526
+ count: result.elements.length,
12527
+ posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
12528
+ ...detail === "full" && { pagination: result.pagination }
12529
+ };
12530
+ }
12531
+ case "linkedin_search": {
12532
+ const result = await adapter.searchPosts(args.query, {
12533
+ sortBy: args.sort,
12534
+ postedLimit: args.posted_within
12535
+ });
12536
+ return {
12537
+ type: "linkedin_search",
12538
+ provider: "harvest",
12539
+ count: result.elements.length,
12540
+ posts: isSummary ? result.elements.map((p) => slimPost(p)) : result.elements,
12541
+ ...detail === "full" && { pagination: result.pagination }
12542
+ };
12543
+ }
12544
+ case "linkedin_engagement": {
12545
+ const result = await adapter.getPostEngagement(
12546
+ args.post_url,
12547
+ args.engagement_type ?? "both"
12548
+ );
12549
+ if (isSummary) {
12550
+ return {
12551
+ type: "linkedin_engagement",
12552
+ provider: "harvest",
12553
+ comments_count: result.comments.length,
12554
+ reactions_count: result.reactions.length
12555
+ };
12556
+ }
12557
+ return {
12558
+ type: "linkedin_engagement",
12559
+ provider: "harvest",
12560
+ comments_count: result.comments.length,
12561
+ reactions_count: result.reactions.length,
12562
+ comments: result.comments,
12563
+ reactions: result.reactions
12564
+ };
12565
+ }
12566
+ default:
12567
+ throw new Error(`Unknown extract type: ${type}`);
12218
12568
  }
12219
- throw new Error(
12220
- 'LinkedIn post extraction is not supported by harvest. Use gtm_linkedin with action "posts" instead.'
12221
- );
12222
12569
  }
12570
+ var POST_TEXT_LIMIT;
12223
12571
  var init_extract2 = __esm({
12224
12572
  "src/handlers/extract.ts"() {
12225
12573
  "use strict";
12226
12574
  init_errors();
12575
+ POST_TEXT_LIMIT = 200;
12227
12576
  }
12228
12577
  });
12229
12578
 
@@ -12273,11 +12622,19 @@ async function handleFetch(args) {
12273
12622
  } else {
12274
12623
  responseBody = await response.text();
12275
12624
  }
12276
- return {
12277
- status: response.status,
12278
- headers: Object.fromEntries(response.headers.entries()),
12279
- body: responseBody
12280
- };
12625
+ const detail = args.detail ?? "summary";
12626
+ if (detail === "full") {
12627
+ return {
12628
+ status: response.status,
12629
+ headers: Object.fromEntries(response.headers.entries()),
12630
+ body: responseBody
12631
+ };
12632
+ }
12633
+ let slimBody = responseBody;
12634
+ if (typeof responseBody === "string" && responseBody.length > BODY_TRUNCATE_LIMIT) {
12635
+ slimBody = responseBody.slice(0, BODY_TRUNCATE_LIMIT) + `... (truncated, ${responseBody.length} chars total)`;
12636
+ }
12637
+ return { status: response.status, body: slimBody };
12281
12638
  } catch (error) {
12282
12639
  logError("handler:gtm_fetch", error instanceof Error ? error : new Error(String(error)), {
12283
12640
  url: fullUrl,
@@ -12288,7 +12645,7 @@ async function handleFetch(args) {
12288
12645
  clearTimeout(timeout);
12289
12646
  }
12290
12647
  }
12291
- var BLOCKED_PATTERNS, BLOCKED_HOSTS;
12648
+ var BLOCKED_PATTERNS, BLOCKED_HOSTS, BODY_TRUNCATE_LIMIT;
12292
12649
  var init_fetch2 = __esm({
12293
12650
  "src/handlers/fetch.ts"() {
12294
12651
  "use strict";
@@ -12305,6 +12662,7 @@ var init_fetch2 = __esm({
12305
12662
  /^fe80:/
12306
12663
  ];
12307
12664
  BLOCKED_HOSTS = ["localhost", "0.0.0.0", "[::1]"];
12665
+ BODY_TRUNCATE_LIMIT = 2e3;
12308
12666
  }
12309
12667
  });
12310
12668
 
@@ -12426,36 +12784,95 @@ var campaign_exports = {};
12426
12784
  __export(campaign_exports, {
12427
12785
  handleCampaign: () => handleCampaign
12428
12786
  });
12429
- async function handleCampaign(args, ctx) {
12430
- const action = args.action;
12787
+ function resolveChannel(args, ctx) {
12431
12788
  const providerName = args.provider || void 0;
12432
- const providers = providerName ? [ctx.registry.get(providerName)].filter(Boolean) : ctx.registry.getForCapability("campaign_email");
12433
- if (providers.length === 0) {
12789
+ if (providerName) {
12790
+ const provider = ctx.registry.get(providerName);
12791
+ if (!provider) {
12792
+ throw createGtmError(
12793
+ "PROVIDER_NOT_CONFIGURED",
12794
+ `Provider "${providerName}" not found. Check your config or use action: "list" without a provider to discover available campaigns.`
12795
+ );
12796
+ }
12797
+ if (provider.capabilities.includes("outreach_linkedin")) {
12798
+ return {
12799
+ type: "linkedin",
12800
+ manager: provider,
12801
+ providerName: provider.name
12802
+ };
12803
+ }
12804
+ if (provider.capabilities.includes("campaign_email")) {
12805
+ return {
12806
+ type: "email",
12807
+ manager: provider,
12808
+ providerName: provider.name
12809
+ };
12810
+ }
12434
12811
  throw createGtmError(
12435
12812
  "PROVIDER_NOT_CONFIGURED",
12436
- "No providers configured for email campaigns. Add a provider with campaign_email capability."
12813
+ `Provider "${providerName}" does not support campaign_email or outreach_linkedin.`
12437
12814
  );
12438
12815
  }
12439
- const provider = providers[0];
12440
- const manager = provider;
12816
+ const emailProviders = ctx.registry.getForCapability("campaign_email");
12817
+ if (emailProviders.length > 0) {
12818
+ const provider = emailProviders[0];
12819
+ return {
12820
+ type: "email",
12821
+ manager: provider,
12822
+ providerName: provider.name
12823
+ };
12824
+ }
12825
+ const linkedinProviders = ctx.registry.getForCapability("outreach_linkedin");
12826
+ if (linkedinProviders.length > 0) {
12827
+ const provider = linkedinProviders[0];
12828
+ return {
12829
+ type: "linkedin",
12830
+ manager: provider,
12831
+ providerName: provider.name
12832
+ };
12833
+ }
12834
+ throw createGtmError(
12835
+ "PROVIDER_NOT_CONFIGURED",
12836
+ "No providers configured for campaigns. Add a provider with campaign_email or outreach_linkedin capability."
12837
+ );
12838
+ }
12839
+ async function handleCampaign(args, ctx) {
12840
+ const action = args.action;
12841
+ const channel = resolveChannel(args, ctx);
12441
12842
  switch (action) {
12442
12843
  case "list":
12443
- return { provider: provider.name, campaigns: await manager.listCampaigns() };
12844
+ return { provider: channel.providerName, campaigns: await channel.manager.listCampaigns() };
12444
12845
  case "stats": {
12445
12846
  const campaignId = args.campaign_id;
12446
12847
  if (!campaignId)
12447
12848
  throw createGtmError("QUERY_INVALID", "campaign_id is required for stats action");
12448
- return manager.getCampaignStats(campaignId);
12849
+ return channel.manager.getCampaignStats(campaignId);
12449
12850
  }
12450
12851
  case "add_leads": {
12451
12852
  const campaignId = args.campaign_id;
12452
- const leads = args.leads;
12853
+ const rawLeads = args.leads;
12453
12854
  if (!campaignId)
12454
12855
  throw createGtmError("QUERY_INVALID", "campaign_id is required for add_leads action");
12455
- if (!leads?.length)
12856
+ if (!rawLeads?.length)
12456
12857
  throw createGtmError("QUERY_INVALID", "leads array is required for add_leads action");
12858
+ if (channel.type === "linkedin") {
12859
+ const leads2 = rawLeads.map((l) => ({
12860
+ linkedin_url: l.linkedin_url,
12861
+ first_name: l.first_name,
12862
+ last_name: l.last_name,
12863
+ company: l.company
12864
+ }));
12865
+ return channel.manager.addLeadsToCampaign(campaignId, leads2);
12866
+ }
12867
+ const leads = rawLeads.map((l) => ({
12868
+ email: l.email,
12869
+ first_name: l.first_name,
12870
+ last_name: l.last_name,
12871
+ company: l.company,
12872
+ custom_fields: l.custom_fields
12873
+ }));
12457
12874
  try {
12458
- return await manager.addLeadsToCampaign(campaignId, leads);
12875
+ return await channel.manager.addLeadsToCampaign(campaignId, leads);
12459
12876
  } catch (error) {
12460
12877
  if (error instanceof ApiClientError && (error.message.includes("Validation error") || error.statusCode === 400 || error.statusCode === 422)) {
12461
12878
  throw createGtmError(
@@ -12468,15 +12885,21 @@ async function handleCampaign(args, ctx) {
12468
12885
  }
12469
12886
  }
12470
12887
  case "create": {
12888
+ if (channel.type === "linkedin") {
12889
+ throw createGtmError(
12890
+ "PROVIDER_NOT_CONFIGURED",
12891
+ "LinkedIn outreach providers do not support campaign creation via API. Create campaigns directly in the provider dashboard."
12892
+ );
12893
+ }
12471
12894
  const name = args.name;
12472
12895
  if (!name) throw createGtmError("QUERY_INVALID", "name is required for create action");
12473
- if (!manager.createCampaign) {
12896
+ if (!channel.manager.createCampaign) {
12474
12897
  throw createGtmError(
12475
12898
  "PROVIDER_NOT_CONFIGURED",
12476
- `Provider "${provider.name}" does not support campaign creation. Currently only PlusVibe supports this action.`
12899
+ `Provider "${channel.providerName}" does not support campaign creation. Currently only PlusVibe supports this action.`
12477
12900
  );
12478
12901
  }
12479
- return manager.createCampaign(name);
12902
+ return channel.manager.createCampaign(name);
12480
12903
  }
12481
12904
  default:
12482
12905
  throw createGtmError("QUERY_INVALID", `Unknown campaign action: ${action}`);
@@ -12490,47 +12913,6 @@ var init_campaign2 = __esm({
12490
12913
  }
12491
12914
  });
12492
12915
 
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
12916
  // src/handlers/crm.ts
12535
12917
  var crm_exports = {};
12536
12918
  __export(crm_exports, {
@@ -12631,6 +13013,17 @@ async function handleStatus(args, ctx) {
12631
13013
  })
12632
13014
  );
12633
13015
  const client = ctx.session.getActiveContext();
13016
+ const detail = args.detail ?? "summary";
13017
+ if (detail !== "full") {
13018
+ return {
13019
+ client: client.clientName,
13020
+ providers: results.map(({ provider, healthy, credits }) => ({
13021
+ provider,
13022
+ healthy,
13023
+ ...credits && { credits }
13024
+ }))
13025
+ };
13026
+ }
12634
13027
  const configuredCapabilities = new Set(results.flatMap((r) => r.capabilities));
12635
13028
  const unconfigured = TOOL_CAPABILITY_REQUIREMENTS.filter(
12636
13029
  (req) => !configuredCapabilities.has(req.capability)
@@ -12649,7 +13042,7 @@ var init_status2 = __esm({
12649
13042
  init_dist();
12650
13043
  TOOL_CAPABILITY_REQUIREMENTS = [
12651
13044
  {
12652
- tool: "gtm_linkedin",
13045
+ tool: "gtm_extract",
12653
13046
  requires: "harvest",
12654
13047
  capability: "extract_linkedin",
12655
13048
  setup: "Set GTM_HARVEST_API_KEY or add harvest.api_key to config.yaml"
@@ -12679,10 +13072,10 @@ var init_status2 = __esm({
12679
13072
  setup: "Set GTM_PLUSVIBE_API_KEY, GTM_INSTANTLY_API_KEY, or GTM_SMARTLEAD_API_KEY"
12680
13073
  },
12681
13074
  {
12682
- tool: "gtm_outreach",
13075
+ tool: "gtm_campaign",
12683
13076
  requires: "heyreach",
12684
13077
  capability: "outreach_linkedin",
12685
- setup: "Set GTM_HEYREACH_API_KEY"
13078
+ setup: "Set GTM_HEYREACH_API_KEY (LinkedIn outreach via gtm_campaign)"
12686
13079
  },
12687
13080
  { tool: "gtm_crm", requires: "attio", capability: "crm_read", setup: "Set GTM_ATTIO_API_KEY" },
12688
13081
  {
@@ -12753,90 +13146,59 @@ var init_configure2 = __esm({
12753
13146
  }
12754
13147
  });
12755
13148
 
12756
- // src/handlers/linkedin.ts
12757
- var linkedin_exports = {};
12758
- __export(linkedin_exports, {
12759
- handleLinkedin: () => handleLinkedin
13149
+ // src/handlers/feedback.ts
13150
+ var feedback_exports = {};
13151
+ __export(feedback_exports, {
13152
+ handleFeedback: () => handleFeedback
12760
13153
  });
12761
- async function handleLinkedin(args, ctx) {
12762
- const adapter = ctx.registry.get("harvest");
12763
- if (!adapter) {
12764
- throw Object.assign(
12765
- new Error(
12766
- "HarvestAPI provider not configured. Add harvest.api_key to your config.yaml or set GTM_HARVEST_API_KEY environment variable."
12767
- ),
12768
- { statusCode: 503 }
13154
+ async function handleFeedback(args) {
13155
+ const input = args;
13156
+ if (!SUPABASE_KEY) {
13157
+ throw createGtmError(
13158
+ "PROVIDER_NOT_CONFIGURED",
13159
+ "Supabase not configured for feedback. Set GTM_SUPABASE_KEY or SUPABASE_SERVICE_ROLE_KEY env var."
12769
13160
  );
12770
13161
  }
12771
- const action = args.action;
12772
- switch (action) {
12773
- case "profile": {
12774
- const profile = await adapter.getProfile(
12775
- args.linkedin_url,
12776
- args.find_email
12777
- );
12778
- return { action: "profile", provider: "harvest", profile };
12779
- }
12780
- case "posts": {
12781
- const result = await adapter.getProfilePosts({
12782
- profile: args.profile,
12783
- postedLimit: args.posted_within
12784
- });
12785
- return {
12786
- action: "posts",
12787
- provider: "harvest",
12788
- count: result.elements.length,
12789
- posts: result.elements,
12790
- pagination: result.pagination
12791
- };
12792
- }
12793
- case "company_posts": {
12794
- const result = await adapter.getCompanyPosts({
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}`);
13162
+ const row = {
13163
+ type: input.type,
13164
+ message: input.message.trim(),
13165
+ context: input.context ?? {}
13166
+ };
13167
+ const response = await fetch(`${SUPABASE_URL}/rest/v1/gtm_feedback`, {
13168
+ method: "POST",
13169
+ headers: {
13170
+ "Content-Type": "application/json",
13171
+ apikey: SUPABASE_KEY,
13172
+ Authorization: `Bearer ${SUPABASE_KEY}`,
13173
+ Prefer: "return=representation"
13174
+ },
13175
+ body: JSON.stringify(row)
13176
+ });
13177
+ if (!response.ok) {
13178
+ const body = await response.text().catch(() => "unknown");
13179
+ throw createGtmError(
13180
+ "PROVIDER_REQUEST_FAILED",
13181
+ `Failed to submit feedback: ${response.status}`,
13182
+ {
13183
+ details: { status: response.status, body }
13184
+ }
13185
+ );
12835
13186
  }
13187
+ const [created] = await response.json();
13188
+ return {
13189
+ submitted: true,
13190
+ id: created?.id,
13191
+ type: input.type,
13192
+ message: `Feedback submitted. Thank you! (ID: ${created?.id ?? "unknown"})`
13193
+ };
12836
13194
  }
12837
- var init_linkedin2 = __esm({
12838
- "src/handlers/linkedin.ts"() {
13195
+ var SUPABASE_URL, SUPABASE_KEY;
13196
+ var init_feedback2 = __esm({
13197
+ "src/handlers/feedback.ts"() {
12839
13198
  "use strict";
13199
+ init_errors();
13200
+ SUPABASE_URL = process.env.GTM_SUPABASE_URL ?? process.env.SUPABASE_URL ?? "https://qvawbxpijxlwdkolmjrs.supabase.co";
13201
+ SUPABASE_KEY = process.env.GTM_SUPABASE_KEY ?? process.env.SUPABASE_SERVICE_ROLE_KEY ?? "";
12840
13202
  }
12841
13203
  });
12842
13204
 
@@ -12850,13 +13212,34 @@ function isRateLimitError2(error) {
12850
13212
  async function sleep(ms) {
12851
13213
  return new Promise((resolve) => setTimeout(resolve, ms));
12852
13214
  }
13215
+ function withHandlerTimeout(promise, ms) {
13216
+ return new Promise((resolve, reject) => {
13217
+ const timer = setTimeout(
13218
+ () => reject(
13219
+ Object.assign(new Error(`Tool call timed out after ${ms}ms`), { code: "HANDLER_TIMEOUT" })
13220
+ ),
13221
+ ms
13222
+ );
13223
+ promise.then(
13224
+ (val) => {
13225
+ clearTimeout(timer);
13226
+ resolve(val);
13227
+ },
13228
+ (err) => {
13229
+ clearTimeout(timer);
13230
+ reject(err);
13231
+ }
13232
+ );
13233
+ });
13234
+ }
12853
13235
  async function handleToolCall(name, args, ctx) {
12854
13236
  let lastError;
12855
13237
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
12856
13238
  try {
12857
- const result = await dispatch(name, args, ctx);
13239
+ const timeoutMs = args._testTimeoutMs ?? HANDLER_TIMEOUT_MS;
13240
+ const result = await withHandlerTimeout(dispatch(name, args, ctx), timeoutMs);
12858
13241
  return {
12859
- content: [{ type: "text", text: JSON.stringify(result, null, 2) ?? "{}" }]
13242
+ content: [{ type: "text", text: JSON.stringify(result) ?? "{}" }]
12860
13243
  };
12861
13244
  } catch (error) {
12862
13245
  lastError = error;
@@ -12868,6 +13251,13 @@ async function handleToolCall(name, args, ctx) {
12868
13251
  break;
12869
13252
  }
12870
13253
  }
13254
+ if (lastError instanceof Error && "code" in lastError && lastError.code === "HANDLER_TIMEOUT") {
13255
+ const wrapped2 = createGtmError("HANDLER_TIMEOUT", lastError.message);
13256
+ return {
13257
+ content: [{ type: "text", text: JSON.stringify(wrapped2) }],
13258
+ isError: true
13259
+ };
13260
+ }
12871
13261
  if (isGtmError(lastError)) {
12872
13262
  return {
12873
13263
  content: [{ type: "text", text: JSON.stringify(lastError) }],
@@ -12915,10 +13305,6 @@ async function dispatch(name, args, ctx) {
12915
13305
  const { handleCampaign: handleCampaign2 } = await Promise.resolve().then(() => (init_campaign2(), campaign_exports));
12916
13306
  return handleCampaign2(args, ctx);
12917
13307
  }
12918
- case "gtm_outreach": {
12919
- const { handleOutreach: handleOutreach2 } = await Promise.resolve().then(() => (init_outreach2(), outreach_exports));
12920
- return handleOutreach2(args, ctx);
12921
- }
12922
13308
  case "gtm_crm": {
12923
13309
  const { handleCrm: handleCrm2 } = await Promise.resolve().then(() => (init_crm2(), crm_exports));
12924
13310
  return handleCrm2(args, ctx);
@@ -12931,9 +13317,9 @@ async function dispatch(name, args, ctx) {
12931
13317
  const { handleConfigure: handleConfigure2 } = await Promise.resolve().then(() => (init_configure2(), configure_exports));
12932
13318
  return handleConfigure2(args, ctx);
12933
13319
  }
12934
- case "gtm_linkedin": {
12935
- const { handleLinkedin: handleLinkedin2 } = await Promise.resolve().then(() => (init_linkedin2(), linkedin_exports));
12936
- return handleLinkedin2(args, ctx);
13320
+ case "gtm_feedback": {
13321
+ const { handleFeedback: handleFeedback2 } = await Promise.resolve().then(() => (init_feedback2(), feedback_exports));
13322
+ return handleFeedback2(args);
12937
13323
  }
12938
13324
  // ─── Meta-Tools ───────────────────────────────────────
12939
13325
  case "gtm_guide": {
@@ -12949,7 +13335,7 @@ async function dispatch(name, args, ctx) {
12949
13335
  }
12950
13336
  const result = {
12951
13337
  name: tool.name,
12952
- description: tool.description,
13338
+ description: TOOL_DESCRIPTIONS[tool.name] ?? tool.description,
12953
13339
  inputSchema: tool.inputSchema
12954
13340
  };
12955
13341
  const toolKey = toolName.replace("gtm_", "");
@@ -12972,7 +13358,7 @@ async function dispatch(name, args, ctx) {
12972
13358
  throw new Error(`Unknown tool: ${name}`);
12973
13359
  }
12974
13360
  }
12975
- var MAX_RETRIES, BASE_DELAY_MS;
13361
+ var MAX_RETRIES, BASE_DELAY_MS, HANDLER_TIMEOUT_MS;
12976
13362
  var init_handlers = __esm({
12977
13363
  "src/handlers/index.ts"() {
12978
13364
  "use strict";
@@ -12980,8 +13366,10 @@ var init_handlers = __esm({
12980
13366
  init_errors();
12981
13367
  init_manifests();
12982
13368
  init_tools();
13369
+ init_descriptions();
12983
13370
  MAX_RETRIES = 3;
12984
13371
  BASE_DELAY_MS = 1e3;
13372
+ HANDLER_TIMEOUT_MS = 12e4;
12985
13373
  }
12986
13374
  });
12987
13375
 
@@ -13002,7 +13390,7 @@ function validateToolArgs(toolName, args) {
13002
13390
  }
13003
13391
  return { success: true, data: result.data };
13004
13392
  }
13005
- var contactInputSchema, campaignLeadSchema, outreachLeadSchema, toolSchemas, TOOL_NAMES;
13393
+ var contactInputSchema, campaignLeadSchema, detailSchema, toolSchemas, TOOL_NAMES;
13006
13394
  var init_validation = __esm({
13007
13395
  "src/validation.ts"() {
13008
13396
  "use strict";
@@ -13014,18 +13402,16 @@ var init_validation = __esm({
13014
13402
  linkedin_url: z2.string().optional()
13015
13403
  });
13016
13404
  campaignLeadSchema = z2.object({
13017
- email: z2.string().email("email must be a valid email address"),
13405
+ email: z2.string().email("email must be a valid email address").optional(),
13406
+ linkedin_url: z2.string().min(1, "linkedin_url is required").optional(),
13018
13407
  first_name: z2.string().optional(),
13019
13408
  last_name: z2.string().optional(),
13020
13409
  company: z2.string().optional(),
13021
13410
  custom_fields: z2.record(z2.string()).optional()
13411
+ }).refine((data) => !!data.email || !!data.linkedin_url, {
13412
+ message: "At least one of email or linkedin_url is required"
13022
13413
  });
13023
- outreachLeadSchema = z2.object({
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
- });
13414
+ detailSchema = z2.enum(["summary", "full"]).optional();
13029
13415
  toolSchemas = {
13030
13416
  // 1. Company discovery
13031
13417
  gtm_find_companies: z2.object({
@@ -13049,7 +13435,8 @@ var init_validation = __esm({
13049
13435
  platform: z2.string().optional(),
13050
13436
  min_revenue: z2.number().optional(),
13051
13437
  max_revenue: z2.number().optional(),
13052
- output: z2.enum(["file", "inline"]).optional()
13438
+ output: z2.enum(["file", "inline", "csv"]).optional(),
13439
+ detail: detailSchema
13053
13440
  }),
13054
13441
  // 2. People/contact search
13055
13442
  gtm_find_people: z2.object({
@@ -13089,16 +13476,43 @@ var init_validation = __esm({
13089
13476
  max_employees: z2.number().int().min(1).optional(),
13090
13477
  revenue_range: z2.string().optional(),
13091
13478
  fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
13092
- output: z2.enum(["file", "inline"]).optional()
13479
+ output: z2.enum(["file", "inline", "csv"]).optional(),
13480
+ detail: detailSchema
13093
13481
  }).refine(
13094
13482
  (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
13483
  {
13096
13484
  message: "At least one search filter required (job_title, company_name, company_domain, company_domains, seniority, filter_industry, skills, or location)"
13097
13485
  }
13098
13486
  ),
13099
- // LinkedIn intelligence — HarvestAPI
13100
- gtm_linkedin: z2.object({
13101
- action: z2.enum(["profile", "posts", "engagement", "company_posts", "search_posts"]),
13487
+ // 3. Enrichment waterfall
13488
+ gtm_enrich: z2.object({
13489
+ contacts: z2.array(contactInputSchema).min(1, "contacts must contain at least 1 contact").max(100, "contacts must contain at most 100 contacts"),
13490
+ find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
13491
+ waterfall: z2.array(z2.string()).optional(),
13492
+ verify: z2.boolean().optional(),
13493
+ dry_run: z2.boolean().optional(),
13494
+ detail: detailSchema
13495
+ }),
13496
+ // 4. Email verification
13497
+ gtm_verify: z2.object({
13498
+ email: z2.string().email("email must be a valid email address")
13499
+ }),
13500
+ // 5. Extract/scrape — web pages + all LinkedIn intelligence
13501
+ gtm_extract: z2.object({
13502
+ type: z2.enum(
13503
+ [
13504
+ "linkedin_profile",
13505
+ "linkedin_posts",
13506
+ "linkedin_engagement",
13507
+ "linkedin_search",
13508
+ "linkedin_company_posts",
13509
+ "web_page"
13510
+ ],
13511
+ {
13512
+ message: "type must be one of: linkedin_profile, linkedin_posts, linkedin_engagement, linkedin_search, linkedin_company_posts, web_page"
13513
+ }
13514
+ ),
13515
+ url: z2.string().url("url must be a valid URL").optional(),
13102
13516
  linkedin_url: z2.string().optional(),
13103
13517
  find_email: z2.boolean().optional(),
13104
13518
  profile: z2.string().optional(),
@@ -13108,53 +13522,38 @@ var init_validation = __esm({
13108
13522
  engagement_type: z2.enum(["comments", "reactions", "both"]).optional(),
13109
13523
  query: z2.string().optional(),
13110
13524
  sort: z2.enum(["relevance", "date"]).optional(),
13111
- limit: z2.number().int().min(1).max(100).optional()
13525
+ limit: z2.number().int().min(1).max(100).optional(),
13526
+ fields: z2.array(z2.string()).optional(),
13527
+ detail: detailSchema
13112
13528
  }).refine(
13113
13529
  (data) => {
13114
- switch (data.action) {
13115
- case "profile":
13116
- return !!data.linkedin_url;
13117
- case "engagement":
13530
+ switch (data.type) {
13531
+ case "linkedin_profile":
13532
+ return !!(data.linkedin_url || data.url);
13533
+ case "linkedin_posts":
13534
+ return !!data.profile;
13535
+ case "linkedin_engagement":
13118
13536
  return !!data.post_url;
13119
- case "search_posts":
13537
+ case "linkedin_search":
13120
13538
  return !!data.query;
13121
- case "posts":
13122
- return !!data.profile;
13123
- case "company_posts":
13539
+ case "linkedin_company_posts":
13124
13540
  return !!data.company;
13541
+ case "web_page":
13542
+ return !!data.url;
13125
13543
  default:
13126
13544
  return true;
13127
13545
  }
13128
13546
  },
13129
- { message: "Missing required field for the specified action" }
13547
+ { message: "Missing required field for the specified type" }
13130
13548
  ),
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
13549
  // 6. Raw HTTP fetch
13152
13550
  gtm_fetch: z2.object({
13153
13551
  url: z2.string().url("url must be a valid URL"),
13154
13552
  method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).optional(),
13155
13553
  headers: z2.record(z2.string()).optional(),
13156
13554
  body: z2.record(z2.unknown()).optional(),
13157
- params: z2.record(z2.string()).optional()
13555
+ params: z2.record(z2.string()).optional(),
13556
+ detail: detailSchema
13158
13557
  }),
13159
13558
  // 7. AI data operations
13160
13559
  gtm_ai: z2.object({
@@ -13167,7 +13566,7 @@ var init_validation = __esm({
13167
13566
  prompt: z2.string().optional(),
13168
13567
  model: z2.string().optional()
13169
13568
  }),
13170
- // 8. Cold email campaign management
13569
+ // 8. Campaign management (cold email + LinkedIn outreach)
13171
13570
  gtm_campaign: z2.object({
13172
13571
  action: z2.enum(["list", "stats", "add_leads", "create"], {
13173
13572
  message: "action must be one of: list, stats, add_leads, create"
@@ -13175,15 +13574,8 @@ var init_validation = __esm({
13175
13574
  campaign_id: z2.string().optional(),
13176
13575
  leads: z2.array(campaignLeadSchema).optional(),
13177
13576
  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()
13577
+ provider: z2.string().optional(),
13578
+ detail: detailSchema
13187
13579
  }),
13188
13580
  // 10. CRM read/write
13189
13581
  gtm_crm: z2.object({
@@ -13196,7 +13588,8 @@ var init_validation = __esm({
13196
13588
  }),
13197
13589
  // 11. Provider status/credits
13198
13590
  gtm_status: z2.object({
13199
- provider: z2.string().optional()
13591
+ provider: z2.string().optional(),
13592
+ detail: detailSchema
13200
13593
  }),
13201
13594
  // 12. Configuration management
13202
13595
  gtm_configure: z2.object({
@@ -13218,6 +13611,7 @@ var init_validation = __esm({
13218
13611
  "cold_outreach",
13219
13612
  "linkedin_outreach",
13220
13613
  "crm_sync",
13614
+ "export_to_webhook",
13221
13615
  "list_tasks"
13222
13616
  ])
13223
13617
  }),
@@ -13229,6 +13623,14 @@ var init_validation = __esm({
13229
13623
  gtm_execute: z2.object({
13230
13624
  tool: z2.string().min(1, "tool is required"),
13231
13625
  arguments: z2.record(z2.unknown()).optional()
13626
+ }),
13627
+ // 16. Feedback
13628
+ gtm_feedback: z2.object({
13629
+ type: z2.enum(["bug", "feature_request", "provider_request"], {
13630
+ message: "type must be one of: bug, feature_request, provider_request"
13631
+ }),
13632
+ message: z2.string().min(1, "message is required"),
13633
+ context: z2.record(z2.unknown()).optional()
13232
13634
  })
13233
13635
  };
13234
13636
  TOOL_NAMES = Object.keys(toolSchemas);
@@ -13299,15 +13701,16 @@ function createServer() {
13299
13701
  isError: true
13300
13702
  };
13301
13703
  }
13704
+ const fullDescription = TOOL_DESCRIPTIONS[tool.name] ?? tool.description;
13302
13705
  return {
13303
13706
  content: [
13304
13707
  {
13305
13708
  type: "text",
13306
- text: JSON.stringify(
13307
- { name: tool.name, description: tool.description, inputSchema: tool.inputSchema },
13308
- null,
13309
- 2
13310
- )
13709
+ text: JSON.stringify({
13710
+ name: tool.name,
13711
+ description: fullDescription,
13712
+ inputSchema: tool.inputSchema
13713
+ })
13311
13714
  }
13312
13715
  ]
13313
13716
  };