feat: add email outreach CLIs for backlink building

Add 4 new zero-dependency CLI tools for email outreach:
- hunter.js: Email finding/verification via Hunter.io (query param auth)
- snov.js: Email finding + drip campaigns via Snov.io (OAuth2 auth)
- lemlist.js: Cold email campaigns via Lemlist (Basic auth)
- instantly.js: Cold email at scale via Instantly.ai (query param auth)

Includes integration guides and registry/README updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Corey Haines 2026-02-17 14:09:50 -08:00
parent f123804827
commit aad399682c
10 changed files with 1404 additions and 0 deletions

View file

@ -47,6 +47,10 @@ Quick reference for AI agents to discover tool capabilities and integration meth
| postmark | Email | ✓ | - | [](clis/postmark.js) | ✓ | [postmark.md](integrations/postmark.md) |
| brevo | Email/SMS | ✓ | - | [](clis/brevo.js) | ✓ | [brevo.md](integrations/brevo.md) |
| activecampaign | Email/CRM | ✓ | - | [](clis/activecampaign.js) | ✓ | [activecampaign.md](integrations/activecampaign.md) |
| hunter | Email Outreach | ✓ | - | [](clis/hunter.js) | - | [hunter.md](integrations/hunter.md) |
| snov | Email Outreach | ✓ | - | [](clis/snov.js) | - | [snov.md](integrations/snov.md) |
| lemlist | Email Outreach | ✓ | - | [](clis/lemlist.js) | - | [lemlist.md](integrations/lemlist.md) |
| instantly | Email Outreach | ✓ | - | [](clis/instantly.js) | - | [instantly.md](integrations/instantly.md) |
| google-ads | Ads | ✓ | ✓ | [](clis/google-ads.js) | ✓ | [google-ads.md](integrations/google-ads.md) |
| meta-ads | Ads | ✓ | - | [](clis/meta-ads.js) | ✓ | [meta-ads.md](integrations/meta-ads.md) |
| linkedin-ads | Ads | ✓ | - | [](clis/linkedin-ads.js) | - | [linkedin-ads.md](integrations/linkedin-ads.md) |
@ -287,6 +291,19 @@ Webinar and virtual event platforms.
**Agent recommendation**: Demio for marketing-focused webinars. Livestorm for full event engagement.
### Email Outreach
Cold email outreach and email finding tools for link building and sales prospecting.
| Tool | Best For | Notes |
|------|----------|-------|
| **hunter** | Email finding, domain search | Largest email database |
| **snov** | Email finding, drip campaigns | Built-in sequences |
| **lemlist** | Cold email campaigns | Personalization features |
| **instantly** | Cold email at scale | Email warmup built-in |
**Agent recommendation**: Hunter for finding emails. Lemlist or Instantly for sending cold email campaigns. Snov for combined finding + outreach.
### Commerce & CMS
E-commerce platforms and content management systems.
@ -342,6 +359,10 @@ To use MCP tools, ensure the appropriate MCP server is configured in your enviro
1. Read [customer-io.md](integrations/customer-io.md) for behavior-based automation
2. Read [resend.md](integrations/resend.md) for transactional email
### Running email outreach for backlinks
1. Read [hunter.md](integrations/hunter.md) for finding emails
2. Read [lemlist.md](integrations/lemlist.md) or [instantly.md](integrations/instantly.md) for sending campaigns
### Running paid ads
1. Read [google-ads.md](integrations/google-ads.md) for search campaigns
2. Read [meta-ads.md](integrations/meta-ads.md) for social campaigns

View file

