diff --git a/.gitignore b/.gitignore index 28fd24c..d0ee8f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,25 @@ +# Dependencies +node_modules/ + +# Environment variables / secrets +.env +.env.* +!.env.example + +# macOS +.DS_Store +**/.DS_Store + +# macOS / iCloud duplicate files +* 2.* +* 2/ + # Remotion video project video/ + +# Editor +*.swp +*.swo +*~ +.idea/ +.vscode/ diff --git a/AGENTS.md b/AGENTS.md index f75797c..e562a60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,10 @@ marketingskills/ ├── skills/ # Agent Skills │ └── skill-name/ │ └── SKILL.md # Required skill file +├── tools/ +│ ├── clis/ # Zero-dependency Node.js CLI tools (51 tools) +│ ├── integrations/ # API integration guides per tool +│ └── REGISTRY.md # Tool index with capabilities ├── CONTRIBUTING.md ├── LICENSE └── README.md @@ -27,14 +31,19 @@ marketingskills/ ## Build / Lint / Test Commands -**Not applicable** - This is a content-only repository with no executable code. - -Verify manually: +**Skills** are content-only (no build step). Verify manually: - YAML frontmatter is valid - `name` field matches directory name exactly - `name` is 1-64 chars, lowercase alphanumeric and hyphens only - `description` is 1-1024 characters +**CLI tools** (`tools/clis/*.js`) are zero-dependency Node.js scripts (Node 18+). Verify with: +```bash +node --check tools/clis/.js # Syntax check +node tools/clis/.js # Show usage (no args = help) +node tools/clis/.js --dry-run # Preview request without sending +``` + ## Agent Skills Specification Skills follow the [Agent Skills spec](https://agentskills.io/specification.md). diff --git a/VERSIONS.md b/VERSIONS.md index 519c29a..47b9523 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -33,6 +33,13 @@ Current versions of all skills. Agents can compare against local versions to che ## Recent Changes +### 2026-02-17 +- Added 51 zero-dependency CLI tools for marketing platforms (`tools/clis/`) +- Added 31 new integration guides (`tools/integrations/`) +- Added 4 email outreach CLIs: hunter, snov, lemlist, instantly +- Security hardening: header auth for meta-ads, URL encoding, input validation +- All CLIs reviewed via independent codex audit (auth, security, error handling, consistency) + ### 2026-01-27 - Initial version tracking added - Added tools registry with 29 integration guides diff --git a/tools/REGISTRY.md b/tools/REGISTRY.md index 6f81b9e..76eb2f2 100644 --- a/tools/REGISTRY.md +++ b/tools/REGISTRY.md @@ -14,32 +14,61 @@ 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) | +| plausible | Analytics | ✓ | - | [✓](clis/plausible.js) | - | [plausible.md](integrations/plausible.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) | +| dataforseo | SEO | ✓ | - | [✓](clis/dataforseo.js) | ✓ | [dataforseo.md](integrations/dataforseo.md) | +| keywords-everywhere | SEO | ✓ | - | [✓](clis/keywords-everywhere.js) | - | [keywords-everywhere.md](integrations/keywords-everywhere.md) | +| clearbit | Data Enrichment | ✓ | - | [✓](clis/clearbit.js) | ✓ | [clearbit.md](integrations/clearbit.md) | +| apollo | Data Enrichment | ✓ | - | [✓](clis/apollo.js) | - | [apollo.md](integrations/apollo.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) | +| paddle | Payments | ✓ | - | [✓](clis/paddle.js) | ✓ | [paddle.md](integrations/paddle.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) | +| partnerstack | Affiliate | ✓ | - | [✓](clis/partnerstack.js) | - | [partnerstack.md](integrations/partnerstack.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) | +| beehiiv | Newsletter | ✓ | - | [✓](clis/beehiiv.js) | - | [beehiiv.md](integrations/beehiiv.md) | +| klaviyo | Email/SMS | ✓ | - | [✓](clis/klaviyo.js) | ✓ | [klaviyo.md](integrations/klaviyo.md) | +| postmark | Email | ✓ | - | [✓](clis/postmark.js) | ✓ | [postmark.md](integrations/postmark.md) | +| brevo | Email/SMS | ✓ | - | [✓](clis/brevo.js) | ✓ | [brevo.md](integrations/brevo.md) | +| activecampaign | Email/CRM | ✓ | - | [✓](clis/activecampaign.js) | ✓ | [activecampaign.md](integrations/activecampaign.md) | +| hunter | Email Outreach | ✓ | - | [✓](clis/hunter.js) | - | [hunter.md](integrations/hunter.md) | +| snov | Email Outreach | ✓ | - | [✓](clis/snov.js) | - | [snov.md](integrations/snov.md) | +| lemlist | Email Outreach | ✓ | - | [✓](clis/lemlist.js) | - | [lemlist.md](integrations/lemlist.md) | +| instantly | Email Outreach | ✓ | - | [✓](clis/instantly.js) | - | [instantly.md](integrations/instantly.md) | +| google-ads | Ads | ✓ | ✓ | [✓](clis/google-ads.js) | ✓ | [google-ads.md](integrations/google-ads.md) | +| meta-ads | Ads | ✓ | - | [✓](clis/meta-ads.js) | ✓ | [meta-ads.md](integrations/meta-ads.md) | +| linkedin-ads | Ads | ✓ | - | [✓](clis/linkedin-ads.js) | - | [linkedin-ads.md](integrations/linkedin-ads.md) | +| tiktok-ads | Ads | ✓ | - | [✓](clis/tiktok-ads.js) | ✓ | [tiktok-ads.md](integrations/tiktok-ads.md) | +| zapier | Automation | ✓ | ✓ | [✓](clis/zapier.js) | - | [zapier.md](integrations/zapier.md) | +| hotjar | CRO | ✓ | - | [✓](clis/hotjar.js) | - | [hotjar.md](integrations/hotjar.md) | +| optimizely | A/B Testing | ✓ | - | [✓](clis/optimizely.js) | ✓ | [optimizely.md](integrations/optimizely.md) | +| calendly | Scheduling | ✓ | - | [✓](clis/calendly.js) | - | [calendly.md](integrations/calendly.md) | +| savvycal | Scheduling | ✓ | - | [✓](clis/savvycal.js) | - | [savvycal.md](integrations/savvycal.md) | +| typeform | Forms | ✓ | - | [✓](clis/typeform.js) | ✓ | [typeform.md](integrations/typeform.md) | +| intercom | Messaging | ✓ | - | [✓](clis/intercom.js) | ✓ | [intercom.md](integrations/intercom.md) | +| buffer | Social | ✓ | - | [✓](clis/buffer.js) | - | [buffer.md](integrations/buffer.md) | +| wistia | Video | ✓ | - | [✓](clis/wistia.js) | - | [wistia.md](integrations/wistia.md) | +| trustpilot | Reviews | ✓ | - | [✓](clis/trustpilot.js) | - | [trustpilot.md](integrations/trustpilot.md) | +| g2 | Reviews | ✓ | - | [✓](clis/g2.js) | - | [g2.md](integrations/g2.md) | +| onesignal | Push | ✓ | - | [✓](clis/onesignal.js) | ✓ | [onesignal.md](integrations/onesignal.md) | +| demio | Webinar | ✓ | - | [✓](clis/demio.js) | - | [demio.md](integrations/demio.md) | +| livestorm | Webinar | ✓ | - | [✓](clis/livestorm.js) | - | [livestorm.md](integrations/livestorm.md) | | shopify | Commerce | ✓ | - | ✓ | ✓ | [shopify.md](integrations/shopify.md) | | wordpress | CMS | ✓ | - | ✓ | ✓ | [wordpress.md](integrations/wordpress.md) | | webflow | CMS | ✓ | - | ✓ | ✓ | [webflow.md](integrations/webflow.md) | @@ -60,8 +89,9 @@ Track user behavior, measure conversions, and analyze marketing performance. | **posthog** | Open-source analytics, session replay | - | | **segment** | Customer data platform, routing | - | | **adobe-analytics** | Enterprise analytics | - | +| **plausible** | Privacy-focused analytics | - | -**Agent recommendation**: Start with GA4 if using Google ecosystem. Use Mixpanel or Amplitude for deeper product analytics. +**Agent recommendation**: Start with GA4 if using Google ecosystem. Use Mixpanel or Amplitude for deeper product analytics. Plausible for privacy-focused sites. ### SEO @@ -72,8 +102,10 @@ Search engine optimization tools for keyword research, rank tracking, and site a | **google-search-console** | Free, authoritative search data | Direct from Google | | **semrush** | Competitive analysis, keyword research | Comprehensive | | **ahrefs** | Backlink analysis, content research | Best for links | +| **dataforseo** | SERP tracking, backlinks, on-page audits | Comprehensive API | +| **keywords-everywhere** | Quick keyword research, traffic estimates | Credit-based | -**Agent recommendation**: Google Search Console is essential (free). Add Semrush or Ahrefs for competitive research. +**Agent recommendation**: Google Search Console is essential (free). Add Semrush or Ahrefs for competitive research. DataForSEO for programmatic SERP data. Keywords Everywhere for quick keyword lookups. ### CRM @@ -94,7 +126,9 @@ Payment processing and subscription management. |------|----------|:-------------:| | **stripe** | SaaS subscriptions, developer-friendly | ✓ | -**Agent recommendation**: Stripe is the default for SaaS and developer-focused products. +| **paddle** | SaaS billing with tax handling | - | + +**Agent recommendation**: Stripe is the default for SaaS. Paddle for built-in tax compliance. ### Referral & Affiliate @@ -106,8 +140,9 @@ Tools for referral programs, affiliate tracking, and partner management. | **tolt** | SaaS affiliate programs | ✓ | | **mention-me** | Enterprise referral programs | ✓ | | **dub-co** | Link tracking, attribution | - | +| **partnerstack** | Enterprise partner programs | ✓ | -**Agent recommendation**: Rewardful or Tolt for Stripe-based SaaS. Dub.co for link attribution. +**Agent recommendation**: Rewardful or Tolt for Stripe-based SaaS. PartnerStack for enterprise partner programs. Dub.co for link attribution. ### Email @@ -120,8 +155,13 @@ Email marketing, transactional email, and automation platforms. | **sendgrid** | Transactional email at scale | - | | **resend** | Developer-friendly transactional | ✓ | | **kit** | Creator/newsletter focused | - | +| **beehiiv** | Newsletter platform | - | +| **klaviyo** | E-commerce email + SMS | - | +| **postmark** | Deliverability-focused transactional | - | +| **brevo** | Email + SMS, popular in EU | - | +| **activecampaign** | Email automation + CRM | - | -**Agent recommendation**: Resend for transactional (dev-friendly). Customer.io for advanced automation. Kit for creators. +**Agent recommendation**: Resend for transactional (dev-friendly). Postmark for deliverability. Customer.io for advanced automation. Kit for creators. Beehiiv for newsletters. Klaviyo for e-commerce email/SMS. ActiveCampaign for email + CRM combo. ### Advertising @@ -146,6 +186,124 @@ Workflow automation and integration platforms. **Agent recommendation**: Zapier for connecting tools without code. +### CRO & A/B Testing + +Conversion rate optimization, heatmaps, and experimentation. + +| Tool | Best For | Notes | +|------|----------|-------| +| **hotjar** | Heatmaps, recordings, surveys | Visual behavior data | +| **optimizely** | A/B testing, feature flags | Enterprise experimentation | + +**Agent recommendation**: Hotjar for understanding user behavior. Optimizely for running experiments. + +### Scheduling + +Booking and appointment scheduling tools. + +| Tool | Best For | Notes | +|------|----------|-------| +| **calendly** | Meeting scheduling, lead gen | Most popular | +| **savvycal** | Personalized scheduling | Developer-friendly | + +**Agent recommendation**: Calendly for general use. SavvyCal for personalized booking experiences. + +### Forms & Surveys + +Form builders and survey platforms. + +| Tool | Best For | Notes | +|------|----------|-------| +| **typeform** | Interactive forms, surveys | Conversational UX | + +**Agent recommendation**: Typeform for engaging forms and surveys. + +### Messaging + +In-app messaging, chat, and customer communication. + +| Tool | Best For | Notes | +|------|----------|-------| +| **intercom** | In-app messaging, support, product tours | Full customer platform | + +**Agent recommendation**: Intercom for in-app messaging and customer support. + +### Social Media + +Social media scheduling, management, and analytics. + +| Tool | Best For | Notes | +|------|----------|-------| +| **buffer** | Social scheduling, analytics | Multi-platform | + +**Agent recommendation**: Buffer for scheduling and analytics across social platforms. + +### Video + +Video hosting, analytics, and engagement. + +| Tool | Best For | Notes | +|------|----------|-------| +| **wistia** | Video hosting, marketing analytics | Best for marketing video | + +**Agent recommendation**: Wistia for marketing video hosting with analytics. + +### Data Enrichment + +Company and person data enrichment for sales and marketing. + +| Tool | Best For | Notes | +|------|----------|-------| +| **clearbit** | Company/person enrichment | Now HubSpot Breeze | +| **apollo** | B2B prospecting, email finding | Large database | + +**Agent recommendation**: Clearbit for enrichment. Apollo for prospecting and outbound. + +### Reviews + +Review management and social proof platforms. + +| Tool | Best For | Notes | +|------|----------|-------| +| **trustpilot** | Consumer business reviews | Most recognized | +| **g2** | Software/B2B reviews | Best for SaaS | + +**Agent recommendation**: Trustpilot for consumer products. G2 for B2B software. + +### Push Notifications + +Push notification delivery platforms. + +| Tool | Best For | Notes | +|------|----------|-------| +| **onesignal** | Multi-channel push notifications | Web + mobile | + +**Agent recommendation**: OneSignal for web and mobile push notifications. + +### Webinar + +Webinar and virtual event platforms. + +| Tool | Best For | Notes | +|------|----------|-------| +| **demio** | Marketing webinars | Simple, focused | +| **livestorm** | Video engagement, webinars | Full event platform | + +**Agent recommendation**: Demio for marketing-focused webinars. Livestorm for full event engagement. + +### Email Outreach + +Cold email outreach and email finding tools for link building and sales prospecting. + +| Tool | Best For | Notes | +|------|----------|-------| +| **hunter** | Email finding, domain search | Largest email database | +| **snov** | Email finding, drip campaigns | Built-in sequences | +| **lemlist** | Cold email campaigns | Personalization features | +| **instantly** | Cold email at scale | Email warmup built-in | + +**Agent recommendation**: Hunter for finding emails. Lemlist or Instantly for sending cold email campaigns. Snov for combined finding + outreach. + ### Commerce & CMS E-commerce platforms and content management systems. @@ -160,6 +318,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} [options]` + +--- + ## MCP-Enabled Tools These tools have Model Context Protocol servers available, enabling direct agent interaction: @@ -189,6 +359,10 @@ To use MCP tools, ensure the appropriate MCP server is configured in your enviro 1. Read [customer-io.md](integrations/customer-io.md) for behavior-based automation 2. Read [resend.md](integrations/resend.md) for transactional email +### Running email outreach for backlinks +1. Read [hunter.md](integrations/hunter.md) for finding emails +2. Read [lemlist.md](integrations/lemlist.md) or [instantly.md](integrations/instantly.md) for sending campaigns + ### Running paid ads 1. Read [google-ads.md](integrations/google-ads.md) for search campaigns 2. Read [meta-ads.md](integrations/meta-ads.md) for social campaigns diff --git a/tools/clis/README.md b/tools/clis/README.md new file mode 100644 index 0000000..792b1b0 --- /dev/null +++ b/tools/clis/README.md @@ -0,0 +1,187 @@ +# 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 "

Hi

" +``` + +### 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 | +|-----|---------------------| +| `activecampaign` | `ACTIVECAMPAIGN_API_KEY`, `ACTIVECAMPAIGN_API_URL` | +| `adobe-analytics` | `ADOBE_ACCESS_TOKEN`, `ADOBE_CLIENT_ID`, `ADOBE_COMPANY_ID` | +| `ahrefs` | `AHREFS_API_KEY` | +| `amplitude` | `AMPLITUDE_API_KEY`, `AMPLITUDE_SECRET_KEY` | +| `apollo` | `APOLLO_API_KEY` | +| `beehiiv` | `BEEHIIV_API_KEY` | +| `brevo` | `BREVO_API_KEY` | +| `buffer` | `BUFFER_API_KEY` | +| `calendly` | `CALENDLY_API_KEY` | +| `clearbit` | `CLEARBIT_API_KEY` | +| `customer-io` | `CUSTOMERIO_APP_KEY` (App API), `CUSTOMERIO_SITE_ID` + `CUSTOMERIO_API_KEY` (Track API) | +| `dataforseo` | `DATAFORSEO_LOGIN`, `DATAFORSEO_PASSWORD` | +| `demio` | `DEMIO_API_KEY`, `DEMIO_API_SECRET` | +| `dub` | `DUB_API_KEY` | +| `g2` | `G2_API_TOKEN` | +| `ga4` | `GA4_ACCESS_TOKEN` | +| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` | +| `google-search-console` | `GSC_ACCESS_TOKEN` | +| `hotjar` | `HOTJAR_CLIENT_ID`, `HOTJAR_CLIENT_SECRET` | +| `intercom` | `INTERCOM_API_KEY` | +| `keywords-everywhere` | `KEYWORDS_EVERYWHERE_API_KEY` | +| `kit` | `KIT_API_KEY`, `KIT_API_SECRET` | +| `klaviyo` | `KLAVIYO_API_KEY` | +| `linkedin-ads` | `LINKEDIN_ACCESS_TOKEN` | +| `livestorm` | `LIVESTORM_API_TOKEN` | +| `mailchimp` | `MAILCHIMP_API_KEY` | +| `mention-me` | `MENTIONME_API_KEY` | +| `meta-ads` | `META_ACCESS_TOKEN`, `META_AD_ACCOUNT_ID` | +| `mixpanel` | `MIXPANEL_TOKEN` (ingestion), `MIXPANEL_API_KEY` + `MIXPANEL_SECRET` (query) | +| `onesignal` | `ONESIGNAL_REST_API_KEY`, `ONESIGNAL_APP_ID` | +| `optimizely` | `OPTIMIZELY_API_KEY` | +| `paddle` | `PADDLE_API_KEY`, `PADDLE_SANDBOX` (optional) | +| `partnerstack` | `PARTNERSTACK_PUBLIC_KEY`, `PARTNERSTACK_SECRET_KEY` | +| `plausible` | `PLAUSIBLE_API_KEY`, `PLAUSIBLE_BASE_URL` (optional, for self-hosted) | +| `postmark` | `POSTMARK_API_KEY` | +| `resend` | `RESEND_API_KEY` | +| `rewardful` | `REWARDFUL_API_KEY` | +| `savvycal` | `SAVVYCAL_API_KEY` | +| `segment` | `SEGMENT_WRITE_KEY` (tracking), `SEGMENT_ACCESS_TOKEN` (profile) | +| `semrush` | `SEMRUSH_API_KEY` | +| `sendgrid` | `SENDGRID_API_KEY` | +| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN`, `TIKTOK_ADVERTISER_ID` | +| `tolt` | `TOLT_API_KEY` | +| `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` | +| `typeform` | `TYPEFORM_API_KEY` | +| `hunter` | `HUNTER_API_KEY` | +| `instantly` | `INSTANTLY_API_KEY` | +| `lemlist` | `LEMLIST_API_KEY` | +| `snov` | `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET` | +| `wistia` | `WISTIA_API_KEY` | +| `zapier` | `ZAPIER_API_KEY` | + +## Security + +**Never hardcode API keys or tokens in scripts.** All CLIs read credentials exclusively from environment variables. + +- Store keys in your shell profile (`~/.zshrc`, `~/.bashrc`) or a `.env` file +- The `.env` file is gitignored — but double-check before committing +- Use `--dry-run` on any command to preview the request without sending it (credentials are masked as `***`) +- If you fork this repo, audit your commits to ensure no secrets are included + +## Command Pattern + +All CLIs follow the same structure: + +``` +{tool} [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 "

Hi

" +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 | +|-----|----------|------| +| `activecampaign.js` | Email/CRM | [ActiveCampaign](https://activecampaign.com) | +| `adobe-analytics.js` | Analytics | [Adobe Analytics](https://business.adobe.com/products/analytics) | +| `ahrefs.js` | SEO | [Ahrefs](https://ahrefs.com) | +| `amplitude.js` | Analytics | [Amplitude](https://amplitude.com) | +| `apollo.js` | Data Enrichment | [Apollo.io](https://apollo.io) | +| `beehiiv.js` | Newsletter | [Beehiiv](https://beehiiv.com) | +| `brevo.js` | Email/SMS | [Brevo](https://brevo.com) | +| `buffer.js` | Social | [Buffer](https://buffer.com) | +| `calendly.js` | Scheduling | [Calendly](https://calendly.com) | +| `clearbit.js` | Data Enrichment | [Clearbit](https://clearbit.com) | +| `customer-io.js` | Email | [Customer.io](https://customer.io) | +| `dataforseo.js` | SEO | [DataForSEO](https://dataforseo.com) | +| `demio.js` | Webinar | [Demio](https://demio.com) | +| `dub.js` | Links | [Dub.co](https://dub.co) | +| `g2.js` | Reviews | [G2](https://g2.com) | +| `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) | +| `google-ads.js` | Ads | [Google Ads](https://ads.google.com) | +| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) | +| `hotjar.js` | CRO | [Hotjar](https://hotjar.com) | +| `hunter.js` | Email Outreach | [Hunter.io](https://hunter.io) | +| `instantly.js` | Email Outreach | [Instantly.ai](https://instantly.ai) | +| `intercom.js` | Messaging | [Intercom](https://intercom.com) | +| `keywords-everywhere.js` | SEO | [Keywords Everywhere](https://keywordseverywhere.com) | +| `kit.js` | Email | [Kit](https://kit.com) | +| `klaviyo.js` | Email/SMS | [Klaviyo](https://klaviyo.com) | +| `lemlist.js` | Email Outreach | [Lemlist](https://lemlist.com) | +| `linkedin-ads.js` | Ads | [LinkedIn Ads](https://business.linkedin.com/marketing-solutions/ads) | +| `livestorm.js` | Webinar | [Livestorm](https://livestorm.co) | +| `mailchimp.js` | Email | [Mailchimp](https://mailchimp.com) | +| `mention-me.js` | Referral | [Mention Me](https://www.mention-me.com) | +| `meta-ads.js` | Ads | [Meta Ads](https://www.facebook.com/business/ads) | +| `mixpanel.js` | Analytics | [Mixpanel](https://mixpanel.com) | +| `onesignal.js` | Push | [OneSignal](https://onesignal.com) | +| `optimizely.js` | A/B Testing | [Optimizely](https://optimizely.com) | +| `paddle.js` | Payments | [Paddle](https://paddle.com) | +| `partnerstack.js` | Affiliate | [PartnerStack](https://partnerstack.com) | +| `plausible.js` | Analytics | [Plausible](https://plausible.io) | +| `postmark.js` | Email | [Postmark](https://postmarkapp.com) | +| `resend.js` | Email | [Resend](https://resend.com) | +| `rewardful.js` | Referral | [Rewardful](https://www.getrewardful.com) | +| `savvycal.js` | Scheduling | [SavvyCal](https://savvycal.com) | +| `segment.js` | Analytics | [Segment](https://segment.com) | +| `semrush.js` | SEO | [SEMrush](https://semrush.com) | +| `sendgrid.js` | Email | [SendGrid](https://sendgrid.com) | +| `snov.js` | Email Outreach | [Snov.io](https://snov.io) | +| `tiktok-ads.js` | Ads | [TikTok Ads](https://ads.tiktok.com) | +| `tolt.js` | Referral | [Tolt](https://tolt.io) | +| `trustpilot.js` | Reviews | [Trustpilot](https://trustpilot.com) | +| `typeform.js` | Forms | [Typeform](https://typeform.com) | +| `wistia.js` | Video | [Wistia](https://wistia.com) | +| `zapier.js` | Automation | [Zapier](https://zapier.com) | diff --git a/tools/clis/activecampaign.js b/tools/clis/activecampaign.js new file mode 100755 index 0000000..80842ca --- /dev/null +++ b/tools/clis/activecampaign.js @@ -0,0 +1,435 @@ +#!/usr/bin/env node + +const API_KEY = process.env.ACTIVECAMPAIGN_API_KEY +const API_URL = process.env.ACTIVECAMPAIGN_API_URL + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'ACTIVECAMPAIGN_API_KEY environment variable required' })) + process.exit(1) +} + +if (!API_URL) { + console.error(JSON.stringify({ error: 'ACTIVECAMPAIGN_API_URL environment variable required (e.g. https://yourname.api-us1.com)' })) + process.exit(1) +} + +const BASE_URL = `${API_URL.replace(/\/$/, '')}/api/3` + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Api-Token': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Api-Token': API_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const limit = args.limit ? Number(args.limit) : 20 + const offset = args.offset ? Number(args.offset) : 0 + + switch (cmd) { + case 'contacts': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.email) params.set('email', args.email) + if (args.search) params.set('search', args.search) + if (args['list-id']) params.set('listid', args['list-id']) + if (args.status) params.set('status', args.status) + result = await api('GET', `/contacts?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/contacts/${id}`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const contact = { email } + if (args['first-name']) contact.firstName = args['first-name'] + if (args['last-name']) contact.lastName = args['last-name'] + if (args.phone) contact.phone = args.phone + result = await api('POST', '/contacts', { contact }) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const contact = {} + if (args.email) contact.email = args.email + if (args['first-name']) contact.firstName = args['first-name'] + if (args['last-name']) contact.lastName = args['last-name'] + if (args.phone) contact.phone = args.phone + result = await api('PUT', `/contacts/${id}`, { contact }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/contacts/${id}`) + break + } + case 'sync': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const contact = { email } + if (args['first-name']) contact.firstName = args['first-name'] + if (args['last-name']) contact.lastName = args['last-name'] + if (args.phone) contact.phone = args.phone + result = await api('POST', '/contact/sync', { contact }) + break + } + default: + result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, delete, sync' } + } + break + + case 'lists': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/lists?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/lists/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const list = { name } + if (args['string-id']) list.stringid = args['string-id'] + if (args['sender-url']) list.sender_url = args['sender-url'] + if (args['sender-reminder']) list.sender_reminder = args['sender-reminder'] + result = await api('POST', '/lists', { list }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/lists/${id}`) + break + } + case 'subscribe': { + const listId = args['list-id'] || args.id + const contactId = args['contact-id'] + if (!listId) { result = { error: '--list-id required' }; break } + if (!contactId) { result = { error: '--contact-id required' }; break } + result = await api('POST', '/contactLists', { + contactList: { list: listId, contact: contactId, status: 1 } + }) + break + } + case 'unsubscribe': { + const listId = args['list-id'] || args.id + const contactId = args['contact-id'] + if (!listId) { result = { error: '--list-id required' }; break } + if (!contactId) { result = { error: '--contact-id required' }; break } + result = await api('POST', '/contactLists', { + contactList: { list: listId, contact: contactId, status: 2 } + }) + break + } + default: + result = { error: 'Unknown lists subcommand. Use: list, get, create, delete, subscribe, unsubscribe' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/campaigns?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${id}`) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get' } + } + break + + case 'deals': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.search) params.set('search', args.search) + if (args.stage) params.set('filters[stage]', args.stage) + if (args.owner) params.set('filters[owner]', args.owner) + result = await api('GET', `/deals?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/deals/${id}`) + break + } + case 'create': { + const title = args.title + if (!title) { result = { error: '--title required' }; break } + const deal = { title } + if (args.value) deal.value = Number(args.value) + if (args.currency) deal.currency = args.currency + if (args.pipeline) deal.group = args.pipeline + if (args.stage) deal.stage = args.stage + if (args.owner) deal.owner = args.owner + if (args['contact-id']) deal.contact = args['contact-id'] + result = await api('POST', '/deals', { deal }) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const deal = {} + if (args.title) deal.title = args.title + if (args.value) deal.value = Number(args.value) + if (args.stage) deal.stage = args.stage + if (args.owner) deal.owner = args.owner + if (args.status) deal.status = Number(args.status) + result = await api('PUT', `/deals/${id}`, { deal }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/deals/${id}`) + break + } + default: + result = { error: 'Unknown deals subcommand. Use: list, get, create, update, delete' } + } + break + + case 'automations': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/automations?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/automations/${id}`) + break + } + case 'add-contact': { + const automationId = args.id + const contactId = args['contact-id'] + if (!automationId) { result = { error: '--id required (automation ID)' }; break } + if (!contactId) { result = { error: '--contact-id required (contact ID, not email)' }; break } + result = await api('POST', '/contactAutomations', { + contactAutomation: { contact: contactId, automation: automationId } + }) + break + } + default: + result = { error: 'Unknown automations subcommand. Use: list, get, add-contact' } + } + break + + case 'tags': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.search) params.set('search', args.search) + result = await api('GET', `/tags?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/tags/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + result = await api('POST', '/tags', { + tag: { tag: name, tagType: args.type || 'contact' } + }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/tags/${id}`) + break + } + case 'add-to-contact': { + const tagId = args['tag-id'] + const contactId = args['contact-id'] + if (!tagId) { result = { error: '--tag-id required' }; break } + if (!contactId) { result = { error: '--contact-id required' }; break } + result = await api('POST', '/contactTags', { + contactTag: { contact: contactId, tag: tagId } + }) + break + } + case 'remove-from-contact': { + const contactTagId = args.id + if (!contactTagId) { result = { error: '--id required (contactTag ID)' }; break } + result = await api('DELETE', `/contactTags/${contactTagId}`) + break + } + default: + result = { error: 'Unknown tags subcommand. Use: list, get, create, delete, add-to-contact, remove-from-contact' } + } + break + + case 'pipelines': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/dealGroups?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/dealGroups/${id}`) + break + } + default: + result = { error: 'Unknown pipelines subcommand. Use: list, get' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/webhooks?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/webhooks/${id}`) + break + } + case 'create': { + const name = args.name + const url = args.url + if (!name) { result = { error: '--name required' }; break } + if (!url) { result = { error: '--url required' }; break } + const events = args.events?.split(',') || ['subscribe'] + const sources = args.sources?.split(',') || ['public', 'admin', 'api', 'system'] + result = await api('POST', '/webhooks', { + webhook: { name, url, events, sources } + }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/webhooks/${id}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' } + } + break + + case 'users': + switch (sub) { + case 'me': + result = await api('GET', '/users/me') + break + case 'list': + result = await api('GET', '/users') + break + default: + result = { error: 'Unknown users subcommand. Use: me, list' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + contacts: 'contacts [list | get --id | create --email | update --id | delete --id | sync --email ]', + lists: 'lists [list | get --id | create --name | delete --id | subscribe --list-id --contact-id | unsubscribe --list-id --contact-id ]', + campaigns: 'campaigns [list | get --id ]', + deals: 'deals [list | get --id | create --title | update --id <id> | delete --id <id>]', + automations: 'automations [list | get --id <id> | add-contact --id <aid> --email <email>]', + tags: 'tags [list | get --id <id> | create --name <name> | delete --id <id> | add-to-contact --tag-id <tid> --contact-id <cid>]', + pipelines: 'pipelines [list | get --id <id>]', + webhooks: 'webhooks [list | get --id <id> | create --name <name> --url <url> | delete --id <id>]', + users: 'users [me | list]', + options: '--limit <n> --offset <n> --search <query> --email <email>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/adobe-analytics.js b/tools/clis/adobe-analytics.js new file mode 100755 index 0000000..a3a19bc --- /dev/null +++ b/tools/clis/adobe-analytics.js @@ -0,0 +1,161 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'x-api-key': '***', 'x-proxy-global-company-id': COMPANY_ID, 'Content-Type': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${ACCESS_TOKEN}`, + 'x-api-key': CLIENT_ID, + 'x-proxy-global-company-id': COMPANY_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) +}) diff --git a/tools/clis/ahrefs.js b/tools/clis/ahrefs.js new file mode 100755 index 0000000..63ec6dd --- /dev/null +++ b/tools/clis/ahrefs.js @@ -0,0 +1,192 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' } } + } + 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': { + if (!args.target) { result = { error: '--target required (domain)' }; break } + 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': { + if (!args.target) { result = { error: '--target required (domain or URL)' }; break } + 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': { + if (!args.target) { result = { error: '--target required (domain or URL)' }; break } + 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': { + if (!args.target) { result = { error: '--target required (domain or URL)' }; break } + 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': { + if (!args.target) { result = { error: '--target required (domain or URL)' }; break } + 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) +}) diff --git a/tools/clis/amplitude.js b/tools/clis/amplitude.js new file mode 100755 index 0000000..a1321f4 --- /dev/null +++ b/tools/clis/amplitude.js @@ -0,0 +1,182 @@ +#!/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) { + if (args['dry-run']) { + const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined + if (maskedBody && maskedBody.api_key) maskedBody.api_key = '***' + return { _dry_run: true, method, url: `${INGESTION_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: maskedBody } + } + 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 url = params ? `${QUERY_URL}${path}?${params}` : `${QUERY_URL}${path}` + if (args['dry-run']) { + return { _dry_run: true, method, url, headers: { 'Authorization': '***', 'Content-Type': 'application/json' } } + } + const auth = Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString('base64') + 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) +}) diff --git a/tools/clis/apollo.js b/tools/clis/apollo.js new file mode 100755 index 0000000..a7967ed --- /dev/null +++ b/tools/clis/apollo.js @@ -0,0 +1,142 @@ +#!/usr/bin/env node + +const API_KEY = process.env.APOLLO_API_KEY +const BASE_URL = 'https://api.apollo.io/api/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'APOLLO_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const authBody = body ? { ...body, api_key: API_KEY } : { api_key: API_KEY } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: { ...authBody, api_key: '***' } } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(authBody), + }) + 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 page = args.page ? Number(args.page) : 1 + const perPage = args['per-page'] ? Number(args['per-page']) : 25 + + switch (cmd) { + case 'people': + switch (sub) { + case 'search': { + const body = { page, per_page: perPage } + if (args.titles) body.person_titles = args.titles.split(',') + if (args.locations) body.person_locations = args.locations.split(',') + if (args.seniorities) body.person_seniorities = args.seniorities.split(',') + if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim()) + if (args.keywords) body.q_keywords = args.keywords + result = await api('POST', '/mixed_people/search', body) + break + } + case 'enrich': { + 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.domain) body.domain = args.domain + if (args.linkedin) body.linkedin_url = args.linkedin + if (!args.email && !args.linkedin && !(args['first-name'] && args.domain)) { + result = { error: '--email, --linkedin, or --first-name + --domain required' } + break + } + result = await api('POST', '/people/match', body) + break + } + case 'bulk-enrich': { + const emails = args.emails?.split(',') + if (!emails) { result = { error: '--emails required (comma-separated)' }; break } + const details = emails.map(email => ({ email: email.trim() })) + result = await api('POST', '/people/bulk_match', { details }) + break + } + default: + result = { error: 'Unknown people subcommand. Use: search, enrich, bulk-enrich' } + } + break + + case 'organizations': + switch (sub) { + case 'search': { + const body = { page, per_page: perPage } + if (args.locations) body.organization_locations = args.locations.split(',') + if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim()) + if (args.keywords) body.q_keywords = args.keywords + result = await api('POST', '/mixed_companies/search', body) + break + } + case 'enrich': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/organizations/enrich', { domain }) + break + } + default: + result = { error: 'Unknown organizations subcommand. Use: search, enrich' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + people: { + search: 'people search [--titles <t1,t2>] [--locations <l1,l2>] [--seniorities <s1,s2>] [--employee-ranges <1,100>] [--keywords <kw>] [--page <n>]', + enrich: 'people enrich --email <email> | --first-name <name> --last-name <name> --domain <domain> | --linkedin <url>', + 'bulk-enrich': 'people bulk-enrich --emails <e1,e2,e3>', + }, + organizations: { + search: 'organizations search [--locations <l1,l2>] [--employee-ranges <1,100>] [--keywords <kw>] [--page <n>]', + enrich: 'organizations enrich --domain <domain>', + }, + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/beehiiv.js b/tools/clis/beehiiv.js new file mode 100755 index 0000000..31ea085 --- /dev/null +++ b/tools/clis/beehiiv.js @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +const API_KEY = process.env.BEEHIIV_API_KEY +const BASE_URL = 'https://api.beehiiv.com/v2' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'BEEHIIV_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const pubId = args.publication || args.pub + const limit = args.limit ? Number(args.limit) : 10 + + switch (cmd) { + case 'publications': + switch (sub) { + case 'list': + result = await api('GET', '/publications') + break + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + result = await api('GET', `/publications/${pubId}`) + break + } + default: + result = { error: 'Unknown publications subcommand. Use: list, get' } + } + break + + case 'subscriptions': + switch (sub) { + case 'list': { + if (!pubId) { result = { error: '--publication required' }; break } + const params = new URLSearchParams() + params.set('limit', String(limit)) + if (args.email) params.set('email', args.email) + if (args.status) params.set('status', args.status) + if (args.tier) params.set('tier', args.tier) + if (args.cursor) params.set('cursor', args.cursor) + if (args.expand) params.set('expand[]', args.expand) + result = await api('GET', `/publications/${pubId}/subscriptions?${params.toString()}`) + break + } + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + const subId = args.id + if (!subId) { result = { error: '--id required' }; break } + result = await api('GET', `/publications/${pubId}/subscriptions/${subId}`) + break + } + case 'create': { + if (!pubId) { result = { error: '--publication required' }; break } + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { email } + if (args['reactivate-existing']) body.reactivate_existing = true + if (args['send-welcome-email']) body.send_welcome_email = true + if (args['utm-source']) body.utm_source = args['utm-source'] + if (args['utm-medium']) body.utm_medium = args['utm-medium'] + if (args['utm-campaign']) body.utm_campaign = args['utm-campaign'] + if (args.tier) body.tier = args.tier + if (args['referring-site']) body.referring_site = args['referring-site'] + result = await api('POST', `/publications/${pubId}/subscriptions`, body) + break + } + case 'update': { + if (!pubId) { result = { error: '--publication required' }; break } + const subId = args.id + if (!subId) { result = { error: '--id required' }; break } + const body = {} + if (args.tier) body.tier = args.tier + result = await api('PUT', `/publications/${pubId}/subscriptions/${subId}`, body) + break + } + case 'delete': { + if (!pubId) { result = { error: '--publication required' }; break } + const subId = args.id + if (!subId) { result = { error: '--id required' }; break } + result = await api('DELETE', `/publications/${pubId}/subscriptions/${subId}`) + break + } + default: + result = { error: 'Unknown subscriptions subcommand. Use: list, get, create, update, delete' } + } + break + + case 'posts': + switch (sub) { + case 'list': { + if (!pubId) { result = { error: '--publication required' }; break } + const params = new URLSearchParams() + params.set('limit', String(limit)) + if (args.status) params.set('status', args.status) + if (args.cursor) params.set('cursor', args.cursor) + result = await api('GET', `/publications/${pubId}/posts?${params.toString()}`) + break + } + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + const postId = args.id + if (!postId) { result = { error: '--id required' }; break } + result = await api('GET', `/publications/${pubId}/posts/${postId}`) + break + } + case 'create': { + if (!pubId) { result = { error: '--publication required' }; break } + const title = args.title + if (!title) { result = { error: '--title required' }; break } + const body = { title } + if (args.subtitle) body.subtitle = args.subtitle + if (args.content) body.content = args.content + if (args.status) body.status = args.status + result = await api('POST', `/publications/${pubId}/posts`, body) + break + } + case 'delete': { + if (!pubId) { result = { error: '--publication required' }; break } + const postId = args.id + if (!postId) { result = { error: '--id required' }; break } + result = await api('DELETE', `/publications/${pubId}/posts/${postId}`) + break + } + default: + result = { error: 'Unknown posts subcommand. Use: list, get, create, delete' } + } + break + + case 'segments': + switch (sub) { + case 'list': { + if (!pubId) { result = { error: '--publication required' }; break } + result = await api('GET', `/publications/${pubId}/segments`) + break + } + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + const segId = args.id + if (!segId) { result = { error: '--id required' }; break } + result = await api('GET', `/publications/${pubId}/segments/${segId}`) + break + } + default: + result = { error: 'Unknown segments subcommand. Use: list, get' } + } + break + + case 'automations': + switch (sub) { + case 'list': { + if (!pubId) { result = { error: '--publication required' }; break } + result = await api('GET', `/publications/${pubId}/automations`) + break + } + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + const autoId = args.id + if (!autoId) { result = { error: '--id required' }; break } + result = await api('GET', `/publications/${pubId}/automations/${autoId}`) + break + } + default: + result = { error: 'Unknown automations subcommand. Use: list, get' } + } + break + + case 'referral-program': + switch (sub) { + case 'get': { + if (!pubId) { result = { error: '--publication required' }; break } + result = await api('GET', `/publications/${pubId}/referral_program`) + break + } + default: + result = { error: 'Unknown referral-program subcommand. Use: get' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + publications: 'publications [list | get --publication <id>]', + subscriptions: 'subscriptions [list | get --id <id> | create --email <email> | update --id <id> | delete --id <id>] --publication <id>', + posts: 'posts [list | get --id <id> | create --title <title> | delete --id <id>] --publication <id>', + segments: 'segments [list | get --id <id>] --publication <id>', + automations: 'automations [list | get --id <id>] --publication <id>', + 'referral-program': 'referral-program [get] --publication <id>', + options: '--publication <id> --limit <n> --email <email> --status <status> --tier <tier>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/brevo.js b/tools/clis/brevo.js new file mode 100755 index 0000000..ec5f935 --- /dev/null +++ b/tools/clis/brevo.js @@ -0,0 +1,368 @@ +#!/usr/bin/env node + +const API_KEY = process.env.BREVO_API_KEY +const BASE_URL = 'https://api.brevo.com/v3' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'BREVO_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'api-key': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'api-key': API_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const limit = args.limit ? Number(args.limit) : 50 + const offset = args.offset ? Number(args.offset) : 0 + + switch (cmd) { + case 'account': + switch (sub) { + case 'get': + result = await api('GET', '/account') + break + default: + result = { error: 'Unknown account subcommand. Use: get' } + } + break + + case 'contacts': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.sort) params.set('sort', args.sort) + result = await api('GET', `/contacts?${params.toString()}`) + break + } + case 'get': { + const id = args.id || args.email + if (!id) { result = { error: '--id or --email required' }; break } + result = await api('GET', `/contacts/${encodeURIComponent(id)}`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { email } + if (args['first-name'] || args['last-name']) { + body.attributes = {} + if (args['first-name']) body.attributes.FIRSTNAME = args['first-name'] + if (args['last-name']) body.attributes.LASTNAME = args['last-name'] + } + if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number) + result = await api('POST', '/contacts', body) + break + } + case 'update': { + const id = args.id || args.email + if (!id) { result = { error: '--id or --email required' }; break } + const body = {} + if (args['first-name'] || args['last-name']) { + body.attributes = {} + if (args['first-name']) body.attributes.FIRSTNAME = args['first-name'] + if (args['last-name']) body.attributes.LASTNAME = args['last-name'] + } + if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number) + if (args['unlink-list-ids']) body.unlinkListIds = args['unlink-list-ids'].split(',').map(Number) + result = await api('PUT', `/contacts/${encodeURIComponent(id)}`, body) + break + } + case 'delete': { + const id = args.id || args.email + if (!id) { result = { error: '--id or --email required' }; break } + result = await api('DELETE', `/contacts/${encodeURIComponent(id)}`) + break + } + case 'import': { + const emails = args.emails?.split(',') + if (!emails) { result = { error: '--emails required (comma-separated)' }; break } + const body = { + jsonBody: emails.map(e => ({ email: e.trim() })), + } + if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number) + result = await api('POST', '/contacts/import', body) + break + } + default: + result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, delete, import' } + } + break + + case 'lists': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.sort) params.set('sort', args.sort) + result = await api('GET', `/contacts/lists?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/contacts/lists/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { name, folderId: args.folder ? Number(args.folder) : 1 } + result = await api('POST', '/contacts/lists', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.folder) body.folderId = Number(args.folder) + result = await api('PUT', `/contacts/lists/${id}`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/contacts/lists/${id}`) + break + } + case 'contacts': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + result = await api('GET', `/contacts/lists/${id}/contacts?${params.toString()}`) + break + } + case 'add-contacts': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const emails = args.emails?.split(',') + if (!emails) { result = { error: '--emails required (comma-separated)' }; break } + result = await api('POST', `/contacts/lists/${id}/contacts/add`, { emails }) + break + } + case 'remove-contacts': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const emails = args.emails?.split(',') + if (!emails) { result = { error: '--emails required (comma-separated)' }; break } + result = await api('POST', `/contacts/lists/${id}/contacts/remove`, { emails }) + break + } + default: + result = { error: 'Unknown lists subcommand. Use: list, get, create, update, delete, contacts, add-contacts, remove-contacts' } + } + break + + case 'email': + switch (sub) { + case 'send': { + const senderEmail = args.from + const to = args.to + const subject = args.subject + if (!senderEmail) { result = { error: '--from required' }; break } + if (!to) { result = { error: '--to required' }; break } + if (!subject) { result = { error: '--subject required' }; break } + const body = { + sender: { email: senderEmail }, + to: to.split(',').map(e => ({ email: e.trim() })), + subject, + } + if (args['sender-name']) body.sender.name = args['sender-name'] + if (args.html) body.htmlContent = args.html + if (args.text) body.textContent = args.text + if (!args.html && !args.text) body.textContent = '' + if (args['reply-to']) body.replyTo = { email: args['reply-to'] } + if (args.tags) body.tags = args.tags.split(',') + result = await api('POST', '/smtp/email', body) + break + } + default: + result = { error: 'Unknown email subcommand. Use: send' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.type) params.set('type', args.type) + if (args.status) params.set('status', args.status) + if (args.sort) params.set('sort', args.sort) + result = await api('GET', `/emailCampaigns?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/emailCampaigns/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { + name, + sender: { email: args.from || '' }, + subject: args.subject || '', + } + if (args['sender-name']) body.sender.name = args['sender-name'] + if (args.html) body.htmlContent = args.html + if (args['list-ids']) body.recipients = { listIds: args['list-ids'].split(',').map(Number) } + result = await api('POST', '/emailCampaigns', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.subject) body.subject = args.subject + if (args.html) body.htmlContent = args.html + result = await api('PUT', `/emailCampaigns/${id}`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/emailCampaigns/${id}`) + break + } + case 'send-now': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/emailCampaigns/${id}/sendNow`) + break + } + case 'send-test': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const emails = args.emails?.split(',') + if (!emails) { result = { error: '--emails required (comma-separated)' }; break } + result = await api('POST', `/emailCampaigns/${id}/sendTest`, { emailTo: emails }) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get, create, update, delete, send-now, send-test' } + } + break + + case 'sms': + switch (sub) { + case 'send': { + const sender = args.from + const recipient = args.to + const content = args.content + if (!sender) { result = { error: '--from required (sender name)' }; break } + if (!recipient) { result = { error: '--to required (phone number)' }; break } + if (!content) { result = { error: '--content required' }; break } + result = await api('POST', '/transactionalSMS/sms', { + sender, + recipient, + content, + type: args.type || 'transactional', + }) + break + } + case 'campaigns': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + params.set('offset', String(offset)) + if (args.status) params.set('status', args.status) + result = await api('GET', `/smsCampaigns?${params.toString()}`) + break + } + default: + result = { error: 'Unknown sms subcommand. Use: send, campaigns' } + } + break + + case 'senders': + switch (sub) { + case 'list': + result = await api('GET', '/senders') + break + case 'create': { + const name = args.name + const email = args.email + if (!name) { result = { error: '--name required' }; break } + if (!email) { result = { error: '--email required' }; break } + result = await api('POST', '/senders', { name, email }) + break + } + default: + result = { error: 'Unknown senders subcommand. Use: list, create' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + account: 'account [get]', + contacts: 'contacts [list | get --email <email> | create --email <email> | update --email <email> | delete --email <email> | import --emails <e1,e2>]', + lists: 'lists [list | get --id <id> | create --name <name> | delete --id <id> | contacts --id <id> | add-contacts --id <id> --emails <e1,e2> | remove-contacts --id <id> --emails <e1,e2>]', + email: 'email [send --from <from> --to <to> --subject <subj>]', + campaigns: 'campaigns [list | get --id <id> | create --name <name> | send-now --id <id> | send-test --id <id> --emails <e1,e2>]', + sms: 'sms [send --from <name> --to <phone> --content <msg> | campaigns]', + senders: 'senders [list | create --name <name> --email <email>]', + options: '--limit <n> --offset <n> --status <status>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/buffer.js b/tools/clis/buffer.js new file mode 100755 index 0000000..63a4c55 --- /dev/null +++ b/tools/clis/buffer.js @@ -0,0 +1,260 @@ +#!/usr/bin/env node + +const API_KEY = process.env.BUFFER_API_KEY +const BASE_URL = 'https://api.bufferapp.com/1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'BUFFER_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const headers = { + 'Authorization': `Bearer ${API_KEY}`, + 'Accept': 'application/json', + } + if (body && method !== 'GET') { + headers['Content-Type'] = 'application/x-www-form-urlencoded' + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, 'Authorization': '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? new URLSearchParams(body).toString() : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +async function apiJson(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'user': + switch (sub) { + case 'info': + result = await api('GET', '/user.json') + break + case 'deauthorize': + result = await api('POST', '/user/deauthorize.json') + break + default: + result = { error: 'Unknown user subcommand. Use: info, deauthorize' } + } + break + + case 'profiles': + switch (sub) { + case 'list': + result = await api('GET', '/profiles.json') + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/profiles/${id}.json`) + break + } + case 'schedules': { + const id = args.id + if (!id) { result = { error: '--id required (profile ID)' }; break } + result = await api('GET', `/profiles/${id}/schedules.json`) + break + } + default: + result = { error: 'Unknown profiles subcommand. Use: list, get, schedules' } + } + break + + case 'updates': + switch (sub) { + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (update ID)' }; break } + result = await api('GET', `/updates/${id}.json`) + break + } + case 'pending': { + const id = args.id + if (!id) { result = { error: '--id required (profile ID)' }; break } + const params = new URLSearchParams() + if (args.page) params.set('page', args.page) + if (args.count) params.set('count', args.count) + if (args.since) params.set('since', args.since) + const qs = params.toString() ? `?${params.toString()}` : '' + result = await api('GET', `/profiles/${id}/updates/pending.json${qs}`) + break + } + case 'sent': { + const id = args.id + if (!id) { result = { error: '--id required (profile ID)' }; break } + const params = new URLSearchParams() + if (args.page) params.set('page', args.page) + if (args.count) params.set('count', args.count) + if (args.since) params.set('since', args.since) + const qs = params.toString() ? `?${params.toString()}` : '' + result = await api('GET', `/profiles/${id}/updates/sent.json${qs}`) + break + } + case 'create': { + const profileIds = args['profile-ids'] + const text = args.text + if (!profileIds) { result = { error: '--profile-ids required (comma-separated)' }; break } + if (!text) { result = { error: '--text required' }; break } + const body = { text } + profileIds.split(',').forEach(id => { + if (!body['profile_ids[]']) body['profile_ids[]'] = [] + }) + const formBody = new URLSearchParams() + formBody.append('text', text) + profileIds.split(',').forEach(id => formBody.append('profile_ids[]', id.trim())) + if (args['scheduled-at']) formBody.append('scheduled_at', args['scheduled-at']) + if (args.now) formBody.append('now', 'true') + if (args.top) formBody.append('top', 'true') + if (args.shorten) formBody.append('shorten', 'true') + if (args['dry-run']) { + result = { _dry_run: true, method: 'POST', url: `${BASE_URL}/updates/create.json`, headers: { 'Authorization': '***', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: formBody.toString() } + break + } + const res = await fetch(`${BASE_URL}/updates/create.json`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: formBody.toString(), + }) + const resText = await res.text() + try { result = JSON.parse(resText) } catch { result = { status: res.status, body: resText } } + break + } + case 'update': { + const id = args.id + const text = args.text + if (!id) { result = { error: '--id required (update ID)' }; break } + if (!text) { result = { error: '--text required' }; break } + const body = { text } + if (args['scheduled-at']) body.scheduled_at = args['scheduled-at'] + result = await api('POST', `/updates/${id}/update.json`, body) + break + } + case 'share': { + const id = args.id + if (!id) { result = { error: '--id required (update ID)' }; break } + result = await api('POST', `/updates/${id}/share.json`) + break + } + case 'destroy': { + const id = args.id + if (!id) { result = { error: '--id required (update ID)' }; break } + result = await api('POST', `/updates/${id}/destroy.json`) + break + } + case 'reorder': { + const id = args.id + const order = args.order + if (!id) { result = { error: '--id required (profile ID)' }; break } + if (!order) { result = { error: '--order required (comma-separated update IDs)' }; break } + const formBody = new URLSearchParams() + order.split(',').forEach(uid => formBody.append('order[]', uid.trim())) + if (args['dry-run']) { + result = { _dry_run: true, method: 'POST', url: `${BASE_URL}/profiles/${id}/updates/reorder.json`, headers: { 'Authorization': '***', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: formBody.toString() } + break + } + const res = await fetch(`${BASE_URL}/profiles/${id}/updates/reorder.json`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: formBody.toString(), + }) + const resText = await res.text() + try { result = JSON.parse(resText) } catch { result = { status: res.status, body: resText } } + break + } + case 'shuffle': { + const id = args.id + if (!id) { result = { error: '--id required (profile ID)' }; break } + result = await api('POST', `/profiles/${id}/updates/shuffle.json`) + break + } + default: + result = { error: 'Unknown updates subcommand. Use: get, pending, sent, create, update, share, destroy, reorder, shuffle' } + } + break + + case 'info': + result = await api('GET', '/info/configuration.json') + break + + default: + result = { + error: 'Unknown command', + usage: { + user: 'user [info | deauthorize]', + profiles: 'profiles [list | get --id <id> | schedules --id <id>]', + updates: 'updates [get --id <id> | pending --id <profile-id> | sent --id <profile-id> | create --profile-ids <ids> --text <text> [--scheduled-at <time>] [--now] | update --id <id> --text <text> | share --id <id> | destroy --id <id> | reorder --id <profile-id> --order <id1,id2> | shuffle --id <profile-id>]', + info: 'info', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/calendly.js b/tools/clis/calendly.js new file mode 100755 index 0000000..d0bec15 --- /dev/null +++ b/tools/clis/calendly.js @@ -0,0 +1,253 @@ +#!/usr/bin/env node + +const API_KEY = process.env.CALENDLY_API_KEY +const BASE_URL = 'https://api.calendly.com' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'CALENDLY_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const count = args.count ? Number(args.count) : 20 + + switch (cmd) { + case 'users': + switch (sub) { + case 'me': + result = await api('GET', '/users/me') + break + default: + result = { error: 'Unknown users subcommand. Use: me' } + } + break + + case 'event-types': + switch (sub) { + case 'list': { + const user = args.user + const org = args.organization + if (!user && !org) { result = { error: '--user or --organization URI required' }; break } + const params = new URLSearchParams() + if (user) params.set('user', user) + if (org) params.set('organization', org) + if (args.active) params.set('active', args.active) + params.set('count', String(count)) + if (args['page-token']) params.set('page_token', args['page-token']) + result = await api('GET', `/event_types?${params}`) + break + } + case 'get': { + const uuid = args.uuid + if (!uuid) { result = { error: '--uuid required' }; break } + result = await api('GET', `/event_types/${uuid}`) + break + } + default: + result = { error: 'Unknown event-types subcommand. Use: list, get' } + } + break + + case 'events': + switch (sub) { + case 'list': { + const user = args.user + const org = args.organization + if (!user && !org) { result = { error: '--user or --organization URI required' }; break } + const params = new URLSearchParams() + if (user) params.set('user', user) + if (org) params.set('organization', org) + if (args['min-start']) params.set('min_start_time', args['min-start']) + if (args['max-start']) params.set('max_start_time', args['max-start']) + if (args.status) params.set('status', args.status) + params.set('count', String(count)) + if (args['page-token']) params.set('page_token', args['page-token']) + if (args.sort) params.set('sort', args.sort) + result = await api('GET', `/scheduled_events?${params}`) + break + } + case 'get': { + const uuid = args.uuid + if (!uuid) { result = { error: '--uuid required' }; break } + result = await api('GET', `/scheduled_events/${uuid}`) + break + } + case 'cancel': { + const uuid = args.uuid + if (!uuid) { result = { error: '--uuid required' }; break } + const body = {} + if (args.reason) body.reason = args.reason + result = await api('POST', `/scheduled_events/${uuid}/cancellation`, body) + break + } + case 'invitees': { + const uuid = args.uuid + if (!uuid) { result = { error: '--uuid required (event UUID)' }; break } + const params = new URLSearchParams() + params.set('count', String(count)) + if (args['page-token']) params.set('page_token', args['page-token']) + if (args.email) params.set('email', args.email) + if (args.status) params.set('status', args.status) + result = await api('GET', `/scheduled_events/${uuid}/invitees?${params}`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list, get, cancel, invitees' } + } + break + + case 'availability': + switch (sub) { + case 'times': { + const eventType = args['event-type'] + if (!eventType) { result = { error: '--event-type URI required' }; break } + const startTime = args['start-time'] + const endTime = args['end-time'] + if (!startTime || !endTime) { result = { error: '--start-time and --end-time required (ISO 8601)' }; break } + const params = new URLSearchParams({ + event_type: eventType, + start_time: startTime, + end_time: endTime, + }) + result = await api('GET', `/event_type_available_times?${params}`) + break + } + case 'busy': { + const user = args.user + if (!user) { result = { error: '--user URI required' }; break } + const startTime = args['start-time'] + const endTime = args['end-time'] + if (!startTime || !endTime) { result = { error: '--start-time and --end-time required (ISO 8601)' }; break } + const params = new URLSearchParams({ + user, + start_time: startTime, + end_time: endTime, + }) + result = await api('GET', `/user_busy_times?${params}`) + break + } + default: + result = { error: 'Unknown availability subcommand. Use: times, busy' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': { + const org = args.organization + const scope = args.scope || 'organization' + if (!org) { result = { error: '--organization URI required' }; break } + const params = new URLSearchParams({ + organization: org, + scope, + }) + params.set('count', String(count)) + if (args['page-token']) params.set('page_token', args['page-token']) + result = await api('GET', `/webhook_subscriptions?${params}`) + break + } + case 'create': { + const url = args.url + const events = args.events?.split(',') + const org = args.organization + const scope = args.scope || 'organization' + if (!url || !events || !org) { result = { error: '--url, --events (comma-separated), and --organization required' }; break } + const body = { url, events, organization: org, scope } + if (args.user) body.user = args.user + result = await api('POST', '/webhook_subscriptions', body) + break + } + case 'delete': { + const uuid = args.uuid + if (!uuid) { result = { error: '--uuid required' }; break } + result = await api('DELETE', `/webhook_subscriptions/${uuid}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' } + } + break + + case 'org': + switch (sub) { + case 'members': { + const org = args.organization + if (!org) { result = { error: '--organization URI required' }; break } + const params = new URLSearchParams({ organization: org }) + params.set('count', String(count)) + if (args['page-token']) params.set('page_token', args['page-token']) + result = await api('GET', `/organization_memberships?${params}`) + break + } + default: + result = { error: 'Unknown org subcommand. Use: members' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + users: 'users me', + 'event-types': 'event-types [list --user <uri> | get --uuid <id>]', + events: 'events [list --user <uri> | get --uuid <id> | cancel --uuid <id> | invitees --uuid <id>]', + availability: 'availability [times --event-type <uri> --start-time <iso> --end-time <iso> | busy --user <uri> --start-time <iso> --end-time <iso>]', + webhooks: 'webhooks [list --organization <uri> | create --url <url> --events <e1,e2> --organization <uri> | delete --uuid <id>]', + org: 'org [members --organization <uri>]', + options: '--count <n> --page-token <token> --status <active|canceled>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/clearbit.js b/tools/clis/clearbit.js new file mode 100755 index 0000000..e0e9683 --- /dev/null +++ b/tools/clis/clearbit.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +const API_KEY = process.env.CLEARBIT_API_KEY + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'CLEARBIT_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, baseUrl, path, body) { + const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64') + if (args['dry-run']) { + return { _dry_run: true, method, url: `${baseUrl}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${baseUrl}${path}`, { + method, + headers: { + 'Authorization': auth, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'person': + switch (sub) { + case 'find': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + result = await api('GET', 'https://person-stream.clearbit.com', `/v2/people/find?email=${encodeURIComponent(email)}`) + break + } + default: + result = { error: 'Unknown person subcommand. Use: find' } + } + break + + case 'company': + switch (sub) { + case 'find': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('GET', 'https://company-stream.clearbit.com', `/v2/companies/find?domain=${encodeURIComponent(domain)}`) + break + } + default: + result = { error: 'Unknown company subcommand. Use: find' } + } + break + + case 'combined': + switch (sub) { + case 'find': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + result = await api('GET', 'https://person-stream.clearbit.com', `/v2/combined/find?email=${encodeURIComponent(email)}`) + break + } + default: + result = { error: 'Unknown combined subcommand. Use: find' } + } + break + + case 'reveal': + switch (sub) { + case 'find': { + const ip = args.ip + if (!ip) { result = { error: '--ip required' }; break } + result = await api('GET', 'https://reveal.clearbit.com', `/v1/companies/find?ip=${encodeURIComponent(ip)}`) + break + } + default: + result = { error: 'Unknown reveal subcommand. Use: find' } + } + break + + case 'name-to-domain': + switch (sub) { + case 'find': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + result = await api('GET', 'https://company.clearbit.com', `/v1/domains/find?name=${encodeURIComponent(name)}`) + break + } + default: + result = { error: 'Unknown name-to-domain subcommand. Use: find' } + } + break + + case 'prospector': + switch (sub) { + case 'search': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + const params = new URLSearchParams({ domain }) + if (args.role) params.set('role', args.role) + if (args.seniority) params.set('seniority', args.seniority) + if (args.title) params.set('title', args.title) + if (args.page) params.set('page', args.page) + if (args['page-size']) params.set('page_size', args['page-size']) + result = await api('GET', 'https://prospector.clearbit.com', `/v1/people/search?${params.toString()}`) + break + } + default: + result = { error: 'Unknown prospector subcommand. Use: search' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + person: 'person find --email <email>', + company: 'company find --domain <domain>', + combined: 'combined find --email <email>', + reveal: 'reveal find --ip <ip>', + 'name-to-domain': 'name-to-domain find --name <company_name>', + prospector: 'prospector search --domain <domain> [--role <role>] [--seniority <level>] [--title <title>]', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/customer-io.js b/tools/clis/customer-io.js new file mode 100755 index 0000000..12addf7 --- /dev/null +++ b/tools/clis/customer-io.js @@ -0,0 +1,205 @@ +#!/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' } + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${TRACK_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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' } + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${APP_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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 + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } + 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 + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } + result = await appApi('GET', `/customers/${customerId}/attributes`) + break + } + case 'delete': { + const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } + result = await trackApi('DELETE', `/customers/${customerId}`) + break + } + case 'track-event': { + const customerId = rest[0] || args.id + if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break } + if (!args.name) { result = { error: '--name required (event name)' }; break } + 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': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } + result = await appApi('GET', `/campaigns/${campaignId}`) + break + } + case 'metrics': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } + result = await appApi('GET', `/campaigns/${campaignId}/metrics`) + break + } + case 'trigger': { + const campaignId = rest[0] || args.id + if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break } + 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/${campaignId}/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) +}) diff --git a/tools/clis/dataforseo.js b/tools/clis/dataforseo.js new file mode 100755 index 0000000..b4286ac --- /dev/null +++ b/tools/clis/dataforseo.js @@ -0,0 +1,257 @@ +#!/usr/bin/env node + +const LOGIN = process.env.DATAFORSEO_LOGIN +const PASSWORD = process.env.DATAFORSEO_PASSWORD +const BASE_URL = 'https://api.dataforseo.com/v3' + +if (!LOGIN || !PASSWORD) { + console.error(JSON.stringify({ error: 'DATAFORSEO_LOGIN and DATAFORSEO_PASSWORD environment variables required' })) + process.exit(1) +} + +const AUTH = 'Basic ' + Buffer.from(`${LOGIN}:${PASSWORD}`).toString('base64') + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': 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 } + } +} + +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 location = args.location || 'United States' + const locationCode = args['location-code'] ? Number(args['location-code']) : 2840 + const language = args.language || 'English' + const languageCode = args['language-code'] || 'en' + const limit = args.limit ? Number(args.limit) : 100 + + switch (cmd) { + case 'serp': + switch (sub) { + case 'google': { + const keyword = args.keyword + if (!keyword) { result = { error: '--keyword required' }; break } + result = await api('POST', '/serp/google/organic/live/regular', [{ + keyword, + location_name: location, + language_name: language, + }]) + break + } + case 'locations': + result = await api('GET', '/serp/google/locations') + break + case 'languages': + result = await api('GET', '/serp/google/languages') + break + default: + result = { error: 'Unknown serp subcommand. Use: google, locations, languages' } + } + break + + case 'keywords': + switch (sub) { + case 'volume': { + const keywords = args.keywords?.split(',') + if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break } + result = await api('POST', '/keywords_data/google_ads/search_volume/live', [{ + keywords, + location_code: locationCode, + language_code: languageCode, + }]) + break + } + case 'for-site': { + const target = args.target + if (!target) { result = { error: '--target required (domain)' }; break } + result = await api('POST', '/keywords_data/google_ads/keywords_for_site/live', [{ + target, + location_code: locationCode, + language_code: languageCode, + }]) + break + } + case 'for-keywords': { + const keywords = args.keywords?.split(',') + if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break } + result = await api('POST', '/keywords_data/google_ads/keywords_for_keywords/live', [{ + keywords, + location_code: locationCode, + language_code: languageCode, + }]) + break + } + case 'trends': { + const keywords = args.keywords?.split(',') + if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break } + result = await api('POST', '/keywords_data/google_trends/explore/live', [{ + keywords, + location_code: locationCode, + language_code: languageCode, + }]) + break + } + default: + result = { error: 'Unknown keywords subcommand. Use: volume, for-site, for-keywords, trends' } + } + break + + case 'backlinks': + switch (sub) { + case 'summary': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/backlinks/summary/live', [{ + target, + backlinks_status_type: 'live', + }]) + break + } + case 'list': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/backlinks/backlinks/live', [{ + target, + mode: args.mode || 'as_is', + limit, + backlinks_status_type: 'live', + }]) + break + } + case 'refdomains': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/backlinks/referring_domains/live', [{ + target, + limit, + }]) + break + } + case 'anchors': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/backlinks/anchors/live', [{ + target, + limit, + }]) + break + } + case 'index': + result = await api('GET', '/backlinks/index') + break + default: + result = { error: 'Unknown backlinks subcommand. Use: summary, list, refdomains, anchors, index' } + } + break + + case 'onpage': + switch (sub) { + case 'audit': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + result = await api('POST', '/on_page/instant_pages', [{ + url, + enable_javascript: args['no-js'] ? false : true, + }]) + break + } + default: + result = { error: 'Unknown onpage subcommand. Use: audit' } + } + break + + case 'labs': + switch (sub) { + case 'competitors': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/dataforseo_labs/google/competitors_domain/live', [{ + target, + location_code: locationCode, + language_code: languageCode, + limit, + }]) + break + } + case 'ranked-keywords': { + const target = args.target + if (!target) { result = { error: '--target required' }; break } + result = await api('POST', '/dataforseo_labs/google/ranked_keywords/live', [{ + target, + location_code: locationCode, + language_code: languageCode, + limit, + }]) + break + } + case 'domain-intersection': { + const targets = args.targets?.split(',') + if (!targets || targets.length < 2) { result = { error: '--targets required (comma-separated, at least 2 domains)' }; break } + const payload = { location_code: locationCode, language_code: languageCode, limit } + targets.forEach((t, i) => { payload[`target${i + 1}`] = t }) + result = await api('POST', '/dataforseo_labs/google/domain_intersection/live', [payload]) + break + } + default: + result = { error: 'Unknown labs subcommand. Use: competitors, ranked-keywords, domain-intersection' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + serp: 'serp [google --keyword <kw> | locations | languages]', + keywords: 'keywords [volume --keywords <kw1,kw2> | for-site --target <domain> | for-keywords --keywords <kw1,kw2> | trends --keywords <kw1,kw2>]', + backlinks: 'backlinks [summary --target <domain> | list --target <domain> | refdomains --target <domain> | anchors --target <domain> | index]', + onpage: 'onpage [audit --url <url>]', + labs: 'labs [competitors --target <domain> | ranked-keywords --target <domain> | domain-intersection --targets <d1,d2>]', + options: '--location-code <code> --language-code <code> --limit <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/demio.js b/tools/clis/demio.js new file mode 100755 index 0000000..9e12082 --- /dev/null +++ b/tools/clis/demio.js @@ -0,0 +1,149 @@ +#!/usr/bin/env node + +const API_KEY = process.env.DEMIO_API_KEY +const API_SECRET = process.env.DEMIO_API_SECRET +const BASE_URL = 'https://my.demio.com/api/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'DEMIO_API_KEY environment variable required' })) + process.exit(1) +} + +if (!API_SECRET) { + console.error(JSON.stringify({ error: 'DEMIO_API_SECRET environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Api-Key': '***', 'Api-Secret': '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Api-Key': API_KEY, + 'Api-Secret': API_SECRET, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'ping': + result = await api('GET', '/ping') + break + + case 'events': + switch (sub) { + case 'list': { + const type = args.type + let qs = '' + if (type) qs = `?type=${encodeURIComponent(type)}` + result = await api('GET', `/events${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/event/${id}`) + break + } + case 'date': { + const eventId = args['event-id'] + const dateId = args['date-id'] + if (!eventId) { result = { error: '--event-id required' }; break } + if (!dateId) { result = { error: '--date-id required' }; break } + result = await api('GET', `/event/${eventId}/date/${dateId}`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list, get, date' } + } + break + + case 'register': + switch (sub) { + case 'create': { + const id = args.id || args['event-id'] + const name = args.name + const email = args.email + if (!id) { result = { error: '--id (event id) required' }; break } + if (!name) { result = { error: '--name required' }; break } + if (!email) { result = { error: '--email required' }; break } + const payload = { id, name, email } + if (args['date-id']) payload.date_id = args['date-id'] + if (args['ref-url']) payload.ref_url = args['ref-url'] + result = await api('POST', '/event/register', payload) + break + } + default: + result = { error: 'Unknown register subcommand. Use: create' } + } + break + + case 'participants': + switch (sub) { + case 'list': { + const dateId = args['date-id'] + if (!dateId) { result = { error: '--date-id required' }; break } + result = await api('GET', `/date/${dateId}/participants`) + break + } + default: + result = { error: 'Unknown participants subcommand. Use: list' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + ping: 'ping', + events: 'events [list --type <upcoming|past|all> | get --id <id> | date --event-id <id> --date-id <id>]', + register: 'register [create --id <event_id> --name <name> --email <email> --date-id <date_id>]', + participants: 'participants [list --date-id <id>]', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/dub.js b/tools/clis/dub.js new file mode 100755 index 0000000..5cad79e --- /dev/null +++ b/tools/clis/dub.js @@ -0,0 +1,158 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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': { + if (!args.url) { result = { error: '--url required' }; break } + 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) + if (args['link-id']) params.set('linkId', args['link-id']) + if (args['external-id']) params.set('externalId', args['external-id']) + result = await api('GET', `/links/info?${params}`) + break + } + case 'update': { + if (!args.id) { result = { error: '--id required (link ID)' }; break } + 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': + if (!args.id) { result = { error: '--id required (link ID)' }; break } + result = await api('DELETE', `/links/${args.id}`) + break + case 'bulk-create': { + let links + try { + links = JSON.parse(args.links || '[]') + } catch { + result = { error: 'Invalid JSON in --links' }; break + } + 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) +}) diff --git a/tools/clis/g2.js b/tools/clis/g2.js new file mode 100755 index 0000000..967ef6a --- /dev/null +++ b/tools/clis/g2.js @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +const API_TOKEN = process.env.G2_API_TOKEN +const BASE_URL = 'https://data.g2.com/api/v1' + +if (!API_TOKEN) { + console.error(JSON.stringify({ error: 'G2_API_TOKEN environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/vnd.api+json', Accept: 'application/vnd.api+json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Token token=${API_TOKEN}`, + 'Content-Type': 'application/vnd.api+json', + 'Accept': 'application/vnd.api+json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const page = args.page ? Number(args.page) : 1 + const perPage = args['per-page'] ? Number(args['per-page']) : 25 + const productId = args['product-id'] || args.product + + switch (cmd) { + case 'reviews': + switch (sub) { + case 'list': { + let qs = `?page[size]=${perPage}&page[number]=${page}` + if (productId) qs += `&filter[product_id]=${productId}` + if (args.state) qs += `&filter[state]=${encodeURIComponent(args.state)}` + result = await api('GET', `/survey-responses${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/survey-responses/${id}`) + break + } + default: + result = { error: 'Unknown reviews subcommand. Use: list, get' } + } + break + + case 'products': + switch (sub) { + case 'list': { + let qs = `?page[size]=${perPage}&page[number]=${page}` + if (args.name) qs += `&filter[name]=${encodeURIComponent(args.name)}` + result = await api('GET', `/products${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/products/${id}`) + break + } + default: + result = { error: 'Unknown products subcommand. Use: list, get' } + } + break + + case 'reports': + switch (sub) { + case 'list': { + let qs = `?page[size]=${perPage}&page[number]=${page}` + result = await api('GET', `/reports${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/reports/${id}`) + break + } + default: + result = { error: 'Unknown reports subcommand. Use: list, get' } + } + break + + case 'competitors': + switch (sub) { + case 'list': { + if (!productId) { result = { error: '--product-id required' }; break } + let qs = `?page[size]=${perPage}&page[number]=${page}&filter[product_id]=${productId}` + result = await api('GET', `/competitor-comparisons${qs}`) + break + } + default: + result = { error: 'Unknown competitors subcommand. Use: list' } + } + break + + case 'categories': + switch (sub) { + case 'list': { + let qs = `?page[size]=${perPage}&page[number]=${page}` + if (args.name) qs += `&filter[name]=${encodeURIComponent(args.name)}` + result = await api('GET', `/categories${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/categories/${id}`) + break + } + default: + result = { error: 'Unknown categories subcommand. Use: list, get' } + } + break + + case 'tracking': + switch (sub) { + case 'visitors': { + let qs = `?page[size]=${perPage}&page[number]=${page}` + if (args.start) qs += `&filter[start_date]=${encodeURIComponent(args.start)}` + if (args.end) qs += `&filter[end_date]=${encodeURIComponent(args.end)}` + result = await api('GET', `/tracking-events${qs}`) + break + } + default: + result = { error: 'Unknown tracking subcommand. Use: visitors' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + reviews: 'reviews [list --product-id <id> | get --id <id>]', + products: 'products [list --name <name> | get --id <id>]', + reports: 'reports [list | get --id <id>]', + competitors: 'competitors [list --product-id <id>]', + categories: 'categories [list --name <name> | get --id <id>]', + tracking: 'tracking [visitors --start <date> --end <date>]', + options: '--page <n> --per-page <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/ga4.js b/tools/clis/ga4.js new file mode 100755 index 0000000..ed91886 --- /dev/null +++ b/tools/clis/ga4.js @@ -0,0 +1,194 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${baseUrl}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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 }) + if (args['dry-run']) { + return { _dry_run: true, method: 'POST', url: `${MP_URL}?${new URLSearchParams({ measurement_id: measurementId, api_secret: '***' })}`, headers: { 'Content-Type': 'application/json' }, body: body || undefined } + } + 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 } + let eventParams = {} + if (args.params) { + try { + eventParams = JSON.parse(args.params) + } catch { + result = { error: 'Invalid JSON in --params' }; break + } + } + 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) +}) diff --git a/tools/clis/google-ads.js b/tools/clis/google-ads.js new file mode 100755 index 0000000..81c35a4 --- /dev/null +++ b/tools/clis/google-ads.js @@ -0,0 +1,189 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'developer-token': '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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}`, + amount_micros: amountMicros, + }, + updateMask: 'amount_micros', + }], + }) + 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) +}) diff --git a/tools/clis/google-search-console.js b/tools/clis/google-search-console.js new file mode 100755 index 0000000..2264a18 --- /dev/null +++ b/tools/clis/google-search-console.js @@ -0,0 +1,166 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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'] + 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': + if (!args.url) { result = { error: '--url required (URL to inspect)' }; break } + 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': { + if (!args['sitemap-url']) { result = { error: '--sitemap-url required' }; break } + 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) +}) diff --git a/tools/clis/hotjar.js b/tools/clis/hotjar.js new file mode 100755 index 0000000..e1fb505 --- /dev/null +++ b/tools/clis/hotjar.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node + +const CLIENT_ID = process.env.HOTJAR_CLIENT_ID +const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET +const OAUTH_URL = 'https://api.hotjar.io' +const BASE_URL = 'https://api.hotjar.io/v2' + +if (!CLIENT_ID || !CLIENT_SECRET) { + console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' })) + process.exit(1) +} + +let cachedToken = null + +async function getToken() { + if (cachedToken) return cachedToken + const res = await fetch(`${OAUTH_URL}/oauth/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`, + }) + const data = await res.json() + if (!data.access_token) { + throw new Error(data.error_description || data.error || 'Failed to obtain access token') + } + cachedToken = data.access_token + return cachedToken +} + +async function api(method, path) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' } } + } + const token = await getToken() + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': '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 siteId = args['site-id'] + const limit = args.limit || '100' + const cursor = args.cursor + + switch (cmd) { + case 'sites': + switch (sub) { + case 'list': + result = await api('GET', '/sites') + break + default: + result = { error: 'Unknown sites subcommand. Use: list' } + } + break + + case 'surveys': + if (!siteId) { result = { error: '--site-id required' }; break } + switch (sub) { + case 'list': + result = await api('GET', `/sites/${siteId}/surveys`) + break + case 'responses': { + const surveyId = args['survey-id'] + if (!surveyId) { result = { error: '--survey-id required' }; break } + const params = new URLSearchParams({ limit }) + if (cursor) params.set('cursor', cursor) + result = await api('GET', `/sites/${siteId}/surveys/${surveyId}/responses?${params.toString()}`) + break + } + default: + result = { error: 'Unknown surveys subcommand. Use: list, responses' } + } + break + + case 'heatmaps': + if (!siteId) { result = { error: '--site-id required' }; break } + switch (sub) { + case 'list': + result = await api('GET', `/sites/${siteId}/heatmaps`) + break + default: + result = { error: 'Unknown heatmaps subcommand. Use: list' } + } + break + + case 'recordings': + if (!siteId) { result = { error: '--site-id required' }; break } + switch (sub) { + case 'list': { + const params = new URLSearchParams({ limit }) + if (cursor) params.set('cursor', cursor) + if (args['date-from']) params.set('date_from', args['date-from']) + if (args['date-to']) params.set('date_to', args['date-to']) + result = await api('GET', `/sites/${siteId}/recordings?${params.toString()}`) + break + } + default: + result = { error: 'Unknown recordings subcommand. Use: list' } + } + break + + case 'forms': + if (!siteId) { result = { error: '--site-id required' }; break } + switch (sub) { + case 'list': + result = await api('GET', `/sites/${siteId}/forms`) + break + default: + result = { error: 'Unknown forms subcommand. Use: list' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + sites: 'sites list', + surveys: 'surveys list --site-id <id> | surveys responses --site-id <id> --survey-id <id> [--limit <n>] [--cursor <cursor>]', + heatmaps: 'heatmaps list --site-id <id>', + recordings: 'recordings list --site-id <id> [--limit <n>] [--cursor <cursor>] [--date-from <date>] [--date-to <date>]', + forms: 'forms list --site-id <id>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/hunter.js b/tools/clis/hunter.js new file mode 100755 index 0000000..49ca649 --- /dev/null +++ b/tools/clis/hunter.js @@ -0,0 +1,249 @@ +#!/usr/bin/env node + +const API_KEY = process.env.HUNTER_API_KEY +const BASE_URL = 'https://api.hunter.io/v2' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'HUNTER_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const separator = path.includes('?') ? '&' : '?' + const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}` + if (args['dry-run']) { + return { _dry_run: true, method, url: url.replace(API_KEY, '***'), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'domain': + switch (sub) { + case 'search': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + const params = new URLSearchParams({ domain }) + if (args.limit) params.set('limit', args.limit) + if (args.type) params.set('type', args.type) + result = await api('GET', `/domain-search?${params.toString()}`) + break + } + case 'count': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + const params = new URLSearchParams({ domain }) + if (args.type) params.set('type', args.type) + result = await api('GET', `/email-count?${params.toString()}`) + break + } + default: + result = { error: 'Unknown domain subcommand. Use: search, count' } + } + break + + case 'email': + switch (sub) { + case 'find': { + const domain = args.domain + const firstName = args['first-name'] + const lastName = args['last-name'] + if (!domain) { result = { error: '--domain required' }; break } + if (!firstName) { result = { error: '--first-name required' }; break } + if (!lastName) { result = { error: '--last-name required' }; break } + const params = new URLSearchParams({ domain, first_name: firstName, last_name: lastName }) + result = await api('GET', `/email-finder?${params.toString()}`) + break + } + case 'verify': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const params = new URLSearchParams({ email }) + result = await api('GET', `/email-verifier?${params.toString()}`) + break + } + default: + result = { error: 'Unknown email subcommand. Use: find, verify' } + } + break + + case 'account': + switch (sub) { + case 'info': + result = await api('GET', '/account') + break + default: + result = { error: 'Unknown account subcommand. Use: info' } + } + break + + case 'leads': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.offset) params.set('offset', args.offset) + const qs = params.toString() + result = await api('GET', `/leads${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/leads/${id}`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { email } + if (args['first-name']) body.first_name = args['first-name'] + if (args['last-name']) body.last_name = args['last-name'] + if (args.company) body.company = args.company + result = await api('POST', '/leads', body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/leads/${id}`) + break + } + default: + result = { error: 'Unknown leads subcommand. Use: list, get, create, delete' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.offset) params.set('offset', args.offset) + const qs = params.toString() + result = await api('GET', `/campaigns${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${id}`) + break + } + case 'start': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/campaigns/${id}/start`) + break + } + case 'pause': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/campaigns/${id}/pause`) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get, start, pause' } + } + break + + case 'leads-lists': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.offset) params.set('offset', args.offset) + const qs = params.toString() + result = await api('GET', `/leads_lists${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/leads_lists/${id}`) + break + } + default: + result = { error: 'Unknown leads-lists subcommand. Use: list, get' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + domain: { + search: 'domain search --domain <domain> [--limit <n>] [--type personal|generic]', + count: 'domain count --domain <domain> [--type personal|generic]', + }, + email: { + find: 'email find --domain <domain> --first-name <name> --last-name <name>', + verify: 'email verify --email <email>', + }, + account: 'account info', + leads: { + list: 'leads list [--limit <n>] [--offset <n>]', + get: 'leads get --id <id>', + create: 'leads create --email <email> [--first-name <name>] [--last-name <name>] [--company <company>]', + delete: 'leads delete --id <id>', + }, + campaigns: { + list: 'campaigns list [--limit <n>] [--offset <n>]', + get: 'campaigns get --id <id>', + start: 'campaigns start --id <id>', + pause: 'campaigns pause --id <id>', + }, + 'leads-lists': { + list: 'leads-lists list [--limit <n>] [--offset <n>]', + get: 'leads-lists get --id <id>', + }, + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/instantly.js b/tools/clis/instantly.js new file mode 100755 index 0000000..f279fc6 --- /dev/null +++ b/tools/clis/instantly.js @@ -0,0 +1,270 @@ +#!/usr/bin/env node + +const API_KEY = process.env.INSTANTLY_API_KEY +const BASE_URL = 'https://api.instantly.ai/api/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'INSTANTLY_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const separator = path.includes('?') ? '&' : '?' + const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}` + if (args['dry-run']) { + const maskedUrl = url.replace(API_KEY, '***') + const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined + if (maskedBody && maskedBody.api_key) maskedBody.api_key = '***' + return { _dry_run: true, method, url: maskedUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: maskedBody } + } + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.skip) params.set('skip', args.skip) + const qs = params.toString() + result = await api('GET', `/campaign/list${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const params = new URLSearchParams({ campaign_id: id }) + result = await api('GET', `/campaign/get?${params.toString()}`) + break + } + case 'status': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const params = new URLSearchParams({ campaign_id: id }) + result = await api('GET', `/campaign/get/status?${params.toString()}`) + break + } + case 'launch': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', '/campaign/launch', { api_key: API_KEY, campaign_id: id }) + break + } + case 'pause': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', '/campaign/pause', { api_key: API_KEY, campaign_id: id }) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get, status, launch, pause' } + } + break + + case 'leads': + switch (sub) { + case 'list': { + const campaignId = args['campaign-id'] + if (!campaignId) { result = { error: '--campaign-id required' }; break } + const params = new URLSearchParams({ campaign_id: campaignId }) + if (args.limit) params.set('limit', args.limit) + if (args.skip) params.set('skip', args.skip) + result = await api('GET', `/lead/get?${params.toString()}`) + break + } + case 'add': { + const campaignId = args['campaign-id'] + const email = args.email + if (!campaignId) { result = { error: '--campaign-id required' }; break } + if (!email) { result = { error: '--email required' }; break } + const lead = { email } + if (args['first-name']) lead.first_name = args['first-name'] + if (args['last-name']) lead.last_name = args['last-name'] + if (args.company) lead.company_name = args.company + result = await api('POST', '/lead/add', { api_key: API_KEY, campaign_id: campaignId, leads: [lead] }) + break + } + case 'delete': { + const campaignId = args['campaign-id'] + const email = args.email + if (!campaignId) { result = { error: '--campaign-id required' }; break } + if (!email) { result = { error: '--email required' }; break } + result = await api('POST', '/lead/delete', { api_key: API_KEY, campaign_id: campaignId, delete_list: [email] }) + break + } + case 'status': { + const campaignId = args['campaign-id'] + const email = args.email + if (!campaignId) { result = { error: '--campaign-id required' }; break } + if (!email) { result = { error: '--email required' }; break } + const params = new URLSearchParams({ campaign_id: campaignId, email }) + result = await api('GET', `/lead/get/status?${params.toString()}`) + break + } + default: + result = { error: 'Unknown leads subcommand. Use: list, add, delete, status' } + } + break + + case 'accounts': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.skip) params.set('skip', args.skip) + const qs = params.toString() + result = await api('GET', `/account/list${qs ? '?' + qs : ''}`) + break + } + case 'status': { + const accountId = args['account-id'] + if (!accountId) { result = { error: '--account-id required' }; break } + const params = new URLSearchParams({ email: accountId }) + result = await api('GET', `/account/get/status?${params.toString()}`) + break + } + case 'warmup-status': { + const accountId = args['account-id'] + if (!accountId) { result = { error: '--account-id required' }; break } + const params = new URLSearchParams({ email: accountId }) + result = await api('GET', `/account/get/warmup?${params.toString()}`) + break + } + default: + result = { error: 'Unknown accounts subcommand. Use: list, status, warmup-status' } + } + break + + case 'analytics': + switch (sub) { + case 'campaign': { + const campaignId = args['campaign-id'] + if (!campaignId) { result = { error: '--campaign-id required' }; break } + const body = { api_key: API_KEY, campaign_id: campaignId } + if (args['start-date']) body.start_date = args['start-date'] + if (args['end-date']) body.end_date = args['end-date'] + result = await api('POST', '/analytics/campaign/summary', body) + break + } + case 'steps': { + const campaignId = args['campaign-id'] + if (!campaignId) { result = { error: '--campaign-id required' }; break } + const body = { api_key: API_KEY, campaign_id: campaignId } + if (args['start-date']) body.start_date = args['start-date'] + if (args['end-date']) body.end_date = args['end-date'] + result = await api('POST', '/analytics/campaign/step', body) + break + } + case 'account': { + const startDate = args['start-date'] + const endDate = args['end-date'] + if (!startDate) { result = { error: '--start-date required' }; break } + if (!endDate) { result = { error: '--end-date required' }; break } + result = await api('POST', '/analytics/campaign/count', { api_key: API_KEY, start_date: startDate, end_date: endDate }) + break + } + default: + result = { error: 'Unknown analytics subcommand. Use: campaign, steps, account' } + } + break + + case 'blocklist': + switch (sub) { + case 'list': + result = await api('GET', '/blocklist') + break + case 'add': { + const entries = args.entries + if (!entries) { result = { error: '--entries required (comma-separated emails or domains)' }; break } + const entryList = entries.split(',').map(e => e.trim()) + result = await api('POST', '/blocklist/add', { api_key: API_KEY, entries: entryList }) + break + } + default: + result = { error: 'Unknown blocklist subcommand. Use: list, add' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + campaigns: { + list: 'campaigns list [--limit <n>] [--skip <n>]', + get: 'campaigns get --id <id>', + status: 'campaigns status --id <id>', + launch: 'campaigns launch --id <id>', + pause: 'campaigns pause --id <id>', + }, + leads: { + list: 'leads list --campaign-id <id> [--limit <n>] [--skip <n>]', + add: 'leads add --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>] [--company <name>]', + delete: 'leads delete --campaign-id <id> --email <email>', + status: 'leads status --campaign-id <id> --email <email>', + }, + accounts: { + list: 'accounts list [--limit <n>] [--skip <n>]', + status: 'accounts status --account-id <email>', + 'warmup-status': 'accounts warmup-status --account-id <email>', + }, + analytics: { + campaign: 'analytics campaign --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]', + steps: 'analytics steps --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]', + account: 'analytics account --start-date YYYY-MM-DD --end-date YYYY-MM-DD', + }, + blocklist: { + list: 'blocklist list', + add: 'blocklist add --entries <email-or-domain,email-or-domain>', + }, + options: '--dry-run (show request without executing)', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/intercom.js b/tools/clis/intercom.js new file mode 100755 index 0000000..4630255 --- /dev/null +++ b/tools/clis/intercom.js @@ -0,0 +1,399 @@ +#!/usr/bin/env node + +const API_KEY = process.env.INTERCOM_API_KEY +const BASE_URL = 'https://api.intercom.io' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'INTERCOM_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json', 'Intercom-Version': '2.11' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Intercom-Version': '2.11', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const perPage = args['per-page'] ? Number(args['per-page']) : undefined + + switch (cmd) { + case 'contacts': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (perPage) params.set('per_page', String(perPage)) + if (args['starting-after']) params.set('starting_after', args['starting-after']) + const qs = params.toString() + result = await api('GET', `/contacts${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/contacts/${id}`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { + role: args.role || 'user', + email, + } + if (args.name) body.name = args.name + if (args.phone) body.phone = args.phone + result = await api('POST', '/contacts', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.email) body.email = args.email + if (args.phone) body.phone = args.phone + if (args.role) body.role = args.role + result = await api('PUT', `/contacts/${id}`, body) + break + } + case 'search': { + const field = args.field + const operator = args.operator || '=' + const value = args.value + if (!field || !value) { result = { error: '--field and --value required' }; break } + const body = { + query: { field, operator, value }, + } + if (perPage) body.pagination = { per_page: perPage } + result = await api('POST', '/contacts/search', body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/contacts/${id}`) + break + } + case 'tag': { + const id = args.id + const tagId = args['tag-id'] + if (!id || !tagId) { result = { error: '--id (contact ID) and --tag-id required' }; break } + result = await api('POST', `/contacts/${id}/tags`, { id: tagId }) + break + } + case 'untag': { + const id = args.id + const tagId = args['tag-id'] + if (!id || !tagId) { result = { error: '--id (contact ID) and --tag-id required' }; break } + result = await api('DELETE', `/contacts/${id}/tags/${tagId}`) + break + } + default: + result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, search, delete, tag, untag' } + } + break + + case 'conversations': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (perPage) params.set('per_page', String(perPage)) + if (args['starting-after']) params.set('starting_after', args['starting-after']) + const qs = params.toString() + result = await api('GET', `/conversations${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/conversations/${id}`) + break + } + case 'search': { + const field = args.field + const operator = args.operator || '=' + const value = args.value + if (!field || value === undefined) { result = { error: '--field and --value required' }; break } + const body = { + query: { field, operator, value }, + } + if (perPage) body.pagination = { per_page: perPage } + result = await api('POST', '/conversations/search', body) + break + } + case 'reply': { + const id = args.id + const body = args.body + const adminId = args['admin-id'] + if (!id || !body || !adminId) { result = { error: '--id, --body, and --admin-id required' }; break } + result = await api('POST', `/conversations/${id}/reply`, { + message_type: 'comment', + type: 'admin', + admin_id: adminId, + body, + }) + break + } + case 'close': { + const id = args.id + const adminId = args['admin-id'] + if (!id || !adminId) { result = { error: '--id and --admin-id required' }; break } + result = await api('POST', `/conversations/${id}/parts`, { + message_type: 'close', + type: 'admin', + admin_id: adminId, + body: args.body || '', + }) + break + } + default: + result = { error: 'Unknown conversations subcommand. Use: list, get, search, reply, close' } + } + break + + case 'messages': + switch (sub) { + case 'create': { + const messageType = args.type || 'inapp' + const body = args.body + const adminId = args['admin-id'] + const to = args.to + if (!body || !adminId || !to) { result = { error: '--body, --admin-id, and --to (user ID) required' }; break } + result = await api('POST', '/messages', { + message_type: messageType, + body, + from: { type: 'admin', id: adminId }, + to: { type: 'user', id: to }, + }) + break + } + default: + result = { error: 'Unknown messages subcommand. Use: create --body <text> --admin-id <id> --to <user_id> [--type inapp|email]' } + } + break + + case 'companies': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (perPage) params.set('per_page', String(perPage)) + if (args.page) params.set('page', args.page) + const qs = params.toString() + result = await api('GET', `/companies${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/companies/${id}`) + break + } + case 'create': { + const companyId = args['company-id'] + const name = args.name + if (!companyId) { result = { error: '--company-id required' }; break } + const body = { company_id: companyId } + if (name) body.name = name + if (args.plan) body.plan = args.plan + if (args.industry) body.industry = args.industry + result = await api('POST', '/companies', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.plan) body.plan = args.plan + if (args.industry) body.industry = args.industry + result = await api('PUT', `/companies/${id}`, body) + break + } + default: + result = { error: 'Unknown companies subcommand. Use: list, get, create, update' } + } + break + + case 'tags': + switch (sub) { + case 'list': + result = await api('GET', '/tags') + break + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + result = await api('POST', '/tags', { name }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/tags/${id}`) + break + } + default: + result = { error: 'Unknown tags subcommand. Use: list, create, delete' } + } + break + + case 'articles': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (perPage) params.set('per_page', String(perPage)) + if (args.page) params.set('page', args.page) + const qs = params.toString() + result = await api('GET', `/articles${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/articles/${id}`) + break + } + case 'create': { + const title = args.title + const authorId = args['author-id'] + if (!title || !authorId) { result = { error: '--title and --author-id required' }; break } + const body = { + title, + author_id: Number(authorId), + state: args.state || 'draft', + } + if (args.body) body.body = args.body + result = await api('POST', '/articles', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.title) body.title = args.title + if (args.body) body.body = args.body + if (args.state) body.state = args.state + result = await api('PUT', `/articles/${id}`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/articles/${id}`) + break + } + default: + result = { error: 'Unknown articles subcommand. Use: list, get, create, update, delete' } + } + break + + case 'admins': + switch (sub) { + case 'list': + result = await api('GET', '/admins') + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/admins/${id}`) + break + } + default: + result = { error: 'Unknown admins subcommand. Use: list, get' } + } + break + + case 'events': + switch (sub) { + case 'create': { + const eventName = args.name + const userId = args['user-id'] + if (!eventName || !userId) { result = { error: '--name and --user-id required' }; break } + const body = { + event_name: eventName, + user_id: userId, + created_at: args['created-at'] ? Number(args['created-at']) : Math.floor(Date.now() / 1000), + } + if (args.metadata) { + try { body.metadata = JSON.parse(args.metadata) } catch { body.metadata = {} } + } + result = await api('POST', '/events', body) + break + } + case 'list': { + const userId = args['user-id'] + if (!userId) { result = { error: '--user-id required' }; break } + const params = new URLSearchParams({ type: 'user', user_id: userId }) + if (perPage) params.set('per_page', String(perPage)) + result = await api('GET', `/events?${params}`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: create, list' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + contacts: 'contacts [list | get --id <id> | create --email <email> | update --id <id> | search --field <f> --value <v> | delete --id <id> | tag --id <id> --tag-id <id> | untag --id <id> --tag-id <id>]', + conversations: 'conversations [list | get --id <id> | search --field <f> --value <v> | reply --id <id> --body <text> --admin-id <id> | close --id <id> --admin-id <id>]', + messages: 'messages [create --body <text> --admin-id <id> --to <user_id>]', + companies: 'companies [list | get --id <id> | create --company-id <id> --name <name> | update --id <id>]', + tags: 'tags [list | create --name <name> | delete --id <id>]', + articles: 'articles [list | get --id <id> | create --title <title> --author-id <id> | update --id <id> | delete --id <id>]', + admins: 'admins [list | get --id <id>]', + events: 'events [create --name <name> --user-id <id> | list --user-id <id>]', + options: '--per-page <n> --starting-after <cursor> --page <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/keywords-everywhere.js b/tools/clis/keywords-everywhere.js new file mode 100755 index 0000000..5f0dc8c --- /dev/null +++ b/tools/clis/keywords-everywhere.js @@ -0,0 +1,185 @@ +#!/usr/bin/env node + +const API_KEY = process.env.KEYWORDS_EVERYWHERE_API_KEY +const BASE_URL = 'https://api.keywordseverywhere.com/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'KEYWORDS_EVERYWHERE_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const headers = { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const country = args.country || 'us' + const currency = args.currency || 'USD' + const dataSource = args['data-source'] || 'gkp' + + switch (cmd) { + case 'keywords': + switch (sub) { + case 'data': { + const kw = args.kw?.split(',') + if (!kw) { result = { error: '--kw required (comma-separated keywords, max 100)' }; break } + result = await api('POST', '/get_keyword_data', { country, currency, dataSource, kw }) + break + } + case 'related': { + const kw = args.kw?.split(',') + if (!kw) { result = { error: '--kw required (comma-separated keywords)' }; break } + result = await api('POST', '/get_related_keywords', { country, currency, dataSource, kw }) + break + } + case 'pasf': { + const kw = args.kw?.split(',') + if (!kw) { result = { error: '--kw required (comma-separated keywords)' }; break } + result = await api('POST', '/get_pasf_keywords', { country, currency, dataSource, kw }) + break + } + default: + result = { error: 'Unknown keywords subcommand. Use: data, related, pasf' } + } + break + + case 'domain': + switch (sub) { + case 'keywords': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get_domain_keywords', { country, currency, domain }) + break + } + case 'traffic': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get_domain_traffic', { country, domain }) + break + } + case 'backlinks': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get_domain_backlinks', { domain }) + break + } + case 'unique-backlinks': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get_unique_domain_backlinks', { domain }) + break + } + default: + result = { error: 'Unknown domain subcommand. Use: keywords, traffic, backlinks, unique-backlinks' } + } + break + + case 'url': + switch (sub) { + case 'keywords': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + result = await api('POST', '/get_url_keywords', { country, currency, url }) + break + } + case 'traffic': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + result = await api('POST', '/get_url_traffic', { country, url }) + break + } + case 'backlinks': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + result = await api('POST', '/get_page_backlinks', { url }) + break + } + case 'unique-backlinks': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + result = await api('POST', '/get_unique_page_backlinks', { url }) + break + } + default: + result = { error: 'Unknown url subcommand. Use: keywords, traffic, backlinks, unique-backlinks' } + } + break + + case 'account': + switch (sub) { + case 'credits': + result = await api('GET', '/get_credits') + break + case 'countries': + result = await api('GET', '/get_countries') + break + case 'currencies': + result = await api('GET', '/get_currencies') + break + default: + result = { error: 'Unknown account subcommand. Use: credits, countries, currencies' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + keywords: 'keywords [data|related|pasf] --kw <kw1,kw2,...>', + domain: 'domain [keywords|traffic|backlinks|unique-backlinks] --domain <domain>', + url: 'url [keywords|traffic|backlinks|unique-backlinks] --url <url>', + account: 'account [credits|countries|currencies]', + options: '--country <us> --currency <USD> --data-source <gkp>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/kit.js b/tools/clis/kit.js new file mode 100755 index 0000000..3cfd3ee --- /dev/null +++ b/tools/clis/kit.js @@ -0,0 +1,232 @@ +#!/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 (useSecret && !API_SECRET) { + return { error: 'KIT_API_SECRET required for this endpoint' } + } 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 (useSecret && !API_SECRET) { + return { error: 'KIT_API_SECRET required for this endpoint' } + } else if (API_KEY) { + authBody.api_key = API_KEY + } + opts.body = JSON.stringify(authBody) + } + if (args['dry-run']) { + const dryRunHeaders = { ...opts.headers } + const dryRunUrl = url.toString().replace(API_SECRET, '***').replace(API_KEY, '***') + let dryRunBody = undefined + if (opts.body) { + const parsed = JSON.parse(opts.body) + if (parsed.api_secret) parsed.api_secret = '***' + if (parsed.api_key) parsed.api_key = '***' + dryRunBody = parsed + } + return { _dry_run: true, method, url: dryRunUrl, headers: dryRunHeaders, body: dryRunBody } + } + 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': + if (!rest[0]) { result = { error: 'Subscriber ID required' }; break } + result = await api('GET', `/subscribers/${rest[0]}`) + break + case 'update': { + if (!rest[0]) { result = { error: 'Subscriber ID required' }; break } + const body = {} + if (args['first-name']) body.first_name = args['first-name'] + if (args.fields) { + try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break } + } + 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': { + if (!rest[0]) { result = { error: 'Form ID required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + const formId = rest[0] + const body = { email: args.email } + if (args['first-name']) body.first_name = args['first-name'] + if (args.fields) { + try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break } + } + 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': { + if (!rest[0]) { result = { error: 'Sequence ID required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + const sequenceId = rest[0] + const body = { email: args.email } + if (args['first-name']) body.first_name = args['first-name'] + if (args.fields) { + try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break } + } + 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': { + if (!rest[0]) { result = { error: 'Tag ID required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + const tagId = rest[0] + const body = { email: args.email } + if (args['first-name']) body.first_name = args['first-name'] + if (args.fields) { + try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break } + } + result = await api('POST', `/tags/${tagId}/subscribe`, body, false) + break + } + case 'remove': { + if (!rest[0]) { result = { error: 'Tag ID required' }; break } + const tagId = rest[0] + const subscriberId = rest[1] || args['subscriber-id'] + if (!subscriberId) { result = { error: 'Subscriber ID required' }; break } + 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': { + if (!args.subject || !args.content) { result = { error: '--subject and --content required' }; break } + 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) +}) diff --git a/tools/clis/klaviyo.js b/tools/clis/klaviyo.js new file mode 100755 index 0000000..d74966f --- /dev/null +++ b/tools/clis/klaviyo.js @@ -0,0 +1,348 @@ +#!/usr/bin/env node + +const API_KEY = process.env.KLAVIYO_API_KEY +const BASE_URL = 'https://a.klaviyo.com/api' +const REVISION = '2024-10-15' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'KLAVIYO_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const headers = { + 'Authorization': `Klaviyo-API-Key ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'revision': REVISION, + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + 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 'profiles': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.filter) params.set('filter', args.filter) + if (args.sort) params.set('sort', args.sort) + if (args['page-size']) params.set('page[size]', args['page-size']) + if (args['page-cursor']) params.set('page[cursor]', args['page-cursor']) + const qs = params.toString() + result = await api('GET', `/profiles/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/profiles/${id}/`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const attributes = { email } + if (args['first-name']) attributes.first_name = args['first-name'] + if (args['last-name']) attributes.last_name = args['last-name'] + if (args.phone) attributes.phone_number = args.phone + result = await api('POST', '/profiles/', { + data: { type: 'profile', attributes } + }) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const attributes = {} + if (args.email) attributes.email = args.email + if (args['first-name']) attributes.first_name = args['first-name'] + if (args['last-name']) attributes.last_name = args['last-name'] + if (args.phone) attributes.phone_number = args.phone + result = await api('PATCH', `/profiles/${id}/`, { + data: { type: 'profile', id, attributes } + }) + break + } + default: + result = { error: 'Unknown profiles subcommand. Use: list, get, create, update' } + } + break + + case 'lists': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/lists/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/lists/${id}/`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + result = await api('POST', '/lists/', { + data: { type: 'list', attributes: { name } } + }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/lists/${id}/`) + break + } + case 'add-profiles': { + const id = args.id + if (!id) { result = { error: '--id required (list ID)' }; break } + const profileIds = args.profiles?.split(',') + if (!profileIds) { result = { error: '--profiles required (comma-separated profile IDs)' }; break } + result = await api('POST', `/lists/${id}/relationships/profiles/`, { + data: profileIds.map(pid => ({ type: 'profile', id: pid })) + }) + break + } + case 'remove-profiles': { + const id = args.id + if (!id) { result = { error: '--id required (list ID)' }; break } + const profileIds = args.profiles?.split(',') + if (!profileIds) { result = { error: '--profiles required (comma-separated profile IDs)' }; break } + result = await api('DELETE', `/lists/${id}/relationships/profiles/`, { + data: profileIds.map(pid => ({ type: 'profile', id: pid })) + }) + break + } + default: + result = { error: 'Unknown lists subcommand. Use: list, get, create, delete, add-profiles, remove-profiles' } + } + break + + case 'events': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.filter) params.set('filter', args.filter) + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/events/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/events/${id}/`) + break + } + case 'create': { + const metric = args.metric + const email = args.email + if (!metric) { result = { error: '--metric required (metric name)' }; break } + if (!email) { result = { error: '--email required' }; break } + const properties = {} + if (args.value) properties.value = Number(args.value) + if (args.property) { + const pairs = args.property.split(',') + for (const pair of pairs) { + const [k, v] = pair.split(':') + if (k && v) properties[k] = v + } + } + result = await api('POST', '/events/', { + data: { + type: 'event', + attributes: { + metric: { data: { type: 'metric', attributes: { name: metric } } }, + profile: { data: { type: 'profile', attributes: { email } } }, + properties, + time: new Date().toISOString(), + } + } + }) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list, get, create' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.filter) params.set('filter', args.filter) + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/campaigns/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${id}/`) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get' } + } + break + + case 'flows': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.filter) params.set('filter', args.filter) + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/flows/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/flows/${id}/`) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const attributes = {} + if (args.status) attributes.status = args.status + result = await api('PATCH', `/flows/${id}/`, { + data: { type: 'flow', id, attributes } + }) + break + } + default: + result = { error: 'Unknown flows subcommand. Use: list, get, update' } + } + break + + case 'metrics': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/metrics/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/metrics/${id}/`) + break + } + default: + result = { error: 'Unknown metrics subcommand. Use: list, get' } + } + break + + case 'segments': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/segments/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/segments/${id}/`) + break + } + default: + result = { error: 'Unknown segments subcommand. Use: list, get' } + } + break + + case 'templates': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.filter) params.set('filter', args.filter) + if (args['page-size']) params.set('page[size]', args['page-size']) + const qs = params.toString() + result = await api('GET', `/templates/${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/templates/${id}/`) + break + } + default: + result = { error: 'Unknown templates subcommand. Use: list, get' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + profiles: 'profiles [list | get --id <id> | create --email <email> | update --id <id>]', + lists: 'lists [list | get --id <id> | create --name <name> | delete --id <id> | add-profiles --id <list-id> --profiles <id1,id2> | remove-profiles --id <list-id> --profiles <id1,id2>]', + events: 'events [list | get --id <id> | create --metric <name> --email <email>]', + campaigns: 'campaigns [list | get --id <id>]', + flows: 'flows [list | get --id <id> | update --id <id> --status <status>]', + metrics: 'metrics [list | get --id <id>]', + segments: 'segments [list | get --id <id>]', + templates: 'templates [list | get --id <id>]', + options: '--filter <filter> --page-size <n> --page-cursor <cursor>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/lemlist.js b/tools/clis/lemlist.js new file mode 100755 index 0000000..8225d86 --- /dev/null +++ b/tools/clis/lemlist.js @@ -0,0 +1,221 @@ +#!/usr/bin/env node + +const API_KEY = process.env.LEMLIST_API_KEY +const BASE_URL = 'https://api.lemlist.com/api' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'LEMLIST_API_KEY environment variable required' })) + process.exit(1) +} + +const AUTH = 'Basic ' + Buffer.from(`:${API_KEY}`).toString('base64') + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': AUTH, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const offset = args.offset ? Number(args.offset) : 0 + const limit = args.limit ? Number(args.limit) : 100 + + switch (cmd) { + case 'team': + switch (sub) { + case 'info': + result = await api('GET', '/team') + break + default: + result = { error: 'Unknown team subcommand. Use: info' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('offset', String(offset)) + params.set('limit', String(limit)) + result = await api('GET', `/campaigns?${params}`) + break + } + case 'get': { + if (!args.id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${args.id}`) + break + } + case 'stats': { + if (!args.id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${args.id}/stats`) + break + } + case 'export': { + if (!args.id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${args.id}/export`) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get, stats, export' } + } + break + + case 'leads': + switch (sub) { + case 'list': { + if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } + const params = new URLSearchParams() + params.set('offset', String(offset)) + params.set('limit', String(limit)) + result = await api('GET', `/campaigns/${args['campaign-id']}/leads?${params}`) + break + } + case 'get': { + if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`) + break + } + case 'add': { + if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + const body = {} + if (args['first-name']) body.firstName = args['first-name'] + if (args['last-name']) body.lastName = args['last-name'] + if (args.company) body.companyName = args.company + result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`, body) + break + } + case 'delete': { + if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`) + break + } + default: + result = { error: 'Unknown leads subcommand. Use: list, get, add, delete' } + } + break + + case 'unsubscribes': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('offset', String(offset)) + params.set('limit', String(limit)) + result = await api('GET', `/unsubscribes?${params}`) + break + } + case 'add': { + if (!args.email) { result = { error: '--email required' }; break } + result = await api('POST', `/unsubscribes/${encodeURIComponent(args.email)}`) + break + } + case 'delete': { + if (!args.email) { result = { error: '--email required' }; break } + result = await api('DELETE', `/unsubscribes/${encodeURIComponent(args.email)}`) + break + } + default: + result = { error: 'Unknown unsubscribes subcommand. Use: list, add, delete' } + } + break + + case 'activities': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args['campaign-id']) params.set('campaignId', args['campaign-id']) + if (args.type) params.set('type', args.type) + params.set('offset', String(offset)) + params.set('limit', String(limit)) + result = await api('GET', `/activities?${params}`) + break + } + default: + result = { error: 'Unknown activities subcommand. Use: list' } + } + break + + case 'hooks': + switch (sub) { + case 'list': + result = await api('GET', '/hooks') + break + case 'create': { + if (!args['target-url']) { result = { error: '--target-url required' }; break } + if (!args.event) { result = { error: '--event required' }; break } + result = await api('POST', '/hooks', { targetUrl: args['target-url'], event: args.event }) + break + } + case 'delete': { + if (!args.id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/hooks/${args.id}`) + break + } + default: + result = { error: 'Unknown hooks subcommand. Use: list, create, delete' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + team: 'team info', + campaigns: 'campaigns [list | get --id <id> | stats --id <id> | export --id <id>] [--offset 0] [--limit 100]', + leads: 'leads [list | get --email <email> | add --email <email> | delete --email <email>] --campaign-id <id> [--first-name <name>] [--last-name <name>] [--company <name>]', + unsubscribes: 'unsubscribes [list | add --email <email> | delete --email <email>] [--offset 0] [--limit 100]', + activities: 'activities list [--campaign-id <id>] [--type emailsSent|emailsOpened|emailsClicked|emailsReplied|emailsBounced] [--offset 0] [--limit 100]', + hooks: 'hooks [list | create --target-url <url> --event <event> | delete --id <id>]', + options: '--dry-run --offset <n> --limit <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/linkedin-ads.js b/tools/clis/linkedin-ads.js new file mode 100755 index 0000000..801500f --- /dev/null +++ b/tools/clis/linkedin-ads.js @@ -0,0 +1,185 @@ +#!/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 headers = { + 'Authorization': `Bearer ${TOKEN}`, + 'X-RestLi-Protocol-Version': '2.0.0', + 'Content-Type': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + 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 } + if (!args['campaign-group-id']) { result = { error: '--campaign-group-id required' }; break } + const body = { + account: `urn:li:sponsoredAccount:${args['account-id']}`, + campaignGroup: `urn:li:sponsoredCampaignGroup:${args['campaign-group-id']}`, + name: args.name, + type: args.type || 'SPONSORED_UPDATES', + costType: args['cost-type'] || 'CPC', + unitCost: { + amount: parseFloat(args['unit-cost'] || '5.00'), + currencyCode: 'USD', + }, + dailyBudget: { + amount: parseFloat(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) +}) diff --git a/tools/clis/livestorm.js b/tools/clis/livestorm.js new file mode 100755 index 0000000..23df978 --- /dev/null +++ b/tools/clis/livestorm.js @@ -0,0 +1,292 @@ +#!/usr/bin/env node + +const API_TOKEN = process.env.LIVESTORM_API_TOKEN +const BASE_URL = 'https://api.livestorm.co/v1' + +if (!API_TOKEN) { + console.error(JSON.stringify({ error: 'LIVESTORM_API_TOKEN environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const headers = { + 'Authorization': `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/vnd.api+json', + 'Accept': 'application/vnd.api+json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const page = args.page ? Number(args.page) : 1 + const perPage = args['per-page'] ? Number(args['per-page']) : 25 + + switch (cmd) { + case 'ping': + result = await api('GET', '/ping') + break + + case 'events': + switch (sub) { + case 'list': { + let qs = `?page[number]=${page}&page[size]=${perPage}` + if (args.title) qs += `&filter[title]=${encodeURIComponent(args.title)}` + result = await api('GET', `/events${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/events/${id}`) + break + } + case 'create': { + const title = args.title + if (!title) { result = { error: '--title required' }; break } + const payload = { + data: { + type: 'events', + attributes: { + title, + slug: args.slug || title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), + }, + }, + } + if (args.description) payload.data.attributes.description = args.description + if (args['estimated-duration']) payload.data.attributes.estimated_duration = Number(args['estimated-duration']) + result = await api('POST', '/events', payload) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const attrs = {} + if (args.title) attrs.title = args.title + if (args.description) attrs.description = args.description + if (args.slug) attrs.slug = args.slug + result = await api('PATCH', `/events/${id}`, { + data: { type: 'events', id, attributes: attrs }, + }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/events/${id}`) + break + } + case 'people': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + let qs = `?page[number]=${page}&page[size]=${perPage}` + result = await api('GET', `/events/${id}/people${qs}`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list, get, create, update, delete, people' } + } + break + + case 'sessions': + switch (sub) { + case 'list': { + let qs = `?page[number]=${page}&page[size]=${perPage}` + result = await api('GET', `/sessions${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/sessions/${id}`) + break + } + case 'create': { + const eventId = args['event-id'] + if (!eventId) { result = { error: '--event-id required' }; break } + const payload = { + data: { + type: 'sessions', + attributes: {}, + }, + } + if (args['estimated-started-at']) payload.data.attributes.estimated_started_at = args['estimated-started-at'] + if (args.timezone) payload.data.attributes.timezone = args.timezone + result = await api('POST', `/events/${eventId}/sessions`, payload) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/sessions/${id}`) + break + } + case 'people': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + let qs = `?page[number]=${page}&page[size]=${perPage}` + result = await api('GET', `/sessions/${id}/people${qs}`) + break + } + case 'register': { + const id = args.id + if (!id) { result = { error: '--id (session id) required' }; break } + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const fields = { email } + if (args['first-name']) fields.first_name = args['first-name'] + if (args['last-name']) fields.last_name = args['last-name'] + result = await api('POST', `/sessions/${id}/people`, { + data: { + type: 'people', + attributes: { fields }, + }, + }) + break + } + case 'unregister': { + const id = args.id + if (!id) { result = { error: '--id (session id) required' }; break } + const email = args.email + if (!email) { result = { error: '--email required' }; break } + result = await api('DELETE', `/sessions/${id}/people?filter[email]=${encodeURIComponent(email)}`) + break + } + case 'chat': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + let qs = `?page[number]=${page}&page[size]=${perPage}` + result = await api('GET', `/sessions/${id}/chat-messages${qs}`) + break + } + case 'questions': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + let qs = `?page[number]=${page}&page[size]=${perPage}` + result = await api('GET', `/sessions/${id}/questions${qs}`) + break + } + case 'recordings': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/sessions/${id}/recordings`) + break + } + default: + result = { error: 'Unknown sessions subcommand. Use: list, get, create, delete, people, register, unregister, chat, questions, recordings' } + } + break + + case 'people': + switch (sub) { + case 'list': { + let qs = `?page[number]=${page}&page[size]=${perPage}` + if (args.email) qs += `&filter[email]=${encodeURIComponent(args.email)}` + result = await api('GET', `/people${qs}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/people/${id}`) + break + } + default: + result = { error: 'Unknown people subcommand. Use: list, get' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': { + result = await api('GET', '/webhooks') + break + } + case 'create': { + const url = args.url + if (!url) { result = { error: '--url required' }; break } + const eventName = args.event || 'attendance' + result = await api('POST', '/webhooks', { + data: { + type: 'webhooks', + attributes: { + target_url: url, + event_name: eventName, + }, + }, + }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/webhooks/${id}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' } + } + break + + case 'organization': + result = await api('GET', '/organization') + break + + default: + result = { + error: 'Unknown command', + usage: { + ping: 'ping', + events: 'events [list | get --id <id> | create --title <t> | update --id <id> --title <t> | delete --id <id> | people --id <id>]', + sessions: 'sessions [list | get --id <id> | create --event-id <id> | delete --id <id> | people --id <id> | register --id <id> --email <e> | unregister --id <id> --email <e> | chat --id <id> | questions --id <id> | recordings --id <id>]', + people: 'people [list --email <e> | get --id <id>]', + webhooks: 'webhooks [list | create --url <url> --event <name> | delete --id <id>]', + organization: 'organization', + options: '--page <n> --per-page <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/mailchimp.js b/tools/clis/mailchimp.js new file mode 100755 index 0000000..5add6aa --- /dev/null +++ b/tools/clis/mailchimp.js @@ -0,0 +1,220 @@ +#!/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 auth = 'Basic ' + Buffer.from(`anystring:${API_KEY}`).toString('base64') + const headers = { + 'Authorization': auth, + 'Content-Type': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + 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': + if (!rest[0]) { result = { error: 'List ID required' }; break } + 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 (!rest[0]) { result = { error: 'List ID required' }; break } + if (!args.email) { result = { error: '--email required' }; break } + 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': + if (!rest[0]) { result = { error: 'Campaign ID required' }; break } + result = await api('GET', `/campaigns/${rest[0]}`) + break + case 'create': { + if (!args['list-id']) { result = { error: '--list-id required' }; break } + 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': + if (!rest[0]) { result = { error: 'Campaign ID required' }; break } + 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': + if (!rest[0]) { result = { error: 'Campaign ID required' }; break } + 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) +}) diff --git a/tools/clis/mention-me.js b/tools/clis/mention-me.js new file mode 100755 index 0000000..56e9881 --- /dev/null +++ b/tools/clis/mention-me.js @@ -0,0 +1,161 @@ +#!/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 headers = { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + 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': { + const refId = rest[0] || args.id + if (!refId) { result = { error: 'Referral ID required (positional arg or --id)' }; break } + result = await api('GET', `/referral/${refId}`) + break + } + case 'list': { + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } + 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': + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } + 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': + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } + result = await api('GET', `/referrer/${args['customer-id']}/rewards`) + break + case 'redeem': { + if (!args['customer-id']) { result = { error: '--customer-id required' }; break } + 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) +}) diff --git a/tools/clis/meta-ads.js b/tools/clis/meta-ads.js new file mode 100755 index 0000000..d19b028 --- /dev/null +++ b/tools/clis/meta-ads.js @@ -0,0 +1,181 @@ +#!/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 url = `${BASE_URL}${path}` + const opts = { + method, + headers: { 'Authorization': `Bearer ${TOKEN}` }, + } + if (body) { + opts.headers['Content-Type'] = 'application/json' + opts.body = JSON.stringify(body) + } + if (args['dry-run']) { + return { _dry_run: true, method, url, headers: { ...opts.headers, Authorization: '***' }, body: body || undefined } + } + 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) +}) diff --git a/tools/clis/mixpanel.js b/tools/clis/mixpanel.js new file mode 100755 index 0000000..671ca45 --- /dev/null +++ b/tools/clis/mixpanel.js @@ -0,0 +1,248 @@ +#!/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 headers = { 'Content-Type': 'application/json' } + if (args['dry-run']) { + const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined + if (Array.isArray(maskedBody)) maskedBody.forEach(item => { + if (item.properties && item.properties.token) item.properties.token = '***' + if (item.$token) item.$token = '***' + }) + return { _dry_run: true, method, url: `${INGESTION_URL}${path}`, headers, body: maskedBody } + } + const res = await fetch(`${INGESTION_URL}${path}`, { + method, + headers, + 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 headers = { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url, headers: { ...headers, Authorization: '***' } } + } + const res = await fetch(url, { + method, + headers, + }) + 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 headers = { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method: 'POST', url: `${QUERY_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${QUERY_URL}${path}`, { + method: 'POST', + headers, + 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 } + let properties + try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break } + 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 } + let properties + try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break } + 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'] || new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10), + to_date: args['to-date'] || new Date().toISOString().slice(0, 10), + }, + }, + } + 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': { + if (!args['from-date'] || !args['to-date']) { result = { error: '--from-date and --to-date required (YYYY-MM-DD)' }; break } + const params = new URLSearchParams() + params.set('from_date', args['from-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']) + if (args.event) params.set('event', JSON.stringify([args.event])) + 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) +}) diff --git a/tools/clis/onesignal.js b/tools/clis/onesignal.js new file mode 100755 index 0000000..63431e1 --- /dev/null +++ b/tools/clis/onesignal.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node + +const REST_API_KEY = process.env.ONESIGNAL_REST_API_KEY +const APP_ID = process.env.ONESIGNAL_APP_ID +const BASE_URL = 'https://api.onesignal.com' + +if (!REST_API_KEY) { + console.error(JSON.stringify({ error: 'ONESIGNAL_REST_API_KEY environment variable required' })) + process.exit(1) +} + +if (!APP_ID) { + console.error(JSON.stringify({ error: 'ONESIGNAL_APP_ID environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const headers = { + 'Authorization': `Basic ${REST_API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const limit = args.limit ? Number(args.limit) : 50 + const offset = args.offset ? Number(args.offset) : 0 + + switch (cmd) { + case 'notifications': + switch (sub) { + case 'send': { + const message = args.message + if (!message) { result = { error: '--message required' }; break } + const payload = { + app_id: APP_ID, + contents: { en: message }, + } + if (args.heading) payload.headings = { en: args.heading } + if (args.url) payload.url = args.url + if (args.data) { + try { payload.data = JSON.parse(args.data) } catch { payload.data = { value: args.data } } + } + if (args.segment) { + payload.included_segments = args.segment.split(',') + } else if (args.emails) { + payload.include_email_tokens = args.emails.split(',') + } else if (args['player-ids']) { + payload.include_player_ids = args['player-ids'].split(',') + } else if (args.aliases) { + try { + payload.include_aliases = JSON.parse(args.aliases) + } catch { + payload.include_aliases = { external_id: args.aliases.split(',') } + } + payload.target_channel = args.channel || 'push' + } else { + payload.included_segments = ['Subscribed Users'] + } + if (args['send-after']) payload.send_after = args['send-after'] + if (args.ttl) payload.ttl = Number(args.ttl) + result = await api('POST', '/api/v1/notifications', payload) + break + } + case 'list': { + result = await api('GET', `/api/v1/notifications?app_id=${APP_ID}&limit=${limit}&offset=${offset}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/api/v1/notifications/${id}?app_id=${APP_ID}`) + break + } + case 'cancel': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/api/v1/notifications/${id}?app_id=${APP_ID}`) + break + } + default: + result = { error: 'Unknown notifications subcommand. Use: send, list, get, cancel' } + } + break + + case 'segments': + switch (sub) { + case 'list': { + result = await api('GET', `/api/v1/apps/${APP_ID}/segments?offset=${offset}&limit=${limit}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + let filters + try { filters = args.filters ? JSON.parse(args.filters) : [{ field: 'session_count', relation: '>', value: '0' }] } catch { result = { error: 'Invalid JSON in --filters' }; break } + result = await api('POST', `/api/v1/apps/${APP_ID}/segments`, { name, filters }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/api/v1/apps/${APP_ID}/segments/${id}`) + break + } + default: + result = { error: 'Unknown segments subcommand. Use: list, create, delete' } + } + break + + case 'users': + switch (sub) { + case 'get': { + const aliasLabel = args['alias-label'] || 'external_id' + const aliasId = args['alias-id'] + if (!aliasId) { result = { error: '--alias-id required' }; break } + result = await api('GET', `/api/v1/apps/${APP_ID}/users/by/${aliasLabel}/${aliasId}`) + break + } + case 'create': { + const payload = {} + if (args['external-id']) { + payload.identity = { external_id: args['external-id'] } + } + if (args.email) { + payload.subscriptions = [{ type: 'Email', token: args.email }] + } + if (args.tags) { + try { payload.tags = JSON.parse(args.tags) } catch { result = { error: 'Invalid --tags JSON' }; break } + } + result = await api('POST', `/api/v1/apps/${APP_ID}/users`, payload) + break + } + case 'delete': { + const aliasLabel = args['alias-label'] || 'external_id' + const aliasId = args['alias-id'] + if (!aliasId) { result = { error: '--alias-id required' }; break } + result = await api('DELETE', `/api/v1/apps/${APP_ID}/users/by/${aliasLabel}/${aliasId}`) + break + } + default: + result = { error: 'Unknown users subcommand. Use: get, create, delete' } + } + break + + case 'templates': + switch (sub) { + case 'list': { + result = await api('GET', `/api/v1/templates?app_id=${APP_ID}&limit=${limit}&offset=${offset}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/api/v1/templates/${id}?app_id=${APP_ID}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const payload = { app_id: APP_ID, name } + if (args.message) payload.contents = { en: args.message } + if (args.heading) payload.headings = { en: args.heading } + result = await api('POST', '/api/v1/templates', payload) + break + } + default: + result = { error: 'Unknown templates subcommand. Use: list, get, create' } + } + break + + case 'app': + switch (sub) { + case 'get': { + result = await api('GET', `/api/v1/apps/${APP_ID}`) + break + } + default: + result = { error: 'Unknown app subcommand. Use: get' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + notifications: 'notifications [send --message <msg> --segment <s> | list | get --id <id> | cancel --id <id>]', + segments: 'segments [list | create --name <n> --filters <json> | delete --id <id>]', + users: 'users [get --alias-id <id> | create --external-id <id> --email <e> | delete --alias-id <id>]', + templates: 'templates [list | get --id <id> | create --name <n> --message <msg>]', + app: 'app [get]', + options: '--limit <n> --offset <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/optimizely.js b/tools/clis/optimizely.js new file mode 100755 index 0000000..26a2318 --- /dev/null +++ b/tools/clis/optimizely.js @@ -0,0 +1,233 @@ +#!/usr/bin/env node + +const API_KEY = process.env.OPTIMIZELY_API_KEY +const BASE_URL = 'https://api.optimizely.com/v2' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'OPTIMIZELY_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const projectId = args['project-id'] + const page = args.page ? Number(args.page) : 1 + const perPage = args['per-page'] ? Number(args['per-page']) : 25 + + switch (cmd) { + case 'projects': + switch (sub) { + case 'list': + result = await api('GET', `/projects?page=${page}&per_page=${perPage}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/projects/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { name } + if (args.platform) body.platform = args.platform + result = await api('POST', '/projects', body) + break + } + default: + result = { error: 'Unknown projects subcommand. Use: list, get, create' } + } + break + + case 'experiments': + switch (sub) { + case 'list': { + if (!projectId) { result = { error: '--project-id required' }; break } + const params = new URLSearchParams({ project_id: projectId, page: String(page), per_page: String(perPage) }) + if (args.status) params.set('status', args.status) + result = await api('GET', `/experiments?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/experiments/${id}`) + break + } + case 'create': { + if (!projectId) { result = { error: '--project-id required' }; break } + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { + project_id: Number(projectId), + name, + type: args.type || 'a/b', + status: 'not_started', + } + if (args['traffic-allocation']) body.traffic_allocation = Number(args['traffic-allocation']) + result = await api('POST', '/experiments', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.status) body.status = args.status + if (args['traffic-allocation']) body.traffic_allocation = Number(args['traffic-allocation']) + result = await api('PATCH', `/experiments/${id}`, body) + break + } + case 'results': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + 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']) + const qs = params.toString() + result = await api('GET', `/experiments/${id}/results${qs ? '?' + qs : ''}`) + break + } + case 'archive': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('PATCH', `/experiments/${id}`, { status: 'archived' }) + break + } + default: + result = { error: 'Unknown experiments subcommand. Use: list, get, create, update, results, archive' } + } + break + + case 'campaigns': + switch (sub) { + case 'list': { + if (!projectId) { result = { error: '--project-id required' }; break } + result = await api('GET', `/campaigns?project_id=${projectId}&page=${page}&per_page=${perPage}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${id}`) + break + } + case 'results': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/campaigns/${id}/results`) + break + } + default: + result = { error: 'Unknown campaigns subcommand. Use: list, get, results' } + } + break + + case 'audiences': + switch (sub) { + case 'list': { + if (!projectId) { result = { error: '--project-id required' }; break } + result = await api('GET', `/audiences?project_id=${projectId}&page=${page}&per_page=${perPage}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/audiences/${id}`) + break + } + default: + result = { error: 'Unknown audiences subcommand. Use: list, get' } + } + break + + case 'events': + switch (sub) { + case 'list': { + if (!projectId) { result = { error: '--project-id required' }; break } + result = await api('GET', `/events?project_id=${projectId}&page=${page}&per_page=${perPage}`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list' } + } + break + + case 'pages': + switch (sub) { + case 'list': { + if (!projectId) { result = { error: '--project-id required' }; break } + result = await api('GET', `/pages?project_id=${projectId}&page=${page}&per_page=${perPage}`) + break + } + default: + result = { error: 'Unknown pages subcommand. Use: list' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + projects: 'projects [list | get --id <id> | create --name <name>]', + experiments: 'experiments [list --project-id <id> | get --id <id> | create --project-id <id> --name <name> | update --id <id> --status <status> | results --id <id> | archive --id <id>]', + campaigns: 'campaigns [list --project-id <id> | get --id <id> | results --id <id>]', + audiences: 'audiences [list --project-id <id> | get --id <id>]', + events: 'events list --project-id <id>', + pages: 'pages list --project-id <id>', + options: '--page <n> --per-page <n> --status <not_started|running|paused|archived>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/paddle.js b/tools/clis/paddle.js new file mode 100755 index 0000000..0349981 --- /dev/null +++ b/tools/clis/paddle.js @@ -0,0 +1,385 @@ +#!/usr/bin/env node + +const API_KEY = process.env.PADDLE_API_KEY +const BASE_URL = process.env.PADDLE_SANDBOX === 'true' + ? 'https://sandbox-api.paddle.com' + : 'https://api.paddle.com' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'PADDLE_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +function buildQuery() { + const params = new URLSearchParams() + if (args.status) params.set('status', args.status) + if (args.after) params.set('after', args.after) + if (args['per-page']) params.set('per_page', args['per-page']) + if (args['order-by']) params.set('order_by', args['order-by']) + return params.toString() ? `?${params.toString()}` : '' +} + +async function main() { + let result + + switch (cmd) { + case 'products': + switch (sub) { + case 'list': + result = await api('GET', `/products${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/products/${id}`) + break + } + case 'create': { + const name = args.name + const taxCategory = args['tax-category'] + if (!name) { result = { error: '--name required' }; break } + if (!taxCategory) { result = { error: '--tax-category required (e.g. standard, digital-goods, saas)' }; break } + const body = { name, tax_category: taxCategory } + if (args.description) body.description = args.description + if (args['image-url']) body.image_url = args['image-url'] + result = await api('POST', '/products', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.description) body.description = args.description + if (args.status) body.status = args.status + if (args['tax-category']) body.tax_category = args['tax-category'] + result = await api('PATCH', `/products/${id}`, body) + break + } + default: + result = { error: 'Unknown products subcommand. Use: list, get, create, update' } + } + break + + case 'prices': + switch (sub) { + case 'list': + result = await api('GET', `/prices${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/prices/${id}`) + break + } + case 'create': { + const productId = args['product-id'] + const amount = args.amount + const currency = args.currency || 'USD' + if (!productId) { result = { error: '--product-id required' }; break } + if (!amount) { result = { error: '--amount required (in lowest denomination, e.g. cents)' }; break } + const body = { + product_id: productId, + description: args.description || 'Price', + unit_price: { amount, currency_code: currency }, + } + if (args.interval && args.frequency) { + body.billing_cycle = { + interval: args.interval, + frequency: Number(args.frequency), + } + } + result = await api('POST', '/prices', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.description) body.description = args.description + if (args.amount && args.currency) { + body.unit_price = { amount: args.amount, currency_code: args.currency } + } + if (args.status) body.status = args.status + result = await api('PATCH', `/prices/${id}`, body) + break + } + default: + result = { error: 'Unknown prices subcommand. Use: list, get, create, update' } + } + break + + case 'customers': + switch (sub) { + case 'list': + result = await api('GET', `/customers${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/customers/${id}`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { email } + if (args.name) body.name = args.name + result = await api('POST', '/customers', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.email) body.email = args.email + if (args.status) body.status = args.status + result = await api('PATCH', `/customers/${id}`, body) + break + } + default: + result = { error: 'Unknown customers subcommand. Use: list, get, create, update' } + } + break + + case 'subscriptions': + switch (sub) { + case 'list': + result = await api('GET', `/subscriptions${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/subscriptions/${id}`) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args['proration-billing-mode']) body.proration_billing_mode = args['proration-billing-mode'] + if (args['scheduled-change']) { + try { body.scheduled_change = JSON.parse(args['scheduled-change']) } catch { result = { error: 'Invalid JSON in --scheduled-change' }; break } + } + result = await api('PATCH', `/subscriptions/${id}`, body) + break + } + case 'cancel': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = { effective_from: args['effective-from'] || 'next_billing_period' } + result = await api('POST', `/subscriptions/${id}/cancel`, body) + break + } + case 'pause': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args['resume-at']) body.resume_at = args['resume-at'] + result = await api('POST', `/subscriptions/${id}/pause`, body) + break + } + case 'resume': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = { effective_from: args['effective-from'] || 'immediately' } + result = await api('POST', `/subscriptions/${id}/resume`, body) + break + } + default: + result = { error: 'Unknown subscriptions subcommand. Use: list, get, update, cancel, pause, resume' } + } + break + + case 'transactions': + switch (sub) { + case 'list': + result = await api('GET', `/transactions${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/transactions/${id}`) + break + } + case 'create': { + const items = args.items + if (!items) { result = { error: '--items required (JSON array of {price_id, quantity})' }; break } + let parsedItems + try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break } + const body = { items: parsedItems } + if (args['customer-id']) body.customer_id = args['customer-id'] + result = await api('POST', '/transactions', body) + break + } + default: + result = { error: 'Unknown transactions subcommand. Use: list, get, create' } + } + break + + case 'discounts': + switch (sub) { + case 'list': + result = await api('GET', `/discounts${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/discounts/${id}`) + break + } + case 'create': { + const amount = args.amount + const type = args.type + if (!amount) { result = { error: '--amount required' }; break } + if (!type) { result = { error: '--type required (flat, flat_per_seat, percentage)' }; break } + const body = { + amount, + type, + description: args.description || 'Discount', + } + if (args.code) body.code = args.code + if (args['max-uses']) body.maximum_recurring_intervals = Number(args['max-uses']) + if (args['currency-code']) body.currency_code = args['currency-code'] + result = await api('POST', '/discounts', body) + break + } + default: + result = { error: 'Unknown discounts subcommand. Use: list, get, create' } + } + break + + case 'adjustments': + switch (sub) { + case 'list': + result = await api('GET', `/adjustments${buildQuery()}`) + break + case 'create': { + const transactionId = args['transaction-id'] + const action = args.action + const items = args.items + const reason = args.reason + if (!transactionId) { result = { error: '--transaction-id required' }; break } + if (!action) { result = { error: '--action required (refund, credit, chargeback)' }; break } + if (!reason) { result = { error: '--reason required' }; break } + if (!items) { result = { error: '--items required (JSON array of {item_id, type, amount})' }; break } + let parsedItems + try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break } + result = await api('POST', '/adjustments', { + transaction_id: transactionId, + action, + reason, + items: parsedItems, + }) + break + } + default: + result = { error: 'Unknown adjustments subcommand. Use: list, create' } + } + break + + case 'events': + switch (sub) { + case 'list': + result = await api('GET', `/events${buildQuery()}`) + break + case 'types': + result = await api('GET', '/event-types') + break + default: + result = { error: 'Unknown events subcommand. Use: list, types' } + } + break + + case 'notifications': + switch (sub) { + case 'list': + result = await api('GET', `/notifications${buildQuery()}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/notifications/${id}`) + break + } + case 'replay': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/notifications/${id}/replay`) + break + } + default: + result = { error: 'Unknown notifications subcommand. Use: list, get, replay' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + products: 'products [list | get --id <id> | create --name <n> --tax-category <cat> | update --id <id>]', + prices: 'prices [list | get --id <id> | create --product-id <id> --amount <amt> [--currency USD] [--interval month --frequency 1] | update --id <id>]', + customers: 'customers [list | get --id <id> | create --email <email> [--name <name>] | update --id <id>]', + subscriptions: 'subscriptions [list | get --id <id> | update --id <id> | cancel --id <id> [--effective-from next_billing_period] | pause --id <id> | resume --id <id>]', + transactions: 'transactions [list | get --id <id> | create --items <json>]', + discounts: 'discounts [list | get --id <id> | create --amount <amt> --type <type> [--code <code>]]', + adjustments: 'adjustments [list | create --transaction-id <id> --action <action> --reason <reason> --items <json>]', + events: 'events [list | types]', + notifications: 'notifications [list | get --id <id> | replay --id <id>]', + env: 'Set PADDLE_SANDBOX=true for sandbox environment', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/partnerstack.js b/tools/clis/partnerstack.js new file mode 100755 index 0000000..b6c3d2e --- /dev/null +++ b/tools/clis/partnerstack.js @@ -0,0 +1,382 @@ +#!/usr/bin/env node + +const PUBLIC_KEY = process.env.PARTNERSTACK_PUBLIC_KEY +const SECRET_KEY = process.env.PARTNERSTACK_SECRET_KEY +const BASE_URL = 'https://api.partnerstack.com/api/v2' + +if (!PUBLIC_KEY || !SECRET_KEY) { + console.error(JSON.stringify({ error: 'PARTNERSTACK_PUBLIC_KEY and PARTNERSTACK_SECRET_KEY environment variables required' })) + process.exit(1) +} + +const AUTH = 'Basic ' + Buffer.from(`${PUBLIC_KEY}:${SECRET_KEY}`).toString('base64') + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': AUTH, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ +const limit = args.limit ? Number(args.limit) : 10 + +function buildQuery() { + const params = new URLSearchParams() + if (args.limit) params.set('limit', args.limit) + if (args.after) params.set('starting_after', args.after) + if (args.before) params.set('ending_before', args.before) + if (args['order-by']) params.set('order_by', args['order-by']) + return params.toString() ? `?${params.toString()}` : '' +} + +async function main() { + let result + + switch (cmd) { + case 'partnerships': + switch (sub) { + case 'list': + result = await api('GET', `/partnerships${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (partnership key)' }; break } + result = await api('GET', `/partnerships/${key}`) + break + } + case 'create': { + const email = args.email + const group = args.group + if (!email) { result = { error: '--email required' }; break } + if (!group) { result = { error: '--group required (group key)' }; break } + const body = { email, group_key: group } + if (args.name) body.name = args.name + if (args['first-name']) body.first_name = args['first-name'] + if (args['last-name']) body.last_name = args['last-name'] + result = await api('POST', '/partnerships', body) + break + } + case 'update': { + const key = args.key + if (!key) { result = { error: '--key required (partnership key)' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.group) body.group_key = args.group + result = await api('PATCH', `/partnerships/${key}`, body) + break + } + default: + result = { error: 'Unknown partnerships subcommand. Use: list, get, create, update' } + } + break + + case 'customers': + switch (sub) { + case 'list': + result = await api('GET', `/customers${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (customer key)' }; break } + result = await api('GET', `/customers/${key}`) + break + } + case 'create': { + const email = args.email + const partnerKey = args['partner-key'] + if (!email) { result = { error: '--email required' }; break } + if (!partnerKey) { result = { error: '--partner-key required' }; break } + const body = { email, partner_key: partnerKey } + if (args.name) body.name = args.name + if (args['customer-key']) body.customer_key = args['customer-key'] + result = await api('POST', '/customers', body) + break + } + case 'update': { + const key = args.key + if (!key) { result = { error: '--key required (customer key)' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.email) body.email = args.email + if (args['partner-key']) body.partner_key = args['partner-key'] + result = await api('PATCH', `/customers/${key}`, body) + break + } + case 'delete': { + const key = args.key + if (!key) { result = { error: '--key required (customer key)' }; break } + result = await api('DELETE', `/customers/${key}`) + break + } + default: + result = { error: 'Unknown customers subcommand. Use: list, get, create, update, delete' } + } + break + + case 'transactions': + switch (sub) { + case 'list': + result = await api('GET', `/transactions${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (transaction key)' }; break } + result = await api('GET', `/transactions/${key}`) + break + } + case 'create': { + const customerKey = args['customer-key'] + const amount = args.amount + if (!customerKey) { result = { error: '--customer-key required' }; break } + if (!amount) { result = { error: '--amount required (in cents)' }; break } + const body = { + customer_key: customerKey, + amount: Number(amount), + } + if (args.currency) body.currency = args.currency + if (args.category) body.category = args.category + if (args['product-key']) body.product_key = args['product-key'] + result = await api('POST', '/transactions', body) + break + } + case 'delete': { + const key = args.key + if (!key) { result = { error: '--key required (transaction key)' }; break } + result = await api('DELETE', `/transactions/${key}`) + break + } + default: + result = { error: 'Unknown transactions subcommand. Use: list, get, create, delete' } + } + break + + case 'deals': + switch (sub) { + case 'list': + result = await api('GET', `/deals${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (deal key)' }; break } + result = await api('GET', `/deals/${key}`) + break + } + case 'create': { + const partnerKey = args['partner-key'] + const name = args.name + if (!partnerKey) { result = { error: '--partner-key required' }; break } + if (!name) { result = { error: '--name required' }; break } + const body = { partner_key: partnerKey, name } + if (args.amount) body.amount = Number(args.amount) + if (args.currency) body.currency = args.currency + if (args.stage) body.stage = args.stage + result = await api('POST', '/deals', body) + break + } + case 'update': { + const key = args.key + if (!key) { result = { error: '--key required (deal key)' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.amount) body.amount = Number(args.amount) + if (args.stage) body.stage = args.stage + if (args.status) body.status = args.status + result = await api('PATCH', `/deals/${key}`, body) + break + } + case 'archive': { + const key = args.key + if (!key) { result = { error: '--key required (deal key)' }; break } + result = await api('DELETE', `/deals/${key}`) + break + } + default: + result = { error: 'Unknown deals subcommand. Use: list, get, create, update, archive' } + } + break + + case 'actions': + switch (sub) { + case 'list': + result = await api('GET', `/actions${buildQuery()}`) + break + case 'create': { + const customerKey = args['customer-key'] + const actionKey = args['action-key'] + if (!customerKey) { result = { error: '--customer-key required' }; break } + if (!actionKey) { result = { error: '--action-key required' }; break } + const body = { + customer_key: customerKey, + key: actionKey, + } + if (args.value) body.value = Number(args.value) + result = await api('POST', '/actions', body) + break + } + default: + result = { error: 'Unknown actions subcommand. Use: list, create' } + } + break + + case 'rewards': + switch (sub) { + case 'list': + result = await api('GET', `/rewards${buildQuery()}`) + break + case 'create': { + const partnerKey = args['partner-key'] + const amount = args.amount + if (!partnerKey) { result = { error: '--partner-key required' }; break } + if (!amount) { result = { error: '--amount required (in cents)' }; break } + const body = { + partner_key: partnerKey, + amount: Number(amount), + } + if (args.description) body.description = args.description + if (args.currency) body.currency = args.currency + result = await api('POST', '/rewards', body) + break + } + default: + result = { error: 'Unknown rewards subcommand. Use: list, create' } + } + break + + case 'leads': + switch (sub) { + case 'list': + result = await api('GET', `/leads${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (lead key)' }; break } + result = await api('GET', `/leads/${key}`) + break + } + case 'create': { + const partnerKey = args['partner-key'] + const email = args.email + if (!partnerKey) { result = { error: '--partner-key required' }; break } + if (!email) { result = { error: '--email required' }; break } + const body = { partner_key: partnerKey, email } + if (args.name) body.name = args.name + if (args.company) body.company = args.company + result = await api('POST', '/leads', body) + break + } + case 'update': { + const key = args.key + if (!key) { result = { error: '--key required (lead key)' }; break } + const body = {} + if (args.email) body.email = args.email + if (args.name) body.name = args.name + if (args.status) body.status = args.status + result = await api('PATCH', `/leads/${key}`, body) + break + } + default: + result = { error: 'Unknown leads subcommand. Use: list, get, create, update' } + } + break + + case 'groups': + switch (sub) { + case 'list': + result = await api('GET', `/groups${buildQuery()}`) + break + default: + result = { error: 'Unknown groups subcommand. Use: list' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': + result = await api('GET', `/webhooks${buildQuery()}`) + break + case 'get': { + const key = args.key + if (!key) { result = { error: '--key required (webhook key)' }; break } + result = await api('GET', `/webhooks/${key}`) + break + } + case 'create': { + const target = args.target + if (!target) { result = { error: '--target required (webhook URL)' }; break } + const body = { target } + if (args.events) body.events = args.events.split(',') + result = await api('POST', '/webhooks', body) + break + } + case 'delete': { + const key = args.key + if (!key) { result = { error: '--key required (webhook key)' }; break } + result = await api('DELETE', `/webhooks/${key}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + partnerships: 'partnerships [list | get --key <key> | create --email <email> --group <group-key> | update --key <key>]', + customers: 'customers [list | get --key <key> | create --email <email> --partner-key <key> | update --key <key> | delete --key <key>]', + transactions: 'transactions [list | get --key <key> | create --customer-key <key> --amount <cents> | delete --key <key>]', + deals: 'deals [list | get --key <key> | create --partner-key <key> --name <name> | update --key <key> | archive --key <key>]', + actions: 'actions [list | create --customer-key <key> --action-key <key> [--value <n>]]', + rewards: 'rewards [list | create --partner-key <key> --amount <cents>]', + leads: 'leads [list | get --key <key> | create --partner-key <key> --email <email> | update --key <key>]', + groups: 'groups [list]', + webhooks: 'webhooks [list | get --key <key> | create --target <url> [--events <evt1,evt2>] | delete --key <key>]', + options: '--limit <n> --after <cursor> --before <cursor> --order-by <field>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/plausible.js b/tools/clis/plausible.js new file mode 100755 index 0000000..62f4111 --- /dev/null +++ b/tools/clis/plausible.js @@ -0,0 +1,249 @@ +#!/usr/bin/env node + +const API_KEY = process.env.PLAUSIBLE_API_KEY +const BASE_URL = process.env.PLAUSIBLE_BASE_URL || 'https://plausible.io' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'PLAUSIBLE_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const siteId = args['site-id'] + const dateRange = args['date-range'] || '30d' + const limit = args.limit ? Number(args.limit) : 100 + + switch (cmd) { + case 'stats': + if (!siteId) { result = { error: '--site-id required (your domain, e.g. example.com)' }; break } + switch (sub) { + case 'aggregate': { + const metrics = args.metrics?.split(',') || ['visitors', 'pageviews', 'bounce_rate', 'visit_duration'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + }) + break + } + case 'timeseries': { + const metrics = args.metrics?.split(',') || ['visitors', 'pageviews'] + const period = args.period || 'time:day' + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: [period], + }) + break + } + case 'pages': { + const metrics = args.metrics?.split(',') || ['visitors', 'pageviews'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: ['event:page'], + pagination: { limit }, + }) + break + } + case 'sources': { + const metrics = args.metrics?.split(',') || ['visitors', 'bounce_rate'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: ['visit:source'], + pagination: { limit }, + }) + break + } + case 'countries': { + const metrics = args.metrics?.split(',') || ['visitors', 'percentage'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: ['visit:country'], + pagination: { limit }, + }) + break + } + case 'devices': { + const metrics = args.metrics?.split(',') || ['visitors', 'percentage'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: ['visit:device'], + pagination: { limit }, + }) + break + } + case 'utm': { + const param = args.param || 'utm_source' + const metrics = args.metrics?.split(',') || ['visitors', 'bounce_rate'] + result = await api('POST', '/api/v2/query', { + site_id: siteId, + metrics, + date_range: dateRange, + dimensions: [`visit:${param}`], + pagination: { limit }, + }) + break + } + case 'query': { + const metrics = args.metrics?.split(',') + if (!metrics) { result = { error: '--metrics required (comma-separated)' }; break } + const body = { site_id: siteId, metrics, date_range: dateRange } + if (args.dimensions) body.dimensions = args.dimensions.split(',') + if (args.filters) { + try { body.filters = JSON.parse(args.filters) } catch { result = { error: '--filters must be valid JSON' }; break } + } + body.pagination = { limit } + result = await api('POST', '/api/v2/query', body) + break + } + case 'realtime': + result = await api('GET', `/api/v1/stats/realtime/visitors?site_id=${encodeURIComponent(siteId)}`) + break + default: + result = { error: 'Unknown stats subcommand. Use: aggregate, timeseries, pages, sources, countries, devices, utm, query, realtime' } + } + break + + case 'sites': + switch (sub) { + case 'list': + result = await api('GET', '/api/v1/sites') + break + case 'get': { + if (!siteId) { result = { error: '--site-id required' }; break } + result = await api('GET', `/api/v1/sites/${encodeURIComponent(siteId)}`) + break + } + case 'create': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + const body = { domain } + if (args.timezone) body.timezone = args.timezone + result = await api('POST', '/api/v1/sites', body) + break + } + case 'delete': { + if (!siteId) { result = { error: '--site-id required' }; break } + result = await api('DELETE', `/api/v1/sites/${encodeURIComponent(siteId)}`) + break + } + default: + result = { error: 'Unknown sites subcommand. Use: list, get, create, delete' } + } + break + + case 'goals': + if (!siteId) { result = { error: '--site-id required' }; break } + switch (sub) { + case 'list': + result = await api('GET', `/api/v1/sites/goals?site_id=${encodeURIComponent(siteId)}`) + break + case 'create': { + const goalType = args['goal-type'] + if (!goalType) { result = { error: '--goal-type required (event or page)' }; break } + const body = { site_id: siteId, goal_type: goalType } + if (goalType === 'event') { + if (!args['event-name']) { result = { error: '--event-name required for event goals' }; break } + body.event_name = args['event-name'] + } else if (goalType === 'page') { + if (!args['page-path']) { result = { error: '--page-path required for page goals' }; break } + body.page_path = args['page-path'] + } + result = await api('PUT', '/api/v1/sites/goals', body) + break + } + case 'delete': { + const goalId = args['goal-id'] + if (!goalId) { result = { error: '--goal-id required' }; break } + result = await api('DELETE', `/api/v1/sites/goals/${goalId}`, { site_id: siteId }) + break + } + default: + result = { error: 'Unknown goals subcommand. Use: list, create, delete' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + stats: { + aggregate: 'stats aggregate --site-id <domain> [--date-range <30d>] [--metrics <m1,m2>]', + timeseries: 'stats timeseries --site-id <domain> [--date-range <30d>] [--period <time:day>]', + pages: 'stats pages --site-id <domain> [--date-range <30d>] [--limit <n>]', + sources: 'stats sources --site-id <domain> [--date-range <30d>]', + countries: 'stats countries --site-id <domain> [--date-range <30d>]', + devices: 'stats devices --site-id <domain> [--date-range <30d>]', + utm: 'stats utm --site-id <domain> [--param <utm_source>] [--date-range <30d>]', + query: 'stats query --site-id <domain> --metrics <m1,m2> [--dimensions <d1,d2>] [--filters <json>]', + realtime: 'stats realtime --site-id <domain>', + }, + sites: 'sites [list | get --site-id <domain> | create --domain <domain> | delete --site-id <domain>]', + goals: 'goals [list | create --goal-type <event|page> --event-name <name> | delete --goal-id <id>] --site-id <domain>', + options: '--date-range <day|7d|30d|month|6mo|12mo|year> --limit <n>', + env: 'PLAUSIBLE_BASE_URL for self-hosted instances (default: https://plausible.io)', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/postmark.js b/tools/clis/postmark.js new file mode 100755 index 0000000..e66ca08 --- /dev/null +++ b/tools/clis/postmark.js @@ -0,0 +1,375 @@ +#!/usr/bin/env node + +const API_KEY = process.env.POSTMARK_API_KEY +const BASE_URL = 'https://api.postmarkapp.com' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'POSTMARK_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body, useAccountToken) { + if (args['dry-run']) { + const maskedHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json' } + if (useAccountToken) { + maskedHeaders['X-Postmark-Account-Token'] = '***' + } else { + maskedHeaders['X-Postmark-Server-Token'] = '***' + } + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: maskedHeaders, body: body || undefined } + } + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if (useAccountToken) { + headers['X-Postmark-Account-Token'] = API_KEY + } else { + headers['X-Postmark-Server-Token'] = API_KEY + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + 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 'email': + switch (sub) { + case 'send': { + const from = args.from + const to = args.to + const subject = args.subject + if (!from) { result = { error: '--from required' }; break } + if (!to) { result = { error: '--to required' }; break } + if (!subject) { result = { error: '--subject required' }; break } + const body = { + From: from, + To: to, + Subject: subject, + } + if (args.html) body.HtmlBody = args.html + if (args.text) body.TextBody = args.text + if (!args.html && !args.text) body.TextBody = '' + if (args.tag) body.Tag = args.tag + if (args.stream) body.MessageStream = args.stream + if (args['track-opens']) body.TrackOpens = true + if (args['track-links']) body.TrackLinks = args['track-links'] + if (args.cc) body.Cc = args.cc + if (args.bcc) body.Bcc = args.bcc + if (args['reply-to']) body.ReplyTo = args['reply-to'] + result = await api('POST', '/email', body) + break + } + case 'send-template': { + const from = args.from + const to = args.to + const template = args.template + if (!from) { result = { error: '--from required' }; break } + if (!to) { result = { error: '--to required' }; break } + if (!template) { result = { error: '--template required (template ID or alias)' }; break } + const body = { + From: from, + To: to, + TemplateModel: {}, + } + const templateNum = Number(template) + if (!isNaN(templateNum)) { + body.TemplateId = templateNum + } else { + body.TemplateAlias = template + } + if (args.model) { + const pairs = args.model.split(',') + for (const pair of pairs) { + const [k, v] = pair.split(':') + if (k && v) body.TemplateModel[k] = v + } + } + if (args.stream) body.MessageStream = args.stream + if (args.tag) body.Tag = args.tag + result = await api('POST', '/email/withTemplate', body) + break + } + case 'send-batch': { + const from = args.from + const to = args.to + const subject = args.subject + if (!from || !to || !subject) { + result = { error: '--from, --to (comma-separated), and --subject required' }; break + } + const recipients = to.split(',') + const messages = recipients.map(recipient => ({ + From: from, + To: recipient.trim(), + Subject: subject, + TextBody: args.text || '', + HtmlBody: args.html || undefined, + MessageStream: args.stream || undefined, + Tag: args.tag || undefined, + })) + result = await api('POST', '/email/batch', messages) + break + } + default: + result = { error: 'Unknown email subcommand. Use: send, send-template, send-batch' } + } + break + + case 'templates': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('Count', args.count || '100') + params.set('Offset', args.offset || '0') + if (args.type) params.set('TemplateType', args.type) + result = await api('GET', `/templates?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (template ID or alias)' }; break } + result = await api('GET', `/templates/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { + Name: name, + Subject: args.subject || '', + } + if (args.html) body.HtmlBody = args.html + if (args.text) body.TextBody = args.text + if (args.alias) body.Alias = args.alias + if (args.type) body.TemplateType = args.type + result = await api('POST', '/templates', body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (template ID or alias)' }; break } + result = await api('DELETE', `/templates/${id}`) + break + } + default: + result = { error: 'Unknown templates subcommand. Use: list, get, create, delete' } + } + break + + case 'bounces': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('count', args.count || '50') + params.set('offset', args.offset || '0') + if (args.type) params.set('type', args.type) + if (args.inactive) params.set('inactive', args.inactive) + if (args.email) params.set('emailFilter', args.email) + result = await api('GET', `/bounces?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/bounces/${id}`) + break + } + case 'stats': + result = await api('GET', '/deliverystats') + break + case 'activate': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('PUT', `/bounces/${id}/activate`) + break + } + default: + result = { error: 'Unknown bounces subcommand. Use: list, get, stats, activate' } + } + break + + case 'messages': + switch (sub) { + case 'outbound': { + const params = new URLSearchParams() + params.set('count', args.count || '50') + params.set('offset', args.offset || '0') + if (args.recipient) params.set('recipient', args.recipient) + if (args.tag) params.set('tag', args.tag) + if (args.status) params.set('status', args.status) + result = await api('GET', `/messages/outbound?${params.toString()}`) + break + } + case 'inbound': { + const params = new URLSearchParams() + params.set('count', args.count || '50') + params.set('offset', args.offset || '0') + if (args.recipient) params.set('recipient', args.recipient) + if (args.status) params.set('status', args.status) + result = await api('GET', `/messages/inbound?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (message ID)' }; break } + result = await api('GET', `/messages/outbound/${id}/details`) + break + } + default: + result = { error: 'Unknown messages subcommand. Use: outbound, inbound, get' } + } + break + + case 'stats': + switch (sub) { + case 'overview': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound?${params.toString()}`) + break + } + case 'sends': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound/sends?${params.toString()}`) + break + } + case 'bounces': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound/bounces?${params.toString()}`) + break + } + case 'opens': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound/opens?${params.toString()}`) + break + } + case 'clicks': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound/clicks?${params.toString()}`) + break + } + case 'spam': { + const params = new URLSearchParams() + if (args.tag) params.set('tag', args.tag) + if (args.from) params.set('fromdate', args.from) + if (args.to) params.set('todate', args.to) + result = await api('GET', `/stats/outbound/spam?${params.toString()}`) + break + } + default: + result = { error: 'Unknown stats subcommand. Use: overview, sends, bounces, opens, clicks, spam' } + } + break + + case 'server': + switch (sub) { + case 'get': + result = await api('GET', '/server') + break + default: + result = { error: 'Unknown server subcommand. Use: get' } + } + break + + case 'suppressions': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (args.stream) params.set('MessageStream', args.stream) + result = await api('GET', `/message-streams/${args.stream || 'outbound'}/suppressions/dump`) + break + } + case 'create': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const stream = args.stream || 'outbound' + result = await api('POST', `/message-streams/${stream}/suppressions`, { + Suppressions: email.split(',').map(e => ({ EmailAddress: e.trim() })) + }) + break + } + case 'delete': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const stream = args.stream || 'outbound' + result = await api('POST', `/message-streams/${stream}/suppressions/delete`, { + Suppressions: email.split(',').map(e => ({ EmailAddress: e.trim() })) + }) + break + } + default: + result = { error: 'Unknown suppressions subcommand. Use: list, create, delete' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + email: 'email [send --from <from> --to <to> --subject <subj> | send-template --from <from> --to <to> --template <id> | send-batch --from <from> --to <to1,to2> --subject <subj>]', + templates: 'templates [list | get --id <id> | create --name <name> | delete --id <id>]', + bounces: 'bounces [list | get --id <id> | stats | activate --id <id>]', + messages: 'messages [outbound | inbound | get --id <id>]', + stats: 'stats [overview | sends | bounces | opens | clicks | spam]', + server: 'server [get]', + suppressions: 'suppressions [list | create --email <email> | delete --email <email>]', + options: '--tag <tag> --from <date> --to <date> --stream <stream-id>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/resend.js b/tools/clis/resend.js new file mode 100755 index 0000000..20c4851 --- /dev/null +++ b/tools/clis/resend.js @@ -0,0 +1,370 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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': { + if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break } + 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 + if (!audienceId) { result = { error: 'Audience ID required as subcommand arg' }; break } + 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': + if (!rest[1]) { result = { error: 'Contact ID required' }; break } + 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': { + if (!rest[1]) { result = { error: 'Contact ID required' }; break } + 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': + if (!rest[1]) { result = { error: 'Contact ID required' }; break } + 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': { + if (!args.url) { result = { error: '--url required (webhook URL)' }; break } + const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced'] + result = await api('POST', '/webhooks', { url: args.url, 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': { + let emails + try { + emails = JSON.parse(args.emails || '[]') + } catch (e) { + result = { error: 'Invalid JSON for --emails: ' + e.message }; break + } + 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) { + try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break } + } + 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) { + try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break } + } + 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) +}) diff --git a/tools/clis/rewardful.js b/tools/clis/rewardful.js new file mode 100755 index 0000000..dbaf01e --- /dev/null +++ b/tools/clis/rewardful.js @@ -0,0 +1,160 @@ +#!/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 auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64') + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': 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 } + } +} + +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': + if (!rest[0]) { result = { error: 'Affiliate ID required' }; break } + 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': { + if (!rest[0]) { result = { error: 'Affiliate ID required' }; break } + 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': + if (!rest[0]) { result = { error: 'Commission ID required' }; break } + result = await api('GET', `/commissions/${rest[0]}`) + break + default: + result = { error: 'Unknown commissions subcommand. Use: list, get' } + } + break + + case 'links': + switch (sub) { + case 'create': { + if (!args['affiliate-id']) { result = { error: '--affiliate-id required' }; break } + 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) +}) diff --git a/tools/clis/savvycal.js b/tools/clis/savvycal.js new file mode 100755 index 0000000..046c565 --- /dev/null +++ b/tools/clis/savvycal.js @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +const API_KEY = process.env.SAVVYCAL_API_KEY +const BASE_URL = 'https://api.savvycal.com/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'SAVVYCAL_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const limit = args.limit ? Number(args.limit) : 20 + + switch (cmd) { + case 'me': + result = await api('GET', '/me') + break + + case 'links': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + if (args.after) params.set('after', args.after) + if (args.before) params.set('before', args.before) + result = await api('GET', `/scheduling-links?${params}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/scheduling-links/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { name } + if (args.slug) body.slug = args.slug + if (args.duration) body.duration_minutes = Number(args.duration) + result = await api('POST', '/scheduling-links', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.slug) body.slug = args.slug + if (args.duration) body.duration_minutes = Number(args.duration) + result = await api('PATCH', `/scheduling-links/${id}`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/scheduling-links/${id}`) + break + } + case 'duplicate': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/scheduling-links/${id}/duplicate`) + break + } + case 'toggle': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/scheduling-links/${id}/toggle`) + break + } + case 'slots': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + 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']) + const qs = params.toString() + result = await api('GET', `/scheduling-links/${id}/slots${qs ? '?' + qs : ''}`) + break + } + default: + result = { error: 'Unknown links subcommand. Use: list, get, create, update, delete, duplicate, toggle, slots' } + } + break + + case 'events': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + if (args.after) params.set('after', args.after) + if (args.before) params.set('before', args.before) + result = await api('GET', `/events?${params}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/events/${id}`) + break + } + case 'create': { + const linkId = args['link-id'] + const startAt = args['start-at'] + const name = args.name + const email = args.email + if (!linkId || !startAt || !name || !email) { + result = { error: '--link-id, --start-at, --name, and --email required' } + break + } + result = await api('POST', '/events', { + scheduling_link_id: linkId, + start_at: startAt, + name, + email, + }) + break + } + case 'cancel': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('POST', `/events/${id}/cancel`) + break + } + default: + result = { error: 'Unknown events subcommand. Use: list, get, create, cancel' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + params.set('limit', String(limit)) + if (args.after) params.set('after', args.after) + if (args.before) params.set('before', args.before) + result = await api('GET', `/webhooks?${params}`) + break + } + case 'create': { + const url = args.url + const events = args.events?.split(',') + if (!url || !events) { result = { error: '--url and --events (comma-separated) required' }; break } + result = await api('POST', '/webhooks', { url, events }) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('DELETE', `/webhooks/${id}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + me: 'me', + links: 'links [list | get --id <id> | create --name <name> | update --id <id> | delete --id <id> | duplicate --id <id> | toggle --id <id> | slots --id <id>]', + events: 'events [list | get --id <id> | create --link-id <id> --start-at <iso> --name <name> --email <email> | cancel --id <id>]', + webhooks: 'webhooks [list | create --url <url> --events <e1,e2> | delete --id <id>]', + options: '--limit <n> --after <cursor> --before <cursor>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/segment.js b/tools/clis/segment.js new file mode 100755 index 0000000..d1c9d90 --- /dev/null +++ b/tools/clis/segment.js @@ -0,0 +1,192 @@ +#!/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' } + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${TRACKING_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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' } + } + if (args['dry-run']) { + return { _dry_run: true, method, url: `${PROFILE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' } } + } + 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) { + try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break } + } + 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) { + try { body.traits = JSON.parse(args.traits) } catch { result = { error: 'Invalid JSON in --traits' }; break } + } + 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) { + try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break } + } + 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 } + let batch + try { batch = JSON.parse(args.events) } catch { result = { error: 'Invalid JSON in --events' }; break } + 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) +}) diff --git a/tools/clis/semrush.js b/tools/clis/semrush.js new file mode 100755 index 0000000..8ba985c --- /dev/null +++ b/tools/clis/semrush.js @@ -0,0 +1,207 @@ +#!/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') + if (args['dry-run']) { + const maskedParams = new URLSearchParams(params) + maskedParams.set('key', '***') + return { _dry_run: true, method: 'GET', url: `${BASE_URL}?${maskedParams}`, headers: {}, body: undefined } + } + 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': { + if (!args.domain) { result = { error: '--domain required' }; break } + 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': { + if (!args.domain) { result = { error: '--domain required' }; break } + 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': { + if (!args.domain) { result = { error: '--domain required' }; break } + 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': { + if (!args.phrase) { result = { error: '--phrase required' }; break } + 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': { + if (!args.phrase) { result = { error: '--phrase required' }; break } + 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': { + if (!args.phrase) { result = { error: '--phrase required' }; break } + 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) +}) diff --git a/tools/clis/sendgrid.js b/tools/clis/sendgrid.js new file mode 100755 index 0000000..d5167fd --- /dev/null +++ b/tools/clis/sendgrid.js @@ -0,0 +1,211 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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': { + if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break } + 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']) { + try { body.personalizations[0].dynamic_template_data = JSON.parse(args['template-data']) } catch (e) { result = { error: 'Invalid JSON for --template-data: ' + e.message }; break } + } + } 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': + if (!rest[0]) { result = { error: 'Campaign ID required' }; break } + 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': { + if (!args.email && !rest[0]) { result = { error: '--email required' }; break } + 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) +}) diff --git a/tools/clis/snov.js b/tools/clis/snov.js new file mode 100755 index 0000000..161f415 --- /dev/null +++ b/tools/clis/snov.js @@ -0,0 +1,237 @@ +#!/usr/bin/env node + +const CLIENT_ID = process.env.SNOV_CLIENT_ID +const CLIENT_SECRET = process.env.SNOV_CLIENT_SECRET +const BASE_URL = 'https://api.snov.io/v1' + +if (!CLIENT_ID || !CLIENT_SECRET) { + console.error(JSON.stringify({ error: 'SNOV_CLIENT_ID and SNOV_CLIENT_SECRET environment variables required' })) + process.exit(1) +} + +let cachedToken = null + +async function getToken() { + if (cachedToken) return cachedToken + const res = await fetch('https://api.snov.io/v1/oauth/access_token', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET }), + }) + const data = await res.json() + if (!data.access_token) { + throw new Error(data.error_description || data.error || 'Failed to obtain access token') + } + cachedToken = data.access_token + return cachedToken +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined } + } + const token = await getToken() + const opts = { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + } + if (body) opts.body = JSON.stringify(body) + const res = await fetch(`${BASE_URL}${path}`, opts) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + + switch (cmd) { + case 'domain': + switch (sub) { + case 'search': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + const body = { domain, type: args.type || 'all', limit: Number(args.limit || 100), lastId: 0 } + result = await api('POST', '/get-domain-emails-with-info', body) + break + } + case 'count': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get-domain-emails-count', { domain }) + break + } + default: + result = { error: 'Unknown domain subcommand. Use: search, count' } + } + break + + case 'email': + switch (sub) { + case 'find': { + const domain = args.domain + const firstName = args['first-name'] + const lastName = args['last-name'] + if (!domain || !firstName || !lastName) { result = { error: '--domain, --first-name, and --last-name required' }; break } + result = await api('POST', '/get-emails-from-names', { firstName, lastName, domain }) + break + } + case 'verify': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + result = await api('POST', '/get-emails-verification-status', { emails: [email] }) + break + } + default: + result = { error: 'Unknown email subcommand. Use: find, verify' } + } + break + + case 'prospect': + switch (sub) { + case 'find': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + result = await api('POST', '/get-prospect-by-email', { email }) + break + } + case 'add': { + const email = args.email + if (!email) { result = { error: '--email required' }; break } + const body = { email } + if (args['first-name']) body.firstName = args['first-name'] + if (args['last-name']) body.lastName = args['last-name'] + if (args['list-id']) body.listId = args['list-id'] + result = await api('POST', '/add-prospect-to-list', body) + break + } + default: + result = { error: 'Unknown prospect subcommand. Use: find, add' } + } + break + + case 'lists': + switch (sub) { + case 'list': + result = await api('GET', '/get-user-lists') + break + case 'prospects': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + const body = { listId: id } + if (args.page) body.page = Number(args.page) + if (args['per-page']) body.perPage = Number(args['per-page']) + result = await api('POST', '/prospect-list', body) + break + } + default: + result = { error: 'Unknown lists subcommand. Use: list, prospects' } + } + break + + case 'technology': + switch (sub) { + case 'check': { + const domain = args.domain + if (!domain) { result = { error: '--domain required' }; break } + result = await api('POST', '/get-technology-checker', { domain }) + break + } + default: + result = { error: 'Unknown technology subcommand. Use: check' } + } + break + + case 'drips': + switch (sub) { + case 'list': + result = await api('GET', '/get-user-campaigns') + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required' }; break } + result = await api('GET', `/get-emails-from-campaign?id=${encodeURIComponent(id)}`) + break + } + case 'add-prospect': { + const campaignId = args['campaign-id'] + const email = args.email + if (!campaignId || !email) { result = { error: '--campaign-id and --email required' }; break } + const body = { campaignId, email } + if (args['first-name']) body.firstName = args['first-name'] + if (args['last-name']) body.lastName = args['last-name'] + result = await api('POST', '/add-prospect-to-email-campaign', body) + break + } + default: + result = { error: 'Unknown drips subcommand. Use: list, get, add-prospect' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + domain: { + search: 'domain search --domain <domain> [--type all|personal|generic] [--limit <n>]', + count: 'domain count --domain <domain>', + }, + email: { + find: 'email find --domain <domain> --first-name <name> --last-name <name>', + verify: 'email verify --email <email>', + }, + prospect: { + find: 'prospect find --email <email>', + add: 'prospect add --email <email> [--first-name <name>] [--last-name <name>] [--list-id <id>]', + }, + lists: { + list: 'lists list', + prospects: 'lists prospects --id <id> [--page <n>] [--per-page <n>]', + }, + technology: 'technology check --domain <domain>', + drips: { + list: 'drips list', + get: 'drips get --id <id>', + 'add-prospect': 'drips add-prospect --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>]', + }, + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/tiktok-ads.js b/tools/clis/tiktok-ads.js new file mode 100755 index 0000000..ac73e1f --- /dev/null +++ b/tools/clis/tiktok-ads.js @@ -0,0 +1,190 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Access-Token': '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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 } + const advParams = new URLSearchParams({ advertiser_ids: JSON.stringify([advId]) }) + result = await api('GET', `/advertiser/info/?${advParams}`) + 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 } + const campParams = new URLSearchParams({ advertiser_id: advId, page: '1', page_size: '20' }) + result = await api('GET', `/campaign/get/?${campParams}`) + 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 } + const agParams = new URLSearchParams({ advertiser_id: advId }) + if (args['campaign-id']) agParams.set('campaign_ids', JSON.stringify([args['campaign-id']])) + result = await api('GET', `/adgroup/get/?${agParams}`) + 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 } + const audParams = new URLSearchParams({ advertiser_id: advId }) + result = await api('GET', `/dmp/custom_audience/list/?${audParams}`) + 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) +}) diff --git a/tools/clis/tolt.js b/tools/clis/tolt.js new file mode 100755 index 0000000..42b513a --- /dev/null +++ b/tools/clis/tolt.js @@ -0,0 +1,153 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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': { + const id = rest[0] || args.id + if (!id) { result = { error: 'Affiliate ID required (positional arg or --id)' }; break } + result = await api('GET', `/affiliates/${id}`) + 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': { + if (!args.id) { result = { error: '--id required (affiliate ID)' }; break } + 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) +}) diff --git a/tools/clis/trustpilot.js b/tools/clis/trustpilot.js new file mode 100755 index 0000000..0eb13d7 --- /dev/null +++ b/tools/clis/trustpilot.js @@ -0,0 +1,276 @@ +#!/usr/bin/env node + +const API_KEY = process.env.TRUSTPILOT_API_KEY +const API_SECRET = process.env.TRUSTPILOT_API_SECRET +const BUSINESS_UNIT_ID = process.env.TRUSTPILOT_BUSINESS_UNIT_ID +const BASE_URL = 'https://api.trustpilot.com/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'TRUSTPILOT_API_KEY environment variable required' })) + process.exit(1) +} + +let accessToken = null + +async function getAccessToken() { + if (accessToken) return accessToken + if (!API_SECRET) return null + const res = await fetch(`${BASE_URL}/oauth/oauth-business-users-for-applications/accesstoken`, { + method: 'POST', + headers: { + 'Authorization': 'Basic ' + Buffer.from(`${API_KEY}:${API_SECRET}`).toString('base64'), + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: 'grant_type=client_credentials', + }) + const data = await res.json() + if (data.access_token) { + accessToken = data.access_token + return accessToken + } + return null +} + +async function api(method, path, body, auth = 'apikey') { + if (args['dry-run']) { + const maskedHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json' } + if (auth === 'bearer') { + maskedHeaders['Authorization'] = '***' + } else { + maskedHeaders['apikey'] = '***' + } + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: maskedHeaders, body: body || undefined } + } + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if (auth === 'bearer') { + const token = await getAccessToken() + if (!token) { + return { error: 'TRUSTPILOT_API_SECRET required for private API endpoints' } + } + headers['Authorization'] = `Bearer ${token}` + } else { + headers['apikey'] = API_KEY + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const businessUnitId = args['business-unit'] || BUSINESS_UNIT_ID + const limit = args.limit ? Number(args.limit) : 20 + + switch (cmd) { + case 'business': + switch (sub) { + case 'search': { + const query = args.query + if (!query) { result = { error: '--query required' }; break } + result = await api('GET', `/business-units/search?query=${encodeURIComponent(query)}&limit=${limit}`) + break + } + case 'get': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + result = await api('GET', `/business-units/${businessUnitId}`) + break + } + case 'profile': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + result = await api('GET', `/business-units/${businessUnitId}/profileinfo`) + break + } + case 'categories': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + result = await api('GET', `/business-units/${businessUnitId}/categories`) + break + } + case 'web-links': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + const locale = args.locale || 'en-US' + result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${encodeURIComponent(locale)}`) + break + } + default: + result = { error: 'Unknown business subcommand. Use: search, get, profile, categories, web-links' } + } + break + + case 'reviews': + switch (sub) { + case 'list': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + const reviewParams = new URLSearchParams({ perPage: String(limit), orderBy: args['order-by'] || 'createdat.desc' }) + if (args.stars) reviewParams.set('stars', args.stars) + if (args.language) reviewParams.set('language', args.language) + result = await api('GET', `/business-units/${businessUnitId}/reviews?${reviewParams}`) + break + } + case 'get': { + const reviewId = args.id + if (!reviewId) { result = { error: '--id required' }; break } + result = await api('GET', `/reviews/${reviewId}`) + break + } + case 'private': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + const privateParams = new URLSearchParams({ perPage: String(limit) }) + if (args.stars) privateParams.set('stars', args.stars) + result = await api('GET', `/private/business-units/${businessUnitId}/reviews?${privateParams}`, null, 'bearer') + break + } + case 'latest': + result = await api('GET', `/reviews/latest?count=${limit}`) + break + case 'reply': { + const reviewId = args.id + const message = args.message + if (!reviewId) { result = { error: '--id required' }; break } + if (!message) { result = { error: '--message required' }; break } + result = await api('POST', `/private/reviews/${reviewId}/reply`, { message }, 'bearer') + break + } + case 'delete-reply': { + const reviewId = args.id + if (!reviewId) { result = { error: '--id required' }; break } + result = await api('DELETE', `/private/reviews/${reviewId}/reply`, null, 'bearer') + break + } + default: + result = { error: 'Unknown reviews subcommand. Use: list, get, private, latest, reply, delete-reply' } + } + break + + case 'invitations': + switch (sub) { + case 'create': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + const email = args.email + const name = args.name + if (!email) { result = { error: '--email required' }; break } + if (!name) { result = { error: '--name required' }; break } + const templateId = args.template + const redirectUri = args['redirect-uri'] || 'https://trustpilot.com' + const payload = { + consumerEmail: email, + consumerName: name, + referenceNumber: args.reference || '', + senderEmail: args['sender-email'] || undefined, + replyTo: args['reply-to'] || undefined, + templateId: templateId || undefined, + redirectUri, + } + result = await api('POST', `/private/business-units/${businessUnitId}/email-invitations`, payload, 'bearer') + break + } + case 'link': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + const email = args.email + const name = args.name + if (!email) { result = { error: '--email required' }; break } + if (!name) { result = { error: '--name required' }; break } + result = await api('POST', `/private/business-units/${businessUnitId}/invitation-links`, { + email, + name, + referenceId: args.reference || '', + redirectUri: args['redirect-uri'] || 'https://trustpilot.com', + }, 'bearer') + break + } + case 'templates': { + if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } + result = await api('GET', `/private/business-units/${businessUnitId}/templates`, null, 'bearer') + break + } + default: + result = { error: 'Unknown invitations subcommand. Use: create, link, templates' } + } + break + + case 'tags': + switch (sub) { + case 'get': { + const reviewId = args.id + if (!reviewId) { result = { error: '--id required' }; break } + result = await api('GET', `/private/reviews/${reviewId}/tags`, null, 'bearer') + break + } + case 'add': { + const reviewId = args.id + const group = args.group + const value = args.value + if (!reviewId) { result = { error: '--id required' }; break } + if (!group || !value) { result = { error: '--group and --value required' }; break } + result = await api('PUT', `/private/reviews/${reviewId}/tags`, { + tags: [{ group, value }], + }, 'bearer') + break + } + case 'remove': { + const reviewId = args.id + const group = args.group + const value = args.value + if (!reviewId) { result = { error: '--id required' }; break } + if (!group || !value) { result = { error: '--group and --value required' }; break } + result = await api('DELETE', `/private/reviews/${reviewId}/tags?group=${encodeURIComponent(group)}&value=${encodeURIComponent(value)}`, null, 'bearer') + break + } + default: + result = { error: 'Unknown tags subcommand. Use: get, add, remove' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + business: 'business [search --query <q> | get | profile | categories | web-links]', + reviews: 'reviews [list | get --id <id> | private | latest | reply --id <id> --message <msg> | delete-reply --id <id>]', + invitations: 'invitations [create --email <e> --name <n> | link --email <e> --name <n> | templates]', + tags: 'tags [get --id <id> | add --id <id> --group <g> --value <v> | remove --id <id> --group <g> --value <v>]', + options: '--business-unit <id> --limit <n> --stars <1-5> --language <code>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/typeform.js b/tools/clis/typeform.js new file mode 100755 index 0000000..c3a7cbe --- /dev/null +++ b/tools/clis/typeform.js @@ -0,0 +1,269 @@ +#!/usr/bin/env node + +const API_KEY = process.env.TYPEFORM_API_KEY +const BASE_URL = 'https://api.typeform.com' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'TYPEFORM_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ + +async function main() { + let result + const pageSize = args['page-size'] ? Number(args['page-size']) : undefined + + switch (cmd) { + case 'forms': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (pageSize) params.set('page_size', String(pageSize)) + if (args.page) params.set('page', args.page) + if (args['workspace-id']) params.set('workspace_id', args['workspace-id']) + if (args.search) params.set('search', args.search) + const qs = params.toString() + result = await api('GET', `/forms${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + result = await api('GET', `/forms/${id}`) + break + } + case 'create': { + const title = args.title + if (!title) { result = { error: '--title required' }; break } + const body = { title } + if (args['workspace-id']) { + body.workspace = { href: `${BASE_URL}/workspaces/${args['workspace-id']}` } + } + result = await api('POST', '/forms', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + const body = {} + if (args.title) body.title = args.title + result = await api('PUT', `/forms/${id}`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + result = await api('DELETE', `/forms/${id}`) + break + } + default: + result = { error: 'Unknown forms subcommand. Use: list, get, create, update, delete' } + } + break + + case 'responses': + switch (sub) { + case 'list': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + const params = new URLSearchParams() + if (pageSize) params.set('page_size', String(pageSize)) + if (args.since) params.set('since', args.since) + if (args.until) params.set('until', args.until) + if (args.after) params.set('after', args.after) + if (args.before) params.set('before', args.before) + if (args['response-type']) params.set('response_type', args['response-type']) + if (args.query) params.set('query', args.query) + if (args.fields) params.set('fields', args.fields) + if (args.sort) params.set('sort', args.sort) + const qs = params.toString() + result = await api('GET', `/forms/${id}/responses${qs ? '?' + qs : ''}`) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + const responseIds = args['response-ids'] + if (!responseIds) { result = { error: '--response-ids required (comma-separated)' }; break } + result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${encodeURIComponent(responseIds)}`) + break + } + default: + result = { error: 'Unknown responses subcommand. Use: list, delete' } + } + break + + case 'webhooks': + switch (sub) { + case 'list': { + const id = args.id + if (!id) { result = { error: '--id required (form ID)' }; break } + result = await api('GET', `/forms/${id}/webhooks`) + break + } + case 'get': { + const id = args.id + const tag = args.tag + if (!id || !tag) { result = { error: '--id (form ID) and --tag required' }; break } + result = await api('GET', `/forms/${id}/webhooks/${tag}`) + break + } + case 'create': { + const id = args.id + const tag = args.tag + const url = args.url + if (!id || !tag || !url) { result = { error: '--id (form ID), --tag, and --url required' }; break } + const body = { url, enabled: args.enabled !== 'false' } + result = await api('PUT', `/forms/${id}/webhooks/${tag}`, body) + break + } + case 'delete': { + const id = args.id + const tag = args.tag + if (!id || !tag) { result = { error: '--id (form ID) and --tag required' }; break } + result = await api('DELETE', `/forms/${id}/webhooks/${tag}`) + break + } + default: + result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' } + } + break + + case 'themes': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (pageSize) params.set('page_size', String(pageSize)) + if (args.page) params.set('page', args.page) + const qs = params.toString() + result = await api('GET', `/themes${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (theme ID)' }; break } + result = await api('GET', `/themes/${id}`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { name } + if (args.font) body.font = args.font + result = await api('POST', '/themes', body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (theme ID)' }; break } + result = await api('DELETE', `/themes/${id}`) + break + } + default: + result = { error: 'Unknown themes subcommand. Use: list, get, create, delete' } + } + break + + case 'images': + switch (sub) { + case 'list': + result = await api('GET', '/images') + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (image ID)' }; break } + result = await api('GET', `/images/${id}`) + break + } + default: + result = { error: 'Unknown images subcommand. Use: list, get' } + } + break + + case 'workspaces': + switch (sub) { + case 'list': { + const params = new URLSearchParams() + if (pageSize) params.set('page_size', String(pageSize)) + if (args.page) params.set('page', args.page) + if (args.search) params.set('search', args.search) + const qs = params.toString() + result = await api('GET', `/workspaces${qs ? '?' + qs : ''}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (workspace ID)' }; break } + result = await api('GET', `/workspaces/${id}`) + break + } + default: + result = { error: 'Unknown workspaces subcommand. Use: list, get' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + forms: 'forms [list | get --id <id> | create --title <title> | update --id <id> --title <title> | delete --id <id>]', + responses: 'responses [list --id <form_id> | delete --id <form_id> --response-ids <id1,id2>]', + webhooks: 'webhooks [list --id <form_id> | get --id <form_id> --tag <tag> | create --id <form_id> --tag <tag> --url <url> | delete --id <form_id> --tag <tag>]', + themes: 'themes [list | get --id <id> | create --name <name> | delete --id <id>]', + images: 'images [list | get --id <id>]', + workspaces: 'workspaces [list | get --id <id>]', + options: '--page-size <n> --page <n> --since <iso> --until <iso> --query <text>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/wistia.js b/tools/clis/wistia.js new file mode 100755 index 0000000..cd1d2ec --- /dev/null +++ b/tools/clis/wistia.js @@ -0,0 +1,256 @@ +#!/usr/bin/env node + +const API_KEY = process.env.WISTIA_API_KEY +const BASE_URL = 'https://api.wistia.com/v1' + +if (!API_KEY) { + console.error(JSON.stringify({ error: 'WISTIA_API_KEY environment variable required' })) + process.exit(1) +} + +async function api(method, path, body) { + const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64') + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } + } + const res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': auth, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + const text = await res.text() + try { + return JSON.parse(text) + } catch { + return { status: res.status, body: text } + } +} + +function parseArgs(args) { + const result = { _: [] } + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg.startsWith('--')) { + const key = arg.slice(2) + const next = args[i + 1] + if (next && !next.startsWith('--')) { + result[key] = next + i++ + } else { + result[key] = true + } + } else { + result._.push(arg) + } + } + return result +} + +const args = parseArgs(process.argv.slice(2)) +const [cmd, sub, ...rest] = args._ +const page = args.page ? Number(args.page) : 1 +const perPage = args['per-page'] ? Number(args['per-page']) : 25 + +async function main() { + let result + + switch (cmd) { + case 'projects': + switch (sub) { + case 'list': + result = await api('GET', `/projects.json?page=${page}&per_page=${perPage}`) + break + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (project hashed ID)' }; break } + result = await api('GET', `/projects/${id}.json`) + break + } + case 'create': { + const name = args.name + if (!name) { result = { error: '--name required' }; break } + const body = { name } + if (args.public) body.public = true + result = await api('POST', '/projects.json', body) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required (project hashed ID)' }; break } + const body = {} + if (args.name) body.name = args.name + result = await api('PUT', `/projects/${id}.json`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (project hashed ID)' }; break } + result = await api('DELETE', `/projects/${id}.json`) + break + } + default: + result = { error: 'Unknown projects subcommand. Use: list, get, create, update, delete' } + } + break + + case 'medias': + switch (sub) { + case 'list': { + const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }) + if (args.project) params.set('project_id', args.project) + if (args.name) params.set('name', args.name) + if (args.type) params.set('type', args.type) + result = await api('GET', `/medias.json?${params.toString()}`) + break + } + case 'get': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('GET', `/medias/${id}.json`) + break + } + case 'update': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + const body = {} + if (args.name) body.name = args.name + if (args.description) body.description = args.description + result = await api('PUT', `/medias/${id}.json`, body) + break + } + case 'delete': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('DELETE', `/medias/${id}.json`) + break + } + case 'copy': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + const body = {} + if (args['target-project']) body.project_id = args['target-project'] + result = await api('POST', `/medias/${id}/copy.json`, body) + break + } + case 'stats': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('GET', `/medias/${id}/stats.json`) + break + } + default: + result = { error: 'Unknown medias subcommand. Use: list, get, update, delete, copy, stats' } + } + break + + case 'stats': + switch (sub) { + case 'account': + result = await api('GET', '/stats/account.json') + break + case 'project': { + const id = args.id + if (!id) { result = { error: '--id required (project hashed ID)' }; break } + result = await api('GET', `/stats/projects/${id}.json`) + break + } + case 'media': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('GET', `/stats/medias/${id}.json`) + break + } + case 'media-by-date': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + const params = new URLSearchParams() + if (args.start) params.set('start_date', args.start) + if (args.end) params.set('end_date', args.end) + const qs = params.toString() ? `?${params.toString()}` : '' + result = await api('GET', `/stats/medias/${id}/by_date.json${qs}`) + break + } + case 'engagement': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('GET', `/stats/medias/${id}/engagement.json`) + break + } + case 'visitors': { + const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }) + if (args.search) params.set('search', args.search) + result = await api('GET', `/stats/visitors.json?${params.toString()}`) + break + } + case 'visitor': { + const key = args.key + if (!key) { result = { error: '--key required (visitor key)' }; break } + result = await api('GET', `/stats/visitors/${key}.json`) + break + } + case 'events': { + const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }) + if (args['media-id']) params.set('media_id', args['media-id']) + result = await api('GET', `/stats/events.json?${params.toString()}`) + break + } + default: + result = { error: 'Unknown stats subcommand. Use: account, project, media, media-by-date, engagement, visitors, visitor, events' } + } + break + + case 'account': + result = await api('GET', '/account.json') + break + + case 'captions': + switch (sub) { + case 'list': { + const id = args.id + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + result = await api('GET', `/medias/${id}/captions.json`) + break + } + case 'create': { + const id = args.id + const language = args.language + if (!id) { result = { error: '--id required (media hashed ID)' }; break } + if (!language) { result = { error: '--language required (e.g. eng)' }; break } + const body = { language } + if (args['srt-file']) { + const fs = require('fs') + body.caption_file = fs.readFileSync(args['srt-file'], 'utf8') + } + result = await api('POST', `/medias/${id}/captions.json`, body) + break + } + default: + result = { error: 'Unknown captions subcommand. Use: list, create' } + } + break + + default: + result = { + error: 'Unknown command', + usage: { + projects: 'projects [list | get --id <id> | create --name <name> | update --id <id> --name <name> | delete --id <id>]', + medias: 'medias [list [--project <id>] | get --id <id> | update --id <id> --name <name> | delete --id <id> | copy --id <id> [--target-project <id>] | stats --id <id>]', + stats: 'stats [account | project --id <id> | media --id <id> | media-by-date --id <id> [--start <date> --end <date>] | engagement --id <id> | visitors | visitor --key <key> | events [--media-id <id>]]', + account: 'account', + captions: 'captions [list --id <media-id> | create --id <media-id> --language <lang>]', + options: '--page <n> --per-page <n>', + } + } + } + + console.log(JSON.stringify(result, null, 2)) +} + +main().catch(err => { + console.error(JSON.stringify({ error: err.message })) + process.exit(1) +}) diff --git a/tools/clis/zapier.js b/tools/clis/zapier.js new file mode 100755 index 0000000..082583d --- /dev/null +++ b/tools/clis/zapier.js @@ -0,0 +1,160 @@ +#!/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) { + if (args['dry-run']) { + return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'X-API-Key': '***', 'Content-Type': 'application/json' }, body: body || undefined } + } + 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) { + if (args['dry-run']) { + return { _dry_run: true, method: 'POST', url, headers: { 'Content-Type': 'application/json' }, body: data || undefined } + } + 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) +}) diff --git a/tools/integrations/activecampaign.md b/tools/integrations/activecampaign.md new file mode 100644 index 0000000..4ae4894 --- /dev/null +++ b/tools/integrations/activecampaign.md @@ -0,0 +1,337 @@ +# ActiveCampaign + +Email marketing automation platform with CRM, contacts, deals pipeline, tags, automations, and campaign management. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v3 for contacts, deals, automations, campaigns, tags | +| MCP | - | Not available | +| CLI | ✓ | [activecampaign.js](../clis/activecampaign.js) | +| SDK | ✓ | Python, PHP, Node.js, Ruby | + +## Authentication + +- **Type**: API Token +- **Header**: `Api-Token: {api_token}` +- **Base URL**: `https://{yourAccountName}.api-us1.com/api/3` +- **Get key**: Settings > Developer tab in your ActiveCampaign account +- **Note**: Each user has a unique API key. Base URL is account-specific (found in Settings > Developer). + +## Common Agent Operations + +### Get current user + +```bash +GET https://{account}.api-us1.com/api/3/users/me +``` + +### List contacts + +```bash +GET https://{account}.api-us1.com/api/3/contacts?limit=20&offset=0 + +# Search by email +GET https://{account}.api-us1.com/api/3/contacts?email=user@example.com + +# Search by name +GET https://{account}.api-us1.com/api/3/contacts?search=Jane +``` + +### Create contact + +```bash +POST https://{account}.api-us1.com/api/3/contacts + +{ + "contact": { + "email": "user@example.com", + "firstName": "Jane", + "lastName": "Doe", + "phone": "+15551234567" + } +} +``` + +### Update contact + +```bash +PUT https://{account}.api-us1.com/api/3/contacts/{contactId} + +{ + "contact": { + "firstName": "Updated", + "lastName": "Name" + } +} +``` + +### Sync contact (create or update) + +```bash +POST https://{account}.api-us1.com/api/3/contact/sync + +{ + "contact": { + "email": "user@example.com", + "firstName": "Jane", + "lastName": "Doe" + } +} +``` + +### Delete contact + +```bash +DELETE https://{account}.api-us1.com/api/3/contacts/{contactId} +``` + +### List all lists + +```bash +GET https://{account}.api-us1.com/api/3/lists?limit=20&offset=0 +``` + +### Create list + +```bash +POST https://{account}.api-us1.com/api/3/lists + +{ + "list": { + "name": "Newsletter", + "stringid": "newsletter", + "sender_url": "https://example.com", + "sender_reminder": "You signed up for our newsletter." + } +} +``` + +### Subscribe contact to list + +```bash +POST https://{account}.api-us1.com/api/3/contactLists + +{ + "contactList": { + "list": "1", + "contact": "1", + "status": 1 + } +} +``` + +### Unsubscribe contact from list + +```bash +POST https://{account}.api-us1.com/api/3/contactLists + +{ + "contactList": { + "list": "1", + "contact": "1", + "status": 2 + } +} +``` + +### List campaigns + +```bash +GET https://{account}.api-us1.com/api/3/campaigns?limit=20&offset=0 +``` + +### List deals + +```bash +GET https://{account}.api-us1.com/api/3/deals?limit=20&offset=0 + +# Filter by pipeline stage +GET https://{account}.api-us1.com/api/3/deals?filters[stage]=1 +``` + +### Create deal + +```bash +POST https://{account}.api-us1.com/api/3/deals + +{ + "deal": { + "title": "New Enterprise Deal", + "value": 50000, + "currency": "usd", + "group": "1", + "stage": "1", + "owner": "1", + "contact": "1" + } +} +``` + +### Update deal + +```bash +PUT https://{account}.api-us1.com/api/3/deals/{dealId} + +{ + "deal": { + "stage": "2", + "value": 75000 + } +} +``` + +### List automations + +```bash +GET https://{account}.api-us1.com/api/3/automations?limit=20&offset=0 +``` + +### Add contact to automation + +```bash +POST https://{account}.api-us1.com/api/3/contactAutomations + +{ + "contactAutomation": { + "contact": "1", + "automation": "1" + } +} +``` + +### List tags + +```bash +GET https://{account}.api-us1.com/api/3/tags?limit=20&offset=0 +``` + +### Create tag + +```bash +POST https://{account}.api-us1.com/api/3/tags + +{ + "tag": { + "tag": "VIP Customer", + "tagType": "contact" + } +} +``` + +### Add tag to contact + +```bash +POST https://{account}.api-us1.com/api/3/contactTags + +{ + "contactTag": { + "contact": "1", + "tag": "1" + } +} +``` + +### List pipelines (deal groups) + +```bash +GET https://{account}.api-us1.com/api/3/dealGroups?limit=20&offset=0 +``` + +### List webhooks + +```bash +GET https://{account}.api-us1.com/api/3/webhooks?limit=20&offset=0 +``` + +### Create webhook + +```bash +POST https://{account}.api-us1.com/api/3/webhooks + +{ + "webhook": { + "name": "Contact Updated", + "url": "https://example.com/webhook", + "events": ["subscribe", "unsubscribe"], + "sources": ["public", "admin", "api", "system"] + } +} +``` + +## API Pattern + +ActiveCampaign uses REST with resource wrapping (e.g., `{ "contact": {...} }`). Responses include the resource object plus metadata. Related resources are managed via junction endpoints (e.g., `/contactLists`, `/contactTags`, `/contactAutomations`). The base URL is account-specific. Pagination uses `limit` and `offset` parameters. + +## Key Metrics + +### Contact Fields +- `email` - Email address +- `firstName`, `lastName` - Name fields +- `phone` - Phone number +- `cdate` - Creation date +- `udate` - Last updated date +- `deals` - Related deals count + +### Deal Fields +- `title` - Deal name +- `value` - Deal value in cents +- `currency` - Currency code +- `stage` - Pipeline stage ID +- `group` - Pipeline (deal group) ID +- `owner` - Assigned user ID +- `status` - 0 (open), 1 (won), 2 (lost) + +### Campaign Metrics +- `sends` - Total sends +- `opens` - Opens count +- `clicks` - Clicks count +- `uniqueopens` - Unique opens +- `uniquelinks` - Unique clicks + +## Parameters + +### Contact List Status +- `1` - Subscribed (active) +- `2` - Unsubscribed + +### Deal Status +- `0` - Open +- `1` - Won +- `2` - Lost + +### Tag Types +- `contact` - Contact tags +- `deal` - Deal tags + +### Common Query Parameters +- `limit` - Results per page (default 20) +- `offset` - Skip N results +- `search` - Text search +- `email` - Filter contacts by email +- `filters[stage]` - Filter deals by stage +- `filters[owner]` - Filter deals by owner + +## When to Use + +- Marketing automation with complex conditional workflows +- CRM with deal pipeline management +- Contact management with tagging and segmentation +- Email campaign creation and tracking +- Triggering automations based on external events +- B2B sales pipeline tracking integrated with marketing + +## Rate Limits + +- 5 requests per second per account +- Rate limit applies across all API users on the same account +- 429 responses include `Retry-After` header + +## Relevant Skills + +- email-sequence +- lifecycle-marketing +- crm-integration +- sales-pipeline +- marketing-automation diff --git a/tools/integrations/apollo.md b/tools/integrations/apollo.md new file mode 100644 index 0000000..7b07b5a --- /dev/null +++ b/tools/integrations/apollo.md @@ -0,0 +1,148 @@ +# Apollo.io + +B2B prospecting and data enrichment platform with 210M+ contacts and 35M+ companies for sales intelligence. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | People Search, Company Search, Enrichment, Sequences | +| MCP | - | Not available | +| CLI | ✓ | [apollo.js](../clis/apollo.js) | +| SDK | - | REST API only | + +## Authentication + +- **Type**: API Key +- **Header**: `x-api-key: {api_key}` or `Authorization: Bearer {token}` +- **Get key**: Settings > Integrations > API at https://app.apollo.io + +## Common Agent Operations + +### People Search + +```bash +POST https://api.apollo.io/api/v1/mixed_people/api_search + +{ + "person_titles": ["Sales Manager"], + "person_locations": ["United States"], + "organization_num_employees_ranges": ["1,100"], + "page": 1 +} +``` + +### Person Enrichment + +```bash +POST https://api.apollo.io/api/v1/people/match + +{ + "first_name": "Tim", + "last_name": "Zheng", + "domain": "apollo.io" +} +``` + +### Bulk People Enrichment + +```bash +POST https://api.apollo.io/api/v1/people/bulk_match + +{ + "details": [ + { "email": "tim@apollo.io" }, + { "first_name": "Jane", "last_name": "Doe", "domain": "example.com" } + ] +} +``` + +### Organization Search + +```bash +POST https://api.apollo.io/api/v1/mixed_companies/search + +{ + "organization_locations": ["United States"], + "organization_num_employees_ranges": ["1,100"], + "page": 1 +} +``` + +### Organization Enrichment + +```bash +POST https://api.apollo.io/api/v1/organizations/enrich + +{ + "domain": "apollo.io" +} +``` + +## Key Metrics + +### Person Data +- `first_name`, `last_name` - Name +- `title` - Job title +- `email` - Verified email +- `linkedin_url` - LinkedIn profile +- `organization` - Company details +- `seniority` - Seniority level +- `departments` - Department list + +### Organization Data +- `name` - Company name +- `website_url` - Website +- `estimated_num_employees` - Employee count +- `industry` - Industry +- `annual_revenue` - Revenue +- `technologies` - Tech stack +- `funding_total` - Total funding + +## Parameters + +### People Search +- `person_titles` - Array of job titles +- `person_locations` - Array of locations +- `person_seniorities` - Array: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry +- `organization_num_employees_ranges` - Array of ranges (e.g., "1,100") +- `organization_ids` - Filter by Apollo org IDs +- `page` - Page number (default: 1) +- `per_page` - Results per page (default: 25, max: 100) + +### Person Enrichment +- `email` - Email address +- `first_name` + `last_name` + `domain` - Alternative lookup +- `linkedin_url` - LinkedIn URL +- `reveal_personal_emails` - Include personal emails +- `reveal_phone_number` - Include phone numbers + +### Organization Search +- `organization_locations` - Array of locations +- `organization_num_employees_ranges` - Employee count ranges +- `organization_ids` - Specific org IDs +- `page` - Page number + +## When to Use + +- Building targeted prospect lists by role, seniority, and company size +- Enriching leads with verified contact info +- Finding decision-makers at target accounts +- Company research and firmographic analysis +- ABM campaign targeting +- Sales intelligence and outbound prospecting + +## Rate Limits + +- Rate limits vary by plan +- Standard: 100 requests/minute for most endpoints +- Bulk enrichment: up to 10 people per request +- Search: max 50,000 records (100 per page, 500 pages) + +## Relevant Skills + +- abm-strategy +- lead-enrichment +- lead-scoring +- cold-email +- competitor-alternatives diff --git a/tools/integrations/beehiiv.md b/tools/integrations/beehiiv.md new file mode 100644 index 0000000..d4ee1b4 --- /dev/null +++ b/tools/integrations/beehiiv.md @@ -0,0 +1,157 @@ +# Beehiiv + +Newsletter platform with subscriber management, post publishing, automations, and referral programs. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v2 for publications, subscriptions, posts, segments | +| MCP | - | Not available | +| CLI | ✓ | [beehiiv.js](../clis/beehiiv.js) | +| SDK | - | No official SDK; OpenAPI spec available for codegen | + +## Authentication + +- **Type**: Bearer Token +- **Header**: `Authorization: Bearer {api_key}` +- **Get key**: Settings > API under Workspace Settings at https://app.beehiiv.com +- **Note**: API key is only shown once on creation; copy and store it immediately + +## Common Agent Operations + +### List publications + +```bash +GET https://api.beehiiv.com/v2/publications +``` + +### Get publication details + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId} +``` + +### List subscriptions + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId}/subscriptions?limit=10&status=active + +# Filter by email +GET https://api.beehiiv.com/v2/publications/{publicationId}/subscriptions?email=user@example.com +``` + +### Create subscription + +```bash +POST https://api.beehiiv.com/v2/publications/{publicationId}/subscriptions + +{ + "email": "user@example.com", + "reactivate_existing": false, + "send_welcome_email": true, + "utm_source": "api", + "tier": "free" +} +``` + +### Update subscription + +```bash +PUT https://api.beehiiv.com/v2/publications/{publicationId}/subscriptions/{subscriptionId} + +{ + "tier": "premium" +} +``` + +### Delete subscription + +```bash +DELETE https://api.beehiiv.com/v2/publications/{publicationId}/subscriptions/{subscriptionId} +``` + +### List posts + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId}/posts?limit=10&status=confirmed +``` + +### Create post (Enterprise only) + +```bash +POST https://api.beehiiv.com/v2/publications/{publicationId}/posts + +{ + "title": "Weekly Update", + "subtitle": "What happened this week", + "content": "<p>Hello subscribers...</p>", + "status": "draft" +} +``` + +### List segments + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId}/segments +``` + +### List automations + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId}/automations +``` + +### Get referral program + +```bash +GET https://api.beehiiv.com/v2/publications/{publicationId}/referral_program +``` + +## API Pattern + +All endpoints are scoped to a publication. The publication ID is a required path parameter for most operations. Responses use cursor-based pagination with a `cursor` parameter for fetching subsequent pages. + +## Key Metrics + +### Subscription Fields +- `status` - validating, invalid, pending, active, inactive +- `tier` - free or premium +- `created` - Subscription creation timestamp +- `utm_source`, `utm_medium`, `utm_campaign` - Acquisition tracking +- `referral_code` - Unique referral code for subscriber + +### Post Fields +- `status` - draft, confirmed (scheduled), archived +- `publish_date` - When the post was/will be published +- `stats` - Open rate, click rate, subscriber count (with expand) + +## Parameters + +### Common Query Parameters +- `limit` - Results per page (1-100, default 10) +- `cursor` - Cursor for next page of results +- `expand[]` - Include additional data: stats, custom_fields, referrals +- `status` - Filter by subscription/post status +- `tier` - Filter by subscription tier (free, premium) + +## When to Use + +- Managing newsletter subscribers programmatically +- Syncing subscribers from external signup forms or landing pages +- Building referral program integrations +- Automating post creation and publishing workflows +- Tracking subscriber growth and engagement metrics + +## Rate Limits + +- API rate limits apply per API key +- Use cursor-based pagination for efficient data retrieval +- Batch operations not available; iterate with individual requests + +## Relevant Skills + +- email-sequence +- newsletter-growth +- referral-program +- content-strategy diff --git a/tools/integrations/brevo.md b/tools/integrations/brevo.md new file mode 100644 index 0000000..e82f51f --- /dev/null +++ b/tools/integrations/brevo.md @@ -0,0 +1,268 @@ +# Brevo + +All-in-one marketing platform (formerly Sendinblue) for email, SMS, and WhatsApp with contacts, campaigns, and transactional messaging. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v3 for contacts, campaigns, transactional email/SMS | +| MCP | - | Not available | +| CLI | ✓ | [brevo.js](../clis/brevo.js) | +| SDK | ✓ | Node.js, Python, PHP, Ruby, Java, C#, Go | + +## Authentication + +- **Type**: API Key +- **Header**: `api-key: {api_key}` +- **Get key**: SMTP & API settings at https://app.brevo.com/settings/keys/api +- **Note**: API key is only shown once on creation; store securely. Formerly used `api.sendinblue.com` base URL. + +## Common Agent Operations + +### Get account info + +```bash +GET https://api.brevo.com/v3/account +``` + +### List contacts + +```bash +GET https://api.brevo.com/v3/contacts?limit=50&offset=0 +``` + +### Get contact by email + +```bash +GET https://api.brevo.com/v3/contacts/user@example.com +``` + +### Create contact + +```bash +POST https://api.brevo.com/v3/contacts + +{ + "email": "user@example.com", + "attributes": { + "FIRSTNAME": "Jane", + "LASTNAME": "Doe" + }, + "listIds": [1, 2] +} +``` + +### Update contact + +```bash +PUT https://api.brevo.com/v3/contacts/user@example.com + +{ + "attributes": { + "FIRSTNAME": "Updated" + }, + "listIds": [3] +} +``` + +### Delete contact + +```bash +DELETE https://api.brevo.com/v3/contacts/user@example.com +``` + +### Import contacts + +```bash +POST https://api.brevo.com/v3/contacts/import + +{ + "jsonBody": [ + { "email": "user1@example.com" }, + { "email": "user2@example.com" } + ], + "listIds": [1] +} +``` + +### List contact lists + +```bash +GET https://api.brevo.com/v3/contacts/lists?limit=50&offset=0 +``` + +### Create list + +```bash +POST https://api.brevo.com/v3/contacts/lists + +{ + "name": "Newsletter Subscribers", + "folderId": 1 +} +``` + +### Add contacts to list + +```bash +POST https://api.brevo.com/v3/contacts/lists/{listId}/contacts/add + +{ + "emails": ["user1@example.com", "user2@example.com"] +} +``` + +### Remove contacts from list + +```bash +POST https://api.brevo.com/v3/contacts/lists/{listId}/contacts/remove + +{ + "emails": ["user1@example.com"] +} +``` + +### Send transactional email + +```bash +POST https://api.brevo.com/v3/smtp/email + +{ + "sender": { + "name": "My App", + "email": "noreply@example.com" + }, + "to": [ + { "email": "user@example.com", "name": "Jane Doe" } + ], + "subject": "Order Confirmation", + "htmlContent": "<html><body><p>Your order is confirmed.</p></body></html>" +} +``` + +### List email campaigns + +```bash +GET https://api.brevo.com/v3/emailCampaigns?limit=50&offset=0&type=classic&status=sent +``` + +### Create email campaign + +```bash +POST https://api.brevo.com/v3/emailCampaigns + +{ + "name": "January Newsletter", + "subject": "Monthly Update", + "sender": { "name": "My Brand", "email": "news@example.com" }, + "htmlContent": "<html><body><p>Newsletter content</p></body></html>", + "recipients": { "listIds": [1, 2] } +} +``` + +### Send campaign immediately + +```bash +POST https://api.brevo.com/v3/emailCampaigns/{campaignId}/sendNow +``` + +### Send test email for campaign + +```bash +POST https://api.brevo.com/v3/emailCampaigns/{campaignId}/sendTest + +{ + "emailTo": ["test@example.com"] +} +``` + +### Send transactional SMS + +```bash +POST https://api.brevo.com/v3/transactionalSMS/sms + +{ + "sender": "MyApp", + "recipient": "+15551234567", + "content": "Your verification code is 123456", + "type": "transactional" +} +``` + +### List SMS campaigns + +```bash +GET https://api.brevo.com/v3/smsCampaigns?limit=50&offset=0 +``` + +### List senders + +```bash +GET https://api.brevo.com/v3/senders +``` + +## API Pattern + +Brevo uses standard REST with offset-based pagination (`limit` and `offset` parameters). Contact attributes use uppercase field names (FIRSTNAME, LASTNAME). Lists are nested under the contacts resource path. Transactional email uses the `/smtp/email` endpoint despite being REST-based. + +## Key Metrics + +### Contact Fields +- `email` - Email address +- `attributes` - Custom attributes (FIRSTNAME, LASTNAME, SMS, etc.) +- `listIds` - Associated list IDs +- `emailBlacklisted` - Email opt-out status +- `smsBlacklisted` - SMS opt-out status +- `statistics` - Engagement stats (with expand) + +### Campaign Metrics +- `sent` - Total sends +- `delivered` - Successful deliveries +- `openRate` - Open percentage +- `clickRate` - Click percentage +- `unsubscribed` - Unsubscribe count +- `hardBounces`, `softBounces` - Bounce counts + +### Transactional Email Response +- `messageId` - Unique message identifier for tracking + +## Parameters + +### Contact Parameters +- `email` - Contact email address +- `attributes` - Key-value object of custom attributes +- `listIds` - Array of list IDs to subscribe to +- `unlinkListIds` - Array of list IDs to unsubscribe from + +### Campaign Parameters +- `name` - Campaign name +- `subject` - Email subject line +- `sender` - Object with `name` and `email` +- `htmlContent` / `textContent` - Email body +- `recipients` - Object with `listIds` array +- `type` - classic or trigger + +## When to Use + +- Multi-channel marketing (email + SMS + WhatsApp) +- Transactional email sending with tracking +- Managing contacts and segmented lists +- Creating and scheduling email campaigns +- SMS notifications and marketing +- Affordable all-in-one marketing automation + +## Rate Limits + +- API rate limits depend on plan (free tier: limited sends/day) +- Transactional email: varies by plan +- Contact imports: batch processing with async status +- Rate limit headers returned with responses + +## Relevant Skills + +- email-sequence +- sms-marketing +- transactional-email +- lifecycle-marketing +- contact-management diff --git a/tools/integrations/buffer.md b/tools/integrations/buffer.md new file mode 100644 index 0000000..85f5771 --- /dev/null +++ b/tools/integrations/buffer.md @@ -0,0 +1,138 @@ +# Buffer + +Social media scheduling, publishing, and analytics platform for managing multiple social profiles. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v1 for profiles, updates, scheduling | +| MCP | - | Not available | +| CLI | ✓ | [buffer.js](../clis/buffer.js) | +| SDK | - | No official SDK; legacy API still supported | + +## Authentication + +- **Type**: OAuth 2.0 Bearer Token +- **Header**: `Authorization: Bearer {access_token}` +- **Get key**: Register app at https://buffer.com/developers/apps then complete OAuth flow +- **Note**: Buffer is no longer accepting new developer app registrations; existing apps continue to work. New public API is in development at https://buffer.com/developer-api + +## Common Agent Operations + +### Get user info + +```bash +GET https://api.bufferapp.com/1/user.json + +Authorization: Bearer {token} +``` + +### List connected profiles + +```bash +GET https://api.bufferapp.com/1/profiles.json + +Authorization: Bearer {token} +``` + +### Get profile posting schedules + +```bash +GET https://api.bufferapp.com/1/profiles/{profile_id}/schedules.json +``` + +### Create a scheduled post + +```bash +POST https://api.bufferapp.com/1/updates/create.json +Content-Type: application/x-www-form-urlencoded + +profile_ids[]={profile_id}&text=Your+post+content&scheduled_at=2026-03-01T10:00:00Z +``` + +### Get pending updates for a profile + +```bash +GET https://api.bufferapp.com/1/profiles/{profile_id}/updates/pending.json?count=25 +``` + +### Get sent updates for a profile + +```bash +GET https://api.bufferapp.com/1/profiles/{profile_id}/updates/sent.json?count=25 +``` + +### Publish a pending update immediately + +```bash +POST https://api.bufferapp.com/1/updates/{update_id}/share.json +``` + +### Delete an update + +```bash +POST https://api.bufferapp.com/1/updates/{update_id}/destroy.json +``` + +### Reorder queue + +```bash +POST https://api.bufferapp.com/1/profiles/{profile_id}/updates/reorder.json +Content-Type: application/x-www-form-urlencoded + +order[]={update_id_1}&order[]={update_id_2}&order[]={update_id_3} +``` + +## API Pattern + +Buffer API v1 uses `.json` extensions on all endpoints. POST requests use `application/x-www-form-urlencoded` content type. Array parameters use bracket notation (e.g., `profile_ids[]`). + +Responses include a `success` boolean for mutation operations. + +## Key Metrics + +### Profile Metrics +- `followers` - Follower count for connected profile +- `service` - Platform name (twitter, facebook, instagram, linkedin, etc.) + +### Update Metrics (sent updates) +- `statistics.reach` - Post reach +- `statistics.clicks` - Link clicks +- `statistics.retweets` - Retweets/shares +- `statistics.favorites` - Likes/favorites +- `statistics.mentions` - Mentions + +## Parameters + +### Update Create Parameters +- `profile_ids[]` - Required. Array of profile IDs to post to +- `text` - Required. Post content +- `scheduled_at` - ISO 8601 timestamp for scheduling +- `now` - Set to `true` to publish immediately +- `top` - Set to `true` to add to top of queue +- `shorten` - Set to `true` to auto-shorten links +- `media[photo]` - URL to photo attachment +- `media[thumbnail]` - URL to thumbnail +- `media[link]` - URL for link attachment + +## When to Use + +- Scheduling social media posts across multiple platforms +- Managing social media content queues +- Analyzing post performance across channels +- Automating social media publishing workflows +- Coordinating team social media activity + +## Rate Limits + +- 60 authenticated requests per user per minute +- Exceeding returns HTTP 429 +- Higher limits available by contacting hello@buffer.com + +## Relevant Skills + +- social-media-calendar +- content-repurposing +- social-proof +- launch-sequence diff --git a/tools/integrations/calendly.md b/tools/integrations/calendly.md new file mode 100644 index 0000000..d31e0d3 --- /dev/null +++ b/tools/integrations/calendly.md @@ -0,0 +1,161 @@ +# Calendly + +Scheduling and booking platform API for managing event types, scheduled events, invitees, and availability. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v2 - event types, scheduled events, invitees, availability | +| MCP | - | Not available | +| CLI | ✓ | [calendly.js](../clis/calendly.js) | +| SDK | ✓ | No official SDK; community libraries available | + +## Authentication + +- **Type**: Bearer Token (Personal Access Token or OAuth 2.0) +- **Header**: `Authorization: Bearer {token}` +- **Get key**: https://calendly.com/integrations/api_webhooks (Personal Access Token) + +## Common Agent Operations + +### Get current user + +```bash +GET https://api.calendly.com/users/me +``` + +### List event types + +```bash +GET https://api.calendly.com/event_types?user={user_uri} +``` + +### List scheduled events + +```bash +GET https://api.calendly.com/scheduled_events?user={user_uri}&min_start_time=2024-01-01T00:00:00Z&max_start_time=2024-12-31T23:59:59Z&status=active +``` + +### Get a scheduled event + +```bash +GET https://api.calendly.com/scheduled_events/{event_uuid} +``` + +### List invitees for an event + +```bash +GET https://api.calendly.com/scheduled_events/{event_uuid}/invitees +``` + +### Cancel a scheduled event + +```bash +POST https://api.calendly.com/scheduled_events/{event_uuid}/cancellation + +{ + "reason": "Cancellation reason" +} +``` + +### Get available times + +```bash +GET https://api.calendly.com/event_type_available_times?event_type={event_type_uri}&start_time=2024-01-20T00:00:00Z&end_time=2024-01-27T00:00:00Z +``` + +### Get user busy times + +```bash +GET https://api.calendly.com/user_busy_times?user={user_uri}&start_time=2024-01-20T00:00:00Z&end_time=2024-01-27T00:00:00Z +``` + +### List organization members + +```bash +GET https://api.calendly.com/organization_memberships?organization={organization_uri} +``` + +### Create webhook subscription + +```bash +POST https://api.calendly.com/webhook_subscriptions + +{ + "url": "https://example.com/webhook", + "events": ["invitee.created", "invitee.canceled"], + "organization": "{organization_uri}", + "scope": "organization" +} +``` + +### List webhook subscriptions + +```bash +GET https://api.calendly.com/webhook_subscriptions?organization={organization_uri}&scope=organization +``` + +### Delete webhook subscription + +```bash +DELETE https://api.calendly.com/webhook_subscriptions/{webhook_uuid} +``` + +## Key Metrics + +### Scheduled Event Data +- `uri` - Unique event URI +- `name` - Event type name +- `status` - Event status (active, canceled) +- `start_time` / `end_time` - Event timing +- `event_type` - URI of the event type +- `location` - Meeting location details +- `invitees_counter` - Count of invitees (active, limit, total) + +### Invitee Data +- `name` - Invitee full name +- `email` - Invitee email +- `status` - active or canceled +- `questions_and_answers` - Custom question responses +- `tracking` - UTM parameters +- `created_at` / `updated_at` - Timestamps + +## Parameters + +### List Scheduled Events +- `user` - User URI (required) +- `min_start_time` / `max_start_time` - Date range filter (ISO 8601) +- `status` - Filter by status (active, canceled) +- `count` - Number of results (default 20, max 100) +- `page_token` - Pagination token +- `sort` - Sort order (start_time:asc or start_time:desc) + +### List Event Types +- `user` - User URI +- `organization` - Organization URI +- `active` - Filter active/inactive +- `count` - Results per page +- `sort` - Sort order + +## When to Use + +- Retrieving scheduled meeting data for CRM sync +- Monitoring booking activity and conversion rates +- Automating follow-up workflows after meetings +- Checking availability before suggesting meeting times +- Tracking meeting cancellations and no-shows +- Building custom booking interfaces + +## Rate Limits + +- Not officially documented; implement retry logic with exponential backoff +- Use conservative request rates (avoid bursting) +- Monitor for HTTP 429 responses + +## Relevant Skills + +- lead-generation +- sales-automation +- customer-onboarding +- appointment-scheduling diff --git a/tools/integrations/clearbit.md b/tools/integrations/clearbit.md new file mode 100644 index 0000000..0b441d5 --- /dev/null +++ b/tools/integrations/clearbit.md @@ -0,0 +1,142 @@ +# Clearbit (HubSpot Breeze Intelligence) + +Company and person data enrichment API for converting leads with 100+ firmographic and technographic attributes. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Person, Company, Combined Enrichment, Reveal, Name to Domain, Prospector | +| MCP | - | Not available | +| CLI | ✓ | [clearbit.js](../clis/clearbit.js) | +| SDK | ✓ | Node, Ruby, Python, PHP | + +## Authentication + +- **Type**: Bearer Token (or Basic Auth with API key as username) +- **Header**: `Authorization: Bearer {api_key}` +- **Get key**: https://dashboard.clearbit.com/api + +## Common Agent Operations + +### Person Enrichment (by email) + +```bash +GET https://person.clearbit.com/v2/people/find?email=alex@clearbit.com +``` + +Returns 100+ attributes: name, title, company, location, social profiles, employment history. + +### Company Enrichment (by domain) + +```bash +GET https://company.clearbit.com/v2/companies/find?domain=clearbit.com +``` + +Returns firmographics: industry, size, revenue, tech stack, location, funding. + +### Combined Enrichment (person + company) + +```bash +GET https://person.clearbit.com/v2/combined/find?email=alex@clearbit.com +``` + +Returns both person and company data in a single request. + +### Reveal (IP to company) + +```bash +GET https://reveal.clearbit.com/v1/companies/find?ip=104.132.0.0 +``` + +Identifies the company behind a website visitor by IP address. + +### Name to Domain + +```bash +GET https://company.clearbit.com/v1/domains/find?name=Clearbit +``` + +Converts a company name to its domain. + +### Prospector (find employees) + +```bash +GET https://prospector.clearbit.com/v1/people/search?domain=clearbit.com&role=sales&seniority=executive +``` + +Finds employees at a company filtered by role, seniority, title. + +## API Pattern + +Clearbit uses separate subdomains per API: +- `person.clearbit.com` - Person data +- `company.clearbit.com` - Company data, Name to Domain +- `person-stream.clearbit.com` - Streaming person lookup (blocking, up to 60s) +- `company-stream.clearbit.com` - Streaming company lookup (blocking, up to 60s) +- `reveal.clearbit.com` - IP to company +- `prospector.clearbit.com` - Employee search + +Standard endpoints return `202 Accepted` if data is being processed (use webhooks). Stream endpoints block until data is ready. + +## Key Metrics + +### Person Attributes +- `name.fullName` - Full name +- `title` - Job title +- `role` - Job role (sales, engineering, etc.) +- `seniority` - Seniority level +- `employment.name` - Company name +- `linkedin.handle` - LinkedIn profile + +### Company Attributes +- `name` - Company name +- `domain` - Website domain +- `category.industry` - Industry +- `metrics.employees` - Employee count +- `metrics.estimatedAnnualRevenue` - Revenue range +- `tech` - Technology stack array +- `metrics.raised` - Total funding raised + +## Parameters + +### Person Enrichment +- `email` (required) - Email address to look up +- `webhook_url` - URL for async results +- `subscribe` - Subscribe to future changes + +### Company Enrichment +- `domain` (required) - Company domain to look up +- `webhook_url` - URL for async results + +### Prospector +- `domain` (required) - Company domain +- `role` - Job role filter (sales, engineering, marketing, etc.) +- `seniority` - Seniority filter (executive, director, manager, etc.) +- `title` - Exact title filter +- `page` - Page number (default: 1) +- `page_size` - Results per page (default: 5, max: 20) + +## When to Use + +- Lead scoring and qualification based on firmographic data +- Enriching CRM contacts with company and person data +- De-anonymizing website visitors with Reveal +- Building prospect lists with Prospector +- Personalizing marketing based on company attributes +- Routing leads based on company size, industry, or tech stack + +## Rate Limits + +- Enrichment: 600 requests/minute +- Prospector: 100 requests/minute +- Reveal: 600 requests/minute +- Responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers + +## Relevant Skills + +- lead-scoring +- personalization +- abm-strategy +- lead-enrichment +- competitor-alternatives diff --git a/tools/integrations/dataforseo.md b/tools/integrations/dataforseo.md new file mode 100644 index 0000000..a1155af --- /dev/null +++ b/tools/integrations/dataforseo.md @@ -0,0 +1,165 @@ +# DataForSEO + +Comprehensive SEO data API for SERP results, keyword research, backlinks, and on-page analysis. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | SERP, Keywords Data, Backlinks, On-Page, Labs | +| MCP | - | Not available | +| CLI | ✓ | [dataforseo.js](../clis/dataforseo.js) | +| SDK | ✓ | Python, TypeScript, PHP, Java, C# | + +## Authentication + +- **Type**: Basic Auth +- **Header**: `Authorization: Basic {base64(login:password)}` +- **Get credentials**: API Access tab at https://app.dataforseo.com/api-access +- **Note**: API password is auto-generated, different from account password + +## Common Agent Operations + +### SERP - Google organic (live) + +```bash +POST https://api.dataforseo.com/v3/serp/google/organic/live/regular + +[{ + "keyword": "marketing automation", + "location_name": "United States", + "language_name": "English" +}] +``` + +### Keywords - Search volume (live) + +```bash +POST https://api.dataforseo.com/v3/keywords_data/google_ads/search_volume/live + +[{ + "keywords": ["email marketing", "marketing automation", "crm software"], + "location_code": 2840, + "language_code": "en" +}] +``` + +### Keywords - Keywords for site (live) + +```bash +POST https://api.dataforseo.com/v3/keywords_data/google_ads/keywords_for_site/live + +[{ + "target": "example.com", + "location_code": 2840, + "language_code": "en" +}] +``` + +### Backlinks - Summary + +```bash +POST https://api.dataforseo.com/v3/backlinks/summary/live + +[{ + "target": "example.com", + "internal_list_limit": 10, + "backlinks_status_type": "live" +}] +``` + +### Backlinks - List + +```bash +POST https://api.dataforseo.com/v3/backlinks/backlinks/live + +[{ + "target": "example.com", + "mode": "as_is", + "limit": 100, + "backlinks_status_type": "live" +}] +``` + +### Backlinks - Referring domains + +```bash +POST https://api.dataforseo.com/v3/backlinks/referring_domains/live + +[{ + "target": "example.com", + "limit": 100 +}] +``` + +### Backlinks - Index (database stats) + +```bash +GET https://api.dataforseo.com/v3/backlinks/index +``` + +### On-Page - Instant pages audit + +```bash +POST https://api.dataforseo.com/v3/on_page/instant_pages + +[{ + "url": "https://example.com/page", + "enable_javascript": true +}] +``` + +### SERP - Locations list + +```bash +GET https://api.dataforseo.com/v3/serp/google/locations +``` + +### SERP - Languages list + +```bash +GET https://api.dataforseo.com/v3/serp/google/languages +``` + +## API Pattern + +DataForSEO uses two methods for most endpoints: +- **Live** (`/live`) - Synchronous, results in same response +- **Task-based** (`/task_post` + `/task_get/$id`) - Async for large requests + +Request bodies are always JSON arrays (even for single requests). + +## Key Metrics + +### Keyword Metrics +- `search_volume` - Monthly search volume +- `competition` - Competition level (0-1) +- `cpc` - Cost per click +- `monthly_searches` - Monthly breakdown array + +### Backlink Metrics +- `total_backlinks` - Total backlink count +- `referring_domains` - Unique referring domains +- `domain_rank` - Domain authority score +- `backlinks_spam_score` - Spam score + +## When to Use + +- Programmatic SERP tracking at scale +- Keyword research with search volume data +- Backlink analysis and monitoring +- On-page SEO audits +- Competitor analysis + +## Rate Limits + +- Rate limit headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining` +- Backlinks API: 2000 requests/minute, 30 simultaneous +- Varies by endpoint and plan + +## Relevant Skills + +- seo-audit +- programmatic-seo +- content-strategy +- competitor-alternatives diff --git a/tools/integrations/demio.md b/tools/integrations/demio.md new file mode 100644 index 0000000..d71dd15 --- /dev/null +++ b/tools/integrations/demio.md @@ -0,0 +1,182 @@ +# Demio + +Webinar platform for hosting live, automated, and on-demand webinars with built-in registration and attendee tracking. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Events, Registration, Participants, Sessions | +| MCP | - | Not available | +| CLI | ✓ | [demio.js](../clis/demio.js) | +| SDK | ✓ | PHP (official), Ruby (community) | + +## Authentication + +- **Type**: API Key + API Secret +- **Headers**: `Api-Key: {key}` and `Api-Secret: {secret}` +- **Get credentials**: Account Settings > API (Owner access required) +- **Docs**: https://publicdemioapi.docs.apiary.io/ + +## Common Agent Operations + +### Ping (health check) + +```bash +GET https://my.demio.com/api/v1/ping + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +### List all events + +```bash +GET https://my.demio.com/api/v1/events + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +### List events by type + +```bash +GET https://my.demio.com/api/v1/events?type=upcoming + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +### Get a specific event + +```bash +GET https://my.demio.com/api/v1/event/{event_id} + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +### Get event date details + +```bash +GET https://my.demio.com/api/v1/event/{event_id}/date/{date_id} + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +### Register attendee for event + +```bash +POST https://my.demio.com/api/v1/event/register + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} + Content-Type: application/json + +{ + "id": 12345, + "name": "Jane Doe", + "email": "jane@example.com" +} +``` + +### Register attendee for specific date + +```bash +POST https://my.demio.com/api/v1/event/register + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} + Content-Type: application/json + +{ + "id": 12345, + "date_id": 67890, + "name": "Jane Doe", + "email": "jane@example.com" +} +``` + +### Get participants for event date + +```bash +GET https://my.demio.com/api/v1/date/{date_id}/participants + +Headers: + Api-Key: {API_KEY} + Api-Secret: {API_SECRET} +``` + +## API Pattern + +Demio uses a straightforward REST API: +- All requests require both `Api-Key` and `Api-Secret` headers +- Responses are JSON objects +- Registration returns a `join_link` URL for the attendee +- Events have multiple "dates" (sessions), each with a unique `date_id` + +## Key Metrics + +### Event Metrics +- `id` - Event ID +- `name` - Event name +- `date_id` - Session/date identifier +- `status` - Event status (upcoming, past, active) +- `type` - Event type (live, automated, on-demand) +- `registration_url` - Public registration page URL + +### Participant Metrics +- `name` - Participant name +- `email` - Participant email +- `status` - Attendance status (registered, attended, missed) +- `attended_minutes` - Duration of attendance +- `join_link` - Unique join URL for the participant + +## Parameters + +### Event List Filters +- `type` - Filter by event type: `upcoming`, `past`, `all` + +### Registration Fields +- `id` - Event ID (required) +- `name` - Registrant name (required) +- `email` - Registrant email (required) +- `date_id` - Specific session date ID (optional) +- `ref_url` - Referral URL for tracking (optional) + +### Custom Fields +- Custom fields are supported via their UID (not display name) +- Check your event settings for available custom field UIDs + +## When to Use + +- Automating webinar registration from landing pages or forms +- Syncing webinar attendee data with CRM +- Building custom registration flows for webinars +- Tracking webinar attendance and engagement +- Triggering follow-up sequences based on attendance status +- Managing multiple webinar sessions programmatically + +## Rate Limits + +- **180 requests per minute** (3 per second) +- **Free Trial**: 100 API calls per day +- **Paid Plans**: 5,000 API calls per day (reset at 00:00 UTC) +- Contact Demio to request higher daily limits +- Exceeding limits returns an error response + +## Relevant Skills + +- webinar-marketing +- lead-generation +- event-marketing +- content-strategy +- lifecycle-marketing diff --git a/tools/integrations/g2.md b/tools/integrations/g2.md new file mode 100644 index 0000000..79cc2c7 --- /dev/null +++ b/tools/integrations/g2.md @@ -0,0 +1,179 @@ +# G2 + +Software review and research platform for B2B buyers. Access reviews, product data, competitor comparisons, and buyer intent signals. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Reviews, Products, Reports, Categories, Tracking | +| MCP | - | Not available | +| CLI | ✓ | [g2.js](../clis/g2.js) | +| SDK | - | REST API with JSON:API format | + +## Authentication + +- **Type**: API Token +- **Header**: `Authorization: Token token={YOUR_API_TOKEN}` +- **Content-Type**: `application/vnd.api+json` (JSON:API) +- **Get token**: G2 Admin Portal > Integrations > API Tokens +- **Docs**: https://data.g2.com/api/docs + +## Common Agent Operations + +### List reviews (survey responses) + +```bash +GET https://data.g2.com/api/v1/survey-responses?page[size]=25&page[number]=1 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### Get a specific review + +```bash +GET https://data.g2.com/api/v1/survey-responses/{id} + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### Filter reviews by product + +```bash +GET https://data.g2.com/api/v1/survey-responses?filter[product_id]={product_id}&page[size]=25 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### List products + +```bash +GET https://data.g2.com/api/v1/products?page[size]=25&page[number]=1 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### Get a specific product + +```bash +GET https://data.g2.com/api/v1/products/{id} + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### List reports + +```bash +GET https://data.g2.com/api/v1/reports?page[size]=25&page[number]=1 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### List categories + +```bash +GET https://data.g2.com/api/v1/categories?page[size]=25&page[number]=1 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### Get competitor comparisons + +```bash +GET https://data.g2.com/api/v1/competitor-comparisons?filter[product_id]={product_id}&page[size]=25 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +### Get tracking events (buyer intent) + +```bash +GET https://data.g2.com/api/v1/tracking-events?filter[start_date]=2025-01-01&filter[end_date]=2025-12-31 + +Headers: + Authorization: Token token={API_TOKEN} + Content-Type: application/vnd.api+json +``` + +## API Pattern + +G2 follows the JSON:API specification (https://jsonapi.org/): +- Responses use `data`, `attributes`, `relationships`, `meta` structure +- Pagination: `page[number]` and `page[size]` query parameters +- Filtering: `filter[field]=value` query parameters +- Reviews returned newest-first by default (10 per page default) + +## Key Metrics + +### Review Metrics +- `star_rating` - Overall star rating +- `title` - Review title +- `comment_answers` - Structured review responses (likes, dislikes, recommendations) +- `submitted_at` - Review submission date +- `is_public` - Whether the review is publicly visible + +### Product Metrics +- `name` - Product name +- `slug` - URL slug on G2 +- `avg_rating` - Average star rating +- `total_reviews` - Total review count +- `category` - G2 category placement + +### Buyer Intent (Tracking) +- `company_name` - Visiting company name +- `page_visited` - G2 page URL visited +- `visited_at` - Visit timestamp +- `activity_type` - Type of buyer activity + +## Parameters + +### Pagination +- `page[number]` - Page number (default: 1) +- `page[size]` - Items per page (default: 10, max: 100) + +### Review Filters +- `filter[product_id]` - Filter by product ID +- `filter[state]` - Filter by review state + +### Tracking Filters +- `filter[start_date]` - Start date (YYYY-MM-DD) +- `filter[end_date]` - End date (YYYY-MM-DD) + +## When to Use + +- Monitoring and analyzing software product reviews +- Tracking buyer intent signals from G2 visitors +- Pulling competitor comparison data for positioning +- Feeding review data into CRM or marketing automation +- Building social proof content from G2 reviews +- Tracking G2 category rankings and report placements + +## Rate Limits + +- 10,000 requests per hour per API token +- Implement exponential backoff on 429 responses +- Cache results where possible to reduce API calls + +## Relevant Skills + +- competitor-alternatives +- social-proof +- reputation-management +- customer-feedback +- review-generation diff --git a/tools/integrations/hotjar.md b/tools/integrations/hotjar.md new file mode 100644 index 0000000..91f0c1f --- /dev/null +++ b/tools/integrations/hotjar.md @@ -0,0 +1,147 @@ +# Hotjar + +Behavior analytics platform with heatmaps, session recordings, and surveys for understanding user experience. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Surveys, Responses, Sites, Heatmaps, Recordings | +| MCP | - | Not available | +| CLI | ✓ | [hotjar.js](../clis/hotjar.js) | +| SDK | ✓ | JavaScript tracking snippet, Identify API, Events API | + +## Authentication + +- **Type**: OAuth 2.0 Client Credentials +- **Token endpoint**: `POST https://api.hotjar.io/v1/oauth/token` +- **Header**: `Authorization: Bearer {access_token}` +- **Get credentials**: Hotjar Dashboard > Integrations > API +- **Token expiry**: 3600 seconds (1 hour) + +### Token Request + +```bash +POST https://api.hotjar.io/v1/oauth/token +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials&client_id={client_id}&client_secret={client_secret} +``` + +### Token Response + +```json +{ + "access_token": "<token>", + "token_type": "Bearer", + "expires_in": 3600 +} +``` + +## Common Agent Operations + +### List Sites + +```bash +GET https://api.hotjar.io/v1/sites + +Authorization: Bearer {access_token} +``` + +### List Surveys + +```bash +GET https://api.hotjar.io/v1/sites/{site_id}/surveys + +Authorization: Bearer {access_token} +``` + +### Get Survey Responses + +```bash +GET https://api.hotjar.io/v1/sites/{site_id}/surveys/{survey_id}/responses?limit=100 + +Authorization: Bearer {access_token} +``` + +Supports cursor-based pagination with `cursor` and `limit` parameters. + +### List Heatmaps + +```bash +GET https://api.hotjar.io/v1/sites/{site_id}/heatmaps + +Authorization: Bearer {access_token} +``` + +### List Recordings + +```bash +GET https://api.hotjar.io/v1/sites/{site_id}/recordings + +Authorization: Bearer {access_token} +``` + +### List Forms + +```bash +GET https://api.hotjar.io/v1/sites/{site_id}/forms + +Authorization: Bearer {access_token} +``` + +## Key Metrics + +### Survey Response Data +- `response_id` - Unique response identifier +- `answers` - Array of question/answer pairs +- `created_at` - Response timestamp +- `device_type` - Desktop, mobile, tablet + +### Heatmap Data +- `url` - Page URL +- `click_count` - Total clicks tracked +- `visitors` - Unique visitors +- `created_at` - Heatmap creation date + +### Recording Data +- `recording_id` - Unique recording ID +- `duration` - Session duration +- `pages_visited` - Pages in session +- `device` - Device information + +## Parameters + +### Survey Responses +- `limit` - Results per page (default: 100) +- `cursor` - Pagination cursor from previous response +- `sort` - Sort order (default: created_at desc) + +### Recordings +- `limit` - Results per page +- `cursor` - Pagination cursor +- `date_from` - Start date filter +- `date_to` - End date filter + +## When to Use + +- Analyzing user behavior patterns on landing pages +- Collecting qualitative feedback via on-site surveys +- Identifying UX issues through session recordings +- Understanding scroll depth and engagement via heatmaps +- Validating CRO hypotheses with user behavior data +- Form abandonment analysis + +## Rate Limits + +- 3000 requests/minute (50 per second) +- Rate limited by source IP address +- Cursor-based pagination for large result sets + +## Relevant Skills + +- page-cro +- ab-test-setup +- analytics-tracking +- ux-audit +- landing-page diff --git a/tools/integrations/hunter.md b/tools/integrations/hunter.md new file mode 100644 index 0000000..04250e3 --- /dev/null +++ b/tools/integrations/hunter.md @@ -0,0 +1,90 @@ +# Hunter.io + +Email finding and verification platform for outreach and link building. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for domain search, email finder, verification | +| MCP | - | Not available | +| CLI | [✓](../clis/hunter.js) | Zero-dependency Node.js CLI | +| SDK | - | API-only | + +## Authentication + +- **Type**: API Key (query parameter) +- **Parameter**: `api_key={key}` +- **Env var**: `HUNTER_API_KEY` +- **Get key**: [Hunter dashboard > API](https://hunter.io/api-keys) + +## Common Agent Operations + +### Find emails for a domain + +```bash +node tools/clis/hunter.js domain search --domain example.com --limit 10 +``` + +### Find a specific person's email + +```bash +node tools/clis/hunter.js email find --domain example.com --first-name John --last-name Doe +``` + +### Verify an email address + +```bash +node tools/clis/hunter.js email verify --email john@example.com +``` + +### Count emails available for a domain + +```bash +node tools/clis/hunter.js domain count --domain example.com +``` + +### Manage leads + +```bash +# List leads +node tools/clis/hunter.js leads list --limit 20 + +# Create a lead +node tools/clis/hunter.js leads create --email john@example.com --first-name John --last-name Doe --company "Example Inc" + +# Delete a lead +node tools/clis/hunter.js leads delete --id 12345 +``` + +### Manage campaigns + +```bash +# List campaigns +node tools/clis/hunter.js campaigns list + +# Get campaign details +node tools/clis/hunter.js campaigns get --id 12345 + +# Start/pause a campaign +node tools/clis/hunter.js campaigns start --id 12345 +node tools/clis/hunter.js campaigns pause --id 12345 +``` + +### Check account usage + +```bash +node tools/clis/hunter.js account info +``` + +## Rate Limits + +- Free plan: 25 searches/month, 50 verifications/month +- Paid plans scale with tier +- API rate limit: 10 requests/second + +## Use Cases + +- **Link building**: Find email contacts at target domains for outreach +- **Prospecting**: Build lead lists from company domains +- **Verification**: Clean email lists before sending campaigns diff --git a/tools/integrations/instantly.md b/tools/integrations/instantly.md new file mode 100644 index 0000000..4a18a23 --- /dev/null +++ b/tools/integrations/instantly.md @@ -0,0 +1,104 @@ +# Instantly.ai + +Cold email platform with built-in email warmup and campaign management at scale. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for campaigns, leads, accounts, analytics | +| MCP | - | Not available | +| CLI | [✓](../clis/instantly.js) | Zero-dependency Node.js CLI | +| SDK | - | API-only | + +## Authentication + +- **Type**: API Key (query parameter) +- **Parameter**: `api_key={key}` +- **Env var**: `INSTANTLY_API_KEY` +- **Get key**: [Instantly Settings > Integrations > API](https://app.instantly.ai/app/settings/integrations) + +## Common Agent Operations + +### Manage campaigns + +```bash +# List campaigns +node tools/clis/instantly.js campaigns list --limit 20 + +# Get campaign details +node tools/clis/instantly.js campaigns get --id cam_abc123 + +# Check campaign status +node tools/clis/instantly.js campaigns status --id cam_abc123 + +# Launch a campaign +node tools/clis/instantly.js campaigns launch --id cam_abc123 + +# Pause a campaign +node tools/clis/instantly.js campaigns pause --id cam_abc123 +``` + +### Manage leads + +```bash +# List leads in a campaign +node tools/clis/instantly.js leads list --campaign-id cam_abc123 --limit 50 + +# Add a lead +node tools/clis/instantly.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc" + +# Delete a lead +node tools/clis/instantly.js leads delete --campaign-id cam_abc123 --email john@example.com + +# Check lead status +node tools/clis/instantly.js leads status --campaign-id cam_abc123 --email john@example.com +``` + +### Manage email accounts + +```bash +# List connected accounts +node tools/clis/instantly.js accounts list --limit 20 + +# Check account status +node tools/clis/instantly.js accounts status --account-id me@example.com + +# Check warmup status +node tools/clis/instantly.js accounts warmup-status --account-id me@example.com +``` + +### View analytics + +```bash +# Campaign analytics +node tools/clis/instantly.js analytics campaign --campaign-id cam_abc123 --start 2024-01-01 --end 2024-01-31 + +# Step-by-step analytics +node tools/clis/instantly.js analytics steps --campaign-id cam_abc123 + +# Account-level analytics +node tools/clis/instantly.js analytics account --start 2024-01-01 --end 2024-01-31 +``` + +### Manage blocklist + +```bash +# List blocked emails/domains +node tools/clis/instantly.js blocklist list + +# Add to blocklist +node tools/clis/instantly.js blocklist add --entries "competitor.com,spam@example.com" +``` + +## Rate Limits + +- API rate limits vary by plan +- Recommended: stay under 10 requests/second + +## Use Cases + +- **Link building at scale**: Run large-volume outreach campaigns with built-in warmup +- **Campaign management**: Launch, pause, and monitor cold email campaigns +- **Account health**: Monitor email account warmup and deliverability +- **Analytics**: Track open rates, reply rates, and campaign performance diff --git a/tools/integrations/intercom.md b/tools/integrations/intercom.md new file mode 100644 index 0000000..900c07d --- /dev/null +++ b/tools/integrations/intercom.md @@ -0,0 +1,292 @@ +# Intercom + +Customer messaging and support platform API for managing contacts, conversations, messages, companies, articles, and tags. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v2.11+ - contacts, conversations, messages, companies, articles, tags | +| MCP | - | Not available | +| CLI | ✓ | [intercom.js](../clis/intercom.js) | +| SDK | ✓ | Node.js, Ruby, Python, PHP, Go | + +## Authentication + +- **Type**: Bearer Token (Access Token or OAuth 2.0) +- **Header**: `Authorization: Bearer {token}` +- **Version Header**: `Intercom-Version: 2.11` +- **Get key**: Developer Hub at https://app.intercom.com/a/apps/_/developer-hub + +## Common Agent Operations + +### List contacts + +```bash +GET https://api.intercom.io/contacts +``` + +### Get a contact + +```bash +GET https://api.intercom.io/contacts/{id} +``` + +### Create a contact + +```bash +POST https://api.intercom.io/contacts + +{ + "role": "user", + "email": "user@example.com", + "name": "Jane Doe", + "custom_attributes": { + "plan": "pro" + } +} +``` + +### Update a contact + +```bash +PUT https://api.intercom.io/contacts/{id} + +{ + "name": "Jane Smith", + "custom_attributes": { + "plan": "enterprise" + } +} +``` + +### Search contacts + +```bash +POST https://api.intercom.io/contacts/search + +{ + "query": { + "field": "email", + "operator": "=", + "value": "user@example.com" + } +} +``` + +### Delete a contact + +```bash +DELETE https://api.intercom.io/contacts/{id} +``` + +### List conversations + +```bash +GET https://api.intercom.io/conversations +``` + +### Get a conversation + +```bash +GET https://api.intercom.io/conversations/{id} +``` + +### Search conversations + +```bash +POST https://api.intercom.io/conversations/search + +{ + "query": { + "field": "open", + "operator": "=", + "value": true + } +} +``` + +### Reply to a conversation + +```bash +POST https://api.intercom.io/conversations/{id}/reply + +{ + "message_type": "comment", + "type": "admin", + "admin_id": "{admin_id}", + "body": "Thanks for reaching out!" +} +``` + +### Create a message + +```bash +POST https://api.intercom.io/messages + +{ + "message_type": "inapp", + "body": "Welcome to our platform!", + "from": { + "type": "admin", + "id": "{admin_id}" + }, + "to": { + "type": "user", + "id": "{user_id}" + } +} +``` + +### List companies + +```bash +GET https://api.intercom.io/companies +``` + +### Create or update a company + +```bash +POST https://api.intercom.io/companies + +{ + "company_id": "company_123", + "name": "Acme Corp", + "plan": "enterprise", + "custom_attributes": { + "industry": "Technology" + } +} +``` + +### List tags + +```bash +GET https://api.intercom.io/tags +``` + +### Create a tag + +```bash +POST https://api.intercom.io/tags + +{ + "name": "VIP Customer" +} +``` + +### Tag a contact + +```bash +POST https://api.intercom.io/contacts/{contact_id}/tags + +{ + "id": "{tag_id}" +} +``` + +### List articles + +```bash +GET https://api.intercom.io/articles +``` + +### Create an article + +```bash +POST https://api.intercom.io/articles + +{ + "title": "Getting Started Guide", + "body": "<p>Welcome to our platform...</p>", + "author_id": "{admin_id}", + "state": "published" +} +``` + +### List admins + +```bash +GET https://api.intercom.io/admins +``` + +### Submit events + +```bash +POST https://api.intercom.io/events + +{ + "event_name": "purchased-item", + "created_at": 1706140800, + "user_id": "user_123", + "metadata": { + "item_name": "Pro Plan", + "price": 99.00 + } +} +``` + +## Key Metrics + +### Contact Data +- `id` - Unique contact identifier +- `role` - user or lead +- `email` - Contact email +- `name` - Contact name +- `created_at` / `updated_at` - Timestamps +- `last_seen_at` - Last activity +- `custom_attributes` - Custom data fields +- `tags` - Applied tags +- `companies` - Associated companies + +### Conversation Data +- `id` - Conversation identifier +- `state` - open, closed, snoozed +- `open` - Boolean open status +- `read` - Read status +- `priority` - Priority level +- `statistics` - Response times, counts +- `conversation_parts` - Message history + +## Parameters + +### List Contacts +- `per_page` - Results per page (default 50, max 150) +- `starting_after` - Pagination cursor + +### List Conversations +- `per_page` - Results per page (default 20, max 150) +- `starting_after` - Pagination cursor + +### Search (Contacts & Conversations) +- `query.field` - Field to search +- `query.operator` - Comparison operator (=, !=, >, <, ~, IN, NIN) +- `query.value` - Search value +- `pagination.per_page` - Results per page +- `pagination.starting_after` - Cursor for next page +- `sort.field` / `sort.order` - Sort configuration + +## When to Use + +- Managing customer contact records and segments +- Automating customer messaging and onboarding +- Monitoring and responding to support conversations +- Tracking customer events and behavior +- Building custom support workflows +- Syncing customer data between platforms + +## Rate Limits + +- **Default**: 10,000 API calls per minute per app +- **Per workspace**: 25,000 API calls per minute +- Distributed in 10-second windows (resets every 10 seconds) +- Headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` +- HTTP 429 returned when exceeded + +## Relevant Skills + +- customer-onboarding +- customer-retention +- lead-generation +- customer-support +- in-app-messaging diff --git a/tools/integrations/keywords-everywhere.md b/tools/integrations/keywords-everywhere.md new file mode 100644 index 0000000..fa5bb02 --- /dev/null +++ b/tools/integrations/keywords-everywhere.md @@ -0,0 +1,207 @@ +# Keywords Everywhere + +Keyword research API for search volume, CPC, competition, related keywords, and traffic data. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for keyword data, related keywords, traffic | +| MCP | - | Community MCP server available | +| CLI | ✓ | [keywords-everywhere.js](../clis/keywords-everywhere.js) | +| SDK | - | API-only | + +## Authentication + +- **Type**: API Key (Bearer token) +- **Header**: `Authorization: Bearer {api_key}` +- **Get key**: https://keywordseverywhere.com/first-install-addon.html +- **Limit**: 100 keywords per request + +## Common Agent Operations + +### Get keyword data (volume, CPC, competition) + +```bash +POST https://api.keywordseverywhere.com/v1/get_keyword_data + +Authorization: Bearer {api_key} + +{ + "country": "us", + "currency": "USD", + "dataSource": "gkp", + "kw": ["email marketing", "marketing automation", "crm software"] +} +``` + +### Get related keywords + +```bash +POST https://api.keywordseverywhere.com/v1/get_related_keywords + +Authorization: Bearer {api_key} + +{ + "country": "us", + "currency": "USD", + "dataSource": "gkp", + "kw": ["email marketing"] +} +``` + +### Get "People Also Search For" keywords + +```bash +POST https://api.keywordseverywhere.com/v1/get_pasf_keywords + +Authorization: Bearer {api_key} + +{ + "country": "us", + "currency": "USD", + "dataSource": "gkp", + "kw": ["email marketing"] +} +``` + +### Get domain keywords (what a domain ranks for) + +```bash +POST https://api.keywordseverywhere.com/v1/get_domain_keywords + +Authorization: Bearer {api_key} + +{ + "country": "us", + "currency": "USD", + "domain": "example.com" +} +``` + +### Get URL keywords (what a specific URL ranks for) + +```bash +POST https://api.keywordseverywhere.com/v1/get_url_keywords + +Authorization: Bearer {api_key} + +{ + "country": "us", + "currency": "USD", + "url": "https://example.com/page" +} +``` + +### Get domain traffic + +```bash +POST https://api.keywordseverywhere.com/v1/get_domain_traffic + +Authorization: Bearer {api_key} + +{ + "country": "us", + "domain": "example.com" +} +``` + +### Get URL traffic + +```bash +POST https://api.keywordseverywhere.com/v1/get_url_traffic + +Authorization: Bearer {api_key} + +{ + "country": "us", + "url": "https://example.com/page" +} +``` + +### Get domain backlinks + +```bash +POST https://api.keywordseverywhere.com/v1/get_domain_backlinks + +Authorization: Bearer {api_key} + +{ + "domain": "example.com" +} +``` + +### Get page backlinks + +```bash +POST https://api.keywordseverywhere.com/v1/get_page_backlinks + +Authorization: Bearer {api_key} + +{ + "url": "https://example.com/page" +} +``` + +### Check credits + +```bash +GET https://api.keywordseverywhere.com/v1/get_credits + +Authorization: Bearer {api_key} +``` + +### Get supported countries + +```bash +GET https://api.keywordseverywhere.com/v1/get_countries + +Authorization: Bearer {api_key} +``` + +### Get supported currencies + +```bash +GET https://api.keywordseverywhere.com/v1/get_currencies + +Authorization: Bearer {api_key} +``` + +## Key Metrics + +### Keyword Data +- `vol` - Monthly search volume +- `cpc.value` - Cost per click +- `competition` - Competition score +- `trend` - 12-month trend data + +### Traffic Data +- `estimated_traffic` - Estimated monthly traffic +- `keywords_count` - Number of ranking keywords + +## Parameters + +- `country` - Country code (us, uk, de, fr, etc.) +- `currency` - Currency code (USD, GBP, EUR, etc.) +- `dataSource` - Data source, default `gkp` (Google Keyword Planner) +- `kw` - Array of keywords (max 100 per request) + +## When to Use + +- Quick keyword research with volume and CPC +- Finding related keywords and PASF suggestions +- Analyzing domain/URL keyword rankings +- Traffic estimation for domains and pages +- Backlink discovery + +## Rate Limits + +- 100 keywords per request +- Credit-based pricing (1 credit per keyword) + +## Relevant Skills + +- seo-audit +- content-strategy +- programmatic-seo +- competitor-alternatives diff --git a/tools/integrations/klaviyo.md b/tools/integrations/klaviyo.md new file mode 100644 index 0000000..04ae637 --- /dev/null +++ b/tools/integrations/klaviyo.md @@ -0,0 +1,228 @@ +# Klaviyo + +E-commerce email and SMS marketing platform with profiles, flows, campaigns, segments, and event tracking. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API with JSON:API spec, revision-versioned | +| MCP | - | Not available | +| CLI | ✓ | [klaviyo.js](../clis/klaviyo.js) | +| SDK | ✓ | Python, Node.js, Ruby, PHP, Java, C# | + +## Authentication + +- **Type**: Private API Key +- **Header**: `Authorization: Klaviyo-API-Key {private_api_key}` +- **Revision Header**: `revision: 2024-10-15` (required on all requests) +- **Get key**: Account Settings > API Keys at https://www.klaviyo.com/settings/account/api-keys +- **Note**: Private keys are prefixed with `pk_`; public keys (6-char site ID) are for client-side only + +## Common Agent Operations + +### List profiles + +```bash +GET https://a.klaviyo.com/api/profiles/?page[size]=20 + +# Filter by email +GET https://a.klaviyo.com/api/profiles/?filter=equals(email,"user@example.com") +``` + +### Create profile + +```bash +POST https://a.klaviyo.com/api/profiles/ + +{ + "data": { + "type": "profile", + "attributes": { + "email": "user@example.com", + "first_name": "Jane", + "last_name": "Doe", + "phone_number": "+15551234567" + } + } +} +``` + +### Update profile + +```bash +PATCH https://a.klaviyo.com/api/profiles/{profileId}/ + +{ + "data": { + "type": "profile", + "id": "{profileId}", + "attributes": { + "first_name": "Updated Name" + } + } +} +``` + +### List all lists + +```bash +GET https://a.klaviyo.com/api/lists/ +``` + +### Create list + +```bash +POST https://a.klaviyo.com/api/lists/ + +{ + "data": { + "type": "list", + "attributes": { + "name": "Newsletter Subscribers" + } + } +} +``` + +### Add profiles to list + +```bash +POST https://a.klaviyo.com/api/lists/{listId}/relationships/profiles/ + +{ + "data": [ + { "type": "profile", "id": "{profileId1}" }, + { "type": "profile", "id": "{profileId2}" } + ] +} +``` + +### Track event + +```bash +POST https://a.klaviyo.com/api/events/ + +{ + "data": { + "type": "event", + "attributes": { + "metric": { + "data": { + "type": "metric", + "attributes": { "name": "Placed Order" } + } + }, + "profile": { + "data": { + "type": "profile", + "attributes": { "email": "user@example.com" } + } + }, + "properties": { + "value": 99.99, + "items": ["Product A"] + }, + "time": "2025-01-15T10:00:00Z" + } + } +} +``` + +### List campaigns + +```bash +GET https://a.klaviyo.com/api/campaigns/?filter=equals(messages.channel,"email") +``` + +### List flows + +```bash +GET https://a.klaviyo.com/api/flows/ +``` + +### Update flow status + +```bash +PATCH https://a.klaviyo.com/api/flows/{flowId}/ + +{ + "data": { + "type": "flow", + "id": "{flowId}", + "attributes": { + "status": "live" + } + } +} +``` + +### List metrics + +```bash +GET https://a.klaviyo.com/api/metrics/ +``` + +### List segments + +```bash +GET https://a.klaviyo.com/api/segments/ +``` + +## API Pattern + +Klaviyo uses the JSON:API specification. All request/response bodies use `{ "data": { "type": "...", "attributes": {...} } }` format. Relationships are managed via `/relationships/` sub-endpoints. The `revision` header is required on every request and determines API behavior version. + +## Key Metrics + +### Profile Fields +- `email` - Email address +- `phone_number` - Phone for SMS +- `first_name`, `last_name` - Name fields +- `properties` - Custom properties object +- `subscriptions` - Email/SMS subscription status + +### Event Fields +- `metric` - The metric/event name +- `properties` - Custom event properties +- `time` - Event timestamp +- `value` - Monetary value (for revenue tracking) + +### Campaign/Flow Metrics +- `send_count` - Number of sends +- `open_rate` - Open percentage +- `click_rate` - Click percentage +- `revenue` - Attributed revenue + +## Parameters + +### Common Query Parameters +- `page[size]` - Results per page (default 20, max 100) +- `page[cursor]` - Cursor for pagination +- `filter` - Filter expressions (e.g., `equals(email,"user@example.com")`) +- `sort` - Sort field (prefix `-` for descending) +- `include` - Include related resources +- `fields[resource]` - Sparse fieldsets + +## When to Use + +- E-commerce email/SMS marketing automation +- Syncing customer profiles from external systems +- Tracking purchase events and customer behavior +- Managing email flows and drip campaigns +- Segmenting audiences for targeted campaigns +- Reporting on campaign and flow performance + +## Rate Limits + +- Steady-state: 75 requests/second for most endpoints +- Burst: up to 700 requests in 1 minute +- Rate limit headers: `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` +- Lower limits on some write endpoints (profiles, events) + +## Relevant Skills + +- email-sequence +- ecommerce-email +- lifecycle-marketing +- customer-segmentation diff --git a/tools/integrations/lemlist.md b/tools/integrations/lemlist.md new file mode 100644 index 0000000..3ee6607 --- /dev/null +++ b/tools/integrations/lemlist.md @@ -0,0 +1,110 @@ +# Lemlist + +Cold email outreach platform with personalization and campaign management. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for campaigns, leads, activities, webhooks | +| MCP | - | Not available | +| CLI | [✓](../clis/lemlist.js) | Zero-dependency Node.js CLI | +| SDK | - | API-only | + +## Authentication + +- **Type**: Basic Auth (empty username, API key as password) +- **Header**: `Authorization: Basic base64(:api_key)` +- **Env var**: `LEMLIST_API_KEY` +- **Get key**: [Lemlist Settings > Integrations](https://app.lemlist.com/settings/integrations) + +## Common Agent Operations + +### List campaigns + +```bash +node tools/clis/lemlist.js campaigns list --offset 0 --limit 20 +``` + +### Get campaign details and stats + +```bash +# Get campaign +node tools/clis/lemlist.js campaigns get --id cam_abc123 + +# Get campaign stats +node tools/clis/lemlist.js campaigns stats --id cam_abc123 + +# Export campaign data +node tools/clis/lemlist.js campaigns export --id cam_abc123 +``` + +### Manage leads in a campaign + +```bash +# List leads +node tools/clis/lemlist.js leads list --campaign-id cam_abc123 + +# Add a lead +node tools/clis/lemlist.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc" + +# Get lead details +node tools/clis/lemlist.js leads get --campaign-id cam_abc123 --email john@example.com + +# Remove a lead +node tools/clis/lemlist.js leads delete --campaign-id cam_abc123 --email john@example.com +``` + +### Manage unsubscribes + +```bash +# List unsubscribed emails +node tools/clis/lemlist.js unsubscribes list + +# Add to unsubscribe list +node tools/clis/lemlist.js unsubscribes add --email john@example.com + +# Remove from unsubscribe list +node tools/clis/lemlist.js unsubscribes delete --email john@example.com +``` + +### View activities + +```bash +# All activities +node tools/clis/lemlist.js activities list + +# Filter by campaign and type +node tools/clis/lemlist.js activities list --campaign-id cam_abc123 --type emailsOpened +``` + +### Manage webhooks + +```bash +# List hooks +node tools/clis/lemlist.js hooks list + +# Create a webhook +node tools/clis/lemlist.js hooks create --target-url https://example.com/webhook --event emailsOpened + +# Delete a webhook +node tools/clis/lemlist.js hooks delete --id hook_123 +``` + +### Team info + +```bash +node tools/clis/lemlist.js team info +``` + +## Rate Limits + +- API rate limits vary by plan +- Recommended: stay under 10 requests/second + +## Use Cases + +- **Link building outreach**: Add prospects to campaigns for backlink requests +- **Campaign management**: Monitor open/reply rates across outreach campaigns +- **Lead management**: Add, remove, and track leads across campaigns +- **Webhook integration**: Get real-time notifications for email events diff --git a/tools/integrations/livestorm.md b/tools/integrations/livestorm.md new file mode 100644 index 0000000..faabfc3 --- /dev/null +++ b/tools/integrations/livestorm.md @@ -0,0 +1,313 @@ +# Livestorm + +Video engagement platform for webinars, virtual events, and online meetings with built-in analytics and integrations. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Events, Sessions, People, Recordings, Webhooks | +| MCP | - | Not available | +| CLI | ✓ | [livestorm.js](../clis/livestorm.js) | +| SDK | - | REST API with JSON:API format | + +## Authentication + +- **Type**: API Token +- **Header**: `Authorization: {API_TOKEN}` (no prefix) +- **Content-Type**: `application/vnd.api+json` (JSON:API) +- **Scopes**: Identity, Events, Admin, Webhooks +- **Get token**: Account Settings > Integrations > Public API +- **Docs**: https://developers.livestorm.co/ + +## Common Agent Operations + +### Ping (test authentication) + +```bash +GET https://api.livestorm.co/v1/ping + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### List events + +```bash +GET https://api.livestorm.co/v1/events?page[number]=1&page[size]=25 + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Create an event + +```bash +POST https://api.livestorm.co/v1/events + +Headers: + Authorization: {API_TOKEN} + Content-Type: application/vnd.api+json + +{ + "data": { + "type": "events", + "attributes": { + "title": "Product Demo Webinar", + "slug": "product-demo-webinar", + "estimated_duration": 60 + } + } +} +``` + +### Get event details + +```bash +GET https://api.livestorm.co/v1/events/{event_id} + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Update an event + +```bash +PATCH https://api.livestorm.co/v1/events/{event_id} + +Headers: + Authorization: {API_TOKEN} + Content-Type: application/vnd.api+json + +{ + "data": { + "type": "events", + "id": "{event_id}", + "attributes": { + "title": "Updated Webinar Title" + } + } +} +``` + +### List sessions + +```bash +GET https://api.livestorm.co/v1/sessions?page[number]=1&page[size]=25 + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Create a session for an event + +```bash +POST https://api.livestorm.co/v1/events/{event_id}/sessions + +Headers: + Authorization: {API_TOKEN} + Content-Type: application/vnd.api+json + +{ + "data": { + "type": "sessions", + "attributes": { + "estimated_started_at": "2025-06-15T14:00:00.000Z", + "timezone": "America/New_York" + } + } +} +``` + +### Register someone for a session + +```bash +POST https://api.livestorm.co/v1/sessions/{session_id}/people + +Headers: + Authorization: {API_TOKEN} + Content-Type: application/vnd.api+json + +{ + "data": { + "type": "people", + "attributes": { + "fields": { + "email": "attendee@example.com", + "first_name": "Jane", + "last_name": "Doe" + } + } + } +} +``` + +### List session participants + +```bash +GET https://api.livestorm.co/v1/sessions/{session_id}/people?page[number]=1&page[size]=25 + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Remove a registrant from session + +```bash +DELETE https://api.livestorm.co/v1/sessions/{session_id}/people?filter[email]=attendee@example.com + +Headers: + Authorization: {API_TOKEN} +``` + +### List session chat messages + +```bash +GET https://api.livestorm.co/v1/sessions/{session_id}/chat-messages + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### List session questions + +```bash +GET https://api.livestorm.co/v1/sessions/{session_id}/questions + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Get session recordings + +```bash +GET https://api.livestorm.co/v1/sessions/{session_id}/recordings + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### List all people + +```bash +GET https://api.livestorm.co/v1/people?page[number]=1&page[size]=25 + +Headers: + Authorization: {API_TOKEN} + Accept: application/vnd.api+json +``` + +### Create a webhook + +```bash +POST https://api.livestorm.co/v1/webhooks + +Headers: + Authorization: {API_TOKEN} + Content-Type: application/vnd.api+json + +{ + "data": { + "type": "webhooks", + "attributes": { + "target_url": "https://example.com/webhook", + "event_name": "attendance" + } + } +} +``` + +## API Pattern + +Livestorm follows the JSON:API specification: +- All responses use `data`, `attributes`, `relationships` structure +- Pagination: `page[number]` and `page[size]` query parameters +- Filtering: `filter[field]=value` query parameters +- Events contain multiple Sessions; Sessions contain People +- ISO 8601 timestamps throughout + +## Key Metrics + +### Event Metrics +- `title` - Event title +- `slug` - URL-friendly identifier +- `estimated_duration` - Duration in minutes +- `registration_page_enabled` - Registration page status +- `everyone_can_speak` - Whether all attendees can speak + +### Session Metrics +- `status` - Session status (upcoming, live, past) +- `estimated_started_at` - Scheduled start time +- `started_at` - Actual start time +- `ended_at` - Actual end time +- `timezone` - Session timezone +- `attendees_count` - Number of attendees +- `registrants_count` - Number of registrants + +### People Metrics +- `email` - Contact email +- `first_name` / `last_name` - Contact name +- `registrant_detail` - Registration metadata +- `attendance_rate` - Attendance percentage +- `attended_at` - Join timestamp +- `left_at` - Leave timestamp + +## Parameters + +### Pagination +- `page[number]` - Page number (default: 1) +- `page[size]` - Items per page (default: 25) + +### Event Attributes +- `title` - Event title (required for create) +- `slug` - URL slug +- `description` - Event description +- `estimated_duration` - Duration in minutes + +### Session Attributes +- `estimated_started_at` - ISO 8601 start time +- `timezone` - IANA timezone string + +### Registration Fields +- `email` - Registrant email (required) +- `first_name` - First name +- `last_name` - Last name + +### Webhook Events +- `attendance` - Triggered on session attendance +- `registration` - Triggered on new registration +- `unregistration` - Triggered on unregistration + +## When to Use + +- Hosting product demos and marketing webinars +- Automated webinar registration and attendee management +- Tracking webinar engagement and attendance rates +- Retrieving session recordings for content repurposing +- Building custom registration pages with API-driven registration +- Syncing webinar data with CRM and marketing automation +- Monitoring session Q&A and chat for follow-up + +## Rate Limits + +- **10,000 API calls per 30-day period** (organization-wide) +- Rate limits shared across all API tokens in the organization +- Plan accordingly for high-volume operations +- Use webhooks instead of polling to conserve quota + +## Relevant Skills + +- webinar-marketing +- event-marketing +- lead-generation +- content-strategy +- lifecycle-marketing +- customer-engagement diff --git a/tools/integrations/onesignal.md b/tools/integrations/onesignal.md new file mode 100644 index 0000000..3b881e1 --- /dev/null +++ b/tools/integrations/onesignal.md @@ -0,0 +1,229 @@ +# OneSignal + +Push notification, email, SMS, and in-app messaging platform for customer engagement at scale. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Notifications, Users, Segments, Templates, Apps | +| MCP | - | Not available | +| CLI | ✓ | [onesignal.js](../clis/onesignal.js) | +| SDK | ✓ | JavaScript, Node.js, Python, Java, PHP, Ruby, Go, .NET | + +## Authentication + +- **Type**: REST API Key (Basic Auth) +- **Header**: `Authorization: Basic {REST_API_KEY}` +- **App ID**: Required as `app_id` in request bodies +- **Get credentials**: Dashboard > Settings > Keys & IDs +- **Security**: HTTPS required, TLS 1.2+ on port 443 + +## Common Agent Operations + +### Send push notification to segment + +```bash +POST https://api.onesignal.com/api/v1/notifications + +Headers: + Authorization: Basic {REST_API_KEY} + Content-Type: application/json + +{ + "app_id": "YOUR_APP_ID", + "included_segments": ["Subscribed Users"], + "headings": { "en": "New Feature!" }, + "contents": { "en": "Check out our latest update." }, + "url": "https://example.com/feature" +} +``` + +### Send notification to specific users + +```bash +POST https://api.onesignal.com/api/v1/notifications + +Headers: + Authorization: Basic {REST_API_KEY} + Content-Type: application/json + +{ + "app_id": "YOUR_APP_ID", + "include_aliases": { "external_id": ["user-123", "user-456"] }, + "target_channel": "push", + "contents": { "en": "You have a new message." } +} +``` + +### Schedule a notification + +```bash +POST https://api.onesignal.com/api/v1/notifications + +Headers: + Authorization: Basic {REST_API_KEY} + Content-Type: application/json + +{ + "app_id": "YOUR_APP_ID", + "included_segments": ["Subscribed Users"], + "contents": { "en": "Scheduled notification" }, + "send_after": "2025-12-01 12:00:00 GMT-0500" +} +``` + +### List notifications + +```bash +GET https://api.onesignal.com/api/v1/notifications?app_id={APP_ID}&limit=50&offset=0 + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +### View a notification + +```bash +GET https://api.onesignal.com/api/v1/notifications/{notification_id}?app_id={APP_ID} + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +### Cancel a scheduled notification + +```bash +DELETE https://api.onesignal.com/api/v1/notifications/{notification_id}?app_id={APP_ID} + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +### List segments + +```bash +GET https://api.onesignal.com/api/v1/apps/{APP_ID}/segments + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +### Create a segment + +```bash +POST https://api.onesignal.com/api/v1/apps/{APP_ID}/segments + +Headers: + Authorization: Basic {REST_API_KEY} + Content-Type: application/json + +{ + "name": "Active Users", + "filters": [ + { "field": "session_count", "relation": ">", "value": "5" } + ] +} +``` + +### Get user by external ID + +```bash +GET https://api.onesignal.com/api/v1/apps/{APP_ID}/users/by/external_id/{external_id} + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +### Create a user + +```bash +POST https://api.onesignal.com/api/v1/apps/{APP_ID}/users + +Headers: + Authorization: Basic {REST_API_KEY} + Content-Type: application/json + +{ + "identity": { "external_id": "user-789" }, + "subscriptions": [ + { "type": "Email", "token": "user@example.com" } + ], + "tags": { "plan": "pro", "signup_source": "organic" } +} +``` + +### List templates + +```bash +GET https://api.onesignal.com/api/v1/templates?app_id={APP_ID} + +Headers: + Authorization: Basic {REST_API_KEY} +``` + +## Key Metrics + +### Notification Metrics +- `successful` - Number of successful deliveries +- `failed` - Number of failed deliveries +- `converted` - Users who clicked/converted +- `remaining` - Notifications still queued +- `errored` - Count of errors +- `opened` - Notification open count + +### User Metrics +- `session_count` - Total user sessions +- `last_active` - Last activity timestamp +- `tags` - Custom key-value metadata +- `subscriptions` - Active subscription channels + +## Parameters + +### Notification Parameters +- `app_id` - Application ID (required) +- `included_segments` - Target segments array +- `excluded_segments` - Excluded segments array +- `include_aliases` - Target specific users by alias +- `target_channel` - Channel: `push`, `email`, `sms` +- `contents` - Message content by language code +- `headings` - Notification title by language code +- `url` - Launch URL on click +- `data` - Custom key-value data payload +- `send_after` - Scheduled send time (UTC string) +- `ttl` - Time to live in seconds + +### Segment Filter Fields +- `session_count` - Number of sessions +- `first_session` - First session date +- `last_session` - Last session date +- `tag` - Custom tag value +- `language` - User language +- `app_version` - App version +- `country` - User country code + +## When to Use + +- Sending push notifications for product updates +- Triggered notifications based on user behavior +- Multi-channel messaging (push + email + SMS) +- Re-engagement campaigns for inactive users +- Segmenting users for targeted messaging +- A/B testing notification content +- Scheduling promotional campaigns + +## Rate Limits + +- **Free Plan**: 150 notification requests/second per app +- **Paid Plan**: 6,000 notification requests/second per app +- **User/Subscription ops**: 1,000 requests/second per app +- **Burst limit**: No more than 10x total subscribers in 15 minutes +- **429 response**: Includes `RetryAfter` header with seconds to wait + +## Relevant Skills + +- push-notifications +- customer-engagement +- retention-campaign +- re-engagement +- lifecycle-marketing diff --git a/tools/integrations/optimizely.md b/tools/integrations/optimizely.md new file mode 100644 index 0000000..fe5fca9 --- /dev/null +++ b/tools/integrations/optimizely.md @@ -0,0 +1,171 @@ +# Optimizely + +A/B testing and experimentation platform with a REST API for managing projects, experiments, campaigns, and results. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Projects, Experiments, Campaigns, Audiences, Results | +| MCP | - | Not available | +| CLI | ✓ | [optimizely.js](../clis/optimizely.js) | +| SDK | ✓ | JavaScript, Python, Ruby, Java, Go, C#, PHP, React, Swift, Android | + +## Authentication + +- **Type**: Bearer Token (Personal Access Token or OAuth 2.0) +- **Header**: `Authorization: Bearer {personal_token}` +- **Get token**: https://app.optimizely.com/v2/profile/api > Generate New Token + +## Common Agent Operations + +### List Projects + +```bash +GET https://api.optimizely.com/v2/projects +``` + +### Get Project + +```bash +GET https://api.optimizely.com/v2/projects/{project_id} +``` + +### List Experiments + +```bash +GET https://api.optimizely.com/v2/experiments?project_id={project_id} +``` + +### Get Experiment + +```bash +GET https://api.optimizely.com/v2/experiments/{experiment_id} +``` + +### Get Experiment Results + +```bash +GET https://api.optimizely.com/v2/experiments/{experiment_id}/results +``` + +### Create Experiment + +```bash +POST https://api.optimizely.com/v2/experiments + +{ + "project_id": 12345, + "name": "Homepage CTA Test", + "type": "a/b", + "variations": [ + { "name": "Control", "weight": 5000 }, + { "name": "Variation 1", "weight": 5000 } + ], + "metrics": [{ "event_id": 67890 }], + "status": "not_started" +} +``` + +### Update Experiment + +```bash +PATCH https://api.optimizely.com/v2/experiments/{experiment_id} + +{ + "status": "running" +} +``` + +### List Campaigns + +```bash +GET https://api.optimizely.com/v2/campaigns?project_id={project_id} +``` + +### Get Campaign Results + +```bash +GET https://api.optimizely.com/v2/campaigns/{campaign_id}/results +``` + +### List Audiences + +```bash +GET https://api.optimizely.com/v2/audiences?project_id={project_id} +``` + +### List Events + +```bash +GET https://api.optimizely.com/v2/events?project_id={project_id} +``` + +### List Pages + +```bash +GET https://api.optimizely.com/v2/pages?project_id={project_id} +``` + +## Key Metrics + +### Experiment Results +- `variation_id` - Variation identifier +- `variation_name` - Variation display name +- `visitors` - Unique visitors per variation +- `conversions` - Conversion count +- `conversion_rate` - Rate as decimal +- `improvement` - Percentage improvement vs. control +- `statistical_significance` - Confidence level +- `is_baseline` - Whether this is the control + +### Experiment Properties +- `name` - Experiment name +- `status` - not_started, running, paused, archived +- `type` - a/b, multivariate, personalization +- `traffic_allocation` - Percentage of traffic (0-10000 = 0-100%) +- `variations` - Array of variations with weights + +## Parameters + +### List Experiments +- `project_id` (required) - Project to list experiments for +- `page` - Page number +- `per_page` - Results per page (default: 25) +- `status` - Filter by status + +### Get Results +- `start_time` - Results start time (ISO 8601) +- `end_time` - Results end time (ISO 8601) + +### Create Experiment +- `project_id` (required) - Parent project +- `name` (required) - Experiment name +- `type` - Experiment type (default: a/b) +- `variations` (required) - Array of variations with name and weight +- `metrics` - Array of metric/event configurations +- `audience_conditions` - Targeting conditions +- `traffic_allocation` - Traffic percentage (0-10000) + +## When to Use + +- Running A/B tests on web pages and features +- Managing experimentation programs at scale +- Pulling experiment results for analysis +- Automating experiment creation and monitoring +- Feature flag management +- Personalization campaigns + +## Rate Limits + +- 50 requests/second per personal token +- Pagination via `page` and `per_page` parameters +- OpenAPI spec available at https://api.optimizely.com/v2/swagger.json + +## Relevant Skills + +- ab-test-setup +- page-cro +- landing-page +- personalization +- analytics-tracking diff --git a/tools/integrations/paddle.md b/tools/integrations/paddle.md new file mode 100644 index 0000000..aec2d11 --- /dev/null +++ b/tools/integrations/paddle.md @@ -0,0 +1,212 @@ +# Paddle + +SaaS billing and payments platform with built-in tax compliance, acting as merchant of record for global sales. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for products, prices, subscriptions, transactions | +| MCP | - | Not available | +| CLI | ✓ | [paddle.js](../clis/paddle.js) | +| SDK | ✓ | Node.js, Python, PHP, Go | + +## Authentication + +- **Type**: Bearer Token +- **Header**: `Authorization: Bearer {api_key}` +- **Get key**: Paddle dashboard > Developer Tools > Authentication +- **Production URL**: `https://api.paddle.com` +- **Sandbox URL**: `https://sandbox-api.paddle.com` +- **Note**: Version specified via header, not path. Set `PADDLE_SANDBOX=true` env var for sandbox. + +## Common Agent Operations + +### List products + +```bash +GET https://api.paddle.com/products +``` + +### Create a product + +```bash +POST https://api.paddle.com/products + +{ + "name": "Pro Plan", + "tax_category": "standard", + "description": "Professional tier subscription" +} +``` + +### Create a price for a product + +```bash +POST https://api.paddle.com/prices + +{ + "product_id": "pro_01abc...", + "description": "Monthly Pro", + "unit_price": { + "amount": "2999", + "currency_code": "USD" + }, + "billing_cycle": { + "interval": "month", + "frequency": 1 + } +} +``` + +### List customers + +```bash +GET https://api.paddle.com/customers +``` + +### Create a customer + +```bash +POST https://api.paddle.com/customers + +{ + "email": "customer@example.com", + "name": "Jane Smith" +} +``` + +### List subscriptions + +```bash +GET https://api.paddle.com/subscriptions?status=active +``` + +### Get subscription details + +```bash +GET https://api.paddle.com/subscriptions/{subscription_id} +``` + +### Cancel a subscription + +```bash +POST https://api.paddle.com/subscriptions/{subscription_id}/cancel + +{ + "effective_from": "next_billing_period" +} +``` + +### Pause a subscription + +```bash +POST https://api.paddle.com/subscriptions/{subscription_id}/pause +``` + +### List transactions + +```bash +GET https://api.paddle.com/transactions +``` + +### Create a discount + +```bash +POST https://api.paddle.com/discounts + +{ + "amount": "20", + "type": "percentage", + "description": "20% off first month", + "code": "WELCOME20" +} +``` + +### Create a refund adjustment + +```bash +POST https://api.paddle.com/adjustments + +{ + "transaction_id": "txn_01abc...", + "action": "refund", + "reason": "Customer requested refund", + "items": [{"item_id": "txnitm_01abc...", "type": "full"}] +} +``` + +### List events + +```bash +GET https://api.paddle.com/events +``` + +### List event types + +```bash +GET https://api.paddle.com/event-types +``` + +## Key Metrics + +### Transaction Metrics +- `totals.total` - Total amount charged +- `totals.tax` - Tax amount +- `totals.subtotal` - Amount before tax +- `totals.discount` - Discount applied +- `currency_code` - Transaction currency + +### Subscription Metrics +- `status` - active, canceled, paused, past_due, trialing +- `current_billing_period` - Current period start/end +- `next_billed_at` - Next billing date +- `scheduled_change` - Pending changes (cancellation, plan change) + +### Product/Price Metrics +- `unit_price.amount` - Price in lowest denomination +- `billing_cycle` - Interval and frequency +- `trial_period` - Trial duration if set + +## Parameters + +### List Filtering +- `status` - Filter by status (e.g., active, archived) +- `after` - Cursor for pagination +- `per_page` - Results per page (default: 50) +- `order_by` - Sort field and direction + +### Subscription Cancel Options +- `effective_from` - `immediately` or `next_billing_period` + +### Price Billing Cycle +- `interval` - `day`, `week`, `month`, `year` +- `frequency` - Number of intervals between billings + +### Tax Categories +- `standard` - Standard tax rate +- `digital-goods` - Digital goods tax rate +- `saas` - SaaS-specific tax rate + +## When to Use + +- Managing SaaS subscription billing with tax compliance +- Creating products and pricing tiers +- Processing refunds and adjustments +- Handling subscription lifecycle (create, pause, cancel, resume) +- Global tax handling as merchant of record +- Discount and coupon management for promotions + +## Rate Limits + +- 100 requests per minute +- Applies across all endpoints +- HTTP 429 returned when exceeded + +## Relevant Skills + +- pricing-page +- saas-metrics +- churn-reduction +- launch-sequence +- monetization-strategy diff --git a/tools/integrations/partnerstack.md b/tools/integrations/partnerstack.md new file mode 100644 index 0000000..bbc4211 --- /dev/null +++ b/tools/integrations/partnerstack.md @@ -0,0 +1,222 @@ +# PartnerStack + +Partner and affiliate program management platform for SaaS companies with deal tracking, rewards, and multi-tier partnerships. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Vendor API v2 for partnerships, deals, customers, transactions | +| MCP | - | Not available | +| CLI | ✓ | [partnerstack.js](../clis/partnerstack.js) | +| SDK | - | No official SDK; REST API with Basic Auth | + +## Authentication + +- **Type**: Basic Auth (Vendor API) +- **Header**: `Authorization: Basic {base64(public_key:secret_key)}` +- **Get credentials**: Vendor dashboard > Settings > Integrations > PartnerStack API Keys +- **Note**: Separate Test and Production API keys. Test transactions can only be added to customers created with Test keys. + +## Common Agent Operations + +### List partnerships + +```bash +GET https://api.partnerstack.com/api/v2/partnerships?limit=25 + +Authorization: Basic {base64(public_key:secret_key)} +``` + +### Create a partnership + +```bash +POST https://api.partnerstack.com/api/v2/partnerships + +{ + "email": "partner@example.com", + "group_key": "affiliates", + "first_name": "Jane", + "last_name": "Smith" +} +``` + +### List customers + +```bash +GET https://api.partnerstack.com/api/v2/customers?limit=25 +``` + +### Create a customer (attribute to partner) + +```bash +POST https://api.partnerstack.com/api/v2/customers + +{ + "email": "customer@example.com", + "partner_key": "prtnr_abc123", + "name": "John Doe" +} +``` + +### Record a transaction + +```bash +POST https://api.partnerstack.com/api/v2/transactions + +{ + "customer_key": "cust_abc123", + "amount": 9900, + "currency": "USD", + "product_key": "pro_plan" +} +``` + +### List deals + +```bash +GET https://api.partnerstack.com/api/v2/deals?limit=25 +``` + +### Create a deal + +```bash +POST https://api.partnerstack.com/api/v2/deals + +{ + "partner_key": "prtnr_abc123", + "name": "Enterprise Opportunity", + "amount": 50000, + "stage": "qualified" +} +``` + +### Record an action (event-based rewards) + +```bash +POST https://api.partnerstack.com/api/v2/actions + +{ + "customer_key": "cust_abc123", + "key": "signup_completed", + "value": 1 +} +``` + +### Create a reward + +```bash +POST https://api.partnerstack.com/api/v2/rewards + +{ + "partner_key": "prtnr_abc123", + "amount": 5000, + "description": "Bonus for Q1 performance" +} +``` + +### List leads + +```bash +GET https://api.partnerstack.com/api/v2/leads?limit=25 +``` + +### Create a lead + +```bash +POST https://api.partnerstack.com/api/v2/leads + +{ + "partner_key": "prtnr_abc123", + "email": "lead@company.com", + "name": "Potential Customer", + "company": "Acme Corp" +} +``` + +### List partner groups + +```bash +GET https://api.partnerstack.com/api/v2/groups +``` + +### Manage webhooks + +```bash +POST https://api.partnerstack.com/api/v2/webhooks + +{ + "target": "https://example.com/webhooks/partnerstack", + "events": ["deal.created", "transaction.created", "customer.created"] +} +``` + +## API Pattern + +PartnerStack uses cursor-based pagination. List responses include `has_more` and item keys for `starting_after` / `ending_before` parameters. + +All responses follow the format: +```json +{ + "data": { ... }, + "message": "...", + "status": "2xx" +} +``` + +## Key Metrics + +### Partnership Metrics +- `partner_key` - Unique partner identifier +- `group` - Partner tier/group +- `status` - active, pending, archived +- `created_at` - Partnership start date + +### Transaction Metrics +- `amount` - Transaction value in cents +- `currency` - Currency code +- `product_key` - Associated product +- `customer_key` - Associated customer + +### Deal Metrics +- `amount` - Deal value +- `stage` - Deal pipeline stage +- `status` - open, won, lost + +### Reward Metrics +- `amount` - Reward amount in cents +- `status` - pending, approved, paid + +## Parameters + +### Pagination Parameters +- `limit` - Items per page (1-250, default: 10) +- `starting_after` - Cursor for next page (item key) +- `ending_before` - Cursor for previous page (item key) +- `order_by` - Sort field, prefix with `-` for descending + +### Common Filters +- `include_archived` - Include archived records +- `has_sub_id` - Filter by sub ID presence + +## When to Use + +- Managing SaaS affiliate and referral programs +- Tracking partner-driven revenue and attributions +- Automating partner onboarding and rewards +- Deal registration and pipeline tracking +- Multi-tier partnership programs (affiliates, resellers, agencies) +- Event-based reward triggers (signups, upgrades, etc.) + +## Rate Limits + +- Not explicitly documented +- Use reasonable request rates; implement exponential backoff on 429 responses + +## Relevant Skills + +- referral-program +- affiliate-marketing +- partner-enablement +- saas-metrics +- launch-sequence diff --git a/tools/integrations/plausible.md b/tools/integrations/plausible.md new file mode 100644 index 0000000..c0cf52c --- /dev/null +++ b/tools/integrations/plausible.md @@ -0,0 +1,177 @@ +# Plausible Analytics + +Privacy-focused, open-source web analytics with a simple API for stats queries without cookies or personal data collection. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Stats v2 Query, Sites Provisioning, Goals, Shared Links | +| MCP | - | Not available | +| CLI | ✓ | [plausible.js](../clis/plausible.js) | +| SDK | - | REST API only | + +## Authentication + +- **Type**: Bearer Token +- **Header**: `Authorization: Bearer {api_key}` +- **Get key**: https://plausible.io/settings > API Keys +- **Note**: Sites API requires Enterprise plan + +## Common Agent Operations + +### Stats Query (v2) + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "pageviews", "bounce_rate", "visit_duration"], + "date_range": "30d" +} +``` + +### Top Pages + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "pageviews"], + "date_range": "30d", + "dimensions": ["event:page"] +} +``` + +### Traffic Sources + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "bounce_rate"], + "date_range": "30d", + "dimensions": ["visit:source"] +} +``` + +### Time Series + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "pageviews"], + "date_range": "30d", + "dimensions": ["time:day"] +} +``` + +### Breakdown by Country + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "percentage"], + "date_range": "30d", + "dimensions": ["visit:country"] +} +``` + +### Filtered Query (specific page) + +```bash +POST https://plausible.io/api/v2/query + +{ + "site_id": "example.com", + "metrics": ["visitors", "pageviews", "bounce_rate"], + "date_range": "30d", + "filters": [["is", "event:page", ["/pricing"]]] +} +``` + +### Realtime Visitors (v1) + +```bash +GET https://plausible.io/api/v1/stats/realtime/visitors?site_id=example.com +``` + +### List Sites + +```bash +GET https://plausible.io/api/v1/sites +``` + +## Key Metrics + +### Available Metrics +- `visitors` - Unique visitors +- `visits` - Total visits (sessions) +- `pageviews` - Total page views +- `views_per_visit` - Pages per session +- `bounce_rate` - Bounce rate percentage +- `visit_duration` - Average session duration (seconds) +- `events` - Total events +- `conversion_rate` - Goal conversion rate +- `time_on_page` - Average time on page +- `scroll_depth` - Average scroll depth +- `percentage` - Share of total + +### Available Dimensions +- `event:page` - Page path +- `event:goal` - Goal name +- `visit:source` - Traffic source +- `visit:referrer` - Referrer URL +- `visit:channel` - Traffic channel +- `visit:utm_source`, `visit:utm_medium`, `visit:utm_campaign` - UTM params +- `visit:device` - Device type +- `visit:browser` - Browser name +- `visit:os` - Operating system +- `visit:country`, `visit:region`, `visit:city` - Location +- `visit:entry_page`, `visit:exit_page` - Entry/exit pages +- `time`, `time:day`, `time:week`, `time:month` - Time periods + +## Parameters + +### Stats Query (v2) +- `site_id` (required) - Domain registered in Plausible +- `metrics` (required) - Array of metrics to return +- `date_range` (required) - Time period: "day", "7d", "30d", "month", "6mo", "12mo", "year", or custom ["2024-01-01", "2024-01-31"] +- `dimensions` - Array of dimensions to group by +- `filters` - Array of filter conditions: `[operator, dimension, values]` +- `order_by` - Array of sort specs: `[[metric, "desc"]]` +- `pagination` - `{ "limit": 100, "offset": 0 }` + +### Filter Operators +- `is` / `is_not` - Exact match +- `contains` / `contains_not` - Substring match +- `matches` / `matches_not` - Wildcard match + +## When to Use + +- Privacy-first web analytics without cookies +- Simple, lightweight traffic analysis +- UTM campaign performance tracking +- Goal and conversion tracking +- Geographic and device breakdown +- GDPR/CCPA-compliant analytics alternative to GA4 + +## Rate Limits + +- 600 requests/hour per API key +- All requests must be over HTTPS + +## Relevant Skills + +- analytics-tracking +- content-strategy +- programmatic-seo +- page-cro +- utm-tracking diff --git a/tools/integrations/postmark.md b/tools/integrations/postmark.md new file mode 100644 index 0000000..e686890 --- /dev/null +++ b/tools/integrations/postmark.md @@ -0,0 +1,234 @@ +# Postmark + +Transactional email delivery service with fast delivery, templates, bounce management, and detailed analytics. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for email sending, templates, bounces, stats | +| MCP | - | Not available | +| CLI | ✓ | [postmark.js](../clis/postmark.js) | +| SDK | ✓ | Node.js, Ruby, Python, PHP, Java, .NET, Go | + +## Authentication + +- **Type**: Server Token (or Account Token for account-level ops) +- **Header**: `X-Postmark-Server-Token: {server_token}` (server-level) +- **Header**: `X-Postmark-Account-Token: {account_token}` (account-level) +- **Get key**: API Tokens tab at https://account.postmarkapp.com/servers +- **Note**: Server tokens are per-server; account tokens apply across all servers + +## Common Agent Operations + +### Send single email + +```bash +POST https://api.postmarkapp.com/email + +{ + "From": "sender@example.com", + "To": "recipient@example.com", + "Subject": "Welcome!", + "HtmlBody": "<html><body><p>Hello!</p></body></html>", + "TextBody": "Hello!", + "MessageStream": "outbound", + "TrackOpens": true, + "TrackLinks": "HtmlAndText" +} +``` + +### Send with template + +```bash +POST https://api.postmarkapp.com/email/withTemplate + +{ + "From": "sender@example.com", + "To": "recipient@example.com", + "TemplateId": 12345, + "TemplateModel": { + "name": "Jane", + "action_url": "https://example.com/verify" + }, + "MessageStream": "outbound" +} +``` + +### Send batch emails + +```bash +POST https://api.postmarkapp.com/email/batch + +[ + { + "From": "sender@example.com", + "To": "user1@example.com", + "Subject": "Notification", + "TextBody": "Hello user 1" + }, + { + "From": "sender@example.com", + "To": "user2@example.com", + "Subject": "Notification", + "TextBody": "Hello user 2" + } +] +``` + +### List templates + +```bash +GET https://api.postmarkapp.com/templates?Count=100&Offset=0 +``` + +### Get template + +```bash +GET https://api.postmarkapp.com/templates/{templateIdOrAlias} +``` + +### Create template + +```bash +POST https://api.postmarkapp.com/templates + +{ + "Name": "Welcome Email", + "Alias": "welcome", + "Subject": "Welcome {{name}}!", + "HtmlBody": "<html><body><p>Hello {{name}}</p></body></html>", + "TextBody": "Hello {{name}}" +} +``` + +### Get delivery stats + +```bash +GET https://api.postmarkapp.com/deliverystats +``` + +### List bounces + +```bash +GET https://api.postmarkapp.com/bounces?count=50&offset=0&type=HardBounce +``` + +### Activate bounce (reactivate recipient) + +```bash +PUT https://api.postmarkapp.com/bounces/{bounceId}/activate +``` + +### Search outbound messages + +```bash +GET https://api.postmarkapp.com/messages/outbound?count=50&offset=0&recipient=user@example.com +``` + +### Get outbound stats overview + +```bash +GET https://api.postmarkapp.com/stats/outbound?fromdate=2025-01-01&todate=2025-01-31 +``` + +### Get open stats + +```bash +GET https://api.postmarkapp.com/stats/outbound/opens?fromdate=2025-01-01&todate=2025-01-31 +``` + +### Get click stats + +```bash +GET https://api.postmarkapp.com/stats/outbound/clicks?fromdate=2025-01-01&todate=2025-01-31 +``` + +### Get server info + +```bash +GET https://api.postmarkapp.com/server +``` + +### List suppressions + +```bash +GET https://api.postmarkapp.com/message-streams/outbound/suppressions/dump +``` + +### Create suppression + +```bash +POST https://api.postmarkapp.com/message-streams/outbound/suppressions + +{ + "Suppressions": [ + { "EmailAddress": "user@example.com" } + ] +} +``` + +## API Pattern + +Postmark uses simple REST endpoints with PascalCase field names in request/response bodies. Authentication is via custom headers rather than Authorization. Pagination uses `Count` and `Offset` parameters. Email sending is synchronous with immediate delivery confirmation. + +## Key Metrics + +### Delivery Metrics +- `Sent` - Total emails sent +- `Bounced` - Bounce count by type (hard, soft, transient) +- `SpamComplaints` - Spam complaint count +- `Opens` - Open count and unique opens +- `Clicks` - Click count and unique clicks + +### Bounce Types +- `HardBounce` - Permanent delivery failure +- `SoftBounce` - Temporary delivery failure +- `Transient` - Temporary issue (retry) +- `SpamNotification` - Marked as spam + +### Message Fields +- `MessageID` - Unique message identifier +- `SubmittedAt` - Submission timestamp +- `Status` - Delivery status +- `Recipients` - Recipient list + +## Parameters + +### Email Parameters +- `From` - Sender address (must be verified) +- `To` - Recipient (comma-separated for multiple) +- `Subject` - Email subject +- `HtmlBody` / `TextBody` - Email content +- `MessageStream` - outbound (transactional) or broadcast +- `TrackOpens` - Enable open tracking (boolean) +- `TrackLinks` - None, HtmlAndText, HtmlOnly, TextOnly +- `Tag` - Custom tag for categorization + +### Stats Parameters +- `fromdate` - Start date (YYYY-MM-DD) +- `todate` - End date (YYYY-MM-DD) +- `tag` - Filter by tag + +## When to Use + +- Transactional emails (password resets, order confirmations, notifications) +- Template-based email sending with dynamic variables +- Monitoring email deliverability and bounce rates +- Tracking email engagement (opens, clicks) +- Managing email suppressions and bounces +- High-reliability email delivery with fast performance + +## Rate Limits + +- 500 messages per batch request +- 10 MB max per single message (including attachments) +- 50 MB max per batch request +- API rate limits vary by plan + +## Relevant Skills + +- email-sequence +- transactional-email +- email-deliverability +- onboarding-email diff --git a/tools/integrations/savvycal.md b/tools/integrations/savvycal.md new file mode 100644 index 0000000..b355c8e --- /dev/null +++ b/tools/integrations/savvycal.md @@ -0,0 +1,181 @@ +# SavvyCal + +Scheduling platform API for managing scheduling links, events, availability slots, and webhooks. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API v1 - scheduling links, events, webhooks | +| MCP | - | Not available | +| CLI | ✓ | [savvycal.js](../clis/savvycal.js) | +| SDK | - | No official SDK | + +## Authentication + +- **Type**: Bearer Token (Personal Access Token or OAuth 2.0) +- **Header**: `Authorization: Bearer {token}` +- **Get key**: Developer Settings in SavvyCal dashboard (create a Personal Access Token) + +## Common Agent Operations + +### Get current user + +```bash +GET https://api.savvycal.com/v1/me +``` + +### List scheduling links + +```bash +GET https://api.savvycal.com/v1/scheduling-links +``` + +### Get a scheduling link + +```bash +GET https://api.savvycal.com/v1/scheduling-links/{id} +``` + +### Create a scheduling link + +```bash +POST https://api.savvycal.com/v1/scheduling-links + +{ + "name": "30 Minute Meeting", + "slug": "30min", + "duration_minutes": 30 +} +``` + +### Update a scheduling link + +```bash +PATCH https://api.savvycal.com/v1/scheduling-links/{id} + +{ + "name": "Updated Meeting Name" +} +``` + +### Delete a scheduling link + +```bash +DELETE https://api.savvycal.com/v1/scheduling-links/{id} +``` + +### Duplicate a scheduling link + +```bash +POST https://api.savvycal.com/v1/scheduling-links/{id}/duplicate +``` + +### Toggle link state (active/disabled) + +```bash +POST https://api.savvycal.com/v1/scheduling-links/{id}/toggle +``` + +### Get available time slots + +```bash +GET https://api.savvycal.com/v1/scheduling-links/{id}/slots +``` + +### List events + +```bash +GET https://api.savvycal.com/v1/events +``` + +### Get an event + +```bash +GET https://api.savvycal.com/v1/events/{id} +``` + +### Create an event + +```bash +POST https://api.savvycal.com/v1/events + +{ + "scheduling_link_id": "{link_id}", + "start_at": "2024-01-20T10:00:00Z", + "name": "John Doe", + "email": "john@example.com" +} +``` + +### Cancel an event + +```bash +POST https://api.savvycal.com/v1/events/{id}/cancel +``` + +### List webhooks + +```bash +GET https://api.savvycal.com/v1/webhooks +``` + +### Create a webhook + +```bash +POST https://api.savvycal.com/v1/webhooks + +{ + "url": "https://example.com/webhook", + "events": ["event.created", "event.canceled"] +} +``` + +## Key Metrics + +### Scheduling Link Data +- `id` - Unique link identifier +- `name` - Display name +- `slug` - URL slug +- `duration_minutes` - Meeting duration +- `state` - Active or disabled +- `url` - Full scheduling URL + +### Event Data +- `id` - Unique event identifier +- `name` - Invitee name +- `email` - Invitee email +- `start_at` / `end_at` - Event timing +- `status` - Event status +- `scheduling_link` - Associated scheduling link + +## Parameters + +### List Events +- `before` / `after` - Pagination cursors +- `limit` - Results per page (default 20, max 100) + +### List Scheduling Links +- `before` / `after` - Pagination cursors +- `limit` - Results per page + +## When to Use + +- Managing scheduling links programmatically +- Retrieving booked events for CRM or analytics sync +- Checking available time slots for custom booking UIs +- Automating scheduling link creation for campaigns +- Monitoring booking activity via webhooks + +## Rate Limits + +- Not officially documented +- Implement retry logic with exponential backoff +- Monitor for HTTP 429 responses + +## Relevant Skills + +- lead-generation +- sales-automation +- appointment-scheduling +- customer-onboarding diff --git a/tools/integrations/snov.md b/tools/integrations/snov.md new file mode 100644 index 0000000..f1cf17e --- /dev/null +++ b/tools/integrations/snov.md @@ -0,0 +1,94 @@ +# Snov.io + +Email finding, verification, and drip campaign platform for outreach. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | REST API for email finding, verification, prospects, drip campaigns | +| MCP | - | Not available | +| CLI | [✓](../clis/snov.js) | Zero-dependency Node.js CLI | +| SDK | - | API-only | + +## Authentication + +- **Type**: OAuth2 client credentials +- **Flow**: POST to `/oauth/access_token` with client_id + client_secret +- **Env vars**: `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET` +- **Get keys**: [Snov.io > Integration > API](https://app.snov.io/integration/api) + +The CLI handles token acquisition automatically. + +## Common Agent Operations + +### Search emails by domain + +```bash +node tools/clis/snov.js domain search --domain example.com --type all --limit 10 +``` + +### Find a specific person's email + +```bash +node tools/clis/snov.js email find --domain example.com --first-name John --last-name Doe +``` + +### Verify an email + +```bash +node tools/clis/snov.js email verify --email john@example.com +``` + +### Find prospect by email + +```bash +node tools/clis/snov.js prospect find --email john@example.com +``` + +### Add prospect to a list + +```bash +node tools/clis/snov.js prospect add --email john@example.com --first-name John --last-name Doe --list-id 12345 +``` + +### Manage prospect lists + +```bash +# List all lists +node tools/clis/snov.js lists list + +# Get prospects in a list +node tools/clis/snov.js lists prospects --id 12345 --page 1 --per-page 50 +``` + +### Check domain technology stack + +```bash +node tools/clis/snov.js technology check --domain example.com +``` + +### Manage drip campaigns + +```bash +# List campaigns +node tools/clis/snov.js drips list + +# Get campaign details +node tools/clis/snov.js drips get --id 12345 + +# Add prospect to drip campaign +node tools/clis/snov.js drips add-prospect --id 12345 --email john@example.com +``` + +## Rate Limits + +- Rate limits vary by plan +- OAuth tokens expire after a set period; CLI handles refresh automatically + +## Use Cases + +- **Link building**: Find contacts and run automated drip outreach +- **Prospecting**: Build and manage prospect lists +- **Technology research**: Check what tech stack a target domain uses +- **Email verification**: Clean lists before sending diff --git a/tools/integrations/trustpilot.md b/tools/integrations/trustpilot.md new file mode 100644 index 0000000..cd28dc2 --- /dev/null +++ b/tools/integrations/trustpilot.md @@ -0,0 +1,191 @@ +# Trustpilot + +Business review management platform for collecting, managing, and showcasing customer reviews. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Business Units, Reviews, Invitations, Tags | +| MCP | - | Not available | +| CLI | ✓ | [trustpilot.js](../clis/trustpilot.js) | +| SDK | ✓ | Node.js (official), community wrappers | + +## Authentication + +- **Type**: API Key (public endpoints) + OAuth 2.0 (private endpoints) +- **Public Header**: `apikey: {YOUR_API_KEY}` +- **Private Header**: `Authorization: Bearer {access_token}` +- **OAuth Grant**: Client Credentials (`Basic base64(API_KEY:API_SECRET)`) +- **Token Lifetime**: Access tokens expire after 100 hours, refresh tokens after 30 days +- **Get credentials**: https://businessapp.b2b.trustpilot.com/ > Integrations > API + +## Common Agent Operations + +### Search for a business unit + +```bash +GET https://api.trustpilot.com/v1/business-units/search?query=example.com&limit=10 + +Headers: + apikey: {API_KEY} +``` + +### Get business unit details + +```bash +GET https://api.trustpilot.com/v1/business-units/{businessUnitId} + +Headers: + apikey: {API_KEY} +``` + +### Get business profile info + +```bash +GET https://api.trustpilot.com/v1/business-units/{businessUnitId}/profileinfo + +Headers: + apikey: {API_KEY} +``` + +### List public reviews + +```bash +GET https://api.trustpilot.com/v1/business-units/{businessUnitId}/reviews?perPage=20&orderBy=createdat.desc + +Headers: + apikey: {API_KEY} +``` + +### List private reviews (with customer data) + +```bash +GET https://api.trustpilot.com/v1/private/business-units/{businessUnitId}/reviews?perPage=20 + +Headers: + Authorization: Bearer {access_token} +``` + +### Reply to a review + +```bash +POST https://api.trustpilot.com/v1/private/reviews/{reviewId}/reply + +Headers: + Authorization: Bearer {access_token} + +{ + "message": "Thank you for your feedback!" +} +``` + +### Send email invitation + +```bash +POST https://api.trustpilot.com/v1/private/business-units/{businessUnitId}/email-invitations + +Headers: + Authorization: Bearer {access_token} + +{ + "consumerEmail": "customer@example.com", + "consumerName": "Jane Doe", + "referenceNumber": "order-123", + "redirectUri": "https://example.com/thanks" +} +``` + +### Generate review invitation link + +```bash +POST https://api.trustpilot.com/v1/private/business-units/{businessUnitId}/invitation-links + +Headers: + Authorization: Bearer {access_token} + +{ + "email": "customer@example.com", + "name": "Jane Doe", + "referenceId": "order-123", + "redirectUri": "https://example.com/thanks" +} +``` + +### List invitation templates + +```bash +GET https://api.trustpilot.com/v1/private/business-units/{businessUnitId}/templates + +Headers: + Authorization: Bearer {access_token} +``` + +### Add tags to a review + +```bash +PUT https://api.trustpilot.com/v1/private/reviews/{reviewId}/tags + +Headers: + Authorization: Bearer {access_token} + +{ + "tags": [{ "group": "sentiment", "value": "positive" }] +} +``` + +## Key Metrics + +### Business Unit Metrics +- `numberOfReviews` - Total review count +- `trustScore` - Overall trust score (1-5) +- `stars` - Star rating displayed +- `status` - Claim status (claimed, unclaimed) + +### Review Metrics +- `stars` - Individual review star rating (1-5) +- `language` - Review language code +- `createdAt` - Review creation timestamp +- `isVerified` - Whether the review is verified +- `status` - Review status (active, reported, flagged) + +## Parameters + +### Review Filters +- `stars` - Filter by star rating (1-5) +- `language` - Filter by language code (e.g., `en`) +- `orderBy` - Sort order (`createdat.desc`, `createdat.asc`, `stars.desc`, `stars.asc`) +- `perPage` - Results per page (max 100) + +### Invitation Parameters +- `consumerEmail` - Recipient email (required) +- `consumerName` - Recipient name (required) +- `referenceNumber` - Order or transaction reference +- `templateId` - Email template ID +- `redirectUri` - URL to redirect after review submission +- `senderEmail` - Custom sender email +- `replyTo` - Custom reply-to address + +## When to Use + +- Collecting and managing customer reviews at scale +- Automating post-purchase review invitation flows +- Monitoring brand reputation and review sentiment +- Responding to customer feedback programmatically +- Showcasing TrustScore and reviews on marketing pages +- Tagging and categorizing reviews for analysis + +## Rate Limits + +- Recommended: no more than 833 calls per 5 minutes (10K/hour) +- Throttled at more than 1 request per second +- Rate limit headers returned in responses +- Use webhooks instead of polling where possible + +## Relevant Skills + +- reputation-management +- customer-feedback +- review-generation +- social-proof +- post-purchase-flow diff --git a/tools/integrations/typeform.md b/tools/integrations/typeform.md new file mode 100644 index 0000000..aef2eab --- /dev/null +++ b/tools/integrations/typeform.md @@ -0,0 +1,190 @@ +# Typeform + +Forms and surveys platform API for creating typeforms, retrieving responses, managing webhooks, themes, images, and workspaces. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Create, Responses, Webhooks APIs | +| MCP | - | Not available | +| CLI | ✓ | [typeform.js](../clis/typeform.js) | +| SDK | ✓ | JavaScript (@typeform/js-api-client), Embed SDK | + +## Authentication + +- **Type**: Bearer Token (Personal Access Token or OAuth 2.0) +- **Header**: `Authorization: Bearer {token}` +- **Get key**: https://admin.typeform.com/account#/section/tokens + +## Common Agent Operations + +### List forms + +```bash +GET https://api.typeform.com/forms +``` + +### Get a form + +```bash +GET https://api.typeform.com/forms/{form_id} +``` + +### Create a form + +```bash +POST https://api.typeform.com/forms + +{ + "title": "Customer Feedback Survey", + "fields": [ + { + "type": "short_text", + "title": "What is your name?" + }, + { + "type": "rating", + "title": "How would you rate our service?", + "properties": { + "steps": 5 + } + } + ] +} +``` + +### Update a form + +```bash +PUT https://api.typeform.com/forms/{form_id} + +{ + "title": "Updated Survey Title" +} +``` + +### Delete a form + +```bash +DELETE https://api.typeform.com/forms/{form_id} +``` + +### Retrieve responses + +```bash +GET https://api.typeform.com/forms/{form_id}/responses?page_size=25&since=2024-01-01T00:00:00Z +``` + +### Delete responses + +```bash +DELETE https://api.typeform.com/forms/{form_id}/responses?included_response_ids={id1},{id2} +``` + +### List webhooks + +```bash +GET https://api.typeform.com/forms/{form_id}/webhooks +``` + +### Create or update webhook + +```bash +PUT https://api.typeform.com/forms/{form_id}/webhooks/{tag} + +{ + "url": "https://example.com/webhook", + "enabled": true +} +``` + +### Delete webhook + +```bash +DELETE https://api.typeform.com/forms/{form_id}/webhooks/{tag} +``` + +### List themes + +```bash +GET https://api.typeform.com/themes +``` + +### List images + +```bash +GET https://api.typeform.com/images +``` + +### List workspaces + +```bash +GET https://api.typeform.com/workspaces +``` + +### Get a workspace + +```bash +GET https://api.typeform.com/workspaces/{workspace_id} +``` + +## Key Metrics + +### Response Data +- `response_id` - Unique response identifier +- `landed_at` / `submitted_at` - Timestamps +- `answers` - Array of field answers +- `variables` - Calculated variables +- `hidden` - Hidden field values +- `calculated` - Score calculations + +### Form Data +- `id` - Form ID (from URL) +- `title` - Form title +- `fields` - Array of form fields +- `logic` - Logic jumps +- `settings` - Form settings (notifications, meta, etc.) +- `_links` - Display and responses URLs + +## Parameters + +### Retrieve Responses +- `page_size` - Results per page (default 25, max 1000) +- `since` / `until` - Date range filter (ISO 8601 or Unix timestamp) +- `after` / `before` - Pagination tokens +- `response_type` - Filter: started, partial, completed (default: completed) +- `query` - Text search within responses +- `fields` - Show only specific fields in answers +- `sort` - Sort order: `{fieldID},{asc|desc}` +- `included_response_ids` / `excluded_response_ids` - Filter specific responses +- `answered_fields` - Only responses containing specified fields + +### List Forms +- `page` - Page number +- `page_size` - Results per page (default 10, max 200) +- `workspace_id` - Filter by workspace +- `search` - Search by form title + +## When to Use + +- Collecting lead information and survey data +- Building custom form experiences programmatically +- Automating survey creation for campaigns +- Analyzing form response data at scale +- Setting up real-time response webhooks +- Managing form themes and branding + +## Rate Limits + +- **Create & Responses APIs**: 2 requests per second per account +- **Webhooks & Embed**: No rate limits (push-based) +- Monitor for HTTP 429 responses + +## Relevant Skills + +- lead-generation +- customer-research +- page-cro +- signup-flow-cro +- customer-feedback diff --git a/tools/integrations/wistia.md b/tools/integrations/wistia.md new file mode 100644 index 0000000..91340d5 --- /dev/null +++ b/tools/integrations/wistia.md @@ -0,0 +1,164 @@ +# Wistia + +Video hosting, management, and analytics platform built for marketers with detailed engagement tracking. + +## Capabilities + +| Integration | Available | Notes | +|-------------|-----------|-------| +| API | ✓ | Data API (v1/modern), Stats API, Upload API | +| MCP | - | Not available | +| CLI | ✓ | [wistia.js](../clis/wistia.js) | +| SDK | ✓ | Ruby (official), community wrappers for other languages | + +## Authentication + +- **Type**: Bearer Token +- **Header**: `Authorization: Bearer {api_token}` +- **Get key**: Account Settings > API tab at https://account.wistia.com/account/api +- **Note**: Only Account Owners can create/manage tokens. Tokens can only be copied when first created. + +## Common Agent Operations + +### List all projects + +```bash +GET https://api.wistia.com/v1/projects.json?page=1&per_page=25 +``` + +### Create a project + +```bash +POST https://api.wistia.com/v1/projects.json + +{ + "name": "Marketing Videos Q1" +} +``` + +### List all media + +```bash +GET https://api.wistia.com/v1/medias.json?page=1&per_page=25 +``` + +### Get media details + +```bash +GET https://api.wistia.com/v1/medias/{media_hashed_id}.json +``` + +### Get media stats + +```bash +GET https://api.wistia.com/v1/medias/{media_hashed_id}/stats.json +``` + +### Get account-wide stats + +```bash +GET https://api.wistia.com/v1/stats/account.json +``` + +### Get media engagement data (heatmap) + +```bash +GET https://api.wistia.com/v1/stats/medias/{media_id}/engagement.json +``` + +### Get media stats by date + +```bash +GET https://api.wistia.com/v1/stats/medias/{media_id}/by_date.json?start_date=2026-01-01&end_date=2026-01-31 +``` + +### List visitors + +```bash +GET https://api.wistia.com/v1/stats/visitors.json?page=1&per_page=25 +``` + +### List viewing events + +```bash +GET https://api.wistia.com/v1/stats/events.json?media_id={media_id} +``` + +### Update media metadata + +```bash +PUT https://api.wistia.com/v1/medias/{media_hashed_id}.json + +{ + "name": "Updated Video Title", + "description": "New description" +} +``` + +### List captions for a video + +```bash +GET https://api.wistia.com/v1/medias/{media_hashed_id}/captions.json +``` + +## API Versions + +Wistia has two API versions: +- **v1** (`/v1/`) - Legacy, perpetually supported, no breaking changes +- **modern** (`/modern/`) - Current version, date-based versioning via `X-Wistia-Api-Version` header + +The CLI uses v1 for maximum stability. + +## Key Metrics + +### Media Stats +- `plays` - Total video plays +- `visitors` - Unique visitors +- `pageLoads` - Page load count +- `averagePercentWatched` - Average watch percentage +- `percentOfVisitorsClickingPlay` - Play click rate + +### Engagement Data +- Heatmap data showing exactly where viewers watch, rewatch, and drop off +- Per-second engagement breakdown + +### Account Stats +- `total_medias` - Total video count +- `total_plays` - Account-wide plays +- `total_hours_watched` - Total hours of video watched + +## Parameters + +### Media List Parameters +- `page` - Page number (default: 1) +- `per_page` - Results per page (default: 25, max: 100) +- `project_id` - Filter by project +- `name` - Filter by name +- `type` - Filter by type (Video, Audio, Image, etc.) + +### Stats Date Parameters +- `start_date` - Start date (YYYY-MM-DD) +- `end_date` - End date (YYYY-MM-DD) + +## When to Use + +- Hosting marketing and product videos with analytics +- Tracking video engagement and viewer behavior +- A/B testing video thumbnails and CTAs +- Embedding videos with custom player branding +- Analyzing which parts of videos drive engagement +- Lead generation via video email gates + +## Rate Limits + +- 600 requests per minute per account +- Exceeding returns HTTP 429 with `Retry-After` header +- Asset access (media file downloads) does not count toward limit +- Events data returns records from past 2 years only + +## Relevant Skills + +- video-marketing +- content-repurposing +- landing-page-optimization +- lead-generation