@coldiq/mcp 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/executor.d.ts +4 -0
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +24 -4
- package/dist/executor.js.map +1 -1
- package/dist/registry.d.ts +6 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -1
- package/dist/registry.js.map +1 -1
- package/dist/tools/find-email.d.ts +1 -1
- package/dist/tools/find-email.d.ts.map +1 -1
- package/dist/tools/find-email.js +4 -1
- package/dist/tools/find-email.js.map +1 -1
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +89 -72
- package/dist/tools/find-emails.js.map +1 -1
- package/package.json +1 -1
- package/src/executor.ts +30 -4
- package/src/registry.ts +11 -3
- package/src/tools/find-email.ts +4 -1
- package/src/tools/find-emails.ts +103 -88
- package/tests/executor.test.ts +169 -0
- package/tests/registry.test.ts +19 -2
- package/tests/tools/find-emails.test.ts +43 -0
|
@@ -2,8 +2,8 @@ import { z } from 'zod';
|
|
|
2
2
|
import { callApi } from '../client.js';
|
|
3
3
|
export const findEmailsName = 'find_emails';
|
|
4
4
|
export const findEmailsDescription = 'Find professional emails for multiple people in one batch. ' +
|
|
5
|
-
'Uses bulk
|
|
6
|
-
'
|
|
5
|
+
'Uses Prospeo bulk first; for any misses, runs FullEnrich and FindyMail/IcyPeas in parallel — ' +
|
|
6
|
+
'whichever provider returns an email first wins. ' +
|
|
7
7
|
'Much faster than calling find_email one-by-one. Max 50 people per call. ' +
|
|
8
8
|
'Each person needs a unique id to match results back. ' +
|
|
9
9
|
'Always pass first_name, last_name, and domain — they are the primary enrichment signal. ' +
|
|
@@ -27,88 +27,69 @@ function sleep(ms) {
|
|
|
27
27
|
function missesOf(people, results) {
|
|
28
28
|
return people.filter((p) => !results.find((r) => r.id === p.id)?.email);
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
:
|
|
30
|
+
async function fullEnrichStep(misses, results) {
|
|
31
|
+
const feBody = {
|
|
32
|
+
name: 'mcp-enrich-batch',
|
|
33
|
+
data: misses.map((p) => ({
|
|
34
|
+
custom_id: p.id,
|
|
35
|
+
first_name: p.first_name,
|
|
36
|
+
last_name: p.last_name,
|
|
37
|
+
domain: p.domain,
|
|
38
|
+
...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
|
|
39
|
+
enrich_fields: ['contact.emails'],
|
|
40
|
+
})),
|
|
38
41
|
};
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
enrich_fields: ['contact.emails'],
|
|
65
|
-
})),
|
|
66
|
-
};
|
|
67
|
-
const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody);
|
|
68
|
-
if (feCreateRes.ok) {
|
|
69
|
-
const enrichmentId = feCreateRes.data.enrichment_id;
|
|
70
|
-
if (enrichmentId) {
|
|
71
|
-
const deadline = Date.now() + 90_000;
|
|
72
|
-
while (Date.now() < deadline) {
|
|
73
|
-
await sleep(5000);
|
|
74
|
-
const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`);
|
|
75
|
-
if (!pollRes.ok)
|
|
42
|
+
const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody);
|
|
43
|
+
if (!feCreateRes.ok)
|
|
44
|
+
return;
|
|
45
|
+
const enrichmentId = feCreateRes.data.enrichment_id;
|
|
46
|
+
if (!enrichmentId)
|
|
47
|
+
return;
|
|
48
|
+
// Tightened from 90s → 45s: Step 3 runs in parallel and typically fills misses
|
|
49
|
+
// faster, so FullEnrich's marginal value decays past this point.
|
|
50
|
+
const deadline = Date.now() + 45_000;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
await sleep(5000);
|
|
53
|
+
const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`);
|
|
54
|
+
if (!pollRes.ok)
|
|
55
|
+
continue;
|
|
56
|
+
const pd = pollRes.data;
|
|
57
|
+
const status = pd.status;
|
|
58
|
+
if (status !== 'DONE' && status !== 'FAILED')
|
|
59
|
+
continue;
|
|
60
|
+
if (status === 'DONE') {
|
|
61
|
+
const feItems = pd.data;
|
|
62
|
+
if (Array.isArray(feItems)) {
|
|
63
|
+
for (const item of feItems) {
|
|
64
|
+
const personId = item.custom_id;
|
|
65
|
+
const hit = personId ? results.find((r) => r.id === personId) : undefined;
|
|
66
|
+
if (!hit || hit.email)
|
|
76
67
|
continue;
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const feItems = pd.data;
|
|
82
|
-
if (Array.isArray(feItems)) {
|
|
83
|
-
feItems.forEach((item) => {
|
|
84
|
-
const personId = item.custom_id;
|
|
85
|
-
const hit = personId ? results.find((r) => r.id === personId) : undefined;
|
|
86
|
-
if (!hit || hit.email)
|
|
87
|
-
return;
|
|
88
|
-
const emails = item.emails;
|
|
89
|
-
if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
|
|
90
|
-
hit.email = emails[0];
|
|
91
|
-
hit.provider = 'fullenrich';
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
break;
|
|
68
|
+
const emails = item.emails;
|
|
69
|
+
if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
|
|
70
|
+
hit.email = emails[0];
|
|
71
|
+
hit.provider = 'fullenrich';
|
|
97
72
|
}
|
|
98
73
|
}
|
|
99
74
|
}
|
|
100
75
|
}
|
|
76
|
+
return;
|
|
101
77
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await Promise.all(
|
|
78
|
+
}
|
|
79
|
+
async function findymailIcypeasStep(misses, results) {
|
|
80
|
+
await Promise.all(misses.map(async (person) => {
|
|
105
81
|
const hit = results.find((r) => r.id === person.id);
|
|
82
|
+
if (!hit)
|
|
83
|
+
return;
|
|
84
|
+
// Skip if the parallel FullEnrich branch already filled this person.
|
|
85
|
+
if (hit.email)
|
|
86
|
+
return;
|
|
106
87
|
const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ');
|
|
107
88
|
const fmRes = await callApi('POST', '/findymail/search/name', {
|
|
108
89
|
name: fullName,
|
|
109
90
|
domain: person.domain,
|
|
110
91
|
});
|
|
111
|
-
if (fmRes.ok) {
|
|
92
|
+
if (!hit.email && fmRes.ok) {
|
|
112
93
|
const d = fmRes.data;
|
|
113
94
|
if (typeof d.email === 'string' && d.email.includes('@')) {
|
|
114
95
|
hit.email = d.email;
|
|
@@ -116,12 +97,14 @@ export async function findEmailsHandler(input) {
|
|
|
116
97
|
return;
|
|
117
98
|
}
|
|
118
99
|
}
|
|
100
|
+
if (hit.email)
|
|
101
|
+
return;
|
|
119
102
|
const icyRes = await callApi('POST', '/icypeas/email-search', {
|
|
120
103
|
firstname: person.first_name,
|
|
121
104
|
lastname: person.last_name,
|
|
122
105
|
domainOrCompany: person.domain,
|
|
123
106
|
});
|
|
124
|
-
if (icyRes.ok) {
|
|
107
|
+
if (!hit.email && icyRes.ok) {
|
|
125
108
|
const d = icyRes.data;
|
|
126
109
|
const email = typeof d.email === 'string' && d.email.includes('@')
|
|
127
110
|
? d.email
|
|
@@ -134,6 +117,40 @@ export async function findEmailsHandler(input) {
|
|
|
134
117
|
}
|
|
135
118
|
}
|
|
136
119
|
}));
|
|
120
|
+
}
|
|
121
|
+
export async function findEmailsHandler(input) {
|
|
122
|
+
const people = input.people;
|
|
123
|
+
const results = people.map((p) => ({ id: p.id, email: null, provider: null }));
|
|
124
|
+
// Step 1: Prospeo bulk — 1 call for all people
|
|
125
|
+
const bulkBody = {
|
|
126
|
+
data: people.map((p) => p.linkedin_url
|
|
127
|
+
? { identifier: p.id, linkedin_url: p.linkedin_url }
|
|
128
|
+
: { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain }),
|
|
129
|
+
};
|
|
130
|
+
const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody);
|
|
131
|
+
if (bulkRes.ok) {
|
|
132
|
+
const data = bulkRes.data;
|
|
133
|
+
for (const item of data.results ?? []) {
|
|
134
|
+
const email = item.person?.email?.email;
|
|
135
|
+
if (typeof email === 'string' && email.includes('@')) {
|
|
136
|
+
const hit = results.find((r) => r.id === item.identifier);
|
|
137
|
+
if (hit) {
|
|
138
|
+
hit.email = email;
|
|
139
|
+
hit.provider = 'prospeo';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Steps 2 & 3 run concurrently for Prospeo misses.
|
|
145
|
+
// Step 2: FullEnrich bulk async (slow tail, better European coverage)
|
|
146
|
+
// Step 3: per-person FindyMail → IcyPeas waterfall
|
|
147
|
+
// Both branches write to the shared `results` array; every write site checks
|
|
148
|
+
// `if (hit.email)` first so whichever provider arrives first wins and the
|
|
149
|
+
// other does not overwrite.
|
|
150
|
+
const misses = missesOf(people, results);
|
|
151
|
+
if (misses.length > 0) {
|
|
152
|
+
await Promise.all([fullEnrichStep(misses, results), findymailIcypeasStep(misses, results)]);
|
|
153
|
+
}
|
|
137
154
|
const found = results.filter((r) => r.email !== null).length;
|
|
138
155
|
return {
|
|
139
156
|
content: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find-emails.js","sourceRoot":"","sources":["../../src/tools/find-emails.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAE3C,MAAM,CAAC,MAAM,qBAAqB,GAChC,6DAA6D;IAC7D
|
|
1
|
+
{"version":3,"file":"find-emails.js","sourceRoot":"","sources":["../../src/tools/find-emails.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/executor.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface ExecutionResult {
|
|
|
13
13
|
export interface ExecutionError {
|
|
14
14
|
error: string
|
|
15
15
|
providers_tried: Array<{ provider: string; status: number; error: string }>
|
|
16
|
+
/** Set when at least one provider returned 402 — points the user at the top-up page. */
|
|
17
|
+
billingUrl?: string
|
|
18
|
+
/** True when every attempted provider failed with 402 (the user is fully blocked on credits). */
|
|
19
|
+
insufficientCredits?: boolean
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function isExecutionError(result: ExecutionResult | ExecutionError): result is ExecutionError {
|
|
@@ -69,7 +73,11 @@ async function executeAsync(
|
|
|
69
73
|
const deadline = Date.now() + asyncCfg.timeoutMs
|
|
70
74
|
let pollCount = 0
|
|
71
75
|
while (Date.now() < deadline) {
|
|
72
|
-
|
|
76
|
+
const interval =
|
|
77
|
+
typeof asyncCfg.pollIntervalMs === 'function'
|
|
78
|
+
? asyncCfg.pollIntervalMs(pollCount + 1)
|
|
79
|
+
: asyncCfg.pollIntervalMs
|
|
80
|
+
await sleep(interval)
|
|
73
81
|
pollCount++
|
|
74
82
|
|
|
75
83
|
const pollRes = await callApi('GET', asyncCfg.pollEndpoint(jobId))
|
|
@@ -140,6 +148,7 @@ export async function executeWithFallback(
|
|
|
140
148
|
: getProviders(capability)
|
|
141
149
|
|
|
142
150
|
const errors: Array<{ id: string; status: number; error: string }> = []
|
|
151
|
+
let billingUrl: string | undefined
|
|
143
152
|
|
|
144
153
|
for (const provider of providers) {
|
|
145
154
|
if (provider.isApplicable) {
|
|
@@ -189,16 +198,33 @@ export async function executeWithFallback(
|
|
|
189
198
|
: `Status ${result.status}, no results`
|
|
190
199
|
log(`${capability} → ${provider.id} ✗ ${errMsg}`)
|
|
191
200
|
errors.push({ id: provider.id, status: result.status, error: errMsg })
|
|
201
|
+
|
|
202
|
+
// Capture the billing URL the API surfaces in 402 responses so the
|
|
203
|
+
// top-level error can include a clickable link for the user.
|
|
204
|
+
if (result.status === 402 && !billingUrl
|
|
205
|
+
&& result.data && typeof result.data === 'object'
|
|
206
|
+
&& typeof (result.data as Record<string, unknown>).billingUrl === 'string') {
|
|
207
|
+
billingUrl = (result.data as Record<string, string>).billingUrl
|
|
208
|
+
}
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
debug(`${capability}: all ${errors.length} providers failed — ${JSON.stringify(errors)}`)
|
|
195
212
|
const sanitized = errors.map((e, i) => ({
|
|
196
213
|
provider: `provider_${i + 1}`,
|
|
197
214
|
status: e.status,
|
|
198
|
-
error: e.error.slice(0,
|
|
215
|
+
error: e.error.slice(0, 200),
|
|
199
216
|
}))
|
|
200
|
-
|
|
201
|
-
|
|
217
|
+
|
|
218
|
+
const allOutOfCredits = errors.length > 0 && errors.every((e) => e.status === 402)
|
|
219
|
+
const topLevelError = allOutOfCredits && billingUrl
|
|
220
|
+
? `Insufficient credits — every provider for ${capability} returned 402. Top up at ${billingUrl} to continue.`
|
|
221
|
+
: `All ${errors.length} providers failed for ${capability}`
|
|
222
|
+
|
|
223
|
+
const out: ExecutionError = {
|
|
224
|
+
error: topLevelError,
|
|
202
225
|
providers_tried: sanitized,
|
|
203
226
|
}
|
|
227
|
+
if (billingUrl) out.billingUrl = billingUrl
|
|
228
|
+
if (allOutOfCredits) out.insufficientCredits = true
|
|
229
|
+
return out
|
|
204
230
|
}
|
package/src/registry.ts
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
export interface AsyncConfig {
|
|
6
6
|
/** Build the poll endpoint path from the ID returned by the create call */
|
|
7
7
|
pollEndpoint: (id: string) => string
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Milliseconds between poll attempts. Either a constant or a function of the
|
|
10
|
+
* 1-indexed attempt number (1 = first poll). Use the function form to apply
|
|
11
|
+
* a fast-then-backoff schedule.
|
|
12
|
+
*/
|
|
13
|
+
pollIntervalMs: number | ((attempt: number) => number)
|
|
10
14
|
/** Max milliseconds before giving up */
|
|
11
15
|
timeoutMs: number
|
|
12
16
|
/** Check if the async job is done */
|
|
@@ -963,7 +967,11 @@ const findPeopleProviders: ProviderEntry[] = [
|
|
|
963
967
|
},
|
|
964
968
|
async: {
|
|
965
969
|
pollEndpoint: (id: string) => `/leadsfactory/contact-finder/searches/${id}`,
|
|
966
|
-
|
|
970
|
+
// Continuously growing backoff: fast first probe (3s), then ramp up indefinitely
|
|
971
|
+
// — never plateaus, but the cumulative delay is bounded by timeoutMs.
|
|
972
|
+
// Schedule: 3s, 7s, 15s, 25s, 35s, 45s, 55s, 65s, +10s per attempt thereafter.
|
|
973
|
+
pollIntervalMs: (attempt) =>
|
|
974
|
+
attempt === 1 ? 3000 : attempt === 2 ? 7000 : 15000 + (attempt - 3) * 10000,
|
|
967
975
|
timeoutMs: 300_000, // 5 minutes
|
|
968
976
|
isComplete: (data) => {
|
|
969
977
|
const d = data as Record<string, unknown>
|
package/src/tools/find-email.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { executeWithFallback, isExecutionError } from '../executor.js'
|
|
|
4
4
|
export const findEmailName = 'find_email'
|
|
5
5
|
|
|
6
6
|
export const findEmailDescription =
|
|
7
|
-
"Find a person's professional email given their name and company.
|
|
7
|
+
"Find a person's professional email given their name and company. " +
|
|
8
|
+
'For more than one person, ALWAYS use find_emails instead — never loop this tool. ' +
|
|
9
|
+
'Looping find_email runs the full provider waterfall sequentially per person and is dramatically slower than the batched find_emails path. ' +
|
|
10
|
+
'Tries multiple providers in sequence until an email is found (waterfall). Returns the email and which provider found it.'
|
|
8
11
|
|
|
9
12
|
export const findEmailSchema = {
|
|
10
13
|
first_name: z.string().optional().describe('First name of the person'),
|
package/src/tools/find-emails.ts
CHANGED
|
@@ -5,8 +5,8 @@ export const findEmailsName = 'find_emails'
|
|
|
5
5
|
|
|
6
6
|
export const findEmailsDescription =
|
|
7
7
|
'Find professional emails for multiple people in one batch. ' +
|
|
8
|
-
'Uses bulk
|
|
9
|
-
'
|
|
8
|
+
'Uses Prospeo bulk first; for any misses, runs FullEnrich and FindyMail/IcyPeas in parallel — ' +
|
|
9
|
+
'whichever provider returns an email first wins. ' +
|
|
10
10
|
'Much faster than calling find_email one-by-one. Max 50 people per call. ' +
|
|
11
11
|
'Each person needs a unique id to match results back. ' +
|
|
12
12
|
'Always pass first_name, last_name, and domain — they are the primary enrichment signal. ' +
|
|
@@ -50,106 +50,72 @@ function missesOf(people: PersonInput[], results: EmailResult[]): PersonInput[]
|
|
|
50
50
|
return people.filter((p) => !results.find((r) => r.id === p.id)?.email)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
p.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
),
|
|
53
|
+
async function fullEnrichStep(misses: PersonInput[], results: EmailResult[]): Promise<void> {
|
|
54
|
+
const feBody = {
|
|
55
|
+
name: 'mcp-enrich-batch',
|
|
56
|
+
data: misses.map((p) => ({
|
|
57
|
+
custom_id: p.id,
|
|
58
|
+
first_name: p.first_name,
|
|
59
|
+
last_name: p.last_name,
|
|
60
|
+
domain: p.domain,
|
|
61
|
+
...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
|
|
62
|
+
enrich_fields: ['contact.emails'],
|
|
63
|
+
})),
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
first_name: p.first_name,
|
|
96
|
-
last_name: p.last_name,
|
|
97
|
-
domain: p.domain,
|
|
98
|
-
...(p.linkedin_url ? { linkedin_url: p.linkedin_url } : {}),
|
|
99
|
-
enrich_fields: ['contact.emails'],
|
|
100
|
-
})),
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody)
|
|
104
|
-
|
|
105
|
-
if (feCreateRes.ok) {
|
|
106
|
-
const enrichmentId = (feCreateRes.data as Record<string, unknown>).enrichment_id as string | undefined
|
|
107
|
-
|
|
108
|
-
if (enrichmentId) {
|
|
109
|
-
const deadline = Date.now() + 90_000
|
|
110
|
-
while (Date.now() < deadline) {
|
|
111
|
-
await sleep(5000)
|
|
112
|
-
const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`)
|
|
113
|
-
if (!pollRes.ok) continue
|
|
114
|
-
|
|
115
|
-
const pd = pollRes.data as Record<string, unknown>
|
|
116
|
-
const status = pd.status as string | undefined
|
|
117
|
-
if (status === 'DONE' || status === 'FAILED') {
|
|
118
|
-
if (status === 'DONE') {
|
|
119
|
-
const feItems = pd.data as Array<Record<string, unknown>> | undefined
|
|
120
|
-
if (Array.isArray(feItems)) {
|
|
121
|
-
feItems.forEach((item) => {
|
|
122
|
-
const personId = item.custom_id as string | undefined
|
|
123
|
-
const hit = personId ? results.find((r) => r.id === personId) : undefined
|
|
124
|
-
if (!hit || hit.email) return
|
|
125
|
-
const emails = item.emails as string[] | undefined
|
|
126
|
-
if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
|
|
127
|
-
hit.email = emails[0]
|
|
128
|
-
hit.provider = 'fullenrich'
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
break
|
|
66
|
+
const feCreateRes = await callApi('POST', '/fullenrich/contact/enrich/bulk', feBody)
|
|
67
|
+
if (!feCreateRes.ok) return
|
|
68
|
+
|
|
69
|
+
const enrichmentId = (feCreateRes.data as Record<string, unknown>).enrichment_id as string | undefined
|
|
70
|
+
if (!enrichmentId) return
|
|
71
|
+
|
|
72
|
+
// Tightened from 90s → 45s: Step 3 runs in parallel and typically fills misses
|
|
73
|
+
// faster, so FullEnrich's marginal value decays past this point.
|
|
74
|
+
const deadline = Date.now() + 45_000
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
await sleep(5000)
|
|
77
|
+
const pollRes = await callApi('GET', `/fullenrich/contact/enrich/bulk/${enrichmentId}`)
|
|
78
|
+
if (!pollRes.ok) continue
|
|
79
|
+
|
|
80
|
+
const pd = pollRes.data as Record<string, unknown>
|
|
81
|
+
const status = pd.status as string | undefined
|
|
82
|
+
if (status !== 'DONE' && status !== 'FAILED') continue
|
|
83
|
+
|
|
84
|
+
if (status === 'DONE') {
|
|
85
|
+
const feItems = pd.data as Array<Record<string, unknown>> | undefined
|
|
86
|
+
if (Array.isArray(feItems)) {
|
|
87
|
+
for (const item of feItems) {
|
|
88
|
+
const personId = item.custom_id as string | undefined
|
|
89
|
+
const hit = personId ? results.find((r) => r.id === personId) : undefined
|
|
90
|
+
if (!hit || hit.email) continue
|
|
91
|
+
const emails = item.emails as string[] | undefined
|
|
92
|
+
if (Array.isArray(emails) && emails.length > 0 && typeof emails[0] === 'string' && emails[0].includes('@')) {
|
|
93
|
+
hit.email = emails[0]
|
|
94
|
+
hit.provider = 'fullenrich'
|
|
134
95
|
}
|
|
135
96
|
}
|
|
136
97
|
}
|
|
137
98
|
}
|
|
99
|
+
return
|
|
138
100
|
}
|
|
101
|
+
}
|
|
139
102
|
|
|
140
|
-
|
|
141
|
-
const afterFullEnrich = missesOf(people, results)
|
|
142
|
-
|
|
103
|
+
async function findymailIcypeasStep(misses: PersonInput[], results: EmailResult[]): Promise<void> {
|
|
143
104
|
await Promise.all(
|
|
144
|
-
|
|
145
|
-
const hit = results.find((r) => r.id === person.id)
|
|
105
|
+
misses.map(async (person) => {
|
|
106
|
+
const hit = results.find((r) => r.id === person.id)
|
|
107
|
+
if (!hit) return
|
|
108
|
+
|
|
109
|
+
// Skip if the parallel FullEnrich branch already filled this person.
|
|
110
|
+
if (hit.email) return
|
|
111
|
+
|
|
146
112
|
const fullName = [person.first_name, person.last_name].filter(Boolean).join(' ')
|
|
147
113
|
|
|
148
114
|
const fmRes = await callApi('POST', '/findymail/search/name', {
|
|
149
115
|
name: fullName,
|
|
150
116
|
domain: person.domain,
|
|
151
117
|
})
|
|
152
|
-
if (fmRes.ok) {
|
|
118
|
+
if (!hit.email && fmRes.ok) {
|
|
153
119
|
const d = fmRes.data as Record<string, unknown>
|
|
154
120
|
if (typeof d.email === 'string' && d.email.includes('@')) {
|
|
155
121
|
hit.email = d.email
|
|
@@ -158,12 +124,14 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
|
|
|
158
124
|
}
|
|
159
125
|
}
|
|
160
126
|
|
|
127
|
+
if (hit.email) return
|
|
128
|
+
|
|
161
129
|
const icyRes = await callApi('POST', '/icypeas/email-search', {
|
|
162
130
|
firstname: person.first_name,
|
|
163
131
|
lastname: person.last_name,
|
|
164
132
|
domainOrCompany: person.domain,
|
|
165
133
|
})
|
|
166
|
-
if (icyRes.ok) {
|
|
134
|
+
if (!hit.email && icyRes.ok) {
|
|
167
135
|
const d = icyRes.data as Record<string, unknown>
|
|
168
136
|
const email =
|
|
169
137
|
typeof d.email === 'string' && d.email.includes('@')
|
|
@@ -178,6 +146,53 @@ export async function findEmailsHandler(input: Record<string, unknown>) {
|
|
|
178
146
|
}
|
|
179
147
|
}),
|
|
180
148
|
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function findEmailsHandler(input: Record<string, unknown>) {
|
|
152
|
+
const people = input.people as PersonInput[]
|
|
153
|
+
const results: EmailResult[] = people.map((p) => ({ id: p.id, email: null, provider: null }))
|
|
154
|
+
|
|
155
|
+
// Step 1: Prospeo bulk — 1 call for all people
|
|
156
|
+
const bulkBody = {
|
|
157
|
+
data: people.map((p) =>
|
|
158
|
+
p.linkedin_url
|
|
159
|
+
? { identifier: p.id, linkedin_url: p.linkedin_url }
|
|
160
|
+
: { identifier: p.id, first_name: p.first_name, last_name: p.last_name, company_name: p.domain },
|
|
161
|
+
),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const bulkRes = await callApi('POST', '/prospeo/bulk-enrich-person', bulkBody)
|
|
165
|
+
|
|
166
|
+
if (bulkRes.ok) {
|
|
167
|
+
const data = bulkRes.data as {
|
|
168
|
+
results?: Array<{
|
|
169
|
+
identifier: string
|
|
170
|
+
person?: { email?: { email?: string } }
|
|
171
|
+
}>
|
|
172
|
+
}
|
|
173
|
+
for (const item of data.results ?? []) {
|
|
174
|
+
const email = item.person?.email?.email
|
|
175
|
+
if (typeof email === 'string' && email.includes('@')) {
|
|
176
|
+
const hit = results.find((r) => r.id === item.identifier)
|
|
177
|
+
if (hit) {
|
|
178
|
+
hit.email = email
|
|
179
|
+
hit.provider = 'prospeo'
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Steps 2 & 3 run concurrently for Prospeo misses.
|
|
186
|
+
// Step 2: FullEnrich bulk async (slow tail, better European coverage)
|
|
187
|
+
// Step 3: per-person FindyMail → IcyPeas waterfall
|
|
188
|
+
// Both branches write to the shared `results` array; every write site checks
|
|
189
|
+
// `if (hit.email)` first so whichever provider arrives first wins and the
|
|
190
|
+
// other does not overwrite.
|
|
191
|
+
const misses = missesOf(people, results)
|
|
192
|
+
|
|
193
|
+
if (misses.length > 0) {
|
|
194
|
+
await Promise.all([fullEnrichStep(misses, results), findymailIcypeasStep(misses, results)])
|
|
195
|
+
}
|
|
181
196
|
|
|
182
197
|
const found = results.filter((r) => r.email !== null).length
|
|
183
198
|
return {
|