kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add hunter.io client
rodzic
db6acfe3da
commit
e3cad9dc13
|
@ -3,6 +3,7 @@
|
||||||
"extends": ["@fisch0920/eslint-config/node"],
|
"extends": ["@fisch0920/eslint-config/node"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"unicorn/no-static-only-class": "off",
|
"unicorn/no-static-only-class": "off",
|
||||||
|
"unicorn/no-array-reduce": "off",
|
||||||
"@typescript-eslint/naming-convention": "off"
|
"@typescript-eslint/naming-convention": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,8 @@ import restoreCursor from 'restore-cursor'
|
||||||
// import { MidjourneyClient } from '../src/index.js'
|
// import { MidjourneyClient } from '../src/index.js'
|
||||||
// import { BingClient } from '../src/index.js'
|
// import { BingClient } from '../src/index.js'
|
||||||
// import { TavilyClient } from '../src/index.js'
|
// import { TavilyClient } from '../src/index.js'
|
||||||
import { SocialDataClient } from '../src/index.js'
|
// import { SocialDataClient } from '../src/index.js'
|
||||||
|
import { HunterClient } from '../src/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scratch pad for testing.
|
* Scratch pad for testing.
|
||||||
|
@ -120,8 +121,19 @@ async function main() {
|
||||||
// })
|
// })
|
||||||
// console.log(JSON.stringify(res, null, 2))
|
// console.log(JSON.stringify(res, null, 2))
|
||||||
|
|
||||||
const socialData = new SocialDataClient()
|
// const socialData = new SocialDataClient()
|
||||||
const res = await socialData.getUserByUsername('transitive_bs')
|
// const res = await socialData.getUserByUsername('transitive_bs')
|
||||||
|
// console.log(JSON.stringify(res, null, 2))
|
||||||
|
|
||||||
|
const hunter = new HunterClient()
|
||||||
|
// const res = await hunter.emailVerifier({
|
||||||
|
// email: 'travis@transitivebullsh.it'
|
||||||
|
// })
|
||||||
|
const res = await hunter.emailFinder({
|
||||||
|
domain: 'aomni.com',
|
||||||
|
first_name: 'David',
|
||||||
|
last_name: 'Zhang'
|
||||||
|
})
|
||||||
console.log(JSON.stringify(res, null, 2))
|
console.log(JSON.stringify(res, null, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,6 +138,7 @@ Depending on the AI SDK and tool you want to use, you'll also need to install th
|
||||||
| [E2B](https://e2b.dev) | `e2b` | Hosted Python code intrepreter sandbox which is really useful for data analysis, flexible code execution, and advanced reasoning on-the-fly. |
|
| [E2B](https://e2b.dev) | `e2b` | Hosted Python code intrepreter sandbox which is really useful for data analysis, flexible code execution, and advanced reasoning on-the-fly. |
|
||||||
| [Exa](https://docs.exa.ai) | `ExaClient` | Web search tailored for LLMs. |
|
| [Exa](https://docs.exa.ai) | `ExaClient` | Web search tailored for LLMs. |
|
||||||
| [Firecrawl](https://www.firecrawl.dev) | `FirecrawlClient` | Website scraping and sanitization. |
|
| [Firecrawl](https://www.firecrawl.dev) | `FirecrawlClient` | Website scraping and sanitization. |
|
||||||
|
| [Hunter](https://hunter.io) | `HunterClient` | Email finder, verifier, and enrichment. |
|
||||||
| [Midjourney](https://www.imagineapi.dev) | `MidjourneyClient` | Unofficial Midjourney client for generative images. |
|
| [Midjourney](https://www.imagineapi.dev) | `MidjourneyClient` | Unofficial Midjourney client for generative images. |
|
||||||
| [Novu](https://novu.co) | `NovuClient` | Sending notifications (email, SMS, in-app, push, etc). |
|
| [Novu](https://novu.co) | `NovuClient` | Sending notifications (email, SMS, in-app, push, etc). |
|
||||||
| [People Data Labs](https://www.peopledatalabs.com) | `PeopleDataLabsClient` | People & company data (WIP). |
|
| [People Data Labs](https://www.peopledatalabs.com) | `PeopleDataLabsClient` | People & company data (WIP). |
|
||||||
|
@ -204,6 +205,7 @@ See the [examples](./examples) directory for examples of how to use each of thes
|
||||||
- replicate
|
- replicate
|
||||||
- huggingface
|
- huggingface
|
||||||
- [skyvern](https://github.com/Skyvern-AI/skyvern)
|
- [skyvern](https://github.com/Skyvern-AI/skyvern)
|
||||||
|
- pull from [clay](https://www.clay.com/integrations)
|
||||||
- pull from [langchain](https://github.com/langchain-ai/langchainjs/tree/main/langchain)
|
- pull from [langchain](https://github.com/langchain-ai/langchainjs/tree/main/langchain)
|
||||||
- provide a converter for langchain `DynamicStructuredTool`
|
- provide a converter for langchain `DynamicStructuredTool`
|
||||||
- pull from [nango](https://docs.nango.dev/integrations/overview)
|
- pull from [nango](https://docs.nango.dev/integrations/overview)
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
import defaultKy, { type KyInstance } from 'ky'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { aiFunction, AIFunctionsProvider } from '../fns.js'
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
getEnv,
|
||||||
|
pruneNullOrUndefinedDeep,
|
||||||
|
sanitizeSearchParams
|
||||||
|
} from '../utils.js'
|
||||||
|
|
||||||
|
export namespace hunter {
|
||||||
|
export const API_BASE_URL = 'https://api.hunter.io'
|
||||||
|
|
||||||
|
export const DepartmentSchema = z.enum([
|
||||||
|
'executive',
|
||||||
|
'it',
|
||||||
|
'finance',
|
||||||
|
'management',
|
||||||
|
'sales',
|
||||||
|
'legal',
|
||||||
|
'support',
|
||||||
|
'hr',
|
||||||
|
'marketing',
|
||||||
|
'communication',
|
||||||
|
'education',
|
||||||
|
'design',
|
||||||
|
'health',
|
||||||
|
'operations'
|
||||||
|
])
|
||||||
|
export type Department = z.infer<typeof DepartmentSchema>
|
||||||
|
|
||||||
|
export const SenioritySchema = z.enum(['junior', 'senior', 'executive'])
|
||||||
|
export type Seniority = z.infer<typeof SenioritySchema>
|
||||||
|
|
||||||
|
export const PersonFieldSchema = z.enum([
|
||||||
|
'full_name',
|
||||||
|
'position',
|
||||||
|
'phone_number'
|
||||||
|
])
|
||||||
|
export type PersonField = z.infer<typeof PersonFieldSchema>
|
||||||
|
|
||||||
|
export const DomainSearchOptionsSchema = z.object({
|
||||||
|
domain: z.string().optional().describe('domain to search for'),
|
||||||
|
company: z.string().optional().describe('company name to search for'),
|
||||||
|
limit: z.number().int().positive().optional(),
|
||||||
|
offset: z.number().int().nonnegative().optional(),
|
||||||
|
type: z.enum(['personal', 'generic']).optional(),
|
||||||
|
seniority: z.union([SenioritySchema, z.array(SenioritySchema)]).optional(),
|
||||||
|
department: z
|
||||||
|
.union([DepartmentSchema, z.array(DepartmentSchema)])
|
||||||
|
.optional(),
|
||||||
|
required_field: z
|
||||||
|
.union([PersonFieldSchema, z.array(PersonFieldSchema)])
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
export type DomainSearchOptions = z.infer<typeof DomainSearchOptionsSchema>
|
||||||
|
|
||||||
|
export const EmailFinderOptionsSchema = z.object({
|
||||||
|
domain: z.string().optional().describe('domain to search for'),
|
||||||
|
company: z.string().optional().describe('company name to search for'),
|
||||||
|
first_name: z.string().describe("person's first name"),
|
||||||
|
last_name: z.string().describe("person's last name"),
|
||||||
|
max_duration: z.number().int().positive().min(3).max(20).optional()
|
||||||
|
})
|
||||||
|
export type EmailFinderOptions = z.infer<typeof EmailFinderOptionsSchema>
|
||||||
|
|
||||||
|
export const EmailVerifierOptionsSchema = z.object({
|
||||||
|
email: z.string().describe('email address to verify')
|
||||||
|
})
|
||||||
|
export type EmailVerifierOptions = z.infer<typeof EmailVerifierOptionsSchema>
|
||||||
|
|
||||||
|
export interface DomainSearchResponse {
|
||||||
|
data: DomainSearchData
|
||||||
|
meta: {
|
||||||
|
results: number
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
params: {
|
||||||
|
domain?: string
|
||||||
|
company?: string
|
||||||
|
type?: string
|
||||||
|
seniority?: string
|
||||||
|
department?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainSearchData {
|
||||||
|
domain: string
|
||||||
|
disposable: boolean
|
||||||
|
webmail?: boolean
|
||||||
|
accept_all?: boolean
|
||||||
|
pattern?: string
|
||||||
|
organization?: string
|
||||||
|
description?: string
|
||||||
|
industry?: string
|
||||||
|
twitter?: string
|
||||||
|
facebook?: string
|
||||||
|
linkedin?: string
|
||||||
|
instagram?: string
|
||||||
|
youtube?: string
|
||||||
|
technologies?: string[]
|
||||||
|
country?: string
|
||||||
|
state?: string
|
||||||
|
city?: string
|
||||||
|
postal_code?: string
|
||||||
|
street?: string
|
||||||
|
headcount?: string
|
||||||
|
company_type?: string
|
||||||
|
emails?: Email[]
|
||||||
|
linked_domains?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Email {
|
||||||
|
value: string
|
||||||
|
type: string
|
||||||
|
confidence: number
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
position?: string
|
||||||
|
seniority?: string
|
||||||
|
department?: string
|
||||||
|
linkedin?: string
|
||||||
|
twitter?: string
|
||||||
|
phone_number?: string
|
||||||
|
verification?: Verification
|
||||||
|
sources?: Source[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Source {
|
||||||
|
domain: string
|
||||||
|
uri: string
|
||||||
|
extracted_on: string
|
||||||
|
last_seen_on: string
|
||||||
|
still_on_page?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Verification {
|
||||||
|
date: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailFinderResponse {
|
||||||
|
data: EmailFinderData
|
||||||
|
meta: {
|
||||||
|
params: {
|
||||||
|
first_name?: string
|
||||||
|
last_name?: string
|
||||||
|
full_name?: string
|
||||||
|
domain?: string
|
||||||
|
company?: string
|
||||||
|
max_duration?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailFinderData {
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
email: string
|
||||||
|
score: number
|
||||||
|
domain: string
|
||||||
|
accept_all: boolean
|
||||||
|
position?: string
|
||||||
|
twitter?: any
|
||||||
|
linkedin_url?: any
|
||||||
|
phone_number?: any
|
||||||
|
company?: string
|
||||||
|
sources?: Source[]
|
||||||
|
verification?: Verification
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailVerifierResponse {
|
||||||
|
data: EmailVerifierData
|
||||||
|
meta: {
|
||||||
|
params: {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailVerifierData {
|
||||||
|
status:
|
||||||
|
| 'valid'
|
||||||
|
| 'invalid'
|
||||||
|
| 'accept_all'
|
||||||
|
| 'webmail'
|
||||||
|
| 'disposable'
|
||||||
|
| 'unknown'
|
||||||
|
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
|
||||||
|
sources?: Source[]
|
||||||
|
_deprecation_notice?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight wrapper around Hunter.io email finder, verifier, and enrichment
|
||||||
|
* APIs.
|
||||||
|
*
|
||||||
|
* @see https://hunter.io/api-documentation
|
||||||
|
*/
|
||||||
|
export class HunterClient extends AIFunctionsProvider {
|
||||||
|
protected readonly ky: KyInstance
|
||||||
|
protected readonly apiKey: string
|
||||||
|
protected readonly apiBaseUrl: string
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
apiKey = getEnv('HUNTER_API_KEY'),
|
||||||
|
apiBaseUrl = hunter.API_BASE_URL,
|
||||||
|
ky = defaultKy
|
||||||
|
}: {
|
||||||
|
apiKey?: string
|
||||||
|
apiBaseUrl?: string
|
||||||
|
ky?: KyInstance
|
||||||
|
} = {}) {
|
||||||
|
assert(
|
||||||
|
apiKey,
|
||||||
|
'HunterClient missing required "apiKey" (defaults to "HUNTER_API_KEY")'
|
||||||
|
)
|
||||||
|
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.apiKey = apiKey
|
||||||
|
this.apiBaseUrl = apiBaseUrl
|
||||||
|
|
||||||
|
this.ky = ky.extend({
|
||||||
|
prefixUrl: this.apiBaseUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@aiFunction({
|
||||||
|
name: 'hunter_domain_search',
|
||||||
|
description:
|
||||||
|
'Gets all the email addresses associated with a given company or domain.',
|
||||||
|
inputSchema: hunter.DomainSearchOptionsSchema.pick({
|
||||||
|
domain: true,
|
||||||
|
company: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
async domainSearch(domainOrOpts: string | hunter.DomainSearchOptions) {
|
||||||
|
const opts =
|
||||||
|
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
|
||||||
|
if (!opts.domain && !opts.company) {
|
||||||
|
throw new Error('Either "domain" or "company" is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.ky
|
||||||
|
.get('v2/domain-search', {
|
||||||
|
searchParams: sanitizeSearchParams(
|
||||||
|
{
|
||||||
|
...opts,
|
||||||
|
api_key: this.apiKey
|
||||||
|
},
|
||||||
|
{ csv: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.json<hunter.DomainSearchResponse>()
|
||||||
|
|
||||||
|
return pruneNullOrUndefinedDeep(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
@aiFunction({
|
||||||
|
name: 'hunter_email_finder',
|
||||||
|
description:
|
||||||
|
'Finds the most likely email address from a domain name, a first name and a last name.',
|
||||||
|
inputSchema: hunter.EmailFinderOptionsSchema.pick({
|
||||||
|
domain: true,
|
||||||
|
company: true,
|
||||||
|
first_name: true,
|
||||||
|
last_name: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
async emailFinder(opts: hunter.EmailFinderOptions) {
|
||||||
|
if (!opts.domain && !opts.company) {
|
||||||
|
throw new Error('Either "domain" or "company" is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await this.ky
|
||||||
|
.get('v2/email-finder', {
|
||||||
|
searchParams: sanitizeSearchParams({
|
||||||
|
...opts,
|
||||||
|
api_key: this.apiKey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.json<hunter.EmailFinderResponse>()
|
||||||
|
|
||||||
|
return pruneNullOrUndefinedDeep(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
@aiFunction({
|
||||||
|
name: 'hunter_email_verifier',
|
||||||
|
description: 'Verifies the deliverability of an email address.',
|
||||||
|
inputSchema: hunter.EmailVerifierOptionsSchema
|
||||||
|
})
|
||||||
|
async emailVerifier(emailOrOpts: string | hunter.EmailVerifierOptions) {
|
||||||
|
const opts =
|
||||||
|
typeof emailOrOpts === 'string' ? { email: emailOrOpts } : emailOrOpts
|
||||||
|
|
||||||
|
const res = await this.ky
|
||||||
|
.get('v2/email-verifier', {
|
||||||
|
searchParams: sanitizeSearchParams({
|
||||||
|
...opts,
|
||||||
|
api_key: this.apiKey
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.json<hunter.EmailVerifierResponse>()
|
||||||
|
|
||||||
|
return pruneNullOrUndefinedDeep(res)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ export * from './dexa-client.js'
|
||||||
export * from './diffbot-client.js'
|
export * from './diffbot-client.js'
|
||||||
export * from './exa-client.js'
|
export * from './exa-client.js'
|
||||||
export * from './firecrawl-client.js'
|
export * from './firecrawl-client.js'
|
||||||
|
export * from './hunter-client.js'
|
||||||
export * from './midjourney-client.js'
|
export * from './midjourney-client.js'
|
||||||
export * from './novu-client.js'
|
export * from './novu-client.js'
|
||||||
export * from './people-data-labs-client.js'
|
export * from './people-data-labs-client.js'
|
||||||
|
|
19
src/utils.ts
19
src/utils.ts
|
@ -134,10 +134,10 @@ export function sanitizeSearchParams(
|
||||||
string,
|
string,
|
||||||
string | number | boolean | string[] | number[] | boolean[] | undefined
|
string | number | boolean | string[] | number[] | boolean[] | undefined
|
||||||
>
|
>
|
||||||
| object
|
| object,
|
||||||
|
{ csv = false }: { csv?: boolean } = {}
|
||||||
): URLSearchParams {
|
): URLSearchParams {
|
||||||
return new URLSearchParams(
|
const entries = Object.entries(searchParams).flatMap(([key, value]) => {
|
||||||
Object.entries(searchParams).flatMap(([key, value]) => {
|
|
||||||
if (key === undefined || value === undefined) {
|
if (key === undefined || value === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,20 @@ export function sanitizeSearchParams(
|
||||||
|
|
||||||
return [[key, String(value)]]
|
return [[key, String(value)]]
|
||||||
}) as [string, string][]
|
}) as [string, string][]
|
||||||
|
|
||||||
|
if (!csv) {
|
||||||
|
return new URLSearchParams(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvEntries = entries.reduce(
|
||||||
|
(acc, [key, value]) => ({
|
||||||
|
...acc,
|
||||||
|
[key]: acc[key] ? `${acc[key]},${value}` : value
|
||||||
|
}),
|
||||||
|
{} as any
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return new URLSearchParams(csvEntries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Ładowanie…
Reference in New Issue