gtm-now 0.1.0 → 0.3.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
@@ -203,15 +203,19 @@ var init_config = __esm({
203
203
  GTM_ATTIO_API_KEY: "attio",
204
204
  GTM_APOLLO_API_KEY: "apollo",
205
205
  GTM_INSTANTLY_API_KEY: "instantly",
206
- GTM_SMARTLEAD_API_KEY: "smartlead"
206
+ GTM_SMARTLEAD_API_KEY: "smartlead",
207
+ GTM_HARVEST_API_KEY: "harvest",
208
+ GTM_STORELEADS_API_KEY: "storeleads"
207
209
  };
208
210
  ENV_WORKSPACE_MAP = {
209
211
  GTM_PLUSVIBE_WORKSPACE_ID: "plusvibe"
210
212
  };
211
213
  providerConfigSchema = z.object({
212
- api_key: z.string(),
214
+ api_key: z.string().optional(),
213
215
  workspace_id: z.string().optional(),
214
- account_id: z.string().optional()
216
+ account_id: z.string().optional(),
217
+ supabase_url: z.string().optional(),
218
+ supabase_key: z.string().optional()
215
219
  });
216
220
  clientConfigSchema = z.object({
217
221
  providers: z.record(providerConfigSchema).optional(),
@@ -312,7 +316,8 @@ var init_registry = __esm({
312
316
  list() {
313
317
  return [...this.providers.values()].map((p) => ({
314
318
  name: p.name,
315
- capabilities: p.capabilities
319
+ capabilities: p.capabilities,
320
+ notes: p.notes
316
321
  }));
317
322
  }
318
323
  deregisterAll() {
@@ -8128,6 +8133,21 @@ var init_client12 = __esm({
8128
8133
  method: "GET"
8129
8134
  });
8130
8135
  }
8136
+ async getGrowth(domain) {
8137
+ return this.request(`/growth?domain=${encodeURIComponent(domain)}`, {
8138
+ method: "GET"
8139
+ });
8140
+ }
8141
+ async getVendors(domain) {
8142
+ return this.request(`/vendors?domain=${encodeURIComponent(domain)}`, {
8143
+ method: "GET"
8144
+ });
8145
+ }
8146
+ async getMetrics(domain) {
8147
+ return this.request(`/metrics?domain=${encodeURIComponent(domain)}`, {
8148
+ method: "GET"
8149
+ });
8150
+ }
8131
8151
  // ─── Match ─────────────────────────────────────────────
8132
8152
  async match(name) {
8133
8153
  return this.request(`/match?name=${encodeURIComponent(name)}`, {
@@ -8410,6 +8430,7 @@ var init_client18 = __esm({
8410
8430
  var init_client19 = __esm({
8411
8431
  "../integrations/dist/twilio/client.js"() {
8412
8432
  "use strict";
8433
+ init_dist();
8413
8434
  init_base_client();
8414
8435
  }
8415
8436
  });
@@ -8446,6 +8467,66 @@ var init_factory = __esm({
8446
8467
  }
8447
8468
  });
8448
8469
 
8470
+ // ../integrations/dist/harvest/client.js
8471
+ var HarvestClient;
8472
+ var init_client22 = __esm({
8473
+ "../integrations/dist/harvest/client.js"() {
8474
+ "use strict";
8475
+ init_base_client();
8476
+ HarvestClient = class extends BaseApiClient {
8477
+ constructor(config) {
8478
+ super({
8479
+ name: "Harvest",
8480
+ baseUrl: config.baseUrl ?? "https://api.harvest-api.com",
8481
+ headers: { "X-API-Key": config.apiKey },
8482
+ timeoutMs: config.timeoutMs
8483
+ });
8484
+ }
8485
+ // ─── Posts ──────────────────────────────────────────────
8486
+ async searchPosts(options) {
8487
+ const params = this.buildParams(options);
8488
+ return this.request(`/linkedin/post-search?${params}`);
8489
+ }
8490
+ async getProfilePosts(options) {
8491
+ const params = this.buildParams(options);
8492
+ return this.request(`/linkedin/profile-posts?${params}`);
8493
+ }
8494
+ async getCompanyPosts(options) {
8495
+ const params = this.buildParams(options);
8496
+ return this.request(`/linkedin/company-posts?${params}`);
8497
+ }
8498
+ // ─── Engagement ─────────────────────────────────────────
8499
+ async getPostComments(postUrl, options) {
8500
+ const params = this.buildParams({ post: postUrl, ...options });
8501
+ return this.request(`/linkedin/post-comments?${params}`);
8502
+ }
8503
+ async getPostReactions(postUrl, options) {
8504
+ const params = this.buildParams({ post: postUrl, ...options });
8505
+ return this.request(`/linkedin/post-reactions?${params}`);
8506
+ }
8507
+ // ─── Profiles ───────────────────────────────────────────
8508
+ async getProfile(identifier, options) {
8509
+ const isUrl = identifier.includes("linkedin.com");
8510
+ const params = this.buildParams({
8511
+ ...isUrl ? { url: identifier } : { publicIdentifier: identifier },
8512
+ ...options?.findEmail != null && { findEmail: options.findEmail }
8513
+ });
8514
+ return this.request(`/linkedin/profile?${params}`);
8515
+ }
8516
+ // ─── Helpers ────────────────────────────────────────────
8517
+ buildParams(obj) {
8518
+ const params = new URLSearchParams();
8519
+ for (const [key, value] of Object.entries(obj)) {
8520
+ if (value !== void 0 && value !== null && value !== "") {
8521
+ params.set(key, String(value));
8522
+ }
8523
+ }
8524
+ return params;
8525
+ }
8526
+ };
8527
+ }
8528
+ });
8529
+
8449
8530
  // ../integrations/dist/index.js
8450
8531
  var init_dist2 = __esm({
8451
8532
  "../integrations/dist/index.js"() {
@@ -8482,6 +8563,7 @@ var init_dist2 = __esm({
8482
8563
  init_client21();
8483
8564
  init_parser();
8484
8565
  init_factory();
8566
+ init_client22();
8485
8567
  }
8486
8568
  });
8487
8569
 
@@ -8502,6 +8584,7 @@ var init_bettercontact = __esm({
8502
8584
  "verify_email",
8503
8585
  "find_people"
8504
8586
  ];
8587
+ notes = "Best for: email + phone waterfall. Charges only on successful find.";
8505
8588
  client;
8506
8589
  constructor(apiKey) {
8507
8590
  this.client = new BetterContactClient({ apiKey });
@@ -8664,6 +8747,10 @@ function parseName(fullName) {
8664
8747
  if (parts.length === 1) return { first: parts[0], last: "" };
8665
8748
  return { first: parts[0], last: parts.slice(1).join(" ") };
8666
8749
  }
8750
+ function domainToCompanyName(domain) {
8751
+ const name = domain.split(".")[0] ?? domain;
8752
+ return name.charAt(0).toUpperCase() + name.slice(1);
8753
+ }
8667
8754
  var DiscoAdapter;
8668
8755
  var init_disco = __esm({
8669
8756
  "src/providers/adapters/disco.ts"() {
@@ -8673,6 +8760,7 @@ var init_disco = __esm({
8673
8760
  DiscoAdapter = class {
8674
8761
  name = "disco";
8675
8762
  capabilities = ["find_companies", "find_people"];
8763
+ notes = "Best for: lookalike discovery, ICP text matching, tech stack filtering, website-content businesses. 60M+ company database from web crawl.";
8676
8764
  client;
8677
8765
  constructor(apiKey) {
8678
8766
  this.client = new DiscoClient({ apiKey });
@@ -8689,13 +8777,17 @@ var init_disco = __esm({
8689
8777
  // ─── CompanyFinder ──────────────────────────────────────
8690
8778
  async findCompanies(query) {
8691
8779
  const response = await this.client.discover({
8692
- domain: query.domain,
8780
+ domain: query.domains ?? query.domain,
8693
8781
  icp_text: query.icp_text,
8694
8782
  max_records: query.limit,
8695
8783
  country: query.country,
8696
8784
  ...query.min_employees != null && query.max_employees != null && {
8697
8785
  employee_range: `${query.min_employees},${query.max_employees}`
8698
- }
8786
+ },
8787
+ tech_stack: query.tech_stack,
8788
+ category: query.category?.toUpperCase(),
8789
+ negate_domain: query.negate_domains,
8790
+ offset: query.offset
8699
8791
  });
8700
8792
  return response.map((c) => ({
8701
8793
  domain: c.domain,
@@ -8705,23 +8797,40 @@ var init_disco = __esm({
8705
8797
  employees: c.employees ? parseInt(c.employees, 10) || void 0 : void 0,
8706
8798
  country: c.address?.country,
8707
8799
  linkedin_url: c.social_urls?.find((u) => u.includes("linkedin")),
8708
- score: c.score
8800
+ score: c.score,
8801
+ tech_stack: c.keywords ? Object.keys(c.keywords) : void 0,
8802
+ sources: [this.name]
8709
8803
  }));
8710
8804
  }
8711
8805
  async countCompanies(query) {
8712
8806
  const response = await this.client.count({
8713
- domain: query.domain,
8807
+ domain: query.domains ?? query.domain,
8714
8808
  icp_text: query.icp_text,
8715
8809
  country: query.country,
8716
8810
  ...query.min_employees != null && query.max_employees != null && {
8717
8811
  employee_range: `${query.min_employees},${query.max_employees}`
8718
- }
8812
+ },
8813
+ tech_stack: query.tech_stack,
8814
+ category: query.category?.toUpperCase(),
8815
+ negate_domain: query.negate_domains
8816
+ // offset intentionally omitted — count is always total, not paged
8719
8817
  });
8720
8818
  return response.count;
8721
8819
  }
8722
8820
  async getCompany(domain) {
8723
8821
  try {
8724
8822
  const data = await this.client.getBizData(domain);
8823
+ const [growth, vendors, metrics] = await Promise.allSettled([
8824
+ this.client.getGrowth(domain),
8825
+ this.client.getVendors(domain),
8826
+ this.client.getMetrics(domain)
8827
+ ]);
8828
+ const growthData = growth.status === "fulfilled" ? growth.value : void 0;
8829
+ const vendorsData = vendors.status === "fulfilled" ? vendors.value : void 0;
8830
+ const metricsData = metrics.status === "fulfilled" ? metrics.value : void 0;
8831
+ const keywordTech = data.keywords ? Object.keys(data.keywords) : [];
8832
+ const vendorTech = vendorsData?.vendors?.map((v) => v.name) ?? [];
8833
+ const mergedTech = [.../* @__PURE__ */ new Set([...keywordTech, ...vendorTech])];
8725
8834
  return {
8726
8835
  domain: data.domain,
8727
8836
  name: data.name,
@@ -8729,10 +8838,22 @@ var init_disco = __esm({
8729
8838
  industry: data.industry_groups ? Object.keys(data.industry_groups)[0] : void 0,
8730
8839
  employees: data.employees ? parseInt(data.employees, 10) || void 0 : void 0,
8731
8840
  country: data.address?.country,
8732
- linkedin_url: data.social_urls?.find((u) => u.includes("linkedin"))
8841
+ linkedin_url: data.social_urls?.find((u) => u.includes("linkedin")),
8842
+ tech_stack: mergedTech.length > 0 ? mergedTech : void 0,
8843
+ growth_signals: growthData ? {
8844
+ quarterly_growth: growthData.quarterly_growth,
8845
+ digital_footprint_trend: growthData.digital_footprint_trend
8846
+ } : void 0,
8847
+ engagement_metrics: metricsData ? {
8848
+ traffic_estimate: metricsData.traffic_estimate,
8849
+ social_presence: metricsData.social_presence
8850
+ } : void 0,
8851
+ sources: [this.name]
8733
8852
  };
8734
- } catch (_error) {
8735
- logWarn("disco:getCompany", `Failed to get company data for ${domain}`);
8853
+ } catch (error) {
8854
+ logError("disco:getCompany", error instanceof Error ? error : new Error(String(error)), {
8855
+ domain
8856
+ });
8736
8857
  return null;
8737
8858
  }
8738
8859
  }
@@ -8744,9 +8865,15 @@ var init_disco = __esm({
8744
8865
  icp_text: !query.company_domain && query.company_name ? query.company_name : void 0,
8745
8866
  title: query.job_title,
8746
8867
  person_country: query.location,
8747
- // Only return contacts with email — essential for GTM outreach
8748
- has_email: true,
8749
- max_records: query.limit ?? 25
8868
+ has_email: query.has_email ?? true,
8869
+ max_records: query.limit ?? 25,
8870
+ // Cast to DiscoSeniority — PeopleQuery uses string[] for cross-provider compat
8871
+ seniority: query.seniority,
8872
+ department: query.department,
8873
+ has_phone: query.has_phone,
8874
+ has_linkedin: query.has_linkedin,
8875
+ min_connections: query.min_connections,
8876
+ offset: query.offset
8750
8877
  });
8751
8878
  return response.map((c) => {
8752
8879
  const nameParts = parseName(c.name);
@@ -8754,10 +8881,17 @@ var init_disco = __esm({
8754
8881
  first_name: nameParts.first,
8755
8882
  last_name: nameParts.last,
8756
8883
  job_title: c.title,
8757
- company: c.domain,
8884
+ company: domainToCompanyName(c.domain),
8885
+ company_domain: c.domain,
8758
8886
  linkedin_url: c.social_urls?.find((u) => u.includes("linkedin")),
8759
8887
  email: c.email,
8760
- phone: c.phone
8888
+ phone: c.phone,
8889
+ seniority: c.seniority,
8890
+ department: c.department,
8891
+ skills: c.skills,
8892
+ connections: c.connections,
8893
+ location: c.country,
8894
+ sources: [this.name]
8761
8895
  };
8762
8896
  });
8763
8897
  }
@@ -8775,6 +8909,7 @@ var init_prospeo2 = __esm({
8775
8909
  ProspeoMcpAdapter = class {
8776
8910
  name = "prospeo";
8777
8911
  capabilities = ["enrich_email"];
8912
+ notes = "Best for: email finding from LinkedIn URLs. Good hit rate on LinkedIn-active professionals.";
8778
8913
  client;
8779
8914
  constructor(apiKey) {
8780
8915
  this.client = new ProspeoAdapter(apiKey);
@@ -8830,6 +8965,7 @@ var init_plusvibe = __esm({
8830
8965
  PlusVibeAdapter = class {
8831
8966
  name = "plusvibe";
8832
8967
  capabilities = ["campaign_email"];
8968
+ notes = "Best for: cold email campaign management, inbox rotation, analytics.";
8833
8969
  client;
8834
8970
  constructor(apiKey, workspaceId) {
8835
8971
  this.client = new PlusVibeClient(apiKey, workspaceId);
@@ -8896,6 +9032,7 @@ var init_heyreach = __esm({
8896
9032
  HeyReachAdapter = class {
8897
9033
  name = "heyreach";
8898
9034
  capabilities = ["outreach_linkedin"];
9035
+ notes = "Best for: LinkedIn outreach automation, connection requests, messaging campaigns.";
8899
9036
  client;
8900
9037
  constructor(apiKey) {
8901
9038
  this.client = new HeyReachClient(apiKey);
@@ -8963,6 +9100,7 @@ var init_attio = __esm({
8963
9100
  AttioAdapter = class {
8964
9101
  name = "attio";
8965
9102
  capabilities = ["crm_read", "crm_write"];
9103
+ notes = "Best for: CRM operations \u2014 people, companies, deals, tags.";
8966
9104
  client;
8967
9105
  constructor(apiKey) {
8968
9106
  this.client = new AttioClient(apiKey);
@@ -9063,6 +9201,7 @@ var init_apollo = __esm({
9063
9201
  ApolloAdapter = class {
9064
9202
  name = "apollo";
9065
9203
  capabilities = ["find_companies", "find_people", "enrich_email"];
9204
+ notes = "Best for: people search by job title/company, org enrichment, email finding. 275M+ contact database.";
9066
9205
  client;
9067
9206
  constructor(apiKey) {
9068
9207
  this.client = new ApolloClient({ apiKey });
@@ -9081,6 +9220,8 @@ var init_apollo = __esm({
9081
9220
  }
9082
9221
  // ─── CompanyFinder ──────────────────────────────────────
9083
9222
  async findCompanies(query) {
9223
+ const perPage = Math.min(query.limit ?? 25, 100);
9224
+ const page = query.offset ? Math.floor(query.offset / perPage) + 1 : 1;
9084
9225
  const response = await this.client.searchOrganizations({
9085
9226
  ...query.domain && { q_organization_domains: query.domain },
9086
9227
  ...query.icp_text && { q_organization_keyword_tags: [query.icp_text] },
@@ -9088,8 +9229,8 @@ var init_apollo = __esm({
9088
9229
  ...query.min_employees != null && query.max_employees != null && {
9089
9230
  organization_num_employees_ranges: [`${query.min_employees},${query.max_employees}`]
9090
9231
  },
9091
- per_page: Math.min(query.limit ?? 25, 100),
9092
- page: 1
9232
+ per_page: perPage,
9233
+ page
9093
9234
  });
9094
9235
  return response.organizations.map((org) => ({
9095
9236
  domain: org.primary_domain ?? org.website_url ?? "",
@@ -9098,7 +9239,11 @@ var init_apollo = __esm({
9098
9239
  industry: org.industry ?? void 0,
9099
9240
  employees: org.estimated_num_employees ?? void 0,
9100
9241
  country: org.country ?? void 0,
9101
- linkedin_url: org.linkedin_url ?? void 0
9242
+ linkedin_url: org.linkedin_url ?? void 0,
9243
+ tech_stack: org.technology_names?.length ? org.technology_names : void 0,
9244
+ funding: org.total_funding_printed ?? void 0,
9245
+ revenue_estimate: org.annual_revenue ?? void 0,
9246
+ sources: [this.name]
9102
9247
  }));
9103
9248
  }
9104
9249
  async getCompany(domain) {
@@ -9113,22 +9258,31 @@ var init_apollo = __esm({
9113
9258
  industry: org.industry ?? void 0,
9114
9259
  employees: org.estimated_num_employees ?? void 0,
9115
9260
  country: org.country ?? void 0,
9116
- linkedin_url: org.linkedin_url ?? void 0
9261
+ linkedin_url: org.linkedin_url ?? void 0,
9262
+ tech_stack: org.technology_names?.length ? org.technology_names : void 0,
9263
+ funding: org.total_funding_printed ?? void 0,
9264
+ revenue_estimate: org.annual_revenue ?? void 0,
9265
+ sources: [this.name]
9117
9266
  };
9118
- } catch (_error) {
9119
- logWarn("apollo:getCompany", `Failed to enrich company data for ${domain}`);
9267
+ } catch (error) {
9268
+ logError("apollo:getCompany", error instanceof Error ? error : new Error(String(error)), {
9269
+ domain
9270
+ });
9120
9271
  return null;
9121
9272
  }
9122
9273
  }
9123
9274
  // ─── PeopleFinder ───────────────────────────────────────
9124
9275
  async findPeople(query) {
9276
+ const perPage = Math.min(query.limit ?? 25, 100);
9277
+ const page = query.offset ? Math.floor(query.offset / perPage) + 1 : 1;
9125
9278
  const response = await this.client.searchPeople({
9126
9279
  ...query.job_title && { person_titles: [query.job_title] },
9127
9280
  ...query.company_domain && { q_organization_domains: query.company_domain },
9128
9281
  ...query.company_name && { q_keywords: query.company_name },
9129
9282
  ...query.location && { person_locations: [query.location] },
9130
- per_page: Math.min(query.limit ?? 25, 100),
9131
- page: 1
9283
+ ...query.seniority && { person_seniorities: query.seniority },
9284
+ per_page: perPage,
9285
+ page
9132
9286
  });
9133
9287
  return response.people.map((p) => ({
9134
9288
  first_name: p.first_name,
@@ -9137,7 +9291,13 @@ var init_apollo = __esm({
9137
9291
  company: p.organization_name ?? void 0,
9138
9292
  linkedin_url: p.linkedin_url ?? void 0,
9139
9293
  // People search does NOT return email — null by design
9140
- email: p.email ?? void 0
9294
+ email: p.email ?? void 0,
9295
+ phone: p.phone_number ?? void 0,
9296
+ seniority: p.seniority ?? void 0,
9297
+ department: p.departments?.[0] ?? void 0,
9298
+ headline: p.headline ?? void 0,
9299
+ location: [p.city, p.state, p.country].filter(Boolean).join(", ") || void 0,
9300
+ sources: [this.name]
9141
9301
  }));
9142
9302
  }
9143
9303
  // ─── EmailEnricher ──────────────────────────────────────
@@ -9171,6 +9331,7 @@ var init_instantly = __esm({
9171
9331
  InstantlyAdapter = class {
9172
9332
  name = "instantly";
9173
9333
  capabilities = ["campaign_email"];
9334
+ notes = "Best for: cold email campaigns with built-in warmup, deliverability tools, and analytics.";
9174
9335
  client;
9175
9336
  constructor(apiKey) {
9176
9337
  this.client = new InstantlyClient(apiKey);
@@ -9238,6 +9399,7 @@ var init_smartlead = __esm({
9238
9399
  SmartLeadAdapter = class {
9239
9400
  name = "smartlead";
9240
9401
  capabilities = ["campaign_email"];
9402
+ notes = "Best for: cold email campaigns with multi-mailbox rotation and AI warmup.";
9241
9403
  client;
9242
9404
  constructor(apiKey) {
9243
9405
  this.client = new SmartLeadClient(apiKey);
@@ -9276,13 +9438,18 @@ var init_smartlead = __esm({
9276
9438
  };
9277
9439
  }
9278
9440
  async addLeadsToCampaign(campaignId, leads) {
9279
- const smartLeadLeads = leads.map((l) => ({
9280
- email: l.email,
9281
- first_name: l.first_name ?? "",
9282
- last_name: l.last_name ?? "",
9283
- company_name: l.company ?? "",
9284
- custom_fields: l.custom_fields
9285
- }));
9441
+ const smartLeadLeads = leads.map((l) => {
9442
+ const { phone_number, linkedin_profile, ...remainingCustom } = l.custom_fields ?? {};
9443
+ return {
9444
+ email: l.email,
9445
+ first_name: l.first_name ?? "",
9446
+ last_name: l.last_name ?? "",
9447
+ company_name: l.company ?? "",
9448
+ ...phone_number && { phone_number },
9449
+ ...linkedin_profile && { linkedin_profile },
9450
+ ...Object.keys(remainingCustom).length > 0 && { custom_fields: remainingCustom }
9451
+ };
9452
+ });
9286
9453
  const response = await this.client.addLeadsToCampaign(Number(campaignId), smartLeadLeads);
9287
9454
  return {
9288
9455
  added: response.added_count,
@@ -9295,6 +9462,159 @@ var init_smartlead = __esm({
9295
9462
  }
9296
9463
  });
9297
9464
 
9465
+ // src/providers/adapters/storeleads.ts
9466
+ var BASE_URL, MAX_PAGE_SIZE, REQUEST_TIMEOUT_MS, FIELDS, StoreLeadsAdapter;
9467
+ var init_storeleads = __esm({
9468
+ "src/providers/adapters/storeleads.ts"() {
9469
+ "use strict";
9470
+ init_dist();
9471
+ BASE_URL = "https://storeleads.app/json/api/v1/all/domain";
9472
+ MAX_PAGE_SIZE = 50;
9473
+ REQUEST_TIMEOUT_MS = 15e3;
9474
+ FIELDS = [
9475
+ "domain",
9476
+ "merchant_name",
9477
+ "categories",
9478
+ "country_code",
9479
+ "administrative_area_level_1",
9480
+ "employee_count",
9481
+ "estimated_yearly_sales",
9482
+ "description",
9483
+ "contact_info",
9484
+ "platform"
9485
+ ].join(",");
9486
+ StoreLeadsAdapter = class {
9487
+ name = "storeleads";
9488
+ capabilities = ["find_companies"];
9489
+ notes = "Best for: DTC, e-commerce, Shopify brands. 1.2M+ active stores with revenue estimates and categories. Requires API key from storeleads.app.";
9490
+ apiKey;
9491
+ constructor(apiKey) {
9492
+ this.apiKey = apiKey;
9493
+ }
9494
+ async checkCredentials() {
9495
+ try {
9496
+ const url = `${BASE_URL}?page_size=1&fields=domain`;
9497
+ const res = await fetch(url, {
9498
+ headers: { Authorization: `Bearer ${this.apiKey}` },
9499
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
9500
+ });
9501
+ return res.ok;
9502
+ } catch (error) {
9503
+ logError(
9504
+ "storeleads:checkCredentials",
9505
+ error instanceof Error ? error : new Error(String(error))
9506
+ );
9507
+ return false;
9508
+ }
9509
+ }
9510
+ async search(query) {
9511
+ const params = new URLSearchParams();
9512
+ params.set("fields", FIELDS);
9513
+ params.set("sort", "-estimated_yearly_sales");
9514
+ params.set("page_size", String(Math.min(query.limit ?? 50, MAX_PAGE_SIZE)));
9515
+ if (query.search) params.set("q", query.search);
9516
+ if (query.category) params.set("f:categories", query.category);
9517
+ if (query.country) params.set("f:country_code", query.country);
9518
+ if (query.state) params.set("f:administrative_area_level_1", query.state);
9519
+ if (query.platform) params.set("f:platform", query.platform);
9520
+ if (query.min_revenue != null)
9521
+ params.set("f:estimated_yearly_sales:min", String(query.min_revenue));
9522
+ if (query.max_revenue != null)
9523
+ params.set("f:estimated_yearly_sales:max", String(query.max_revenue));
9524
+ if (query.min_employees != null) params.set("f:ec:min", String(query.min_employees));
9525
+ if (query.max_employees != null) params.set("f:ec:max", String(query.max_employees));
9526
+ const url = `${BASE_URL}?${params.toString()}`;
9527
+ const res = await fetch(url, {
9528
+ headers: { Authorization: `Bearer ${this.apiKey}` },
9529
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
9530
+ });
9531
+ if (!res.ok) {
9532
+ const body = await res.text().catch(() => "");
9533
+ throw Object.assign(
9534
+ new Error(`StoreLeads API error ${res.status}: ${body || res.statusText}`),
9535
+ { statusCode: res.status === 429 ? 429 : 502 }
9536
+ );
9537
+ }
9538
+ const data = await res.json();
9539
+ return data.map((d) => ({
9540
+ domain: d.domain ?? "",
9541
+ name: d.merchant_name ?? d.domain ?? "",
9542
+ description: d.description ?? void 0,
9543
+ industry: d.categories?.join(", ") ?? void 0,
9544
+ employees: d.employee_count ?? void 0,
9545
+ country: d.country_code ?? void 0,
9546
+ linkedin_url: d.contact_info?.linkedin ?? void 0,
9547
+ score: d.estimated_yearly_sales ? Number(d.estimated_yearly_sales) : void 0
9548
+ }));
9549
+ }
9550
+ };
9551
+ }
9552
+ });
9553
+
9554
+ // src/providers/adapters/harvest.ts
9555
+ var HarvestApiAdapter;
9556
+ var init_harvest = __esm({
9557
+ "src/providers/adapters/harvest.ts"() {
9558
+ "use strict";
9559
+ init_dist();
9560
+ init_dist2();
9561
+ HarvestApiAdapter = class {
9562
+ name = "harvest";
9563
+ capabilities = [];
9564
+ // Empty capabilities array — handler calls by name via registry.get('harvest'),
9565
+ // not by capability lookup. This avoids collision with extract handler's
9566
+ // getForCapability('extract_linkedin') which would try to cast to Extractor interface.
9567
+ notes = "Best for: LinkedIn profile scraping, post search, engagement mining, activity signals. Called directly by gtm_linkedin tool.";
9568
+ client;
9569
+ credentialsCached = null;
9570
+ constructor(apiKey) {
9571
+ this.client = new HarvestClient({ apiKey });
9572
+ }
9573
+ async checkCredentials() {
9574
+ if (this.credentialsCached !== null) return this.credentialsCached;
9575
+ try {
9576
+ await this.client.searchPosts({ search: "test", postedLimit: "24h" });
9577
+ this.credentialsCached = true;
9578
+ return true;
9579
+ } catch (error) {
9580
+ logError(
9581
+ "harvest:checkCredentials",
9582
+ error instanceof Error ? error : new Error(String(error))
9583
+ );
9584
+ this.credentialsCached = false;
9585
+ return false;
9586
+ }
9587
+ }
9588
+ // ─── Profile ────────────────────────────────────────────
9589
+ async getProfile(identifier, findEmail) {
9590
+ return this.client.getProfile(identifier, findEmail ? { findEmail: true } : void 0);
9591
+ }
9592
+ // ─── Posts ──────────────────────────────────────────────
9593
+ async searchPosts(query, options) {
9594
+ return this.client.searchPosts({ search: query, ...options });
9595
+ }
9596
+ async getProfilePosts(options) {
9597
+ return this.client.getProfilePosts(options);
9598
+ }
9599
+ async getCompanyPosts(options) {
9600
+ return this.client.getCompanyPosts(options);
9601
+ }
9602
+ // ─── Engagement ─────────────────────────────────────────
9603
+ async getPostEngagement(postUrl, type, options) {
9604
+ const [comments, reactions] = await Promise.all([
9605
+ type === "reactions" ? Promise.resolve({ elements: [] }) : this.client.getPostComments(postUrl, options),
9606
+ type === "comments" ? Promise.resolve({ elements: [] }) : this.client.getPostReactions(postUrl, options)
9607
+ ]);
9608
+ return {
9609
+ comments: comments.elements,
9610
+ reactions: reactions.elements,
9611
+ post_url: postUrl
9612
+ };
9613
+ }
9614
+ };
9615
+ }
9616
+ });
9617
+
9298
9618
  // src/providers/adapters/index.ts
9299
9619
  var init_adapters = __esm({
9300
9620
  "src/providers/adapters/index.ts"() {
@@ -9308,6 +9628,8 @@ var init_adapters = __esm({
9308
9628
  init_apollo();
9309
9629
  init_instantly();
9310
9630
  init_smartlead();
9631
+ init_storeleads();
9632
+ init_harvest();
9311
9633
  }
9312
9634
  });
9313
9635
 
@@ -9315,23 +9637,27 @@ var init_adapters = __esm({
9315
9637
  function createAdapter(name, config) {
9316
9638
  switch (name) {
9317
9639
  case "bettercontact":
9318
- return new BetterContactAdapter(config.api_key);
9640
+ return config.api_key ? new BetterContactAdapter(config.api_key) : null;
9319
9641
  case "disco":
9320
- return new DiscoAdapter(config.api_key);
9642
+ return config.api_key ? new DiscoAdapter(config.api_key) : null;
9321
9643
  case "prospeo":
9322
- return new ProspeoMcpAdapter(config.api_key);
9644
+ return config.api_key ? new ProspeoMcpAdapter(config.api_key) : null;
9323
9645
  case "plusvibe":
9324
- return new PlusVibeAdapter(config.api_key, config.workspace_id ?? "");
9646
+ return config.api_key ? new PlusVibeAdapter(config.api_key, config.workspace_id ?? "") : null;
9325
9647
  case "heyreach":
9326
- return new HeyReachAdapter(config.api_key);
9648
+ return config.api_key ? new HeyReachAdapter(config.api_key) : null;
9327
9649
  case "attio":
9328
- return new AttioAdapter(config.api_key);
9650
+ return config.api_key ? new AttioAdapter(config.api_key) : null;
9329
9651
  case "apollo":
9330
- return new ApolloAdapter(config.api_key);
9652
+ return config.api_key ? new ApolloAdapter(config.api_key) : null;
9331
9653
  case "instantly":
9332
- return new InstantlyAdapter(config.api_key);
9654
+ return config.api_key ? new InstantlyAdapter(config.api_key) : null;
9333
9655
  case "smartlead":
9334
- return new SmartLeadAdapter(config.api_key);
9656
+ return config.api_key ? new SmartLeadAdapter(config.api_key) : null;
9657
+ case "storeleads":
9658
+ return config.api_key ? new StoreLeadsAdapter(config.api_key) : null;
9659
+ case "harvest":
9660
+ return config.api_key ? new HarvestApiAdapter(config.api_key) : null;
9335
9661
  default:
9336
9662
  return null;
9337
9663
  }
@@ -9339,7 +9665,6 @@ function createAdapter(name, config) {
9339
9665
  function registerProviders(registry, clientConfig) {
9340
9666
  registry.deregisterAll();
9341
9667
  for (const [name, config] of Object.entries(clientConfig.providers)) {
9342
- if (!config.api_key) continue;
9343
9668
  const adapter = createAdapter(name, config);
9344
9669
  if (adapter) {
9345
9670
  registry.register(adapter);
@@ -9347,7 +9672,7 @@ function registerProviders(registry, clientConfig) {
9347
9672
  capabilities: adapter.capabilities
9348
9673
  });
9349
9674
  } else {
9350
- logWarn("provider:register", `Unknown provider "${name}" in config \u2014 skipped`);
9675
+ logWarn("provider:register", `Could not create adapter for "${name}" \u2014 skipped`);
9351
9676
  }
9352
9677
  }
9353
9678
  }
@@ -9364,6 +9689,15 @@ var prospect_exports = {};
9364
9689
  __export(prospect_exports, {
9365
9690
  handleProspect: () => handleProspect
9366
9691
  });
9692
+ function deduplicatePeople(people) {
9693
+ const seen = /* @__PURE__ */ new Set();
9694
+ return people.filter((p) => {
9695
+ const key = p.email ?? p.linkedin_url ?? `${p.first_name}_${p.last_name}_${p.company}`;
9696
+ if (seen.has(key)) return false;
9697
+ seen.add(key);
9698
+ return true;
9699
+ });
9700
+ }
9367
9701
  async function handleProspect(name, args, ctx) {
9368
9702
  if (name === "gtm_find_companies") {
9369
9703
  return findCompanies(args, ctx);
@@ -9380,16 +9714,43 @@ async function findCompanies(args, ctx) {
9380
9714
  }
9381
9715
  const provider = providers[0];
9382
9716
  const finder = provider;
9717
+ const domain = args.domain;
9718
+ const isDirectLookup = domain && !args.domains && !args.icp_text && !args.category && !args.tech_stack && !args.country && finder.getCompany;
9719
+ if (isDirectLookup) {
9720
+ const company = await finder.getCompany(domain);
9721
+ if (!company) {
9722
+ throw new Error(`Company not found: ${domain}`);
9723
+ }
9724
+ return { provider: provider.name, count: 1, has_more: false, offset: 0, companies: [company] };
9725
+ }
9383
9726
  const query = {
9384
- domain: args.domain,
9727
+ domain,
9385
9728
  icp_text: args.icp_text,
9386
9729
  limit: args.limit,
9387
9730
  country: args.country,
9388
9731
  min_employees: args.min_employees,
9389
- max_employees: args.max_employees
9732
+ max_employees: args.max_employees,
9733
+ // ─── Expanded params ───
9734
+ domains: args.domains,
9735
+ tech_stack: args.tech_stack,
9736
+ category: args.category,
9737
+ negate_domains: args.negate_domains,
9738
+ min_digital_footprint: args.min_digital_footprint,
9739
+ max_digital_footprint: args.max_digital_footprint,
9740
+ offset: args.offset
9741
+ // Note: args.provider is NOT forwarded into the query — it's used by the handler
9742
+ // for provider routing (selecting which adapter to call), not as a search parameter.
9390
9743
  };
9391
9744
  const results = await finder.findCompanies(query);
9392
- return { provider: provider.name, count: results.length, companies: results };
9745
+ const limit = args.limit ?? 50;
9746
+ const offset = args.offset ?? 0;
9747
+ return {
9748
+ provider: provider.name,
9749
+ count: results.length,
9750
+ has_more: results.length >= limit,
9751
+ offset,
9752
+ companies: results
9753
+ };
9393
9754
  }
9394
9755
  async function findPeople(args, ctx) {
9395
9756
  const providerName = args.provider || void 0;
@@ -9406,10 +9767,28 @@ async function findPeople(args, ctx) {
9406
9767
  company_name: args.company_name,
9407
9768
  company_domain: args.company_domain,
9408
9769
  location: args.location,
9409
- limit: args.limit
9770
+ limit: args.limit,
9771
+ // ─── Expanded params ───
9772
+ seniority: args.seniority,
9773
+ department: args.department,
9774
+ has_email: args.has_email,
9775
+ has_phone: args.has_phone,
9776
+ has_linkedin: args.has_linkedin,
9777
+ min_connections: args.min_connections,
9778
+ offset: args.offset
9779
+ // Note: args.provider is NOT forwarded into the query — used for provider routing only.
9410
9780
  };
9411
9781
  const results = await finder.findPeople(query);
9412
- return { provider: provider.name, count: results.length, people: results };
9782
+ const dedupedResults = deduplicatePeople(results);
9783
+ const limit = args.limit ?? 25;
9784
+ const offset = args.offset ?? 0;
9785
+ return {
9786
+ provider: provider.name,
9787
+ count: dedupedResults.length,
9788
+ has_more: dedupedResults.length >= limit,
9789
+ offset,
9790
+ people: dedupedResults
9791
+ };
9413
9792
  }
9414
9793
  var init_prospect = __esm({
9415
9794
  "src/handlers/prospect.ts"() {
@@ -9554,6 +9933,16 @@ async function enrichContacts(args, ctx) {
9554
9933
  const find = args.find ?? ["email"];
9555
9934
  const verify = args.verify ?? true;
9556
9935
  const dryRun = args.dry_run ?? false;
9936
+ const WATERFALL_FIELDS = ["email", "phone"];
9937
+ const richFields = find.filter((f) => !WATERFALL_FIELDS.includes(f));
9938
+ if (richFields.length > 0) {
9939
+ return {
9940
+ error: "rich_enrichment_not_available",
9941
+ supported_fields: WATERFALL_FIELDS,
9942
+ requested_rich_fields: richFields,
9943
+ message: `Rich enrichment fields (${richFields.join(", ")}) require the Enricher interface, coming in Phase 2. Currently supported: ${WATERFALL_FIELDS.join(", ")}.`
9944
+ };
9945
+ }
9557
9946
  const clientConfig = ctx.session.getActiveContext().config;
9558
9947
  const waterfallProviders = args.waterfall ?? clientConfig.waterfall.email;
9559
9948
  const config = {
@@ -9988,22 +10377,68 @@ async function handleStatus(args, ctx) {
9988
10377
  return {
9989
10378
  provider: summary.name,
9990
10379
  capabilities: summary.capabilities,
10380
+ notes: summary.notes,
9991
10381
  healthy,
9992
10382
  credits
9993
10383
  };
9994
10384
  })
9995
10385
  );
9996
10386
  const client = ctx.session.getActiveContext();
10387
+ const configuredCapabilities = new Set(results.flatMap((r) => r.capabilities));
10388
+ const unconfigured = TOOL_CAPABILITY_REQUIREMENTS.filter(
10389
+ (req) => !configuredCapabilities.has(req.capability)
10390
+ ).map(({ tool, requires, setup }) => ({ tool, requires, setup }));
9997
10391
  return {
9998
10392
  client: client.clientName,
9999
10393
  providers: results,
10000
- waterfall: client.config.waterfall
10394
+ waterfall: client.config.waterfall,
10395
+ unconfigured: unconfigured.length > 0 ? unconfigured : void 0
10001
10396
  };
10002
10397
  }
10398
+ var TOOL_CAPABILITY_REQUIREMENTS;
10003
10399
  var init_status = __esm({
10004
10400
  "src/handlers/status.ts"() {
10005
10401
  "use strict";
10006
10402
  init_dist();
10403
+ TOOL_CAPABILITY_REQUIREMENTS = [
10404
+ {
10405
+ tool: "gtm_linkedin",
10406
+ requires: "harvest",
10407
+ capability: "extract_linkedin",
10408
+ setup: "Set GTM_HARVEST_API_KEY or add harvest.api_key to config.yaml"
10409
+ },
10410
+ {
10411
+ tool: "gtm_storeleads",
10412
+ requires: "storeleads",
10413
+ capability: "find_companies",
10414
+ setup: "Set SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY or add storeleads config"
10415
+ },
10416
+ {
10417
+ tool: "gtm_enrich",
10418
+ requires: "prospeo or bettercontact",
10419
+ capability: "enrich_email",
10420
+ setup: "Set GTM_PROSPEO_API_KEY or GTM_BETTERCONTACT_API_KEY"
10421
+ },
10422
+ {
10423
+ tool: "gtm_verify",
10424
+ requires: "bettercontact",
10425
+ capability: "verify_email",
10426
+ setup: "Set GTM_BETTERCONTACT_API_KEY"
10427
+ },
10428
+ {
10429
+ tool: "gtm_campaign",
10430
+ requires: "plusvibe, instantly, or smartlead",
10431
+ capability: "campaign_email",
10432
+ setup: "Set GTM_PLUSVIBE_API_KEY, GTM_INSTANTLY_API_KEY, or GTM_SMARTLEAD_API_KEY"
10433
+ },
10434
+ {
10435
+ tool: "gtm_outreach",
10436
+ requires: "heyreach",
10437
+ capability: "outreach_linkedin",
10438
+ setup: "Set GTM_HEYREACH_API_KEY"
10439
+ },
10440
+ { tool: "gtm_crm", requires: "attio", capability: "crm_read", setup: "Set GTM_ATTIO_API_KEY" }
10441
+ ];
10007
10442
  }
10008
10443
  });
10009
10444
 
@@ -10065,25 +10500,176 @@ var init_configure = __esm({
10065
10500
  }
10066
10501
  });
10067
10502
 
10503
+ // src/handlers/storeleads.ts
10504
+ var storeleads_exports = {};
10505
+ __export(storeleads_exports, {
10506
+ handleStoreleads: () => handleStoreleads
10507
+ });
10508
+ async function handleStoreleads(args, ctx) {
10509
+ const adapter = ctx.registry.get("storeleads");
10510
+ if (!adapter) {
10511
+ throw Object.assign(
10512
+ new Error(
10513
+ "StorLeads provider not configured. Set GTM_STORELEADS_API_KEY env var or add storeleads.api_key to your config.yaml. Get a key at storeleads.app."
10514
+ ),
10515
+ { statusCode: 503 }
10516
+ );
10517
+ }
10518
+ const query = {
10519
+ search: args.search,
10520
+ category: args.category,
10521
+ country: args.country,
10522
+ state: args.state,
10523
+ min_revenue: args.min_revenue,
10524
+ max_revenue: args.max_revenue,
10525
+ min_employees: args.min_employees,
10526
+ max_employees: args.max_employees,
10527
+ platform: args.platform,
10528
+ limit: args.limit
10529
+ };
10530
+ const results = await adapter.search(query);
10531
+ return {
10532
+ provider: "storeleads",
10533
+ count: results.length,
10534
+ companies: results
10535
+ };
10536
+ }
10537
+ var init_storeleads2 = __esm({
10538
+ "src/handlers/storeleads.ts"() {
10539
+ "use strict";
10540
+ }
10541
+ });
10542
+
10543
+ // src/handlers/linkedin.ts
10544
+ var linkedin_exports = {};
10545
+ __export(linkedin_exports, {
10546
+ handleLinkedin: () => handleLinkedin
10547
+ });
10548
+ async function handleLinkedin(args, ctx) {
10549
+ const adapter = ctx.registry.get("harvest");
10550
+ if (!adapter) {
10551
+ throw Object.assign(
10552
+ new Error(
10553
+ "HarvestAPI provider not configured. Add harvest.api_key to your config.yaml or set GTM_HARVEST_API_KEY environment variable."
10554
+ ),
10555
+ { statusCode: 503 }
10556
+ );
10557
+ }
10558
+ const action = args.action;
10559
+ switch (action) {
10560
+ case "profile": {
10561
+ const profile = await adapter.getProfile(
10562
+ args.linkedin_url,
10563
+ args.find_email
10564
+ );
10565
+ return { action: "profile", provider: "harvest", profile };
10566
+ }
10567
+ case "posts": {
10568
+ const result = await adapter.getProfilePosts({
10569
+ profile: args.profile,
10570
+ postedLimit: args.posted_within
10571
+ });
10572
+ return {
10573
+ action: "posts",
10574
+ provider: "harvest",
10575
+ count: result.elements.length,
10576
+ posts: result.elements,
10577
+ pagination: result.pagination
10578
+ };
10579
+ }
10580
+ case "company_posts": {
10581
+ const result = await adapter.getCompanyPosts({
10582
+ company: args.company,
10583
+ postedLimit: args.posted_within
10584
+ });
10585
+ return {
10586
+ action: "company_posts",
10587
+ provider: "harvest",
10588
+ count: result.elements.length,
10589
+ posts: result.elements,
10590
+ pagination: result.pagination
10591
+ };
10592
+ }
10593
+ case "search_posts": {
10594
+ const result = await adapter.searchPosts(args.query, {
10595
+ sortBy: args.sort,
10596
+ postedLimit: args.posted_within
10597
+ });
10598
+ return {
10599
+ action: "search_posts",
10600
+ provider: "harvest",
10601
+ count: result.elements.length,
10602
+ posts: result.elements,
10603
+ pagination: result.pagination
10604
+ };
10605
+ }
10606
+ case "engagement": {
10607
+ const result = await adapter.getPostEngagement(
10608
+ args.post_url,
10609
+ args.engagement_type ?? "both"
10610
+ );
10611
+ return {
10612
+ action: "engagement",
10613
+ provider: "harvest",
10614
+ comments_count: result.comments.length,
10615
+ reactions_count: result.reactions.length,
10616
+ comments: result.comments,
10617
+ reactions: result.reactions
10618
+ };
10619
+ }
10620
+ default:
10621
+ throw new Error(`Unknown linkedin action: ${action}`);
10622
+ }
10623
+ }
10624
+ var init_linkedin = __esm({
10625
+ "src/handlers/linkedin.ts"() {
10626
+ "use strict";
10627
+ }
10628
+ });
10629
+
10068
10630
  // src/handlers/index.ts
10631
+ function isRateLimitError(error) {
10632
+ if (error instanceof Error && "status" in error) {
10633
+ return error.status === 429;
10634
+ }
10635
+ return false;
10636
+ }
10637
+ async function sleep(ms) {
10638
+ return new Promise((resolve) => setTimeout(resolve, ms));
10639
+ }
10069
10640
  async function handleToolCall(name, args, ctx) {
10070
- try {
10071
- const result = await dispatch(name, args, ctx);
10072
- return {
10073
- content: [{ type: "text", text: JSON.stringify(result, null, 2) ?? "{}" }]
10074
- };
10075
- } catch (error) {
10076
- logError(`handler:${name}`, error instanceof Error ? error : new Error(String(error)));
10077
- return {
10078
- content: [
10079
- {
10080
- type: "text",
10081
- text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) })
10082
- }
10083
- ],
10084
- isError: true
10085
- };
10641
+ let lastError;
10642
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
10643
+ try {
10644
+ const result = await dispatch(name, args, ctx);
10645
+ return {
10646
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) ?? "{}" }]
10647
+ };
10648
+ } catch (error) {
10649
+ lastError = error;
10650
+ if (isRateLimitError(error) && attempt < MAX_RETRIES) {
10651
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
10652
+ await sleep(delay);
10653
+ continue;
10654
+ }
10655
+ break;
10656
+ }
10086
10657
  }
10658
+ logError(
10659
+ `handler:${name}`,
10660
+ lastError instanceof Error ? lastError : new Error(String(lastError))
10661
+ );
10662
+ return {
10663
+ content: [
10664
+ {
10665
+ type: "text",
10666
+ text: JSON.stringify({
10667
+ error: lastError instanceof Error ? lastError.message : String(lastError)
10668
+ })
10669
+ }
10670
+ ],
10671
+ isError: true
10672
+ };
10087
10673
  }
10088
10674
  async function dispatch(name, args, ctx) {
10089
10675
  switch (name) {
@@ -10129,14 +10715,25 @@ async function dispatch(name, args, ctx) {
10129
10715
  const { handleConfigure: handleConfigure2 } = await Promise.resolve().then(() => (init_configure(), configure_exports));
10130
10716
  return handleConfigure2(args, ctx);
10131
10717
  }
10718
+ case "gtm_storeleads": {
10719
+ const { handleStoreleads: handleStoreleads2 } = await Promise.resolve().then(() => (init_storeleads2(), storeleads_exports));
10720
+ return handleStoreleads2(args, ctx);
10721
+ }
10722
+ case "gtm_linkedin": {
10723
+ const { handleLinkedin: handleLinkedin2 } = await Promise.resolve().then(() => (init_linkedin(), linkedin_exports));
10724
+ return handleLinkedin2(args, ctx);
10725
+ }
10132
10726
  default:
10133
10727
  throw new Error(`Unknown tool: ${name}`);
10134
10728
  }
10135
10729
  }
10730
+ var MAX_RETRIES, BASE_DELAY_MS;
10136
10731
  var init_handlers = __esm({
10137
10732
  "src/handlers/index.ts"() {
10138
10733
  "use strict";
10139
10734
  init_dist();
10735
+ MAX_RETRIES = 3;
10736
+ BASE_DELAY_MS = 1e3;
10140
10737
  }
10141
10738
  });
10142
10739
 
@@ -10148,7 +10745,7 @@ var init_prospect2 = __esm({
10148
10745
  prospectTools = [
10149
10746
  {
10150
10747
  name: "gtm_find_companies",
10151
- description: "Discover companies matching ICP criteria. Search by natural language description, lookalike domains, industry, size, or geography. Returns scored company results with domain, description, and firmographic data.",
10748
+ 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 gtm_storeleads instead \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.",
10152
10749
  inputSchema: {
10153
10750
  type: "object",
10154
10751
  properties: {
@@ -10156,15 +10753,15 @@ var init_prospect2 = __esm({
10156
10753
  type: "string",
10157
10754
  description: 'Natural language ICP description (e.g., "B2B SaaS startups in US, 50-200 employees")'
10158
10755
  },
10756
+ domain: {
10757
+ type: "string",
10758
+ description: "Single domain for direct company lookup"
10759
+ },
10159
10760
  domains: {
10160
10761
  type: "array",
10161
10762
  items: { type: "string" },
10162
10763
  description: 'Lookalike domains to find similar companies (e.g., ["stripe.com", "plaid.com"])'
10163
10764
  },
10164
- industry: {
10165
- type: "string",
10166
- description: 'Industry filter (e.g., "SaaS", "FinTech", "Healthcare")'
10167
- },
10168
10765
  country: {
10169
10766
  type: "string",
10170
10767
  description: 'Country filter (e.g., "US", "UK", "DE")'
@@ -10177,13 +10774,39 @@ var init_prospect2 = __esm({
10177
10774
  type: "number",
10178
10775
  description: "Maximum employee count"
10179
10776
  },
10777
+ tech_stack: {
10778
+ type: "array",
10779
+ items: { type: "string" },
10780
+ description: 'Filter by technology stack (e.g., ["React", "AWS"])'
10781
+ },
10782
+ category: {
10783
+ type: "string",
10784
+ description: 'Industry category filter (e.g., "SaaS", "FinTech", "Healthcare")'
10785
+ },
10786
+ negate_domains: {
10787
+ type: "array",
10788
+ items: { type: "string" },
10789
+ description: "Exclude these domains from results"
10790
+ },
10791
+ min_digital_footprint: {
10792
+ type: "number",
10793
+ description: "Minimum DiscoLike digital footprint score"
10794
+ },
10795
+ max_digital_footprint: {
10796
+ type: "number",
10797
+ description: "Maximum DiscoLike digital footprint score"
10798
+ },
10799
+ offset: {
10800
+ type: "number",
10801
+ description: "Pagination offset (0-based)"
10802
+ },
10180
10803
  limit: {
10181
10804
  type: "number",
10182
- description: "Max results to return (default: 50, max: 500)"
10805
+ description: "Max results to return (default: 50, max: 1000)"
10183
10806
  },
10184
10807
  provider: {
10185
10808
  type: "string",
10186
- description: "Provider override (uses first available find_companies provider if omitted)"
10809
+ description: "Provider override: disco, apollo, storeleads (uses first available if omitted)"
10187
10810
  }
10188
10811
  },
10189
10812
  required: []
@@ -10191,7 +10814,7 @@ var init_prospect2 = __esm({
10191
10814
  },
10192
10815
  {
10193
10816
  name: "gtm_find_people",
10194
- description: "Find contacts/people at companies. Search by job title, company, location, or domain. Returns person results with name, title, company, and optionally email/LinkedIn URL.",
10817
+ 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 domains to find people there\n- Seniority filtering (owner, c_suite, vp, director, manager, etc.)\n- Returns validated emails when available\n- Best for: finding decision-makers at known companies\n\nAfter finding people, use:\n- gtm_enrich to get verified email/phone if not returned\n- gtm_linkedin to check LinkedIn activity and engagement signals",
10195
10818
  inputSchema: {
10196
10819
  type: "object",
10197
10820
  properties: {
@@ -10211,13 +10834,43 @@ var init_prospect2 = __esm({
10211
10834
  type: "string",
10212
10835
  description: 'Location filter (e.g., "San Francisco", "United States")'
10213
10836
  },
10837
+ seniority: {
10838
+ type: "array",
10839
+ items: { type: "string" },
10840
+ description: "Filter by seniority: owner, c_suite, vp, director, manager, senior, entry"
10841
+ },
10842
+ department: {
10843
+ type: "array",
10844
+ items: { type: "string" },
10845
+ description: "Filter by department: engineering, sales, marketing, finance, hr, etc."
10846
+ },
10847
+ has_email: {
10848
+ type: "boolean",
10849
+ description: "Filter to contacts with known email (default: true)"
10850
+ },
10851
+ has_phone: {
10852
+ type: "boolean",
10853
+ description: "Filter to contacts with known phone number"
10854
+ },
10855
+ has_linkedin: {
10856
+ type: "boolean",
10857
+ description: "Filter to contacts with LinkedIn profile"
10858
+ },
10859
+ min_connections: {
10860
+ type: "number",
10861
+ description: "Minimum LinkedIn connections"
10862
+ },
10863
+ offset: {
10864
+ type: "number",
10865
+ description: "Pagination offset (0-based)"
10866
+ },
10214
10867
  limit: {
10215
10868
  type: "number",
10216
- description: "Max results to return (default: 25, max: 100)"
10869
+ description: "Max results to return (default: 25, max: 1000)"
10217
10870
  },
10218
10871
  provider: {
10219
10872
  type: "string",
10220
- description: "Provider override (uses first available find_people provider if omitted)"
10873
+ description: "Provider override: disco, apollo (uses first available if omitted)"
10221
10874
  }
10222
10875
  },
10223
10876
  required: []
@@ -10235,7 +10888,7 @@ var init_enrich2 = __esm({
10235
10888
  enrichTools = [
10236
10889
  {
10237
10890
  name: "gtm_enrich",
10238
- description: "Enrich contacts with email and/or phone using a waterfall of providers. For batches > 5, always dry_run first to show cost estimate before executing. Providers only charge when contact info is found \u2014 no result, no charge.",
10891
+ 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.",
10239
10892
  inputSchema: {
10240
10893
  type: "object",
10241
10894
  properties: {
@@ -10257,9 +10910,12 @@ var init_enrich2 = __esm({
10257
10910
  },
10258
10911
  find: {
10259
10912
  type: "array",
10260
- items: { type: "string", enum: ["email", "phone"] },
10913
+ items: {
10914
+ type: "string",
10915
+ enum: ["email", "phone", "title", "company", "funding", "tech_stack"]
10916
+ },
10261
10917
  default: ["email"],
10262
- description: "What to find"
10918
+ 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."
10263
10919
  },
10264
10920
  waterfall: {
10265
10921
  type: "array",
@@ -10626,6 +11282,118 @@ var init_configure2 = __esm({
10626
11282
  }
10627
11283
  });
10628
11284
 
11285
+ // src/tools/storeleads.ts
11286
+ var storeleadsTools;
11287
+ var init_storeleads3 = __esm({
11288
+ "src/tools/storeleads.ts"() {
11289
+ "use strict";
11290
+ storeleadsTools = [
11291
+ {
11292
+ name: "gtm_storeleads",
11293
+ description: 'Search 1.2M+ active e-commerce stores (primarily Shopify) by category, revenue, country, and more. This is a curated database with estimated revenue data that no other provider has.\n\nWhen to use:\n- DTC, e-commerce, consumer brand, or Shopify queries \u2014 this is your primary source\n- When you need revenue-based filtering (e.g. "$1M-$10M/year brands")\n- When you need category-specific results (Apparel, Beauty, Food & Drink, etc.)\n\nWhen NOT to use:\n- B2B/SaaS companies \u2014 use gtm_find_companies with DiscoLike instead\n- When you need contact/people data \u2014 use gtm_find_people after getting domains\n- When you need lookalike discovery \u2014 use gtm_find_companies with seed domains',
11294
+ inputSchema: {
11295
+ type: "object",
11296
+ properties: {
11297
+ search: {
11298
+ type: "string",
11299
+ description: "Fuzzy text match on name, description, categories"
11300
+ },
11301
+ category: {
11302
+ type: "string",
11303
+ description: 'Filter by category (e.g. "Apparel", "Beauty & Fitness", "Food & Drink")'
11304
+ },
11305
+ country: { type: "string", description: 'ISO country code (e.g. "US", "GB")' },
11306
+ state: { type: "string", description: "US state or equivalent" },
11307
+ min_revenue: {
11308
+ type: "number",
11309
+ description: "Minimum estimated yearly sales in USD"
11310
+ },
11311
+ max_revenue: {
11312
+ type: "number",
11313
+ description: "Maximum estimated yearly sales in USD"
11314
+ },
11315
+ min_employees: { type: "number", description: "Minimum employee count" },
11316
+ max_employees: { type: "number", description: "Maximum employee count" },
11317
+ platform: {
11318
+ type: "string",
11319
+ description: 'E-commerce platform (e.g. "Shopify", "Shopify Plus")'
11320
+ },
11321
+ limit: {
11322
+ type: "number",
11323
+ description: "Results to return (default 50, max 50)"
11324
+ }
11325
+ }
11326
+ }
11327
+ }
11328
+ ];
11329
+ }
11330
+ });
11331
+
11332
+ // src/tools/linkedin.ts
11333
+ var linkedinTools;
11334
+ var init_linkedin2 = __esm({
11335
+ "src/tools/linkedin.ts"() {
11336
+ "use strict";
11337
+ linkedinTools = [
11338
+ {
11339
+ name: "gtm_linkedin",
11340
+ 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.",
11341
+ inputSchema: {
11342
+ type: "object",
11343
+ required: ["action"],
11344
+ properties: {
11345
+ action: {
11346
+ type: "string",
11347
+ enum: ["profile", "posts", "engagement", "company_posts", "search_posts"],
11348
+ description: "Action to perform"
11349
+ },
11350
+ linkedin_url: {
11351
+ type: "string",
11352
+ description: "LinkedIn profile URL (required for profile action)"
11353
+ },
11354
+ find_email: {
11355
+ type: "boolean",
11356
+ description: "Find email via SMTP verification \u2014 uses extra credits (profile action only)"
11357
+ },
11358
+ profile: {
11359
+ type: "string",
11360
+ description: "LinkedIn URL or publicIdentifier (required for posts action)"
11361
+ },
11362
+ company: {
11363
+ type: "string",
11364
+ description: "Company LinkedIn URL or universalName (required for company_posts action)"
11365
+ },
11366
+ posted_within: {
11367
+ type: "string",
11368
+ enum: ["24h", "week", "month"],
11369
+ description: "Filter posts by recency"
11370
+ },
11371
+ post_url: {
11372
+ type: "string",
11373
+ description: "LinkedIn post URL (required for engagement action)"
11374
+ },
11375
+ engagement_type: {
11376
+ type: "string",
11377
+ enum: ["comments", "reactions", "both"],
11378
+ description: "Type of engagement to retrieve (default: both)"
11379
+ },
11380
+ query: {
11381
+ type: "string",
11382
+ description: "Keyword search (required for search_posts action)"
11383
+ },
11384
+ sort: {
11385
+ type: "string",
11386
+ enum: ["relevance", "date"],
11387
+ description: "Sort order for search_posts"
11388
+ },
11389
+ limit: { type: "number", description: "Max results to return (max 100)" }
11390
+ }
11391
+ }
11392
+ }
11393
+ ];
11394
+ }
11395
+ });
11396
+
10629
11397
  // src/tools/index.ts
10630
11398
  var capabilityTools, guideTool, toolHelpTool, executeTool, metaTools, tools, toolsByName, toolCategories, workflowRecipes;
10631
11399
  var init_tools = __esm({
@@ -10641,6 +11409,8 @@ var init_tools = __esm({
10641
11409
  init_crm2();
10642
11410
  init_status2();
10643
11411
  init_configure2();
11412
+ init_storeleads3();
11413
+ init_linkedin2();
10644
11414
  init_prospect2();
10645
11415
  init_enrich2();
10646
11416
  init_extract2();
@@ -10651,6 +11421,8 @@ var init_tools = __esm({
10651
11421
  init_crm2();
10652
11422
  init_status2();
10653
11423
  init_configure2();
11424
+ init_storeleads3();
11425
+ init_linkedin2();
10654
11426
  capabilityTools = [
10655
11427
  ...prospectTools,
10656
11428
  ...enrichTools,
@@ -10661,7 +11433,9 @@ var init_tools = __esm({
10661
11433
  ...outreachTools,
10662
11434
  ...crmTools,
10663
11435
  ...statusTools,
10664
- ...configureTools
11436
+ ...configureTools,
11437
+ ...storeleadsTools,
11438
+ ...linkedinTools
10665
11439
  ];
10666
11440
  guideTool = {
10667
11441
  name: "gtm_guide",
@@ -10731,28 +11505,46 @@ var init_tools = __esm({
10731
11505
  crm: crmTools.map((t) => t.name),
10732
11506
  status: statusTools.map((t) => t.name),
10733
11507
  configure: configureTools.map((t) => t.name),
11508
+ storeleads: storeleadsTools.map((t) => t.name),
11509
+ linkedin: linkedinTools.map((t) => t.name),
10734
11510
  meta: metaTools.map((t) => t.name)
10735
11511
  };
10736
11512
  workflowRecipes = {
10737
11513
  build_tam: `# Build a TAM List \u2014 Workflow
10738
11514
 
10739
- ## Steps
11515
+ Start by identifying what kind of companies you're targeting:
10740
11516
 
10741
- 1. DISCOVER \u2014 Find companies matching your ICP
10742
- \u2192 gtm_find_companies({ icp_text: "...", limit: 100 })
10743
-
10744
- 2. FIND PEOPLE \u2014 Get decision-makers at those companies
11517
+ ## DTC / E-commerce / Consumer Brands
11518
+ 1. gtm_storeleads \u2014 filter by category, revenue, country
11519
+ \u2192 gtm_storeleads({ category: "Beauty", min_revenue: 1000000, country: "US" })
11520
+ 2. gtm_find_people \u2014 find decision-makers at those domains
10745
11521
  \u2192 gtm_find_people({ company_domain: "...", job_title: "CEO" })
10746
-
10747
- 3. ENRICH \u2014 Get email addresses (DRY RUN FIRST for batches > 5)
11522
+ 3. gtm_enrich (dry_run first) \u2014 get verified emails
10748
11523
  \u2192 gtm_enrich({ contacts: [...], find: ["email"], dry_run: true })
10749
- Review the cost estimate, then run without dry_run to execute.
11524
+ 4. Optional: gtm_linkedin \u2014 check if they're active on LinkedIn
11525
+ \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
10750
11526
 
10751
- 4. VERIFY \u2014 Validate emails before outreach
10752
- \u2192 gtm_verify({ emails: [...] })
10753
-
10754
- 5. PUSH \u2014 Add to cold email or LinkedIn campaign
10755
- \u2192 gtm_campaign({ action: "add_leads", campaign_id: "...", leads: [...] })
11527
+ ## B2B / SaaS / Services with websites
11528
+ 1. gtm_find_companies \u2014 use DiscoLike ICP text or lookalikes
11529
+ \u2192 gtm_find_companies({ icp_text: "...", limit: 100 })
11530
+ 2. gtm_find_people \u2014 find contacts by title
11531
+ 3. gtm_enrich \u2014 get verified emails
11532
+ 4. gtm_campaign or gtm_outreach \u2014 push to campaigns
11533
+
11534
+ ## LinkedIn-native prospects
11535
+ 1. gtm_find_people \u2014 search by title + company
11536
+ 2. gtm_linkedin \u2014 scrape profiles for activity signals
11537
+ \u2192 gtm_linkedin({ action: "profile", linkedin_url: "..." })
11538
+ 3. gtm_enrich \u2014 get emails for active prospects
11539
+ 4. gtm_outreach \u2014 LinkedIn campaign, or gtm_campaign for email
11540
+
11541
+ ## Engagement-based (warm leads)
11542
+ 1. gtm_linkedin (search_posts) \u2014 find relevant content
11543
+ \u2192 gtm_linkedin({ action: "search_posts", query: "...", sort: "date" })
11544
+ 2. gtm_linkedin (engagement) \u2014 extract commenters/reactors
11545
+ \u2192 gtm_linkedin({ action: "engagement", post_url: "...", engagement_type: "both" })
11546
+ 3. gtm_enrich \u2014 get emails
11547
+ 4. gtm_campaign \u2014 personalized outreach referencing their engagement
10756
11548
 
10757
11549
  ## Key principle
10758
11550
  Always dry_run enrichment first. Credits are spent per successful find \u2014 previewing cost avoids surprises.`,
@@ -10833,12 +11625,16 @@ Always search before creating to avoid duplicates. Most CRMs have built-in email
10833
11625
 
10834
11626
  Call gtm_guide with one of these tasks:
10835
11627
 
10836
- - build_tam \u2014 Discover companies + find people + enrich + push to campaign
11628
+ - build_tam \u2014 Build a TAM list (DTC/e-commerce, B2B, LinkedIn-native, or engagement-based paths)
10837
11629
  - enrich_list \u2014 Take a contact list and enrich with email/phone via waterfall
10838
11630
  - cold_outreach \u2014 Enrich + personalize + push to cold email campaign
10839
11631
  - linkedin_outreach \u2014 Extract profiles + personalize + push to LinkedIn campaign
10840
11632
  - crm_sync \u2014 Find/create/update people and deals in CRM
10841
11633
 
11634
+ New tools available:
11635
+ - gtm_storeleads \u2014 Search 1.2M+ e-commerce stores by category, revenue, platform
11636
+ - gtm_linkedin \u2014 LinkedIn profile scraping, post search, engagement mining
11637
+
10842
11638
  For any task: always dry_run enrichment batches > 5 to preview cost before executing.`
10843
11639
  };
10844
11640
  }
@@ -10890,22 +11686,84 @@ var init_validation = __esm({
10890
11686
  gtm_find_companies: z2.object({
10891
11687
  icp_text: z2.string().optional(),
10892
11688
  domain: z2.string().optional(),
11689
+ domains: z2.array(z2.string()).optional(),
10893
11690
  limit: z2.number().int().min(1).max(1e3).optional(),
10894
11691
  country: z2.string().optional(),
10895
11692
  min_employees: z2.number().int().min(0).optional(),
10896
- max_employees: z2.number().int().min(1).optional()
11693
+ max_employees: z2.number().int().min(1).optional(),
11694
+ tech_stack: z2.array(z2.string()).optional(),
11695
+ category: z2.string().optional(),
11696
+ negate_domains: z2.array(z2.string()).optional(),
11697
+ min_digital_footprint: z2.number().min(0).optional(),
11698
+ max_digital_footprint: z2.number().min(0).optional(),
11699
+ offset: z2.number().int().min(0).optional(),
11700
+ provider: z2.string().optional()
10897
11701
  }),
10898
11702
  // 2. People/contact search
10899
11703
  gtm_find_people: z2.object({
10900
11704
  job_title: z2.string().optional(),
10901
11705
  company_name: z2.string().optional(),
10902
11706
  company_domain: z2.string().optional(),
10903
- limit: z2.number().int().min(1).max(1e3).optional()
11707
+ location: z2.string().optional(),
11708
+ limit: z2.number().int().min(1).max(1e3).optional(),
11709
+ seniority: z2.array(z2.string()).optional(),
11710
+ department: z2.array(z2.string()).optional(),
11711
+ has_email: z2.boolean().optional(),
11712
+ has_phone: z2.boolean().optional(),
11713
+ has_linkedin: z2.boolean().optional(),
11714
+ min_connections: z2.number().int().min(0).optional(),
11715
+ offset: z2.number().int().min(0).optional(),
11716
+ provider: z2.string().optional()
10904
11717
  }),
11718
+ // StorLeads — DTC/e-commerce company search
11719
+ gtm_storeleads: z2.object({
11720
+ search: z2.string().optional(),
11721
+ category: z2.string().optional(),
11722
+ country: z2.string().optional(),
11723
+ state: z2.string().optional(),
11724
+ min_revenue: z2.number().min(0).optional(),
11725
+ max_revenue: z2.number().min(0).optional(),
11726
+ min_employees: z2.number().int().min(0).optional(),
11727
+ max_employees: z2.number().int().min(1).optional(),
11728
+ platform: z2.string().optional(),
11729
+ limit: z2.number().int().min(1).max(500).optional()
11730
+ }),
11731
+ // LinkedIn intelligence — HarvestAPI
11732
+ gtm_linkedin: z2.object({
11733
+ action: z2.enum(["profile", "posts", "engagement", "company_posts", "search_posts"]),
11734
+ linkedin_url: z2.string().optional(),
11735
+ find_email: z2.boolean().optional(),
11736
+ profile: z2.string().optional(),
11737
+ company: z2.string().optional(),
11738
+ posted_within: z2.enum(["24h", "week", "month"]).optional(),
11739
+ post_url: z2.string().optional(),
11740
+ engagement_type: z2.enum(["comments", "reactions", "both"]).optional(),
11741
+ query: z2.string().optional(),
11742
+ sort: z2.enum(["relevance", "date"]).optional(),
11743
+ limit: z2.number().int().min(1).max(100).optional()
11744
+ }).refine(
11745
+ (data) => {
11746
+ switch (data.action) {
11747
+ case "profile":
11748
+ return !!data.linkedin_url;
11749
+ case "engagement":
11750
+ return !!data.post_url;
11751
+ case "search_posts":
11752
+ return !!data.query;
11753
+ case "posts":
11754
+ return !!data.profile;
11755
+ case "company_posts":
11756
+ return !!data.company;
11757
+ default:
11758
+ return true;
11759
+ }
11760
+ },
11761
+ { message: "Missing required field for the specified action" }
11762
+ ),
10905
11763
  // 3. Enrichment waterfall
10906
11764
  gtm_enrich: z2.object({
10907
11765
  contacts: z2.array(contactInputSchema).min(1, "contacts must contain at least 1 contact").max(100, "contacts must contain at most 100 contacts"),
10908
- find: z2.array(z2.enum(["email", "phone"])).optional(),
11766
+ find: z2.array(z2.enum(["email", "phone", "title", "company", "funding", "tech_stack"])).optional(),
10909
11767
  waterfall: z2.array(z2.string()).optional(),
10910
11768
  verify: z2.boolean().optional(),
10911
11769
  dry_run: z2.boolean().optional()