chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import type { HunterEnrichmentParams, HunterEnrichmentResponse } from '@/tools/hunter/types'
|
||||
import { HUNTER_API_KEY_PREFIX } from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const companiesFindTool: ToolConfig<HunterEnrichmentParams, HunterEnrichmentResponse> = {
|
||||
id: 'hunter_companies_find',
|
||||
name: 'Hunter Companies Find',
|
||||
description: 'Enriches company data using domain name.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
// Companies Find (enrichment) is free on Hunter — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Domain to find company data for (e.g., "stripe.com", "company.io")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/companies/find')
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
url.searchParams.append('domain', params.domain || '')
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const c = data.data ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
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: {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { HunterDiscoverParams, HunterDiscoverResponse } from '@/tools/hunter/types'
|
||||
import { DISCOVER_RESULTS_OUTPUT, HUNTER_API_KEY_PREFIX } from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const discoverTool: ToolConfig<HunterDiscoverParams, HunterDiscoverResponse> = {
|
||||
id: 'hunter_discover',
|
||||
name: 'Hunter Discover',
|
||||
description: 'Returns companies matching a set of criteria using Hunter.io AI-powered search.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
// Discover is free on Hunter — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 30,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Natural language search query for companies',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company domain name to filter by (e.g., "stripe.com", "company.io")',
|
||||
},
|
||||
headcount: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company size filter (e.g., "1-10", "11-50")',
|
||||
},
|
||||
company_type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Type of organization',
|
||||
},
|
||||
technology: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Technology used by companies',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Validate that at least one search parameter is provided
|
||||
if (
|
||||
!params.query &&
|
||||
!params.domain &&
|
||||
!params.headcount &&
|
||||
!params.company_type &&
|
||||
!params.technology
|
||||
) {
|
||||
throw new Error(
|
||||
'At least one search parameter (query, domain, headcount, company_type, or technology) must be provided'
|
||||
)
|
||||
}
|
||||
|
||||
const url = new URL('https://api.hunter.io/v2/discover')
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
|
||||
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 = { include: [params.company_type] }
|
||||
if (params.technology) {
|
||||
body.technology = {
|
||||
include: [params.technology],
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
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: 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,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
results: DISCOVER_RESULTS_OUTPUT,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type {
|
||||
HunterDomainSearchParams,
|
||||
HunterDomainSearchResponse,
|
||||
HunterEmail,
|
||||
} from '@/tools/hunter/types'
|
||||
import {
|
||||
EMAILS_OUTPUT,
|
||||
HUNTER_API_KEY_PREFIX,
|
||||
HUNTER_SEARCH_CREDIT_USD,
|
||||
} from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const domainSearchTool: ToolConfig<HunterDomainSearchParams, HunterDomainSearchResponse> = {
|
||||
id: 'hunter_domain_search',
|
||||
name: 'Hunter Domain Search',
|
||||
description: 'Returns all the email addresses found using one given domain name, with sources.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
getCost: (_params, output) => {
|
||||
const emails = output.emails
|
||||
if (!Array.isArray(emails)) {
|
||||
throw new Error('Hunter domain search response missing emails, cannot determine cost')
|
||||
}
|
||||
// Hunter counts one search credit only when a call returns at least one result.
|
||||
const cost = emails.length > 0 ? HUNTER_SEARCH_CREDIT_USD : 0
|
||||
return { cost, metadata: { credits: cost > 0 ? 1 : 0, emails: emails.length } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Domain name to search for email addresses (e.g., "stripe.com", "company.io")',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum email addresses to return (e.g., 10, 25, 50). Default: 10',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of email addresses to skip for pagination (e.g., 0, 10, 20)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter for personal or generic emails (e.g., "personal", "generic", "all")',
|
||||
},
|
||||
seniority: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by seniority level (e.g., "junior", "senior", "executive")',
|
||||
},
|
||||
department: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter by specific department (e.g., "sales", "marketing", "engineering", "hr")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/domain-search')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.limit) url.searchParams.append('limit', Number(params.limit).toString())
|
||||
if (params.offset) url.searchParams.append('offset', Number(params.offset).toString())
|
||||
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
|
||||
if (params.seniority && params.seniority !== 'all')
|
||||
url.searchParams.append('seniority', params.seniority)
|
||||
if (params.department) url.searchParams.append('department', params.department)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const d = data.data ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
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:
|
||||
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' },
|
||||
})) ?? [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The searched domain name',
|
||||
},
|
||||
disposable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the domain is a disposable email service',
|
||||
},
|
||||
webmail: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the domain is a webmail provider (e.g., Gmail)',
|
||||
},
|
||||
accept_all: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the server accepts all email addresses (may cause false positives)',
|
||||
},
|
||||
pattern: {
|
||||
type: 'string',
|
||||
description: 'The email pattern used by the organization (e.g., {first}, {first}.{last})',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
description: 'The organization/company name',
|
||||
},
|
||||
linked_domains: {
|
||||
type: 'array',
|
||||
description: 'Other domains linked to the organization',
|
||||
items: { type: 'string', description: 'Domain name' },
|
||||
},
|
||||
emails: EMAILS_OUTPUT,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { HunterEmailCountParams, HunterEmailCountResponse } from '@/tools/hunter/types'
|
||||
import { DEPARTMENT_OUTPUT, HUNTER_API_KEY_PREFIX, SENIORITY_OUTPUT } from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const emailCountTool: ToolConfig<HunterEmailCountParams, HunterEmailCountResponse> = {
|
||||
id: 'hunter_email_count',
|
||||
name: 'Hunter Email Count',
|
||||
description: 'Returns the total number of email addresses found for a domain or company.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
// Email Count is free on Hunter — no credits consumed.
|
||||
pricing: { type: 'per_request', cost: 0 },
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Domain to count emails for (e.g., "stripe.com"). Required if company not provided',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Company name to count emails for (e.g., "Stripe", "Acme Inc"). Required if domain not provided',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter for personal or generic emails only (e.g., "personal", "generic", "all")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
if (!params.domain && !params.company) {
|
||||
throw new Error('Either domain or company must be provided')
|
||||
}
|
||||
|
||||
const url = new URL('https://api.hunter.io/v2/email-count')
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.domain) url.searchParams.append('domain', params.domain)
|
||||
if (params.company) url.searchParams.append('company', params.company)
|
||||
if (params.type && params.type !== 'all') url.searchParams.append('type', params.type)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
total: data.data?.total || 0,
|
||||
personal_emails: data.data?.personal_emails || 0,
|
||||
generic_emails: data.data?.generic_emails || 0,
|
||||
department: data.data?.department || {
|
||||
executive: 0,
|
||||
it: 0,
|
||||
finance: 0,
|
||||
management: 0,
|
||||
sales: 0,
|
||||
legal: 0,
|
||||
support: 0,
|
||||
hr: 0,
|
||||
marketing: 0,
|
||||
communication: 0,
|
||||
education: 0,
|
||||
design: 0,
|
||||
health: 0,
|
||||
operations: 0,
|
||||
},
|
||||
seniority: data.data?.seniority || {
|
||||
junior: 0,
|
||||
senior: 0,
|
||||
executive: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
total: {
|
||||
type: 'number',
|
||||
description: 'Total number of email addresses found',
|
||||
},
|
||||
personal_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of personal email addresses (individual employees)',
|
||||
},
|
||||
generic_emails: {
|
||||
type: 'number',
|
||||
description: 'Number of generic/role-based email addresses (e.g., contact@, info@)',
|
||||
},
|
||||
department: DEPARTMENT_OUTPUT,
|
||||
seniority: SENIORITY_OUTPUT,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { HunterEmailFinderParams, HunterEmailFinderResponse } from '@/tools/hunter/types'
|
||||
import {
|
||||
HUNTER_API_KEY_PREFIX,
|
||||
HUNTER_SEARCH_CREDIT_USD,
|
||||
SOURCES_OUTPUT,
|
||||
VERIFICATION_OUTPUT,
|
||||
} from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const emailFinderTool: ToolConfig<HunterEmailFinderParams, HunterEmailFinderResponse> = {
|
||||
id: 'hunter_email_finder',
|
||||
name: 'Hunter Email Finder',
|
||||
description:
|
||||
'Finds the most likely email address for a person given their name and company domain.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
getCost: (_params, output) => {
|
||||
// Hunter counts one search credit only when an email is found.
|
||||
const found = typeof output.email === 'string' && output.email.length > 0
|
||||
const cost = found ? HUNTER_SEARCH_CREDIT_USD : 0
|
||||
return { cost, metadata: { credits: found ? 1 : 0 } }
|
||||
},
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company domain name (e.g., "stripe.com", "company.io")',
|
||||
},
|
||||
first_name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Person\'s first name (e.g., "John", "Sarah")',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Person\'s last name (e.g., "Smith", "Johnson")',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Company name (e.g., "Stripe", "Acme Inc")',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-finder')
|
||||
url.searchParams.append('domain', params.domain)
|
||||
url.searchParams.append('first_name', params.first_name)
|
||||
url.searchParams.append('last_name', params.last_name)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
if (params.company) url.searchParams.append('company', params.company)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
const d = data.data ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
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: {
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { HunterEmailVerifierParams, HunterEmailVerifierResponse } from '@/tools/hunter/types'
|
||||
import {
|
||||
HUNTER_API_KEY_PREFIX,
|
||||
HUNTER_VERIFICATION_CREDIT_USD,
|
||||
SOURCES_OUTPUT,
|
||||
} from '@/tools/hunter/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const emailVerifierTool: ToolConfig<HunterEmailVerifierParams, HunterEmailVerifierResponse> =
|
||||
{
|
||||
id: 'hunter_email_verifier',
|
||||
name: 'Hunter Email Verifier',
|
||||
description:
|
||||
'Verifies the deliverability of an email address and provides detailed verification status.',
|
||||
version: '1.0.0',
|
||||
|
||||
hosting: {
|
||||
envKeyPrefix: HUNTER_API_KEY_PREFIX,
|
||||
apiKeyParam: 'apiKey',
|
||||
byokProviderId: 'hunter',
|
||||
pricing: {
|
||||
type: 'custom',
|
||||
// The verifier always consumes one verification (0.5 credit), regardless of result.
|
||||
getCost: () => ({
|
||||
cost: HUNTER_VERIFICATION_CREDIT_USD,
|
||||
metadata: { verifications: 1 },
|
||||
}),
|
||||
},
|
||||
rateLimit: {
|
||||
mode: 'per_request',
|
||||
requestsPerMinute: 60,
|
||||
},
|
||||
},
|
||||
|
||||
params: {
|
||||
email: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The email address to verify',
|
||||
},
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Hunter.io API Key',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://api.hunter.io/v2/email-verifier')
|
||||
url.searchParams.append('email', params.email)
|
||||
url.searchParams.append('api_key', params.apiKey)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
result: data.data?.result || 'unknown',
|
||||
score: data.data?.score || 0,
|
||||
email: data.data?.email || '',
|
||||
regexp: data.data?.regexp || false,
|
||||
gibberish: data.data?.gibberish || false,
|
||||
disposable: data.data?.disposable || false,
|
||||
webmail: data.data?.webmail || false,
|
||||
mx_records: data.data?.mx_records || false,
|
||||
smtp_server: data.data?.smtp_server || false,
|
||||
smtp_check: data.data?.smtp_check || false,
|
||||
accept_all: data.data?.accept_all || false,
|
||||
block: data.data?.block || false,
|
||||
status: data.data?.status || 'unknown',
|
||||
sources: data.data?.sources || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'string',
|
||||
description: 'Deliverability result: deliverable, undeliverable, or risky',
|
||||
},
|
||||
score: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Deliverability score (0-100). Webmail and disposable emails receive an arbitrary score of 50.',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'The verified email address',
|
||||
},
|
||||
regexp: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the email passes regular expression validation',
|
||||
},
|
||||
gibberish: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the email appears to be auto-generated (e.g., e65rc109q@company.com)',
|
||||
},
|
||||
disposable: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the email is from a disposable email service',
|
||||
},
|
||||
webmail: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the email is from a webmail provider (e.g., Gmail)',
|
||||
},
|
||||
mx_records: {
|
||||
type: 'boolean',
|
||||
description: 'Whether MX records exist for the domain',
|
||||
},
|
||||
smtp_server: {
|
||||
type: 'boolean',
|
||||
description: 'Whether connection to the SMTP server was successful',
|
||||
},
|
||||
smtp_check: {
|
||||
type: 'boolean',
|
||||
description: "Whether the email address doesn't bounce",
|
||||
},
|
||||
accept_all: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the server accepts all email addresses (may cause false positives)',
|
||||
},
|
||||
block: {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Whether the domain is blocking verification (validity could not be determined)',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Verification status: valid, invalid, accept_all, webmail, disposable, unknown, or blocked',
|
||||
},
|
||||
sources: SOURCES_OUTPUT,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { companiesFindTool } from '@/tools/hunter/companies_find'
|
||||
import { discoverTool } from '@/tools/hunter/discover'
|
||||
import { domainSearchTool } from '@/tools/hunter/domain_search'
|
||||
import { emailCountTool } from '@/tools/hunter/email_count'
|
||||
import { emailFinderTool } from '@/tools/hunter/email_finder'
|
||||
import { emailVerifierTool } from '@/tools/hunter/email_verifier'
|
||||
|
||||
export const hunterDiscoverTool = discoverTool
|
||||
export const hunterDomainSearchTool = domainSearchTool
|
||||
export const hunterEmailFinderTool = emailFinderTool
|
||||
export const hunterEmailVerifierTool = emailVerifierTool
|
||||
export const hunterCompaniesFindTool = companiesFindTool
|
||||
export const hunterEmailCountTool = emailCountTool
|
||||
@@ -0,0 +1,413 @@
|
||||
// Common types for Hunter.io tools
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Env var prefix for Sim-hosted Hunter.io keys.
|
||||
* Resolved at runtime as `HUNTER_API_KEY_COUNT` + `HUNTER_API_KEY_1..N`.
|
||||
*/
|
||||
export const HUNTER_API_KEY_PREFIX = 'HUNTER_API_KEY'
|
||||
|
||||
/**
|
||||
* Dollar cost per Hunter.io credit when billing hosted-key usage.
|
||||
*
|
||||
* Hunter does not report per-call credit consumption in its responses, so cost is
|
||||
* derived from the response: a "search" credit is consumed only when a call returns
|
||||
* at least one result; a verification consumes half a credit.
|
||||
*
|
||||
* Estimate based on the Growth plan ($149/mo for 10,000 credits ≈ $0.015/credit) —
|
||||
* see https://hunter.io/pricing. Tune if Sim's plan rate changes.
|
||||
*/
|
||||
export const HUNTER_SEARCH_CREDIT_USD = 0.015
|
||||
export const HUNTER_VERIFICATION_CREDIT_USD = 0.0075
|
||||
|
||||
/**
|
||||
* Shared output property definitions for Hunter.io API responses.
|
||||
* These are reusable across all Hunter tools to ensure consistency.
|
||||
* Based on Hunter.io API v2 documentation: https://hunter.io/api-documentation/v2
|
||||
*/
|
||||
|
||||
/**
|
||||
* Output definition for source objects where emails were found
|
||||
*/
|
||||
export const SOURCE_OUTPUT_PROPERTIES = {
|
||||
domain: { type: 'string', description: 'Domain where the email was found' },
|
||||
uri: { type: 'string', description: 'Full URL of the source page' },
|
||||
extracted_on: {
|
||||
type: 'string',
|
||||
description: 'Date when the email was first extracted (YYYY-MM-DD)',
|
||||
},
|
||||
last_seen_on: { type: 'string', description: 'Date when the email was last seen (YYYY-MM-DD)' },
|
||||
still_on_page: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the email is still present on the source page',
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete sources array output definition
|
||||
*/
|
||||
export const SOURCES_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'List of sources where the email was found (limited to 20)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: SOURCE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Output definition for verification objects
|
||||
*/
|
||||
export const VERIFICATION_OUTPUT_PROPERTIES = {
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Date when the email was verified (YYYY-MM-DD)',
|
||||
optional: true,
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Verification status (valid, invalid, accept_all, webmail, disposable, unknown)',
|
||||
optional: true,
|
||||
},
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete verification object output definition
|
||||
*/
|
||||
export const VERIFICATION_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Email verification information',
|
||||
properties: VERIFICATION_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Output definition for email objects in domain search responses
|
||||
*/
|
||||
export const EMAIL_OUTPUT_PROPERTIES = {
|
||||
value: { type: 'string', description: 'The email address' },
|
||||
type: { type: 'string', description: 'Email type: personal or generic (role-based)' },
|
||||
confidence: {
|
||||
type: 'number',
|
||||
description: 'Probability score (0-100) that the email is correct',
|
||||
},
|
||||
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, education, design, health, operations)',
|
||||
optional: true,
|
||||
},
|
||||
linkedin: { type: 'string', description: 'LinkedIn profile URL', optional: true },
|
||||
twitter: { type: 'string', description: 'Twitter handle', optional: true },
|
||||
phone_number: { type: 'string', description: 'Phone number', optional: true },
|
||||
sources: SOURCES_OUTPUT,
|
||||
verification: VERIFICATION_OUTPUT,
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete emails array output definition for domain search
|
||||
*/
|
||||
export const EMAILS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'List of email addresses found for the domain (up to 100 per request)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: EMAIL_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Output definition for department breakdown in email count
|
||||
*/
|
||||
export const DEPARTMENT_OUTPUT_PROPERTIES = {
|
||||
executive: { type: 'number', description: 'Number of executive department emails' },
|
||||
it: { type: 'number', description: 'Number of IT department emails' },
|
||||
finance: { type: 'number', description: 'Number of finance department emails' },
|
||||
management: { type: 'number', description: 'Number of management department emails' },
|
||||
sales: { type: 'number', description: 'Number of sales department emails' },
|
||||
legal: { type: 'number', description: 'Number of legal department emails' },
|
||||
support: { type: 'number', description: 'Number of support department emails' },
|
||||
hr: { type: 'number', description: 'Number of HR department emails' },
|
||||
marketing: { type: 'number', description: 'Number of marketing department emails' },
|
||||
communication: { type: 'number', description: 'Number of communication department emails' },
|
||||
education: { type: 'number', description: 'Number of education department emails' },
|
||||
design: { type: 'number', description: 'Number of design department emails' },
|
||||
health: { type: 'number', description: 'Number of health department emails' },
|
||||
operations: { type: 'number', description: 'Number of operations department emails' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete department object output definition
|
||||
*/
|
||||
export const DEPARTMENT_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Email count breakdown by department',
|
||||
properties: DEPARTMENT_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Output definition for seniority breakdown in email count
|
||||
*/
|
||||
export const SENIORITY_OUTPUT_PROPERTIES = {
|
||||
junior: { type: 'number', description: 'Number of junior-level emails' },
|
||||
senior: { type: 'number', description: 'Number of senior-level emails' },
|
||||
executive: { type: 'number', description: 'Number of executive-level emails' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
/**
|
||||
* Complete seniority object output definition
|
||||
*/
|
||||
export const SENIORITY_OUTPUT: OutputProperty = {
|
||||
type: 'object',
|
||||
description: 'Email count breakdown by seniority level',
|
||||
properties: SENIORITY_OUTPUT_PROPERTIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' },
|
||||
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>
|
||||
|
||||
/**
|
||||
* Complete discover results array output definition
|
||||
*/
|
||||
export const DISCOVER_RESULTS_OUTPUT: OutputProperty = {
|
||||
type: 'array',
|
||||
description: 'List of companies matching the search criteria',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: DISCOVER_RESULT_OUTPUT_PROPERTIES,
|
||||
},
|
||||
}
|
||||
|
||||
// Common parameters for all Hunter.io tools
|
||||
interface HunterBaseParams {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
// Discover tool types
|
||||
export interface HunterDiscoverParams extends HunterBaseParams {
|
||||
query?: string
|
||||
domain?: string
|
||||
headcount?: string
|
||||
company_type?: string
|
||||
technology?: string
|
||||
}
|
||||
|
||||
interface HunterDiscoverResult {
|
||||
domain: string
|
||||
organization: string
|
||||
personal_emails: number
|
||||
generic_emails: number
|
||||
total_emails: number
|
||||
}
|
||||
|
||||
export interface HunterDiscoverResponse extends ToolResponse {
|
||||
output: {
|
||||
results: HunterDiscoverResult[]
|
||||
}
|
||||
}
|
||||
|
||||
// Domain Search tool types
|
||||
export interface HunterDomainSearchParams extends HunterBaseParams {
|
||||
domain: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
type?: 'personal' | 'generic' | 'all'
|
||||
seniority?: 'junior' | 'senior' | 'executive' | 'all'
|
||||
department?: string
|
||||
}
|
||||
|
||||
export interface HunterEmail {
|
||||
value: string
|
||||
type: string
|
||||
confidence: number
|
||||
sources: Array<{
|
||||
domain: string
|
||||
uri: string
|
||||
extracted_on: string
|
||||
last_seen_on: string
|
||||
still_on_page: boolean
|
||||
}>
|
||||
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 | null
|
||||
status: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface HunterDomainSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
domain: string
|
||||
disposable: boolean
|
||||
webmail: boolean
|
||||
accept_all: boolean
|
||||
pattern: string
|
||||
organization: string
|
||||
linked_domains: string[]
|
||||
emails: HunterEmail[]
|
||||
}
|
||||
}
|
||||
|
||||
// Email Finder tool types
|
||||
export interface HunterEmailFinderParams extends HunterBaseParams {
|
||||
domain: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
company?: string
|
||||
}
|
||||
|
||||
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
|
||||
extracted_on: string
|
||||
last_seen_on: string
|
||||
still_on_page: boolean
|
||||
}>
|
||||
verification: {
|
||||
date: string | null
|
||||
status: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Email Verifier tool types
|
||||
export interface HunterEmailVerifierParams extends HunterBaseParams {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface HunterEmailVerifierResponse extends ToolResponse {
|
||||
output: {
|
||||
result: 'deliverable' | 'undeliverable' | 'risky'
|
||||
score: number
|
||||
email: string
|
||||
regexp: boolean
|
||||
gibberish: boolean
|
||||
disposable: boolean
|
||||
webmail: boolean
|
||||
mx_records: boolean
|
||||
smtp_server: boolean
|
||||
smtp_check: boolean
|
||||
accept_all: boolean
|
||||
block: boolean
|
||||
status: 'valid' | 'invalid' | 'accept_all' | 'webmail' | 'disposable' | 'unknown'
|
||||
sources: Array<{
|
||||
domain: string
|
||||
uri: string
|
||||
extracted_on: string
|
||||
last_seen_on: string
|
||||
still_on_page: boolean
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
// Enrichment tool types
|
||||
export interface HunterEnrichmentParams extends HunterBaseParams {
|
||||
email?: string
|
||||
domain?: string
|
||||
linkedin_handle?: string
|
||||
}
|
||||
|
||||
export interface HunterEnrichmentResponse extends ToolResponse {
|
||||
output: {
|
||||
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[]
|
||||
}
|
||||
}
|
||||
|
||||
// Email Count tool types
|
||||
export interface HunterEmailCountParams extends HunterBaseParams {
|
||||
domain?: string
|
||||
company?: string
|
||||
type?: 'personal' | 'generic' | 'all'
|
||||
}
|
||||
|
||||
export interface HunterEmailCountResponse extends ToolResponse {
|
||||
output: {
|
||||
total: number
|
||||
personal_emails: number
|
||||
generic_emails: number
|
||||
department: {
|
||||
executive: number
|
||||
it: number
|
||||
finance: number
|
||||
management: number
|
||||
sales: number
|
||||
legal: number
|
||||
support: number
|
||||
hr: number
|
||||
marketing: number
|
||||
communication: number
|
||||
education: number
|
||||
design: number
|
||||
health: number
|
||||
operations: number
|
||||
}
|
||||
seniority: {
|
||||
junior: number
|
||||
senior: number
|
||||
executive: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type HunterResponse =
|
||||
| HunterDiscoverResponse
|
||||
| HunterDomainSearchResponse
|
||||
| HunterEmailFinderResponse
|
||||
| HunterEmailVerifierResponse
|
||||
| HunterEnrichmentResponse
|
||||
| HunterEmailCountResponse
|
||||
Reference in New Issue
Block a user