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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+107
View File
@@ -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' },
},
},
}
+127
View File
@@ -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,
},
}
+175
View File
@@ -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,
},
}
+124
View File
@@ -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,
},
}
+132
View File
@@ -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,
},
}
+146
View File
@@ -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,
},
}
+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,
})
})
})
+13
View File
@@ -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
+413
View File
@@ -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