gtm-now 0.10.6 → 0.10.8

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
@@ -205,7 +205,8 @@ var init_config = __esm({
205
205
  GTM_SMARTLEAD_API_KEY: "smartlead",
206
206
  GTM_HARVEST_API_KEY: "harvest",
207
207
  GTM_STORELEADS_API_KEY: "storeleads",
208
- GTM_EXA_API_KEY: "exa"
208
+ GTM_EXA_API_KEY: "exa",
209
+ GTM_LEADMAGIC_API_KEY: "leadmagic"
209
210
  };
210
211
  ENV_WORKSPACE_MAP = {
211
212
  GTM_PLUSVIBE_WORKSPACE_ID: "plusvibe"
@@ -8737,6 +8738,111 @@ var init_client25 = __esm({
8737
8738
  }
8738
8739
  });
8739
8740
 
8741
+ // ../integrations/dist/leadmagic.js
8742
+ var LeadMagicClient;
8743
+ var init_leadmagic2 = __esm({
8744
+ "../integrations/dist/leadmagic.js"() {
8745
+ "use strict";
8746
+ init_base_client();
8747
+ LeadMagicClient = class extends BaseApiClient {
8748
+ constructor(config) {
8749
+ super({
8750
+ name: "LeadMagic",
8751
+ baseUrl: config.baseUrl ?? "https://api.leadmagic.io",
8752
+ headers: { "X-API-Key": config.apiKey },
8753
+ timeoutMs: config.timeoutMs
8754
+ });
8755
+ }
8756
+ // ─── People ──────────────────────────────────────────────────────────────────
8757
+ async findEmail(params) {
8758
+ return this.request("/v1/people/email-finder", {
8759
+ method: "POST",
8760
+ body: JSON.stringify(params)
8761
+ });
8762
+ }
8763
+ async validateEmail(params) {
8764
+ return this.request("/v1/people/email-validation", {
8765
+ method: "POST",
8766
+ body: JSON.stringify(params)
8767
+ });
8768
+ }
8769
+ async findMobile(params) {
8770
+ return this.request("/v1/people/mobile-finder", {
8771
+ method: "POST",
8772
+ body: JSON.stringify(params)
8773
+ });
8774
+ }
8775
+ async searchProfile(params) {
8776
+ return this.request("/v1/people/profile-search", {
8777
+ method: "POST",
8778
+ body: JSON.stringify(params)
8779
+ });
8780
+ }
8781
+ async findEmployees(params) {
8782
+ return this.request("/v1/people/employee-finder", {
8783
+ method: "POST",
8784
+ body: JSON.stringify(params)
8785
+ });
8786
+ }
8787
+ async findRole(params) {
8788
+ return this.request("/v1/people/role-finder", {
8789
+ method: "POST",
8790
+ body: JSON.stringify(params)
8791
+ });
8792
+ }
8793
+ async emailToProfile(params) {
8794
+ return this.request("/v1/people/b2b-profile", {
8795
+ method: "POST",
8796
+ body: JSON.stringify(params)
8797
+ });
8798
+ }
8799
+ async profileToEmail(params) {
8800
+ return this.request("/v1/people/b2b-profile-email", {
8801
+ method: "POST",
8802
+ body: JSON.stringify(params)
8803
+ });
8804
+ }
8805
+ async findPersonalEmail(params) {
8806
+ return this.request("/v1/people/personal-email-finder", {
8807
+ method: "POST",
8808
+ body: JSON.stringify(params)
8809
+ });
8810
+ }
8811
+ async detectJobChange(params) {
8812
+ return this.request("/v1/people/job-change-detector", {
8813
+ method: "POST",
8814
+ body: JSON.stringify(params)
8815
+ });
8816
+ }
8817
+ // ─── Companies ───────────────────────────────────────────────────────────────
8818
+ async searchCompany(params) {
8819
+ return this.request("/v1/companies/company-search", {
8820
+ method: "POST",
8821
+ body: JSON.stringify(params)
8822
+ });
8823
+ }
8824
+ async searchCompetitors(params) {
8825
+ return this.request("/v1/companies/competitors-search", {
8826
+ method: "POST",
8827
+ body: JSON.stringify(params)
8828
+ });
8829
+ }
8830
+ async getCompanyFunding(params) {
8831
+ return this.request("/v1/companies/company-funding", {
8832
+ method: "POST",
8833
+ body: JSON.stringify(params)
8834
+ });
8835
+ }
8836
+ async getTechnographics(params) {
8837
+ return this.request("/v1/companies/technographics", {
8838
+ method: "POST",
8839
+ body: JSON.stringify(params)
8840
+ });
8841
+ }
8842
+ };
8843
+ }
8844
+ });
8845
+
8740
8846
  // ../integrations/dist/index.js
8741
8847
  var init_dist2 = __esm({
8742
8848
  "../integrations/dist/index.js"() {
@@ -8778,6 +8884,7 @@ var init_dist2 = __esm({
8778
8884
  init_factory();
8779
8885
  init_client24();
8780
8886
  init_client25();
8887
+ init_leadmagic2();
8781
8888
  }
8782
8889
  });
8783
8890
 
@@ -9481,13 +9588,14 @@ function mapSeniority(values) {
9481
9588
  function isApiClientError(error, statusCode) {
9482
9589
  return error instanceof Error && "statusCode" in error && error.statusCode === statusCode;
9483
9590
  }
9484
- var PROSPEO_PAGE_SIZE, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
9591
+ var PROSPEO_PAGE_SIZE, MAX_PAGES, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
9485
9592
  var init_prospeo2 = __esm({
9486
9593
  "src/providers/adapters/prospeo.ts"() {
9487
9594
  "use strict";
9488
9595
  init_dist();
9489
9596
  init_dist2();
9490
9597
  PROSPEO_PAGE_SIZE = 25;
9598
+ MAX_PAGES = 40;
9491
9599
  SENIORITY_TO_PROSPEO = {
9492
9600
  c_suite: "C-Suite",
9493
9601
  founder: "Founder/Owner",
@@ -9577,24 +9685,33 @@ var init_prospeo2 = __esm({
9577
9685
  { statusCode: 400 }
9578
9686
  );
9579
9687
  }
9580
- const page = query.offset ? Math.floor(query.offset / PROSPEO_PAGE_SIZE) + 1 : 1;
9688
+ const requestedLimit = query.limit ?? PROSPEO_PAGE_SIZE;
9689
+ const startPage = query.offset ? Math.floor(query.offset / PROSPEO_PAGE_SIZE) + 1 : 1;
9581
9690
  try {
9582
- const response = await this.client.searchPeople({ page, filters });
9583
- if (response.error || !response.results?.length) return [];
9584
- return response.results.map((r) => ({
9585
- first_name: r.person?.first_name ?? "",
9586
- last_name: r.person?.last_name ?? "",
9587
- job_title: r.person?.current_job_title ?? void 0,
9588
- company: r.company?.name ?? void 0,
9589
- company_domain: r.company?.domain ?? void 0,
9590
- email: r.person?.email?.email ?? void 0,
9591
- phone: r.person?.mobile?.mobile ?? void 0,
9592
- linkedin_url: r.person?.linkedin_url ?? void 0,
9593
- seniority: r.person?.job_history?.[0]?.seniority ?? void 0,
9594
- department: r.person?.job_history?.[0]?.departments?.[0] ?? void 0,
9595
- location: r.person?.location ? [r.person.location.city, r.person.location.state, r.person.location.country].filter(Boolean).join(", ") : void 0,
9596
- sources: [this.name]
9597
- }));
9691
+ const allResults = [];
9692
+ let page = startPage;
9693
+ while (allResults.length < requestedLimit && page < startPage + MAX_PAGES) {
9694
+ const response = await this.client.searchPeople({ page, filters });
9695
+ if (response.error || !response.results?.length) break;
9696
+ const mapped = response.results.map((r) => ({
9697
+ first_name: r.person?.first_name ?? "",
9698
+ last_name: r.person?.last_name ?? "",
9699
+ job_title: r.person?.current_job_title ?? void 0,
9700
+ company: r.company?.name ?? void 0,
9701
+ company_domain: r.company?.domain ?? void 0,
9702
+ email: r.person?.email?.email ?? void 0,
9703
+ phone: r.person?.mobile?.mobile ?? void 0,
9704
+ linkedin_url: r.person?.linkedin_url ?? void 0,
9705
+ seniority: r.person?.job_history?.[0]?.seniority ?? void 0,
9706
+ department: r.person?.job_history?.[0]?.departments?.[0] ?? void 0,
9707
+ location: r.person?.location ? [r.person.location.city, r.person.location.state, r.person.location.country].filter(Boolean).join(", ") : void 0,
9708
+ sources: [this.name]
9709
+ }));
9710
+ allResults.push(...mapped);
9711
+ if (response.results.length < PROSPEO_PAGE_SIZE) break;
9712
+ page++;
9713
+ }
9714
+ return allResults.slice(0, requestedLimit);
9598
9715
  } catch (error) {
9599
9716
  if (isApiClientError(error, 400)) {
9600
9717
  const body = error.responseBody;
@@ -10539,6 +10656,193 @@ var init_exa = __esm({
10539
10656
  }
10540
10657
  });
10541
10658
 
10659
+ // src/providers/adapters/leadmagic.ts
10660
+ function mapEmailQuality(status) {
10661
+ switch (status) {
10662
+ case "valid":
10663
+ return "95%";
10664
+ case "valid_catch_all":
10665
+ return "80%";
10666
+ case "catch_all":
10667
+ return "60%";
10668
+ default:
10669
+ return "50%";
10670
+ }
10671
+ }
10672
+ function mapVerificationStatus(status) {
10673
+ switch (status) {
10674
+ case "valid":
10675
+ return "deliverable";
10676
+ case "valid_catch_all":
10677
+ return "catch_all_safe";
10678
+ case "catch_all":
10679
+ return "catch_all_not_safe";
10680
+ case "invalid":
10681
+ return "undeliverable";
10682
+ default:
10683
+ return "unknown";
10684
+ }
10685
+ }
10686
+ var LeadMagicAdapter2;
10687
+ var init_leadmagic3 = __esm({
10688
+ "src/providers/adapters/leadmagic.ts"() {
10689
+ "use strict";
10690
+ init_dist();
10691
+ init_dist2();
10692
+ LeadMagicAdapter2 = class {
10693
+ name = "leadmagic";
10694
+ capabilities = [
10695
+ "enrich_email",
10696
+ "enrich_phone",
10697
+ "verify_email",
10698
+ "find_people"
10699
+ ];
10700
+ notes = "Best for: email enrichment (name+domain), email validation (0.05 credits), mobile finder (needs LinkedIn URL, 5 credits), employee lists (0.05/employee). Also supports: profile search, company search, technographics, funding, competitors, job change detection. Pay-on-find pricing \u2014 free when no result.";
10701
+ client;
10702
+ constructor(apiKey) {
10703
+ this.client = new LeadMagicClient({ apiKey });
10704
+ }
10705
+ async checkCredentials() {
10706
+ try {
10707
+ await this.client.validateEmail({ email: "test@example.com" });
10708
+ return true;
10709
+ } catch (error) {
10710
+ logError(
10711
+ "leadmagic:checkCredentials",
10712
+ error instanceof Error ? error : new Error(String(error))
10713
+ );
10714
+ return false;
10715
+ }
10716
+ }
10717
+ async checkCredits() {
10718
+ return null;
10719
+ }
10720
+ // ─── EmailEnricher ──────────────────────────────────────
10721
+ async enrichEmail(contact) {
10722
+ try {
10723
+ if (contact.first_name && contact.last_name && (contact.company_domain || contact.company)) {
10724
+ const response = await this.client.findEmail({
10725
+ first_name: contact.first_name,
10726
+ last_name: contact.last_name,
10727
+ domain: contact.company_domain,
10728
+ company_name: !contact.company_domain ? contact.company : void 0
10729
+ });
10730
+ if (response.email && response.status !== "not_found") {
10731
+ return {
10732
+ email: response.email,
10733
+ quality: mapEmailQuality(response.status),
10734
+ provider: this.name
10735
+ };
10736
+ }
10737
+ }
10738
+ if (contact.linkedin_url) {
10739
+ const response = await this.client.profileToEmail({
10740
+ profile_url: contact.linkedin_url
10741
+ });
10742
+ if (response.email) {
10743
+ return {
10744
+ email: response.email,
10745
+ quality: "80%",
10746
+ // Profile-to-email doesn't return validation status
10747
+ provider: this.name
10748
+ };
10749
+ }
10750
+ }
10751
+ return null;
10752
+ } catch (error) {
10753
+ logError("leadmagic:enrichEmail", error instanceof Error ? error : new Error(String(error)));
10754
+ return null;
10755
+ }
10756
+ }
10757
+ // ─── PhoneEnricher ──────────────────────────────────────
10758
+ async enrichPhone(contact) {
10759
+ try {
10760
+ const response = await this.client.findMobile({
10761
+ profile_url: contact.linkedin_url,
10762
+ work_email: void 0,
10763
+ // Could pass email if available
10764
+ personal_email: void 0
10765
+ });
10766
+ if (!response.mobile_number) return null;
10767
+ return {
10768
+ phone: response.mobile_number,
10769
+ provider: this.name
10770
+ };
10771
+ } catch (error) {
10772
+ logError("leadmagic:enrichPhone", error instanceof Error ? error : new Error(String(error)));
10773
+ return null;
10774
+ }
10775
+ }
10776
+ // ─── EmailVerifier ──────────────────────────────────────
10777
+ async verifyEmail(email) {
10778
+ try {
10779
+ const response = await this.client.validateEmail({ email });
10780
+ return {
10781
+ email: response.email ?? email,
10782
+ valid: response.email_status === "valid" || response.email_status === "valid_catch_all",
10783
+ status: mapVerificationStatus(response.email_status),
10784
+ provider: this.name
10785
+ };
10786
+ } catch (error) {
10787
+ logError("leadmagic:verifyEmail", error instanceof Error ? error : new Error(String(error)));
10788
+ return {
10789
+ email,
10790
+ valid: false,
10791
+ status: "unknown",
10792
+ provider: this.name
10793
+ };
10794
+ }
10795
+ }
10796
+ // ─── PeopleFinder ───────────────────────────────────────
10797
+ async findPeople(query) {
10798
+ try {
10799
+ if (query.job_title && (query.company_domain || query.company_name)) {
10800
+ const response = await this.client.findRole({
10801
+ job_title: query.job_title,
10802
+ company_domain: query.company_domain,
10803
+ company_name: !query.company_domain ? query.company_name : void 0
10804
+ });
10805
+ if (response.first_name && response.last_name) {
10806
+ return [
10807
+ {
10808
+ first_name: response.first_name,
10809
+ last_name: response.last_name,
10810
+ job_title: response.job_title ?? void 0,
10811
+ company: response.company_name ?? void 0,
10812
+ company_domain: response.company_website ?? query.company_domain ?? void 0,
10813
+ linkedin_url: response.profile_url ?? void 0,
10814
+ sources: [this.name]
10815
+ }
10816
+ ];
10817
+ }
10818
+ return [];
10819
+ }
10820
+ if (query.company_domain || query.company_name) {
10821
+ const response = await this.client.findEmployees({
10822
+ company_domain: query.company_domain,
10823
+ company_name: !query.company_domain ? query.company_name : void 0,
10824
+ limit: query.limit ?? 20
10825
+ });
10826
+ if (!response.data?.length) return [];
10827
+ return response.data.map((emp) => ({
10828
+ first_name: emp.first_name ?? "",
10829
+ last_name: emp.last_name ?? "",
10830
+ job_title: emp.title ?? void 0,
10831
+ company: emp.company_name ?? void 0,
10832
+ company_domain: emp.website ?? query.company_domain ?? void 0,
10833
+ sources: [this.name]
10834
+ }));
10835
+ }
10836
+ return [];
10837
+ } catch (error) {
10838
+ logError("leadmagic:findPeople", error instanceof Error ? error : new Error(String(error)));
10839
+ return [];
10840
+ }
10841
+ }
10842
+ };
10843
+ }
10844
+ });
10845
+
10542
10846
  // src/providers/adapters/index.ts
10543
10847
  var init_adapters = __esm({
10544
10848
  "src/providers/adapters/index.ts"() {
@@ -10556,6 +10860,7 @@ var init_adapters = __esm({
10556
10860
  init_storeleads();
10557
10861
  init_harvest();
10558
10862
  init_exa();
10863
+ init_leadmagic3();
10559
10864
  }
10560
10865
  });
10561
10866
 
@@ -10588,6 +10893,8 @@ function createAdapter(name, config) {
10588
10893
  return config.api_key ? new HarvestApiAdapter(config.api_key) : null;
10589
10894
  case "exa":
10590
10895
  return config.api_key ? new ExaAdapter(config.api_key) : null;
10896
+ case "leadmagic":
10897
+ return config.api_key ? new LeadMagicAdapter2(config.api_key) : null;
10591
10898
  default:
10592
10899
  return null;
10593
10900
  }
@@ -10890,7 +11197,7 @@ var init_prospect = __esm({
10890
11197
  },
10891
11198
  {
10892
11199
  name: "gtm_find_people",
10893
- description: 'Find contacts by job title, company, or domain. Returns name, title, email, LinkedIn URL. Slim by default; pass fields: "all" for full payload.',
11200
+ description: 'Find contacts by job title, company, or domain. Returns name, title, email, LinkedIn URL. Slim by default; pass fields: "all" for full payload. Pass enrich: true to resolve full emails inline (avoids a separate gtm_enrich call).',
10894
11201
  inputSchema: {
10895
11202
  type: "object",
10896
11203
  properties: {
@@ -10979,6 +11286,10 @@ var init_prospect = __esm({
10979
11286
  type: "string",
10980
11287
  description: 'Company revenue range as "min,max" (e.g., "1000000,10000000")'
10981
11288
  },
11289
+ enrich: {
11290
+ type: "boolean",
11291
+ description: "Resolve full email addresses inline. Some providers return masked emails (e.g., j***@company.com) in search results. Pass enrich: true to run waterfall enrichment on those contacts automatically, so you get real emails without a separate gtm_enrich call."
11292
+ },
10982
11293
  fields: {
10983
11294
  description: 'Response fields. Defaults to slim (name, title, email, company, company_domain, linkedin). Pass "all" for full payload including skills, phone, seniority, department, connections. Or pass an array of specific field names.',
10984
11295
  oneOf: [
@@ -11872,61 +12183,342 @@ For any task: always dry_run enrichment batches > 5 to preview cost before execu
11872
12183
  }
11873
12184
  });
11874
12185
 
11875
- // src/providers/types.ts
11876
- function applySlimFields(obj, fields) {
11877
- const result = {};
11878
- for (const field of fields) {
11879
- if (field in obj) {
11880
- result[field] = obj[field];
11881
- }
11882
- }
11883
- return result;
12186
+ // src/providers/waterfall.ts
12187
+ function getCacheKey(contact) {
12188
+ if (!contact.linkedin_url) return null;
12189
+ return contact.linkedin_url.toLowerCase().trim();
11884
12190
  }
11885
- var SLIM_COMPANY_FIELDS, SLIM_PERSON_FIELDS;
11886
- var init_types3 = __esm({
11887
- "src/providers/types.ts"() {
11888
- "use strict";
11889
- SLIM_COMPANY_FIELDS = [
11890
- "domain",
11891
- "name",
11892
- "industry",
11893
- "employees",
11894
- "country"
11895
- ];
11896
- SLIM_PERSON_FIELDS = [
11897
- "first_name",
11898
- "last_name",
11899
- "job_title",
11900
- "email",
11901
- "company",
11902
- "company_domain",
11903
- "linkedin_url"
11904
- ];
12191
+ function isExhaustedError(error) {
12192
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
12193
+ return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
12194
+ }
12195
+ function isRateLimitError(error) {
12196
+ if (error instanceof Error && "status" in error) {
12197
+ return error.status === 429;
11905
12198
  }
11906
- });
11907
-
11908
- // src/providers/manifest-validation.ts
11909
- function validateParams(provider, tool, args) {
11910
- const manifest = getToolManifest(provider, tool);
11911
- if (!manifest) return [];
11912
- const supported = new Set(manifest.supported_params);
11913
- const warnings = [];
11914
- for (const [key, value] of Object.entries(args)) {
11915
- if (value === void 0 || value === null) continue;
11916
- if (controlSet.has(key)) continue;
11917
- if (!supported.has(key)) {
11918
- const alternatives = Object.entries(MANIFESTS).filter(([_, m]) => m[tool]?.supported_params.includes(key)).map(([name]) => name);
11919
- const suggestion = alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : "";
11920
- warnings.push(`${key} is not supported by ${provider}.${suggestion}`);
12199
+ const msg = error instanceof Error ? error.message : String(error);
12200
+ return msg.includes("429") || msg.toLowerCase().includes("rate limit");
12201
+ }
12202
+ function withTimeout(promise, ms, label) {
12203
+ return new Promise((resolve, reject) => {
12204
+ const timer = setTimeout(
12205
+ () => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
12206
+ ms
12207
+ );
12208
+ promise.then(
12209
+ (val) => {
12210
+ clearTimeout(timer);
12211
+ resolve(val);
12212
+ },
12213
+ (err) => {
12214
+ clearTimeout(timer);
12215
+ reject(err);
12216
+ }
12217
+ );
12218
+ });
12219
+ }
12220
+ async function runWaterfall(contacts, config, registry) {
12221
+ const exhausted = /* @__PURE__ */ new Set();
12222
+ const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
12223
+ const concurrency = config.concurrency ?? 1;
12224
+ if (concurrency <= 1) {
12225
+ const results2 = [];
12226
+ for (const contact of contacts) {
12227
+ if (Date.now() > batchDeadline) {
12228
+ results2.push(makeTimeoutResult(contact));
12229
+ continue;
12230
+ }
12231
+ const result = await enrichContact(contact, config, registry, exhausted);
12232
+ results2.push(result);
12233
+ }
12234
+ return results2;
12235
+ }
12236
+ const results = new Array(contacts.length);
12237
+ let nextIndex = 0;
12238
+ async function worker() {
12239
+ while (nextIndex < contacts.length) {
12240
+ const i = nextIndex++;
12241
+ if (Date.now() > batchDeadline) {
12242
+ results[i] = makeTimeoutResult(contacts[i]);
12243
+ continue;
12244
+ }
12245
+ results[i] = await enrichContact(contacts[i], config, registry, exhausted);
11921
12246
  }
11922
12247
  }
11923
- return warnings;
11924
- }
11925
- function clampLimit(provider, tool, limit) {
11926
- const manifest = getToolManifest(provider, tool);
11927
- if (!manifest) return limit ?? 50;
11928
- if (limit === void 0) return manifest.default_limit;
11929
- return Math.min(limit, manifest.max_limit);
12248
+ await Promise.all(Array.from({ length: Math.min(concurrency, contacts.length) }, () => worker()));
12249
+ return results;
12250
+ }
12251
+ function makeTimeoutResult(contact) {
12252
+ return {
12253
+ contact,
12254
+ attempts: [
12255
+ {
12256
+ provider: "waterfall",
12257
+ capability: "enrich_email",
12258
+ found: false,
12259
+ charged: false,
12260
+ duration_ms: 0,
12261
+ error: "Batch timeout exceeded \u2014 skipped remaining contacts"
12262
+ }
12263
+ ],
12264
+ charged: false
12265
+ };
12266
+ }
12267
+ async function enrichContact(contact, config, registry, exhausted) {
12268
+ const attempts = [];
12269
+ let email;
12270
+ let emailProvider;
12271
+ let phone;
12272
+ let phoneProvider;
12273
+ let charged = false;
12274
+ const cacheKey = getCacheKey(contact);
12275
+ const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
12276
+ for (const field of config.find) {
12277
+ const capability = field === "email" ? "enrich_email" : "enrich_phone";
12278
+ const cachedValue = cached?.[field];
12279
+ if (cachedValue) {
12280
+ if (field === "email") {
12281
+ email = cachedValue;
12282
+ emailProvider = cached.provider;
12283
+ }
12284
+ if (field === "phone") {
12285
+ phone = cachedValue;
12286
+ phoneProvider = cached.provider;
12287
+ }
12288
+ attempts.push({
12289
+ provider: cached.provider,
12290
+ capability,
12291
+ found: true,
12292
+ charged: false,
12293
+ duration_ms: 0,
12294
+ cached: true
12295
+ });
12296
+ continue;
12297
+ }
12298
+ const fieldResult = await enrichField(contact, field, config, registry, exhausted);
12299
+ attempts.push(...fieldResult.attempts);
12300
+ if (field === "email" && fieldResult.value) {
12301
+ email = fieldResult.value;
12302
+ emailProvider = fieldResult.provider;
12303
+ charged = true;
12304
+ } else if (field === "phone" && fieldResult.value) {
12305
+ phone = fieldResult.value;
12306
+ phoneProvider = fieldResult.provider;
12307
+ charged = true;
12308
+ }
12309
+ }
12310
+ if (cacheKey && (email || phone)) {
12311
+ const existing = enrichmentCache.get(cacheKey);
12312
+ enrichmentCache.set(cacheKey, {
12313
+ email: email ?? existing?.email,
12314
+ phone: phone ?? existing?.phone,
12315
+ provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
12316
+ timestamp: Date.now()
12317
+ });
12318
+ }
12319
+ return {
12320
+ contact,
12321
+ email,
12322
+ phone,
12323
+ email_provider: emailProvider,
12324
+ phone_provider: phoneProvider,
12325
+ attempts,
12326
+ charged
12327
+ };
12328
+ }
12329
+ async function enrichField(contact, field, config, registry, exhausted) {
12330
+ const capability = field === "email" ? "enrich_email" : "enrich_phone";
12331
+ const attempts = [];
12332
+ for (const providerName of config.providers) {
12333
+ if (exhausted.has(providerName)) {
12334
+ attempts.push({
12335
+ provider: providerName,
12336
+ capability,
12337
+ found: false,
12338
+ charged: false,
12339
+ duration_ms: 0,
12340
+ error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
12341
+ });
12342
+ continue;
12343
+ }
12344
+ const provider = registry.get(providerName);
12345
+ if (!provider || !provider.capabilities.includes(capability)) continue;
12346
+ for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
12347
+ const start = Date.now();
12348
+ try {
12349
+ let value;
12350
+ const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
12351
+ if (field === "email" && "enrichEmail" in provider) {
12352
+ const result = await withTimeout(
12353
+ provider.enrichEmail(contact),
12354
+ providerTimeoutMs,
12355
+ providerName
12356
+ );
12357
+ value = result?.email;
12358
+ } else if (field === "phone" && "enrichPhone" in provider) {
12359
+ const result = await withTimeout(
12360
+ provider.enrichPhone(contact),
12361
+ providerTimeoutMs,
12362
+ providerName
12363
+ );
12364
+ value = result?.phone;
12365
+ }
12366
+ const found = !!value;
12367
+ attempts.push({
12368
+ provider: providerName,
12369
+ capability,
12370
+ found,
12371
+ charged: found,
12372
+ duration_ms: Date.now() - start
12373
+ });
12374
+ if (found && config.stopOnFirst) {
12375
+ return { value, provider: providerName, attempts };
12376
+ }
12377
+ break;
12378
+ } catch (error) {
12379
+ const errorMsg = error instanceof Error ? error.message : String(error);
12380
+ if (isExhaustedError(error)) {
12381
+ exhausted.add(providerName);
12382
+ attempts.push({
12383
+ provider: providerName,
12384
+ capability,
12385
+ found: false,
12386
+ charged: false,
12387
+ duration_ms: Date.now() - start,
12388
+ error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
12389
+ });
12390
+ break;
12391
+ }
12392
+ if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
12393
+ const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
12394
+ await new Promise((r) => setTimeout(r, delay));
12395
+ continue;
12396
+ }
12397
+ attempts.push({
12398
+ provider: providerName,
12399
+ capability,
12400
+ found: false,
12401
+ charged: false,
12402
+ duration_ms: Date.now() - start,
12403
+ error: errorMsg
12404
+ });
12405
+ logError(
12406
+ `waterfall:${providerName}`,
12407
+ error instanceof Error ? error : new Error(String(error)),
12408
+ { contact: `${contact.first_name} ${contact.last_name}` }
12409
+ );
12410
+ break;
12411
+ }
12412
+ }
12413
+ }
12414
+ return { attempts };
12415
+ }
12416
+ function dryRunWaterfall(contactsCount, config, registry) {
12417
+ const activeProviders = config.providers.filter((name) => registry.get(name));
12418
+ const costs = {};
12419
+ for (const name of activeProviders) {
12420
+ const pricing = DEFAULT_PRICING[name];
12421
+ if (pricing?.estimated_cost_per_hit) {
12422
+ costs[name] = pricing.estimated_cost_per_hit;
12423
+ } else if (pricing?.estimated_cost_per_lookup) {
12424
+ costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
12425
+ } else {
12426
+ costs[name] = "unknown";
12427
+ }
12428
+ }
12429
+ return {
12430
+ dry_run: true,
12431
+ contacts_count: contactsCount,
12432
+ waterfall: activeProviders,
12433
+ pricing_model: "pay-on-find",
12434
+ estimated_cost_per_hit: costs,
12435
+ note: "Charged only when contact info is found. No result = no charge.",
12436
+ confirm: "Call gtm_enrich again without dry_run to execute."
12437
+ };
12438
+ }
12439
+ var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
12440
+ var init_waterfall = __esm({
12441
+ "src/providers/waterfall.ts"() {
12442
+ "use strict";
12443
+ init_dist();
12444
+ enrichmentCache = /* @__PURE__ */ new Map();
12445
+ DEFAULT_PRICING = {
12446
+ bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
12447
+ prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
12448
+ apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
12449
+ hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
12450
+ leadmagic: { charged_only_on_find: true, estimated_cost_per_hit: "$0.03" }
12451
+ };
12452
+ EXHAUSTION_PATTERNS = [
12453
+ "insufficient credits",
12454
+ "no credits",
12455
+ "0 credits",
12456
+ "plan limit",
12457
+ "quota exceeded",
12458
+ "upgrade your plan",
12459
+ "not accessible with this api_key on a free plan"
12460
+ ];
12461
+ WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
12462
+ DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
12463
+ DEFAULT_BATCH_TIMEOUT_MS = 3e5;
12464
+ }
12465
+ });
12466
+
12467
+ // src/providers/types.ts
12468
+ function applySlimFields(obj, fields) {
12469
+ const result = {};
12470
+ for (const field of fields) {
12471
+ if (field in obj) {
12472
+ result[field] = obj[field];
12473
+ }
12474
+ }
12475
+ return result;
12476
+ }
12477
+ var SLIM_COMPANY_FIELDS, SLIM_PERSON_FIELDS;
12478
+ var init_types3 = __esm({
12479
+ "src/providers/types.ts"() {
12480
+ "use strict";
12481
+ SLIM_COMPANY_FIELDS = [
12482
+ "domain",
12483
+ "name",
12484
+ "industry",
12485
+ "employees",
12486
+ "country"
12487
+ ];
12488
+ SLIM_PERSON_FIELDS = [
12489
+ "first_name",
12490
+ "last_name",
12491
+ "job_title",
12492
+ "email",
12493
+ "company",
12494
+ "company_domain",
12495
+ "linkedin_url"
12496
+ ];
12497
+ }
12498
+ });
12499
+
12500
+ // src/providers/manifest-validation.ts
12501
+ function validateParams(provider, tool, args) {
12502
+ const manifest = getToolManifest(provider, tool);
12503
+ if (!manifest) return [];
12504
+ const supported = new Set(manifest.supported_params);
12505
+ const warnings = [];
12506
+ for (const [key, value] of Object.entries(args)) {
12507
+ if (value === void 0 || value === null) continue;
12508
+ if (controlSet.has(key)) continue;
12509
+ if (!supported.has(key)) {
12510
+ const alternatives = Object.entries(MANIFESTS).filter(([_, m]) => m[tool]?.supported_params.includes(key)).map(([name]) => name);
12511
+ const suggestion = alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : "";
12512
+ warnings.push(`${key} is not supported by ${provider}.${suggestion}`);
12513
+ }
12514
+ }
12515
+ return warnings;
12516
+ }
12517
+ function clampLimit(provider, tool, limit) {
12518
+ const manifest = getToolManifest(provider, tool);
12519
+ if (!manifest) return limit ?? 50;
12520
+ if (limit === void 0) return manifest.default_limit;
12521
+ return Math.min(limit, manifest.max_limit);
11930
12522
  }
11931
12523
  var controlSet;
11932
12524
  var init_manifest_validation = __esm({
@@ -12199,6 +12791,43 @@ async function findPeople(args, ctx) {
12199
12791
  };
12200
12792
  const results = await finder.findPeople(query);
12201
12793
  const dedupedResults = deduplicatePeople(results);
12794
+ const shouldEnrich = args.enrich ?? false;
12795
+ let enrichedCount = 0;
12796
+ if (shouldEnrich) {
12797
+ const needsEnrichment = dedupedResults.filter(
12798
+ (r) => !r.email || r.email.includes("*")
12799
+ );
12800
+ if (needsEnrichment.length > 0) {
12801
+ try {
12802
+ const contacts = needsEnrichment.map((r) => ({
12803
+ first_name: r.first_name,
12804
+ last_name: r.last_name,
12805
+ company: r.company,
12806
+ company_domain: r.company_domain,
12807
+ linkedin_url: r.linkedin_url
12808
+ }));
12809
+ const clientConfig = ctx.session.getActiveContext().config;
12810
+ const waterfallProviders = clientConfig.waterfall?.email ?? ["prospeo"];
12811
+ const waterfallConfig = {
12812
+ providers: waterfallProviders,
12813
+ find: ["email"],
12814
+ verify: false,
12815
+ stopOnFirst: true,
12816
+ concurrency: 5
12817
+ };
12818
+ const enrichResults = await runWaterfall(contacts, waterfallConfig, ctx.registry);
12819
+ for (let i = 0; i < needsEnrichment.length; i++) {
12820
+ const enriched = enrichResults[i];
12821
+ if (enriched?.email) {
12822
+ needsEnrichment[i].email = enriched.email;
12823
+ enrichedCount++;
12824
+ }
12825
+ }
12826
+ } catch (error) {
12827
+ logError("prospect:inlineEnrich", error instanceof Error ? error : new Error(String(error)));
12828
+ }
12829
+ }
12830
+ }
12202
12831
  const resultsWithAlias = dedupedResults.map((r) => ({
12203
12832
  ...r,
12204
12833
  company_domain: r.company_domain ?? r.company
@@ -12265,6 +12894,7 @@ async function findPeople(args, ctx) {
12265
12894
  has_more,
12266
12895
  offset,
12267
12896
  ...Object.keys(applied_filters).length > 0 && { applied_filters },
12897
+ ...shouldEnrich && { enriched: enrichedCount },
12268
12898
  people: slimmedResults,
12269
12899
  ...warnings?.length ? { warnings } : {}
12270
12900
  };
@@ -12272,7 +12902,9 @@ async function findPeople(args, ctx) {
12272
12902
  var init_prospect2 = __esm({
12273
12903
  "src/handlers/prospect.ts"() {
12274
12904
  "use strict";
12905
+ init_dist();
12275
12906
  init_errors();
12907
+ init_waterfall();
12276
12908
  init_types3();
12277
12909
  init_manifest_validation();
12278
12910
  init_result_writer();
@@ -12280,266 +12912,6 @@ var init_prospect2 = __esm({
12280
12912
  }
12281
12913
  });
12282
12914
 
12283
- // src/providers/waterfall.ts
12284
- function getCacheKey(contact) {
12285
- if (!contact.linkedin_url) return null;
12286
- return contact.linkedin_url.toLowerCase().trim();
12287
- }
12288
- function isExhaustedError(error) {
12289
- const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
12290
- return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
12291
- }
12292
- function isRateLimitError(error) {
12293
- if (error instanceof Error && "status" in error) {
12294
- return error.status === 429;
12295
- }
12296
- const msg = error instanceof Error ? error.message : String(error);
12297
- return msg.includes("429") || msg.toLowerCase().includes("rate limit");
12298
- }
12299
- function withTimeout(promise, ms, label) {
12300
- return new Promise((resolve, reject) => {
12301
- const timer = setTimeout(
12302
- () => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
12303
- ms
12304
- );
12305
- promise.then(
12306
- (val) => {
12307
- clearTimeout(timer);
12308
- resolve(val);
12309
- },
12310
- (err) => {
12311
- clearTimeout(timer);
12312
- reject(err);
12313
- }
12314
- );
12315
- });
12316
- }
12317
- async function runWaterfall(contacts, config, registry) {
12318
- const results = [];
12319
- const exhausted = /* @__PURE__ */ new Set();
12320
- const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
12321
- for (const contact of contacts) {
12322
- if (Date.now() > batchDeadline) {
12323
- results.push({
12324
- contact,
12325
- attempts: [
12326
- {
12327
- provider: "waterfall",
12328
- capability: "enrich_email",
12329
- found: false,
12330
- charged: false,
12331
- duration_ms: 0,
12332
- error: "Batch timeout exceeded \u2014 skipped remaining contacts"
12333
- }
12334
- ],
12335
- charged: false
12336
- });
12337
- continue;
12338
- }
12339
- const result = await enrichContact(contact, config, registry, exhausted);
12340
- results.push(result);
12341
- }
12342
- return results;
12343
- }
12344
- async function enrichContact(contact, config, registry, exhausted) {
12345
- const attempts = [];
12346
- let email;
12347
- let emailProvider;
12348
- let phone;
12349
- let phoneProvider;
12350
- let charged = false;
12351
- const cacheKey = getCacheKey(contact);
12352
- const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
12353
- for (const field of config.find) {
12354
- const capability = field === "email" ? "enrich_email" : "enrich_phone";
12355
- const cachedValue = cached?.[field];
12356
- if (cachedValue) {
12357
- if (field === "email") {
12358
- email = cachedValue;
12359
- emailProvider = cached.provider;
12360
- }
12361
- if (field === "phone") {
12362
- phone = cachedValue;
12363
- phoneProvider = cached.provider;
12364
- }
12365
- attempts.push({
12366
- provider: cached.provider,
12367
- capability,
12368
- found: true,
12369
- charged: false,
12370
- duration_ms: 0,
12371
- cached: true
12372
- });
12373
- continue;
12374
- }
12375
- const fieldResult = await enrichField(contact, field, config, registry, exhausted);
12376
- attempts.push(...fieldResult.attempts);
12377
- if (field === "email" && fieldResult.value) {
12378
- email = fieldResult.value;
12379
- emailProvider = fieldResult.provider;
12380
- charged = true;
12381
- } else if (field === "phone" && fieldResult.value) {
12382
- phone = fieldResult.value;
12383
- phoneProvider = fieldResult.provider;
12384
- charged = true;
12385
- }
12386
- }
12387
- if (cacheKey && (email || phone)) {
12388
- const existing = enrichmentCache.get(cacheKey);
12389
- enrichmentCache.set(cacheKey, {
12390
- email: email ?? existing?.email,
12391
- phone: phone ?? existing?.phone,
12392
- provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
12393
- timestamp: Date.now()
12394
- });
12395
- }
12396
- return {
12397
- contact,
12398
- email,
12399
- phone,
12400
- email_provider: emailProvider,
12401
- phone_provider: phoneProvider,
12402
- attempts,
12403
- charged
12404
- };
12405
- }
12406
- async function enrichField(contact, field, config, registry, exhausted) {
12407
- const capability = field === "email" ? "enrich_email" : "enrich_phone";
12408
- const attempts = [];
12409
- for (const providerName of config.providers) {
12410
- if (exhausted.has(providerName)) {
12411
- attempts.push({
12412
- provider: providerName,
12413
- capability,
12414
- found: false,
12415
- charged: false,
12416
- duration_ms: 0,
12417
- error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
12418
- });
12419
- continue;
12420
- }
12421
- const provider = registry.get(providerName);
12422
- if (!provider || !provider.capabilities.includes(capability)) continue;
12423
- for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
12424
- const start = Date.now();
12425
- try {
12426
- let value;
12427
- const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
12428
- if (field === "email" && "enrichEmail" in provider) {
12429
- const result = await withTimeout(
12430
- provider.enrichEmail(contact),
12431
- providerTimeoutMs,
12432
- providerName
12433
- );
12434
- value = result?.email;
12435
- } else if (field === "phone" && "enrichPhone" in provider) {
12436
- const result = await withTimeout(
12437
- provider.enrichPhone(contact),
12438
- providerTimeoutMs,
12439
- providerName
12440
- );
12441
- value = result?.phone;
12442
- }
12443
- const found = !!value;
12444
- attempts.push({
12445
- provider: providerName,
12446
- capability,
12447
- found,
12448
- charged: found,
12449
- duration_ms: Date.now() - start
12450
- });
12451
- if (found && config.stopOnFirst) {
12452
- return { value, provider: providerName, attempts };
12453
- }
12454
- break;
12455
- } catch (error) {
12456
- const errorMsg = error instanceof Error ? error.message : String(error);
12457
- if (isExhaustedError(error)) {
12458
- exhausted.add(providerName);
12459
- attempts.push({
12460
- provider: providerName,
12461
- capability,
12462
- found: false,
12463
- charged: false,
12464
- duration_ms: Date.now() - start,
12465
- error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
12466
- });
12467
- break;
12468
- }
12469
- if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
12470
- const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
12471
- await new Promise((r) => setTimeout(r, delay));
12472
- continue;
12473
- }
12474
- attempts.push({
12475
- provider: providerName,
12476
- capability,
12477
- found: false,
12478
- charged: false,
12479
- duration_ms: Date.now() - start,
12480
- error: errorMsg
12481
- });
12482
- logError(
12483
- `waterfall:${providerName}`,
12484
- error instanceof Error ? error : new Error(String(error)),
12485
- { contact: `${contact.first_name} ${contact.last_name}` }
12486
- );
12487
- break;
12488
- }
12489
- }
12490
- }
12491
- return { attempts };
12492
- }
12493
- function dryRunWaterfall(contactsCount, config, registry) {
12494
- const activeProviders = config.providers.filter((name) => registry.get(name));
12495
- const costs = {};
12496
- for (const name of activeProviders) {
12497
- const pricing = DEFAULT_PRICING[name];
12498
- if (pricing?.estimated_cost_per_hit) {
12499
- costs[name] = pricing.estimated_cost_per_hit;
12500
- } else if (pricing?.estimated_cost_per_lookup) {
12501
- costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
12502
- } else {
12503
- costs[name] = "unknown";
12504
- }
12505
- }
12506
- return {
12507
- dry_run: true,
12508
- contacts_count: contactsCount,
12509
- waterfall: activeProviders,
12510
- pricing_model: "pay-on-find",
12511
- estimated_cost_per_hit: costs,
12512
- note: "Charged only when contact info is found. No result = no charge.",
12513
- confirm: "Call gtm_enrich again without dry_run to execute."
12514
- };
12515
- }
12516
- var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
12517
- var init_waterfall = __esm({
12518
- "src/providers/waterfall.ts"() {
12519
- "use strict";
12520
- init_dist();
12521
- enrichmentCache = /* @__PURE__ */ new Map();
12522
- DEFAULT_PRICING = {
12523
- bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
12524
- prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
12525
- apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
12526
- hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" }
12527
- };
12528
- EXHAUSTION_PATTERNS = [
12529
- "insufficient credits",
12530
- "no credits",
12531
- "0 credits",
12532
- "plan limit",
12533
- "quota exceeded",
12534
- "upgrade your plan",
12535
- "not accessible with this api_key on a free plan"
12536
- ];
12537
- WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
12538
- DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
12539
- DEFAULT_BATCH_TIMEOUT_MS = 3e5;
12540
- }
12541
- });
12542
-
12543
12915
  // src/handlers/enrich.ts
12544
12916
  var enrich_exports = {};
12545
12917
  __export(enrich_exports, {
@@ -13448,7 +13820,8 @@ async function createTable(args) {
13448
13820
  const body = await response.json().catch(() => ({}));
13449
13821
  throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
13450
13822
  }
13451
- return response.json();
13823
+ const result = await response.json();
13824
+ return { ...result, row_ids: rows.map((r) => r._row_id) };
13452
13825
  }
13453
13826
  async function updateTable(args) {
13454
13827
  const tableId = args.table_id;
@@ -13758,6 +14131,7 @@ var init_validation = __esm({
13758
14131
  min_employees: z2.number().int().min(0).optional(),
13759
14132
  max_employees: z2.number().int().min(1).optional(),
13760
14133
  revenue_range: z2.string().optional(),
14134
+ enrich: z2.boolean().optional(),
13761
14135
  fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
13762
14136
  output: z2.enum(["file", "inline", "csv"]).optional(),
13763
14137
  detail: detailSchema