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 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: {
@@ -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 >100 results). "inline" returns in response.'
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 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: {
@@ -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 >100 results). "inline" returns in response.'
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 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/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 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: {
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: ["linkedin_profile", "linkedin_post", "web_page"],
10919
- description: "Content type (auto-detected from URL if omitted)"
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
- 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)"
11131
+ description: "Provider override (auto-selects based on available campaign_email or outreach_linkedin provider if omitted)"
11087
11132
  },
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."
11101
- },
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,125 @@ 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: {
11289
+ type: {
11284
11290
  type: "string",
11285
- enum: ["comments", "reactions", "both"],
11286
- description: "Type of engagement to retrieve (default: both)"
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
- query: {
11294
+ message: {
11289
11295
  type: "string",
11290
- description: "Keyword search (required for search_posts action)"
11296
+ description: "Describe the issue, request, or suggestion in detail"
11291
11297
  },
11292
- sort: {
11293
- type: "string",
11294
- enum: ["relevance", "date"],
11295
- description: "Sort order for search_posts"
11296
- },
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 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
- init_linkedin();
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
- init_linkedin();
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
- ...linkedinTools
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: gtm_linkedin \u2014 check if they're active on LinkedIn
11429
- \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
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 or gtm_outreach \u2014 push to campaigns
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. gtm_linkedin \u2014 scrape profiles for activity signals
11441
- \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
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. gtm_outreach \u2014 LinkedIn campaign, or gtm_campaign for email
11529
+ 4. gtm_campaign \u2014 push to LinkedIn or email campaign
11444
11530
 
11445
11531
  ## 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" })
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 gtm_outreach({ action: "list" })
11591
+ \u2192 gtm_campaign({ action: "list", provider: "heyreach" })
11506
11592
 
11507
11593
  4. PUSH \u2014 Add leads to LinkedIn outreach campaign
11508
- \u2192 gtm_outreach({ action: "add_leads", campaign_id: "...", leads: [{ linkedin_url: "..." }] })
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
- - gtm_linkedin \u2014 LinkedIn profile scraping, post search, engagement mining
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 = 100;
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 provider.enrichEmail(contact);
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 provider.enrichPhone(contact);
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: args.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(capability);
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
- `No providers configured for ${capability}. Add a provider with that capability.`
12386
+ "No providers configured for extract_web. Add a provider with that capability."
12201
12387
  );
12202
12388
  }
12203
- const provider = providers[0];
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 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
- };
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
- return {
12277
- status: response.status,
12278
- headers: Object.fromEntries(response.headers.entries()),
12279
- body: responseBody
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
- async function handleCampaign(args, ctx) {
12430
- const action = args.action;
12696
+ function resolveChannel(args, ctx) {
12431
12697
  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) {
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
- "No providers configured for email campaigns. Add a provider with campaign_email capability."
12722
+ `Provider "${providerName}" does not support campaign_email or outreach_linkedin.`
12437
12723
  );
12438
12724
  }
12439
- const provider = providers[0];
12440
- const manager = provider;
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: provider.name, campaigns: await manager.listCampaigns() };
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 leads = args.leads;
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 (!leads?.length)
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 "${provider.name}" does not support campaign creation. Currently only PlusVibe supports this action.`
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: "gtm_linkedin",
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: "gtm_outreach",
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/linkedin.ts
12757
- var linkedin_exports = {};
12758
- __export(linkedin_exports, {
12759
- handleLinkedin: () => handleLinkedin
13058
+ // src/handlers/feedback.ts
13059
+ var feedback_exports = {};
13060
+ __export(feedback_exports, {
13061
+ handleFeedback: () => handleFeedback
12760
13062
  });
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 }
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 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}`);
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 init_linkedin2 = __esm({
12838
- "src/handlers/linkedin.ts"() {
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 result = await dispatch(name, args, ctx);
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, null, 2) ?? "{}" }]
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 "gtm_linkedin": {
12935
- const { handleLinkedin: handleLinkedin2 } = await Promise.resolve().then(() => (init_linkedin2(), linkedin_exports));
12936
- return handleLinkedin2(args, ctx);
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, outreachLeadSchema, toolSchemas, TOOL_NAMES;
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
- 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
- });
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
- // LinkedIn intelligence — HarvestAPI
13100
- gtm_linkedin: z2.object({
13101
- action: z2.enum(["profile", "posts", "engagement", "company_posts", "search_posts"]),
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.action) {
13115
- case "profile":
13116
- return !!data.linkedin_url;
13117
- case "engagement":
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 "search_posts":
13446
+ case "linkedin_search":
13120
13447
  return !!data.query;
13121
- case "posts":
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 action" }
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. Cold email campaign management
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
- { name: tool.name, description: tool.description, inputSchema: tool.inputSchema },
13308
- null,
13309
- 2
13310
- )
13617
+ text: JSON.stringify({
13618
+ name: tool.name,
13619
+ description: fullDescription,
13620
+ inputSchema: tool.inputSchema
13621
+ })
13311
13622
  }
13312
13623
  ]
13313
13624
  };