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:
@@ -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) |
|
||||
|
||||
Executable
+249
@@ -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)
|
||||
})
|
||||
Executable
+270
@@ -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)
|
||||
})
|
||||
Executable
+221
@@ -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)
|
||||
})
|
||||
Executable
+237
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user