feat: add 22 zero-dependency CLI tools for marketing platforms

Single-file Node.js scripts for every tool in the registry that lacked
a CLI. All follow the same pattern: env var auth, JSON output, consistent
`{tool} <resource> <action>` command structure, zero npm dependencies.

CLIs added: resend, sendgrid, mailchimp, kit, customer-io, ahrefs,
semrush, google-search-console, ga4, mixpanel, amplitude, segment,
adobe-analytics, rewardful, tolt, mention-me, dub, google-ads,
meta-ads, linkedin-ads, tiktok-ads, zapier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Corey Haines 2026-02-17 06:28:46 -08:00
parent a04cb61a57
commit 98bd9ede62
24 changed files with 4232 additions and 22 deletions

View file

@ -14,32 +14,32 @@ Quick reference for AI agents to discover tool capabilities and integration meth
| Tool | Category | API | MCP | CLI | SDK | Guide |
|------|----------|:---:|:---:|:---:|:---:|-------|
| ga4 | Analytics | ✓ | ✓ | - | ✓ | [ga4.md](integrations/ga4.md) |
| mixpanel | Analytics | ✓ | - | - | ✓ | [mixpanel.md](integrations/mixpanel.md) |
| amplitude | Analytics | ✓ | - | - | ✓ | [amplitude.md](integrations/amplitude.md) |
| ga4 | Analytics | ✓ | ✓ | [](clis/ga4.js) | ✓ | [ga4.md](integrations/ga4.md) |
| mixpanel | Analytics | ✓ | - | [](clis/mixpanel.js) | ✓ | [mixpanel.md](integrations/mixpanel.md) |
| amplitude | Analytics | ✓ | - | [](clis/amplitude.js) | ✓ | [amplitude.md](integrations/amplitude.md) |
| posthog | Analytics | ✓ | - | ✓ | ✓ | [posthog.md](integrations/posthog.md) |
| segment | Analytics | ✓ | - | - | ✓ | [segment.md](integrations/segment.md) |
| adobe-analytics | Analytics | ✓ | - | - | ✓ | [adobe-analytics.md](integrations/adobe-analytics.md) |
| google-search-console | SEO | ✓ | - | - | ✓ | [google-search-console.md](integrations/google-search-console.md) |
| semrush | SEO | ✓ | - | - | - | [semrush.md](integrations/semrush.md) |
| ahrefs | SEO | ✓ | - | - | - | [ahrefs.md](integrations/ahrefs.md) |
| segment | Analytics | ✓ | - | [](clis/segment.js) | ✓ | [segment.md](integrations/segment.md) |
| adobe-analytics | Analytics | ✓ | - | [](clis/adobe-analytics.js) | ✓ | [adobe-analytics.md](integrations/adobe-analytics.md) |
| google-search-console | SEO | ✓ | - | [](clis/google-search-console.js) | ✓ | [google-search-console.md](integrations/google-search-console.md) |
| semrush | SEO | ✓ | - | [](clis/semrush.js) | - | [semrush.md](integrations/semrush.md) |
| ahrefs | SEO | ✓ | - | [](clis/ahrefs.js) | - | [ahrefs.md](integrations/ahrefs.md) |
| hubspot | CRM | ✓ | - | ✓ | ✓ | [hubspot.md](integrations/hubspot.md) |
| salesforce | CRM | ✓ | - | ✓ | ✓ | [salesforce.md](integrations/salesforce.md) |
| stripe | Payments | ✓ | ✓ | ✓ | ✓ | [stripe.md](integrations/stripe.md) |
| rewardful | Referral | ✓ | - | - | - | [rewardful.md](integrations/rewardful.md) |
| tolt | Referral | ✓ | - | - | - | [tolt.md](integrations/tolt.md) |
| dub-co | Links | ✓ | - | - | ✓ | [dub-co.md](integrations/dub-co.md) |
| mention-me | Referral | ✓ | - | - | - | [mention-me.md](integrations/mention-me.md) |
| mailchimp | Email | ✓ | ✓ | - | ✓ | [mailchimp.md](integrations/mailchimp.md) |
| customer-io | Email | ✓ | - | - | ✓ | [customer-io.md](integrations/customer-io.md) |
| sendgrid | Email | ✓ | - | - | ✓ | [sendgrid.md](integrations/sendgrid.md) |
| resend | Email | ✓ | ✓ | - | ✓ | [resend.md](integrations/resend.md) |
| kit | Email | ✓ | - | - | ✓ | [kit.md](integrations/kit.md) |
| google-ads | Ads | ✓ | ✓ | - | ✓ | [google-ads.md](integrations/google-ads.md) |
| meta-ads | Ads | ✓ | - | - | ✓ | [meta-ads.md](integrations/meta-ads.md) |
| linkedin-ads | Ads | ✓ | - | - | - | [linkedin-ads.md](integrations/linkedin-ads.md) |
| tiktok-ads | Ads | ✓ | - | - | ✓ | [tiktok-ads.md](integrations/tiktok-ads.md) |
| zapier | Automation | ✓ | ✓ | - | - | [zapier.md](integrations/zapier.md) |
| rewardful | Referral | ✓ | - | [](clis/rewardful.js) | - | [rewardful.md](integrations/rewardful.md) |
| tolt | Referral | ✓ | - | [](clis/tolt.js) | - | [tolt.md](integrations/tolt.md) |
| dub-co | Links | ✓ | - | [](clis/dub.js) | ✓ | [dub-co.md](integrations/dub-co.md) |
| mention-me | Referral | ✓ | - | [](clis/mention-me.js) | - | [mention-me.md](integrations/mention-me.md) |
| mailchimp | Email | ✓ | ✓ | [](clis/mailchimp.js) | ✓ | [mailchimp.md](integrations/mailchimp.md) |
| customer-io | Email | ✓ | - | [](clis/customer-io.js) | ✓ | [customer-io.md](integrations/customer-io.md) |
| sendgrid | Email | ✓ | - | [](clis/sendgrid.js) | ✓ | [sendgrid.md](integrations/sendgrid.md) |
| resend | Email | ✓ | ✓ | [](clis/resend.js) | ✓ | [resend.md](integrations/resend.md) |
| kit | Email | ✓ | - | [](clis/kit.js) | ✓ | [kit.md](integrations/kit.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) |
| tiktok-ads | Ads | ✓ | - | [](clis/tiktok-ads.js) | ✓ | [tiktok-ads.md](integrations/tiktok-ads.md) |
| zapier | Automation | ✓ | ✓ | [](clis/zapier.js) | - | [zapier.md](integrations/zapier.md) |
| shopify | Commerce | ✓ | - | ✓ | ✓ | [shopify.md](integrations/shopify.md) |
| wordpress | CMS | ✓ | - | ✓ | ✓ | [wordpress.md](integrations/wordpress.md) |
| webflow | CMS | ✓ | - | ✓ | ✓ | [webflow.md](integrations/webflow.md) |
@ -160,6 +160,18 @@ E-commerce platforms and content management systems.
---
## CLI Tools
Zero-dependency, single-file Node.js CLIs for tools that don't ship their own. See [`clis/README.md`](clis/README.md) for install instructions and usage.
All CLIs follow a consistent pattern:
- **No dependencies** — Node 18+ only, uses native `fetch`
- **JSON output** — pipe to `jq`, save to file, or use in scripts
- **Env var auth** — set `{TOOL}_API_KEY` and go
- **Consistent commands**`{tool} <resource> <action> [options]`
---
## MCP-Enabled Tools
These tools have Model Context Protocol servers available, enabling direct agent interaction:

120
tools/clis/README.md Normal file
View file

