gtm-now 0.10.6 → 0.10.7

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
@@ -9481,13 +9481,14 @@ function mapSeniority(values) {
9481
9481
  function isApiClientError(error, statusCode) {
9482
9482
  return error instanceof Error && "statusCode" in error && error.statusCode === statusCode;
9483
9483
  }
9484
- var PROSPEO_PAGE_SIZE, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
9484
+ var PROSPEO_PAGE_SIZE, MAX_PAGES, SENIORITY_TO_PROSPEO, ProspeoMcpAdapter;
9485
9485
  var init_prospeo2 = __esm({
9486
9486
  "src/providers/adapters/prospeo.ts"() {
9487
9487
  "use strict";
9488
9488
  init_dist();
9489
9489
  init_dist2();
9490
9490
  PROSPEO_PAGE_SIZE = 25;
9491
+ MAX_PAGES = 40;
9491
9492
  SENIORITY_TO_PROSPEO = {
9492
9493
  c_suite: "C-Suite",
9493
9494
  founder: "Founder/Owner",
@@ -9577,24 +9578,33 @@ var init_prospeo2 = __esm({
9577
9578
  { statusCode: 400 }
9578
9579
  );
9579
9580
  }
9580
- const page = query.offset ? Math.floor(query.offset / PROSPEO_PAGE_SIZE) + 1 : 1;
9581
+ const requestedLimit = query.limit ?? PROSPEO_PAGE_SIZE;
9582
+ const startPage = query.offset ? Math.floor(query.offset / PROSPEO_PAGE_SIZE) + 1 : 1;
9581
9583
  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
- }));
9584
+ const allResults = [];
9585
+ let page = startPage;
9586
+ while (allResults.length < requestedLimit && page < startPage + MAX_PAGES) {
9587
+ const response = await this.client.searchPeople({ page, filters });
9588
+ if (response.error || !response.results?.length) break;
9589
+ const mapped = response.results.map((r) => ({
9590
+ first_name: r.person?.first_name ?? "",
9591
+ last_name: r.person?.last_name ?? "",
9592
+ job_title: r.person?.current_job_title ?? void 0,
9593
+ company: r.company?.name ?? void 0,
9594
+ company_domain: r.company?.domain ?? void 0,
9595
+ email: r.person?.email?.email ?? void 0,
9596
+ phone: r.person?.mobile?.mobile ?? void 0,
9597
+ linkedin_url: r.person?.linkedin_url ?? void 0,
9598
+ seniority: r.person?.job_history?.[0]?.seniority ?? void 0,
9599
+ department: r.person?.job_history?.[0]?.departments?.[0] ?? void 0,
9600
+ location: r.person?.location ? [r.person.location.city, r.person.location.state, r.person.location.country].filter(Boolean).join(", ") : void 0,
9601
+ sources: [this.name]
9602
+ }));
9603
+ allResults.push(...mapped);
9604
+ if (response.results.length < PROSPEO_PAGE_SIZE) break;
9605
+ page++;
9606
+ }
9607
+ return allResults.slice(0, requestedLimit);
9598
9608
  } catch (error) {
9599
9609
  if (isApiClientError(error, 400)) {
9600
9610
  const body = error.responseBody;
@@ -10890,7 +10900,7 @@ var init_prospect = __esm({
10890
10900
  },
10891
10901
  {
10892
10902
  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.',
10903
+ 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
10904
  inputSchema: {
10895
10905
  type: "object",
10896
10906
  properties: {
@@ -10979,6 +10989,10 @@ var init_prospect = __esm({
10979
10989
  type: "string",
10980
10990
  description: 'Company revenue range as "min,max" (e.g., "1000000,10000000")'
10981
10991
  },
10992
+ enrich: {
10993
+ type: "boolean",
10994
+ 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."
10995
+ },
10982
10996
  fields: {
10983
10997
  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
10998
  oneOf: [
@@ -11872,172 +11886,452 @@ For any task: always dry_run enrichment batches > 5 to preview cost before execu
11872
11886
  }
11873
11887
  });
11874
11888
 
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;
11884
- }
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
- ];
11905
- }
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}`);
11921
- }
11922
- }
11923
- return warnings;
11889
+ // src/providers/waterfall.ts
11890
+ function getCacheKey(contact) {
11891
+ if (!contact.linkedin_url) return null;
11892
+ return contact.linkedin_url.toLowerCase().trim();
11924
11893
  }
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);
11894
+ function isExhaustedError(error) {
11895
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
11896
+ return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
11930
11897
  }
11931
- var controlSet;
11932
- var init_manifest_validation = __esm({
11933
- "src/providers/manifest-validation.ts"() {
11934
- "use strict";
11935
- init_manifests();
11936
- init_manifest_types();
11937
- controlSet = new Set(CONTROL_PARAMS);
11898
+ function isRateLimitError(error) {
11899
+ if (error instanceof Error && "status" in error) {
11900
+ return error.status === 429;
11938
11901
  }
11939
- });
11940
-
11941
- // src/utils/result-writer.ts
11942
- import * as fs from "fs";
11943
- import * as path from "path";
11944
- import * as os from "os";
11945
- function shouldWriteToFile(count, output) {
11946
- if (output === "file") return true;
11947
- if (output === "inline") return false;
11948
- if (output === "csv") return false;
11949
- return count > FILE_OUTPUT_THRESHOLD;
11902
+ const msg = error instanceof Error ? error.message : String(error);
11903
+ return msg.includes("429") || msg.toLowerCase().includes("rate limit");
11950
11904
  }
11951
- function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR) {
11952
- fs.mkdirSync(resultsDir, { recursive: true });
11953
- const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
11954
- const filename = `${tool}-${timestamp}.jsonl`;
11955
- const filePath = path.join(resultsDir, filename);
11956
- const metaLine = JSON.stringify({ _meta: { ...meta, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
11957
- const dataLines = results.map((r) => JSON.stringify(r));
11958
- const content = [metaLine, ...dataLines].join("\n") + "\n";
11959
- fs.writeFileSync(filePath, content, "utf-8");
11960
- try {
11961
- cleanOldResults(7, resultsDir);
11962
- } catch {
11963
- }
11964
- return filePath;
11905
+ function withTimeout(promise, ms, label) {
11906
+ return new Promise((resolve, reject) => {
11907
+ const timer = setTimeout(
11908
+ () => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
11909
+ ms
11910
+ );
11911
+ promise.then(
11912
+ (val) => {
11913
+ clearTimeout(timer);
11914
+ resolve(val);
11915
+ },
11916
+ (err) => {
11917
+ clearTimeout(timer);
11918
+ reject(err);
11919
+ }
11920
+ );
11921
+ });
11965
11922
  }
11966
- function writeResultsCsv(tool, results, resultsDir = DEFAULT_RESULTS_DIR) {
11967
- fs.mkdirSync(resultsDir, { recursive: true });
11968
- const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
11969
- const filename = `${tool}-${timestamp}.csv`;
11970
- const filePath = path.join(resultsDir, filename);
11971
- if (results.length === 0) {
11972
- fs.writeFileSync(filePath, "", "utf-8");
11973
- return filePath;
11974
- }
11975
- const headers = Object.keys(results[0]);
11976
- const headerLine = headers.map(escapeCsvField).join(",");
11977
- const dataLines = results.map(
11978
- (row) => headers.map((h) => escapeCsvField(formatCsvValue(row[h]))).join(",")
11979
- );
11980
- const content = [headerLine, ...dataLines].join("\n") + "\n";
11981
- fs.writeFileSync(filePath, content, "utf-8");
11982
- try {
11983
- cleanOldResults(7, resultsDir);
11984
- } catch {
11923
+ async function runWaterfall(contacts, config, registry) {
11924
+ const exhausted = /* @__PURE__ */ new Set();
11925
+ const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
11926
+ const concurrency = config.concurrency ?? 1;
11927
+ if (concurrency <= 1) {
11928
+ const results2 = [];
11929
+ for (const contact of contacts) {
11930
+ if (Date.now() > batchDeadline) {
11931
+ results2.push(makeTimeoutResult(contact));
11932
+ continue;
11933
+ }
11934
+ const result = await enrichContact(contact, config, registry, exhausted);
11935
+ results2.push(result);
11936
+ }
11937
+ return results2;
11938
+ }
11939
+ const results = new Array(contacts.length);
11940
+ let nextIndex = 0;
11941
+ async function worker() {
11942
+ while (nextIndex < contacts.length) {
11943
+ const i = nextIndex++;
11944
+ if (Date.now() > batchDeadline) {
11945
+ results[i] = makeTimeoutResult(contacts[i]);
11946
+ continue;
11947
+ }
11948
+ results[i] = await enrichContact(contacts[i], config, registry, exhausted);
11949
+ }
11985
11950
  }
11986
- return filePath;
11987
- }
11988
- function formatCsvValue(value) {
11989
- if (value === null || value === void 0) return "";
11990
- if (Array.isArray(value)) return value.join(";");
11991
- if (typeof value === "object") return JSON.stringify(value);
11992
- return String(value);
11951
+ await Promise.all(Array.from({ length: Math.min(concurrency, contacts.length) }, () => worker()));
11952
+ return results;
11993
11953
  }
11994
- function escapeCsvField(value) {
11995
- if (value.includes(",") || value.includes('"') || value.includes("\n")) {
11996
- return `"${value.replace(/"/g, '""')}"`;
11997
- }
11998
- return value;
11954
+ function makeTimeoutResult(contact) {
11955
+ return {
11956
+ contact,
11957
+ attempts: [
11958
+ {
11959
+ provider: "waterfall",
11960
+ capability: "enrich_email",
11961
+ found: false,
11962
+ charged: false,
11963
+ duration_ms: 0,
11964
+ error: "Batch timeout exceeded \u2014 skipped remaining contacts"
11965
+ }
11966
+ ],
11967
+ charged: false
11968
+ };
11999
11969
  }
