fix(hunter): align tools, block, and outputs with Hunter.io v2 API spec (#4511)

* fix(hunter): align tools, block, and outputs with Hunter.io v2 API spec

* fix(hunter): match documented Discover response and coerce numeric employees
This commit is contained in:
Waleed
2026-05-07 20:01:35 -07:00
committed by GitHub
parent a2aa648f86
commit 235a62fd95
9 changed files with 639 additions and 265 deletions
+40 -28
View File
@@ -1,5 +1,5 @@
---
title: Hunter io
title: Hunter.io
description: Find and verify professional email addresses
---
@@ -53,11 +53,16 @@ Returns companies matching a set of criteria using Hunter.io AI-powered search.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `results` | array | List of companies matching the search criteria |
| ↳ `name` | string | Company name |
| ↳ `domain` | string | Company domain |
| ↳ `name` | string | Company/organization name |
| ↳ `headcount` | number | Company size/headcount |
| ↳ `technologies` | array | Technologies used by the company |
| ↳ `email_count` | number | Total number of email addresses found |
| ↳ `logo` | string | URL of the company logo |
| ↳ `linkedin_url` | string | LinkedIn profile URL of the company |
| ↳ `company_type` | string | Company type \(e.g., privately held, public company\) |
| ↳ `industry` | string | Industry of the company |
| ↳ `size` | string | Headcount range of the company |
| ↳ `location` | string | Headquarters location |
| ↳ `founded_year` | number | Year the company was founded |
| ↳ `crunchbase_url` | string | Crunchbase URL of the company |
### `hunter_domain_search`
@@ -86,8 +91,9 @@ Returns all the email addresses found using one given domain name, with sources.
| ↳ `first_name` | string | Person's first name |
| ↳ `last_name` | string | Person's last name |
| ↳ `position` | string | Job title/position |
| ↳ `position_raw` | string | Raw job title as found |
| ↳ `seniority` | string | Seniority level \(junior, senior, executive\) |
| ↳ `department` | string | Department \(executive, it, finance, management, sales, legal, support, hr, marketing, communication\) |
| ↳ `department` | string | Department \(executive, it, finance, management, sales, legal, support, hr, marketing, communication, education, design, health, operations\) |
| ↳ `linkedin` | string | LinkedIn profile URL |
| ↳ `twitter` | string | Twitter handle |
| ↳ `phone_number` | string | Phone number |
@@ -106,19 +112,7 @@ Returns all the email addresses found using one given domain name, with sources.
| `accept_all` | boolean | Whether the server accepts all email addresses \(may cause false positives\) |
| `pattern` | string | The email pattern used by the organization \(e.g., \{first\}, \{first\}.\{last\}\) |
| `organization` | string | The organization/company name |
| `description` | string | Description of the organization |
| `industry` | string | Industry classification of the organization |
| `twitter` | string | Twitter handle of the organization |
| `facebook` | string | Facebook page URL of the organization |
| `linkedin` | string | LinkedIn company page URL |
| `instagram` | string | Instagram profile of the organization |
| `youtube` | string | YouTube channel of the organization |
| `technologies` | array | Technologies used by the organization |
| `country` | string | Country where the organization is headquartered |
| `state` | string | State/province where the organization is located |
| `city` | string | City where the organization is located |
| `postal_code` | string | Postal code of the organization |
| `street` | string | Street address of the organization |
| `linked_domains` | array | Other domains linked to the organization |
### `hunter_email_finder`
@@ -147,8 +141,17 @@ Finds the most likely email address for a person given their name and company do
| `verification` | object | Email verification information |
| ↳ `date` | string | Date when the email was verified \(YYYY-MM-DD\) |
| ↳ `status` | string | Verification status \(valid, invalid, accept_all, webmail, disposable, unknown\) |
| `first_name` | string | Person's first name |
| `last_name` | string | Person's last name |
| `email` | string | The found email address |
| `score` | number | Confidence score \(0-100\) for the found email address |
| `domain` | string | Domain that was searched |
| `accept_all` | boolean | Whether the server accepts all email addresses \(may cause false positives\) |
| `position` | string | Job title/position |
| `twitter` | string | Twitter handle |
| `linkedin_url` | string | LinkedIn profile URL |
| `phone_number` | string | Phone number |
| `company` | string | Company name |
### `hunter_email_verifier`
@@ -200,15 +203,24 @@ Enriches company data using domain name.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `company` | object | Company information |
| ↳ `name` | string | Company name |
| ↳ `domain` | string | Company domain |
| ↳ `industry` | string | Industry classification |
| ↳ `size` | string | Company size/headcount range |
| ↳ `country` | string | Country where the company is located |
| ↳ `linkedin` | string | LinkedIn company page URL |
| ↳ `twitter` | string | Twitter handle |
| `person` | object | Person information \(undefined for companies_find tool\) |
| `name` | string | Company name |
| `domain` | string | Company domain |
| `description` | string | Company description |
| `industry` | string | Industry classification |
| `sector` | string | Business sector |
| `size` | string | Employee headcount range \(e.g., "11-50"\) |
| `founded_year` | number | Year founded |
| `location` | string | Headquarters location \(formatted\) |
| `country` | string | Country \(full name\) |
| `country_code` | string | ISO 3166-1 alpha-2 country code |
| `state` | string | State/province |
| `city` | string | City |
| `linkedin` | string | LinkedIn handle \(e.g., company/hunterio\) |
| `twitter` | string | Twitter handle |
| `facebook` | string | Facebook handle |
| `logo` | string | Company logo URL |
| `phone` | string | Company phone number |
| `tech` | array | Technologies used by the company |
### `hunter_email_count`
@@ -6526,7 +6526,7 @@
{
"type": "hunter",
"slug": "hunter-io",
"name": "Hunter io",
"name": "Hunter.io",
"description": "Find and verify professional email addresses",
"longDescription": "Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses.",
"bgColor": "#E0E0E0",
+152 -8
View File
@@ -4,7 +4,7 @@ import type { HunterResponse } from '@/tools/hunter/types'
export const HunterBlock: BlockConfig<HunterResponse> = {
type: 'hunter',
name: 'Hunter io',
name: 'Hunter.io',
description: 'Find and verify professional email addresses',
authMode: AuthMode.ApiKey,
longDescription:
@@ -45,6 +45,15 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
type: 'short-input',
placeholder: '10',
condition: { field: 'operation', value: 'hunter_domain_search' },
mode: 'advanced',
},
{
id: 'offset',
title: 'Offset',
type: 'short-input',
placeholder: '0',
condition: { field: 'operation', value: 'hunter_domain_search' },
mode: 'advanced',
},
{
id: 'type',
@@ -57,6 +66,7 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
],
value: () => 'all',
condition: { field: 'operation', value: 'hunter_domain_search' },
mode: 'advanced',
},
{
id: 'seniority',
@@ -70,6 +80,7 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
],
value: () => 'all',
condition: { field: 'operation', value: 'hunter_domain_search' },
mode: 'advanced',
},
{
id: 'department',
@@ -77,6 +88,7 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
type: 'short-input',
placeholder: 'e.g., sales, marketing, engineering',
condition: { field: 'operation', value: 'hunter_domain_search' },
mode: 'advanced',
},
// Email Finder operation inputs
{
@@ -109,6 +121,7 @@ export const HunterBlock: BlockConfig<HunterResponse> = {
type: 'short-input',
placeholder: 'Enter company name',
condition: { field: 'operation', value: 'hunter_email_finder' },
mode: 'advanced',
},
// Email Verifier operation inputs
{
@@ -146,6 +159,54 @@ Return ONLY the search query text - no explanations.`,
type: 'short-input',
placeholder: 'Filter by domain',
condition: { field: 'operation', value: 'hunter_discover' },
mode: 'advanced',
},
{
id: 'headcount',
title: 'Headcount',
type: 'dropdown',
options: [
{ label: 'Any', id: '' },
{ label: '1-10', id: '1-10' },
{ label: '11-50', id: '11-50' },
{ label: '51-200', id: '51-200' },
{ label: '201-500', id: '201-500' },
{ label: '501-1000', id: '501-1000' },
{ label: '1001-5000', id: '1001-5000' },
{ label: '5001-10000', id: '5001-10000' },
{ label: '10001+', id: '10001+' },
],
value: () => '',
condition: { field: 'operation', value: 'hunter_discover' },
mode: 'advanced',
},
{
id: 'company_type',
title: 'Company Type',
type: 'dropdown',
options: [
{ label: 'Any', id: '' },
{ label: 'Educational', id: 'educational' },
{ label: 'Government Agency', id: 'government agency' },
{ label: 'Non Profit', id: 'non profit' },
{ label: 'Partnership', id: 'partnership' },
{ label: 'Privately Held', id: 'privately held' },
{ label: 'Public Company', id: 'public company' },
{ label: 'Self Employed', id: 'self employed' },
{ label: 'Self Owned', id: 'self owned' },
{ label: 'Sole Proprietorship', id: 'sole proprietorship' },
],
value: () => '',
condition: { field: 'operation', value: 'hunter_discover' },
mode: 'advanced',
},
{
id: 'technology',
title: 'Technology',
type: 'short-input',
placeholder: 'e.g., react, salesforce',
condition: { field: 'operation', value: 'hunter_discover' },
mode: 'advanced',
},
// Find Company operation inputs
@@ -172,6 +233,7 @@ Return ONLY the search query text - no explanations.`,
type: 'short-input',
placeholder: 'Enter company name',
condition: { field: 'operation', value: 'hunter_email_count' },
mode: 'advanced',
},
{
id: 'type',
@@ -184,6 +246,7 @@ Return ONLY the search query text - no explanations.`,
],
value: () => 'all',
condition: { field: 'operation', value: 'hunter_email_count' },
mode: 'advanced',
},
// API Key (common)
{
@@ -225,7 +288,14 @@ Return ONLY the search query text - no explanations.`,
},
params: (params) => {
const result: Record<string, unknown> = {}
if (params.limit) result.limit = Number(params.limit)
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null || value === '') continue
if (key === 'limit' || key === 'offset') {
result[key] = Number(value)
} else {
result[key] = value
}
}
return result
},
},
@@ -253,14 +323,88 @@ Return ONLY the search query text - no explanations.`,
technology: { type: 'string', description: 'Technology filter' },
},
outputs: {
results: { type: 'json', description: 'Search results' },
emails: { type: 'json', description: 'Email addresses found' },
// Domain Search
domain: { type: 'string', description: 'Domain name' },
organization: { type: 'string', description: 'Organization name (domain search)' },
pattern: { type: 'string', description: 'Email pattern (e.g., {first}.{last})' },
disposable: { type: 'boolean', description: 'Whether the domain is disposable' },
webmail: { type: 'boolean', description: 'Whether the domain is a webmail provider' },
accept_all: { type: 'boolean', description: 'Whether the server accepts all emails' },
linked_domains: { type: 'array', description: 'Linked domains' },
emails: {
type: 'array',
description:
'List of emails found for the domain (value, type, confidence, first_name, last_name, position, seniority, department, linkedin, twitter, phone_number, sources, verification)',
},
// Email Finder
email: { type: 'string', description: 'Found email address' },
score: { type: 'number', description: 'Confidence score' },
result: { type: 'string', description: 'Verification result' },
status: { type: 'string', description: 'Status message' },
total: { type: 'number', description: 'Total results count' },
score: { type: 'number', description: 'Confidence score (0-100)' },
first_name: { type: 'string', description: 'Person first name' },
last_name: { type: 'string', description: 'Person last name' },
position: { type: 'string', description: 'Job position' },
linkedin_url: { type: 'string', description: 'LinkedIn profile URL (email-finder, discover)' },
phone_number: { type: 'string', description: 'Phone number' },
company: { type: 'string', description: 'Company name (email-finder)' },
sources: {
type: 'array',
description:
'Source pages where the email was found (domain, uri, extracted_on, last_seen_on, still_on_page)',
},
verification: {
type: 'json',
description: 'Email verification information (date, status)',
},
// Email Verifier
result: {
type: 'string',
description: 'Deliverability result (deliverable, undeliverable, risky)',
},
status: {
type: 'string',
description: 'Verification status (valid, invalid, accept_all, webmail, disposable, unknown)',
},
regexp: { type: 'boolean', description: 'Email passes regex validation' },
gibberish: { type: 'boolean', description: 'Whether email looks auto-generated' },
mx_records: { type: 'boolean', description: 'MX records exist for the domain' },
smtp_server: { type: 'boolean', description: 'SMTP server reachable' },
smtp_check: { type: 'boolean', description: 'Email does not bounce' },
block: { type: 'boolean', description: 'Whether the domain blocks verification' },
// Discover
results: {
type: 'array',
description:
'Companies matching the search (domain, organization, personal_emails, generic_emails, total_emails)',
},
// Companies Find (flattened)
name: { type: 'string', description: 'Company name (companies-find, discover)' },
description: { type: 'string', description: 'Company description' },
industry: { type: 'string', description: 'Industry classification' },
sector: { type: 'string', description: 'Business sector' },
size: { type: 'string', description: 'Employee headcount range (e.g., "11-50")' },
founded_year: { type: 'number', description: 'Year founded' },
location: { type: 'string', description: 'Headquarters location (formatted)' },
country: { type: 'string', description: 'Country (full name)' },
country_code: { type: 'string', description: 'ISO 3166-1 alpha-2 country code' },
state: { type: 'string', description: 'State/province' },
city: { type: 'string', description: 'City' },
linkedin: { type: 'string', description: 'LinkedIn handle (companies-find)' },
twitter: { type: 'string', description: 'Twitter handle' },
facebook: { type: 'string', description: 'Facebook handle' },
logo: { type: 'string', description: 'Company logo URL' },
phone: { type: 'string', description: 'Company phone number' },
tech: { type: 'array', description: 'Technologies used by the company' },
// Email Count
total: { type: 'number', description: 'Total email count' },
personal_emails: { type: 'number', description: 'Personal emails count' },
generic_emails: { type: 'number', description: 'Generic emails count' },
department: {
type: 'json',
description:
'Email count by department (executive, it, finance, management, sales, legal, support, hr, marketing, communication, education, design, health, operations)',
},
seniority: {
type: 'json',
description: 'Email count by seniority level (junior, senior, executive)',
},
},
}
+42 -18
View File
@@ -1,5 +1,4 @@
import type { HunterEnrichmentParams, HunterEnrichmentResponse } from '@/tools/hunter/types'
import { COMPANY_OUTPUT } from '@/tools/hunter/types'
import type { ToolConfig } from '@/tools/types'
export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichmentResponse> = {
@@ -39,32 +38,57 @@ export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichm
transformResponse: async (response: Response) => {
const data = await response.json()
const c = data.data ?? {}
return {
success: true,
output: {
person: undefined,
company: data.data
? {
name: data.data.name || '',
domain: data.data.domain || '',
industry: data.data.industry || '',
size: data.data.size || '',
country: data.data.country || '',
linkedin: data.data.linkedin || '',
twitter: data.data.twitter || '',
}
: undefined,
name: c.name ?? '',
domain: c.domain ?? '',
description: c.description ?? '',
industry: c.category?.industry ?? '',
sector: c.category?.sector ?? '',
size:
c.metrics?.employeesRange ??
(c.metrics?.employees != null ? String(c.metrics.employees) : ''),
founded_year: c.foundedYear ?? null,
location: c.location ?? '',
country: c.geo?.country ?? '',
country_code: c.geo?.countryCode ?? '',
state: c.geo?.state ?? '',
city: c.geo?.city ?? '',
linkedin: c.linkedin?.handle ?? '',
twitter: c.twitter?.handle ?? '',
facebook: c.facebook?.handle ?? '',
logo: c.logo ?? '',
phone: c.phone ?? '',
tech: c.tech ?? [],
},
}
},
outputs: {
person: {
type: 'object',
description: 'Person information (undefined for companies_find tool)',
optional: true,
name: { type: 'string', description: 'Company name' },
domain: { type: 'string', description: 'Company domain' },
description: { type: 'string', description: 'Company description' },
industry: { type: 'string', description: 'Industry classification' },
sector: { type: 'string', description: 'Business sector' },
size: { type: 'string', description: 'Employee headcount range (e.g., "11-50")' },
founded_year: { type: 'number', description: 'Year founded', optional: true },
location: { type: 'string', description: 'Headquarters location (formatted)' },
country: { type: 'string', description: 'Country (full name)' },
country_code: { type: 'string', description: 'ISO 3166-1 alpha-2 country code' },
state: { type: 'string', description: 'State/province' },
city: { type: 'string', description: 'City' },
linkedin: { type: 'string', description: 'LinkedIn handle (e.g., company/hunterio)' },
twitter: { type: 'string', description: 'Twitter handle' },
facebook: { type: 'string', description: 'Facebook handle' },
logo: { type: 'string', description: 'Company logo URL' },
phone: { type: 'string', description: 'Company phone number' },
tech: {
type: 'array',
description: 'Technologies used by the company',
items: { type: 'string', description: 'Technology name' },
},
company: COMPANY_OUTPUT,
},
}
+15 -12
View File
@@ -71,13 +71,12 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, any> = {}
const body: Record<string, unknown> = {}
// Add optional parameters if provided
if (params.query) body.query = params.query
if (params.domain) body.organization = { domain: [params.domain] }
if (params.headcount) body.headcount = params.headcount
if (params.company_type) body.company_type = params.company_type
if (params.headcount) body.headcount = [params.headcount]
if (params.company_type) body.company_type = { include: [params.company_type] }
if (params.technology) {
body.technology = {
include: [params.technology],
@@ -90,18 +89,22 @@ export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverRespon
transformResponse: async (response: Response) => {
const data = await response.json()
const companies: Array<{
domain?: string
organization?: string
emails_count?: { personal?: number; generic?: number; total?: number }
}> = Array.isArray(data?.data) ? data.data : []
return {
success: true,
output: {
results:
data.data?.map((company: any) => ({
domain: company.domain || '',
name: company.organization || '',
headcount: company.headcount,
technologies: company.technologies || [],
email_count: company.emails_count?.total || 0,
})) || [],
results: companies.map((c) => ({
domain: c.domain ?? '',
organization: c.organization ?? '',
personal_emails: c.emails_count?.personal ?? 0,
generic_emails: c.emails_count?.generic ?? 0,
total_emails: c.emails_count?.total ?? 0,
})),
},
}
},
+32 -89
View File
@@ -1,4 +1,8 @@
import type { HunterDomainSearchParams, HunterDomainSearchResponse } from '@/tools/hunter/types'
import type {
HunterDomainSearchParams,
HunterDomainSearchResponse,
HunterEmail,
} from '@/tools/hunter/types'
import { EMAILS_OUTPUT } from '@/tools/hunter/types'
import type { ToolConfig } from '@/tools/types'
@@ -77,45 +81,35 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
transformResponse: async (response: Response) => {
const data = await response.json()
const d = data.data ?? {}
return {
success: true,
output: {
domain: data.data?.domain || '',
disposable: data.data?.disposable || false,
webmail: data.data?.webmail || false,
accept_all: data.data?.accept_all || false,
pattern: data.data?.pattern || '',
organization: data.data?.organization || '',
description: data.data?.description || '',
industry: data.data?.industry || '',
twitter: data.data?.twitter || '',
facebook: data.data?.facebook || '',
linkedin: data.data?.linkedin || '',
instagram: data.data?.instagram || '',
youtube: data.data?.youtube || '',
technologies: data.data?.technologies || [],
country: data.data?.country || '',
state: data.data?.state || '',
city: data.data?.city || '',
postal_code: data.data?.postal_code || '',
street: data.data?.street || '',
domain: d.domain ?? '',
disposable: d.disposable ?? false,
webmail: d.webmail ?? false,
accept_all: d.accept_all ?? false,
pattern: d.pattern ?? '',
organization: d.organization ?? '',
linked_domains: d.linked_domains ?? [],
emails:
data.data?.emails?.map((email: any) => ({
value: email.value || '',
type: email.type || '',
confidence: email.confidence || 0,
sources: email.sources || [],
first_name: email.first_name || '',
last_name: email.last_name || '',
position: email.position || '',
seniority: email.seniority || '',
department: email.department || '',
linkedin: email.linkedin || '',
twitter: email.twitter || '',
phone_number: email.phone_number || '',
verification: email.verification || {},
})) || [],
d.emails?.map((email: Partial<HunterEmail>) => ({
value: email.value ?? '',
type: email.type ?? '',
confidence: email.confidence ?? 0,
sources: email.sources ?? [],
first_name: email.first_name ?? null,
last_name: email.last_name ?? null,
position: email.position ?? null,
position_raw: email.position_raw ?? null,
seniority: email.seniority ?? null,
department: email.department ?? null,
linkedin: email.linkedin ?? null,
twitter: email.twitter ?? null,
phone_number: email.phone_number ?? null,
verification: email.verification ?? { date: null, status: 'unknown' },
})) ?? [],
},
}
},
@@ -145,61 +139,10 @@ export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomain
type: 'string',
description: 'The organization/company name',
},
description: {
type: 'string',
description: 'Description of the organization',
},
industry: {
type: 'string',
description: 'Industry classification of the organization',
},
twitter: {
type: 'string',
description: 'Twitter handle of the organization',
},
facebook: {
type: 'string',
description: 'Facebook page URL of the organization',
},
linkedin: {
type: 'string',
description: 'LinkedIn company page URL',
},
instagram: {
type: 'string',
description: 'Instagram profile of the organization',
},
youtube: {
type: 'string',
description: 'YouTube channel of the organization',
},
technologies: {
linked_domains: {
type: 'array',
description: 'Technologies used by the organization',
items: {
type: 'string',
description: 'Technology name',
},
},
country: {
type: 'string',
description: 'Country where the organization is headquartered',
},
state: {
type: 'string',
description: 'State/province where the organization is located',
},
city: {
type: 'string',
description: 'City where the organization is located',
},
postal_code: {
type: 'string',
description: 'Postal code of the organization',
},
street: {
type: 'string',
description: 'Street address of the organization',
description: 'Other domains linked to the organization',
items: { type: 'string', description: 'Domain name' },
},
emails: EMAILS_OUTPUT,
},
+27 -8
View File
@@ -62,27 +62,46 @@ export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFin
transformResponse: async (response: Response) => {
const data = await response.json()
const d = data.data ?? {}
return {
success: true,
output: {
email: data.data?.email || '',
score: data.data?.score || 0,
sources: data.data?.sources || [],
verification: data.data?.verification || {},
first_name: d.first_name ?? '',
last_name: d.last_name ?? '',
email: d.email ?? '',
score: d.score ?? 0,
domain: d.domain ?? '',
accept_all: d.accept_all ?? false,
position: d.position ?? null,
twitter: d.twitter ?? null,
linkedin_url: d.linkedin_url ?? null,
phone_number: d.phone_number ?? null,
company: d.company ?? null,
sources: d.sources ?? [],
verification: d.verification ?? { date: null, status: 'unknown' },
},
}
},
outputs: {
email: {
type: 'string',
description: 'The found email address',
},
first_name: { type: 'string', description: "Person's first name" },
last_name: { type: 'string', description: "Person's last name" },
email: { type: 'string', description: 'The found email address' },
score: {
type: 'number',
description: 'Confidence score (0-100) for the found email address',
},
domain: { type: 'string', description: 'Domain that was searched' },
accept_all: {
type: 'boolean',
description: 'Whether the server accepts all email addresses (may cause false positives)',
},
position: { type: 'string', description: 'Job title/position', optional: true },
twitter: { type: 'string', description: 'Twitter handle', optional: true },
linkedin_url: { type: 'string', description: 'LinkedIn profile URL', optional: true },
phone_number: { type: 'string', description: 'Phone number', optional: true },
company: { type: 'string', description: 'Company name', optional: true },
sources: SOURCES_OUTPUT,
verification: VERIFICATION_OUTPUT,
},
+269
View File
@@ -0,0 +1,269 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { companiesFindTool } from '@/tools/hunter/companies_find'
import { discoverTool } from '@/tools/hunter/discover'
import { domainSearchTool } from '@/tools/hunter/domain_search'
import { emailFinderTool } from '@/tools/hunter/email_finder'
const respond = (body: unknown) => new Response(JSON.stringify(body))
describe('hunter domain_search', () => {
const transform = domainSearchTool.transformResponse!
it('maps the documented response shape', async () => {
const result = await transform(
respond({
data: {
domain: 'stripe.com',
disposable: false,
webmail: false,
accept_all: true,
pattern: '{first}',
organization: 'Stripe',
linked_domains: ['stripe.io'],
emails: [
{
value: 'patrick@stripe.com',
type: 'personal',
confidence: 92,
first_name: 'Patrick',
last_name: 'Collison',
position: 'CEO',
seniority: 'executive',
department: 'executive',
linkedin: null,
twitter: 'patrickc',
phone_number: null,
sources: [],
verification: { date: '2024-01-01', status: 'valid' },
},
],
},
})
)
expect(result.success).toBe(true)
expect(result.output.domain).toBe('stripe.com')
expect(result.output.linked_domains).toEqual(['stripe.io'])
expect(result.output.emails).toHaveLength(1)
expect(result.output.emails[0]).toMatchObject({
value: 'patrick@stripe.com',
first_name: 'Patrick',
twitter: 'patrickc',
verification: { status: 'valid' },
})
})
it('returns safe defaults when fields are missing', async () => {
const result = await transform(respond({ data: null }))
expect(result.output).toMatchObject({
domain: '',
disposable: false,
webmail: false,
accept_all: false,
pattern: '',
organization: '',
linked_domains: [],
emails: [],
})
})
it('nullifies missing optional email fields', async () => {
const result = await transform(
respond({
data: {
emails: [{ value: 'a@b.com', type: 'generic', confidence: 50 }],
},
})
)
expect(result.output.emails[0]).toMatchObject({
first_name: null,
last_name: null,
position: null,
linkedin: null,
verification: { status: 'unknown' },
})
})
})
describe('hunter email_finder', () => {
const transform = emailFinderTool.transformResponse!
it('extracts the documented finder fields', async () => {
const result = await transform(
respond({
data: {
first_name: 'Alex',
last_name: 'Smith',
email: 'alex@acme.com',
score: 85,
domain: 'acme.com',
accept_all: false,
position: 'Engineer',
twitter: null,
linkedin_url: 'https://linkedin.com/in/alex',
phone_number: null,
company: 'Acme',
sources: [],
verification: { date: null, status: 'valid' },
},
})
)
expect(result.output).toMatchObject({
first_name: 'Alex',
email: 'alex@acme.com',
score: 85,
linkedin_url: 'https://linkedin.com/in/alex',
company: 'Acme',
verification: { status: 'valid' },
})
})
it('falls back to safe defaults', async () => {
const result = await transform(respond({ data: {} }))
expect(result.output).toMatchObject({
email: '',
score: 0,
accept_all: false,
sources: [],
verification: { date: null, status: 'unknown' },
})
})
})
describe('hunter discover', () => {
const transform = discoverTool.transformResponse!
it('maps documented data array shape', async () => {
const result = await transform(
respond({
data: [
{
domain: 'hunter.io',
organization: 'Hunter',
emails_count: { personal: 23, generic: 5, total: 28 },
},
],
})
)
expect(result.output.results).toEqual([
{
domain: 'hunter.io',
organization: 'Hunter',
personal_emails: 23,
generic_emails: 5,
total_emails: 28,
},
])
})
it('returns empty array when data is missing', async () => {
const result = await transform(respond({}))
expect(result.output.results).toEqual([])
})
it('falls back to zero counts when emails_count is missing', async () => {
const result = await transform(
respond({ data: [{ domain: 'acme.com', organization: 'Acme' }] })
)
expect(result.output.results[0]).toEqual({
domain: 'acme.com',
organization: 'Acme',
personal_emails: 0,
generic_emails: 0,
total_emails: 0,
})
})
it('throws when no search params provided', () => {
const buildUrl = discoverTool.request.url as (p: Record<string, unknown>) => string
expect(() => buildUrl({ apiKey: 'k' })).toThrow(/At least one search parameter/)
})
it('builds body per docs (headcount as plain array, technology wrapped)', () => {
const buildBody = discoverTool.request.body as (
p: Record<string, unknown>
) => Record<string, unknown>
const body = buildBody({ apiKey: 'k', headcount: '11-50', technology: 'react' })
expect(body).toEqual({
headcount: ['11-50'],
technology: { include: ['react'] },
})
})
})
describe('hunter companies_find', () => {
const transform = companiesFindTool.transformResponse!
it('flattens nested company fields', async () => {
const result = await transform(
respond({
data: {
name: 'Stripe',
domain: 'stripe.com',
description: 'Payments',
category: { industry: 'Fintech', sector: 'Software' },
metrics: { employees: '1000+' },
foundedYear: 2010,
location: 'San Francisco, CA',
geo: { country: 'United States', countryCode: 'US', state: 'CA', city: 'SF' },
linkedin: { handle: 'company/stripe' },
twitter: { handle: 'stripe' },
facebook: { handle: 'stripe' },
logo: 'https://logo.png',
phone: '+1-555',
tech: ['react', 'node'],
},
})
)
expect(result.output).toEqual({
name: 'Stripe',
domain: 'stripe.com',
description: 'Payments',
industry: 'Fintech',
sector: 'Software',
size: '1000+',
founded_year: 2010,
location: 'San Francisco, CA',
country: 'United States',
country_code: 'US',
state: 'CA',
city: 'SF',
linkedin: 'company/stripe',
twitter: 'stripe',
facebook: 'stripe',
logo: 'https://logo.png',
phone: '+1-555',
tech: ['react', 'node'],
})
})
it('prefers employeesRange and coerces numeric employees', async () => {
const rangeResult = await transform(
respond({ data: { metrics: { employees: 5432, employeesRange: '1001-5000' } } })
)
expect(rangeResult.output.size).toBe('1001-5000')
const numericResult = await transform(respond({ data: { metrics: { employees: 5432 } } }))
expect(numericResult.output.size).toBe('5432')
})
it('survives missing nested objects', async () => {
const result = await transform(respond({ data: {} }))
expect(result.output).toMatchObject({
name: '',
industry: '',
sector: '',
size: '',
country: '',
linkedin: '',
tech: [],
founded_year: null,
})
})
})
+61 -101
View File
@@ -71,14 +71,20 @@ export const EMAIL_OUTPUT_PROPERTIES = {
type: 'number',
description: 'Probability score (0-100) that the email is correct',
},
first_name: { type: 'string', description: "Person's first name" },
last_name: { type: 'string', description: "Person's last name" },
position: { type: 'string', description: 'Job title/position' },
seniority: { type: 'string', description: 'Seniority level (junior, senior, executive)' },
first_name: { type: 'string', description: "Person's first name", optional: true },
last_name: { type: 'string', description: "Person's last name", optional: true },
position: { type: 'string', description: 'Job title/position', optional: true },
position_raw: { type: 'string', description: 'Raw job title as found', optional: true },
seniority: {
type: 'string',
description: 'Seniority level (junior, senior, executive)',
optional: true,
},
department: {
type: 'string',
description:
'Department (executive, it, finance, management, sales, legal, support, hr, marketing, communication)',
'Department (executive, it, finance, management, sales, legal, support, hr, marketing, communication, education, design, health, operations)',
optional: true,
},
linkedin: { type: 'string', description: 'LinkedIn profile URL', optional: true },
twitter: { type: 'string', description: 'Twitter handle', optional: true },
@@ -147,36 +153,16 @@ export const SENIORITY_OUTPUT: OutputProperty = {
}
/**
* Output definition for emails_count object in discover results
*/
export const EMAILS_COUNT_OUTPUT_PROPERTIES = {
personal: { type: 'number', description: 'Number of personal email addresses' },
generic: { type: 'number', description: 'Number of generic/role-based email addresses' },
total: { type: 'number', description: 'Total number of email addresses' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete emails_count object output definition
*/
export const EMAILS_COUNT_OUTPUT: OutputProperty = {
type: 'object',
description: 'Email count breakdown',
properties: EMAILS_COUNT_OUTPUT_PROPERTIES,
}
/**
* Output definition for discover result company objects
* Output definition for discover result company objects.
* Hunter Discover returns minimal info per company — use Domain Search or
* Company Enrichment for richer data on a specific result.
*/
export const DISCOVER_RESULT_OUTPUT_PROPERTIES = {
domain: { type: 'string', description: 'Company domain' },
name: { type: 'string', description: 'Company/organization name' },
headcount: { type: 'number', description: 'Company size/headcount', optional: true },
technologies: {
type: 'array',
description: 'Technologies used by the company',
items: { type: 'string', description: 'Technology name' },
},
email_count: { type: 'number', description: 'Total number of email addresses found' },
organization: { type: 'string', description: 'Organization name' },
personal_emails: { type: 'number', description: 'Count of personal emails' },
generic_emails: { type: 'number', description: 'Count of generic (role-based) emails' },
total_emails: { type: 'number', description: 'Total emails found for the company' },
} as const satisfies Record<string, OutputProperty>
/**
@@ -191,28 +177,6 @@ export const DISCOVER_RESULTS_OUTPUT: OutputProperty = {
},
}
/**
* Output definition for company enrichment objects
*/
export const COMPANY_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Company name' },
domain: { type: 'string', description: 'Company domain' },
industry: { type: 'string', description: 'Industry classification' },
size: { type: 'string', description: 'Company size/headcount range' },
country: { type: 'string', description: 'Country where the company is located' },
linkedin: { type: 'string', description: 'LinkedIn company page URL', optional: true },
twitter: { type: 'string', description: 'Twitter handle', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* Complete company object output definition
*/
export const COMPANY_OUTPUT: OutputProperty = {
type: 'object',
description: 'Company information',
properties: COMPANY_OUTPUT_PROPERTIES,
}
// Common parameters for all Hunter.io tools
export interface HunterBaseParams {
apiKey: string
@@ -229,10 +193,10 @@ export interface HunterDiscoverParams extends HunterBaseParams {
export interface HunterDiscoverResult {
domain: string
name: string
headcount?: number
technologies?: string[]
email_count?: number
organization: string
personal_emails: number
generic_emails: number
total_emails: number
}
export interface HunterDiscoverResponse extends ToolResponse {
@@ -262,16 +226,17 @@ export interface HunterEmail {
last_seen_on: string
still_on_page: boolean
}>
first_name: string
last_name: string
position: string
seniority: string
department: string
linkedin: string
twitter: string
phone_number: string
first_name: string | null
last_name: string | null
position: string | null
position_raw: string | null
seniority: string | null
department: string | null
linkedin: string | null
twitter: string | null
phone_number: string | null
verification: {
date: string
date: string | null
status: string
}
}
@@ -284,19 +249,7 @@ export interface HunterDomainSearchResponse extends ToolResponse {
accept_all: boolean
pattern: string
organization: string
description: string
industry: string
twitter: string
facebook: string
linkedin: string
instagram: string
youtube: string
technologies: string[]
country: string
state: string
city: string
postal_code: string
street: string
linked_domains: string[]
emails: HunterEmail[]
}
}
@@ -311,8 +264,17 @@ export interface HunterEmailFinderParams extends HunterBaseParams {
export interface HunterEmailFinderResponse extends ToolResponse {
output: {
first_name: string
last_name: string
email: string
score: number
domain: string
accept_all: boolean
position: string | null
twitter: string | null
linkedin_url: string | null
phone_number: string | null
company: string | null
sources: Array<{
domain: string
uri: string
@@ -321,7 +283,7 @@ export interface HunterEmailFinderResponse extends ToolResponse {
still_on_page: boolean
}>
verification: {
date: string
date: string | null
status: string
}
}
@@ -366,26 +328,24 @@ export interface HunterEnrichmentParams extends HunterBaseParams {
export interface HunterEnrichmentResponse extends ToolResponse {
output: {
person?: {
first_name: string
last_name: string
email: string
position: string
seniority: string
department: string
linkedin: string
twitter: string
phone_number: string
}
company?: {
name: string
domain: string
industry: string
size: string
country: string
linkedin: string
twitter: string
}
name: string
domain: string
description: string
industry: string
sector: string
size: string
founded_year: number | null
location: string
country: string
country_code: string
state: string
city: string
linkedin: string
twitter: string
facebook: string
logo: string
phone: string
tech: string[]
}
}