diff --git a/tools/REGISTRY.md b/tools/REGISTRY.md index da8b6c3..76eb2f2 100644 --- a/tools/REGISTRY.md +++ b/tools/REGISTRY.md @@ -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 diff --git a/tools/clis/README.md b/tools/clis/README.md index aad2279..0e38cd9 100644 --- a/tools/clis/README.md +++ b/tools/clis/README.md @@ -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) | diff --git a/tools/clis/hunter.js b/tools/clis/hunter.js new file mode 100755 index 0000000..49ca649 --- /dev/null +++ b/tools/clis/hunter.js @@ -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 [--limit ] [--type personal|generic]', + count: 'domain count --domain [--type personal|generic]', + }, + email: { + find: 'email find --domain --first-name --last-name ', + verify: 'email verify --email ', + }, + account: 'account info', + leads: { + list: 'leads list [--limit ] [--offset ]', + get: 'leads get --id ', + create: 'leads create --email [--first-name ] [--last-name ] [--company ]', + delete: 'leads delete --id ', + }, + campaigns: { + list: 'campaigns list [--limit ] [--offset ]', + get: 'campaigns get --id ', + start: 'campaigns start --id ', + pause: 'campaigns pause --id ', + }, + 'leads-lists': { + list: 'leads-lists list [--limit ] [--offset ]', + get: 'leads-lists get --id ', + }, + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/instantly.js b/tools/clis/instantly.js new file mode 100755 index 0000000..f279fc6 --- /dev/null +++ b/tools/clis/instantly.js @@ -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 ] [--skip ]', + get: 'campaigns get --id ', + status: 'campaigns status --id ', + launch: 'campaigns launch --id ', + pause: 'campaigns pause --id ', + }, + leads: { + list: 'leads list --campaign-id [--limit ] [--skip ]', + add: 'leads add --campaign-id --email [--first-name ] [--last-name ] [--company ]', + delete: 'leads delete --campaign-id --email ', + status: 'leads status --campaign-id --email ', + }, + accounts: { + list: 'accounts list [--limit ] [--skip ]', + status: 'accounts status --account-id ', + 'warmup-status': 'accounts warmup-status --account-id ', + }, + analytics: { + campaign: 'analytics campaign --campaign-id [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]', + steps: 'analytics steps --campaign-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 ', + }, + 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) +}) diff --git a/tools/clis/lemlist.js b/tools/clis/lemlist.js new file mode 100755 index 0000000..9732fb9 --- /dev/null +++ b/tools/clis/lemlist.js @@ -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 | stats --id | export --id ] [--offset 0] [--limit 100]', + leads: 'leads [list | get --email | add --email | delete --email ] --campaign-id [--first-name ] [--last-name ] [--company ]', + unsubscribes: 'unsubscribes [list | add --email | delete --email ] [--offset 0] [--limit 100]', + activities: 'activities list [--campaign-id ] [--type emailsSent|emailsOpened|emailsClicked|emailsReplied|emailsBounced] [--offset 0] [--limit 100]', + hooks: 'hooks [list | create --target-url --event | delete --id ]', + options: '--dry-run --offset --limit ', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/snov.js b/tools/clis/snov.js new file mode 100755 index 0000000..0d47016 --- /dev/null +++ b/tools/clis/snov.js @@ -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 [--type all|personal|generic] [--limit ]', + count: 'domain count --domain ', + }, + email: { + find: 'email find --domain --first-name --last-name ', + verify: 'email verify --email ', + }, + prospect: { + find: 'prospect find --email ', + add: 'prospect add --email [--first-name ] [--last-name ] [--list-id ]', + }, + lists: { + list: 'lists list', + prospects: 'lists prospects --id [--page ] [--per-page ]', + }, + technology: 'technology check --domain ', + drips: { + list: 'drips list', + get: 'drips get --id ', + 'add-prospect': 'drips add-prospect --campaign-id --email [--first-name ] [--last-name ]', + }, + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/integrations/hunter.md b/tools/integrations/hunter.md new file mode 100644 index 0000000..04250e3 --- /dev/null +++ b/tools/integrations/hunter.md @@ -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 diff --git a/tools/integrations/instantly.md b/tools/integrations/instantly.md new file mode 100644 index 0000000..4a18a23 --- /dev/null +++ b/tools/integrations/instantly.md @@ -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 diff --git a/tools/integrations/lemlist.md b/tools/integrations/lemlist.md new file mode 100644 index 0000000..3ee6607 --- /dev/null +++ b/tools/integrations/lemlist.md @@ -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 diff --git a/tools/integrations/snov.md b/tools/integrations/snov.md new file mode 100644 index 0000000..f1cf17e --- /dev/null +++ b/tools/integrations/snov.md @@ -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