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:
parent
a04cb61a57
commit
98bd9ede62
24 changed files with 4232 additions and 22 deletions
|
|
@ -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
120
tools/clis/README.md
Normal 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
157
tools/clis/adobe-analytics.js
Executable 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
184
tools/clis/ahrefs.js
Executable 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
174
tools/clis/amplitude.js
Executable 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
186
tools/clis/customer-io.js
Executable 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
145
tools/clis/dub.js
Executable 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
181
tools/clis/ga4.js
Executable 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
186
tools/clis/google-ads.js
Executable 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)
|
||||
})
|
||||
161
tools/clis/google-search-console.js
Executable file
161
tools/clis/google-search-console.js
Executable 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
197
tools/clis/kit.js
Executable 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
178
tools/clis/linkedin-ads.js
Executable 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
208
tools/clis/mailchimp.js
Executable 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
150
tools/clis/mention-me.js
Executable 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
176
tools/clis/meta-ads.js
Executable 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
227
tools/clis/mixpanel.js
Executable 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
352
tools/clis/resend.js
Executable 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
152
tools/clis/rewardful.js
Executable 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
179
tools/clis/segment.js
Executable 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
196
tools/clis/semrush.js
Executable 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
205
tools/clis/sendgrid.js
Executable 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
184
tools/clis/tiktok-ads.js
Executable 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
146
tools/clis/tolt.js
Executable 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
154
tools/clis/zapier.js
Executable 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)
|
||||
})
|
||||
Loading…
Reference in a new issue