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 +510 -434
- 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;
|
|
@@ -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/
|
|
11876
|
-
function
|
|
11877
|
-
|
|
11878
|
-
|
|
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
|
|
11926
|
-
const
|
|
11927
|
-
|
|
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
|
-
|
|
11932
|
-
|
|
11933
|
-
|
|
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
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
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
|
|
11967
|
-
|
|
11968
|
-
const
|
|
11969
|
-
const
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
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
|
-
|
|
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
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
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
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
const
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
const
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
if (
|
|
12014
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
12026
|
-
|
|
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
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
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
|
-
|
|
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
|