@ -0,0 +1,120 @@
# Marketing CLIs
Zero-dependency, single-file CLI tools for marketing platforms that don't ship their own.
Every CLI is a standalone Node.js script (Node 18+) with no `npm install` required — just `chmod +x` and go.
## Install
### Option 1: Run directly
```bash
node tools/clis/ahrefs.js backlinks list --target example.com
```
### Option 2: Symlink for global access
```bash
# Symlink any CLI you want available globally
ln -sf "$(pwd)/tools/clis/ahrefs.js" ~/.local/bin/ahrefs
ln -sf "$(pwd)/tools/clis/resend.js" ~/.local/bin/resend
# Then use directly
ahrefs backlinks list --target example.com
resend send --from you@example.com --to them@example.com --subject "Hello" --html "<p>Hi</p>"
```
### Option 3: Add the whole directory to PATH
```bash
export PATH="$PATH:/path/to/marketingskills/tools/clis"
```
## Authentication
Every CLI reads credentials from environment variables:
| CLI | Environment Variable |
|-----|---------------------|
| `ahrefs` | `AHREFS_API_KEY` |
| `adobe-analytics` | `ADOBE_CLIENT_ID`, `ADOBE_ACCESS_TOKEN` |
| `amplitude` | `AMPLITUDE_API_KEY`, `AMPLITUDE_SECRET_KEY` |
| `customer-io` | `CUSTOMERIO_APP_KEY` (App API), `CUSTOMERIO_SITE_ID` + `CUSTOMERIO_API_KEY` (Track API) |
| `dub` | `DUB_API_KEY` |
| `ga4` | `GA4_ACCESS_TOKEN` |
| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN` |
| `google-search-console` | `GSC_ACCESS_TOKEN` |
| `kit` | `KIT_API_KEY`, `KIT_API_SECRET` |
| `linkedin-ads` | `LINKEDIN_ACCESS_TOKEN` |
| `mailchimp` | `MAILCHIMP_API_KEY` |
| `mention-me` | `MENTIONME_API_KEY` |
| `meta-ads` | `META_ACCESS_TOKEN` |
| `mixpanel` | `MIXPANEL_TOKEN` (ingestion), `MIXPANEL_API_KEY` + `MIXPANEL_SECRET` (query) |
| `resend` | `RESEND_API_KEY` |
| `rewardful` | `REWARDFUL_API_KEY` |
| `segment` | `SEGMENT_WRITE_KEY` (tracking), `SEGMENT_ACCESS_TOKEN` (profile) |
| `semrush` | `SEMRUSH_API_KEY` |
| `sendgrid` | `SENDGRID_API_KEY` |
| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN` |
| `tolt` | `TOLT_API_KEY` |
| `zapier` | `ZAPIER_API_KEY` |
## Command Pattern
All CLIs follow the same structure:
```
{tool} <resource> <action> [options]
```
Examples:
```bash
ahrefs backlinks list --target example.com --limit 50
semrush keywords overview --phrase "marketing automation" --database us
mailchimp campaigns list --limit 20
resend send --from you@example.com --to them@example.com --subject "Hello" --html "<p>Hi</p>"
dub links create --url https://example.com/landing --key summer-sale
```
## Output
All CLIs output JSON to stdout for easy piping:
```bash
# Pipe to jq
ahrefs backlinks list --target example.com | jq '.backlinks[].url_from'
# Save to file
semrush keywords overview --phrase "saas marketing" --database us > keywords.json
# Use in scripts
DOMAINS=$(rewardful affiliates list | jq -r '.data[].email')
```
## Available CLIs
| CLI | Category | Tool |
|-----|----------|------|
| `resend.js` | Email | [Resend](https://resend.com) |
| `sendgrid.js` | Email | [SendGrid](https://sendgrid.com) |
| `mailchimp.js` | Email | [Mailchimp](https://mailchimp.com) |
| `kit.js` | Email | [Kit](https://kit.com) |
| `customer-io.js` | Email | [Customer.io](https://customer.io) |
| `ahrefs.js` | SEO | [Ahrefs](https://ahrefs.com) |
| `semrush.js` | SEO | [SEMrush](https://semrush.com) |
| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) |
| `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) |
| `mixpanel.js` | Analytics | [Mixpanel](https://mixpanel.com) |
| `amplitude.js` | Analytics | [Amplitude](https://amplitude.com) |
| `segment.js` | Analytics | [Segment](https://segment.com) |
| `adobe-analytics.js` | Analytics | [Adobe Analytics](https://business.adobe.com/products/analytics) |
| `rewardful.js` | Referral | [Rewardful](https://www.getrewardful.com) |
| `tolt.js` | Referral | [Tolt](https://tolt.io) |
| `mention-me.js` | Referral | [Mention Me](https://www.mention-me.com) |
| `dub.js` | Links | [Dub.co](https://dub.co) |
| `google-ads.js` | Ads | [Google Ads](https://ads.google.com) |
| `meta-ads.js` | Ads | [Meta Ads](https://www.facebook.com/business/ads) |
| `linkedin-ads.js` | Ads | [LinkedIn Ads](https://business.linkedin.com/marketing-solutions/ads) |
| `tiktok-ads.js` | Ads | [TikTok Ads](https://ads.tiktok.com) |
| `zapier.js` | Automation | [Zapier](https://zapier.com) |

157
tools/clis/adobe-analytics.js Executable file
View file

@ -0,0 +1,157 @@
#!/usr/bin/env node
const ACCESS_TOKEN = process.env.ADOBE_ACCESS_TOKEN
const CLIENT_ID = process.env.ADOBE_CLIENT_ID
const COMPANY_ID = process.env.ADOBE_COMPANY_ID
if (!ACCESS_TOKEN || !CLIENT_ID || !COMPANY_ID) {
console.error(JSON.stringify({ error: 'ADOBE_ACCESS_TOKEN, ADOBE_CLIENT_ID, and ADOBE_COMPANY_ID environment variables required' }))
process.exit(1)
}
const BASE_URL = `https://analytics.adobe.io/api/${COMPANY_ID}`
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'x-api-key': CLIENT_ID,
'Content-Type': '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 'reportsuites':
switch (sub) {
case 'list':
result = await api('GET', '/reportsuites')
break
default:
result = { error: 'Unknown reportsuites subcommand. Use: list' }
}
break
case 'dimensions':
switch (sub) {
case 'list': {
if (!args.rsid) { result = { error: '--rsid required' }; break }
const params = new URLSearchParams()
params.set('rsid', args.rsid)
result = await api('GET', `/dimensions?${params}`)
break
}
default:
result = { error: 'Unknown dimensions subcommand. Use: list' }
}
break
case 'metrics':
switch (sub) {
case 'list': {
if (!args.rsid) { result = { error: '--rsid required' }; break }
const params = new URLSearchParams()
params.set('rsid', args.rsid)
result = await api('GET', `/metrics?${params}`)
break
}
default:
result = { error: 'Unknown metrics subcommand. Use: list' }
}
break
case 'reports':
switch (sub) {
case 'run': {
if (!args.rsid) { result = { error: '--rsid required' }; break }
if (!args['start-date']) { result = { error: '--start-date required' }; break }
if (!args['end-date']) { result = { error: '--end-date required' }; break }
if (!args.metrics) { result = { error: '--metrics required (comma-separated)' }; break }
const body = {
rsid: args.rsid,
globalFilters: [{
type: 'dateRange',
dateRange: `${args['start-date']}T00:00:00/${args['end-date']}T23:59:59`,
}],
metricContainer: {
metrics: args.metrics.split(',').map(m => ({ id: m.trim() })),
},
}
if (args.dimension) {
body.dimension = args.dimension
}
result = await api('POST', '/reports', body)
break
}
default:
result = { error: 'Unknown reports subcommand. Use: run' }
}
break
case 'segments':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.rsid) params.set('rsid', args.rsid)
result = await api('GET', `/segments?${params}`)
break
}
default:
result = { error: 'Unknown segments subcommand. Use: list' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
reportsuites: 'reportsuites list',
dimensions: 'dimensions list --rsid <report_suite_id>',
metrics: 'metrics list --rsid <report_suite_id>',
reports: 'reports run --rsid <report_suite_id> --start-date <YYYY-MM-DD> --end-date <YYYY-MM-DD> --metrics <metrics> [--dimension <dimension>]',
segments: 'segments list [--rsid <report_suite_id>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

184
tools/clis/ahrefs.js Executable file
View file

@ -0,0 +1,184 @@
#!/usr/bin/env node
const API_KEY = process.env.AHREFS_API_KEY
const BASE_URL = 'https://api.ahrefs.com/v3'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'AHREFS_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
})
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 mode = args.mode || 'domain'
switch (cmd) {
case 'domain-rating':
switch (sub) {
case 'get': {
const params = new URLSearchParams({ target: args.target })
result = await api('GET', `/site-explorer/domain-rating?${params}`)
break
}
default:
result = { error: 'Unknown domain-rating subcommand. Use: get' }
}
break
case 'backlinks':
switch (sub) {
case 'list': {
const params = new URLSearchParams({ target: args.target, mode })
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/site-explorer/backlinks?${params}`)
break
}
default:
result = { error: 'Unknown backlinks subcommand. Use: list' }
}
break
case 'refdomains':
switch (sub) {
case 'list': {
const params = new URLSearchParams({ target: args.target, mode })
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/site-explorer/refdomains?${params}`)
break
}
default:
result = { error: 'Unknown refdomains subcommand. Use: list' }
}
break
case 'keywords':
switch (sub) {
case 'organic': {
const params = new URLSearchParams({ target: args.target, mode })
if (args.country) params.set('country', args.country)
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/site-explorer/organic-keywords?${params}`)
break
}
default:
result = { error: 'Unknown keywords subcommand. Use: organic' }
}
break
case 'top-pages':
switch (sub) {
case 'list': {
const params = new URLSearchParams({ target: args.target, mode })
if (args.country) params.set('country', args.country)
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/site-explorer/top-pages?${params}`)
break
}
default:
result = { error: 'Unknown top-pages subcommand. Use: list' }
}
break
case 'keyword-overview':
switch (sub) {
case 'get': {
const params = new URLSearchParams({ keywords: args.keywords })
if (args.country) params.set('country', args.country)
result = await api('GET', `/keywords-explorer/overview?${params}`)
break
}
default:
result = { error: 'Unknown keyword-overview subcommand. Use: get' }
}
break
case 'keyword-suggestions':
switch (sub) {
case 'get': {
const params = new URLSearchParams({ keyword: args.keyword })
if (args.country) params.set('country', args.country)
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/keywords-explorer/matching-terms?${params}`)
break
}
default:
result = { error: 'Unknown keyword-suggestions subcommand. Use: get' }
}
break
case 'serp':
switch (sub) {
case 'get': {
const params = new URLSearchParams({ keyword: args.keyword })
if (args.country) params.set('country', args.country)
result = await api('GET', `/keywords-explorer/serp-overview?${params}`)
break
}
default:
result = { error: 'Unknown serp subcommand. Use: get' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
'domain-rating': 'domain-rating get --target <domain>',
'backlinks': 'backlinks list --target <domain> [--mode <mode>] [--limit <n>]',
'refdomains': 'refdomains list --target <domain> [--mode <mode>] [--limit <n>]',
'keywords': 'keywords organic --target <domain> [--country <cc>] [--limit <n>]',
'top-pages': 'top-pages list --target <domain> [--country <cc>] [--limit <n>]',
'keyword-overview': 'keyword-overview get --keywords <kw1,kw2> [--country <cc>]',
'keyword-suggestions': 'keyword-suggestions get --keyword <keyword> [--country <cc>] [--limit <n>]',
'serp': 'serp get --keyword <keyword> [--country <cc>]',
'modes': 'domain (default), subdomains, prefix, exact',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

174
tools/clis/amplitude.js Executable file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env node
const API_KEY = process.env.AMPLITUDE_API_KEY
const SECRET_KEY = process.env.AMPLITUDE_SECRET_KEY
const INGESTION_URL = 'https://api2.amplitude.com'
const QUERY_URL = 'https://amplitude.com/api/2'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'AMPLITUDE_API_KEY environment variable required' }))
process.exit(1)
}
async function ingestApi(method, path, body) {
const res = await fetch(`${INGESTION_URL}${path}`, {
method,
headers: { 'Content-Type': '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 }
}
}
async function queryApi(method, path, params) {
if (!SECRET_KEY) {
return { error: 'AMPLITUDE_SECRET_KEY required for query/export operations' }
}
const auth = Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString('base64')
const url = params ? `${QUERY_URL}${path}?${params}` : `${QUERY_URL}${path}`
const res = await fetch(url, {
method,
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
})
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 'track':
switch (sub) {
case 'event': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
if (!args['event-type']) { result = { error: '--event-type required' }; break }
const event = {
user_id: args['user-id'],
event_type: args['event-type'],
}
if (args.properties) {
event.event_properties = JSON.parse(args.properties)
}
result = await ingestApi('POST', '/2/httpapi', {
api_key: API_KEY,
events: [event],
})
break
}
case 'batch': {
if (!args.events) { result = { error: '--events required (JSON array)' }; break }
const events = JSON.parse(args.events)
result = await ingestApi('POST', '/batch', {
api_key: API_KEY,
events,
})
break
}
default:
result = { error: 'Unknown track subcommand. Use: event, batch' }
}
break
case 'users':
switch (sub) {
case 'activity': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
const params = new URLSearchParams()
params.set('user', args['user-id'])
result = await queryApi('GET', '/useractivity', params)
break
}
default:
result = { error: 'Unknown users subcommand. Use: activity' }
}
break
case 'export':
switch (sub) {
case 'events': {
if (!args.start) { result = { error: '--start required (e.g. 20240101T00)' }; break }
if (!args.end) { result = { error: '--end required (e.g. 20240131T23)' }; break }
const params = new URLSearchParams()
params.set('start', args.start)
params.set('end', args.end)
result = await queryApi('GET', '/export', params)
break
}
default:
result = { error: 'Unknown export subcommand. Use: events' }
}
break
case 'retention':
switch (sub) {
case 'get': {
if (!args.start) { result = { error: '--start required (e.g. 20240101)' }; break }
if (!args.end) { result = { error: '--end required (e.g. 20240131)' }; break }
const params = new URLSearchParams()
params.set('start', args.start)
params.set('end', args.end)
if (args.event) {
params.set('e', JSON.stringify({ event_type: args.event }))
}
result = await queryApi('GET', '/retention', params)
break
}
default:
result = { error: 'Unknown retention subcommand. Use: get' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
track: 'track [event --user-id <id> --event-type <type> [--properties <json>] | batch --events <json>]',
users: 'users activity --user-id <id>',
export: 'export events --start <YYYYMMDDThh> --end <YYYYMMDDThh>',
retention: 'retention get --start <YYYYMMDD> --end <YYYYMMDD> [--event <type>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

186
tools/clis/customer-io.js Executable file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env node
const APP_KEY = process.env.CUSTOMERIO_APP_KEY
const SITE_ID = process.env.CUSTOMERIO_SITE_ID
const API_KEY = process.env.CUSTOMERIO_API_KEY
const TRACK_URL = 'https://track.customer.io/api/v1'
const APP_URL = 'https://api.customer.io/v1'
const hasTrackAuth = SITE_ID && API_KEY
const hasAppAuth = APP_KEY
if (!hasTrackAuth && !hasAppAuth) {
console.error(JSON.stringify({ error: 'CUSTOMERIO_APP_KEY (for App API) or CUSTOMERIO_SITE_ID + CUSTOMERIO_API_KEY (for Track API) environment variables required' }))
process.exit(1)
}
const basicAuth = hasTrackAuth ? Buffer.from(`${SITE_ID}:${API_KEY}`).toString('base64') : null
async function trackApi(method, path, body) {
if (!hasTrackAuth) {
return { error: 'Track API requires CUSTOMERIO_SITE_ID and CUSTOMERIO_API_KEY environment variables' }
}
const res = await fetch(`${TRACK_URL}${path}`, {
method,
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': '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 }
}
}
async function appApi(method, path, body) {
if (!hasAppAuth) {
return { error: 'App API requires CUSTOMERIO_APP_KEY environment variable' }
}
const res = await fetch(`${APP_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${APP_KEY}`,
'Content-Type': '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 'customers':
switch (sub) {
case 'identify': {
const customerId = rest[0] || args.id
const body = {}
if (args.email) body.email = args.email
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args['created-at']) body.created_at = parseInt(args['created-at'])
if (args.plan) body.plan = args.plan
if (args.data) Object.assign(body, JSON.parse(args.data))
result = await trackApi('PUT', `/customers/${customerId}`, body)
break
}
case 'get': {
const customerId = rest[0] || args.id
result = await appApi('GET', `/customers/${customerId}/attributes`)
break
}
case 'delete': {
const customerId = rest[0] || args.id
result = await trackApi('DELETE', `/customers/${customerId}`)
break
}
case 'track-event': {
const customerId = rest[0] || args.id
const body = { name: args.name }
if (args.data) body.data = JSON.parse(args.data)
result = await trackApi('POST', `/customers/${customerId}/events`, body)
break
}
default:
result = { error: 'Unknown customers subcommand. Use: identify, get, delete, track-event' }
}
break
case 'campaigns':
switch (sub) {
case 'list':
result = await appApi('GET', '/campaigns')
break
case 'get':
result = await appApi('GET', `/campaigns/${rest[0]}`)
break
case 'metrics':
result = await appApi('GET', `/campaigns/${rest[0]}/metrics`)
break
case 'trigger': {
const body = {}
if (args.emails) body.emails = args.emails.split(',')
if (args.ids) body.ids = args.ids.split(',')
if (args.data) body.data = JSON.parse(args.data)
result = await appApi('POST', `/campaigns/${rest[0]}/triggers`, body)
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get, metrics, trigger' }
}
break
case 'send':
switch (sub) {
case 'email': {
const body = {
transactional_message_id: args['message-id'],
to: args.to,
identifiers: {},
}
if (args['identifier-id']) body.identifiers.id = args['identifier-id']
if (args['identifier-email']) body.identifiers.email = args['identifier-email']
if (args.data) body.message_data = JSON.parse(args.data)
if (args.from) body.from = args.from
if (args.subject) body.subject = args.subject
if (args.body) body.body = args.body
result = await appApi('POST', '/send/email', body)
break
}
default:
result = { error: 'Unknown send subcommand. Use: email' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
customers: 'customers [identify|get|delete|track-event] <customer_id> [--email <email>] [--first-name <name>] [--plan <plan>] [--data <json>] [--name <event>]',
campaigns: 'campaigns [list|get|metrics|trigger] [campaign_id] [--emails <e1,e2>] [--ids <id1,id2>] [--data <json>]',
send: 'send email --message-id <id> --to <email> [--identifier-id <id>] [--identifier-email <email>] [--data <json>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

145
tools/clis/dub.js Executable file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env node
const API_KEY = process.env.DUB_API_KEY
const BASE_URL = 'https://api.dub.co'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'DUB_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'links':
switch (sub) {
case 'create': {
const body = {}
if (args.url) body.url = args.url
if (args.domain) body.domain = args.domain
if (args.key) body.key = args.key
if (args.tags) body.tags = args.tags.split(',')
result = await api('POST', '/links', body)
break
}
case 'list': {
const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain)
if (args.page) params.set('page', args.page)
result = await api('GET', `/links?${params}`)
break
}
case 'get': {
const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain)
if (args.key) params.set('key', args.key)
result = await api('GET', `/links?${params}`)
break
}
case 'update': {
const body = {}
if (args.url) body.url = args.url
if (args.tags) body.tags = args.tags.split(',')
result = await api('PATCH', `/links/${args.id}`, body)
break
}
case 'delete':
result = await api('DELETE', `/links/${args.id}`)
break
case 'bulk-create': {
const links = JSON.parse(args.links || '[]')
result = await api('POST', '/links/bulk', links)
break
}
default:
result = { error: 'Unknown links subcommand. Use: create, list, get, update, delete, bulk-create' }
}
break
case 'analytics':
switch (sub) {
case 'get': {
const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain)
if (args.key) params.set('key', args.key)
if (args.interval) params.set('interval', args.interval)
result = await api('GET', `/analytics?${params}`)
break
}
case 'country': {
const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain)
if (args.key) params.set('key', args.key)
result = await api('GET', `/analytics/country?${params}`)
break
}
case 'device': {
const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain)
if (args.key) params.set('key', args.key)
result = await api('GET', `/analytics/device?${params}`)
break
}
default:
result = { error: 'Unknown analytics subcommand. Use: get, country, device' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
links: 'links [create|list|get|update|delete|bulk-create] [--url <url>] [--domain <domain>] [--key <key>] [--tags <tags>] [--id <id>] [--page <page>] [--links <json>]',
analytics: 'analytics [get|country|device] [--domain <domain>] [--key <key>] [--interval <interval>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

181
tools/clis/ga4.js Executable file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env node
const ACCESS_TOKEN = process.env.GA4_ACCESS_TOKEN
const DATA_API = 'https://analyticsdata.googleapis.com/v1beta'
const ADMIN_API = 'https://analyticsadmin.googleapis.com/v1beta'
const MP_URL = 'https://www.google-analytics.com/mp/collect'
if (!ACCESS_TOKEN) {
console.error(JSON.stringify({ error: 'GA4_ACCESS_TOKEN environment variable required' }))
process.exit(1)
}
async function api(method, baseUrl, path, body) {
const res = await fetch(`${baseUrl}${path}`, {
method,
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': '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 }
}
}
async function mpApi(measurementId, apiSecret, body) {
const params = new URLSearchParams({ measurement_id: measurementId, api_secret: apiSecret })
const res = await fetch(`${MP_URL}?${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const text = await res.text()
if (!text) return { status: res.status, success: res.ok }
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 'reports':
switch (sub) {
case 'run': {
const property = args.property
if (!property) { result = { error: '--property required' }; break }
const body = {
dateRanges: [{
startDate: args['start-date'] || '30daysAgo',
endDate: args['end-date'] || 'today',
}],
}
if (args.dimensions) {
body.dimensions = args.dimensions.split(',').map(d => ({ name: d.trim() }))
}
if (args.metrics) {
body.metrics = args.metrics.split(',').map(m => ({ name: m.trim() }))
}
result = await api('POST', DATA_API, `/properties/${property}:runReport`, body)
break
}
default:
result = { error: 'Unknown reports subcommand. Use: run' }
}
break
case 'realtime':
switch (sub) {
case 'run': {
const property = args.property
if (!property) { result = { error: '--property required' }; break }
const body = {}
if (args.dimensions) {
body.dimensions = args.dimensions.split(',').map(d => ({ name: d.trim() }))
}
if (args.metrics) {
body.metrics = args.metrics.split(',').map(m => ({ name: m.trim() }))
}
result = await api('POST', DATA_API, `/properties/${property}:runRealtimeReport`, body)
break
}
default:
result = { error: 'Unknown realtime subcommand. Use: run' }
}
break
case 'conversions':
switch (sub) {
case 'list': {
const property = args.property
if (!property) { result = { error: '--property required' }; break }
result = await api('GET', ADMIN_API, `/properties/${property}/conversionEvents`)
break
}
case 'create': {
const property = args.property
if (!property) { result = { error: '--property required' }; break }
if (!args['event-name']) { result = { error: '--event-name required' }; break }
result = await api('POST', ADMIN_API, `/properties/${property}/conversionEvents`, {
eventName: args['event-name'],
})
break
}
default:
result = { error: 'Unknown conversions subcommand. Use: list, create' }
}
break
case 'events':
switch (sub) {
case 'send': {
if (!args['measurement-id']) { result = { error: '--measurement-id required' }; break }
if (!args['api-secret']) { result = { error: '--api-secret required' }; break }
if (!args['client-id']) { result = { error: '--client-id required' }; break }
if (!args['event-name']) { result = { error: '--event-name required' }; break }
const eventParams = args.params ? JSON.parse(args.params) : {}
const body = {
client_id: args['client-id'],
events: [{
name: args['event-name'],
params: eventParams,
}],
}
result = await mpApi(args['measurement-id'], args['api-secret'], body)
break
}
default:
result = { error: 'Unknown events subcommand. Use: send' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
reports: 'reports run --property <id> [--start-date <date>] [--end-date <date>] [--dimensions <dims>] [--metrics <metrics>]',
realtime: 'realtime run --property <id> [--dimensions <dims>] [--metrics <metrics>]',
conversions: 'conversions [list|create] --property <id> [--event-name <name>]',
events: 'events send --measurement-id <id> --api-secret <secret> --client-id <id> --event-name <name> [--params <json>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

186
tools/clis/google-ads.js Executable file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env node
const TOKEN = process.env.GOOGLE_ADS_TOKEN
const DEV_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN
const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID
const BASE_URL = 'https://googleads.googleapis.com/v14'
if (!TOKEN || !DEV_TOKEN || !CUSTOMER_ID) {
console.error(JSON.stringify({ error: 'GOOGLE_ADS_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, and GOOGLE_ADS_CUSTOMER_ID environment variables required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${TOKEN}`,
'developer-token': DEV_TOKEN,
'Content-Type': '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 }
}
}
async function gaql(query) {
return api('POST', `/customers/${CUSTOMER_ID}/googleAds:searchStream`, { query })
}
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._
function daysToDateRange(days) {
const d = parseInt(days) || 30
if (d === 7) return 'LAST_7_DAYS'
if (d === 14) return 'LAST_14_DAYS'
if (d === 30) return 'LAST_30_DAYS'
if (d === 90) return 'LAST_90_DAYS'
return `LAST_${d}_DAYS`
}
async function main() {
let result
switch (cmd) {
case 'account':
switch (sub) {
case 'info':
default:
result = await gaql('SELECT customer.id, customer.descriptive_name FROM customer')
}
break
case 'campaigns':
switch (sub) {
case 'list':
result = await gaql('SELECT campaign.id, campaign.name, campaign.status, campaign_budget.amount_micros FROM campaign ORDER BY campaign.id')
break
case 'performance': {
const dateRange = daysToDateRange(args.days)
result = await gaql(`SELECT campaign.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING ${dateRange}`)
break
}
case 'pause': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('POST', `/customers/${CUSTOMER_ID}/campaigns:mutate`, {
operations: [{
update: {
resourceName: `customers/${CUSTOMER_ID}/campaigns/${args.id}`,
status: 'PAUSED',
},
updateMask: 'status',
}],
})
break
}
case 'enable': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('POST', `/customers/${CUSTOMER_ID}/campaigns:mutate`, {
operations: [{
update: {
resourceName: `customers/${CUSTOMER_ID}/campaigns/${args.id}`,
status: 'ENABLED',
},
updateMask: 'status',
}],
})
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, performance, pause, enable' }
}
break
case 'adgroups':
switch (sub) {
case 'performance': {
const dateRange = daysToDateRange(args.days)
const limit = args.limit ? ` LIMIT ${args.limit}` : ''
result = await gaql(`SELECT ad_group.name, metrics.impressions, metrics.clicks, metrics.conversions FROM ad_group WHERE segments.date DURING ${dateRange}${limit}`)
break
}
default:
result = { error: 'Unknown adgroups subcommand. Use: performance' }
}
break
case 'keywords':
switch (sub) {
case 'performance': {
const dateRange = daysToDateRange(args.days)
const limit = args.limit || '50'
result = await gaql(`SELECT ad_group_criterion.keyword.text, metrics.impressions, metrics.clicks, metrics.average_cpc FROM keyword_view WHERE segments.date DURING ${dateRange} ORDER BY metrics.clicks DESC LIMIT ${limit}`)
break
}
default:
result = { error: 'Unknown keywords subcommand. Use: performance' }
}
break
case 'budgets':
switch (sub) {
case 'update': {
if (!args.id || !args.amount) { result = { error: '--id and --amount required' }; break }
const amountMicros = String(Math.round(parseFloat(args.amount) * 1000000))
result = await api('POST', `/customers/${CUSTOMER_ID}/campaignBudgets:mutate`, {
operations: [{
update: {
resourceName: `customers/${CUSTOMER_ID}/campaignBudgets/${args.id}`,
amountMicros,
},
updateMask: 'amountMicros',
}],
})
break
}
default:
result = { error: 'Unknown budgets subcommand. Use: update' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
account: 'account [info]',
campaigns: 'campaigns [list|performance|pause|enable] [--days 30] [--id <id>]',
adgroups: 'adgroups [performance] [--days 30] [--limit <n>]',
keywords: 'keywords [performance] [--days 30] [--limit 50]',
budgets: 'budgets [update] --id <budget_id> --amount <dollars>',
},
}
}
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,161 @@
#!/usr/bin/env node
const ACCESS_TOKEN = process.env.GSC_ACCESS_TOKEN
const BASE_URL = 'https://searchconsole.googleapis.com'
if (!ACCESS_TOKEN) {
console.error(JSON.stringify({ error: 'GSC_ACCESS_TOKEN environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Content-Type': '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._
function getDefaultDates() {
const end = new Date()
end.setDate(end.getDate() - 3)
const start = new Date(end)
start.setDate(start.getDate() - 28)
return {
startDate: start.toISOString().split('T')[0],
endDate: end.toISOString().split('T')[0],
}
}
async function main() {
let result
const siteUrl = args['site-url']
switch (cmd) {
case 'search': {
if (!siteUrl) {
result = { error: '--site-url is required for search commands' }
break
}
const encodedSiteUrl = encodeURIComponent(siteUrl)
const defaults = getDefaultDates()
const body = {
startDate: args['start-date'] || defaults.startDate,
endDate: args['end-date'] || defaults.endDate,
rowLimit: parseInt(args.limit || '100', 10),
}
switch (sub) {
case 'query':
body.dimensions = ['query']
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
break
case 'pages':
body.dimensions = ['page']
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
break
case 'countries':
body.dimensions = ['country', 'query']
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
break
default:
result = { error: 'Unknown search subcommand. Use: query, pages, countries' }
}
break
}
case 'inspect': {
if (!siteUrl) {
result = { error: '--site-url is required for inspect commands' }
break
}
switch (sub) {
case 'url':
result = await api('POST', '/v1/urlInspection/index:inspect', {
inspectionUrl: args.url,
siteUrl: siteUrl,
})
break
default:
result = { error: 'Unknown inspect subcommand. Use: url' }
}
break
}
case 'sitemaps': {
if (!siteUrl) {
result = { error: '--site-url is required for sitemaps commands' }
break
}
const encodedSiteUrl = encodeURIComponent(siteUrl)
switch (sub) {
case 'list':
result = await api('GET', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps`)
break
case 'submit': {
const sitemapUrl = encodeURIComponent(args['sitemap-url'])
result = await api('PUT', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps/${sitemapUrl}`)
if (!result.body && !result.error) {
result = { success: true, message: 'Sitemap submitted successfully' }
}
break
}
default:
result = { error: 'Unknown sitemaps subcommand. Use: list, submit' }
}
break
}
default:
result = {
error: 'Unknown command',
usage: {
'search query': 'search query --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
'search pages': 'search pages --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
'search countries': 'search countries --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
'inspect url': 'inspect url --site-url <url> --url <page-url>',
'sitemaps list': 'sitemaps list --site-url <url>',
'sitemaps submit': 'sitemaps submit --site-url <url> --sitemap-url <sitemap-url>',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

197
tools/clis/kit.js Executable file
View file

@ -0,0 +1,197 @@
#!/usr/bin/env node
const API_SECRET = process.env.KIT_API_SECRET
const API_KEY = process.env.KIT_API_KEY
const BASE_URL = 'https://api.convertkit.com/v3'
if (!API_SECRET && !API_KEY) {
console.error(JSON.stringify({ error: 'KIT_API_SECRET or KIT_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body, useSecret = true) {
const url = new URL(`${BASE_URL}${path}`)
if (method === 'GET' || method === 'DELETE') {
if (useSecret && API_SECRET) {
url.searchParams.set('api_secret', API_SECRET)
} else if (API_KEY) {
url.searchParams.set('api_key', API_KEY)
}
}
const opts = {
method,
headers: { 'Content-Type': 'application/json' },
}
if (body && (method === 'POST' || method === 'PUT')) {
const authBody = { ...body }
if (useSecret && API_SECRET) {
authBody.api_secret = API_SECRET
} else if (API_KEY) {
authBody.api_key = API_KEY
}
opts.body = JSON.stringify(authBody)
}
const res = await fetch(url.toString(), 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 'subscribers':
switch (sub) {
case 'list': {
const params = args.page ? `&page=${args.page}` : ''
result = await api('GET', `/subscribers?${params}`)
break
}
case 'get':
result = await api('GET', `/subscribers/${rest[0]}`)
break
case 'update': {
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
result = await api('PUT', `/subscribers/${rest[0]}`, body)
break
}
case 'unsubscribe': {
const body = { email: args.email }
result = await api('PUT', '/unsubscribe', body)
break
}
default:
result = { error: 'Unknown subscribers subcommand. Use: list, get, update, unsubscribe' }
}
break
case 'forms':
switch (sub) {
case 'list':
result = await api('GET', '/forms', null, false)
break
case 'subscribe': {
const formId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
result = await api('POST', `/forms/${formId}/subscribe`, body, false)
break
}
default:
result = { error: 'Unknown forms subcommand. Use: list, subscribe' }
}
break
case 'sequences':
switch (sub) {
case 'list':
result = await api('GET', '/sequences', null, false)
break
case 'subscribe': {
const sequenceId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
result = await api('POST', `/sequences/${sequenceId}/subscribe`, body, false)
break
}
default:
result = { error: 'Unknown sequences subcommand. Use: list, subscribe' }
}
break
case 'tags':
switch (sub) {
case 'list':
result = await api('GET', '/tags', null, false)
break
case 'subscribe': {
const tagId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
result = await api('POST', `/tags/${tagId}/subscribe`, body, false)
break
}
case 'remove': {
const tagId = rest[0]
const subscriberId = rest[1] || args['subscriber-id']
result = await api('DELETE', `/subscribers/${subscriberId}/tags/${tagId}`)
break
}
default:
result = { error: 'Unknown tags subcommand. Use: list, subscribe, remove' }
}
break
case 'broadcasts':
switch (sub) {
case 'list': {
const params = args.page ? `&page=${args.page}` : ''
result = await api('GET', `/broadcasts?${params}`)
break
}
case 'create': {
const body = {
subject: args.subject,
content: args.content,
}
if (args['email-layout-template']) body.email_layout_template = args['email-layout-template']
result = await api('POST', '/broadcasts', body)
break
}
default:
result = { error: 'Unknown broadcasts subcommand. Use: list, create' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
subscribers: 'subscribers [list|get|update|unsubscribe] [id] [--email <email>] [--first-name <name>] [--fields <json>] [--page <n>]',
forms: 'forms [list|subscribe] [form_id] [--email <email>] [--first-name <name>] [--fields <json>]',
sequences: 'sequences [list|subscribe] [sequence_id] [--email <email>] [--first-name <name>] [--fields <json>]',
tags: 'tags [list|subscribe|remove] [tag_id] [subscriber_id] [--email <email>] [--subscriber-id <id>]',
broadcasts: 'broadcasts [list|create] [--subject <subject>] [--content <html>] [--email-layout-template <template>] [--page <n>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

178
tools/clis/linkedin-ads.js Executable file
View file

@ -0,0 +1,178 @@
#!/usr/bin/env node
const TOKEN = process.env.LINKEDIN_ACCESS_TOKEN
const BASE_URL = 'https://api.linkedin.com/v2'
if (!TOKEN) {
console.error(JSON.stringify({ error: 'LINKEDIN_ACCESS_TOKEN environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': '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 'accounts':
switch (sub) {
case 'list':
result = await api('GET', '/adAccountsV2?q=search')
break
default:
result = { error: 'Unknown accounts subcommand. Use: list' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
if (!args['account-id']) { result = { error: '--account-id required' }; break }
result = await api('GET', `/adCampaignsV2?q=search&search.account.values[0]=urn:li:sponsoredAccount:${args['account-id']}`)
break
}
case 'create': {
if (!args['account-id'] || !args.name) { result = { error: '--account-id and --name required' }; break }
const body = {
account: `urn:li:sponsoredAccount:${args['account-id']}`,
name: args.name,
type: args.type || 'SPONSORED_UPDATES',
costType: args['cost-type'] || 'CPC',
unitCost: {
amount: args['unit-cost'] || '5.00',
currencyCode: 'USD',
},
dailyBudget: {
amount: args['daily-budget'] || '100.00',
currencyCode: 'USD',
},
status: 'PAUSED',
}
result = await api('POST', '/adCampaignsV2', body)
break
}
case 'update': {
if (!args.id || !args.status) { result = { error: '--id and --status required' }; break }
result = await api('POST', `/adCampaignsV2/${args.id}`, {
patch: {
$set: {
status: args.status,
},
},
})
break
}
case 'analytics': {
if (!args.id) { result = { error: '--id required' }; break }
if (!args['start-year'] || !args['start-month'] || !args['start-day'] || !args['end-year'] || !args['end-month'] || !args['end-day']) {
result = { error: '--start-year, --start-month, --start-day, --end-year, --end-month, --end-day required' }
break
}
const params = new URLSearchParams({
q: 'analytics',
pivot: 'CAMPAIGN',
'dateRange.start.year': args['start-year'],
'dateRange.start.month': args['start-month'],
'dateRange.start.day': args['start-day'],
'dateRange.end.year': args['end-year'],
'dateRange.end.month': args['end-month'],
'dateRange.end.day': args['end-day'],
campaigns: `urn:li:sponsoredCampaign:${args.id}`,
fields: 'impressions,clicks,costInLocalCurrency,conversions',
})
result = await api('GET', `/adAnalyticsV2?${params}`)
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, create, update, analytics' }
}
break
case 'creatives':
switch (sub) {
case 'list': {
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
result = await api('GET', `/adCreativesV2?q=search&search.campaign.values[0]=urn:li:sponsoredCampaign:${args['campaign-id']}`)
break
}
default:
result = { error: 'Unknown creatives subcommand. Use: list' }
}
break
case 'audiences':
switch (sub) {
case 'count': {
if (!args.targeting) { result = { error: '--targeting required (JSON string)' }; break }
let targeting
try {
targeting = JSON.parse(args.targeting)
} catch {
result = { error: 'Invalid JSON for --targeting' }
break
}
result = await api('POST', '/audienceCountsV2', { audienceCriteria: targeting })
break
}
default:
result = { error: 'Unknown audiences subcommand. Use: count' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
accounts: 'accounts [list]',
campaigns: 'campaigns [list|create|update|analytics] [--account-id <id>] [--name <name>] [--type SPONSORED_UPDATES] [--cost-type CPC] [--unit-cost 5.00] [--daily-budget 100.00] [--id <id>] [--status ACTIVE|PAUSED]',
creatives: 'creatives [list] --campaign-id <id>',
audiences: 'audiences [count] --targeting <json>',
},
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

208
tools/clis/mailchimp.js Executable file
View file

@ -0,0 +1,208 @@
#!/usr/bin/env node
const API_KEY = process.env.MAILCHIMP_API_KEY
if (!API_KEY) {
console.error(JSON.stringify({ error: 'MAILCHIMP_API_KEY environment variable required' }))
process.exit(1)
}
const dc = API_KEY.split('-').pop()
const BASE_URL = `https://${dc}.api.mailchimp.com/3.0`
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'lists':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.count) params.set('count', args.count)
if (args.offset) params.set('offset', args.offset)
result = await api('GET', `/lists?${params}`)
break
}
case 'get':
result = await api('GET', `/lists/${rest[0]}`)
break
default:
result = { error: 'Unknown lists subcommand. Use: list, get' }
}
break
case 'members':
switch (sub) {
case 'list': {
if (!args['list-id']) {
result = { error: '--list-id is required for members list' }
break
}
const params = new URLSearchParams()
if (args.count) params.set('count', args.count)
if (args.offset) params.set('offset', args.offset)
if (args.status) params.set('status', args.status)
result = await api('GET', `/lists/${args['list-id']}/members?${params}`)
break
}
case 'add': {
if (!args['list-id']) {
result = { error: '--list-id is required for members add' }
break
}
const body = {
email_address: args.email,
status: args.status || 'subscribed',
}
if (args['first-name'] || args['last-name']) {
body.merge_fields = {}
if (args['first-name']) body.merge_fields.FNAME = args['first-name']
if (args['last-name']) body.merge_fields.LNAME = args['last-name']
}
if (args.tags) body.tags = args.tags.split(',')
result = await api('POST', `/lists/${args['list-id']}/members`, body)
break
}
case 'update': {
if (!args['list-id']) {
result = { error: '--list-id is required for members update' }
break
}
const subscriberHash = rest[0]
const body = {}
if (args.status) body.status = args.status
if (args['first-name'] || args['last-name']) {
body.merge_fields = {}
if (args['first-name']) body.merge_fields.FNAME = args['first-name']
if (args['last-name']) body.merge_fields.LNAME = args['last-name']
}
if (args.tags) body.tags = args.tags.split(',')
result = await api('PATCH', `/lists/${args['list-id']}/members/${subscriberHash}`, body)
break
}
default:
result = { error: 'Unknown members subcommand. Use: list, add, update' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.count) params.set('count', args.count)
if (args.offset) params.set('offset', args.offset)
if (args.status) params.set('status', args.status)
if (args.type) params.set('type', args.type)
result = await api('GET', `/campaigns?${params}`)
break
}
case 'get':
result = await api('GET', `/campaigns/${rest[0]}`)
break
case 'create': {
const body = {
type: args.type || 'regular',
recipients: {
list_id: args['list-id'],
},
settings: {},
}
if (args.subject) body.settings.subject_line = args.subject
if (args['from-name']) body.settings.from_name = args['from-name']
if (args['reply-to']) body.settings.reply_to = args['reply-to']
if (args.title) body.settings.title = args.title
result = await api('POST', '/campaigns', body)
break
}
case 'send':
result = await api('POST', `/campaigns/${rest[0]}/actions/send`)
break
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get, create, send' }
}
break
case 'reports':
switch (sub) {
case 'get':
result = await api('GET', `/reports/${rest[0]}`)
break
default:
result = { error: 'Unknown reports subcommand. Use: get' }
}
break
case 'automations':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.count) params.set('count', args.count)
if (args.offset) params.set('offset', args.offset)
result = await api('GET', `/automations?${params}`)
break
}
default:
result = { error: 'Unknown automations subcommand. Use: list' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
lists: 'lists [list|get] [id] [--count <n>] [--offset <n>]',
members: 'members [list|add|update] [subscriber_hash] --list-id <id> [--email <email>] [--status <status>] [--first-name <name>] [--last-name <name>] [--tags <t1,t2>]',
campaigns: 'campaigns [list|get|create|send] [id] [--list-id <id>] [--subject <subject>] [--from-name <name>] [--reply-to <email>]',
reports: 'reports get <campaign_id>',
automations: 'automations list [--count <n>] [--offset <n>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

150
tools/clis/mention-me.js Executable file
View file

@ -0,0 +1,150 @@
#!/usr/bin/env node
const API_KEY = process.env.MENTIONME_API_KEY
const BASE_URL = 'https://api.mention-me.com/api/v2'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'MENTIONME_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'offers':
switch (sub) {
case 'create': {
const body = {}
if (args.email) body.email = args.email
if (args.firstname) body.firstname = args.firstname
if (args.lastname) body.lastname = args.lastname
if (args['order-number']) body.order_number = args['order-number']
if (args['order-total']) body.order_total = Number(args['order-total'])
if (args['order-currency']) body.order_currency = args['order-currency']
result = await api('POST', '/referrer-offer', body)
break
}
default:
result = { error: 'Unknown offers subcommand. Use: create' }
}
break
case 'referrals':
switch (sub) {
case 'get':
result = await api('GET', `/referral/${rest[0]}`)
break
case 'list': {
result = await api('GET', `/referrer/${args['customer-id']}/referrals`)
break
}
default:
result = { error: 'Unknown referrals subcommand. Use: get, list' }
}
break
case 'share-links':
switch (sub) {
case 'get':
result = await api('GET', `/referrer/${args['customer-id']}/share-links`)
break
default:
result = { error: 'Unknown share-links subcommand. Use: get' }
}
break
case 'rewards':
switch (sub) {
case 'get':
result = await api('GET', `/referrer/${args['customer-id']}/rewards`)
break
case 'redeem': {
const body = {}
if (args['reward-id']) body.reward_id = args['reward-id']
if (args['order-number']) body.order_number = args['order-number']
result = await api('POST', `/referrer/${args['customer-id']}/rewards/redeem`, body)
break
}
default:
result = { error: 'Unknown rewards subcommand. Use: get, redeem' }
}
break
case 'referee':
switch (sub) {
case 'create': {
const body = {}
if (args.email) body.email = args.email
if (args.firstname) body.firstname = args.firstname
if (args['referrer-code']) body.referrer_code = args['referrer-code']
if (args['order-number']) body.order_number = args['order-number']
if (args['order-total']) body.order_total = Number(args['order-total'])
result = await api('POST', '/referee', body)
break
}
default:
result = { error: 'Unknown referee subcommand. Use: create' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
offers: 'offers [create] [--email <email>] [--firstname <name>] [--lastname <name>] [--order-number <num>] [--order-total <total>] [--order-currency <currency>]',
referrals: 'referrals [get|list] [id] [--customer-id <id>]',
'share-links': 'share-links [get] [--customer-id <id>]',
rewards: 'rewards [get|redeem] [--customer-id <id>] [--reward-id <id>] [--order-number <num>]',
referee: 'referee [create] [--email <email>] [--firstname <name>] [--referrer-code <code>] [--order-number <num>] [--order-total <total>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

176
tools/clis/meta-ads.js Executable file
View file

@ -0,0 +1,176 @@
#!/usr/bin/env node
const TOKEN = process.env.META_ACCESS_TOKEN
const DEFAULT_ACCOUNT_ID = process.env.META_AD_ACCOUNT_ID
const BASE_URL = 'https://graph.facebook.com/v18.0'
if (!TOKEN) {
console.error(JSON.stringify({ error: 'META_ACCESS_TOKEN environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const separator = path.includes('?') ? '&' : '?'
const url = `${BASE_URL}${path}${separator}access_token=${TOKEN}`
const opts = { method, headers: {} }
if (body) {
opts.headers['Content-Type'] = 'application/json'
opts.body = JSON.stringify(body)
}
const res = await fetch(url, 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._
function getAccountId() {
return args['account-id'] || DEFAULT_ACCOUNT_ID
}
async function main() {
let result
switch (cmd) {
case 'accounts':
switch (sub) {
case 'list':
result = await api('GET', '/me/adaccounts?fields=id,name,account_status')
break
default:
result = { error: 'Unknown accounts subcommand. Use: list' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const accountId = getAccountId()
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
result = await api('GET', `/act_${accountId}/campaigns?fields=id,name,status,objective,daily_budget`)
break
}
case 'insights': {
if (!args.id) { result = { error: '--id required' }; break }
const datePreset = args['date-preset'] || 'last_30d'
result = await api('GET', `/${args.id}/insights?fields=impressions,clicks,spend,actions,cost_per_action_type&date_preset=${datePreset}`)
break
}
case 'create': {
const accountId = getAccountId()
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
if (!args.name || !args.objective) { result = { error: '--name and --objective required' }; break }
const body = {
name: args.name,
objective: args.objective,
status: args.status || 'PAUSED',
special_ad_categories: [],
}
result = await api('POST', `/act_${accountId}/campaigns`, body)
break
}
case 'update': {
if (!args.id || !args.status) { result = { error: '--id and --status required' }; break }
result = await api('POST', `/${args.id}`, { status: args.status })
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, insights, create, update' }
}
break
case 'adsets':
switch (sub) {
case 'list': {
const accountId = getAccountId()
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
result = await api('GET', `/act_${accountId}/adsets?fields=id,name,status,targeting,daily_budget,bid_amount`)
break
}
default:
result = { error: 'Unknown adsets subcommand. Use: list' }
}
break
case 'ads':
switch (sub) {
case 'list': {
if (!args['adset-id']) { result = { error: '--adset-id required' }; break }
result = await api('GET', `/${args['adset-id']}/ads?fields=id,name,status,creative`)
break
}
default:
result = { error: 'Unknown ads subcommand. Use: list' }
}
break
case 'audiences':
switch (sub) {
case 'list': {
const accountId = getAccountId()
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
result = await api('GET', `/act_${accountId}/customaudiences?fields=id,name,approximate_count`)
break
}
case 'create-lookalike': {
const accountId = getAccountId()
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
if (!args['source-id'] || !args.country) { result = { error: '--source-id and --country required' }; break }
result = await api('POST', `/act_${accountId}/customaudiences`, {
name: args.name || 'Lookalike Audience',
subtype: 'LOOKALIKE',
origin_audience_id: args['source-id'],
lookalike_spec: JSON.stringify({ type: 'similarity', country: args.country }),
})
break
}
default:
result = { error: 'Unknown audiences subcommand. Use: list, create-lookalike' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
accounts: 'accounts [list]',
campaigns: 'campaigns [list|insights|create|update] [--account-id <id>] [--id <id>] [--date-preset last_30d] [--name <name>] [--objective <obj>] [--status <status>]',
adsets: 'adsets [list] [--account-id <id>]',
ads: 'ads [list] --adset-id <id>',
audiences: 'audiences [list|create-lookalike] [--account-id <id>] [--source-id <id>] [--country US]',
},
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

227
tools/clis/mixpanel.js Executable file
View file

@ -0,0 +1,227 @@
#!/usr/bin/env node
const TOKEN = process.env.MIXPANEL_TOKEN
const API_KEY = process.env.MIXPANEL_API_KEY
const SECRET = process.env.MIXPANEL_SECRET
const INGESTION_URL = 'https://api.mixpanel.com'
const QUERY_URL = 'https://mixpanel.com/api/2.0'
const EXPORT_URL = 'https://data.mixpanel.com/api/2.0'
if (!TOKEN && !API_KEY) {
console.error(JSON.stringify({ error: 'MIXPANEL_TOKEN (for ingestion) or MIXPANEL_API_KEY + MIXPANEL_SECRET (for query/export) environment variables required' }))
process.exit(1)
}
async function ingestApi(method, path, body) {
const res = await fetch(`${INGESTION_URL}${path}`, {
method,
headers: { 'Content-Type': '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 }
}
}
async function queryApi(method, baseUrl, path, params) {
if (!API_KEY || !SECRET) {
return { error: 'MIXPANEL_API_KEY and MIXPANEL_SECRET required for query/export operations' }
}
const auth = Buffer.from(`${API_KEY}:${SECRET}`).toString('base64')
const url = params ? `${baseUrl}${path}?${params}` : `${baseUrl}${path}`
const res = await fetch(url, {
method,
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
async function queryApiPost(path, body) {
if (!API_KEY || !SECRET) {
return { error: 'MIXPANEL_API_KEY and MIXPANEL_SECRET required for query/export operations' }
}
const auth = Buffer.from(`${API_KEY}:${SECRET}`).toString('base64')
const res = await fetch(`${QUERY_URL}${path}`, {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
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 'track':
switch (sub) {
case 'event': {
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for tracking' }; break }
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
if (!args.event) { result = { error: '--event required' }; break }
const properties = args.properties ? JSON.parse(args.properties) : {}
properties.token = TOKEN
properties.distinct_id = args['distinct-id']
result = await ingestApi('POST', '/track', [{
event: args.event,
properties,
}])
break
}
default:
result = { error: 'Unknown track subcommand. Use: event' }
}
break
case 'profiles':
switch (sub) {
case 'set': {
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for profiles' }; break }
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
const properties = args.properties ? JSON.parse(args.properties) : {}
result = await ingestApi('POST', '/engage', [{
$token: TOKEN,
$distinct_id: args['distinct-id'],
$set: properties,
}])
break
}
default:
result = { error: 'Unknown profiles subcommand. Use: set' }
}
break
case 'query':
switch (sub) {
case 'events': {
if (!args['project-id']) { result = { error: '--project-id required' }; break }
const body = {
project_id: parseInt(args['project-id']),
bookmark_id: null,
params: {
events: [{ event: args.event || 'all' }],
time_range: {
from_date: args['from-date'] || '30daysAgo',
to_date: args['to-date'] || 'today',
},
},
}
result = await queryApiPost('/insights', body)
break
}
default:
result = { error: 'Unknown query subcommand. Use: events' }
}
break
case 'funnels':
switch (sub) {
case 'get': {
if (!args['funnel-id']) { result = { error: '--funnel-id required' }; break }
const params = new URLSearchParams()
params.set('funnel_id', args['funnel-id'])
if (args['from-date']) params.set('from_date', args['from-date'])
if (args['to-date']) params.set('to_date', args['to-date'])
result = await queryApi('GET', QUERY_URL, '/funnels', params)
break
}
default:
result = { error: 'Unknown funnels subcommand. Use: get' }
}
break
case 'retention':
switch (sub) {
case 'get': {
const params = new URLSearchParams()
if (args['from-date']) params.set('from_date', args['from-date'])
if (args['to-date']) params.set('to_date', args['to-date'])
params.set('retention_type', 'birth')
if (args['born-event']) params.set('born_event', args['born-event'])
result = await queryApi('GET', QUERY_URL, '/retention', params)
break
}
default:
result = { error: 'Unknown retention subcommand. Use: get' }
}
break
case 'export':
switch (sub) {
case 'events': {
if (!args['from-date']) { result = { error: '--from-date required' }; break }
if (!args['to-date']) { result = { error: '--to-date required' }; break }
const params = new URLSearchParams()
params.set('from_date', args['from-date'])
params.set('to_date', args['to-date'])
result = await queryApi('GET', EXPORT_URL, '/export', params)
break
}
default:
result = { error: 'Unknown export subcommand. Use: events' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
track: 'track event --distinct-id <id> --event <name> [--properties <json>]',
profiles: 'profiles set --distinct-id <id> [--properties <json>]',
query: 'query events --project-id <id> [--event <name>] [--from-date <date>] [--to-date <date>]',
funnels: 'funnels get --funnel-id <id> [--from-date <date>] [--to-date <date>]',
retention: 'retention get [--from-date <date>] [--to-date <date>] [--born-event <event>]',
export: 'export events --from-date <date> --to-date <date>',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

352
tools/clis/resend.js Executable file
View file

@ -0,0 +1,352 @@
#!/usr/bin/env node
const API_KEY = process.env.RESEND_API_KEY
const BASE_URL = 'https://api.resend.com'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'RESEND_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'send': {
const body = { from: args.from, to: args.to?.split(','), subject: args.subject }
if (args.html) body.html = args.html
if (args.text) body.text = args.text
if (args.cc) body.cc = args.cc.split(',')
if (args.bcc) body.bcc = args.bcc.split(',')
if (args['reply-to']) body.reply_to = args['reply-to']
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
if (args.tags) body.tags = args.tags.split(',').map(t => {
const [name, value] = t.split(':')
return { name, value }
})
result = await api('POST', '/emails', body)
break
}
case 'emails':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/emails?${params}`)
break
}
case 'get':
result = await api('GET', `/emails/${rest[0]}`)
break
case 'cancel':
result = await api('POST', `/emails/${rest[0]}/cancel`)
break
default:
result = { error: 'Unknown emails subcommand. Use: list, get, cancel' }
}
break
case 'domains':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/domains?${params}`)
break
}
case 'get':
result = await api('GET', `/domains/${rest[0]}`)
break
case 'create':
result = await api('POST', '/domains', { name: args.name, region: args.region })
break
case 'verify':
result = await api('POST', `/domains/${rest[0]}/verify`)
break
case 'delete':
result = await api('DELETE', `/domains/${rest[0]}`)
break
default:
result = { error: 'Unknown domains subcommand. Use: list, get, create, verify, delete' }
}
break
case 'api-keys':
switch (sub) {
case 'list':
result = await api('GET', '/api-keys')
break
case 'create': {
const body = { name: args.name }
if (args.permission) body.permission = args.permission
if (args.domain_id) body.domain_id = args.domain_id
result = await api('POST', '/api-keys', body)
break
}
case 'delete':
result = await api('DELETE', `/api-keys/${rest[0]}`)
break
default:
result = { error: 'Unknown api-keys subcommand. Use: list, create, delete' }
}
break
case 'audiences':
switch (sub) {
case 'list':
result = await api('GET', '/audiences')
break
case 'get':
result = await api('GET', `/audiences/${rest[0]}`)
break
case 'create':
result = await api('POST', '/audiences', { name: args.name })
break
case 'delete':
result = await api('DELETE', `/audiences/${rest[0]}`)
break
default:
result = { error: 'Unknown audiences subcommand. Use: list, get, create, delete' }
}
break
case 'contacts': {
const audienceId = sub
const action = rest[0]
const contactId = rest[1]
switch (action) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/audiences/${audienceId}/contacts?${params}`)
break
}
case 'get':
result = await api('GET', `/audiences/${audienceId}/contacts/${contactId}`)
break
case 'create': {
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args.unsubscribed) body.unsubscribed = args.unsubscribed === 'true'
result = await api('POST', `/audiences/${audienceId}/contacts`, body)
break
}
case 'update': {
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args.unsubscribed !== undefined) body.unsubscribed = args.unsubscribed === 'true'
result = await api('PATCH', `/audiences/${audienceId}/contacts/${contactId}`, body)
break
}
case 'delete':
result = await api('DELETE', `/audiences/${audienceId}/contacts/${contactId}`)
break
default:
result = { error: 'Unknown contacts action. Use: list, get, create, update, delete' }
}
break
}
case 'webhooks':
switch (sub) {
case 'list':
result = await api('GET', '/webhooks')
break
case 'get':
result = await api('GET', `/webhooks/${rest[0]}`)
break
case 'create': {
const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced']
result = await api('POST', '/webhooks', { endpoint_url: args.endpoint, events })
break
}
case 'delete':
result = await api('DELETE', `/webhooks/${rest[0]}`)
break
default:
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
}
break
case 'batch': {
const emails = JSON.parse(args.emails || '[]')
result = await api('POST', '/emails/batch', emails)
break
}
case 'templates':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.after) params.set('after', args.after)
if (args.before) params.set('before', args.before)
result = await api('GET', `/templates?${params}`)
break
}
case 'get':
result = await api('GET', `/templates/${rest[0]}`)
break
case 'create': {
const body = { name: args.name, html: args.html }
if (args.alias) body.alias = args.alias
if (args.from) body.from = args.from
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
result = await api('POST', '/templates', body)
break
}
case 'update': {
const body = {}
if (args.name) body.name = args.name
if (args.html) body.html = args.html
if (args.alias) body.alias = args.alias
if (args.from) body.from = args.from
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
result = await api('PATCH', `/templates/${rest[0]}`, body)
break
}
case 'delete':
result = await api('DELETE', `/templates/${rest[0]}`)
break
case 'publish':
result = await api('POST', `/templates/${rest[0]}/publish`)
break
case 'duplicate':
result = await api('POST', `/templates/${rest[0]}/duplicate`)
break
default:
result = { error: 'Unknown templates subcommand. Use: list, get, create, update, delete, publish, duplicate' }
}
break
case 'broadcasts':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/broadcasts?${params}`)
break
}
case 'get':
result = await api('GET', `/broadcasts/${rest[0]}`)
break
case 'create': {
const body = { segment_id: args['segment-id'], from: args.from, subject: args.subject }
if (args.html) body.html = args.html
if (args.text) body.text = args.text
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.name) body.name = args.name
result = await api('POST', '/broadcasts', body)
break
}
case 'send': {
const body = {}
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
result = await api('POST', `/broadcasts/${rest[0]}/send`, body)
break
}
case 'delete':
result = await api('DELETE', `/broadcasts/${rest[0]}`)
break
default:
result = { error: 'Unknown broadcasts subcommand. Use: list, get, create, send, delete' }
}
break
case 'segments':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/segments?${params}`)
break
}
case 'get':
result = await api('GET', `/segments/${rest[0]}`)
break
case 'create':
result = await api('POST', '/segments', { name: args.name })
break
case 'delete':
result = await api('DELETE', `/segments/${rest[0]}`)
break
default:
result = { error: 'Unknown segments subcommand. Use: list, get, create, delete' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
send: 'send --from <email> --to <email> --subject <subject> --html <html>',
emails: 'emails [list|get|cancel] [id]',
domains: 'domains [list|get|create|verify|delete] [id] [--name <name>]',
'api-keys': 'api-keys [list|create|delete] [id] [--name <name>]',
audiences: 'audiences [list|get|create|delete] [id] [--name <name>]',
contacts: 'contacts <audience_id> [list|get|create|update|delete] [contact_id] [--email <email>]',
webhooks: 'webhooks [list|get|create|delete] [id] [--endpoint <url>]',
batch: 'batch --emails <json_array>',
templates: 'templates [list|get|create|update|delete|publish|duplicate] [id] [--name <name>] [--html <html>] [--variables <json>]',
broadcasts: 'broadcasts [list|get|create|send|delete] [id] [--segment-id <id>] [--from <email>] [--subject <subject>]',
segments: 'segments [list|get|create|delete] [id] [--name <name>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

152
tools/clis/rewardful.js Executable file
View file

@ -0,0 +1,152 @@
#!/usr/bin/env node
const API_KEY = process.env.REWARDFUL_API_KEY
const BASE_URL = 'https://api.getrewardful.com/v1'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'REWARDFUL_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'affiliates':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.page) params.set('page', args.page)
result = await api('GET', `/affiliates?${params}`)
break
}
case 'get':
result = await api('GET', `/affiliates/${rest[0]}`)
break
case 'search': {
const params = new URLSearchParams()
if (args.email) params.set('email', args.email)
result = await api('GET', `/affiliates?${params}`)
break
}
case 'update': {
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args['paypal-email']) body.paypal_email = args['paypal-email']
result = await api('PUT', `/affiliates/${args.id}`, body)
break
}
default:
result = { error: 'Unknown affiliates subcommand. Use: list, get, search, update' }
}
break
case 'referrals':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
result = await api('GET', `/referrals?${params}`)
break
}
case 'get': {
const params = new URLSearchParams()
if (args['stripe-customer-id']) params.set('stripe_customer_id', args['stripe-customer-id'])
result = await api('GET', `/referrals?${params}`)
break
}
default:
result = { error: 'Unknown referrals subcommand. Use: list, get' }
}
break
case 'commissions':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
result = await api('GET', `/commissions?${params}`)
break
}
case 'get':
result = await api('GET', `/commissions/${rest[0]}`)
break
default:
result = { error: 'Unknown commissions subcommand. Use: list, get' }
}
break
case 'links':
switch (sub) {
case 'create': {
const body = {}
if (args.token) body.token = args.token
if (args.url) body.url = args.url
result = await api('POST', `/affiliates/${args['affiliate-id']}/links`, body)
break
}
default:
result = { error: 'Unknown links subcommand. Use: create' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
affiliates: 'affiliates [list|get|search|update] [id] [--email <email>] [--id <id>] [--first-name <name>] [--last-name <name>] [--paypal-email <email>]',
referrals: 'referrals [list|get] [--affiliate-id <id>] [--stripe-customer-id <id>]',
commissions: 'commissions [list|get] [id] [--affiliate-id <id>]',
links: 'links [create] [--affiliate-id <id>] [--token <token>] [--url <url>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

179
tools/clis/segment.js Executable file
View file

@ -0,0 +1,179 @@
#!/usr/bin/env node
const WRITE_KEY = process.env.SEGMENT_WRITE_KEY
const ACCESS_TOKEN = process.env.SEGMENT_ACCESS_TOKEN
const TRACKING_URL = 'https://api.segment.io/v1'
const PROFILE_URL = 'https://profiles.segment.com/v1'
if (!WRITE_KEY && !ACCESS_TOKEN) {
console.error(JSON.stringify({ error: 'SEGMENT_WRITE_KEY (for tracking) or SEGMENT_ACCESS_TOKEN (for profiles) environment variable required' }))
process.exit(1)
}
async function trackApi(method, path, body) {
if (!WRITE_KEY) {
return { error: 'SEGMENT_WRITE_KEY required for tracking operations' }
}
const auth = Buffer.from(`${WRITE_KEY}:`).toString('base64')
const res = await fetch(`${TRACKING_URL}${path}`, {
method,
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': '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 }
}
}
async function profileApi(method, path) {
if (!ACCESS_TOKEN) {
return { error: 'SEGMENT_ACCESS_TOKEN required for profile operations' }
}
const auth = Buffer.from(`${ACCESS_TOKEN}:`).toString('base64')
const res = await fetch(`${PROFILE_URL}${path}`, {
method,
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json',
},
})
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 'track':
switch (sub) {
case 'event': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
if (!args.event) { result = { error: '--event required' }; break }
const body = {
userId: args['user-id'],
event: args.event,
}
if (args.properties) body.properties = JSON.parse(args.properties)
result = await trackApi('POST', '/track', body)
break
}
default:
result = { error: 'Unknown track subcommand. Use: event' }
}
break
case 'identify':
switch (sub) {
case 'user': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
const body = { userId: args['user-id'] }
if (args.traits) body.traits = JSON.parse(args.traits)
result = await trackApi('POST', '/identify', body)
break
}
default:
result = { error: 'Unknown identify subcommand. Use: user' }
}
break
case 'page':
switch (sub) {
case 'view': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
const body = { userId: args['user-id'] }
if (args.name) body.name = args.name
if (args.properties) body.properties = JSON.parse(args.properties)
result = await trackApi('POST', '/page', body)
break
}
default:
result = { error: 'Unknown page subcommand. Use: view' }
}
break
case 'batch':
switch (sub) {
case 'send': {
if (!args.events) { result = { error: '--events required (JSON array)' }; break }
const batch = JSON.parse(args.events)
result = await trackApi('POST', '/batch', { batch })
break
}
default:
result = { error: 'Unknown batch subcommand. Use: send' }
}
break
case 'profiles':
switch (sub) {
case 'traits': {
if (!args['space-id']) { result = { error: '--space-id required' }; break }
if (!args['user-id']) { result = { error: '--user-id required' }; break }
result = await profileApi('GET', `/spaces/${args['space-id']}/collections/users/profiles/user_id:${args['user-id']}/traits`)
break
}
case 'events': {
if (!args['space-id']) { result = { error: '--space-id required' }; break }
if (!args['user-id']) { result = { error: '--user-id required' }; break }
result = await profileApi('GET', `/spaces/${args['space-id']}/collections/users/profiles/user_id:${args['user-id']}/events`)
break
}
default:
result = { error: 'Unknown profiles subcommand. Use: traits, events' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
track: 'track event --user-id <id> --event <name> [--properties <json>]',
identify: 'identify user --user-id <id> [--traits <json>]',
page: 'page view --user-id <id> [--name <name>] [--properties <json>]',
batch: 'batch send --events <json_array>',
profiles: 'profiles [traits|events] --space-id <id> --user-id <id>',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

196
tools/clis/semrush.js Executable file
View file

@ -0,0 +1,196 @@
#!/usr/bin/env node
const API_KEY = process.env.SEMRUSH_API_KEY
const BASE_URL = 'https://api.semrush.com/'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'SEMRUSH_API_KEY environment variable required' }))
process.exit(1)
}
function parseCSV(text) {
const lines = text.trim().split('\n')
if (lines.length < 2) return []
const headers = lines[0].split(';')
const rows = []
for (let i = 1; i < lines.length; i++) {
if (!lines[i].trim()) continue
const values = lines[i].split(';')
const row = {}
for (let j = 0; j < headers.length; j++) {
row[headers[j]] = values[j] || ''
}
rows.push(row)
}
return rows
}
async function api(params) {
params.set('key', API_KEY)
params.set('export_escape', '1')
const res = await fetch(`${BASE_URL}?${params}`)
const text = await res.text()
if (!res.ok) {
return { error: text.trim(), status: res.status }
}
if (text.startsWith('ERROR')) {
return { error: text.trim() }
}
return parseCSV(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 database = args.database || 'us'
switch (cmd) {
case 'domain':
switch (sub) {
case 'overview': {
const params = new URLSearchParams({
type: 'domain_ranks',
export_columns: 'Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac',
domain: args.domain,
})
result = await api(params)
break
}
case 'organic': {
const params = new URLSearchParams({
type: 'domain_organic',
export_columns: 'Ph,Po,Pp,Pd,Nq,Cp,Ur,Tr,Tc,Co,Nr',
domain: args.domain,
database,
})
if (args.limit) params.set('display_limit', args.limit)
result = await api(params)
break
}
case 'competitors': {
const params = new URLSearchParams({
type: 'domain_organic_organic',
export_columns: 'Dn,Cr,Np,Or,Ot,Oc,Ad',
domain: args.domain,
database,
})
if (args.limit) params.set('display_limit', args.limit)
result = await api(params)
break
}
default:
result = { error: 'Unknown domain subcommand. Use: overview, organic, competitors' }
}
break
case 'keywords':
switch (sub) {
case 'overview': {
const params = new URLSearchParams({
type: 'phrase_all',
export_columns: 'Ph,Nq,Cp,Co,Nr',
phrase: args.phrase,
database,
})
result = await api(params)
break
}
case 'related': {
const params = new URLSearchParams({
type: 'phrase_related',
export_columns: 'Ph,Nq,Cp,Co,Nr,Td',
phrase: args.phrase,
database,
})
if (args.limit) params.set('display_limit', args.limit)
result = await api(params)
break
}
case 'difficulty': {
const params = new URLSearchParams({
type: 'phrase_kdi',
export_columns: 'Ph,Kd',
phrase: args.phrase,
database,
})
result = await api(params)
break
}
default:
result = { error: 'Unknown keywords subcommand. Use: overview, related, difficulty' }
}
break
case 'backlinks':
switch (sub) {
case 'overview': {
const params = new URLSearchParams({
type: 'backlinks_overview',
target: args.target,
target_type: 'root_domain',
})
result = await api(params)
break
}
case 'list': {
const params = new URLSearchParams({
type: 'backlinks',
target: args.target,
target_type: 'root_domain',
export_columns: 'source_url,source_title,target_url,anchor',
})
if (args.limit) params.set('display_limit', args.limit)
result = await api(params)
break
}
default:
result = { error: 'Unknown backlinks subcommand. Use: overview, list' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
'domain overview': 'domain overview --domain <domain>',
'domain organic': 'domain organic --domain <domain> [--database <db>] [--limit <n>]',
'domain competitors': 'domain competitors --domain <domain> [--database <db>] [--limit <n>]',
'keywords overview': 'keywords overview --phrase <phrase> [--database <db>]',
'keywords related': 'keywords related --phrase <phrase> [--database <db>] [--limit <n>]',
'keywords difficulty': 'keywords difficulty --phrase <phrase> [--database <db>]',
'backlinks overview': 'backlinks overview --target <domain>',
'backlinks list': 'backlinks list --target <domain> [--limit <n>]',
'databases': 'us (default), uk, de, fr, ca, au, etc.',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

205
tools/clis/sendgrid.js Executable file
View file

@ -0,0 +1,205 @@
#!/usr/bin/env node
const API_KEY = process.env.SENDGRID_API_KEY
const BASE_URL = 'https://api.sendgrid.com/v3'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'SENDGRID_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'send': {
const body = {
personalizations: [{
to: args.to.split(',').map(e => ({ email: e.trim() })),
}],
from: { email: args.from },
subject: args.subject,
}
if (args['template-id']) {
body.template_id = args['template-id']
if (args['template-data']) {
body.personalizations[0].dynamic_template_data = JSON.parse(args['template-data'])
}
} else {
const content = []
if (args.text) content.push({ type: 'text/plain', value: args.text })
if (args.html) content.push({ type: 'text/html', value: args.html })
if (content.length > 0) body.content = content
}
if (args.cc) body.personalizations[0].cc = args.cc.split(',').map(e => ({ email: e.trim() }))
if (args.bcc) body.personalizations[0].bcc = args.bcc.split(',').map(e => ({ email: e.trim() }))
if (args['reply-to']) body.reply_to = { email: args['reply-to'] }
result = await api('POST', '/mail/send', body)
break
}
case 'contacts':
switch (sub) {
case 'list':
result = await api('GET', '/marketing/contacts')
break
case 'add': {
const body = {
contacts: [{
email: args.email,
}],
}
if (args['first-name']) body.contacts[0].first_name = args['first-name']
if (args['last-name']) body.contacts[0].last_name = args['last-name']
if (args['list-ids']) body.list_ids = args['list-ids'].split(',')
result = await api('PUT', '/marketing/contacts', body)
break
}
case 'search': {
const body = { query: args.query }
result = await api('POST', '/marketing/contacts/search', body)
break
}
default:
result = { error: 'Unknown contacts subcommand. Use: list, add, search' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('page_size', args.limit)
result = await api('GET', `/marketing/campaigns?${params}`)
break
}
case 'get':
result = await api('GET', `/marketing/campaigns/${rest[0]}`)
break
default:
result = { error: 'Unknown campaigns subcommand. Use: list, get' }
}
break
case 'stats': {
switch (sub) {
case 'get': {
const params = new URLSearchParams()
if (args['start-date']) params.set('start_date', args['start-date'])
if (args['end-date']) params.set('end_date', args['end-date'])
result = await api('GET', `/stats?${params}`)
break
}
default:
result = { error: 'Unknown stats subcommand. Use: get' }
}
break
}
case 'bounces':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['start-time']) params.set('start_time', args['start-time'])
if (args['end-time']) params.set('end_time', args['end-time'])
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/suppression/bounces?${params}`)
break
}
default:
result = { error: 'Unknown bounces subcommand. Use: list' }
}
break
case 'spam-reports':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['start-time']) params.set('start_time', args['start-time'])
if (args['end-time']) params.set('end_time', args['end-time'])
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/suppression/spam_reports?${params}`)
break
}
default:
result = { error: 'Unknown spam-reports subcommand. Use: list' }
}
break
case 'validate':
switch (sub) {
case 'email': {
const body = { email: args.email || rest[0] }
result = await api('POST', '/validations/email', body)
break
}
default: {
const body = { email: sub }
result = await api('POST', '/validations/email', body)
break
}
}
break
default:
result = {
error: 'Unknown command',
usage: {
send: 'send --from <email> --to <email> --subject <subject> --html <html> [--text <text>] [--template-id <id>] [--template-data <json>]',
contacts: 'contacts [list|add|search] [--email <email>] [--first-name <name>] [--last-name <name>] [--list-ids <ids>] [--query <sgql>]',
campaigns: 'campaigns [list|get] [id] [--limit <n>]',
stats: 'stats get [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>]',
bounces: 'bounces list [--start-time <ts>] [--end-time <ts>] [--limit <n>]',
'spam-reports': 'spam-reports list [--start-time <ts>] [--end-time <ts>] [--limit <n>]',
validate: 'validate <email> OR validate email --email <email>',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

184
tools/clis/tiktok-ads.js Executable file
View file

@ -0,0 +1,184 @@
#!/usr/bin/env node
const TOKEN = process.env.TIKTOK_ACCESS_TOKEN
const ADVERTISER_ID = process.env.TIKTOK_ADVERTISER_ID
const BASE_URL = 'https://business-api.tiktok.com/open_api/v1.3'
if (!TOKEN) {
console.error(JSON.stringify({ error: 'TIKTOK_ACCESS_TOKEN environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const opts = {
method,
headers: {
'Access-Token': TOKEN,
'Content-Type': 'application/json',
},
}
if (body && method === 'POST') {
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._
function getAdvertiserId() {
return args['advertiser-id'] || ADVERTISER_ID
}
async function main() {
let result
switch (cmd) {
case 'advertiser':
switch (sub) {
case 'info': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
result = await api('GET', `/advertiser/info/?advertiser_ids=["${advId}"]`)
break
}
default:
result = { error: 'Unknown advertiser subcommand. Use: info' }
}
break
case 'campaigns':
switch (sub) {
case 'list': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
result = await api('GET', `/campaign/get/?advertiser_id=${advId}&page=1&page_size=20`)
break
}
case 'create': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
if (!args.name || !args.objective) { result = { error: '--name and --objective required' }; break }
const body = {
advertiser_id: advId,
campaign_name: args.name,
objective_type: args.objective,
budget_mode: args['budget-mode'] || 'BUDGET_MODE_DAY',
}
if (args.budget) body.budget = parseFloat(args.budget)
result = await api('POST', '/campaign/create/', body)
break
}
case 'update-status': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
if (!args.ids || !args.status) { result = { error: '--ids and --status required' }; break }
result = await api('POST', '/campaign/status/update/', {
advertiser_id: advId,
campaign_ids: args.ids.split(','),
opt_status: args.status,
})
break
}
default:
result = { error: 'Unknown campaigns subcommand. Use: list, create, update-status' }
}
break
case 'adgroups':
switch (sub) {
case 'list': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
let path = `/adgroup/get/?advertiser_id=${advId}`
if (args['campaign-id']) path += `&campaign_ids=["${args['campaign-id']}"]`
result = await api('GET', path)
break
}
default:
result = { error: 'Unknown adgroups subcommand. Use: list' }
}
break
case 'reports':
switch (sub) {
case 'get': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
if (!args['start-date'] || !args['end-date']) { result = { error: '--start-date and --end-date required (YYYY-MM-DD)' }; break }
const body = {
advertiser_id: advId,
report_type: 'BASIC',
dimensions: args.dimensions ? args.dimensions.split(',') : ['campaign_id'],
metrics: args.metrics ? args.metrics.split(',') : ['spend', 'impressions', 'clicks', 'conversion'],
data_level: args['data-level'] || 'AUCTION_CAMPAIGN',
start_date: args['start-date'],
end_date: args['end-date'],
}
result = await api('POST', '/report/integrated/get/', body)
break
}
default:
result = { error: 'Unknown reports subcommand. Use: get' }
}
break
case 'audiences':
switch (sub) {
case 'list': {
const advId = getAdvertiserId()
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
result = await api('GET', `/dmp/custom_audience/list/?advertiser_id=${advId}`)
break
}
default:
result = { error: 'Unknown audiences subcommand. Use: list' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
advertiser: 'advertiser [info]',
campaigns: 'campaigns [list|create|update-status] [--name <name>] [--objective <obj>] [--budget-mode BUDGET_MODE_DAY] [--budget <amount>] [--ids <id1,id2>] [--status ENABLE|DISABLE]',
adgroups: 'adgroups [list] [--campaign-id <id>]',
reports: 'reports [get] --start-date YYYY-MM-DD --end-date YYYY-MM-DD [--dimensions campaign_id] [--metrics spend,impressions,clicks,conversion] [--data-level AUCTION_CAMPAIGN]',
audiences: 'audiences [list]',
},
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

146
tools/clis/tolt.js Executable file
View file

@ -0,0 +1,146 @@
#!/usr/bin/env node
const API_KEY = process.env.TOLT_API_KEY
const BASE_URL = 'https://api.tolt.io/v1'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'TOLT_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': '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 'affiliates':
switch (sub) {
case 'list':
result = await api('GET', '/affiliates')
break
case 'get':
result = await api('GET', `/affiliates/${rest[0]}`)
break
case 'create': {
const body = {}
if (args.email) body.email = args.email
if (args.name) body.name = args.name
result = await api('POST', '/affiliates', body)
break
}
case 'update': {
const body = {}
if (args['commission-rate']) body.commission_rate = Number(args['commission-rate'])
if (args['payout-method']) body.payout_method = args['payout-method']
if (args['paypal-email']) body.paypal_email = args['paypal-email']
result = await api('PATCH', `/affiliates/${args.id}`, body)
break
}
default:
result = { error: 'Unknown affiliates subcommand. Use: list, get, create, update' }
}
break
case 'referrals':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
result = await api('GET', `/referrals?${params}`)
break
}
case 'get': {
const params = new URLSearchParams()
if (args['customer-id']) params.set('customer_id', args['customer-id'])
result = await api('GET', `/referrals?${params}`)
break
}
default:
result = { error: 'Unknown referrals subcommand. Use: list, get' }
}
break
case 'commissions':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
result = await api('GET', `/commissions?${params}`)
break
}
default:
result = { error: 'Unknown commissions subcommand. Use: list' }
}
break
case 'payouts':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
result = await api('GET', `/payouts?${params}`)
break
}
default:
result = { error: 'Unknown payouts subcommand. Use: list' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
affiliates: 'affiliates [list|get|create|update] [id] [--email <email>] [--name <name>] [--id <id>] [--commission-rate <rate>] [--payout-method <method>] [--paypal-email <email>]',
referrals: 'referrals [list|get] [--affiliate-id <id>] [--customer-id <id>]',
commissions: 'commissions [list] [--affiliate-id <id>]',
payouts: 'payouts [list] [--affiliate-id <id>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})

154
tools/clis/zapier.js Executable file
View file

@ -0,0 +1,154 @@
#!/usr/bin/env node
const API_KEY = process.env.ZAPIER_API_KEY
const BASE_URL = 'https://api.zapier.com/v1'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'ZAPIER_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'X-API-Key': API_KEY,
'Content-Type': '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 }
}
}
async function webhookPost(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
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 'zaps':
switch (sub) {
case 'list':
result = await api('GET', '/zaps')
break
case 'get': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('GET', `/zaps/${args.id}`)
break
}
case 'on': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('POST', `/zaps/${args.id}/on`)
break
}
case 'off': {
if (!args.id) { result = { error: '--id required' }; break }
result = await api('POST', `/zaps/${args.id}/off`)
break
}
default:
result = { error: 'Unknown zaps subcommand. Use: list, get, on, off' }
}
break
case 'tasks':
switch (sub) {
case 'list': {
if (!args['zap-id']) { result = { error: '--zap-id required' }; break }
result = await api('GET', `/zaps/${args['zap-id']}/tasks`)
break
}
default:
result = { error: 'Unknown tasks subcommand. Use: list' }
}
break
case 'profile':
switch (sub) {
case 'me':
result = await api('GET', '/profiles/me')
break
default:
result = { error: 'Unknown profile subcommand. Use: me' }
}
break
case 'hooks':
switch (sub) {
case 'send': {
if (!args.url) { result = { error: '--url required' }; break }
if (!args.data) { result = { error: '--data required (JSON string)' }; break }
let data
try {
data = JSON.parse(args.data)
} catch {
result = { error: 'Invalid JSON for --data' }
break
}
result = await webhookPost(args.url, data)
break
}
default:
result = { error: 'Unknown hooks subcommand. Use: send' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
zaps: 'zaps [list|get|on|off] [--id <zap_id>]',
tasks: 'tasks [list] --zap-id <zap_id>',
profile: 'profile [me]',
hooks: 'hooks [send] --url <webhook_url> --data <json>',
},
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})