@ -81,6 +81,10 @@ Every CLI reads credentials from environment variables:
| `tolt` | `TOLT_API_KEY` |
| `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` |
| `typeform` | `TYPEFORM_API_KEY` |
| `hunter` | `HUNTER_API_KEY` |
| `instantly` | `INSTANTLY_API_KEY` |
| `lemlist` | `LEMLIST_API_KEY` |
| `snov` | `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET` |
| `wistia` | `WISTIA_API_KEY` |
| `zapier` | `ZAPIER_API_KEY` |
@ -140,10 +144,13 @@ DOMAINS=$(rewardful affiliates list | jq -r '.data[].email')
| `google-ads.js` | Ads | [Google Ads](https://ads.google.com) |
| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) |
| `hotjar.js` | CRO | [Hotjar](https://hotjar.com) |
| `hunter.js` | Email Outreach | [Hunter.io](https://hunter.io) |
| `instantly.js` | Email Outreach | [Instantly.ai](https://instantly.ai) |
| `intercom.js` | Messaging | [Intercom](https://intercom.com) |
| `keywords-everywhere.js` | SEO | [Keywords Everywhere](https://keywordseverywhere.com) |
| `kit.js` | Email | [Kit](https://kit.com) |
| `klaviyo.js` | Email/SMS | [Klaviyo](https://klaviyo.com) |
| `lemlist.js` | Email Outreach | [Lemlist](https://lemlist.com) |
| `linkedin-ads.js` | Ads | [LinkedIn Ads](https://business.linkedin.com/marketing-solutions/ads) |
| `livestorm.js` | Webinar | [Livestorm](https://livestorm.co) |
| `mailchimp.js` | Email | [Mailchimp](https://mailchimp.com) |
@ -162,6 +169,7 @@ DOMAINS=$(rewardful affiliates list | jq -r '.data[].email')
| `segment.js` | Analytics | [Segment](https://segment.com) |
| `semrush.js` | SEO | [SEMrush](https://semrush.com) |
| `sendgrid.js` | Email | [SendGrid](https://sendgrid.com) |
| `snov.js` | Email Outreach | [Snov.io](https://snov.io) |
| `tiktok-ads.js` | Ads | [TikTok Ads](https://ads.tiktok.com) |
| `tolt.js` | Referral | [Tolt](https://tolt.io) |
| `trustpilot.js` | Reviews | [Trustpilot](https://trustpilot.com) |

249
tools/clis/hunter.js Executable file
View file

@ -0,0 +1,249 @@
#!/usr/bin/env node
const API_KEY = process.env.HUNTER_API_KEY
const BASE_URL = 'https://api.hunter.io/v2'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'HUNTER_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const separator = path.includes('?') ? '&' : '?'
const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}`
if (args['dry-run']) {
return { _dry_run: true, method, url: url.replace(API_KEY, '***'), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
const args = parseArgs(process.argv.slice(2))
const [cmd, sub, ...rest] = args._
async function main() {
let result
switch (cmd) {
case 'domain':
switch (sub) {
case 'search': {
const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break }
const params = new URLSearchParams({ domain })
if (args.limit) params.set('limit', args.limit)
if (args.type) params.set('type', args.type)
result = await api('GET', `/domain-search?${params.toString()}`)
break
}
case 'count': {
const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break }
const params = new URLSearchParams({ domain })
if (args.type) params.set('type', args.type)
result = await api('GET', `/email-count?${params.toString()}`)
break
}
default:
result = { error: 'Unknown domain subcommand. Use: search, count' }
}
break
case 'email':
switch (sub) {
case 'find': {
const domain = args.domain
const firstName = args['first-name']
const lastName = args['last-name']
if (!domain) { result = { error: '--domain required' }; break }
if (!firstName) { result = { error: '--first-name required' }; break }
if (!lastName) { result = { error: '--last-name required' }; break }
const params = new URLSearchParams({ domain, first_name: firstName, last_name: lastName })
result = await api('GET', `/email-finder?${params.toString()}`)
break
}
case 'verify': {
const email = args.email
if (!email) { result = { error: '--email required' }; break }
const params = new URLSearchParams({ email })
result = await api('GET', `/email-verifier?${params.toString()}`)
break
}
default:
result = { error: 'Unknown email subcommand. Use: find, verify' }
}
break
case 'account':
switch (sub) {
case 'info':
result = await api('GET', '/account')
break
default:
result = { error: 'Unknown account subcommand. Use: info' }
}
break
case 'leads':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.offset) params.set('offset', args.offset)
const qs = params.toString()
result = await api('GET', `/leads${qs ? '?' + qs : ''}`)
break
}
case 'get': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('GET', `/leads/${id}`)
break
}
case 'create': {
const email = args.email
if (!email) { result = { error: '--email required' }; break }
const body = { email }
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args.company) body.company = args.company
result = await api('POST', '/leads', body)
break
}
case 'delete': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('DELETE', `/leads/${id}`)
break
}
default:
result = { error: 'Unknown leads subcommand. Use: list, get, create, delete' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.offset) params.set('offset', args.offset)
const qs = params.toString()
result = await api('GET', `/campaigns${qs ? '?' + qs : ''}`)
break
}
case 'get': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('GET', `/campaigns/${id}`)
break
}
case 'start': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('POST', `/campaigns/${id}/start`)
break
}
case 'pause': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('POST', `/campaigns/${id}/pause`)
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get, start, pause' }
}
break
case 'leads-lists':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.offset) params.set('offset', args.offset)
const qs = params.toString()
result = await api('GET', `/leads_lists${qs ? '?' + qs : ''}`)
break
}
case 'get': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('GET', `/leads_lists/${id}`)
break
}
default:
result = { error: 'Unknown leads-lists subcommand. Use: list, get' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
domain: {
search: 'domain search --domain <domain> [--limit <n>] [--type personal|generic]',
count: 'domain count --domain <domain> [--type personal|generic]',
},
email: {
find: 'email find --domain <domain> --first-name <name> --last-name <name>',
verify: 'email verify --email <email>',
},
account: 'account info',
leads: {
list: 'leads list [--limit <n>] [--offset <n>]',
get: 'leads get --id <id>',
create: 'leads create --email <email> [--first-name <name>] [--last-name <name>] [--company <company>]',
delete: 'leads delete --id <id>',
},
campaigns: {
list: 'campaigns list [--limit <n>] [--offset <n>]',
get: 'campaigns get --id <id>',
start: 'campaigns start --id <id>',
pause: 'campaigns pause --id <id>',
},
'leads-lists': {
list: 'leads-lists list [--limit <n>] [--offset <n>]',
get: 'leads-lists get --id <id>',
},
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

270
tools/clis/instantly.js Executable file
View file

@ -0,0 +1,270 @@
#!/usr/bin/env node
const API_KEY = process.env.INSTANTLY_API_KEY
const BASE_URL = 'https://api.instantly.ai/api/v1'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'INSTANTLY_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const separator = path.includes('?') ? '&' : '?'
const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}`
if (args['dry-run']) {
const maskedUrl = url.replace(API_KEY, '***')
const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined
if (maskedBody && maskedBody.api_key) maskedBody.api_key = '***'
return { _dry_run: true, method, url: maskedUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: maskedBody }
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
const args = parseArgs(process.argv.slice(2))
const [cmd, sub, ...rest] = args._
async function main() {
let result
switch (cmd) {
case 'campaigns':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.skip) params.set('skip', args.skip)
const qs = params.toString()
result = await api('GET', `/campaign/list${qs ? '?' + qs : ''}`)
break
}
case 'get': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
const params = new URLSearchParams({ campaign_id: id })
result = await api('GET', `/campaign/get?${params.toString()}`)
break
}
case 'status': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
const params = new URLSearchParams({ campaign_id: id })
result = await api('GET', `/campaign/get/status?${params.toString()}`)
break
}
case 'launch': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('POST', '/campaign/launch', { api_key: API_KEY, campaign_id: id })
break
}
case 'pause': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('POST', '/campaign/pause', { api_key: API_KEY, campaign_id: id })
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get, status, launch, pause' }
}
break
case 'leads':
switch (sub) {
case 'list': {
const campaignId = args['campaign-id']
if (!campaignId) { result = { error: '--campaign-id required' }; break }
const params = new URLSearchParams({ campaign_id: campaignId })
if (args.limit) params.set('limit', args.limit)
if (args.skip) params.set('skip', args.skip)
result = await api('GET', `/lead/get?${params.toString()}`)
break
}
case 'add': {
const campaignId = args['campaign-id']
const email = args.email
if (!campaignId) { result = { error: '--campaign-id required' }; break }
if (!email) { result = { error: '--email required' }; break }
const lead = { email }
if (args['first-name']) lead.first_name = args['first-name']
if (args['last-name']) lead.last_name = args['last-name']
if (args.company) lead.company_name = args.company
result = await api('POST', '/lead/add', { api_key: API_KEY, campaign_id: campaignId, leads: [lead] })
break
}
case 'delete': {
const campaignId = args['campaign-id']
const email = args.email
if (!campaignId) { result = { error: '--campaign-id required' }; break }
if (!email) { result = { error: '--email required' }; break }
result = await api('POST', '/lead/delete', { api_key: API_KEY, campaign_id: campaignId, delete_list: [email] })
break
}
case 'status': {
const campaignId = args['campaign-id']
const email = args.email
if (!campaignId) { result = { error: '--campaign-id required' }; break }
if (!email) { result = { error: '--email required' }; break }
const params = new URLSearchParams({ campaign_id: campaignId, email })
result = await api('GET', `/lead/get/status?${params.toString()}`)
break
}
default:
result = { error: 'Unknown leads subcommand. Use: list, add, delete, status' }
}
break
case 'accounts':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.skip) params.set('skip', args.skip)
const qs = params.toString()
result = await api('GET', `/account/list${qs ? '?' + qs : ''}`)
break
}
case 'status': {
const accountId = args['account-id']
if (!accountId) { result = { error: '--account-id required' }; break }
const params = new URLSearchParams({ email: accountId })
result = await api('GET', `/account/get/status?${params.toString()}`)
break
}
case 'warmup-status': {
const accountId = args['account-id']
if (!accountId) { result = { error: '--account-id required' }; break }
const params = new URLSearchParams({ email: accountId })
result = await api('GET', `/account/get/warmup?${params.toString()}`)
break
}
default:
result = { error: 'Unknown accounts subcommand. Use: list, status, warmup-status' }
}
break
case 'analytics':
switch (sub) {
case 'campaign': {
const campaignId = args['campaign-id']
if (!campaignId) { result = { error: '--campaign-id required' }; break }
const body = { api_key: API_KEY, campaign_id: campaignId }
if (args['start-date']) body.start_date = args['start-date']
if (args['end-date']) body.end_date = args['end-date']
result = await api('POST', '/analytics/campaign/summary', body)
break
}
case 'steps': {
const campaignId = args['campaign-id']
if (!campaignId) { result = { error: '--campaign-id required' }; break }
const body = { api_key: API_KEY, campaign_id: campaignId }
if (args['start-date']) body.start_date = args['start-date']
if (args['end-date']) body.end_date = args['end-date']
result = await api('POST', '/analytics/campaign/step', body)
break
}
case 'account': {
const startDate = args['start-date']
const endDate = args['end-date']
if (!startDate) { result = { error: '--start-date required' }; break }
if (!endDate) { result = { error: '--end-date required' }; break }
result = await api('POST', '/analytics/campaign/count', { api_key: API_KEY, start_date: startDate, end_date: endDate })
break
}
default:
result = { error: 'Unknown analytics subcommand. Use: campaign, steps, account' }
}
break
case 'blocklist':
switch (sub) {
case 'list':
result = await api('GET', '/blocklist')
break
case 'add': {
const entries = args.entries
if (!entries) { result = { error: '--entries required (comma-separated emails or domains)' }; break }
const entryList = entries.split(',').map(e => e.trim())
result = await api('POST', '/blocklist/add', { api_key: API_KEY, entries: entryList })
break
}
default:
result = { error: 'Unknown blocklist subcommand. Use: list, add' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
campaigns: {
list: 'campaigns list [--limit <n>] [--skip <n>]',
get: 'campaigns get --id <id>',
status: 'campaigns status --id <id>',
launch: 'campaigns launch --id <id>',
pause: 'campaigns pause --id <id>',
},
leads: {
list: 'leads list --campaign-id <id> [--limit <n>] [--skip <n>]',
add: 'leads add --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>] [--company <name>]',
delete: 'leads delete --campaign-id <id> --email <email>',
status: 'leads status --campaign-id <id> --email <email>',
},
accounts: {
list: 'accounts list [--limit <n>] [--skip <n>]',
status: 'accounts status --account-id <email>',
'warmup-status': 'accounts warmup-status --account-id <email>',
},
analytics: {
campaign: 'analytics campaign --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]',
steps: 'analytics steps --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]',
account: 'analytics account --start-date YYYY-MM-DD --end-date YYYY-MM-DD',
},
blocklist: {
list: 'blocklist list',
add: 'blocklist add --entries <email-or-domain,email-or-domain>',
},
options: '--dry-run (show request without executing)',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

221
tools/clis/lemlist.js Executable file
View file

@ -0,0 +1,221 @@
#!/usr/bin/env node
const API_KEY = process.env.LEMLIST_API_KEY
const BASE_URL = 'https://api.lemlist.com/api'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'LEMLIST_API_KEY environment variable required' }))
process.exit(1)
}
const AUTH = 'Basic ' + Buffer.from(`:${API_KEY}`).toString('base64')
async function api(method, path, body) {
if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
}
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': AUTH,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
const args = parseArgs(process.argv.slice(2))
const [cmd, sub, ...rest] = args._
async function main() {
let result
const offset = args.offset ? Number(args.offset) : 0
const limit = args.limit ? Number(args.limit) : 100
switch (cmd) {
case 'team':
switch (sub) {
case 'info':
result = await api('GET', '/team')
break
default:
result = { error: 'Unknown team subcommand. Use: info' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
params.set('offset', String(offset))
params.set('limit', String(limit))
result = await api('GET', `/campaigns?${params}`)
break
}
case 'get': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('GET', `/campaigns/${args.id}`)
break
}
case 'stats': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('GET', `/campaigns/${args.id}/stats`)
break
}
case 'export': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('GET', `/campaigns/${args.id}/export`)
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get, stats, export' }
}
break
case 'leads':
switch (sub) {
case 'list': {
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
const params = new URLSearchParams()
params.set('offset', String(offset))
params.set('limit', String(limit))
result = await api('GET', `/campaigns/${args['campaign-id']}/leads?${params}`)
break
}
case 'get': {
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${args.email}`)
break
}
case 'add': {
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
const body = {}
if (args['first-name']) body.firstName = args['first-name']
if (args['last-name']) body.lastName = args['last-name']
if (args.company) body.companyName = args.company
result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${args.email}`, body)
break
}
case 'delete': {
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${args.email}`)
break
}
default:
result = { error: 'Unknown leads subcommand. Use: list, get, add, delete' }
}
break
case 'unsubscribes':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
params.set('offset', String(offset))
params.set('limit', String(limit))
result = await api('GET', `/unsubscribes?${params}`)
break
}
case 'add': {
if (!args.email) { result = { error: '--email required' }; break }
result = await api('POST', `/unsubscribes/${args.email}`)
break
}
case 'delete': {
if (!args.email) { result = { error: '--email required' }; break }
result = await api('DELETE', `/unsubscribes/${args.email}`)
break
}
default:
result = { error: 'Unknown unsubscribes subcommand. Use: list, add, delete' }
}
break
case 'activities':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['campaign-id']) params.set('campaignId', args['campaign-id'])
if (args.type) params.set('type', args.type)
params.set('offset', String(offset))
params.set('limit', String(limit))
result = await api('GET', `/activities?${params}`)
break
}
default:
result = { error: 'Unknown activities subcommand. Use: list' }
}
break
case 'hooks':
switch (sub) {
case 'list':
result = await api('GET', '/hooks')
break
case 'create': {
if (!args['target-url']) { result = { error: '--target-url required' }; break }
if (!args.event) { result = { error: '--event required' }; break }
result = await api('POST', '/hooks', { targetUrl: args['target-url'], event: args.event })
break
}
case 'delete': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('DELETE', `/hooks/${args.id}`)
break
}
default:
result = { error: 'Unknown hooks subcommand. Use: list, create, delete' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
team: 'team info',
campaigns: 'campaigns [list | get --id <id> | stats --id <id> | export --id <id>] [--offset 0] [--limit 100]',
leads: 'leads [list | get --email <email> | add --email <email> | delete --email <email>] --campaign-id <id> [--first-name <name>] [--last-name <name>] [--company <name>]',
unsubscribes: 'unsubscribes [list | add --email <email> | delete --email <email>] [--offset 0] [--limit 100]',
activities: 'activities list [--campaign-id <id>] [--type emailsSent|emailsOpened|emailsClicked|emailsReplied|emailsBounced] [--offset 0] [--limit 100]',
hooks: 'hooks [list | create --target-url <url> --event <event> | delete --id <id>]',
options: '--dry-run --offset <n> --limit <n>',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

237
tools/clis/snov.js Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env node
const CLIENT_ID = process.env.SNOV_CLIENT_ID
const CLIENT_SECRET = process.env.SNOV_CLIENT_SECRET
const BASE_URL = 'https://api.snov.io/v1'
if (!CLIENT_ID || !CLIENT_SECRET) {
console.error(JSON.stringify({ error: 'SNOV_CLIENT_ID and SNOV_CLIENT_SECRET environment variables required' }))
process.exit(1)
}
let cachedToken = null
async function getToken() {
if (cachedToken) return cachedToken
const res = await fetch('https://api.snov.io/v1/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
})
const data = await res.json()
if (!data.access_token) {
throw new Error(data.error_description || data.error || 'Failed to obtain access token')
}
cachedToken = data.access_token
return cachedToken
}
async function api(method, path, body) {
if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined }
}
const token = await getToken()
const opts = {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
}
if (body) opts.body = JSON.stringify(body)
const res = await fetch(`${BASE_URL}${path}`, opts)
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
const args = parseArgs(process.argv.slice(2))
const [cmd, sub, ...rest] = args._
async function main() {
let result
switch (cmd) {
case 'domain':
switch (sub) {
case 'search': {
const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break }
const body = { domain, type: args.type || 'all', limit: Number(args.limit || 100), lastId: 0 }
result = await api('POST', '/get-domain-emails-with-info', body)
break
}
case 'count': {
const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break }
result = await api('POST', '/get-domain-emails-count', { domain })
break
}
default:
result = { error: 'Unknown domain subcommand. Use: search, count' }
}
break
case 'email':
switch (sub) {
case 'find': {
const domain = args.domain
const firstName = args['first-name']
const lastName = args['last-name']
if (!domain || !firstName || !lastName) { result = { error: '--domain, --first-name, and --last-name required' }; break }
result = await api('POST', '/get-emails-from-names', { firstName, lastName, domain })
break
}
case 'verify': {
const email = args.email
if (!email) { result = { error: '--email required' }; break }
result = await api('POST', '/get-emails-verification-status', { emails: [email] })
break
}
default:
result = { error: 'Unknown email subcommand. Use: find, verify' }
}
break
case 'prospect':
switch (sub) {
case 'find': {
const email = args.email
if (!email) { result = { error: '--email required' }; break }
result = await api('POST', '/get-prospect-by-email', { email })
break
}
case 'add': {
const email = args.email
if (!email) { result = { error: '--email required' }; break }
const body = { email }
if (args['first-name']) body.firstName = args['first-name']
if (args['last-name']) body.lastName = args['last-name']
if (args['list-id']) body.listId = args['list-id']
result = await api('POST', '/add-prospect-to-list', body)
break
}
default:
result = { error: 'Unknown prospect subcommand. Use: find, add' }
}
break
case 'lists':
switch (sub) {
case 'list':
result = await api('GET', '/get-user-lists')
break
case 'prospects': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
const params = new URLSearchParams({ listId: id })
if (args.page) params.set('page', args.page)
if (args['per-page']) params.set('perPage', args['per-page'])
result = await api('GET', `/prospect-list?${params.toString()}`)
break
}
default:
result = { error: 'Unknown lists subcommand. Use: list, prospects' }
}
break
case 'technology':
switch (sub) {
case 'check': {
const domain = args.domain
if (!domain) { result = { error: '--domain required' }; break }
result = await api('POST', '/get-technology-checker', { domain })
break
}
default:
result = { error: 'Unknown technology subcommand. Use: check' }
}
break
case 'drips':
switch (sub) {
case 'list':
result = await api('GET', '/get-user-campaigns')
break
case 'get': {
const id = args.id
if (!id) { result = { error: '--id required' }; break }
result = await api('GET', `/get-emails-from-campaign?id=${encodeURIComponent(id)}`)
break
}
case 'add-prospect': {
const campaignId = args['campaign-id']
const email = args.email
if (!campaignId || !email) { result = { error: '--campaign-id and --email required' }; break }
const body = { campaignId, email }
if (args['first-name']) body.firstName = args['first-name']
if (args['last-name']) body.lastName = args['last-name']
result = await api('POST', '/add-prospect-to-email-campaign', body)
break
}
default:
result = { error: 'Unknown drips subcommand. Use: list, get, add-prospect' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
domain: {
search: 'domain search --domain <domain> [--type all|personal|generic] [--limit <n>]',
count: 'domain count --domain <domain>',
},
email: {
find: 'email find --domain <domain> --first-name <name> --last-name <name>',
verify: 'email verify --email <email>',
},
prospect: {
find: 'prospect find --email <email>',
add: 'prospect add --email <email> [--first-name <name>] [--last-name <name>] [--list-id <id>]',
},
lists: {
list: 'lists list',
prospects: 'lists prospects --id <id> [--page <n>] [--per-page <n>]',
},
technology: 'technology check --domain <domain>',
drips: {
list: 'drips list',
get: 'drips get --id <id>',
'add-prospect': 'drips add-prospect --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>]',
},
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

View file

@ -0,0 +1,90 @@
# Hunter.io
Email finding and verification platform for outreach and link building.
## Capabilities
| Integration | Available | Notes |
|-------------|-----------|-------|
| API | ✓ | REST API for domain search, email finder, verification |
| MCP | - | Not available |
| CLI | [](../clis/hunter.js) | Zero-dependency Node.js CLI |
| SDK | - | API-only |
## Authentication
- **Type**: API Key (query parameter)
- **Parameter**: `api_key={key}`
- **Env var**: `HUNTER_API_KEY`
- **Get key**: [Hunter dashboard > API](https://hunter.io/api-keys)
## Common Agent Operations
### Find emails for a domain
```bash
node tools/clis/hunter.js domain search --domain example.com --limit 10
```
### Find a specific person's email
```bash
node tools/clis/hunter.js email find --domain example.com --first-name John --last-name Doe
```
### Verify an email address
```bash
node tools/clis/hunter.js email verify --email john@example.com
```
### Count emails available for a domain
```bash
node tools/clis/hunter.js domain count --domain example.com
```
### Manage leads
```bash
# List leads
node tools/clis/hunter.js leads list --limit 20
# Create a lead
node tools/clis/hunter.js leads create --email john@example.com --first-name John --last-name Doe --company "Example Inc"
# Delete a lead
node tools/clis/hunter.js leads delete --id 12345
```
### Manage campaigns
```bash
# List campaigns
node tools/clis/hunter.js campaigns list
# Get campaign details
node tools/clis/hunter.js campaigns get --id 12345
# Start/pause a campaign
node tools/clis/hunter.js campaigns start --id 12345
node tools/clis/hunter.js campaigns pause --id 12345
```
### Check account usage
```bash
node tools/clis/hunter.js account info
```
## Rate Limits
- Free plan: 25 searches/month, 50 verifications/month
- Paid plans scale with tier
- API rate limit: 10 requests/second
## Use Cases
- **Link building**: Find email contacts at target domains for outreach
- **Prospecting**: Build lead lists from company domains
- **Verification**: Clean email lists before sending campaigns

View file

@ -0,0 +1,104 @@
# Instantly.ai
Cold email platform with built-in email warmup and campaign management at scale.
## Capabilities
| Integration | Available | Notes |
|-------------|-----------|-------|
| API | ✓ | REST API for campaigns, leads, accounts, analytics |
| MCP | - | Not available |
| CLI | [](../clis/instantly.js) | Zero-dependency Node.js CLI |
| SDK | - | API-only |
## Authentication
- **Type**: API Key (query parameter)
- **Parameter**: `api_key={key}`
- **Env var**: `INSTANTLY_API_KEY`
- **Get key**: [Instantly Settings > Integrations > API](https://app.instantly.ai/app/settings/integrations)
## Common Agent Operations
### Manage campaigns
```bash
# List campaigns
node tools/clis/instantly.js campaigns list --limit 20
# Get campaign details
node tools/clis/instantly.js campaigns get --id cam_abc123
# Check campaign status
node tools/clis/instantly.js campaigns status --id cam_abc123
# Launch a campaign
node tools/clis/instantly.js campaigns launch --id cam_abc123
# Pause a campaign
node tools/clis/instantly.js campaigns pause --id cam_abc123
```
### Manage leads
```bash
# List leads in a campaign
node tools/clis/instantly.js leads list --campaign-id cam_abc123 --limit 50
# Add a lead
node tools/clis/instantly.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc"
# Delete a lead
node tools/clis/instantly.js leads delete --campaign-id cam_abc123 --email john@example.com
# Check lead status
node tools/clis/instantly.js leads status --campaign-id cam_abc123 --email john@example.com
```
### Manage email accounts
```bash
# List connected accounts
node tools/clis/instantly.js accounts list --limit 20
# Check account status
node tools/clis/instantly.js accounts status --account-id me@example.com
# Check warmup status
node tools/clis/instantly.js accounts warmup-status --account-id me@example.com
```
### View analytics
```bash
# Campaign analytics
node tools/clis/instantly.js analytics campaign --campaign-id cam_abc123 --start 2024-01-01 --end 2024-01-31
# Step-by-step analytics
node tools/clis/instantly.js analytics steps --campaign-id cam_abc123
# Account-level analytics
node tools/clis/instantly.js analytics account --start 2024-01-01 --end 2024-01-31
```
### Manage blocklist
```bash
# List blocked emails/domains
node tools/clis/instantly.js blocklist list
# Add to blocklist
node tools/clis/instantly.js blocklist add --entries "competitor.com,spam@example.com"
```
## Rate Limits
- API rate limits vary by plan
- Recommended: stay under 10 requests/second
## Use Cases
- **Link building at scale**: Run large-volume outreach campaigns with built-in warmup
- **Campaign management**: Launch, pause, and monitor cold email campaigns
- **Account health**: Monitor email account warmup and deliverability
- **Analytics**: Track open rates, reply rates, and campaign performance

View file

@ -0,0 +1,110 @@
# Lemlist
Cold email outreach platform with personalization and campaign management.
## Capabilities
| Integration | Available | Notes |
|-------------|-----------|-------|
| API | ✓ | REST API for campaigns, leads, activities, webhooks |
| MCP | - | Not available |
| CLI | [](../clis/lemlist.js) | Zero-dependency Node.js CLI |
| SDK | - | API-only |
## Authentication
- **Type**: Basic Auth (empty username, API key as password)
- **Header**: `Authorization: Basic base64(:api_key)`
- **Env var**: `LEMLIST_API_KEY`
- **Get key**: [Lemlist Settings > Integrations](https://app.lemlist.com/settings/integrations)
## Common Agent Operations
### List campaigns
```bash
node tools/clis/lemlist.js campaigns list --offset 0 --limit 20
```
### Get campaign details and stats
```bash
# Get campaign
node tools/clis/lemlist.js campaigns get --id cam_abc123
# Get campaign stats
node tools/clis/lemlist.js campaigns stats --id cam_abc123
# Export campaign data
node tools/clis/lemlist.js campaigns export --id cam_abc123
```
### Manage leads in a campaign
```bash
# List leads
node tools/clis/lemlist.js leads list --campaign-id cam_abc123
# Add a lead
node tools/clis/lemlist.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc"
# Get lead details
node tools/clis/lemlist.js leads get --campaign-id cam_abc123 --email john@example.com
# Remove a lead
node tools/clis/lemlist.js leads delete --campaign-id cam_abc123 --email john@example.com
```
### Manage unsubscribes
```bash
# List unsubscribed emails
node tools/clis/lemlist.js unsubscribes list
# Add to unsubscribe list
node tools/clis/lemlist.js unsubscribes add --email john@example.com
# Remove from unsubscribe list
node tools/clis/lemlist.js unsubscribes delete --email john@example.com
```
### View activities
```bash
# All activities
node tools/clis/lemlist.js activities list
# Filter by campaign and type
node tools/clis/lemlist.js activities list --campaign-id cam_abc123 --type emailsOpened
```
### Manage webhooks
```bash
# List hooks
node tools/clis/lemlist.js hooks list
# Create a webhook
node tools/clis/lemlist.js hooks create --target-url https://example.com/webhook --event emailsOpened
# Delete a webhook
node tools/clis/lemlist.js hooks delete --id hook_123
```
### Team info
```bash
node tools/clis/lemlist.js team info
```
## Rate Limits
- API rate limits vary by plan
- Recommended: stay under 10 requests/second
## Use Cases
- **Link building outreach**: Add prospects to campaigns for backlink requests
- **Campaign management**: Monitor open/reply rates across outreach campaigns
- **Lead management**: Add, remove, and track leads across campaigns
- **Webhook integration**: Get real-time notifications for email events

View file

@ -0,0 +1,94 @@
# Snov.io
Email finding, verification, and drip campaign platform for outreach.
## Capabilities
| Integration | Available | Notes |
|-------------|-----------|-------|
| API | ✓ | REST API for email finding, verification, prospects, drip campaigns |
| MCP | - | Not available |
| CLI | [](../clis/snov.js) | Zero-dependency Node.js CLI |
| SDK | - | API-only |
## Authentication
- **Type**: OAuth2 client credentials
- **Flow**: POST to `/oauth/access_token` with client_id + client_secret
- **Env vars**: `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET`
- **Get keys**: [Snov.io > Integration > API](https://app.snov.io/integration/api)
The CLI handles token acquisition automatically.
## Common Agent Operations
### Search emails by domain
```bash
node tools/clis/snov.js domain search --domain example.com --type all --limit 10
```
### Find a specific person's email
```bash
node tools/clis/snov.js email find --domain example.com --first-name John --last-name Doe
```
### Verify an email
```bash
node tools/clis/snov.js email verify --email john@example.com
```
### Find prospect by email
```bash
node tools/clis/snov.js prospect find --email john@example.com
```
### Add prospect to a list
```bash
node tools/clis/snov.js prospect add --email john@example.com --first-name John --last-name Doe --list-id 12345
```
### Manage prospect lists
```bash
# List all lists
node tools/clis/snov.js lists list
# Get prospects in a list
node tools/clis/snov.js lists prospects --id 12345 --page 1 --per-page 50
```
### Check domain technology stack
```bash
node tools/clis/snov.js technology check --domain example.com
```
### Manage drip campaigns
```bash
# List campaigns
node tools/clis/snov.js drips list
# Get campaign details
node tools/clis/snov.js drips get --id 12345
# Add prospect to drip campaign
node tools/clis/snov.js drips add-prospect --id 12345 --email john@example.com
```
## Rate Limits
- Rate limits vary by plan
- OAuth tokens expire after a set period; CLI handles refresh automatically
## Use Cases
- **Link building**: Find contacts and run automated drip outreach
- **Prospecting**: Build and manage prospect lists
- **Technology research**: Check what tech stack a target domain uses
- **Email verification**: Clean lists before sending