From 3a85964305e91be5af828615f95ec7bc36931343 Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:28:41 -0800 Subject: [PATCH] feat: add 23 new CLI tools and integration guides New tools across 13 categories: - Email/Newsletter: beehiiv, klaviyo, postmark, brevo, activecampaign - Data Enrichment: clearbit, apollo - CRO/Testing: hotjar, optimizely - Analytics: plausible - Scheduling: calendly, savvycal - Forms: typeform - Messaging: intercom - Social: buffer - Video: wistia - Payments: paddle - Affiliate: partnerstack - Reviews: trustpilot, g2 - Push: onesignal - Webinar: demio, livestorm Each tool includes a zero-dependency CLI and integration guide. Registry and CLI README updated with all new entries. Co-Authored-By: Claude Opus 4.6 --- tools/REGISTRY.md | 145 ++++++++- tools/clis/README.md | 84 ++++-- tools/clis/activecampaign.js | 432 +++++++++++++++++++++++++++ tools/clis/apollo.js | 140 +++++++++ tools/clis/beehiiv.js | 242 +++++++++++++++ tools/clis/brevo.js | 365 ++++++++++++++++++++++ tools/clis/buffer.js | 246 +++++++++++++++ tools/clis/calendly.js | 250 ++++++++++++++++ tools/clis/clearbit.js | 159 ++++++++++ tools/clis/demio.js | 146 +++++++++ tools/clis/g2.js | 183 ++++++++++++ tools/clis/hotjar.js | 163 ++++++++++ tools/clis/intercom.js | 396 ++++++++++++++++++++++++ tools/clis/klaviyo.js | 344 +++++++++++++++++++++ tools/clis/livestorm.js | 288 ++++++++++++++++++ tools/clis/onesignal.js | 236 +++++++++++++++ tools/clis/optimizely.js | 230 ++++++++++++++ tools/clis/paddle.js | 376 +++++++++++++++++++++++ tools/clis/partnerstack.js | 379 +++++++++++++++++++++++ tools/clis/plausible.js | 246 +++++++++++++++ tools/clis/postmark.js | 366 +++++++++++++++++++++++ tools/clis/savvycal.js | 220 ++++++++++++++ tools/clis/trustpilot.js | 266 +++++++++++++++++ tools/clis/typeform.js | 266 +++++++++++++++++ tools/clis/wistia.js | 249 +++++++++++++++ tools/integrations/activecampaign.md | 337 +++++++++++++++++++++ tools/integrations/apollo.md | 148 +++++++++ tools/integrations/beehiiv.md | 157 ++++++++++ tools/integrations/brevo.md | 268 +++++++++++++++++ tools/integrations/buffer.md | 138 +++++++++ tools/integrations/calendly.md | 161 ++++++++++ tools/integrations/clearbit.md | 142 +++++++++ tools/integrations/demio.md | 182 +++++++++++ tools/integrations/g2.md | 179 +++++++++++ tools/integrations/hotjar.md | 147 +++++++++ tools/integrations/intercom.md | 292 ++++++++++++++++++ tools/integrations/klaviyo.md | 228 ++++++++++++++ tools/integrations/livestorm.md | 313 +++++++++++++++++++ tools/integrations/onesignal.md | 229 ++++++++++++++ tools/integrations/optimizely.md | 171 +++++++++++ tools/integrations/paddle.md | 212 +++++++++++++ tools/integrations/partnerstack.md | 222 ++++++++++++++ tools/integrations/plausible.md | 177 +++++++++++ tools/integrations/postmark.md | 234 +++++++++++++++ tools/integrations/savvycal.md | 181 +++++++++++ tools/integrations/trustpilot.md | 191 ++++++++++++ tools/integrations/typeform.md | 190 ++++++++++++ tools/integrations/wistia.md | 164 ++++++++++ 48 files changed, 11057 insertions(+), 23 deletions(-) create mode 100755 tools/clis/activecampaign.js create mode 100755 tools/clis/apollo.js create mode 100755 tools/clis/beehiiv.js create mode 100755 tools/clis/brevo.js create mode 100755 tools/clis/buffer.js create mode 100755 tools/clis/calendly.js create mode 100755 tools/clis/clearbit.js create mode 100755 tools/clis/demio.js create mode 100755 tools/clis/g2.js create mode 100755 tools/clis/hotjar.js create mode 100755 tools/clis/intercom.js create mode 100755 tools/clis/klaviyo.js create mode 100755 tools/clis/livestorm.js create mode 100755 tools/clis/onesignal.js create mode 100755 tools/clis/optimizely.js create mode 100755 tools/clis/paddle.js create mode 100755 tools/clis/partnerstack.js create mode 100755 tools/clis/plausible.js create mode 100755 tools/clis/postmark.js create mode 100755 tools/clis/savvycal.js create mode 100755 tools/clis/trustpilot.js create mode 100755 tools/clis/typeform.js create mode 100755 tools/clis/wistia.js create mode 100644 tools/integrations/activecampaign.md create mode 100644 tools/integrations/apollo.md create mode 100644 tools/integrations/beehiiv.md create mode 100644 tools/integrations/brevo.md create mode 100644 tools/integrations/buffer.md create mode 100644 tools/integrations/calendly.md create mode 100644 tools/integrations/clearbit.md create mode 100644 tools/integrations/demio.md create mode 100644 tools/integrations/g2.md create mode 100644 tools/integrations/hotjar.md create mode 100644 tools/integrations/intercom.md create mode 100644 tools/integrations/klaviyo.md create mode 100644 tools/integrations/livestorm.md create mode 100644 tools/integrations/onesignal.md create mode 100644 tools/integrations/optimizely.md create mode 100644 tools/integrations/paddle.md create mode 100644 tools/integrations/partnerstack.md create mode 100644 tools/integrations/plausible.md create mode 100644 tools/integrations/postmark.md create mode 100644 tools/integrations/savvycal.md create mode 100644 tools/integrations/trustpilot.md create mode 100644 tools/integrations/typeform.md create mode 100644 tools/integrations/wistia.md diff --git a/tools/REGISTRY.md b/tools/REGISTRY.md index 80d3669..da8b6c3 100644 --- a/tools/REGISTRY.md +++ b/tools/REGISTRY.md @@ -20,28 +20,51 @@ Quick reference for AI agents to discover tool capabilities and integration meth | posthog | Analytics | ✓ | - | ✓ | ✓ | [posthog.md](integrations/posthog.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) | +| 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) | | 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) | @@ -62,8 +85,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 @@ -98,7 +122,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 @@ -110,8 +136,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 @@ -124,8 +151,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 @@ -150,6 +182,111 @@ 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. + ### Commerce & CMS E-commerce platforms and content management systems. diff --git a/tools/clis/README.md b/tools/clis/README.md index 4b6cc1a..aad2279 100644 --- a/tools/clis/README.md +++ b/tools/clis/README.md @@ -36,29 +36,52 @@ Every CLI reads credentials from environment variables: | CLI | Environment Variable | |-----|---------------------| -| `ahrefs` | `AHREFS_API_KEY` | +| `activecampaign` | `ACTIVECAMPAIGN_API_KEY`, `ACTIVECAMPAIGN_API_URL` | | `adobe-analytics` | `ADOBE_CLIENT_ID`, `ADOBE_ACCESS_TOKEN` | +| `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-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` | | `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` | +| `partnerstack` | `PARTNERSTACK_PUBLIC_KEY`, `PARTNERSTACK_SECRET_KEY` | +| `plausible` | `PLAUSIBLE_API_KEY` | +| `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` | | `tolt` | `TOLT_API_KEY` | +| `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` | +| `typeform` | `TYPEFORM_API_KEY` | +| `wistia` | `WISTIA_API_KEY` | | `zapier` | `ZAPIER_API_KEY` | ## Command Pattern @@ -98,27 +121,50 @@ DOMAINS=$(rewardful affiliates list | jq -r '.data[].email') | CLI | Category | Tool | |-----|----------|------| -| `resend.js` | Email | [Resend](https://resend.com) | -| `sendgrid.js` | Email | [SendGrid](https://sendgrid.com) | -| `mailchimp.js` | Email | [Mailchimp](https://mailchimp.com) | -| `kit.js` | Email | [Kit](https://kit.com) | -| `customer-io.js` | Email | [Customer.io](https://customer.io) | -| `ahrefs.js` | SEO | [Ahrefs](https://ahrefs.com) | -| `semrush.js` | SEO | [SEMrush](https://semrush.com) | -| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) | -| `dataforseo.js` | SEO | [DataForSEO](https://dataforseo.com) | -| `keywords-everywhere.js` | SEO | [Keywords Everywhere](https://keywordseverywhere.com) | -| `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) | -| `mixpanel.js` | Analytics | [Mixpanel](https://mixpanel.com) | -| `amplitude.js` | Analytics | [Amplitude](https://amplitude.com) | -| `segment.js` | Analytics | [Segment](https://segment.com) | +| `activecampaign.js` | Email/CRM | [ActiveCampaign](https://activecampaign.com) | | `adobe-analytics.js` | Analytics | [Adobe Analytics](https://business.adobe.com/products/analytics) | -| `rewardful.js` | Referral | [Rewardful](https://www.getrewardful.com) | -| `tolt.js` | Referral | [Tolt](https://tolt.io) | -| `mention-me.js` | Referral | [Mention Me](https://www.mention-me.com) | +| `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) | -| `meta-ads.js` | Ads | [Meta Ads](https://www.facebook.com/business/ads) | +| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) | +| `hotjar.js` | CRO | [Hotjar](https://hotjar.com) | +| `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) | | `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) | | `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..eea72f6 --- /dev/null +++ b/tools/clis/activecampaign.js @@ -0,0 +1,432 @@ +#!/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) { + 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 contactEmail = args.email + if (!automationId) { result = { error: '--id required (automation ID)' }; break } + if (!contactEmail) { result = { error: '--email required' }; break } + result = await api('POST', '/contactAutomations', { + contactAutomation: { contact: contactEmail, 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/apollo.js b/tools/clis/apollo.js new file mode 100755 index 0000000..fee1b30 --- /dev/null +++ b/tools/clis/apollo.js @@ -0,0 +1,140 @@ +#!/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 res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'x-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 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/api_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..cf7e38f --- /dev/null +++ b/tools/clis/beehiiv.js @@ -0,0 +1,242 @@ +#!/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) { + 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..9ef0067 --- /dev/null +++ b/tools/clis/brevo.js @@ -0,0 +1,365 @@ +#!/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) { + 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..786071e --- /dev/null +++ b/tools/clis/buffer.js @@ -0,0 +1,246 @@ +#!/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' + } + 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) { + 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') + 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())) + 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..64fb375 --- /dev/null +++ b/tools/clis/calendly.js @@ -0,0 +1,250 @@ +#!/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) { + 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..3fa4c33 --- /dev/null +++ b/tools/clis/clearbit.js @@ -0,0 +1,159 @@ +#!/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 res = await fetch(`${baseUrl}${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 '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/demio.js b/tools/clis/demio.js new file mode 100755 index 0000000..87c4a4d --- /dev/null +++ b/tools/clis/demio.js @@ -0,0 +1,146 @@ +#!/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) { + 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=${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/g2.js b/tools/clis/g2.js new file mode 100755 index 0000000..e2cb965 --- /dev/null +++ b/tools/clis/g2.js @@ -0,0 +1,183 @@ +#!/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) { + 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]=${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]=${args.start}` + if (args.end) qs += `&filter[end_date]=${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/hotjar.js b/tools/clis/hotjar.js new file mode 100755 index 0000000..38f62d3 --- /dev/null +++ b/tools/clis/hotjar.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +const CLIENT_ID = process.env.HOTJAR_CLIENT_ID +const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET +const BASE_URL = 'https://api.hotjar.io/v1' + +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(`${BASE_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) { + 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/intercom.js b/tools/clis/intercom.js new file mode 100755 index 0000000..5fcb57a --- /dev/null +++ b/tools/clis/intercom.js @@ -0,0 +1,396 @@ +#!/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) { + 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/klaviyo.js b/tools/clis/klaviyo.js new file mode 100755 index 0000000..d23bf34 --- /dev/null +++ b/tools/clis/klaviyo.js @@ -0,0 +1,344 @@ +#!/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 res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Klaviyo-API-Key ${API_KEY}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'revision': REVISION, + }, + 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/livestorm.js b/tools/clis/livestorm.js new file mode 100755 index 0000000..7233c78 --- /dev/null +++ b/tools/clis/livestorm.js @@ -0,0 +1,288 @@ +#!/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 res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': 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 + + 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/onesignal.js b/tools/clis/onesignal.js new file mode 100755 index 0000000..9532ea0 --- /dev/null +++ b/tools/clis/onesignal.js @@ -0,0 +1,236 @@ +#!/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 res = await fetch(`${BASE_URL}${path}`, { + method, + headers: { + 'Authorization': `Basic ${REST_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 '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 } + const filters = args.filters ? JSON.parse(args.filters) : [{ field: 'session_count', relation: '>', value: '0' }] + 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..8bfe696 --- /dev/null +++ b/tools/clis/optimizely.js @@ -0,0 +1,230 @@ +#!/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) { + 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('DELETE', `/experiments/${id}`) + 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..ec7cf08 --- /dev/null +++ b/tools/clis/paddle.js @@ -0,0 +1,376 @@ +#!/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) { + 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']) body.scheduled_change = JSON.parse(args['scheduled-change']) + 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 } + const body = { items: JSON.parse(items) } + 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 } + result = await api('POST', '/adjustments', { + transaction_id: transactionId, + action, + reason, + items: JSON.parse(items), + }) + 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..2fc122f --- /dev/null +++ b/tools/clis/partnerstack.js @@ -0,0 +1,379 @@ +#!/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) { + 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..0bc064c --- /dev/null +++ b/tools/clis/plausible.js @@ -0,0 +1,246 @@ +#!/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) { + 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..0a45b93 --- /dev/null +++ b/tools/clis/postmark.js @@ -0,0 +1,366 @@ +#!/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) { + 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/savvycal.js b/tools/clis/savvycal.js new file mode 100755 index 0000000..8245c87 --- /dev/null +++ b/tools/clis/savvycal.js @@ -0,0 +1,220 @@ +#!/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) { + 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/trustpilot.js b/tools/clis/trustpilot.js new file mode 100755 index 0000000..961ae45 --- /dev/null +++ b/tools/clis/trustpilot.js @@ -0,0 +1,266 @@ +#!/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') { + 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=${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 stars = args.stars ? `&stars=${args.stars}` : '' + const lang = args.language ? `&language=${args.language}` : '' + const orderBy = args['order-by'] || 'createdat.desc' + result = await api('GET', `/business-units/${businessUnitId}/reviews?perPage=${limit}&orderBy=${orderBy}${stars}${lang}`) + 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 stars = args.stars ? `&stars=${args.stars}` : '' + result = await api('GET', `/private/business-units/${businessUnitId}/reviews?perPage=${limit}${stars}`, 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..12d24aa --- /dev/null +++ b/tools/clis/typeform.js @@ -0,0 +1,266 @@ +#!/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) { + 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=${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..8ce28d9 --- /dev/null +++ b/tools/clis/wistia.js @@ -0,0 +1,249 @@ +#!/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 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._ +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']) body.caption_file = args['srt-file'] + 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/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/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/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/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/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/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