gtm-now 0.10.5 → 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 +519 -435
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
9593
|
-
|
|
9594
|
-
|
|
9595
|
-
|
|
9596
|
-
|
|
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;
|
|
@@ -10784,7 +10794,7 @@ var init_prospect = __esm({
|
|
|
10784
10794
|
prospectTools = [
|
|
10785
10795
|
{
|
|
10786
10796
|
name: "gtm_find_companies",
|
|
10787
|
-
description:
|
|
10797
|
+
description: 'Discover companies matching ICP criteria via DiscoLike, Apollo, StoreLeads, or Exa (semantic/neural search). Use provider: "exa" for meaning-based search that understands intent, not just keywords.',
|
|
10788
10798
|
inputSchema: {
|
|
10789
10799
|
type: "object",
|
|
10790
10800
|
properties: {
|
|
@@ -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: [
|
|
@@ -11526,7 +11540,15 @@ For DTC/e-commerce brands, use provider: "storeleads" \u2014 it has revenue esti
|
|
|
11526
11540
|
|
|
11527
11541
|
Tip: DiscoLike lookalike mode is extremely powerful. If you have 2-3 example companies, use the domains parameter to find hundreds of similar ones.
|
|
11528
11542
|
|
|
11529
|
-
Single domain lookup: passing just domain (no icp_text, domains, category, tech_stack, or country) does a direct company profile fetch with rich data (growth signals, engagement metrics, tech stack from vendors). This is for researching ONE specific company, not discovery
|
|
11543
|
+
Single domain lookup: passing just domain (no icp_text, domains, category, tech_stack, or country) does a direct company profile fetch with rich data (growth signals, engagement metrics, tech stack from vendors). This is for researching ONE specific company, not discovery.
|
|
11544
|
+
|
|
11545
|
+
Exa (provider: "exa") \u2014 semantic/neural search:
|
|
11546
|
+
- Meaning-based ICP matching: "B2B SaaS companies selling to healthcare, not healthtech vendors" actually works
|
|
11547
|
+
- Best for: nuanced ICP descriptions where keyword matching fails
|
|
11548
|
+
- Uses icp_text for semantic search, domains for lookalike discovery
|
|
11549
|
+
- Does NOT return employee count, industry, or tech stack \u2014 use for discovery, then enrich with DiscoLike or Apollo
|
|
11550
|
+
- Must be explicitly selected with provider: "exa" (not auto-selected)
|
|
11551
|
+
- Requires GTM_EXA_API_KEY env var`,
|
|
11530
11552
|
gtm_find_people: `Find contacts at companies by job title, company, or domain. Returns name, title, email (when available), phone, and LinkedIn URL.
|
|
11531
11553
|
|
|
11532
11554
|
Powered by DiscoLike Contacts, Apollo, or BetterContact depending on configuration.
|
|
@@ -11864,171 +11886,451 @@ For any task: always dry_run enrichment batches > 5 to preview cost before execu
|
|
|
11864
11886
|
}
|
|
11865
11887
|
});
|
|
11866
11888
|
|
|
11867
|
-
// src/providers/
|
|
11868
|
-
function
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
if (field in obj) {
|
|
11872
|
-
result[field] = obj[field];
|
|
11873
|
-
}
|
|
11874
|
-
}
|
|
11875
|
-
return result;
|
|
11876
|
-
}
|
|
11877
|
-
var SLIM_COMPANY_FIELDS, SLIM_PERSON_FIELDS;
|
|
11878
|
-
var init_types3 = __esm({
|
|
11879
|
-
"src/providers/types.ts"() {
|
|
11880
|
-
"use strict";
|
|
11881
|
-
SLIM_COMPANY_FIELDS = [
|
|
11882
|
-
"domain",
|
|
11883
|
-
"name",
|
|
11884
|
-
"industry",
|
|
11885
|
-
"employees",
|
|
11886
|
-
"country"
|
|
11887
|
-
];
|
|
11888
|
-
SLIM_PERSON_FIELDS = [
|
|
11889
|
-
"first_name",
|
|
11890
|
-
"last_name",
|
|
11891
|
-
"job_title",
|
|
11892
|
-
"email",
|
|
11893
|
-
"company",
|
|
11894
|
-
"company_domain",
|
|
11895
|
-
"linkedin_url"
|
|
11896
|
-
];
|
|
11897
|
-
}
|
|
11898
|
-
});
|
|
11899
|
-
|
|
11900
|
-
// src/providers/manifest-validation.ts
|
|
11901
|
-
function validateParams(provider, tool, args) {
|
|
11902
|
-
const manifest = getToolManifest(provider, tool);
|
|
11903
|
-
if (!manifest) return [];
|
|
11904
|
-
const supported = new Set(manifest.supported_params);
|
|
11905
|
-
const warnings = [];
|
|
11906
|
-
for (const [key, value] of Object.entries(args)) {
|
|
11907
|
-
if (value === void 0 || value === null) continue;
|
|
11908
|
-
if (controlSet.has(key)) continue;
|
|
11909
|
-
if (!supported.has(key)) {
|
|
11910
|
-
const alternatives = Object.entries(MANIFESTS).filter(([_, m]) => m[tool]?.supported_params.includes(key)).map(([name]) => name);
|
|
11911
|
-
const suggestion = alternatives.length > 0 ? ` Use provider: '${alternatives[0]}' for ${key} filtering.` : "";
|
|
11912
|
-
warnings.push(`${key} is not supported by ${provider}.${suggestion}`);
|
|
11913
|
-
}
|
|
11914
|
-
}
|
|
11915
|
-
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();
|
|
11916
11893
|
}
|
|
11917
|
-
function
|
|
11918
|
-
const
|
|
11919
|
-
|
|
11920
|
-
if (limit === void 0) return manifest.default_limit;
|
|
11921
|
-
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));
|
|
11922
11897
|
}
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
"use strict";
|
|
11927
|
-
init_manifests();
|
|
11928
|
-
init_manifest_types();
|
|
11929
|
-
controlSet = new Set(CONTROL_PARAMS);
|
|
11898
|
+
function isRateLimitError(error) {
|
|
11899
|
+
if (error instanceof Error && "status" in error) {
|
|
11900
|
+
return error.status === 429;
|
|
11930
11901
|
}
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
// src/utils/result-writer.ts
|
|
11934
|
-
import * as fs from "fs";
|
|
11935
|
-
import * as path from "path";
|
|
11936
|
-
import * as os from "os";
|
|
11937
|
-
function shouldWriteToFile(count, output) {
|
|
11938
|
-
if (output === "file") return true;
|
|
11939
|
-
if (output === "inline") return false;
|
|
11940
|
-
if (output === "csv") return false;
|
|
11941
|
-
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");
|
|
11942
11904
|
}
|
|
11943
|
-
function
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
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
|
+
});
|
|
11957
11922
|
}
|
|
11958
|
-
function
|
|
11959
|
-
|
|
11960
|
-
const
|
|
11961
|
-
const
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
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
|
+
}
|
|
11977
11950
|
}
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
function formatCsvValue(value) {
|
|
11981
|
-
if (value === null || value === void 0) return "";
|
|
11982
|
-
if (Array.isArray(value)) return value.join(";");
|
|
11983
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
11984
|
-
return String(value);
|
|
11951
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, contacts.length) }, () => worker()));
|
|
11952
|
+
return results;
|
|
11985
11953
|
}
|
|
11986
|
-
function
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
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
|
+
};
|
|
11991
11969
|
}
|
|
11992
|
-
function
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
const
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
const
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
if (
|
|
12006
|
-
|
|
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;
|
|
12007
11986
|
}
|
|
12008
|
-
|
|
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;
|
|
12009
12011
|
}
|
|
12010
12012
|
}
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
"use strict";
|
|
12020
|
-
FILE_OUTPUT_THRESHOLD = 20;
|
|
12021
|
-
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
|
+
});
|
|
12022
12021
|
}
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
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();
|
|
12032
12334
|
return people.filter((p) => {
|
|
12033
12335
|
const key = p.linkedin_url ?? p.email ?? `${p.first_name}_${p.last_name}_${p.company}`;
|
|
12034
12336
|
if (seen.has(key)) return false;
|
|
@@ -12191,6 +12493,43 @@ async function findPeople(args, ctx) {
|
|
|
12191
12493
|
};
|
|
12192
12494
|
const results = await finder.findPeople(query);
|
|
12193
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
|
+
}
|
|
12194
12533
|
const resultsWithAlias = dedupedResults.map((r) => ({
|
|
12195
12534
|
...r,
|
|
12196
12535
|
company_domain: r.company_domain ?? r.company
|
|
@@ -12257,6 +12596,7 @@ async function findPeople(args, ctx) {
|
|
|
12257
12596
|
has_more,
|
|
12258
12597
|
offset,
|
|
12259
12598
|
...Object.keys(applied_filters).length > 0 && { applied_filters },
|
|
12599
|
+
...shouldEnrich && { enriched: enrichedCount },
|
|
12260
12600
|
people: slimmedResults,
|
|
12261
12601
|
...warnings?.length ? { warnings } : {}
|
|
12262
12602
|
};
|
|
@@ -12264,7 +12604,9 @@ async function findPeople(args, ctx) {
|
|
|
12264
12604
|
var init_prospect2 = __esm({
|
|
12265
12605
|
"src/handlers/prospect.ts"() {
|
|
12266
12606
|
"use strict";
|
|
12607
|
+
init_dist();
|
|
12267
12608
|
init_errors();
|
|
12609
|
+
init_waterfall();
|
|
12268
12610
|
init_types3();
|
|
12269
12611
|
init_manifest_validation();
|
|
12270
12612
|
init_result_writer();
|
|
@@ -12272,266 +12614,6 @@ var init_prospect2 = __esm({
|
|
|
12272
12614
|
}
|
|
12273
12615
|
});
|
|
12274
12616
|
|
|
12275
|
-
// src/providers/waterfall.ts
|
|
12276
|
-
function getCacheKey(contact) {
|
|
12277
|
-
if (!contact.linkedin_url) return null;
|
|
12278
|
-
return contact.linkedin_url.toLowerCase().trim();
|
|
12279
|
-
}
|
|
12280
|
-
function isExhaustedError(error) {
|
|
12281
|
-
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
12282
|
-
return EXHAUSTION_PATTERNS.some((p) => msg.includes(p));
|
|
12283
|
-
}
|
|
12284
|
-
function isRateLimitError(error) {
|
|
12285
|
-
if (error instanceof Error && "status" in error) {
|
|
12286
|
-
return error.status === 429;
|
|
12287
|
-
}
|
|
12288
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
12289
|
-
return msg.includes("429") || msg.toLowerCase().includes("rate limit");
|
|
12290
|
-
}
|
|
12291
|
-
function withTimeout(promise, ms, label) {
|
|
12292
|
-
return new Promise((resolve, reject) => {
|
|
12293
|
-
const timer = setTimeout(
|
|
12294
|
-
() => reject(new Error(`Provider timeout: ${label} did not respond within ${ms}ms`)),
|
|
12295
|
-
ms
|
|
12296
|
-
);
|
|
12297
|
-
promise.then(
|
|
12298
|
-
(val) => {
|
|
12299
|
-
clearTimeout(timer);
|
|
12300
|
-
resolve(val);
|
|
12301
|
-
},
|
|
12302
|
-
(err) => {
|
|
12303
|
-
clearTimeout(timer);
|
|
12304
|
-
reject(err);
|
|
12305
|
-
}
|
|
12306
|
-
);
|
|
12307
|
-
});
|
|
12308
|
-
}
|
|
12309
|
-
async function runWaterfall(contacts, config, registry) {
|
|
12310
|
-
const results = [];
|
|
12311
|
-
const exhausted = /* @__PURE__ */ new Set();
|
|
12312
|
-
const batchDeadline = Date.now() + (config.batchTimeoutMs ?? DEFAULT_BATCH_TIMEOUT_MS);
|
|
12313
|
-
for (const contact of contacts) {
|
|
12314
|
-
if (Date.now() > batchDeadline) {
|
|
12315
|
-
results.push({
|
|
12316
|
-
contact,
|
|
12317
|
-
attempts: [
|
|
12318
|
-
{
|
|
12319
|
-
provider: "waterfall",
|
|
12320
|
-
capability: "enrich_email",
|
|
12321
|
-
found: false,
|
|
12322
|
-
charged: false,
|
|
12323
|
-
duration_ms: 0,
|
|
12324
|
-
error: "Batch timeout exceeded \u2014 skipped remaining contacts"
|
|
12325
|
-
}
|
|
12326
|
-
],
|
|
12327
|
-
charged: false
|
|
12328
|
-
});
|
|
12329
|
-
continue;
|
|
12330
|
-
}
|
|
12331
|
-
const result = await enrichContact(contact, config, registry, exhausted);
|
|
12332
|
-
results.push(result);
|
|
12333
|
-
}
|
|
12334
|
-
return results;
|
|
12335
|
-
}
|
|
12336
|
-
async function enrichContact(contact, config, registry, exhausted) {
|
|
12337
|
-
const attempts = [];
|
|
12338
|
-
let email;
|
|
12339
|
-
let emailProvider;
|
|
12340
|
-
let phone;
|
|
12341
|
-
let phoneProvider;
|
|
12342
|
-
let charged = false;
|
|
12343
|
-
const cacheKey = getCacheKey(contact);
|
|
12344
|
-
const cached = cacheKey ? enrichmentCache.get(cacheKey) : null;
|
|
12345
|
-
for (const field of config.find) {
|
|
12346
|
-
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12347
|
-
const cachedValue = cached?.[field];
|
|
12348
|
-
if (cachedValue) {
|
|
12349
|
-
if (field === "email") {
|
|
12350
|
-
email = cachedValue;
|
|
12351
|
-
emailProvider = cached.provider;
|
|
12352
|
-
}
|
|
12353
|
-
if (field === "phone") {
|
|
12354
|
-
phone = cachedValue;
|
|
12355
|
-
phoneProvider = cached.provider;
|
|
12356
|
-
}
|
|
12357
|
-
attempts.push({
|
|
12358
|
-
provider: cached.provider,
|
|
12359
|
-
capability,
|
|
12360
|
-
found: true,
|
|
12361
|
-
charged: false,
|
|
12362
|
-
duration_ms: 0,
|
|
12363
|
-
cached: true
|
|
12364
|
-
});
|
|
12365
|
-
continue;
|
|
12366
|
-
}
|
|
12367
|
-
const fieldResult = await enrichField(contact, field, config, registry, exhausted);
|
|
12368
|
-
attempts.push(...fieldResult.attempts);
|
|
12369
|
-
if (field === "email" && fieldResult.value) {
|
|
12370
|
-
email = fieldResult.value;
|
|
12371
|
-
emailProvider = fieldResult.provider;
|
|
12372
|
-
charged = true;
|
|
12373
|
-
} else if (field === "phone" && fieldResult.value) {
|
|
12374
|
-
phone = fieldResult.value;
|
|
12375
|
-
phoneProvider = fieldResult.provider;
|
|
12376
|
-
charged = true;
|
|
12377
|
-
}
|
|
12378
|
-
}
|
|
12379
|
-
if (cacheKey && (email || phone)) {
|
|
12380
|
-
const existing = enrichmentCache.get(cacheKey);
|
|
12381
|
-
enrichmentCache.set(cacheKey, {
|
|
12382
|
-
email: email ?? existing?.email,
|
|
12383
|
-
phone: phone ?? existing?.phone,
|
|
12384
|
-
provider: attempts.find((a) => a.found && !a.cached)?.provider ?? existing?.provider ?? "unknown",
|
|
12385
|
-
timestamp: Date.now()
|
|
12386
|
-
});
|
|
12387
|
-
}
|
|
12388
|
-
return {
|
|
12389
|
-
contact,
|
|
12390
|
-
email,
|
|
12391
|
-
phone,
|
|
12392
|
-
email_provider: emailProvider,
|
|
12393
|
-
phone_provider: phoneProvider,
|
|
12394
|
-
attempts,
|
|
12395
|
-
charged
|
|
12396
|
-
};
|
|
12397
|
-
}
|
|
12398
|
-
async function enrichField(contact, field, config, registry, exhausted) {
|
|
12399
|
-
const capability = field === "email" ? "enrich_email" : "enrich_phone";
|
|
12400
|
-
const attempts = [];
|
|
12401
|
-
for (const providerName of config.providers) {
|
|
12402
|
-
if (exhausted.has(providerName)) {
|
|
12403
|
-
attempts.push({
|
|
12404
|
-
provider: providerName,
|
|
12405
|
-
capability,
|
|
12406
|
-
found: false,
|
|
12407
|
-
charged: false,
|
|
12408
|
-
duration_ms: 0,
|
|
12409
|
-
error: "Skipped \u2014 provider exhausted (no credits remaining this batch)"
|
|
12410
|
-
});
|
|
12411
|
-
continue;
|
|
12412
|
-
}
|
|
12413
|
-
const provider = registry.get(providerName);
|
|
12414
|
-
if (!provider || !provider.capabilities.includes(capability)) continue;
|
|
12415
|
-
for (let retryAttempt = 0; retryAttempt <= WATERFALL_RETRY_DELAYS.length; retryAttempt++) {
|
|
12416
|
-
const start = Date.now();
|
|
12417
|
-
try {
|
|
12418
|
-
let value;
|
|
12419
|
-
const providerTimeoutMs = config.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
12420
|
-
if (field === "email" && "enrichEmail" in provider) {
|
|
12421
|
-
const result = await withTimeout(
|
|
12422
|
-
provider.enrichEmail(contact),
|
|
12423
|
-
providerTimeoutMs,
|
|
12424
|
-
providerName
|
|
12425
|
-
);
|
|
12426
|
-
value = result?.email;
|
|
12427
|
-
} else if (field === "phone" && "enrichPhone" in provider) {
|
|
12428
|
-
const result = await withTimeout(
|
|
12429
|
-
provider.enrichPhone(contact),
|
|
12430
|
-
providerTimeoutMs,
|
|
12431
|
-
providerName
|
|
12432
|
-
);
|
|
12433
|
-
value = result?.phone;
|
|
12434
|
-
}
|
|
12435
|
-
const found = !!value;
|
|
12436
|
-
attempts.push({
|
|
12437
|
-
provider: providerName,
|
|
12438
|
-
capability,
|
|
12439
|
-
found,
|
|
12440
|
-
charged: found,
|
|
12441
|
-
duration_ms: Date.now() - start
|
|
12442
|
-
});
|
|
12443
|
-
if (found && config.stopOnFirst) {
|
|
12444
|
-
return { value, provider: providerName, attempts };
|
|
12445
|
-
}
|
|
12446
|
-
break;
|
|
12447
|
-
} catch (error) {
|
|
12448
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
12449
|
-
if (isExhaustedError(error)) {
|
|
12450
|
-
exhausted.add(providerName);
|
|
12451
|
-
attempts.push({
|
|
12452
|
-
provider: providerName,
|
|
12453
|
-
capability,
|
|
12454
|
-
found: false,
|
|
12455
|
-
charged: false,
|
|
12456
|
-
duration_ms: Date.now() - start,
|
|
12457
|
-
error: `Provider exhausted \u2014 skipping for remaining contacts: ${errorMsg}`
|
|
12458
|
-
});
|
|
12459
|
-
break;
|
|
12460
|
-
}
|
|
12461
|
-
if (isRateLimitError(error) && retryAttempt < WATERFALL_RETRY_DELAYS.length) {
|
|
12462
|
-
const delay = WATERFALL_RETRY_DELAYS[retryAttempt];
|
|
12463
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
12464
|
-
continue;
|
|
12465
|
-
}
|
|
12466
|
-
attempts.push({
|
|
12467
|
-
provider: providerName,
|
|
12468
|
-
capability,
|
|
12469
|
-
found: false,
|
|
12470
|
-
charged: false,
|
|
12471
|
-
duration_ms: Date.now() - start,
|
|
12472
|
-
error: errorMsg
|
|
12473
|
-
});
|
|
12474
|
-
logError(
|
|
12475
|
-
`waterfall:${providerName}`,
|
|
12476
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
12477
|
-
{ contact: `${contact.first_name} ${contact.last_name}` }
|
|
12478
|
-
);
|
|
12479
|
-
break;
|
|
12480
|
-
}
|
|
12481
|
-
}
|
|
12482
|
-
}
|
|
12483
|
-
return { attempts };
|
|
12484
|
-
}
|
|
12485
|
-
function dryRunWaterfall(contactsCount, config, registry) {
|
|
12486
|
-
const activeProviders = config.providers.filter((name) => registry.get(name));
|
|
12487
|
-
const costs = {};
|
|
12488
|
-
for (const name of activeProviders) {
|
|
12489
|
-
const pricing = DEFAULT_PRICING[name];
|
|
12490
|
-
if (pricing?.estimated_cost_per_hit) {
|
|
12491
|
-
costs[name] = pricing.estimated_cost_per_hit;
|
|
12492
|
-
} else if (pricing?.estimated_cost_per_lookup) {
|
|
12493
|
-
costs[name] = `${pricing.estimated_cost_per_lookup}/lookup`;
|
|
12494
|
-
} else {
|
|
12495
|
-
costs[name] = "unknown";
|
|
12496
|
-
}
|
|
12497
|
-
}
|
|
12498
|
-
return {
|
|
12499
|
-
dry_run: true,
|
|
12500
|
-
contacts_count: contactsCount,
|
|
12501
|
-
waterfall: activeProviders,
|
|
12502
|
-
pricing_model: "pay-on-find",
|
|
12503
|
-
estimated_cost_per_hit: costs,
|
|
12504
|
-
note: "Charged only when contact info is found. No result = no charge.",
|
|
12505
|
-
confirm: "Call gtm_enrich again without dry_run to execute."
|
|
12506
|
-
};
|
|
12507
|
-
}
|
|
12508
|
-
var enrichmentCache, DEFAULT_PRICING, EXHAUSTION_PATTERNS, WATERFALL_RETRY_DELAYS, DEFAULT_PROVIDER_TIMEOUT_MS, DEFAULT_BATCH_TIMEOUT_MS;
|
|
12509
|
-
var init_waterfall = __esm({
|
|
12510
|
-
"src/providers/waterfall.ts"() {
|
|
12511
|
-
"use strict";
|
|
12512
|
-
init_dist();
|
|
12513
|
-
enrichmentCache = /* @__PURE__ */ new Map();
|
|
12514
|
-
DEFAULT_PRICING = {
|
|
12515
|
-
bettercontact: { charged_only_on_find: true, estimated_cost_per_hit: "$0.10" },
|
|
12516
|
-
prospeo: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" },
|
|
12517
|
-
apollo: { charged_only_on_find: false, estimated_cost_per_lookup: "$0.03" },
|
|
12518
|
-
hunter: { charged_only_on_find: true, estimated_cost_per_hit: "$0.05" }
|
|
12519
|
-
};
|
|
12520
|
-
EXHAUSTION_PATTERNS = [
|
|
12521
|
-
"insufficient credits",
|
|
12522
|
-
"no credits",
|
|
12523
|
-
"0 credits",
|
|
12524
|
-
"plan limit",
|
|
12525
|
-
"quota exceeded",
|
|
12526
|
-
"upgrade your plan",
|
|
12527
|
-
"not accessible with this api_key on a free plan"
|
|
12528
|
-
];
|
|
12529
|
-
WATERFALL_RETRY_DELAYS = [2e3, 5e3, 1e4];
|
|
12530
|
-
DEFAULT_PROVIDER_TIMEOUT_MS = 15e3;
|
|
12531
|
-
DEFAULT_BATCH_TIMEOUT_MS = 3e5;
|
|
12532
|
-
}
|
|
12533
|
-
});
|
|
12534
|
-
|
|
12535
12617
|
// src/handlers/enrich.ts
|
|
12536
12618
|
var enrich_exports = {};
|
|
12537
12619
|
__export(enrich_exports, {
|
|
@@ -13440,7 +13522,8 @@ async function createTable(args) {
|
|
|
13440
13522
|
const body = await response.json().catch(() => ({}));
|
|
13441
13523
|
throw new Error(`Viewer API error ${response.status}: ${JSON.stringify(body)}`);
|
|
13442
13524
|
}
|
|
13443
|
-
|
|
13525
|
+
const result = await response.json();
|
|
13526
|
+
return { ...result, row_ids: rows.map((r) => r._row_id) };
|
|
13444
13527
|
}
|
|
13445
13528
|
async function updateTable(args) {
|
|
13446
13529
|
const tableId = args.table_id;
|
|
@@ -13750,6 +13833,7 @@ var init_validation = __esm({
|
|
|
13750
13833
|
min_employees: z2.number().int().min(0).optional(),
|
|
13751
13834
|
max_employees: z2.number().int().min(1).optional(),
|
|
13752
13835
|
revenue_range: z2.string().optional(),
|
|
13836
|
+
enrich: z2.boolean().optional(),
|
|
13753
13837
|
fields: z2.union([z2.literal("all"), z2.array(z2.string())]).optional(),
|
|
13754
13838
|
output: z2.enum(["file", "inline", "csv"]).optional(),
|
|
13755
13839
|
detail: detailSchema
|