12000
- function cleanOldResults(maxAgeDays, resultsDir = DEFAULT_RESULTS_DIR) {
12001
- let entries;
12002
- try {
12003
- entries = fs.readdirSync(resultsDir, { withFileTypes: true });
12004
- } catch {
12005
- return;
12006
- }
12007
- const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
12008
- for (const entry of entries) {
12009
- if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
12010
- const filePath = path.join(resultsDir, entry.name);
12011
- try {
12012
- const stat = fs.statSync(filePath);
12013
- if (stat.mtimeMs < cutoff) {
12014
- fs.unlinkSync(filePath);
11970
+ async function enrichContact(contact, config, registry, exhausted) {
11971
+ const attempts = [];
11972
+ let email;
11973
+ let emailProvider;
11974
+ let phone;
11975
+ let phoneProvider;
11976
+ let charged = false;
11977
+ const cacheKey = getCacheKey(contact);
11978
+ const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
11979
+ for (const field of config.find) {
11980
+ const capability = field === "email" ? "enrich_email" : "enrich_phone";
11981
+ const cachedValue = cached?.[field];
11982
+ if (cachedValue) {
11983
+ if (field === "email") {
11984
+ email = cachedValue;
11985
+ emailProvider = cached.provider;
12015
11986
  }
12016
- } catch {
11987
+ if (field === "phone") {
11988
+ phone = cachedValue;
11989
+ phoneProvider = cached.provider;
11990
+ }
11991
+ attempts.push({
11992
+ provider: cached.provider,
11993
+ capability,
11994
+ found: true,
11995
+ charged: false,
11996
+ duration_ms: 0,
11997
+ cached: true
11998
+ });
11999
+ continue;
12000
+ }
12001
+ const fieldResult = await enrichField(contact, field, config, registry, exhausted);
12002
+ attempts.push(...fieldResult.attempts);
12003
+ if (field === "email" && fieldResult.value) {
12004
+ email = fieldResult.value;
12005
+ emailProvider = fieldResult.provider;
12006
+ charged = true;
12007
+ } else if (field === "phone" && fieldResult.value) {
12008
+ phone = fieldResult.value;
12009
+ phoneProvider = fieldResult.provider;
12010
+ charged = true;
12017
12011
  }
12018
12012
  }
12019
- }
12020
- function formatTimestamp(date) {
12021
- const pad = (n) => String(n).padStart(2, "0");
12022
- return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
12023
- }
12024
- var FILE_OUTPUT_THRESHOLD, DEFAULT_RESULTS_DIR;
12025
- var init_result_writer = __esm({
12026
- "src/utils/result-writer.ts"() {
12027
- "use strict";
12028
- FILE_OUTPUT_THRESHOLD = 20;
12029
- DEFAULT_RESULTS_DIR = path.join(os.homedir(), ".gtm", "results");
12013
+ if (cacheKey && (email || phone)) {
12014
+ const existing = enrichmentCache.get(cacheKey);
12015
+ enrichmentCache.set(cacheKey, {
12016
+ email: email ?? existing?.email,
12017
+ phone: phone ?? existing?.phone,
12018
+ provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
12019
+ timestamp: Date.now()
12020
+ });
12030
12021
  }
12031
- });
12032
-
12033
- // src/handlers/prospect.ts
12034
- var prospect_exports = {};
12035
- __export(prospect_exports, {
12036
- handleProspect: () => handleProspect
12037
- });
12038
- function deduplicatePeople(people) {
12039
- const seen = /* @__PURE__ */ new Set();
12040
- return people.filter((p) => {
12022
+ return {
12023
+ contact,
12024
+ email,
12025
+ phone,
12026
+ email_provider: emailProvider,
12027
+ phone_provider: phoneProvider,
12028
+ attempts,
12029
+ charged
12030
+ };
12031
+ }
12032
+ async function enrichField(contact, field, config, registry, exhausted) {
12033
+ const capability = field === "email" ? "enrich_email" : "enrich_phone";
12034
+ const attempts = [];
12035
+ for (const providerName of config.providers) {
12036
+ if (exhausted.has(providerName)) {
12037
+ attempts.push({
12038
+ provider: providerName,
12039
+ capability,
12040
+ found: false,
12041
+ charged: false,
12042
+ duration_ms: 0,
12043
+ error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
12044
+ });
12045
+ continue;
12046
+ }
12047
+ const provider = registry.get(providerName);
12048
+ if (!provider || !provider.capabilities.includes(capability)) continue;
12049
+ for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
12050
+ const start = Date.now();
12051
+ try {
12052
+ let value;
12053
+ const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
12054
+ if (field === "email" && "enrichEmail" in provider) {
12055
+ const result = await withTimeout(
12056
+ provider.enrichEmail(contact),
12057
+ providerTimeoutMs,
12058
+ providerName
12059
+ );
12060
+ value = result?.email;
12061
+ } else if (field === "phone" && "enrichPhone" in provider) {
12062
+ const result = await withTimeout(
12063
+ provider.enrichPhone(contact),
12064
+ providerTimeoutMs,
12065
+ providerName
12066
+ );
12067
+ value = result?.phone;
12068
+ }
12069
+ const found = !!value;
12070
+ attempts.push({
12071
+ provider: providerName,
12072
+ capability,
12073
+ found,
12074
+ charged: found,
12075
+ duration_ms: Date.now() - start
12076
+ });
12077
+ if (found && config.stopOnFirst) {
12078
+ return { value, provider: providerName, attempts };
12079
+ }
12080
+ break;
12081
+ } catch (error) {
12082
+ const errorMsg = error instanceof Error ? error.message : String(error);
12083
+ if (isExhaustedError(error)) {
12084
+ exhausted.add(providerName);
12085
+ attempts.push({
12086
+ provider: providerName,
12087
+ capability,
12088
+ found: false,
12089
+ charged: false,
12090
+ duration_ms: Date.now() - start,
12091
+ error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
12092
+ });
12093
+ break;
12094
+ }
12095
+ if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
12096
+ const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
12097
+ await new Promise((r) => setTimeout(r, delay));
12098
+ continue;
12099
+ }
12100
+ attempts.push({
12101
+ provider: providerName,
12102
+ capability,
12103
+ found: false,
12104
+ charged: false,
12105
+ duration_ms: Date.now() - start,
12106
+ error: errorMsg
12107
+ });
12108
+ logError(
12109
+ `waterfall:${providerName}`,
12110
+ error instanceof Error ? error : new Error(String(error)),
12111
+ { contact: `${contact.first_name} ${contact.last_name}` }
12112
+ );
12113
+ break;
12114
+ }
12115
+ }
12116
+ }
12117
+ return { attempts };
12118
+ }
12119
+ function dryRunWaterfall(contactsCount, config, registry) {
12120
+ const activeProviders = config.providers.filter((name) => registry.get(name));
12121
+ const costs = {};
12122
+ for (const name of activeProviders) {
12123
+ const pricing = DEFAULT_PRICING[name];
12124
+ if (pricing?.estimated_cost_per_hit) {
12125
+ costs[name] = pricing.estimated_cost_per_hit;
12126
+ } else if (pricing?.estimated_cost_per_lookup) {
12127
+ costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
12128
+ } else {
12129
+ costs[name] = "unknown";
12130
+ }
12131
+ }
12132
+ return {
12133
+ dry_run: true,
12134
+ contacts_count: contactsCount,
12135
+ waterfall: activeProviders,
12136
+ pricing_model: "pay-on-find",
12137
+ estimated_cost_per_hit: costs,
12138
+ note: "Charged only when contact info is found. No result = no charge.",
12139
+ confirm: "Call gtm_enrich again without dry_run to execute."
12140
+ };
12141
+ }
12142
+ var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
12143
+ var init_waterfall = __esm({
12144
+ "src/providers/waterfall.ts"() {
12145
+ "use strict";
12146
+ init_dist();
12147
+ enrichmentCache = /* @__PURE__ */ new Map();
12148
+ DEFAULT_PRICING = {
12149
+ bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
12150
+ prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
12151
+ apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
12152
+ hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" }
12153
+ };
12154
+ EXHAUSTION_PATTERNS = [
12155
+ "insufficient credits",
12156
+ "no credits",
12157
+ "0 credits",
12158
+ "plan limit",
12159
+ "quota exceeded",
12160
+ "upgrade your plan",
12161
+ "not accessible with this api_key on a free plan"
12162
+ ];
12163
+ WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
12164
+ DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
12165
+ DEFAULT_BATCH_TIMEOUT_MS = 3e5;
12166
+ }
12167
+ });
12168
+
12169
+ // src/providers/types.ts
12170
+ function applySlimFields(obj, fields) {
12171
+ const result = {};
12172
+ for (const field of fields) {
12173
+ if (field in obj) {
12174
+ result[field] = obj[field];
12175
+ }
12176
+ }
12177
+ return result;
12178
+ }
12179
+ var SLIM_COMPANY_FIELDS, SLIM_PERSON_FIELDS;
12180
+ var init_types3 = __esm({
12181
+ "src/providers/types.ts"() {
12182
+ "use strict";
12183
+ SLIM_COMPANY_FIELDS = [
12184
+ "domain",
12185
+ "name",
12186
+ "industry",
12187
+ "employees",
12188
+ "country"
12189
+ ];
12190
+ SLIM_PERSON_FIELDS = [
12191
+ "first_name",
12192
+ "last_name",
12193
+ "job_title",
12194
+ "email",
12195
+ "company",
12196
+ "company_domain",
12197
+ "linkedin_url"
12198
+ ];
12199
+ }
12200
+ });
12201
+
12202
+ // src/providers/manifest-validation.ts
12203
+ function validateParams(provider, tool, args) {
12204
+ const manifest = getToolManifest(provider, tool);
12205
+ if (!manifest) return [];
12206
+ const supported = new Set(manifest.supported_params);
12207
+ const warnings = [];
12208
+ for (const [key, value] of Object.entries(args)) {
12209
+ if (value === void 0 || value === null) continue;
12210
+ if (controlSet.has(key)) continue;
12211
+ if (!supported.has(key)) {
12212
+ const alternatives = Object.entries(MANIFESTS).filter(([_, m]) => m[tool]?.supported_params.includes(key)).map(([name]) => name);
12213
+ const suggestion = alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : "";
12214
+ warnings.push(`${key} is not supported by ${provider}.${suggestion}`);
12215
+ }
12216
+ }
12217
+ return warnings;
12218
+ }
12219
+ function clampLimit(provider, tool, limit) {
12220
+ const manifest = getToolManifest(provider, tool);
12221
+ if (!manifest) return limit ?? 50;
12222
+ if (limit === void 0) return manifest.default_limit;
12223
+ return Math.min(limit, manifest.max_limit);
12224
+ }
12225
+ var controlSet;
12226
+ var init_manifest_validation = __esm({
12227
+ "src/providers/manifest-validation.ts"() {
12228
+ "use strict";
12229
+ init_manifests();
12230
+ init_manifest_types();
12231
+ controlSet = new Set(CONTROL_PARAMS);
12232
+ }
12233
+ });
12234
+
12235
+ // src/utils/result-writer.ts
12236
+ import * as fs from "fs";
12237
+ import * as path from "path";
12238
+ import * as os from "os";
12239
+ function shouldWriteToFile(count, output) {
12240
+ if (output === "file") return true;
12241
+ if (output === "inline") return false;
12242
+ if (output === "csv") return false;
12243
+ return count > FILE_OUTPUT_THRESHOLD;
12244
+ }
12245
+ function writeResultsFile(tool, results, meta, resultsDir = DEFAULT_RESULTS_DIR) {
12246
+ fs.mkdirSync(resultsDir, { recursive: true });
12247
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
12248
+ const filename = `${tool}-${timestamp}.jsonl`;
12249
+ const filePath = path.join(resultsDir, filename);
12250
+ const metaLine = JSON.stringify({ _meta: { ...meta, timestamp: (/* @__PURE__ */ new Date()).toISOString() } });
12251
+ const dataLines = results.map((r) => JSON.stringify(r));
12252
+ const content = [metaLine, ...dataLines].join("\n") + "\n";
12253
+ fs.writeFileSync(filePath, content, "utf-8");
12254
+ try {
12255
+ cleanOldResults(7, resultsDir);
12256
+ } catch {
12257
+ }
12258
+ return filePath;
12259
+ }
12260
+ function writeResultsCsv(tool, results, resultsDir = DEFAULT_RESULTS_DIR) {
12261
+ fs.mkdirSync(resultsDir, { recursive: true });
12262
+ const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
12263
+ const filename = `${tool}-${timestamp}.csv`;
12264
+ const filePath = path.join(resultsDir, filename);
12265
+ if (results.length === 0) {
12266
+ fs.writeFileSync(filePath, "", "utf-8");
12267
+ return filePath;
12268
+ }
12269
+ const headers = Object.keys(results[0]);
12270
+ const headerLine = headers.map(escapeCsvField).join(",");
12271
+ const dataLines = results.map(
12272
+ (row) => headers.map((h) => escapeCsvField(formatCsvValue(row[h]))).join(",")
12273
+ );
12274
+ const content = [headerLine, ...dataLines].join("\n") + "\n";
12275
+ fs.writeFileSync(filePath, content, "utf-8");
12276
+ try {
12277
+ cleanOldResults(7, resultsDir);
12278
+ } catch {
12279
+ }
12280
+ return filePath;
12281
+ }
12282
+ function formatCsvValue(value) {
12283
+ if (value === null || value === void 0) return "";
12284
+ if (Array.isArray(value)) return value.join(";");
12285
+ if (typeof value === "object") return JSON.stringify(value);
12286
+ return String(value);
12287
+ }
12288
+ function escapeCsvField(value) {
12289
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
12290
+ return `"${value.replace(/"/g, '""')}"`;
12291
+ }
12292
+ return value;
12293
+ }
12294
+ function cleanOldResults(maxAgeDays, resultsDir = DEFAULT_RESULTS_DIR) {
12295
+ let entries;
12296
+ try {
12297
+ entries = fs.readdirSync(resultsDir, { withFileTypes: true });
12298
+ } catch {
12299
+ return;
12300
+ }
12301
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
12302
+ for (const entry of entries) {
12303
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
12304
+ const filePath = path.join(resultsDir, entry.name);
12305
+ try {
12306
+ const stat = fs.statSync(filePath);
12307
+ if (stat.mtimeMs < cutoff) {
12308
+ fs.unlinkSync(filePath);
12309
+ }
12310
+ } catch {
12311
+ }
12312
+ }
12313
+ }
12314
+ function formatTimestamp(date) {
12315
+ const pad = (n) => String(n).padStart(2, "0");
12316
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
12317
+ }
12318
+ var FILE_OUTPUT_THRESHOLD, DEFAULT_RESULTS_DIR;
12319
+ var init_result_writer = __esm({
12320
+ "src/utils/result-writer.ts"() {
12321
+ "use strict";
12322
+ FILE_OUTPUT_THRESHOLD = 20;
12323
+ DEFAULT_RESULTS_DIR = path.join(os.homedir(), ".gtm", "results");
12324
+ }
12325
+ });
12326
+
12327
+ // src/handlers/prospect.ts
12328
+ var prospect_exports = {};
12329
+ __export(prospect_exports, {
12330
+ handleProspect: () => handleProspect
12331
+ });
12332
+ function deduplicatePeople(people) {
12333
+ const seen = /* @__PURE__ */ new Set();
12334
+ return people.filter((p) => {
12041
12335
  const key = p.linkedin_url ?? p.email ?? `${p.first_name}_${p.last_name}_${p.company}`;
12042
12336
  if (seen.has(key)) return false;
12043
12337
  seen.add(key);
@@ -12199,6 +12493,43 @@ async function findPeople(args, ctx) {
12199
12493
  };
12200
12494
  const results = await finder.findPeople(query);
12201
12495
  const dedupedResults = deduplicatePeople(results);
12496
+ const shouldEnrich = args.enrich ?? false;
12497
+ let enrichedCount = 0;
12498
+ if (shouldEnrich) {
12499
+ const needsEnrichment = dedupedResults.filter(
12500
+ (r) => !r.email || r.email.includes("*")
12501
+ );
12502
+ if (needsEnrichment.length > 0) {
12503
+ try {
12504
+ const contacts = needsEnrichment.map((r) => ({
12505
+ first_name: r.first_name,
12506
+ last_name: r.last_name,
12507
+ company: r.company,
12508
+ company_domain: r.company_domain,
12509
+ linkedin_url: r.linkedin_url
12510
+ }));
12511
+ const clientConfig = ctx.session.getActiveContext().config;
12512
+ const waterfallProviders = clientConfig.waterfall?.email ?? ["prospeo"];
12513
+ const waterfallConfig = {
12514
+ providers: waterfallProviders,
12515
+ find: ["email"],
12516
+ verify: false,
12517
+ stopOnFirst: true,
12518
+ concurrency: 5
12519
+ };
12520
+ const enrichResults = await runWaterfall(contacts, waterfallConfig, ctx.registry);
12521
+ for (let i = 0; i < needsEnrichment.length; i++) {
12522
+ const enriched = enrichResults[i];
12523
+ if (enriched?.email) {
12524
+ needsEnrichment[i].email = enriched.email;
12525
+ enrichedCount++;
12526
+ }
12527
+ }
12528
+ } catch (error) {
12529
+ logError("prospect:inlineEnrich", error instanceof Error ? error : new Error(String(error)));
12530
+ }
12531
+ }
12532
+ }
12202
12533
  const resultsWithAlias = dedupedResults.map((r) => ({
12203
12534
  ...r,
12204
12535
  company_domain: r.company_domain ?? r.company
@@ -12265,6 +12596,7 @@ async function findPeople(args, ctx) {
12265
12596
  has_more,
12266
12597
  offset,
12267
12598
  ...Object.keys(applied_filters).length > 0 && { applied_filters },
12599
+ ...shouldEnrich && { enriched: enrichedCount },
12268
12600
  people: slimmedResults,
12269
12601
  ...warnings?.length ? { warnings } : {}
12270
12602
  };
@@ -12272,7 +12604,9 @@ async function findPeople(args, ctx) {
12272
12604
  var init_prospect2 = __esm({
12273
12605
  "src/handlers/prospect.ts"() {
12274
12606
  "use strict";
12607
+ init_dist();
12275
12608
  init_errors();
12609
+ init_waterfall();
12276
12610
  init_types3();
12277
12611
  init_manifest_validation();
12278
12612
  init_result_writer();
@@ -12280,266 +12614,6 @@ var init_prospect2 = __esm({
12280
12614
  }
12281
12615
  });
12282
12616
 
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
12617
  // src/handlers/enrich.ts
12544
12618
  var enrich_exports = {};
12545
12619
  __export(enrich_exports, {
@@ -13448,7 +13522,8 @@ async function createTable(args) {
13448
13522
  const body = await response.json().catch(() => ({}));
13449
13523
  throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
13450
13524
  }
13451
- return response.json();
13525
+ const result = await response.json();
13526
+ return { ...result, row_ids: rows.map((r) => r._row_id) };
13452
13527
  }
13453
13528
  async function updateTable(args) {
13454
13529
  const tableId = args.table_id;
@@ -13758,6 +13833,7 @@ var init_validation = __esm({
13758
13833
  min_employees: z2.number().int().min(0).optional(),
13759
13834
  max_employees: z2.number().int().min(1).optional(),
13760
13835
  revenue_range: z2.string().optional(),
13836
+ enrich: z2.boolean().optional(),
13761
13837
  fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
13762
13838
  output: z2.enum(["file", "inline", "csv"]).optional(),
13763
13839
  detail: detailSchema