Merge pull request #56 from coreyhaines31/development
feat: add 51 zero-dependency CLI tools for marketing platforms
This commit is contained in:
commit
a857eb683a
85 changed files with 17895 additions and 30 deletions
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -1,2 +1,25 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment variables / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
# macOS / iCloud duplicate files
|
||||||
|
* 2.*
|
||||||
|
* 2/
|
||||||
|
|
||||||
# Remotion video project
|
# Remotion video project
|
||||||
video/
|
video/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
|
||||||
15
AGENTS.md
15
AGENTS.md
|
|
@ -20,6 +20,10 @@ marketingskills/
|
||||||
├── skills/ # Agent Skills
|
├── skills/ # Agent Skills
|
||||||
│ └── skill-name/
|
│ └── skill-name/
|
||||||
│ └── SKILL.md # Required skill file
|
│ └── SKILL.md # Required skill file
|
||||||
|
├── tools/
|
||||||
|
│ ├── clis/ # Zero-dependency Node.js CLI tools (51 tools)
|
||||||
|
│ ├── integrations/ # API integration guides per tool
|
||||||
|
│ └── REGISTRY.md # Tool index with capabilities
|
||||||
├── CONTRIBUTING.md
|
├── CONTRIBUTING.md
|
||||||
├── LICENSE
|
├── LICENSE
|
||||||
└── README.md
|
└── README.md
|
||||||
|
|
@ -27,14 +31,19 @@ marketingskills/
|
||||||
|
|
||||||
## Build / Lint / Test Commands
|
## Build / Lint / Test Commands
|
||||||
|
|
||||||
**Not applicable** - This is a content-only repository with no executable code.
|
**Skills** are content-only (no build step). Verify manually:
|
||||||
|
|
||||||
Verify manually:
|
|
||||||
- YAML frontmatter is valid
|
- YAML frontmatter is valid
|
||||||
- `name` field matches directory name exactly
|
- `name` field matches directory name exactly
|
||||||
- `name` is 1-64 chars, lowercase alphanumeric and hyphens only
|
- `name` is 1-64 chars, lowercase alphanumeric and hyphens only
|
||||||
- `description` is 1-1024 characters
|
- `description` is 1-1024 characters
|
||||||
|
|
||||||
|
**CLI tools** (`tools/clis/*.js`) are zero-dependency Node.js scripts (Node 18+). Verify with:
|
||||||
|
```bash
|
||||||
|
node --check tools/clis/<name>.js # Syntax check
|
||||||
|
node tools/clis/<name>.js # Show usage (no args = help)
|
||||||
|
node tools/clis/<name>.js <cmd> --dry-run # Preview request without sending
|
||||||
|
```
|
||||||
|
|
||||||
## Agent Skills Specification
|
## Agent Skills Specification
|
||||||
|
|
||||||
Skills follow the [Agent Skills spec](https://agentskills.io/specification.md).
|
Skills follow the [Agent Skills spec](https://agentskills.io/specification.md).
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,13 @@ Current versions of all skills. Agents can compare against local versions to che
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
|
||||||
|
### 2026-02-17
|
||||||
|
- Added 51 zero-dependency CLI tools for marketing platforms (`tools/clis/`)
|
||||||
|
- Added 31 new integration guides (`tools/integrations/`)
|
||||||
|
- Added 4 email outreach CLIs: hunter, snov, lemlist, instantly
|
||||||
|
- Security hardening: header auth for meta-ads, URL encoding, input validation
|
||||||
|
- All CLIs reviewed via independent codex audit (auth, security, error handling, consistency)
|
||||||
|
|
||||||
### 2026-01-27
|
### 2026-01-27
|
||||||
- Initial version tracking added
|
- Initial version tracking added
|
||||||
- Added tools registry with 29 integration guides
|
- Added tools registry with 29 integration guides
|
||||||
|
|
|
||||||
|
|
@ -14,32 +14,61 @@ Quick reference for AI agents to discover tool capabilities and integration meth
|
||||||
|
|
||||||
| Tool | Category | API | MCP | CLI | SDK | Guide |
|
| Tool | Category | API | MCP | CLI | SDK | Guide |
|
||||||
|------|----------|:---:|:---:|:---:|:---:|-------|
|
|------|----------|:---:|:---:|:---:|:---:|-------|
|
||||||
| ga4 | Analytics | ✓ | ✓ | - | ✓ | [ga4.md](integrations/ga4.md) |
|
| ga4 | Analytics | ✓ | ✓ | [✓](clis/ga4.js) | ✓ | [ga4.md](integrations/ga4.md) |
|
||||||
| mixpanel | Analytics | ✓ | - | - | ✓ | [mixpanel.md](integrations/mixpanel.md) |
|
| mixpanel | Analytics | ✓ | - | [✓](clis/mixpanel.js) | ✓ | [mixpanel.md](integrations/mixpanel.md) |
|
||||||
| amplitude | Analytics | ✓ | - | - | ✓ | [amplitude.md](integrations/amplitude.md) |
|
| amplitude | Analytics | ✓ | - | [✓](clis/amplitude.js) | ✓ | [amplitude.md](integrations/amplitude.md) |
|
||||||
| posthog | Analytics | ✓ | - | ✓ | ✓ | [posthog.md](integrations/posthog.md) |
|
| posthog | Analytics | ✓ | - | ✓ | ✓ | [posthog.md](integrations/posthog.md) |
|
||||||
| segment | Analytics | ✓ | - | - | ✓ | [segment.md](integrations/segment.md) |
|
| segment | Analytics | ✓ | - | [✓](clis/segment.js) | ✓ | [segment.md](integrations/segment.md) |
|
||||||
| adobe-analytics | Analytics | ✓ | - | - | ✓ | [adobe-analytics.md](integrations/adobe-analytics.md) |
|
| adobe-analytics | Analytics | ✓ | - | [✓](clis/adobe-analytics.js) | ✓ | [adobe-analytics.md](integrations/adobe-analytics.md) |
|
||||||
| google-search-console | SEO | ✓ | - | - | ✓ | [google-search-console.md](integrations/google-search-console.md) |
|
| plausible | Analytics | ✓ | - | [✓](clis/plausible.js) | - | [plausible.md](integrations/plausible.md) |
|
||||||
| semrush | SEO | ✓ | - | - | - | [semrush.md](integrations/semrush.md) |
|
| google-search-console | SEO | ✓ | - | [✓](clis/google-search-console.js) | ✓ | [google-search-console.md](integrations/google-search-console.md) |
|
||||||
| ahrefs | SEO | ✓ | - | - | - | [ahrefs.md](integrations/ahrefs.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) |
|
| hubspot | CRM | ✓ | - | ✓ | ✓ | [hubspot.md](integrations/hubspot.md) |
|
||||||
| salesforce | CRM | ✓ | - | ✓ | ✓ | [salesforce.md](integrations/salesforce.md) |
|
| salesforce | CRM | ✓ | - | ✓ | ✓ | [salesforce.md](integrations/salesforce.md) |
|
||||||
| stripe | Payments | ✓ | ✓ | ✓ | ✓ | [stripe.md](integrations/stripe.md) |
|
| stripe | Payments | ✓ | ✓ | ✓ | ✓ | [stripe.md](integrations/stripe.md) |
|
||||||
| rewardful | Referral | ✓ | - | - | - | [rewardful.md](integrations/rewardful.md) |
|
| paddle | Payments | ✓ | - | [✓](clis/paddle.js) | ✓ | [paddle.md](integrations/paddle.md) |
|
||||||
| tolt | Referral | ✓ | - | - | - | [tolt.md](integrations/tolt.md) |
|
| rewardful | Referral | ✓ | - | [✓](clis/rewardful.js) | - | [rewardful.md](integrations/rewardful.md) |
|
||||||
| dub-co | Links | ✓ | - | - | ✓ | [dub-co.md](integrations/dub-co.md) |
|
| tolt | Referral | ✓ | - | [✓](clis/tolt.js) | - | [tolt.md](integrations/tolt.md) |
|
||||||
| mention-me | Referral | ✓ | - | - | - | [mention-me.md](integrations/mention-me.md) |
|
| dub-co | Links | ✓ | - | [✓](clis/dub.js) | ✓ | [dub-co.md](integrations/dub-co.md) |
|
||||||
| mailchimp | Email | ✓ | ✓ | - | ✓ | [mailchimp.md](integrations/mailchimp.md) |
|
| mention-me | Referral | ✓ | - | [✓](clis/mention-me.js) | - | [mention-me.md](integrations/mention-me.md) |
|
||||||
| customer-io | Email | ✓ | - | - | ✓ | [customer-io.md](integrations/customer-io.md) |
|
| partnerstack | Affiliate | ✓ | - | [✓](clis/partnerstack.js) | - | [partnerstack.md](integrations/partnerstack.md) |
|
||||||
| sendgrid | Email | ✓ | - | - | ✓ | [sendgrid.md](integrations/sendgrid.md) |
|
| mailchimp | Email | ✓ | ✓ | [✓](clis/mailchimp.js) | ✓ | [mailchimp.md](integrations/mailchimp.md) |
|
||||||
| resend | Email | ✓ | ✓ | - | ✓ | [resend.md](integrations/resend.md) |
|
| customer-io | Email | ✓ | - | [✓](clis/customer-io.js) | ✓ | [customer-io.md](integrations/customer-io.md) |
|
||||||
| kit | Email | ✓ | - | - | ✓ | [kit.md](integrations/kit.md) |
|
| sendgrid | Email | ✓ | - | [✓](clis/sendgrid.js) | ✓ | [sendgrid.md](integrations/sendgrid.md) |
|
||||||
| google-ads | Ads | ✓ | ✓ | - | ✓ | [google-ads.md](integrations/google-ads.md) |
|
| resend | Email | ✓ | ✓ | [✓](clis/resend.js) | ✓ | [resend.md](integrations/resend.md) |
|
||||||
| meta-ads | Ads | ✓ | - | - | ✓ | [meta-ads.md](integrations/meta-ads.md) |
|
| kit | Email | ✓ | - | [✓](clis/kit.js) | ✓ | [kit.md](integrations/kit.md) |
|
||||||
| linkedin-ads | Ads | ✓ | - | - | - | [linkedin-ads.md](integrations/linkedin-ads.md) |
|
| beehiiv | Newsletter | ✓ | - | [✓](clis/beehiiv.js) | - | [beehiiv.md](integrations/beehiiv.md) |
|
||||||
| tiktok-ads | Ads | ✓ | - | - | ✓ | [tiktok-ads.md](integrations/tiktok-ads.md) |
|
| klaviyo | Email/SMS | ✓ | - | [✓](clis/klaviyo.js) | ✓ | [klaviyo.md](integrations/klaviyo.md) |
|
||||||
| zapier | Automation | ✓ | ✓ | - | - | [zapier.md](integrations/zapier.md) |
|
| postmark | Email | ✓ | - | [✓](clis/postmark.js) | ✓ | [postmark.md](integrations/postmark.md) |
|
||||||
|
| brevo | Email/SMS | ✓ | - | [✓](clis/brevo.js) | ✓ | [brevo.md](integrations/brevo.md) |
|
||||||
|
| activecampaign | Email/CRM | ✓ | - | [✓](clis/activecampaign.js) | ✓ | [activecampaign.md](integrations/activecampaign.md) |
|
||||||
|
| hunter | Email Outreach | ✓ | - | [✓](clis/hunter.js) | - | [hunter.md](integrations/hunter.md) |
|
||||||
|
| snov | Email Outreach | ✓ | - | [✓](clis/snov.js) | - | [snov.md](integrations/snov.md) |
|
||||||
|
| lemlist | Email Outreach | ✓ | - | [✓](clis/lemlist.js) | - | [lemlist.md](integrations/lemlist.md) |
|
||||||
|
| instantly | Email Outreach | ✓ | - | [✓](clis/instantly.js) | - | [instantly.md](integrations/instantly.md) |
|
||||||
|
| google-ads | Ads | ✓ | ✓ | [✓](clis/google-ads.js) | ✓ | [google-ads.md](integrations/google-ads.md) |
|
||||||
|
| meta-ads | Ads | ✓ | - | [✓](clis/meta-ads.js) | ✓ | [meta-ads.md](integrations/meta-ads.md) |
|
||||||
|
| linkedin-ads | Ads | ✓ | - | [✓](clis/linkedin-ads.js) | - | [linkedin-ads.md](integrations/linkedin-ads.md) |
|
||||||
|
| tiktok-ads | Ads | ✓ | - | [✓](clis/tiktok-ads.js) | ✓ | [tiktok-ads.md](integrations/tiktok-ads.md) |
|
||||||
|
| zapier | Automation | ✓ | ✓ | [✓](clis/zapier.js) | - | [zapier.md](integrations/zapier.md) |
|
||||||
|
| hotjar | CRO | ✓ | - | [✓](clis/hotjar.js) | - | [hotjar.md](integrations/hotjar.md) |
|
||||||
|
| optimizely | A/B Testing | ✓ | - | [✓](clis/optimizely.js) | ✓ | [optimizely.md](integrations/optimizely.md) |
|
||||||
|
| calendly | Scheduling | ✓ | - | [✓](clis/calendly.js) | - | [calendly.md](integrations/calendly.md) |
|
||||||
|
| savvycal | Scheduling | ✓ | - | [✓](clis/savvycal.js) | - | [savvycal.md](integrations/savvycal.md) |
|
||||||
|
| typeform | Forms | ✓ | - | [✓](clis/typeform.js) | ✓ | [typeform.md](integrations/typeform.md) |
|
||||||
|
| intercom | Messaging | ✓ | - | [✓](clis/intercom.js) | ✓ | [intercom.md](integrations/intercom.md) |
|
||||||
|
| buffer | Social | ✓ | - | [✓](clis/buffer.js) | - | [buffer.md](integrations/buffer.md) |
|
||||||
|
| wistia | Video | ✓ | - | [✓](clis/wistia.js) | - | [wistia.md](integrations/wistia.md) |
|
||||||
|
| trustpilot | Reviews | ✓ | - | [✓](clis/trustpilot.js) | - | [trustpilot.md](integrations/trustpilot.md) |
|
||||||
|
| g2 | Reviews | ✓ | - | [✓](clis/g2.js) | - | [g2.md](integrations/g2.md) |
|
||||||
|
| onesignal | Push | ✓ | - | [✓](clis/onesignal.js) | ✓ | [onesignal.md](integrations/onesignal.md) |
|
||||||
|
| demio | Webinar | ✓ | - | [✓](clis/demio.js) | - | [demio.md](integrations/demio.md) |
|
||||||
|
| livestorm | Webinar | ✓ | - | [✓](clis/livestorm.js) | - | [livestorm.md](integrations/livestorm.md) |
|
||||||
| shopify | Commerce | ✓ | - | ✓ | ✓ | [shopify.md](integrations/shopify.md) |
|
| shopify | Commerce | ✓ | - | ✓ | ✓ | [shopify.md](integrations/shopify.md) |
|
||||||
| wordpress | CMS | ✓ | - | ✓ | ✓ | [wordpress.md](integrations/wordpress.md) |
|
| wordpress | CMS | ✓ | - | ✓ | ✓ | [wordpress.md](integrations/wordpress.md) |
|
||||||
| webflow | CMS | ✓ | - | ✓ | ✓ | [webflow.md](integrations/webflow.md) |
|
| webflow | CMS | ✓ | - | ✓ | ✓ | [webflow.md](integrations/webflow.md) |
|
||||||
|
|
@ -60,8 +89,9 @@ Track user behavior, measure conversions, and analyze marketing performance.
|
||||||
| **posthog** | Open-source analytics, session replay | - |
|
| **posthog** | Open-source analytics, session replay | - |
|
||||||
| **segment** | Customer data platform, routing | - |
|
| **segment** | Customer data platform, routing | - |
|
||||||
| **adobe-analytics** | Enterprise analytics | - |
|
| **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
|
### SEO
|
||||||
|
|
||||||
|
|
@ -72,8 +102,10 @@ Search engine optimization tools for keyword research, rank tracking, and site a
|
||||||
| **google-search-console** | Free, authoritative search data | Direct from Google |
|
| **google-search-console** | Free, authoritative search data | Direct from Google |
|
||||||
| **semrush** | Competitive analysis, keyword research | Comprehensive |
|
| **semrush** | Competitive analysis, keyword research | Comprehensive |
|
||||||
| **ahrefs** | Backlink analysis, content research | Best for links |
|
| **ahrefs** | Backlink analysis, content research | Best for links |
|
||||||
|
| **dataforseo** | SERP tracking, backlinks, on-page audits | Comprehensive API |
|
||||||
|
| **keywords-everywhere** | Quick keyword research, traffic estimates | Credit-based |
|
||||||
|
|
||||||
**Agent recommendation**: Google Search Console is essential (free). Add Semrush or Ahrefs for competitive research.
|
**Agent recommendation**: Google Search Console is essential (free). Add Semrush or Ahrefs for competitive research. DataForSEO for programmatic SERP data. Keywords Everywhere for quick keyword lookups.
|
||||||
|
|
||||||
### CRM
|
### CRM
|
||||||
|
|
||||||
|
|
@ -94,7 +126,9 @@ Payment processing and subscription management.
|
||||||
|------|----------|:-------------:|
|
|------|----------|:-------------:|
|
||||||
| **stripe** | SaaS subscriptions, developer-friendly | ✓ |
|
| **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
|
### Referral & Affiliate
|
||||||
|
|
||||||
|
|
@ -106,8 +140,9 @@ Tools for referral programs, affiliate tracking, and partner management.
|
||||||
| **tolt** | SaaS affiliate programs | ✓ |
|
| **tolt** | SaaS affiliate programs | ✓ |
|
||||||
| **mention-me** | Enterprise referral programs | ✓ |
|
| **mention-me** | Enterprise referral programs | ✓ |
|
||||||
| **dub-co** | Link tracking, attribution | - |
|
| **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
|
### Email
|
||||||
|
|
||||||
|
|
@ -120,8 +155,13 @@ Email marketing, transactional email, and automation platforms.
|
||||||
| **sendgrid** | Transactional email at scale | - |
|
| **sendgrid** | Transactional email at scale | - |
|
||||||
| **resend** | Developer-friendly transactional | ✓ |
|
| **resend** | Developer-friendly transactional | ✓ |
|
||||||
| **kit** | Creator/newsletter focused | - |
|
| **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
|
### Advertising
|
||||||
|
|
||||||
|
|
@ -146,6 +186,124 @@ Workflow automation and integration platforms.
|
||||||
|
|
||||||
**Agent recommendation**: Zapier for connecting tools without code.
|
**Agent recommendation**: Zapier for connecting tools without code.
|
||||||
|
|
||||||
|
### CRO & A/B Testing
|
||||||
|
|
||||||
|
Conversion rate optimization, heatmaps, and experimentation.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **hotjar** | Heatmaps, recordings, surveys | Visual behavior data |
|
||||||
|
| **optimizely** | A/B testing, feature flags | Enterprise experimentation |
|
||||||
|
|
||||||
|
**Agent recommendation**: Hotjar for understanding user behavior. Optimizely for running experiments.
|
||||||
|
|
||||||
|
### Scheduling
|
||||||
|
|
||||||
|
Booking and appointment scheduling tools.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **calendly** | Meeting scheduling, lead gen | Most popular |
|
||||||
|
| **savvycal** | Personalized scheduling | Developer-friendly |
|
||||||
|
|
||||||
|
**Agent recommendation**: Calendly for general use. SavvyCal for personalized booking experiences.
|
||||||
|
|
||||||
|
### Forms & Surveys
|
||||||
|
|
||||||
|
Form builders and survey platforms.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **typeform** | Interactive forms, surveys | Conversational UX |
|
||||||
|
|
||||||
|
**Agent recommendation**: Typeform for engaging forms and surveys.
|
||||||
|
|
||||||
|
### Messaging
|
||||||
|
|
||||||
|
In-app messaging, chat, and customer communication.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **intercom** | In-app messaging, support, product tours | Full customer platform |
|
||||||
|
|
||||||
|
**Agent recommendation**: Intercom for in-app messaging and customer support.
|
||||||
|
|
||||||
|
### Social Media
|
||||||
|
|
||||||
|
Social media scheduling, management, and analytics.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **buffer** | Social scheduling, analytics | Multi-platform |
|
||||||
|
|
||||||
|
**Agent recommendation**: Buffer for scheduling and analytics across social platforms.
|
||||||
|
|
||||||
|
### Video
|
||||||
|
|
||||||
|
Video hosting, analytics, and engagement.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **wistia** | Video hosting, marketing analytics | Best for marketing video |
|
||||||
|
|
||||||
|
**Agent recommendation**: Wistia for marketing video hosting with analytics.
|
||||||
|
|
||||||
|
### Data Enrichment
|
||||||
|
|
||||||
|
Company and person data enrichment for sales and marketing.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **clearbit** | Company/person enrichment | Now HubSpot Breeze |
|
||||||
|
| **apollo** | B2B prospecting, email finding | Large database |
|
||||||
|
|
||||||
|
**Agent recommendation**: Clearbit for enrichment. Apollo for prospecting and outbound.
|
||||||
|
|
||||||
|
### Reviews
|
||||||
|
|
||||||
|
Review management and social proof platforms.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **trustpilot** | Consumer business reviews | Most recognized |
|
||||||
|
| **g2** | Software/B2B reviews | Best for SaaS |
|
||||||
|
|
||||||
|
**Agent recommendation**: Trustpilot for consumer products. G2 for B2B software.
|
||||||
|
|
||||||
|
### Push Notifications
|
||||||
|
|
||||||
|
Push notification delivery platforms.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **onesignal** | Multi-channel push notifications | Web + mobile |
|
||||||
|
|
||||||
|
**Agent recommendation**: OneSignal for web and mobile push notifications.
|
||||||
|
|
||||||
|
### Webinar
|
||||||
|
|
||||||
|
Webinar and virtual event platforms.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **demio** | Marketing webinars | Simple, focused |
|
||||||
|
| **livestorm** | Video engagement, webinars | Full event platform |
|
||||||
|
|
||||||
|
**Agent recommendation**: Demio for marketing-focused webinars. Livestorm for full event engagement.
|
||||||
|
|
||||||
|
### Email Outreach
|
||||||
|
|
||||||
|
Cold email outreach and email finding tools for link building and sales prospecting.
|
||||||
|
|
||||||
|
| Tool | Best For | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **hunter** | Email finding, domain search | Largest email database |
|
||||||
|
| **snov** | Email finding, drip campaigns | Built-in sequences |
|
||||||
|
| **lemlist** | Cold email campaigns | Personalization features |
|
||||||
|
| **instantly** | Cold email at scale | Email warmup built-in |
|
||||||
|
|
||||||
|
**Agent recommendation**: Hunter for finding emails. Lemlist or Instantly for sending cold email campaigns. Snov for combined finding + outreach.
|
||||||
|
|
||||||
### Commerce & CMS
|
### Commerce & CMS
|
||||||
|
|
||||||
E-commerce platforms and content management systems.
|
E-commerce platforms and content management systems.
|
||||||
|
|
@ -160,6 +318,18 @@ E-commerce platforms and content management systems.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## CLI Tools
|
||||||
|
|
||||||
|
Zero-dependency, single-file Node.js CLIs for tools that don't ship their own. See [`clis/README.md`](clis/README.md) for install instructions and usage.
|
||||||
|
|
||||||
|
All CLIs follow a consistent pattern:
|
||||||
|
- **No dependencies** — Node 18+ only, uses native `fetch`
|
||||||
|
- **JSON output** — pipe to `jq`, save to file, or use in scripts
|
||||||
|
- **Env var auth** — set `{TOOL}_API_KEY` and go
|
||||||
|
- **Consistent commands** — `{tool} <resource> <action> [options]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## MCP-Enabled Tools
|
## MCP-Enabled Tools
|
||||||
|
|
||||||
These tools have Model Context Protocol servers available, enabling direct agent interaction:
|
These tools have Model Context Protocol servers available, enabling direct agent interaction:
|
||||||
|
|
@ -189,6 +359,10 @@ To use MCP tools, ensure the appropriate MCP server is configured in your enviro
|
||||||
1. Read [customer-io.md](integrations/customer-io.md) for behavior-based automation
|
1. Read [customer-io.md](integrations/customer-io.md) for behavior-based automation
|
||||||
2. Read [resend.md](integrations/resend.md) for transactional email
|
2. Read [resend.md](integrations/resend.md) for transactional email
|
||||||
|
|
||||||
|
### Running email outreach for backlinks
|
||||||
|
1. Read [hunter.md](integrations/hunter.md) for finding emails
|
||||||
|
2. Read [lemlist.md](integrations/lemlist.md) or [instantly.md](integrations/instantly.md) for sending campaigns
|
||||||
|
|
||||||
### Running paid ads
|
### Running paid ads
|
||||||
1. Read [google-ads.md](integrations/google-ads.md) for search campaigns
|
1. Read [google-ads.md](integrations/google-ads.md) for search campaigns
|
||||||
2. Read [meta-ads.md](integrations/meta-ads.md) for social campaigns
|
2. Read [meta-ads.md](integrations/meta-ads.md) for social campaigns
|
||||||
|
|
|
||||||
187
tools/clis/README.md
Normal file
187
tools/clis/README.md
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
# Marketing CLIs
|
||||||
|
|
||||||
|
Zero-dependency, single-file CLI tools for marketing platforms that don't ship their own.
|
||||||
|
|
||||||
|
Every CLI is a standalone Node.js script (Node 18+) with no `npm install` required — just `chmod +x` and go.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Option 1: Run directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/ahrefs.js backlinks list --target example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Symlink for global access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Symlink any CLI you want available globally
|
||||||
|
ln -sf "$(pwd)/tools/clis/ahrefs.js" ~/.local/bin/ahrefs
|
||||||
|
ln -sf "$(pwd)/tools/clis/resend.js" ~/.local/bin/resend
|
||||||
|
|
||||||
|
# Then use directly
|
||||||
|
ahrefs backlinks list --target example.com
|
||||||
|
resend send --from you@example.com --to them@example.com --subject "Hello" --html "<p>Hi</p>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Add the whole directory to PATH
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$PATH:/path/to/marketingskills/tools/clis"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Every CLI reads credentials from environment variables:
|
||||||
|
|
||||||
|
| CLI | Environment Variable |
|
||||||
|
|-----|---------------------|
|
||||||
|
| `activecampaign` | `ACTIVECAMPAIGN_API_KEY`, `ACTIVECAMPAIGN_API_URL` |
|
||||||
|
| `adobe-analytics` | `ADOBE_ACCESS_TOKEN`, `ADOBE_CLIENT_ID`, `ADOBE_COMPANY_ID` |
|
||||||
|
| `ahrefs` | `AHREFS_API_KEY` |
|
||||||
|
| `amplitude` | `AMPLITUDE_API_KEY`, `AMPLITUDE_SECRET_KEY` |
|
||||||
|
| `apollo` | `APOLLO_API_KEY` |
|
||||||
|
| `beehiiv` | `BEEHIIV_API_KEY` |
|
||||||
|
| `brevo` | `BREVO_API_KEY` |
|
||||||
|
| `buffer` | `BUFFER_API_KEY` |
|
||||||
|
| `calendly` | `CALENDLY_API_KEY` |
|
||||||
|
| `clearbit` | `CLEARBIT_API_KEY` |
|
||||||
|
| `customer-io` | `CUSTOMERIO_APP_KEY` (App API), `CUSTOMERIO_SITE_ID` + `CUSTOMERIO_API_KEY` (Track API) |
|
||||||
|
| `dataforseo` | `DATAFORSEO_LOGIN`, `DATAFORSEO_PASSWORD` |
|
||||||
|
| `demio` | `DEMIO_API_KEY`, `DEMIO_API_SECRET` |
|
||||||
|
| `dub` | `DUB_API_KEY` |
|
||||||
|
| `g2` | `G2_API_TOKEN` |
|
||||||
|
| `ga4` | `GA4_ACCESS_TOKEN` |
|
||||||
|
| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` |
|
||||||
|
| `google-search-console` | `GSC_ACCESS_TOKEN` |
|
||||||
|
| `hotjar` | `HOTJAR_CLIENT_ID`, `HOTJAR_CLIENT_SECRET` |
|
||||||
|
| `intercom` | `INTERCOM_API_KEY` |
|
||||||
|
| `keywords-everywhere` | `KEYWORDS_EVERYWHERE_API_KEY` |
|
||||||
|
| `kit` | `KIT_API_KEY`, `KIT_API_SECRET` |
|
||||||
|
| `klaviyo` | `KLAVIYO_API_KEY` |
|
||||||
|
| `linkedin-ads` | `LINKEDIN_ACCESS_TOKEN` |
|
||||||
|
| `livestorm` | `LIVESTORM_API_TOKEN` |
|
||||||
|
| `mailchimp` | `MAILCHIMP_API_KEY` |
|
||||||
|
| `mention-me` | `MENTIONME_API_KEY` |
|
||||||
|
| `meta-ads` | `META_ACCESS_TOKEN`, `META_AD_ACCOUNT_ID` |
|
||||||
|
| `mixpanel` | `MIXPANEL_TOKEN` (ingestion), `MIXPANEL_API_KEY` + `MIXPANEL_SECRET` (query) |
|
||||||
|
| `onesignal` | `ONESIGNAL_REST_API_KEY`, `ONESIGNAL_APP_ID` |
|
||||||
|
| `optimizely` | `OPTIMIZELY_API_KEY` |
|
||||||
|
| `paddle` | `PADDLE_API_KEY`, `PADDLE_SANDBOX` (optional) |
|
||||||
|
| `partnerstack` | `PARTNERSTACK_PUBLIC_KEY`, `PARTNERSTACK_SECRET_KEY` |
|
||||||
|
| `plausible` | `PLAUSIBLE_API_KEY`, `PLAUSIBLE_BASE_URL` (optional, for self-hosted) |
|
||||||
|
| `postmark` | `POSTMARK_API_KEY` |
|
||||||
|
| `resend` | `RESEND_API_KEY` |
|
||||||
|
| `rewardful` | `REWARDFUL_API_KEY` |
|
||||||
|
| `savvycal` | `SAVVYCAL_API_KEY` |
|
||||||
|
| `segment` | `SEGMENT_WRITE_KEY` (tracking), `SEGMENT_ACCESS_TOKEN` (profile) |
|
||||||
|
| `semrush` | `SEMRUSH_API_KEY` |
|
||||||
|
| `sendgrid` | `SENDGRID_API_KEY` |
|
||||||
|
| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN`, `TIKTOK_ADVERTISER_ID` |
|
||||||
|
| `tolt` | `TOLT_API_KEY` |
|
||||||
|
| `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` |
|
||||||
|
| `typeform` | `TYPEFORM_API_KEY` |
|
||||||
|
| `hunter` | `HUNTER_API_KEY` |
|
||||||
|
| `instantly` | `INSTANTLY_API_KEY` |
|
||||||
|
| `lemlist` | `LEMLIST_API_KEY` |
|
||||||
|
| `snov` | `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET` |
|
||||||
|
| `wistia` | `WISTIA_API_KEY` |
|
||||||
|
| `zapier` | `ZAPIER_API_KEY` |
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
**Never hardcode API keys or tokens in scripts.** All CLIs read credentials exclusively from environment variables.
|
||||||
|
|
||||||
|
- Store keys in your shell profile (`~/.zshrc`, `~/.bashrc`) or a `.env` file
|
||||||
|
- The `.env` file is gitignored — but double-check before committing
|
||||||
|
- Use `--dry-run` on any command to preview the request without sending it (credentials are masked as `***`)
|
||||||
|
- If you fork this repo, audit your commits to ensure no secrets are included
|
||||||
|
|
||||||
|
## Command Pattern
|
||||||
|
|
||||||
|
All CLIs follow the same structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
{tool} <resource> <action> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ahrefs backlinks list --target example.com --limit 50
|
||||||
|
semrush keywords overview --phrase "marketing automation" --database us
|
||||||
|
mailchimp campaigns list --limit 20
|
||||||
|
resend send --from you@example.com --to them@example.com --subject "Hello" --html "<p>Hi</p>"
|
||||||
|
dub links create --url https://example.com/landing --key summer-sale
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
All CLIs output JSON to stdout for easy piping:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pipe to jq
|
||||||
|
ahrefs backlinks list --target example.com | jq '.backlinks[].url_from'
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
semrush keywords overview --phrase "saas marketing" --database us > keywords.json
|
||||||
|
|
||||||
|
# Use in scripts
|
||||||
|
DOMAINS=$(rewardful affiliates list | jq -r '.data[].email')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available CLIs
|
||||||
|
|
||||||
|
| CLI | Category | Tool |
|
||||||
|
|-----|----------|------|
|
||||||
|
| `activecampaign.js` | Email/CRM | [ActiveCampaign](https://activecampaign.com) |
|
||||||
|
| `adobe-analytics.js` | Analytics | [Adobe Analytics](https://business.adobe.com/products/analytics) |
|
||||||
|
| `ahrefs.js` | SEO | [Ahrefs](https://ahrefs.com) |
|
||||||
|
| `amplitude.js` | Analytics | [Amplitude](https://amplitude.com) |
|
||||||
|
| `apollo.js` | Data Enrichment | [Apollo.io](https://apollo.io) |
|
||||||
|
| `beehiiv.js` | Newsletter | [Beehiiv](https://beehiiv.com) |
|
||||||
|
| `brevo.js` | Email/SMS | [Brevo](https://brevo.com) |
|
||||||
|
| `buffer.js` | Social | [Buffer](https://buffer.com) |
|
||||||
|
| `calendly.js` | Scheduling | [Calendly](https://calendly.com) |
|
||||||
|
| `clearbit.js` | Data Enrichment | [Clearbit](https://clearbit.com) |
|
||||||
|
| `customer-io.js` | Email | [Customer.io](https://customer.io) |
|
||||||
|
| `dataforseo.js` | SEO | [DataForSEO](https://dataforseo.com) |
|
||||||
|
| `demio.js` | Webinar | [Demio](https://demio.com) |
|
||||||
|
| `dub.js` | Links | [Dub.co](https://dub.co) |
|
||||||
|
| `g2.js` | Reviews | [G2](https://g2.com) |
|
||||||
|
| `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) |
|
||||||
|
| `google-ads.js` | Ads | [Google Ads](https://ads.google.com) |
|
||||||
|
| `google-search-console.js` | SEO | [Google Search Console](https://search.google.com/search-console) |
|
||||||
|
| `hotjar.js` | CRO | [Hotjar](https://hotjar.com) |
|
||||||
|
| `hunter.js` | Email Outreach | [Hunter.io](https://hunter.io) |
|
||||||
|
| `instantly.js` | Email Outreach | [Instantly.ai](https://instantly.ai) |
|
||||||
|
| `intercom.js` | Messaging | [Intercom](https://intercom.com) |
|
||||||
|
| `keywords-everywhere.js` | SEO | [Keywords Everywhere](https://keywordseverywhere.com) |
|
||||||
|
| `kit.js` | Email | [Kit](https://kit.com) |
|
||||||
|
| `klaviyo.js` | Email/SMS | [Klaviyo](https://klaviyo.com) |
|
||||||
|
| `lemlist.js` | Email Outreach | [Lemlist](https://lemlist.com) |
|
||||||
|
| `linkedin-ads.js` | Ads | [LinkedIn Ads](https://business.linkedin.com/marketing-solutions/ads) |
|
||||||
|
| `livestorm.js` | Webinar | [Livestorm](https://livestorm.co) |
|
||||||
|
| `mailchimp.js` | Email | [Mailchimp](https://mailchimp.com) |
|
||||||
|
| `mention-me.js` | Referral | [Mention Me](https://www.mention-me.com) |
|
||||||
|
| `meta-ads.js` | Ads | [Meta Ads](https://www.facebook.com/business/ads) |
|
||||||
|
| `mixpanel.js` | Analytics | [Mixpanel](https://mixpanel.com) |
|
||||||
|
| `onesignal.js` | Push | [OneSignal](https://onesignal.com) |
|
||||||
|
| `optimizely.js` | A/B Testing | [Optimizely](https://optimizely.com) |
|
||||||
|
| `paddle.js` | Payments | [Paddle](https://paddle.com) |
|
||||||
|
| `partnerstack.js` | Affiliate | [PartnerStack](https://partnerstack.com) |
|
||||||
|
| `plausible.js` | Analytics | [Plausible](https://plausible.io) |
|
||||||
|
| `postmark.js` | Email | [Postmark](https://postmarkapp.com) |
|
||||||
|
| `resend.js` | Email | [Resend](https://resend.com) |
|
||||||
|
| `rewardful.js` | Referral | [Rewardful](https://www.getrewardful.com) |
|
||||||
|
| `savvycal.js` | Scheduling | [SavvyCal](https://savvycal.com) |
|
||||||
|
| `segment.js` | Analytics | [Segment](https://segment.com) |
|
||||||
|
| `semrush.js` | SEO | [SEMrush](https://semrush.com) |
|
||||||
|
| `sendgrid.js` | Email | [SendGrid](https://sendgrid.com) |
|
||||||
|
| `snov.js` | Email Outreach | [Snov.io](https://snov.io) |
|
||||||
|
| `tiktok-ads.js` | Ads | [TikTok Ads](https://ads.tiktok.com) |
|
||||||
|
| `tolt.js` | Referral | [Tolt](https://tolt.io) |
|
||||||
|
| `trustpilot.js` | Reviews | [Trustpilot](https://trustpilot.com) |
|
||||||
|
| `typeform.js` | Forms | [Typeform](https://typeform.com) |
|
||||||
|
| `wistia.js` | Video | [Wistia](https://wistia.com) |
|
||||||
|
| `zapier.js` | Automation | [Zapier](https://zapier.com) |
|
||||||
435
tools/clis/activecampaign.js
Executable file
435
tools/clis/activecampaign.js
Executable file
|
|
@ -0,0 +1,435 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.ACTIVECAMPAIGN_API_KEY
|
||||||
|
const API_URL = process.env.ACTIVECAMPAIGN_API_URL
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'ACTIVECAMPAIGN_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!API_URL) {
|
||||||
|
console.error(JSON.stringify({ error: 'ACTIVECAMPAIGN_API_URL environment variable required (e.g. https://yourname.api-us1.com)' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL = `${API_URL.replace(/\/$/, '')}/api/3`
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Api-Token': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Api-Token': API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const limit = args.limit ? Number(args.limit) : 20
|
||||||
|
const offset = args.offset ? Number(args.offset) : 0
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'contacts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.email) params.set('email', args.email)
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
if (args['list-id']) params.set('listid', args['list-id'])
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/contacts?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/contacts/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const contact = { email }
|
||||||
|
if (args['first-name']) contact.firstName = args['first-name']
|
||||||
|
if (args['last-name']) contact.lastName = args['last-name']
|
||||||
|
if (args.phone) contact.phone = args.phone
|
||||||
|
result = await api('POST', '/contacts', { contact })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const contact = {}
|
||||||
|
if (args.email) contact.email = args.email
|
||||||
|
if (args['first-name']) contact.firstName = args['first-name']
|
||||||
|
if (args['last-name']) contact.lastName = args['last-name']
|
||||||
|
if (args.phone) contact.phone = args.phone
|
||||||
|
result = await api('PUT', `/contacts/${id}`, { contact })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/contacts/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'sync': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const contact = { email }
|
||||||
|
if (args['first-name']) contact.firstName = args['first-name']
|
||||||
|
if (args['last-name']) contact.lastName = args['last-name']
|
||||||
|
if (args.phone) contact.phone = args.phone
|
||||||
|
result = await api('POST', '/contact/sync', { contact })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, delete, sync' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/lists?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/lists/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const list = { name }
|
||||||
|
if (args['string-id']) list.stringid = args['string-id']
|
||||||
|
if (args['sender-url']) list.sender_url = args['sender-url']
|
||||||
|
if (args['sender-reminder']) list.sender_reminder = args['sender-reminder']
|
||||||
|
result = await api('POST', '/lists', { list })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/lists/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'subscribe': {
|
||||||
|
const listId = args['list-id'] || args.id
|
||||||
|
const contactId = args['contact-id']
|
||||||
|
if (!listId) { result = { error: '--list-id required' }; break }
|
||||||
|
if (!contactId) { result = { error: '--contact-id required' }; break }
|
||||||
|
result = await api('POST', '/contactLists', {
|
||||||
|
contactList: { list: listId, contact: contactId, status: 1 }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unsubscribe': {
|
||||||
|
const listId = args['list-id'] || args.id
|
||||||
|
const contactId = args['contact-id']
|
||||||
|
if (!listId) { result = { error: '--list-id required' }; break }
|
||||||
|
if (!contactId) { result = { error: '--contact-id required' }; break }
|
||||||
|
result = await api('POST', '/contactLists', {
|
||||||
|
contactList: { list: listId, contact: contactId, status: 2 }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown lists subcommand. Use: list, get, create, delete, subscribe, unsubscribe' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/campaigns?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deals':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
if (args.stage) params.set('filters[stage]', args.stage)
|
||||||
|
if (args.owner) params.set('filters[owner]', args.owner)
|
||||||
|
result = await api('GET', `/deals?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/deals/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const title = args.title
|
||||||
|
if (!title) { result = { error: '--title required' }; break }
|
||||||
|
const deal = { title }
|
||||||
|
if (args.value) deal.value = Number(args.value)
|
||||||
|
if (args.currency) deal.currency = args.currency
|
||||||
|
if (args.pipeline) deal.group = args.pipeline
|
||||||
|
if (args.stage) deal.stage = args.stage
|
||||||
|
if (args.owner) deal.owner = args.owner
|
||||||
|
if (args['contact-id']) deal.contact = args['contact-id']
|
||||||
|
result = await api('POST', '/deals', { deal })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const deal = {}
|
||||||
|
if (args.title) deal.title = args.title
|
||||||
|
if (args.value) deal.value = Number(args.value)
|
||||||
|
if (args.stage) deal.stage = args.stage
|
||||||
|
if (args.owner) deal.owner = args.owner
|
||||||
|
if (args.status) deal.status = Number(args.status)
|
||||||
|
result = await api('PUT', `/deals/${id}`, { deal })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/deals/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown deals subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'automations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/automations?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/automations/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add-contact': {
|
||||||
|
const automationId = args.id
|
||||||
|
const contactId = args['contact-id']
|
||||||
|
if (!automationId) { result = { error: '--id required (automation ID)' }; break }
|
||||||
|
if (!contactId) { result = { error: '--contact-id required (contact ID, not email)' }; break }
|
||||||
|
result = await api('POST', '/contactAutomations', {
|
||||||
|
contactAutomation: { contact: contactId, automation: automationId }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown automations subcommand. Use: list, get, add-contact' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tags':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
result = await api('GET', `/tags?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/tags/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
result = await api('POST', '/tags', {
|
||||||
|
tag: { tag: name, tagType: args.type || 'contact' }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/tags/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add-to-contact': {
|
||||||
|
const tagId = args['tag-id']
|
||||||
|
const contactId = args['contact-id']
|
||||||
|
if (!tagId) { result = { error: '--tag-id required' }; break }
|
||||||
|
if (!contactId) { result = { error: '--contact-id required' }; break }
|
||||||
|
result = await api('POST', '/contactTags', {
|
||||||
|
contactTag: { contact: contactId, tag: tagId }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove-from-contact': {
|
||||||
|
const contactTagId = args.id
|
||||||
|
if (!contactTagId) { result = { error: '--id required (contactTag ID)' }; break }
|
||||||
|
result = await api('DELETE', `/contactTags/${contactTagId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tags subcommand. Use: list, get, create, delete, add-to-contact, remove-from-contact' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pipelines':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/dealGroups?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/dealGroups/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown pipelines subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/webhooks?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/webhooks/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
const url = args.url
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
const events = args.events?.split(',') || ['subscribe']
|
||||||
|
const sources = args.sources?.split(',') || ['public', 'admin', 'api', 'system']
|
||||||
|
result = await api('POST', '/webhooks', {
|
||||||
|
webhook: { name, url, events, sources }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/webhooks/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'users':
|
||||||
|
switch (sub) {
|
||||||
|
case 'me':
|
||||||
|
result = await api('GET', '/users/me')
|
||||||
|
break
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/users')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown users subcommand. Use: me, list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
contacts: 'contacts [list | get --id <id> | create --email <email> | update --id <id> | delete --id <id> | sync --email <email>]',
|
||||||
|
lists: 'lists [list | get --id <id> | create --name <name> | delete --id <id> | subscribe --list-id <lid> --contact-id <cid> | unsubscribe --list-id <lid> --contact-id <cid>]',
|
||||||
|
campaigns: 'campaigns [list | get --id <id>]',
|
||||||
|
deals: 'deals [list | get --id <id> | create --title <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)
|
||||||
|
})
|
||||||
161
tools/clis/adobe-analytics.js
Executable file
161
tools/clis/adobe-analytics.js
Executable file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const ACCESS_TOKEN = process.env.ADOBE_ACCESS_TOKEN
|
||||||
|
const CLIENT_ID = process.env.ADOBE_CLIENT_ID
|
||||||
|
const COMPANY_ID = process.env.ADOBE_COMPANY_ID
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN || !CLIENT_ID || !COMPANY_ID) {
|
||||||
|
console.error(JSON.stringify({ error: 'ADOBE_ACCESS_TOKEN, ADOBE_CLIENT_ID, and ADOBE_COMPANY_ID environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL = `https://analytics.adobe.io/api/${COMPANY_ID}`
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'x-api-key': '***', 'x-proxy-global-company-id': COMPANY_ID, 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`,
|
||||||
|
'x-api-key': CLIENT_ID,
|
||||||
|
'x-proxy-global-company-id': COMPANY_ID,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'reportsuites':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/reportsuites')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reportsuites subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dimensions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args.rsid) { result = { error: '--rsid required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('rsid', args.rsid)
|
||||||
|
result = await api('GET', `/dimensions?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown dimensions subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'metrics':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args.rsid) { result = { error: '--rsid required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('rsid', args.rsid)
|
||||||
|
result = await api('GET', `/metrics?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown metrics subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'run': {
|
||||||
|
if (!args.rsid) { result = { error: '--rsid required' }; break }
|
||||||
|
if (!args['start-date']) { result = { error: '--start-date required' }; break }
|
||||||
|
if (!args['end-date']) { result = { error: '--end-date required' }; break }
|
||||||
|
if (!args.metrics) { result = { error: '--metrics required (comma-separated)' }; break }
|
||||||
|
const body = {
|
||||||
|
rsid: args.rsid,
|
||||||
|
globalFilters: [{
|
||||||
|
type: 'dateRange',
|
||||||
|
dateRange: `${args['start-date']}T00:00:00/${args['end-date']}T23:59:59`,
|
||||||
|
}],
|
||||||
|
metricContainer: {
|
||||||
|
metrics: args.metrics.split(',').map(m => ({ id: m.trim() })),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (args.dimension) {
|
||||||
|
body.dimension = args.dimension
|
||||||
|
}
|
||||||
|
result = await api('POST', '/reports', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reports subcommand. Use: run' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'segments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.rsid) params.set('rsid', args.rsid)
|
||||||
|
result = await api('GET', `/segments?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown segments subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
reportsuites: 'reportsuites list',
|
||||||
|
dimensions: 'dimensions list --rsid <report_suite_id>',
|
||||||
|
metrics: 'metrics list --rsid <report_suite_id>',
|
||||||
|
reports: 'reports run --rsid <report_suite_id> --start-date <YYYY-MM-DD> --end-date <YYYY-MM-DD> --metrics <metrics> [--dimension <dimension>]',
|
||||||
|
segments: 'segments list [--rsid <report_suite_id>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
192
tools/clis/ahrefs.js
Executable file
192
tools/clis/ahrefs.js
Executable file
|
|
@ -0,0 +1,192 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.AHREFS_API_KEY
|
||||||
|
const BASE_URL = 'https://api.ahrefs.com/v3'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'AHREFS_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' } }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const mode = args.mode || 'domain'
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'domain-rating':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
if (!args.target) { result = { error: '--target required (domain)' }; break }
|
||||||
|
const params = new URLSearchParams({ target: args.target })
|
||||||
|
result = await api('GET', `/site-explorer/domain-rating?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domain-rating subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'backlinks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args.target) { result = { error: '--target required (domain or URL)' }; break }
|
||||||
|
const params = new URLSearchParams({ target: args.target, mode })
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/site-explorer/backlinks?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown backlinks subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'refdomains':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args.target) { result = { error: '--target required (domain or URL)' }; break }
|
||||||
|
const params = new URLSearchParams({ target: args.target, mode })
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/site-explorer/refdomains?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown refdomains subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keywords':
|
||||||
|
switch (sub) {
|
||||||
|
case 'organic': {
|
||||||
|
if (!args.target) { result = { error: '--target required (domain or URL)' }; break }
|
||||||
|
const params = new URLSearchParams({ target: args.target, mode })
|
||||||
|
if (args.country) params.set('country', args.country)
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/site-explorer/organic-keywords?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keywords subcommand. Use: organic' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'top-pages':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args.target) { result = { error: '--target required (domain or URL)' }; break }
|
||||||
|
const params = new URLSearchParams({ target: args.target, mode })
|
||||||
|
if (args.country) params.set('country', args.country)
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/site-explorer/top-pages?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown top-pages subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keyword-overview':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams({ keywords: args.keywords })
|
||||||
|
if (args.country) params.set('country', args.country)
|
||||||
|
result = await api('GET', `/keywords-explorer/overview?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keyword-overview subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keyword-suggestions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams({ keyword: args.keyword })
|
||||||
|
if (args.country) params.set('country', args.country)
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/keywords-explorer/matching-terms?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keyword-suggestions subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'serp':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams({ keyword: args.keyword })
|
||||||
|
if (args.country) params.set('country', args.country)
|
||||||
|
result = await api('GET', `/keywords-explorer/serp-overview?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown serp subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
'domain-rating': 'domain-rating get --target <domain>',
|
||||||
|
'backlinks': 'backlinks list --target <domain> [--mode <mode>] [--limit <n>]',
|
||||||
|
'refdomains': 'refdomains list --target <domain> [--mode <mode>] [--limit <n>]',
|
||||||
|
'keywords': 'keywords organic --target <domain> [--country <cc>] [--limit <n>]',
|
||||||
|
'top-pages': 'top-pages list --target <domain> [--country <cc>] [--limit <n>]',
|
||||||
|
'keyword-overview': 'keyword-overview get --keywords <kw1,kw2> [--country <cc>]',
|
||||||
|
'keyword-suggestions': 'keyword-suggestions get --keyword <keyword> [--country <cc>] [--limit <n>]',
|
||||||
|
'serp': 'serp get --keyword <keyword> [--country <cc>]',
|
||||||
|
'modes': 'domain (default), subdomains, prefix, exact',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
182
tools/clis/amplitude.js
Executable file
182
tools/clis/amplitude.js
Executable file
|
|
@ -0,0 +1,182 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.AMPLITUDE_API_KEY
|
||||||
|
const SECRET_KEY = process.env.AMPLITUDE_SECRET_KEY
|
||||||
|
const INGESTION_URL = 'https://api2.amplitude.com'
|
||||||
|
const QUERY_URL = 'https://amplitude.com/api/2'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'AMPLITUDE_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ingestApi(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined
|
||||||
|
if (maskedBody && maskedBody.api_key) maskedBody.api_key = '***'
|
||||||
|
return { _dry_run: true, method, url: `${INGESTION_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: maskedBody }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${INGESTION_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryApi(method, path, params) {
|
||||||
|
if (!SECRET_KEY) {
|
||||||
|
return { error: 'AMPLITUDE_SECRET_KEY required for query/export operations' }
|
||||||
|
}
|
||||||
|
const url = params ? `${QUERY_URL}${path}?${params}` : `${QUERY_URL}${path}`
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url, headers: { 'Authorization': '***', 'Content-Type': 'application/json' } }
|
||||||
|
}
|
||||||
|
const auth = Buffer.from(`${API_KEY}:${SECRET_KEY}`).toString('base64')
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'track':
|
||||||
|
switch (sub) {
|
||||||
|
case 'event': {
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
if (!args['event-type']) { result = { error: '--event-type required' }; break }
|
||||||
|
const event = {
|
||||||
|
user_id: args['user-id'],
|
||||||
|
event_type: args['event-type'],
|
||||||
|
}
|
||||||
|
if (args.properties) {
|
||||||
|
event.event_properties = JSON.parse(args.properties)
|
||||||
|
}
|
||||||
|
result = await ingestApi('POST', '/2/httpapi', {
|
||||||
|
api_key: API_KEY,
|
||||||
|
events: [event],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'batch': {
|
||||||
|
if (!args.events) { result = { error: '--events required (JSON array)' }; break }
|
||||||
|
const events = JSON.parse(args.events)
|
||||||
|
result = await ingestApi('POST', '/batch', {
|
||||||
|
api_key: API_KEY,
|
||||||
|
events,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown track subcommand. Use: event, batch' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'users':
|
||||||
|
switch (sub) {
|
||||||
|
case 'activity': {
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('user', args['user-id'])
|
||||||
|
result = await queryApi('GET', '/useractivity', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown users subcommand. Use: activity' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'export':
|
||||||
|
switch (sub) {
|
||||||
|
case 'events': {
|
||||||
|
if (!args.start) { result = { error: '--start required (e.g. 20240101T00)' }; break }
|
||||||
|
if (!args.end) { result = { error: '--end required (e.g. 20240131T23)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('start', args.start)
|
||||||
|
params.set('end', args.end)
|
||||||
|
result = await queryApi('GET', '/export', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown export subcommand. Use: events' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'retention':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
if (!args.start) { result = { error: '--start required (e.g. 20240101)' }; break }
|
||||||
|
if (!args.end) { result = { error: '--end required (e.g. 20240131)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('start', args.start)
|
||||||
|
params.set('end', args.end)
|
||||||
|
if (args.event) {
|
||||||
|
params.set('e', JSON.stringify([{ event_type: args.event }]))
|
||||||
|
}
|
||||||
|
result = await queryApi('GET', '/retention', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown retention subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
track: 'track [event --user-id <id> --event-type <type> [--properties <json>] | batch --events <json>]',
|
||||||
|
users: 'users activity --user-id <id>',
|
||||||
|
export: 'export events --start <YYYYMMDDThh> --end <YYYYMMDDThh>',
|
||||||
|
retention: 'retention get --start <YYYYMMDD> --end <YYYYMMDD> [--event <type>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
142
tools/clis/apollo.js
Executable file
142
tools/clis/apollo.js
Executable file
|
|
@ -0,0 +1,142 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.APOLLO_API_KEY
|
||||||
|
const BASE_URL = 'https://api.apollo.io/api/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'APOLLO_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const authBody = body ? { ...body, api_key: API_KEY } : { api_key: API_KEY }
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: { ...authBody, api_key: '***' } }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(authBody),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const page = args.page ? Number(args.page) : 1
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : 25
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'people':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const body = { page, per_page: perPage }
|
||||||
|
if (args.titles) body.person_titles = args.titles.split(',')
|
||||||
|
if (args.locations) body.person_locations = args.locations.split(',')
|
||||||
|
if (args.seniorities) body.person_seniorities = args.seniorities.split(',')
|
||||||
|
if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim())
|
||||||
|
if (args.keywords) body.q_keywords = args.keywords
|
||||||
|
result = await api('POST', '/mixed_people/search', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'enrich': {
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args.domain) body.domain = args.domain
|
||||||
|
if (args.linkedin) body.linkedin_url = args.linkedin
|
||||||
|
if (!args.email && !args.linkedin && !(args['first-name'] && args.domain)) {
|
||||||
|
result = { error: '--email, --linkedin, or --first-name + --domain required' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = await api('POST', '/people/match', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'bulk-enrich': {
|
||||||
|
const emails = args.emails?.split(',')
|
||||||
|
if (!emails) { result = { error: '--emails required (comma-separated)' }; break }
|
||||||
|
const details = emails.map(email => ({ email: email.trim() }))
|
||||||
|
result = await api('POST', '/people/bulk_match', { details })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown people subcommand. Use: search, enrich, bulk-enrich' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'organizations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const body = { page, per_page: perPage }
|
||||||
|
if (args.locations) body.organization_locations = args.locations.split(',')
|
||||||
|
if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim())
|
||||||
|
if (args.keywords) body.q_keywords = args.keywords
|
||||||
|
result = await api('POST', '/mixed_companies/search', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'enrich': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/organizations/enrich', { domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown organizations subcommand. Use: search, enrich' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
people: {
|
||||||
|
search: 'people search [--titles <t1,t2>] [--locations <l1,l2>] [--seniorities <s1,s2>] [--employee-ranges <1,100>] [--keywords <kw>] [--page <n>]',
|
||||||
|
enrich: 'people enrich --email <email> | --first-name <name> --last-name <name> --domain <domain> | --linkedin <url>',
|
||||||
|
'bulk-enrich': 'people bulk-enrich --emails <e1,e2,e3>',
|
||||||
|
},
|
||||||
|
organizations: {
|
||||||
|
search: 'organizations search [--locations <l1,l2>] [--employee-ranges <1,100>] [--keywords <kw>] [--page <n>]',
|
||||||
|
enrich: 'organizations enrich --domain <domain>',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
245
tools/clis/beehiiv.js
Executable file
245
tools/clis/beehiiv.js
Executable file
|
|
@ -0,0 +1,245 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.BEEHIIV_API_KEY
|
||||||
|
const BASE_URL = 'https://api.beehiiv.com/v2'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'BEEHIIV_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const pubId = args.publication || args.pub
|
||||||
|
const limit = args.limit ? Number(args.limit) : 10
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'publications':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/publications')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown publications subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'subscriptions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
if (args.email) params.set('email', args.email)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
if (args.tier) params.set('tier', args.tier)
|
||||||
|
if (args.cursor) params.set('cursor', args.cursor)
|
||||||
|
if (args.expand) params.set('expand[]', args.expand)
|
||||||
|
result = await api('GET', `/publications/${pubId}/subscriptions?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const subId = args.id
|
||||||
|
if (!subId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/subscriptions/${subId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email }
|
||||||
|
if (args['reactivate-existing']) body.reactivate_existing = true
|
||||||
|
if (args['send-welcome-email']) body.send_welcome_email = true
|
||||||
|
if (args['utm-source']) body.utm_source = args['utm-source']
|
||||||
|
if (args['utm-medium']) body.utm_medium = args['utm-medium']
|
||||||
|
if (args['utm-campaign']) body.utm_campaign = args['utm-campaign']
|
||||||
|
if (args.tier) body.tier = args.tier
|
||||||
|
if (args['referring-site']) body.referring_site = args['referring-site']
|
||||||
|
result = await api('POST', `/publications/${pubId}/subscriptions`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const subId = args.id
|
||||||
|
if (!subId) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.tier) body.tier = args.tier
|
||||||
|
result = await api('PUT', `/publications/${pubId}/subscriptions/${subId}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const subId = args.id
|
||||||
|
if (!subId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/publications/${pubId}/subscriptions/${subId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown subscriptions subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'posts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
if (args.cursor) params.set('cursor', args.cursor)
|
||||||
|
result = await api('GET', `/publications/${pubId}/posts?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const postId = args.id
|
||||||
|
if (!postId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/posts/${postId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const title = args.title
|
||||||
|
if (!title) { result = { error: '--title required' }; break }
|
||||||
|
const body = { title }
|
||||||
|
if (args.subtitle) body.subtitle = args.subtitle
|
||||||
|
if (args.content) body.content = args.content
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
result = await api('POST', `/publications/${pubId}/posts`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const postId = args.id
|
||||||
|
if (!postId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/publications/${pubId}/posts/${postId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown posts subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'segments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/segments`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const segId = args.id
|
||||||
|
if (!segId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/segments/${segId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown segments subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'automations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/automations`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
const autoId = args.id
|
||||||
|
if (!autoId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/automations/${autoId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown automations subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'referral-program':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
if (!pubId) { result = { error: '--publication required' }; break }
|
||||||
|
result = await api('GET', `/publications/${pubId}/referral_program`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown referral-program subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
publications: 'publications [list | get --publication <id>]',
|
||||||
|
subscriptions: 'subscriptions [list | get --id <id> | create --email <email> | update --id <id> | delete --id <id>] --publication <id>',
|
||||||
|
posts: 'posts [list | get --id <id> | create --title <title> | delete --id <id>] --publication <id>',
|
||||||
|
segments: 'segments [list | get --id <id>] --publication <id>',
|
||||||
|
automations: 'automations [list | get --id <id>] --publication <id>',
|
||||||
|
'referral-program': 'referral-program [get] --publication <id>',
|
||||||
|
options: '--publication <id> --limit <n> --email <email> --status <status> --tier <tier>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
368
tools/clis/brevo.js
Executable file
368
tools/clis/brevo.js
Executable file
|
|
@ -0,0 +1,368 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.BREVO_API_KEY
|
||||||
|
const BASE_URL = 'https://api.brevo.com/v3'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'BREVO_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'api-key': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'api-key': API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const limit = args.limit ? Number(args.limit) : 50
|
||||||
|
const offset = args.offset ? Number(args.offset) : 0
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'account':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', '/account')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown account subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'contacts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
result = await api('GET', `/contacts?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id || args.email
|
||||||
|
if (!id) { result = { error: '--id or --email required' }; break }
|
||||||
|
result = await api('GET', `/contacts/${encodeURIComponent(id)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email }
|
||||||
|
if (args['first-name'] || args['last-name']) {
|
||||||
|
body.attributes = {}
|
||||||
|
if (args['first-name']) body.attributes.FIRSTNAME = args['first-name']
|
||||||
|
if (args['last-name']) body.attributes.LASTNAME = args['last-name']
|
||||||
|
}
|
||||||
|
if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number)
|
||||||
|
result = await api('POST', '/contacts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id || args.email
|
||||||
|
if (!id) { result = { error: '--id or --email required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['first-name'] || args['last-name']) {
|
||||||
|
body.attributes = {}
|
||||||
|
if (args['first-name']) body.attributes.FIRSTNAME = args['first-name']
|
||||||
|
if (args['last-name']) body.attributes.LASTNAME = args['last-name']
|
||||||
|
}
|
||||||
|
if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number)
|
||||||
|
if (args['unlink-list-ids']) body.unlinkListIds = args['unlink-list-ids'].split(',').map(Number)
|
||||||
|
result = await api('PUT', `/contacts/${encodeURIComponent(id)}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id || args.email
|
||||||
|
if (!id) { result = { error: '--id or --email required' }; break }
|
||||||
|
result = await api('DELETE', `/contacts/${encodeURIComponent(id)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'import': {
|
||||||
|
const emails = args.emails?.split(',')
|
||||||
|
if (!emails) { result = { error: '--emails required (comma-separated)' }; break }
|
||||||
|
const body = {
|
||||||
|
jsonBody: emails.map(e => ({ email: e.trim() })),
|
||||||
|
}
|
||||||
|
if (args['list-ids']) body.listIds = args['list-ids'].split(',').map(Number)
|
||||||
|
result = await api('POST', '/contacts/import', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, delete, import' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
result = await api('GET', `/contacts/lists?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/contacts/lists/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { name, folderId: args.folder ? Number(args.folder) : 1 }
|
||||||
|
result = await api('POST', '/contacts/lists', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.folder) body.folderId = Number(args.folder)
|
||||||
|
result = await api('PUT', `/contacts/lists/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/contacts/lists/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'contacts': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
result = await api('GET', `/contacts/lists/${id}/contacts?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add-contacts': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const emails = args.emails?.split(',')
|
||||||
|
if (!emails) { result = { error: '--emails required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', `/contacts/lists/${id}/contacts/add`, { emails })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove-contacts': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const emails = args.emails?.split(',')
|
||||||
|
if (!emails) { result = { error: '--emails required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', `/contacts/lists/${id}/contacts/remove`, { emails })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown lists subcommand. Use: list, get, create, update, delete, contacts, add-contacts, remove-contacts' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
const senderEmail = args.from
|
||||||
|
const to = args.to
|
||||||
|
const subject = args.subject
|
||||||
|
if (!senderEmail) { result = { error: '--from required' }; break }
|
||||||
|
if (!to) { result = { error: '--to required' }; break }
|
||||||
|
if (!subject) { result = { error: '--subject required' }; break }
|
||||||
|
const body = {
|
||||||
|
sender: { email: senderEmail },
|
||||||
|
to: to.split(',').map(e => ({ email: e.trim() })),
|
||||||
|
subject,
|
||||||
|
}
|
||||||
|
if (args['sender-name']) body.sender.name = args['sender-name']
|
||||||
|
if (args.html) body.htmlContent = args.html
|
||||||
|
if (args.text) body.textContent = args.text
|
||||||
|
if (!args.html && !args.text) body.textContent = ''
|
||||||
|
if (args['reply-to']) body.replyTo = { email: args['reply-to'] }
|
||||||
|
if (args.tags) body.tags = args.tags.split(',')
|
||||||
|
result = await api('POST', '/smtp/email', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown email subcommand. Use: send' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
result = await api('GET', `/emailCampaigns?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/emailCampaigns/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = {
|
||||||
|
name,
|
||||||
|
sender: { email: args.from || '' },
|
||||||
|
subject: args.subject || '',
|
||||||
|
}
|
||||||
|
if (args['sender-name']) body.sender.name = args['sender-name']
|
||||||
|
if (args.html) body.htmlContent = args.html
|
||||||
|
if (args['list-ids']) body.recipients = { listIds: args['list-ids'].split(',').map(Number) }
|
||||||
|
result = await api('POST', '/emailCampaigns', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.subject) body.subject = args.subject
|
||||||
|
if (args.html) body.htmlContent = args.html
|
||||||
|
result = await api('PUT', `/emailCampaigns/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/emailCampaigns/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send-now': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/emailCampaigns/${id}/sendNow`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send-test': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const emails = args.emails?.split(',')
|
||||||
|
if (!emails) { result = { error: '--emails required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', `/emailCampaigns/${id}/sendTest`, { emailTo: emails })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, create, update, delete, send-now, send-test' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'sms':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
const sender = args.from
|
||||||
|
const recipient = args.to
|
||||||
|
const content = args.content
|
||||||
|
if (!sender) { result = { error: '--from required (sender name)' }; break }
|
||||||
|
if (!recipient) { result = { error: '--to required (phone number)' }; break }
|
||||||
|
if (!content) { result = { error: '--content required' }; break }
|
||||||
|
result = await api('POST', '/transactionalSMS/sms', {
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
content,
|
||||||
|
type: args.type || 'transactional',
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'campaigns': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/smsCampaigns?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sms subcommand. Use: send, campaigns' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'senders':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/senders')
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
const email = args.email
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('POST', '/senders', { name, email })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown senders subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
account: 'account [get]',
|
||||||
|
contacts: 'contacts [list | get --email <email> | create --email <email> | update --email <email> | delete --email <email> | import --emails <e1,e2>]',
|
||||||
|
lists: 'lists [list | get --id <id> | create --name <name> | delete --id <id> | contacts --id <id> | add-contacts --id <id> --emails <e1,e2> | remove-contacts --id <id> --emails <e1,e2>]',
|
||||||
|
email: 'email [send --from <from> --to <to> --subject <subj>]',
|
||||||
|
campaigns: 'campaigns [list | get --id <id> | create --name <name> | send-now --id <id> | send-test --id <id> --emails <e1,e2>]',
|
||||||
|
sms: 'sms [send --from <name> --to <phone> --content <msg> | campaigns]',
|
||||||
|
senders: 'senders [list | create --name <name> --email <email>]',
|
||||||
|
options: '--limit <n> --offset <n> --status <status>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
260
tools/clis/buffer.js
Executable file
260
tools/clis/buffer.js
Executable file
|
|
@ -0,0 +1,260 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.BUFFER_API_KEY
|
||||||
|
const BASE_URL = 'https://api.bufferapp.com/1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'BUFFER_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (body && method !== 'GET') {
|
||||||
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, 'Authorization': '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? new URLSearchParams(body).toString() : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiJson(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'user':
|
||||||
|
switch (sub) {
|
||||||
|
case 'info':
|
||||||
|
result = await api('GET', '/user.json')
|
||||||
|
break
|
||||||
|
case 'deauthorize':
|
||||||
|
result = await api('POST', '/user/deauthorize.json')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown user subcommand. Use: info, deauthorize' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'profiles':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/profiles.json')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/profiles/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'schedules': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (profile ID)' }; break }
|
||||||
|
result = await api('GET', `/profiles/${id}/schedules.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown profiles subcommand. Use: list, get, schedules' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'updates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (update ID)' }; break }
|
||||||
|
result = await api('GET', `/updates/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pending': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (profile ID)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.since) params.set('since', args.since)
|
||||||
|
const qs = params.toString() ? `?${params.toString()}` : ''
|
||||||
|
result = await api('GET', `/profiles/${id}/updates/pending.json${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'sent': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (profile ID)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.since) params.set('since', args.since)
|
||||||
|
const qs = params.toString() ? `?${params.toString()}` : ''
|
||||||
|
result = await api('GET', `/profiles/${id}/updates/sent.json${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const profileIds = args['profile-ids']
|
||||||
|
const text = args.text
|
||||||
|
if (!profileIds) { result = { error: '--profile-ids required (comma-separated)' }; break }
|
||||||
|
if (!text) { result = { error: '--text required' }; break }
|
||||||
|
const body = { text }
|
||||||
|
profileIds.split(',').forEach(id => {
|
||||||
|
if (!body['profile_ids[]']) body['profile_ids[]'] = []
|
||||||
|
})
|
||||||
|
const formBody = new URLSearchParams()
|
||||||
|
formBody.append('text', text)
|
||||||
|
profileIds.split(',').forEach(id => formBody.append('profile_ids[]', id.trim()))
|
||||||
|
if (args['scheduled-at']) formBody.append('scheduled_at', args['scheduled-at'])
|
||||||
|
if (args.now) formBody.append('now', 'true')
|
||||||
|
if (args.top) formBody.append('top', 'true')
|
||||||
|
if (args.shorten) formBody.append('shorten', 'true')
|
||||||
|
if (args['dry-run']) {
|
||||||
|
result = { _dry_run: true, method: 'POST', url: `${BASE_URL}/updates/create.json`, headers: { 'Authorization': '***', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: formBody.toString() }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}/updates/create.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formBody.toString(),
|
||||||
|
})
|
||||||
|
const resText = await res.text()
|
||||||
|
try { result = JSON.parse(resText) } catch { result = { status: res.status, body: resText } }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
const text = args.text
|
||||||
|
if (!id) { result = { error: '--id required (update ID)' }; break }
|
||||||
|
if (!text) { result = { error: '--text required' }; break }
|
||||||
|
const body = { text }
|
||||||
|
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
|
||||||
|
result = await api('POST', `/updates/${id}/update.json`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'share': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (update ID)' }; break }
|
||||||
|
result = await api('POST', `/updates/${id}/share.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'destroy': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (update ID)' }; break }
|
||||||
|
result = await api('POST', `/updates/${id}/destroy.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'reorder': {
|
||||||
|
const id = args.id
|
||||||
|
const order = args.order
|
||||||
|
if (!id) { result = { error: '--id required (profile ID)' }; break }
|
||||||
|
if (!order) { result = { error: '--order required (comma-separated update IDs)' }; break }
|
||||||
|
const formBody = new URLSearchParams()
|
||||||
|
order.split(',').forEach(uid => formBody.append('order[]', uid.trim()))
|
||||||
|
if (args['dry-run']) {
|
||||||
|
result = { _dry_run: true, method: 'POST', url: `${BASE_URL}/profiles/${id}/updates/reorder.json`, headers: { 'Authorization': '***', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: formBody.toString() }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}/profiles/${id}/updates/reorder.json`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: formBody.toString(),
|
||||||
|
})
|
||||||
|
const resText = await res.text()
|
||||||
|
try { result = JSON.parse(resText) } catch { result = { status: res.status, body: resText } }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shuffle': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (profile ID)' }; break }
|
||||||
|
result = await api('POST', `/profiles/${id}/updates/shuffle.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown updates subcommand. Use: get, pending, sent, create, update, share, destroy, reorder, shuffle' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'info':
|
||||||
|
result = await api('GET', '/info/configuration.json')
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
user: 'user [info | deauthorize]',
|
||||||
|
profiles: 'profiles [list | get --id <id> | schedules --id <id>]',
|
||||||
|
updates: 'updates [get --id <id> | pending --id <profile-id> | sent --id <profile-id> | create --profile-ids <ids> --text <text> [--scheduled-at <time>] [--now] | update --id <id> --text <text> | share --id <id> | destroy --id <id> | reorder --id <profile-id> --order <id1,id2> | shuffle --id <profile-id>]',
|
||||||
|
info: 'info',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
253
tools/clis/calendly.js
Executable file
253
tools/clis/calendly.js
Executable file
|
|
@ -0,0 +1,253 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.CALENDLY_API_KEY
|
||||||
|
const BASE_URL = 'https://api.calendly.com'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'CALENDLY_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const count = args.count ? Number(args.count) : 20
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'users':
|
||||||
|
switch (sub) {
|
||||||
|
case 'me':
|
||||||
|
result = await api('GET', '/users/me')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown users subcommand. Use: me' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'event-types':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const user = args.user
|
||||||
|
const org = args.organization
|
||||||
|
if (!user && !org) { result = { error: '--user or --organization URI required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (user) params.set('user', user)
|
||||||
|
if (org) params.set('organization', org)
|
||||||
|
if (args.active) params.set('active', args.active)
|
||||||
|
params.set('count', String(count))
|
||||||
|
if (args['page-token']) params.set('page_token', args['page-token'])
|
||||||
|
result = await api('GET', `/event_types?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const uuid = args.uuid
|
||||||
|
if (!uuid) { result = { error: '--uuid required' }; break }
|
||||||
|
result = await api('GET', `/event_types/${uuid}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown event-types subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const user = args.user
|
||||||
|
const org = args.organization
|
||||||
|
if (!user && !org) { result = { error: '--user or --organization URI required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (user) params.set('user', user)
|
||||||
|
if (org) params.set('organization', org)
|
||||||
|
if (args['min-start']) params.set('min_start_time', args['min-start'])
|
||||||
|
if (args['max-start']) params.set('max_start_time', args['max-start'])
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
params.set('count', String(count))
|
||||||
|
if (args['page-token']) params.set('page_token', args['page-token'])
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
result = await api('GET', `/scheduled_events?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const uuid = args.uuid
|
||||||
|
if (!uuid) { result = { error: '--uuid required' }; break }
|
||||||
|
result = await api('GET', `/scheduled_events/${uuid}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'cancel': {
|
||||||
|
const uuid = args.uuid
|
||||||
|
if (!uuid) { result = { error: '--uuid required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.reason) body.reason = args.reason
|
||||||
|
result = await api('POST', `/scheduled_events/${uuid}/cancellation`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'invitees': {
|
||||||
|
const uuid = args.uuid
|
||||||
|
if (!uuid) { result = { error: '--uuid required (event UUID)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('count', String(count))
|
||||||
|
if (args['page-token']) params.set('page_token', args['page-token'])
|
||||||
|
if (args.email) params.set('email', args.email)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/scheduled_events/${uuid}/invitees?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, get, cancel, invitees' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'availability':
|
||||||
|
switch (sub) {
|
||||||
|
case 'times': {
|
||||||
|
const eventType = args['event-type']
|
||||||
|
if (!eventType) { result = { error: '--event-type URI required' }; break }
|
||||||
|
const startTime = args['start-time']
|
||||||
|
const endTime = args['end-time']
|
||||||
|
if (!startTime || !endTime) { result = { error: '--start-time and --end-time required (ISO 8601)' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
event_type: eventType,
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
})
|
||||||
|
result = await api('GET', `/event_type_available_times?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'busy': {
|
||||||
|
const user = args.user
|
||||||
|
if (!user) { result = { error: '--user URI required' }; break }
|
||||||
|
const startTime = args['start-time']
|
||||||
|
const endTime = args['end-time']
|
||||||
|
if (!startTime || !endTime) { result = { error: '--start-time and --end-time required (ISO 8601)' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
user,
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
})
|
||||||
|
result = await api('GET', `/user_busy_times?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown availability subcommand. Use: times, busy' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const org = args.organization
|
||||||
|
const scope = args.scope || 'organization'
|
||||||
|
if (!org) { result = { error: '--organization URI required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
organization: org,
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
params.set('count', String(count))
|
||||||
|
if (args['page-token']) params.set('page_token', args['page-token'])
|
||||||
|
result = await api('GET', `/webhook_subscriptions?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const url = args.url
|
||||||
|
const events = args.events?.split(',')
|
||||||
|
const org = args.organization
|
||||||
|
const scope = args.scope || 'organization'
|
||||||
|
if (!url || !events || !org) { result = { error: '--url, --events (comma-separated), and --organization required' }; break }
|
||||||
|
const body = { url, events, organization: org, scope }
|
||||||
|
if (args.user) body.user = args.user
|
||||||
|
result = await api('POST', '/webhook_subscriptions', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const uuid = args.uuid
|
||||||
|
if (!uuid) { result = { error: '--uuid required' }; break }
|
||||||
|
result = await api('DELETE', `/webhook_subscriptions/${uuid}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'org':
|
||||||
|
switch (sub) {
|
||||||
|
case 'members': {
|
||||||
|
const org = args.organization
|
||||||
|
if (!org) { result = { error: '--organization URI required' }; break }
|
||||||
|
const params = new URLSearchParams({ organization: org })
|
||||||
|
params.set('count', String(count))
|
||||||
|
if (args['page-token']) params.set('page_token', args['page-token'])
|
||||||
|
result = await api('GET', `/organization_memberships?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown org subcommand. Use: members' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
users: 'users me',
|
||||||
|
'event-types': 'event-types [list --user <uri> | get --uuid <id>]',
|
||||||
|
events: 'events [list --user <uri> | get --uuid <id> | cancel --uuid <id> | invitees --uuid <id>]',
|
||||||
|
availability: 'availability [times --event-type <uri> --start-time <iso> --end-time <iso> | busy --user <uri> --start-time <iso> --end-time <iso>]',
|
||||||
|
webhooks: 'webhooks [list --organization <uri> | create --url <url> --events <e1,e2> --organization <uri> | delete --uuid <id>]',
|
||||||
|
org: 'org [members --organization <uri>]',
|
||||||
|
options: '--count <n> --page-token <token> --status <active|canceled>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
163
tools/clis/clearbit.js
Executable file
163
tools/clis/clearbit.js
Executable file
|
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.CLEARBIT_API_KEY
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'CLEARBIT_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, baseUrl, path, body) {
|
||||||
|
const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64')
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${baseUrl}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${baseUrl}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'person':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('GET', 'https://person-stream.clearbit.com', `/v2/people/find?email=${encodeURIComponent(email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown person subcommand. Use: find' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'company':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('GET', 'https://company-stream.clearbit.com', `/v2/companies/find?domain=${encodeURIComponent(domain)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown company subcommand. Use: find' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'combined':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('GET', 'https://person-stream.clearbit.com', `/v2/combined/find?email=${encodeURIComponent(email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown combined subcommand. Use: find' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reveal':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const ip = args.ip
|
||||||
|
if (!ip) { result = { error: '--ip required' }; break }
|
||||||
|
result = await api('GET', 'https://reveal.clearbit.com', `/v1/companies/find?ip=${encodeURIComponent(ip)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reveal subcommand. Use: find' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'name-to-domain':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
result = await api('GET', 'https://company.clearbit.com', `/v1/domains/find?name=${encodeURIComponent(name)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown name-to-domain subcommand. Use: find' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'prospector':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({ domain })
|
||||||
|
if (args.role) params.set('role', args.role)
|
||||||
|
if (args.seniority) params.set('seniority', args.seniority)
|
||||||
|
if (args.title) params.set('title', args.title)
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
if (args['page-size']) params.set('page_size', args['page-size'])
|
||||||
|
result = await api('GET', 'https://prospector.clearbit.com', `/v1/people/search?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown prospector subcommand. Use: search' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
person: 'person find --email <email>',
|
||||||
|
company: 'company find --domain <domain>',
|
||||||
|
combined: 'combined find --email <email>',
|
||||||
|
reveal: 'reveal find --ip <ip>',
|
||||||
|
'name-to-domain': 'name-to-domain find --name <company_name>',
|
||||||
|
prospector: 'prospector search --domain <domain> [--role <role>] [--seniority <level>] [--title <title>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
205
tools/clis/customer-io.js
Executable file
205
tools/clis/customer-io.js
Executable file
|
|
@ -0,0 +1,205 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const APP_KEY = process.env.CUSTOMERIO_APP_KEY
|
||||||
|
const SITE_ID = process.env.CUSTOMERIO_SITE_ID
|
||||||
|
const API_KEY = process.env.CUSTOMERIO_API_KEY
|
||||||
|
|
||||||
|
const TRACK_URL = 'https://track.customer.io/api/v1'
|
||||||
|
const APP_URL = 'https://api.customer.io/v1'
|
||||||
|
|
||||||
|
const hasTrackAuth = SITE_ID && API_KEY
|
||||||
|
const hasAppAuth = APP_KEY
|
||||||
|
|
||||||
|
if (!hasTrackAuth && !hasAppAuth) {
|
||||||
|
console.error(JSON.stringify({ error: 'CUSTOMERIO_APP_KEY (for App API) or CUSTOMERIO_SITE_ID + CUSTOMERIO_API_KEY (for Track API) environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const basicAuth = hasTrackAuth ? Buffer.from(`${SITE_ID}:${API_KEY}`).toString('base64') : null
|
||||||
|
|
||||||
|
async function trackApi(method, path, body) {
|
||||||
|
if (!hasTrackAuth) {
|
||||||
|
return { error: 'Track API requires CUSTOMERIO_SITE_ID and CUSTOMERIO_API_KEY environment variables' }
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${TRACK_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${TRACK_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${basicAuth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function appApi(method, path, body) {
|
||||||
|
if (!hasAppAuth) {
|
||||||
|
return { error: 'App API requires CUSTOMERIO_APP_KEY environment variable' }
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${APP_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${APP_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${APP_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'customers':
|
||||||
|
switch (sub) {
|
||||||
|
case 'identify': {
|
||||||
|
const customerId = rest[0] || args.id
|
||||||
|
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args['created-at']) body.created_at = parseInt(args['created-at'])
|
||||||
|
if (args.plan) body.plan = args.plan
|
||||||
|
if (args.data) Object.assign(body, JSON.parse(args.data))
|
||||||
|
result = await trackApi('PUT', `/customers/${customerId}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const customerId = rest[0] || args.id
|
||||||
|
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
|
||||||
|
result = await appApi('GET', `/customers/${customerId}/attributes`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const customerId = rest[0] || args.id
|
||||||
|
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
|
||||||
|
result = await trackApi('DELETE', `/customers/${customerId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'track-event': {
|
||||||
|
const customerId = rest[0] || args.id
|
||||||
|
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
|
||||||
|
if (!args.name) { result = { error: '--name required (event name)' }; break }
|
||||||
|
const body = { name: args.name }
|
||||||
|
if (args.data) body.data = JSON.parse(args.data)
|
||||||
|
result = await trackApi('POST', `/customers/${customerId}/events`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown customers subcommand. Use: identify, get, delete, track-event' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await appApi('GET', '/campaigns')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const campaignId = rest[0] || args.id
|
||||||
|
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
|
||||||
|
result = await appApi('GET', `/campaigns/${campaignId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'metrics': {
|
||||||
|
const campaignId = rest[0] || args.id
|
||||||
|
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
|
||||||
|
result = await appApi('GET', `/campaigns/${campaignId}/metrics`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'trigger': {
|
||||||
|
const campaignId = rest[0] || args.id
|
||||||
|
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.emails) body.emails = args.emails.split(',')
|
||||||
|
if (args.ids) body.ids = args.ids.split(',')
|
||||||
|
if (args.data) body.data = JSON.parse(args.data)
|
||||||
|
result = await appApi('POST', `/campaigns/${campaignId}/triggers`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, metrics, trigger' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'send':
|
||||||
|
switch (sub) {
|
||||||
|
case 'email': {
|
||||||
|
const body = {
|
||||||
|
transactional_message_id: args['message-id'],
|
||||||
|
to: args.to,
|
||||||
|
identifiers: {},
|
||||||
|
}
|
||||||
|
if (args['identifier-id']) body.identifiers.id = args['identifier-id']
|
||||||
|
if (args['identifier-email']) body.identifiers.email = args['identifier-email']
|
||||||
|
if (args.data) body.message_data = JSON.parse(args.data)
|
||||||
|
if (args.from) body.from = args.from
|
||||||
|
if (args.subject) body.subject = args.subject
|
||||||
|
if (args.body) body.body = args.body
|
||||||
|
result = await appApi('POST', '/send/email', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown send subcommand. Use: email' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
customers: 'customers [identify|get|delete|track-event] <customer_id> [--email <email>] [--first-name <name>] [--plan <plan>] [--data <json>] [--name <event>]',
|
||||||
|
campaigns: 'campaigns [list|get|metrics|trigger] [campaign_id] [--emails <e1,e2>] [--ids <id1,id2>] [--data <json>]',
|
||||||
|
send: 'send email --message-id <id> --to <email> [--identifier-id <id>] [--identifier-email <email>] [--data <json>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
257
tools/clis/dataforseo.js
Executable file
257
tools/clis/dataforseo.js
Executable file
|
|
@ -0,0 +1,257 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const LOGIN = process.env.DATAFORSEO_LOGIN
|
||||||
|
const PASSWORD = process.env.DATAFORSEO_PASSWORD
|
||||||
|
const BASE_URL = 'https://api.dataforseo.com/v3'
|
||||||
|
|
||||||
|
if (!LOGIN || !PASSWORD) {
|
||||||
|
console.error(JSON.stringify({ error: 'DATAFORSEO_LOGIN and DATAFORSEO_PASSWORD environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH = 'Basic ' + Buffer.from(`${LOGIN}:${PASSWORD}`).toString('base64')
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': AUTH,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const location = args.location || 'United States'
|
||||||
|
const locationCode = args['location-code'] ? Number(args['location-code']) : 2840
|
||||||
|
const language = args.language || 'English'
|
||||||
|
const languageCode = args['language-code'] || 'en'
|
||||||
|
const limit = args.limit ? Number(args.limit) : 100
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'serp':
|
||||||
|
switch (sub) {
|
||||||
|
case 'google': {
|
||||||
|
const keyword = args.keyword
|
||||||
|
if (!keyword) { result = { error: '--keyword required' }; break }
|
||||||
|
result = await api('POST', '/serp/google/organic/live/regular', [{
|
||||||
|
keyword,
|
||||||
|
location_name: location,
|
||||||
|
language_name: language,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'locations':
|
||||||
|
result = await api('GET', '/serp/google/locations')
|
||||||
|
break
|
||||||
|
case 'languages':
|
||||||
|
result = await api('GET', '/serp/google/languages')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown serp subcommand. Use: google, locations, languages' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keywords':
|
||||||
|
switch (sub) {
|
||||||
|
case 'volume': {
|
||||||
|
const keywords = args.keywords?.split(',')
|
||||||
|
if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', '/keywords_data/google_ads/search_volume/live', [{
|
||||||
|
keywords,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'for-site': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required (domain)' }; break }
|
||||||
|
result = await api('POST', '/keywords_data/google_ads/keywords_for_site/live', [{
|
||||||
|
target,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'for-keywords': {
|
||||||
|
const keywords = args.keywords?.split(',')
|
||||||
|
if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', '/keywords_data/google_ads/keywords_for_keywords/live', [{
|
||||||
|
keywords,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'trends': {
|
||||||
|
const keywords = args.keywords?.split(',')
|
||||||
|
if (!keywords) { result = { error: '--keywords required (comma-separated)' }; break }
|
||||||
|
result = await api('POST', '/keywords_data/google_trends/explore/live', [{
|
||||||
|
keywords,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keywords subcommand. Use: volume, for-site, for-keywords, trends' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'backlinks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'summary': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/backlinks/summary/live', [{
|
||||||
|
target,
|
||||||
|
backlinks_status_type: 'live',
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/backlinks/backlinks/live', [{
|
||||||
|
target,
|
||||||
|
mode: args.mode || 'as_is',
|
||||||
|
limit,
|
||||||
|
backlinks_status_type: 'live',
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'refdomains': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/backlinks/referring_domains/live', [{
|
||||||
|
target,
|
||||||
|
limit,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'anchors': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/backlinks/anchors/live', [{
|
||||||
|
target,
|
||||||
|
limit,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'index':
|
||||||
|
result = await api('GET', '/backlinks/index')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown backlinks subcommand. Use: summary, list, refdomains, anchors, index' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'onpage':
|
||||||
|
switch (sub) {
|
||||||
|
case 'audit': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
result = await api('POST', '/on_page/instant_pages', [{
|
||||||
|
url,
|
||||||
|
enable_javascript: args['no-js'] ? false : true,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown onpage subcommand. Use: audit' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'labs':
|
||||||
|
switch (sub) {
|
||||||
|
case 'competitors': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/dataforseo_labs/google/competitors_domain/live', [{
|
||||||
|
target,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
limit,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ranked-keywords': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required' }; break }
|
||||||
|
result = await api('POST', '/dataforseo_labs/google/ranked_keywords/live', [{
|
||||||
|
target,
|
||||||
|
location_code: locationCode,
|
||||||
|
language_code: languageCode,
|
||||||
|
limit,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'domain-intersection': {
|
||||||
|
const targets = args.targets?.split(',')
|
||||||
|
if (!targets || targets.length < 2) { result = { error: '--targets required (comma-separated, at least 2 domains)' }; break }
|
||||||
|
const payload = { location_code: locationCode, language_code: languageCode, limit }
|
||||||
|
targets.forEach((t, i) => { payload[`target${i + 1}`] = t })
|
||||||
|
result = await api('POST', '/dataforseo_labs/google/domain_intersection/live', [payload])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown labs subcommand. Use: competitors, ranked-keywords, domain-intersection' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
serp: 'serp [google --keyword <kw> | locations | languages]',
|
||||||
|
keywords: 'keywords [volume --keywords <kw1,kw2> | for-site --target <domain> | for-keywords --keywords <kw1,kw2> | trends --keywords <kw1,kw2>]',
|
||||||
|
backlinks: 'backlinks [summary --target <domain> | list --target <domain> | refdomains --target <domain> | anchors --target <domain> | index]',
|
||||||
|
onpage: 'onpage [audit --url <url>]',
|
||||||
|
labs: 'labs [competitors --target <domain> | ranked-keywords --target <domain> | domain-intersection --targets <d1,d2>]',
|
||||||
|
options: '--location-code <code> --language-code <code> --limit <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
149
tools/clis/demio.js
Executable file
149
tools/clis/demio.js
Executable file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.DEMIO_API_KEY
|
||||||
|
const API_SECRET = process.env.DEMIO_API_SECRET
|
||||||
|
const BASE_URL = 'https://my.demio.com/api/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'DEMIO_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!API_SECRET) {
|
||||||
|
console.error(JSON.stringify({ error: 'DEMIO_API_SECRET environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Api-Key': '***', 'Api-Secret': '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Api-Key': API_KEY,
|
||||||
|
'Api-Secret': API_SECRET,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'ping':
|
||||||
|
result = await api('GET', '/ping')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const type = args.type
|
||||||
|
let qs = ''
|
||||||
|
if (type) qs = `?type=${encodeURIComponent(type)}`
|
||||||
|
result = await api('GET', `/events${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/event/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'date': {
|
||||||
|
const eventId = args['event-id']
|
||||||
|
const dateId = args['date-id']
|
||||||
|
if (!eventId) { result = { error: '--event-id required' }; break }
|
||||||
|
if (!dateId) { result = { error: '--date-id required' }; break }
|
||||||
|
result = await api('GET', `/event/${eventId}/date/${dateId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, get, date' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'register':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
const id = args.id || args['event-id']
|
||||||
|
const name = args.name
|
||||||
|
const email = args.email
|
||||||
|
if (!id) { result = { error: '--id (event id) required' }; break }
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const payload = { id, name, email }
|
||||||
|
if (args['date-id']) payload.date_id = args['date-id']
|
||||||
|
if (args['ref-url']) payload.ref_url = args['ref-url']
|
||||||
|
result = await api('POST', '/event/register', payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown register subcommand. Use: create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'participants':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const dateId = args['date-id']
|
||||||
|
if (!dateId) { result = { error: '--date-id required' }; break }
|
||||||
|
result = await api('GET', `/date/${dateId}/participants`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown participants subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
ping: 'ping',
|
||||||
|
events: 'events [list --type <upcoming|past|all> | get --id <id> | date --event-id <id> --date-id <id>]',
|
||||||
|
register: 'register [create --id <event_id> --name <name> --email <email> --date-id <date_id>]',
|
||||||
|
participants: 'participants [list --date-id <id>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
158
tools/clis/dub.js
Executable file
158
tools/clis/dub.js
Executable file
|
|
@ -0,0 +1,158 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.DUB_API_KEY
|
||||||
|
const BASE_URL = 'https://api.dub.co'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'DUB_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'links':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
if (!args.url) { result = { error: '--url required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.url) body.url = args.url
|
||||||
|
if (args.domain) body.domain = args.domain
|
||||||
|
if (args.key) body.key = args.key
|
||||||
|
if (args.tags) body.tags = args.tags.split(',')
|
||||||
|
result = await api('POST', '/links', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.domain) params.set('domain', args.domain)
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
result = await api('GET', `/links?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.domain) params.set('domain', args.domain)
|
||||||
|
if (args.key) params.set('key', args.key)
|
||||||
|
if (args['link-id']) params.set('linkId', args['link-id'])
|
||||||
|
if (args['external-id']) params.set('externalId', args['external-id'])
|
||||||
|
result = await api('GET', `/links/info?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!args.id) { result = { error: '--id required (link ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.url) body.url = args.url
|
||||||
|
if (args.tags) body.tags = args.tags.split(',')
|
||||||
|
result = await api('PATCH', `/links/${args.id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
if (!args.id) { result = { error: '--id required (link ID)' }; break }
|
||||||
|
result = await api('DELETE', `/links/${args.id}`)
|
||||||
|
break
|
||||||
|
case 'bulk-create': {
|
||||||
|
let links
|
||||||
|
try {
|
||||||
|
links = JSON.parse(args.links || '[]')
|
||||||
|
} catch {
|
||||||
|
result = { error: 'Invalid JSON in --links' }; break
|
||||||
|
}
|
||||||
|
result = await api('POST', '/links/bulk', links)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown links subcommand. Use: create, list, get, update, delete, bulk-create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'analytics':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.domain) params.set('domain', args.domain)
|
||||||
|
if (args.key) params.set('key', args.key)
|
||||||
|
if (args.interval) params.set('interval', args.interval)
|
||||||
|
result = await api('GET', `/analytics?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'country': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.domain) params.set('domain', args.domain)
|
||||||
|
if (args.key) params.set('key', args.key)
|
||||||
|
result = await api('GET', `/analytics/country?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'device': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.domain) params.set('domain', args.domain)
|
||||||
|
if (args.key) params.set('key', args.key)
|
||||||
|
result = await api('GET', `/analytics/device?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown analytics subcommand. Use: get, country, device' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
links: 'links [create|list|get|update|delete|bulk-create] [--url <url>] [--domain <domain>] [--key <key>] [--tags <tags>] [--id <id>] [--page <page>] [--links <json>]',
|
||||||
|
analytics: 'analytics [get|country|device] [--domain <domain>] [--key <key>] [--interval <interval>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
186
tools/clis/g2.js
Executable file
186
tools/clis/g2.js
Executable file
|
|
@ -0,0 +1,186 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_TOKEN = process.env.G2_API_TOKEN
|
||||||
|
const BASE_URL = 'https://data.g2.com/api/v1'
|
||||||
|
|
||||||
|
if (!API_TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'G2_API_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/vnd.api+json', Accept: 'application/vnd.api+json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Token token=${API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/vnd.api+json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const page = args.page ? Number(args.page) : 1
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : 25
|
||||||
|
const productId = args['product-id'] || args.product
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'reviews':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||||
|
if (productId) qs += `&filter[product_id]=${productId}`
|
||||||
|
if (args.state) qs += `&filter[state]=${encodeURIComponent(args.state)}`
|
||||||
|
result = await api('GET', `/survey-responses${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/survey-responses/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reviews subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'products':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||||
|
if (args.name) qs += `&filter[name]=${encodeURIComponent(args.name)}`
|
||||||
|
result = await api('GET', `/products${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/products/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown products subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||||
|
result = await api('GET', `/reports${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/reports/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reports subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'competitors':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!productId) { result = { error: '--product-id required' }; break }
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}&filter[product_id]=${productId}`
|
||||||
|
result = await api('GET', `/competitor-comparisons${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown competitors subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'categories':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||||
|
if (args.name) qs += `&filter[name]=${encodeURIComponent(args.name)}`
|
||||||
|
result = await api('GET', `/categories${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/categories/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown categories subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tracking':
|
||||||
|
switch (sub) {
|
||||||
|
case 'visitors': {
|
||||||
|
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||||
|
if (args.start) qs += `&filter[start_date]=${encodeURIComponent(args.start)}`
|
||||||
|
if (args.end) qs += `&filter[end_date]=${encodeURIComponent(args.end)}`
|
||||||
|
result = await api('GET', `/tracking-events${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tracking subcommand. Use: visitors' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
reviews: 'reviews [list --product-id <id> | get --id <id>]',
|
||||||
|
products: 'products [list --name <name> | get --id <id>]',
|
||||||
|
reports: 'reports [list | get --id <id>]',
|
||||||
|
competitors: 'competitors [list --product-id <id>]',
|
||||||
|
categories: 'categories [list --name <name> | get --id <id>]',
|
||||||
|
tracking: 'tracking [visitors --start <date> --end <date>]',
|
||||||
|
options: '--page <n> --per-page <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
194
tools/clis/ga4.js
Executable file
194
tools/clis/ga4.js
Executable file
|
|
@ -0,0 +1,194 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const ACCESS_TOKEN = process.env.GA4_ACCESS_TOKEN
|
||||||
|
const DATA_API = 'https://analyticsdata.googleapis.com/v1beta'
|
||||||
|
const ADMIN_API = 'https://analyticsadmin.googleapis.com/v1beta'
|
||||||
|
const MP_URL = 'https://www.google-analytics.com/mp/collect'
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'GA4_ACCESS_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, baseUrl, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${baseUrl}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${baseUrl}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mpApi(measurementId, apiSecret, body) {
|
||||||
|
const params = new URLSearchParams({ measurement_id: measurementId, api_secret: apiSecret })
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method: 'POST', url: `${MP_URL}?${new URLSearchParams({ measurement_id: measurementId, api_secret: '***' })}`, headers: { 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${MP_URL}?${params}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
if (!text) return { status: res.status, success: res.ok }
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'run': {
|
||||||
|
const property = args.property
|
||||||
|
if (!property) { result = { error: '--property required' }; break }
|
||||||
|
const body = {
|
||||||
|
dateRanges: [{
|
||||||
|
startDate: args['start-date'] || '30daysAgo',
|
||||||
|
endDate: args['end-date'] || 'today',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
if (args.dimensions) {
|
||||||
|
body.dimensions = args.dimensions.split(',').map(d => ({ name: d.trim() }))
|
||||||
|
}
|
||||||
|
if (args.metrics) {
|
||||||
|
body.metrics = args.metrics.split(',').map(m => ({ name: m.trim() }))
|
||||||
|
}
|
||||||
|
result = await api('POST', DATA_API, `/properties/${property}:runReport`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reports subcommand. Use: run' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'realtime':
|
||||||
|
switch (sub) {
|
||||||
|
case 'run': {
|
||||||
|
const property = args.property
|
||||||
|
if (!property) { result = { error: '--property required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.dimensions) {
|
||||||
|
body.dimensions = args.dimensions.split(',').map(d => ({ name: d.trim() }))
|
||||||
|
}
|
||||||
|
if (args.metrics) {
|
||||||
|
body.metrics = args.metrics.split(',').map(m => ({ name: m.trim() }))
|
||||||
|
}
|
||||||
|
result = await api('POST', DATA_API, `/properties/${property}:runRealtimeReport`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown realtime subcommand. Use: run' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'conversions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const property = args.property
|
||||||
|
if (!property) { result = { error: '--property required' }; break }
|
||||||
|
result = await api('GET', ADMIN_API, `/properties/${property}/conversionEvents`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const property = args.property
|
||||||
|
if (!property) { result = { error: '--property required' }; break }
|
||||||
|
if (!args['event-name']) { result = { error: '--event-name required' }; break }
|
||||||
|
result = await api('POST', ADMIN_API, `/properties/${property}/conversionEvents`, {
|
||||||
|
eventName: args['event-name'],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown conversions subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
if (!args['measurement-id']) { result = { error: '--measurement-id required' }; break }
|
||||||
|
if (!args['api-secret']) { result = { error: '--api-secret required' }; break }
|
||||||
|
if (!args['client-id']) { result = { error: '--client-id required' }; break }
|
||||||
|
if (!args['event-name']) { result = { error: '--event-name required' }; break }
|
||||||
|
let eventParams = {}
|
||||||
|
if (args.params) {
|
||||||
|
try {
|
||||||
|
eventParams = JSON.parse(args.params)
|
||||||
|
} catch {
|
||||||
|
result = { error: 'Invalid JSON in --params' }; break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
client_id: args['client-id'],
|
||||||
|
events: [{
|
||||||
|
name: args['event-name'],
|
||||||
|
params: eventParams,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
result = await mpApi(args['measurement-id'], args['api-secret'], body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: send' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
reports: 'reports run --property <id> [--start-date <date>] [--end-date <date>] [--dimensions <dims>] [--metrics <metrics>]',
|
||||||
|
realtime: 'realtime run --property <id> [--dimensions <dims>] [--metrics <metrics>]',
|
||||||
|
conversions: 'conversions [list|create] --property <id> [--event-name <name>]',
|
||||||
|
events: 'events send --measurement-id <id> --api-secret <secret> --client-id <id> --event-name <name> [--params <json>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
189
tools/clis/google-ads.js
Executable file
189
tools/clis/google-ads.js
Executable file
|
|
@ -0,0 +1,189 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const TOKEN = process.env.GOOGLE_ADS_TOKEN
|
||||||
|
const DEV_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN
|
||||||
|
const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID
|
||||||
|
const BASE_URL = 'https://googleads.googleapis.com/v14'
|
||||||
|
|
||||||
|
if (!TOKEN || !DEV_TOKEN || !CUSTOMER_ID) {
|
||||||
|
console.error(JSON.stringify({ error: 'GOOGLE_ADS_TOKEN, GOOGLE_ADS_DEVELOPER_TOKEN, and GOOGLE_ADS_CUSTOMER_ID environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'developer-token': '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${TOKEN}`,
|
||||||
|
'developer-token': DEV_TOKEN,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function gaql(query) {
|
||||||
|
return api('POST', `/customers/${CUSTOMER_ID}/googleAds:searchStream`, { query })
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
function daysToDateRange(days) {
|
||||||
|
const d = parseInt(days) || 30
|
||||||
|
if (d === 7) return 'LAST_7_DAYS'
|
||||||
|
if (d === 14) return 'LAST_14_DAYS'
|
||||||
|
if (d === 30) return 'LAST_30_DAYS'
|
||||||
|
if (d === 90) return 'LAST_90_DAYS'
|
||||||
|
return `LAST_${d}_DAYS`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'account':
|
||||||
|
switch (sub) {
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
result = await gaql('SELECT customer.id, customer.descriptive_name FROM customer')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await gaql('SELECT campaign.id, campaign.name, campaign.status, campaign_budget.amount_micros FROM campaign ORDER BY campaign.id')
|
||||||
|
break
|
||||||
|
case 'performance': {
|
||||||
|
const dateRange = daysToDateRange(args.days)
|
||||||
|
result = await gaql(`SELECT campaign.name, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING ${dateRange}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/customers/${CUSTOMER_ID}/campaigns:mutate`, {
|
||||||
|
operations: [{
|
||||||
|
update: {
|
||||||
|
resourceName: `customers/${CUSTOMER_ID}/campaigns/${args.id}`,
|
||||||
|
status: 'PAUSED',
|
||||||
|
},
|
||||||
|
updateMask: 'status',
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'enable': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/customers/${CUSTOMER_ID}/campaigns:mutate`, {
|
||||||
|
operations: [{
|
||||||
|
update: {
|
||||||
|
resourceName: `customers/${CUSTOMER_ID}/campaigns/${args.id}`,
|
||||||
|
status: 'ENABLED',
|
||||||
|
},
|
||||||
|
updateMask: 'status',
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, performance, pause, enable' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'adgroups':
|
||||||
|
switch (sub) {
|
||||||
|
case 'performance': {
|
||||||
|
const dateRange = daysToDateRange(args.days)
|
||||||
|
const limit = args.limit ? ` LIMIT ${args.limit}` : ''
|
||||||
|
result = await gaql(`SELECT ad_group.name, metrics.impressions, metrics.clicks, metrics.conversions FROM ad_group WHERE segments.date DURING ${dateRange}${limit}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown adgroups subcommand. Use: performance' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keywords':
|
||||||
|
switch (sub) {
|
||||||
|
case 'performance': {
|
||||||
|
const dateRange = daysToDateRange(args.days)
|
||||||
|
const limit = args.limit || '50'
|
||||||
|
result = await gaql(`SELECT ad_group_criterion.keyword.text, metrics.impressions, metrics.clicks, metrics.average_cpc FROM keyword_view WHERE segments.date DURING ${dateRange} ORDER BY metrics.clicks DESC LIMIT ${limit}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keywords subcommand. Use: performance' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'budgets':
|
||||||
|
switch (sub) {
|
||||||
|
case 'update': {
|
||||||
|
if (!args.id || !args.amount) { result = { error: '--id and --amount required' }; break }
|
||||||
|
const amountMicros = String(Math.round(parseFloat(args.amount) * 1000000))
|
||||||
|
result = await api('POST', `/customers/${CUSTOMER_ID}/campaignBudgets:mutate`, {
|
||||||
|
operations: [{
|
||||||
|
update: {
|
||||||
|
resourceName: `customers/${CUSTOMER_ID}/campaignBudgets/${args.id}`,
|
||||||
|
amount_micros: amountMicros,
|
||||||
|
},
|
||||||
|
updateMask: 'amount_micros',
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown budgets subcommand. Use: update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
account: 'account [info]',
|
||||||
|
campaigns: 'campaigns [list|performance|pause|enable] [--days 30] [--id <id>]',
|
||||||
|
adgroups: 'adgroups [performance] [--days 30] [--limit <n>]',
|
||||||
|
keywords: 'keywords [performance] [--days 30] [--limit 50]',
|
||||||
|
budgets: 'budgets [update] --id <budget_id> --amount <dollars>',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
166
tools/clis/google-search-console.js
Executable file
166
tools/clis/google-search-console.js
Executable file
|
|
@ -0,0 +1,166 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const ACCESS_TOKEN = process.env.GSC_ACCESS_TOKEN
|
||||||
|
const BASE_URL = 'https://searchconsole.googleapis.com'
|
||||||
|
|
||||||
|
if (!ACCESS_TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'GSC_ACCESS_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${ACCESS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
function getDefaultDates() {
|
||||||
|
const end = new Date()
|
||||||
|
end.setDate(end.getDate() - 3)
|
||||||
|
const start = new Date(end)
|
||||||
|
start.setDate(start.getDate() - 28)
|
||||||
|
return {
|
||||||
|
startDate: start.toISOString().split('T')[0],
|
||||||
|
endDate: end.toISOString().split('T')[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const siteUrl = args['site-url']
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'search': {
|
||||||
|
if (!siteUrl) {
|
||||||
|
result = { error: '--site-url is required for search commands' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const encodedSiteUrl = encodeURIComponent(siteUrl)
|
||||||
|
const defaults = getDefaultDates()
|
||||||
|
const body = {
|
||||||
|
startDate: args['start-date'] || defaults.startDate,
|
||||||
|
endDate: args['end-date'] || defaults.endDate,
|
||||||
|
rowLimit: parseInt(args.limit || '100', 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sub) {
|
||||||
|
case 'query':
|
||||||
|
body.dimensions = ['query']
|
||||||
|
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
|
||||||
|
break
|
||||||
|
case 'pages':
|
||||||
|
body.dimensions = ['page']
|
||||||
|
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
|
||||||
|
break
|
||||||
|
case 'countries':
|
||||||
|
body.dimensions = ['country']
|
||||||
|
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown search subcommand. Use: query, pages, countries' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'inspect': {
|
||||||
|
if (!siteUrl) {
|
||||||
|
result = { error: '--site-url is required for inspect commands' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch (sub) {
|
||||||
|
case 'url':
|
||||||
|
if (!args.url) { result = { error: '--url required (URL to inspect)' }; break }
|
||||||
|
result = await api('POST', '/v1/urlInspection/index:inspect', {
|
||||||
|
inspectionUrl: args.url,
|
||||||
|
siteUrl: siteUrl,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown inspect subcommand. Use: url' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'sitemaps': {
|
||||||
|
if (!siteUrl) {
|
||||||
|
result = { error: '--site-url is required for sitemaps commands' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const encodedSiteUrl = encodeURIComponent(siteUrl)
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps`)
|
||||||
|
break
|
||||||
|
case 'submit': {
|
||||||
|
if (!args['sitemap-url']) { result = { error: '--sitemap-url required' }; break }
|
||||||
|
const sitemapUrl = encodeURIComponent(args['sitemap-url'])
|
||||||
|
result = await api('PUT', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps/${sitemapUrl}`)
|
||||||
|
if (!result.body && !result.error) {
|
||||||
|
result = { success: true, message: 'Sitemap submitted successfully' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sitemaps subcommand. Use: list, submit' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
'search query': 'search query --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
|
||||||
|
'search pages': 'search pages --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
|
||||||
|
'search countries': 'search countries --site-url <url> [--start-date <date>] [--end-date <date>] [--limit <n>]',
|
||||||
|
'inspect url': 'inspect url --site-url <url> --url <page-url>',
|
||||||
|
'sitemaps list': 'sitemaps list --site-url <url>',
|
||||||
|
'sitemaps submit': 'sitemaps submit --site-url <url> --sitemap-url <sitemap-url>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
167
tools/clis/hotjar.js
Executable file
167
tools/clis/hotjar.js
Executable file
|
|
@ -0,0 +1,167 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const CLIENT_ID = process.env.HOTJAR_CLIENT_ID
|
||||||
|
const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET
|
||||||
|
const OAUTH_URL = 'https://api.hotjar.io'
|
||||||
|
const BASE_URL = 'https://api.hotjar.io/v2'
|
||||||
|
|
||||||
|
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||||
|
console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedToken = null
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
if (cachedToken) return cachedToken
|
||||||
|
const res = await fetch(`${OAUTH_URL}/oauth/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!data.access_token) {
|
||||||
|
throw new Error(data.error_description || data.error || 'Failed to obtain access token')
|
||||||
|
}
|
||||||
|
cachedToken = data.access_token
|
||||||
|
return cachedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' } }
|
||||||
|
}
|
||||||
|
const token = await getToken()
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const siteId = args['site-id']
|
||||||
|
const limit = args.limit || '100'
|
||||||
|
const cursor = args.cursor
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'sites':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/sites')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sites subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'surveys':
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/sites/${siteId}/surveys`)
|
||||||
|
break
|
||||||
|
case 'responses': {
|
||||||
|
const surveyId = args['survey-id']
|
||||||
|
if (!surveyId) { result = { error: '--survey-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ limit })
|
||||||
|
if (cursor) params.set('cursor', cursor)
|
||||||
|
result = await api('GET', `/sites/${siteId}/surveys/${surveyId}/responses?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown surveys subcommand. Use: list, responses' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'heatmaps':
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/sites/${siteId}/heatmaps`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown heatmaps subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'recordings':
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams({ limit })
|
||||||
|
if (cursor) params.set('cursor', cursor)
|
||||||
|
if (args['date-from']) params.set('date_from', args['date-from'])
|
||||||
|
if (args['date-to']) params.set('date_to', args['date-to'])
|
||||||
|
result = await api('GET', `/sites/${siteId}/recordings?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown recordings subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'forms':
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/sites/${siteId}/forms`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown forms subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
sites: 'sites list',
|
||||||
|
surveys: 'surveys list --site-id <id> | surveys responses --site-id <id> --survey-id <id> [--limit <n>] [--cursor <cursor>]',
|
||||||
|
heatmaps: 'heatmaps list --site-id <id>',
|
||||||
|
recordings: 'recordings list --site-id <id> [--limit <n>] [--cursor <cursor>] [--date-from <date>] [--date-to <date>]',
|
||||||
|
forms: 'forms list --site-id <id>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
249
tools/clis/hunter.js
Executable file
249
tools/clis/hunter.js
Executable file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.HUNTER_API_KEY
|
||||||
|
const BASE_URL = 'https://api.hunter.io/v2'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'HUNTER_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const separator = path.includes('?') ? '&' : '?'
|
||||||
|
const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}`
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: url.replace(API_KEY, '***'), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'domain':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({ domain })
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
result = await api('GET', `/domain-search?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'count': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({ domain })
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
result = await api('GET', `/email-count?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domain subcommand. Use: search, count' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const domain = args.domain
|
||||||
|
const firstName = args['first-name']
|
||||||
|
const lastName = args['last-name']
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
if (!firstName) { result = { error: '--first-name required' }; break }
|
||||||
|
if (!lastName) { result = { error: '--last-name required' }; break }
|
||||||
|
const params = new URLSearchParams({ domain, first_name: firstName, last_name: lastName })
|
||||||
|
result = await api('GET', `/email-finder?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'verify': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const params = new URLSearchParams({ email })
|
||||||
|
result = await api('GET', `/email-verifier?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown email subcommand. Use: find, verify' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'account':
|
||||||
|
switch (sub) {
|
||||||
|
case 'info':
|
||||||
|
result = await api('GET', '/account')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown account subcommand. Use: info' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leads':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/leads${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/leads/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email }
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args.company) body.company = args.company
|
||||||
|
result = await api('POST', '/leads', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/leads/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown leads subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/campaigns${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'start': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/campaigns/${id}/start`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/campaigns/${id}/pause`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, start, pause' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leads-lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/leads_lists${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/leads_lists/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown leads-lists subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
domain: {
|
||||||
|
search: 'domain search --domain <domain> [--limit <n>] [--type personal|generic]',
|
||||||
|
count: 'domain count --domain <domain> [--type personal|generic]',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
find: 'email find --domain <domain> --first-name <name> --last-name <name>',
|
||||||
|
verify: 'email verify --email <email>',
|
||||||
|
},
|
||||||
|
account: 'account info',
|
||||||
|
leads: {
|
||||||
|
list: 'leads list [--limit <n>] [--offset <n>]',
|
||||||
|
get: 'leads get --id <id>',
|
||||||
|
create: 'leads create --email <email> [--first-name <name>] [--last-name <name>] [--company <company>]',
|
||||||
|
delete: 'leads delete --id <id>',
|
||||||
|
},
|
||||||
|
campaigns: {
|
||||||
|
list: 'campaigns list [--limit <n>] [--offset <n>]',
|
||||||
|
get: 'campaigns get --id <id>',
|
||||||
|
start: 'campaigns start --id <id>',
|
||||||
|
pause: 'campaigns pause --id <id>',
|
||||||
|
},
|
||||||
|
'leads-lists': {
|
||||||
|
list: 'leads-lists list [--limit <n>] [--offset <n>]',
|
||||||
|
get: 'leads-lists get --id <id>',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
270
tools/clis/instantly.js
Executable file
270
tools/clis/instantly.js
Executable file
|
|
@ -0,0 +1,270 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.INSTANTLY_API_KEY
|
||||||
|
const BASE_URL = 'https://api.instantly.ai/api/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'INSTANTLY_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const separator = path.includes('?') ? '&' : '?'
|
||||||
|
const url = `${BASE_URL}${path}${separator}api_key=${API_KEY}`
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedUrl = url.replace(API_KEY, '***')
|
||||||
|
const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined
|
||||||
|
if (maskedBody && maskedBody.api_key) maskedBody.api_key = '***'
|
||||||
|
return { _dry_run: true, method, url: maskedUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: maskedBody }
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.skip) params.set('skip', args.skip)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/campaign/list${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const params = new URLSearchParams({ campaign_id: id })
|
||||||
|
result = await api('GET', `/campaign/get?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'status': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const params = new URLSearchParams({ campaign_id: id })
|
||||||
|
result = await api('GET', `/campaign/get/status?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'launch': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', '/campaign/launch', { api_key: API_KEY, campaign_id: id })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', '/campaign/pause', { api_key: API_KEY, campaign_id: id })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, status, launch, pause' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leads':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ campaign_id: campaignId })
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.skip) params.set('skip', args.skip)
|
||||||
|
result = await api('GET', `/lead/get?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
const email = args.email
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const lead = { email }
|
||||||
|
if (args['first-name']) lead.first_name = args['first-name']
|
||||||
|
if (args['last-name']) lead.last_name = args['last-name']
|
||||||
|
if (args.company) lead.company_name = args.company
|
||||||
|
result = await api('POST', '/lead/add', { api_key: API_KEY, campaign_id: campaignId, leads: [lead] })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
const email = args.email
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('POST', '/lead/delete', { api_key: API_KEY, campaign_id: campaignId, delete_list: [email] })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'status': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
const email = args.email
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const params = new URLSearchParams({ campaign_id: campaignId, email })
|
||||||
|
result = await api('GET', `/lead/get/status?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown leads subcommand. Use: list, add, delete, status' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'accounts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.skip) params.set('skip', args.skip)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/account/list${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'status': {
|
||||||
|
const accountId = args['account-id']
|
||||||
|
if (!accountId) { result = { error: '--account-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ email: accountId })
|
||||||
|
result = await api('GET', `/account/get/status?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'warmup-status': {
|
||||||
|
const accountId = args['account-id']
|
||||||
|
if (!accountId) { result = { error: '--account-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ email: accountId })
|
||||||
|
result = await api('GET', `/account/get/warmup?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown accounts subcommand. Use: list, status, warmup-status' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'analytics':
|
||||||
|
switch (sub) {
|
||||||
|
case 'campaign': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
const body = { api_key: API_KEY, campaign_id: campaignId }
|
||||||
|
if (args['start-date']) body.start_date = args['start-date']
|
||||||
|
if (args['end-date']) body.end_date = args['end-date']
|
||||||
|
result = await api('POST', '/analytics/campaign/summary', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'steps': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
if (!campaignId) { result = { error: '--campaign-id required' }; break }
|
||||||
|
const body = { api_key: API_KEY, campaign_id: campaignId }
|
||||||
|
if (args['start-date']) body.start_date = args['start-date']
|
||||||
|
if (args['end-date']) body.end_date = args['end-date']
|
||||||
|
result = await api('POST', '/analytics/campaign/step', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'account': {
|
||||||
|
const startDate = args['start-date']
|
||||||
|
const endDate = args['end-date']
|
||||||
|
if (!startDate) { result = { error: '--start-date required' }; break }
|
||||||
|
if (!endDate) { result = { error: '--end-date required' }; break }
|
||||||
|
result = await api('POST', '/analytics/campaign/count', { api_key: API_KEY, start_date: startDate, end_date: endDate })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown analytics subcommand. Use: campaign, steps, account' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'blocklist':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/blocklist')
|
||||||
|
break
|
||||||
|
case 'add': {
|
||||||
|
const entries = args.entries
|
||||||
|
if (!entries) { result = { error: '--entries required (comma-separated emails or domains)' }; break }
|
||||||
|
const entryList = entries.split(',').map(e => e.trim())
|
||||||
|
result = await api('POST', '/blocklist/add', { api_key: API_KEY, entries: entryList })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown blocklist subcommand. Use: list, add' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
campaigns: {
|
||||||
|
list: 'campaigns list [--limit <n>] [--skip <n>]',
|
||||||
|
get: 'campaigns get --id <id>',
|
||||||
|
status: 'campaigns status --id <id>',
|
||||||
|
launch: 'campaigns launch --id <id>',
|
||||||
|
pause: 'campaigns pause --id <id>',
|
||||||
|
},
|
||||||
|
leads: {
|
||||||
|
list: 'leads list --campaign-id <id> [--limit <n>] [--skip <n>]',
|
||||||
|
add: 'leads add --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>] [--company <name>]',
|
||||||
|
delete: 'leads delete --campaign-id <id> --email <email>',
|
||||||
|
status: 'leads status --campaign-id <id> --email <email>',
|
||||||
|
},
|
||||||
|
accounts: {
|
||||||
|
list: 'accounts list [--limit <n>] [--skip <n>]',
|
||||||
|
status: 'accounts status --account-id <email>',
|
||||||
|
'warmup-status': 'accounts warmup-status --account-id <email>',
|
||||||
|
},
|
||||||
|
analytics: {
|
||||||
|
campaign: 'analytics campaign --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]',
|
||||||
|
steps: 'analytics steps --campaign-id <id> [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]',
|
||||||
|
account: 'analytics account --start-date YYYY-MM-DD --end-date YYYY-MM-DD',
|
||||||
|
},
|
||||||
|
blocklist: {
|
||||||
|
list: 'blocklist list',
|
||||||
|
add: 'blocklist add --entries <email-or-domain,email-or-domain>',
|
||||||
|
},
|
||||||
|
options: '--dry-run (show request without executing)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
399
tools/clis/intercom.js
Executable file
399
tools/clis/intercom.js
Executable file
|
|
@ -0,0 +1,399 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.INTERCOM_API_KEY
|
||||||
|
const BASE_URL = 'https://api.intercom.io'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'INTERCOM_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json', 'Intercom-Version': '2.11' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Intercom-Version': '2.11',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : undefined
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'contacts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (perPage) params.set('per_page', String(perPage))
|
||||||
|
if (args['starting-after']) params.set('starting_after', args['starting-after'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/contacts${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/contacts/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = {
|
||||||
|
role: args.role || 'user',
|
||||||
|
email,
|
||||||
|
}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.phone) body.phone = args.phone
|
||||||
|
result = await api('POST', '/contacts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.phone) body.phone = args.phone
|
||||||
|
if (args.role) body.role = args.role
|
||||||
|
result = await api('PUT', `/contacts/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'search': {
|
||||||
|
const field = args.field
|
||||||
|
const operator = args.operator || '='
|
||||||
|
const value = args.value
|
||||||
|
if (!field || !value) { result = { error: '--field and --value required' }; break }
|
||||||
|
const body = {
|
||||||
|
query: { field, operator, value },
|
||||||
|
}
|
||||||
|
if (perPage) body.pagination = { per_page: perPage }
|
||||||
|
result = await api('POST', '/contacts/search', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/contacts/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'tag': {
|
||||||
|
const id = args.id
|
||||||
|
const tagId = args['tag-id']
|
||||||
|
if (!id || !tagId) { result = { error: '--id (contact ID) and --tag-id required' }; break }
|
||||||
|
result = await api('POST', `/contacts/${id}/tags`, { id: tagId })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'untag': {
|
||||||
|
const id = args.id
|
||||||
|
const tagId = args['tag-id']
|
||||||
|
if (!id || !tagId) { result = { error: '--id (contact ID) and --tag-id required' }; break }
|
||||||
|
result = await api('DELETE', `/contacts/${id}/tags/${tagId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown contacts subcommand. Use: list, get, create, update, search, delete, tag, untag' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'conversations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (perPage) params.set('per_page', String(perPage))
|
||||||
|
if (args['starting-after']) params.set('starting_after', args['starting-after'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/conversations${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/conversations/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'search': {
|
||||||
|
const field = args.field
|
||||||
|
const operator = args.operator || '='
|
||||||
|
const value = args.value
|
||||||
|
if (!field || value === undefined) { result = { error: '--field and --value required' }; break }
|
||||||
|
const body = {
|
||||||
|
query: { field, operator, value },
|
||||||
|
}
|
||||||
|
if (perPage) body.pagination = { per_page: perPage }
|
||||||
|
result = await api('POST', '/conversations/search', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'reply': {
|
||||||
|
const id = args.id
|
||||||
|
const body = args.body
|
||||||
|
const adminId = args['admin-id']
|
||||||
|
if (!id || !body || !adminId) { result = { error: '--id, --body, and --admin-id required' }; break }
|
||||||
|
result = await api('POST', `/conversations/${id}/reply`, {
|
||||||
|
message_type: 'comment',
|
||||||
|
type: 'admin',
|
||||||
|
admin_id: adminId,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'close': {
|
||||||
|
const id = args.id
|
||||||
|
const adminId = args['admin-id']
|
||||||
|
if (!id || !adminId) { result = { error: '--id and --admin-id required' }; break }
|
||||||
|
result = await api('POST', `/conversations/${id}/parts`, {
|
||||||
|
message_type: 'close',
|
||||||
|
type: 'admin',
|
||||||
|
admin_id: adminId,
|
||||||
|
body: args.body || '',
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown conversations subcommand. Use: list, get, search, reply, close' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'messages':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
const messageType = args.type || 'inapp'
|
||||||
|
const body = args.body
|
||||||
|
const adminId = args['admin-id']
|
||||||
|
const to = args.to
|
||||||
|
if (!body || !adminId || !to) { result = { error: '--body, --admin-id, and --to (user ID) required' }; break }
|
||||||
|
result = await api('POST', '/messages', {
|
||||||
|
message_type: messageType,
|
||||||
|
body,
|
||||||
|
from: { type: 'admin', id: adminId },
|
||||||
|
to: { type: 'user', id: to },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown messages subcommand. Use: create --body <text> --admin-id <id> --to <user_id> [--type inapp|email]' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'companies':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (perPage) params.set('per_page', String(perPage))
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/companies${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/companies/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const companyId = args['company-id']
|
||||||
|
const name = args.name
|
||||||
|
if (!companyId) { result = { error: '--company-id required' }; break }
|
||||||
|
const body = { company_id: companyId }
|
||||||
|
if (name) body.name = name
|
||||||
|
if (args.plan) body.plan = args.plan
|
||||||
|
if (args.industry) body.industry = args.industry
|
||||||
|
result = await api('POST', '/companies', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.plan) body.plan = args.plan
|
||||||
|
if (args.industry) body.industry = args.industry
|
||||||
|
result = await api('PUT', `/companies/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown companies subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tags':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/tags')
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
result = await api('POST', '/tags', { name })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/tags/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tags subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'articles':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (perPage) params.set('per_page', String(perPage))
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/articles${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/articles/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const title = args.title
|
||||||
|
const authorId = args['author-id']
|
||||||
|
if (!title || !authorId) { result = { error: '--title and --author-id required' }; break }
|
||||||
|
const body = {
|
||||||
|
title,
|
||||||
|
author_id: Number(authorId),
|
||||||
|
state: args.state || 'draft',
|
||||||
|
}
|
||||||
|
if (args.body) body.body = args.body
|
||||||
|
result = await api('POST', '/articles', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.title) body.title = args.title
|
||||||
|
if (args.body) body.body = args.body
|
||||||
|
if (args.state) body.state = args.state
|
||||||
|
result = await api('PUT', `/articles/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/articles/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown articles subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'admins':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/admins')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/admins/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown admins subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
const eventName = args.name
|
||||||
|
const userId = args['user-id']
|
||||||
|
if (!eventName || !userId) { result = { error: '--name and --user-id required' }; break }
|
||||||
|
const body = {
|
||||||
|
event_name: eventName,
|
||||||
|
user_id: userId,
|
||||||
|
created_at: args['created-at'] ? Number(args['created-at']) : Math.floor(Date.now() / 1000),
|
||||||
|
}
|
||||||
|
if (args.metadata) {
|
||||||
|
try { body.metadata = JSON.parse(args.metadata) } catch { body.metadata = {} }
|
||||||
|
}
|
||||||
|
result = await api('POST', '/events', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
const userId = args['user-id']
|
||||||
|
if (!userId) { result = { error: '--user-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ type: 'user', user_id: userId })
|
||||||
|
if (perPage) params.set('per_page', String(perPage))
|
||||||
|
result = await api('GET', `/events?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: create, list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
contacts: 'contacts [list | get --id <id> | create --email <email> | update --id <id> | search --field <f> --value <v> | delete --id <id> | tag --id <id> --tag-id <id> | untag --id <id> --tag-id <id>]',
|
||||||
|
conversations: 'conversations [list | get --id <id> | search --field <f> --value <v> | reply --id <id> --body <text> --admin-id <id> | close --id <id> --admin-id <id>]',
|
||||||
|
messages: 'messages [create --body <text> --admin-id <id> --to <user_id>]',
|
||||||
|
companies: 'companies [list | get --id <id> | create --company-id <id> --name <name> | update --id <id>]',
|
||||||
|
tags: 'tags [list | create --name <name> | delete --id <id>]',
|
||||||
|
articles: 'articles [list | get --id <id> | create --title <title> --author-id <id> | update --id <id> | delete --id <id>]',
|
||||||
|
admins: 'admins [list | get --id <id>]',
|
||||||
|
events: 'events [create --name <name> --user-id <id> | list --user-id <id>]',
|
||||||
|
options: '--per-page <n> --starting-after <cursor> --page <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
185
tools/clis/keywords-everywhere.js
Executable file
185
tools/clis/keywords-everywhere.js
Executable file
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.KEYWORDS_EVERYWHERE_API_KEY
|
||||||
|
const BASE_URL = 'https://api.keywordseverywhere.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'KEYWORDS_EVERYWHERE_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const country = args.country || 'us'
|
||||||
|
const currency = args.currency || 'USD'
|
||||||
|
const dataSource = args['data-source'] || 'gkp'
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'keywords':
|
||||||
|
switch (sub) {
|
||||||
|
case 'data': {
|
||||||
|
const kw = args.kw?.split(',')
|
||||||
|
if (!kw) { result = { error: '--kw required (comma-separated keywords, max 100)' }; break }
|
||||||
|
result = await api('POST', '/get_keyword_data', { country, currency, dataSource, kw })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'related': {
|
||||||
|
const kw = args.kw?.split(',')
|
||||||
|
if (!kw) { result = { error: '--kw required (comma-separated keywords)' }; break }
|
||||||
|
result = await api('POST', '/get_related_keywords', { country, currency, dataSource, kw })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pasf': {
|
||||||
|
const kw = args.kw?.split(',')
|
||||||
|
if (!kw) { result = { error: '--kw required (comma-separated keywords)' }; break }
|
||||||
|
result = await api('POST', '/get_pasf_keywords', { country, currency, dataSource, kw })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keywords subcommand. Use: data, related, pasf' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'domain':
|
||||||
|
switch (sub) {
|
||||||
|
case 'keywords': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get_domain_keywords', { country, currency, domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'traffic': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get_domain_traffic', { country, domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'backlinks': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get_domain_backlinks', { domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unique-backlinks': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get_unique_domain_backlinks', { domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domain subcommand. Use: keywords, traffic, backlinks, unique-backlinks' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'url':
|
||||||
|
switch (sub) {
|
||||||
|
case 'keywords': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
result = await api('POST', '/get_url_keywords', { country, currency, url })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'traffic': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
result = await api('POST', '/get_url_traffic', { country, url })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'backlinks': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
result = await api('POST', '/get_page_backlinks', { url })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unique-backlinks': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
result = await api('POST', '/get_unique_page_backlinks', { url })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown url subcommand. Use: keywords, traffic, backlinks, unique-backlinks' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'account':
|
||||||
|
switch (sub) {
|
||||||
|
case 'credits':
|
||||||
|
result = await api('GET', '/get_credits')
|
||||||
|
break
|
||||||
|
case 'countries':
|
||||||
|
result = await api('GET', '/get_countries')
|
||||||
|
break
|
||||||
|
case 'currencies':
|
||||||
|
result = await api('GET', '/get_currencies')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown account subcommand. Use: credits, countries, currencies' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
keywords: 'keywords [data|related|pasf] --kw <kw1,kw2,...>',
|
||||||
|
domain: 'domain [keywords|traffic|backlinks|unique-backlinks] --domain <domain>',
|
||||||
|
url: 'url [keywords|traffic|backlinks|unique-backlinks] --url <url>',
|
||||||
|
account: 'account [credits|countries|currencies]',
|
||||||
|
options: '--country <us> --currency <USD> --data-source <gkp>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
232
tools/clis/kit.js
Executable file
232
tools/clis/kit.js
Executable file
|
|
@ -0,0 +1,232 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_SECRET = process.env.KIT_API_SECRET
|
||||||
|
const API_KEY = process.env.KIT_API_KEY
|
||||||
|
const BASE_URL = 'https://api.convertkit.com/v3'
|
||||||
|
|
||||||
|
if (!API_SECRET && !API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'KIT_API_SECRET or KIT_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body, useSecret = true) {
|
||||||
|
const url = new URL(`${BASE_URL}${path}`)
|
||||||
|
if (method === 'GET' || method === 'DELETE') {
|
||||||
|
if (useSecret && API_SECRET) {
|
||||||
|
url.searchParams.set('api_secret', API_SECRET)
|
||||||
|
} else if (useSecret && !API_SECRET) {
|
||||||
|
return { error: 'KIT_API_SECRET required for this endpoint' }
|
||||||
|
} else if (API_KEY) {
|
||||||
|
url.searchParams.set('api_key', API_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
}
|
||||||
|
if (body && (method === 'POST' || method === 'PUT')) {
|
||||||
|
const authBody = { ...body }
|
||||||
|
if (useSecret && API_SECRET) {
|
||||||
|
authBody.api_secret = API_SECRET
|
||||||
|
} else if (useSecret && !API_SECRET) {
|
||||||
|
return { error: 'KIT_API_SECRET required for this endpoint' }
|
||||||
|
} else if (API_KEY) {
|
||||||
|
authBody.api_key = API_KEY
|
||||||
|
}
|
||||||
|
opts.body = JSON.stringify(authBody)
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const dryRunHeaders = { ...opts.headers }
|
||||||
|
const dryRunUrl = url.toString().replace(API_SECRET, '***').replace(API_KEY, '***')
|
||||||
|
let dryRunBody = undefined
|
||||||
|
if (opts.body) {
|
||||||
|
const parsed = JSON.parse(opts.body)
|
||||||
|
if (parsed.api_secret) parsed.api_secret = '***'
|
||||||
|
if (parsed.api_key) parsed.api_key = '***'
|
||||||
|
dryRunBody = parsed
|
||||||
|
}
|
||||||
|
return { _dry_run: true, method, url: dryRunUrl, headers: dryRunHeaders, body: dryRunBody }
|
||||||
|
}
|
||||||
|
const res = await fetch(url.toString(), opts)
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'subscribers':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = args.page ? `&page=${args.page}` : ''
|
||||||
|
result = await api('GET', `/subscribers?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Subscriber ID required' }; break }
|
||||||
|
result = await api('GET', `/subscribers/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'update': {
|
||||||
|
if (!rest[0]) { result = { error: 'Subscriber ID required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args.fields) {
|
||||||
|
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
|
||||||
|
}
|
||||||
|
result = await api('PUT', `/subscribers/${rest[0]}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unsubscribe': {
|
||||||
|
const body = { email: args.email }
|
||||||
|
result = await api('PUT', '/unsubscribe', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown subscribers subcommand. Use: list, get, update, unsubscribe' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'forms':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/forms', null, false)
|
||||||
|
break
|
||||||
|
case 'subscribe': {
|
||||||
|
if (!rest[0]) { result = { error: 'Form ID required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
const formId = rest[0]
|
||||||
|
const body = { email: args.email }
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args.fields) {
|
||||||
|
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
|
||||||
|
}
|
||||||
|
result = await api('POST', `/forms/${formId}/subscribe`, body, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown forms subcommand. Use: list, subscribe' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'sequences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/sequences', null, false)
|
||||||
|
break
|
||||||
|
case 'subscribe': {
|
||||||
|
if (!rest[0]) { result = { error: 'Sequence ID required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
const sequenceId = rest[0]
|
||||||
|
const body = { email: args.email }
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args.fields) {
|
||||||
|
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
|
||||||
|
}
|
||||||
|
result = await api('POST', `/sequences/${sequenceId}/subscribe`, body, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sequences subcommand. Use: list, subscribe' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tags':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/tags', null, false)
|
||||||
|
break
|
||||||
|
case 'subscribe': {
|
||||||
|
if (!rest[0]) { result = { error: 'Tag ID required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
const tagId = rest[0]
|
||||||
|
const body = { email: args.email }
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args.fields) {
|
||||||
|
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
|
||||||
|
}
|
||||||
|
result = await api('POST', `/tags/${tagId}/subscribe`, body, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove': {
|
||||||
|
if (!rest[0]) { result = { error: 'Tag ID required' }; break }
|
||||||
|
const tagId = rest[0]
|
||||||
|
const subscriberId = rest[1] || args['subscriber-id']
|
||||||
|
if (!subscriberId) { result = { error: 'Subscriber ID required' }; break }
|
||||||
|
result = await api('DELETE', `/subscribers/${subscriberId}/tags/${tagId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tags subcommand. Use: list, subscribe, remove' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'broadcasts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = args.page ? `&page=${args.page}` : ''
|
||||||
|
result = await api('GET', `/broadcasts?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
if (!args.subject || !args.content) { result = { error: '--subject and --content required' }; break }
|
||||||
|
const body = {
|
||||||
|
subject: args.subject,
|
||||||
|
content: args.content,
|
||||||
|
}
|
||||||
|
if (args['email-layout-template']) body.email_layout_template = args['email-layout-template']
|
||||||
|
result = await api('POST', '/broadcasts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown broadcasts subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
subscribers: 'subscribers [list|get|update|unsubscribe] [id] [--email <email>] [--first-name <name>] [--fields <json>] [--page <n>]',
|
||||||
|
forms: 'forms [list|subscribe] [form_id] [--email <email>] [--first-name <name>] [--fields <json>]',
|
||||||
|
sequences: 'sequences [list|subscribe] [sequence_id] [--email <email>] [--first-name <name>] [--fields <json>]',
|
||||||
|
tags: 'tags [list|subscribe|remove] [tag_id] [subscriber_id] [--email <email>] [--subscriber-id <id>]',
|
||||||
|
broadcasts: 'broadcasts [list|create] [--subject <subject>] [--content <html>] [--email-layout-template <template>] [--page <n>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
348
tools/clis/klaviyo.js
Executable file
348
tools/clis/klaviyo.js
Executable file
|
|
@ -0,0 +1,348 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.KLAVIYO_API_KEY
|
||||||
|
const BASE_URL = 'https://a.klaviyo.com/api'
|
||||||
|
const REVISION = '2024-10-15'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'KLAVIYO_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Klaviyo-API-Key ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'revision': REVISION,
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'profiles':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.filter) params.set('filter', args.filter)
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
if (args['page-cursor']) params.set('page[cursor]', args['page-cursor'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/profiles/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/profiles/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const attributes = { email }
|
||||||
|
if (args['first-name']) attributes.first_name = args['first-name']
|
||||||
|
if (args['last-name']) attributes.last_name = args['last-name']
|
||||||
|
if (args.phone) attributes.phone_number = args.phone
|
||||||
|
result = await api('POST', '/profiles/', {
|
||||||
|
data: { type: 'profile', attributes }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const attributes = {}
|
||||||
|
if (args.email) attributes.email = args.email
|
||||||
|
if (args['first-name']) attributes.first_name = args['first-name']
|
||||||
|
if (args['last-name']) attributes.last_name = args['last-name']
|
||||||
|
if (args.phone) attributes.phone_number = args.phone
|
||||||
|
result = await api('PATCH', `/profiles/${id}/`, {
|
||||||
|
data: { type: 'profile', id, attributes }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown profiles subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/lists/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/lists/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
result = await api('POST', '/lists/', {
|
||||||
|
data: { type: 'list', attributes: { name } }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/lists/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add-profiles': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (list ID)' }; break }
|
||||||
|
const profileIds = args.profiles?.split(',')
|
||||||
|
if (!profileIds) { result = { error: '--profiles required (comma-separated profile IDs)' }; break }
|
||||||
|
result = await api('POST', `/lists/${id}/relationships/profiles/`, {
|
||||||
|
data: profileIds.map(pid => ({ type: 'profile', id: pid }))
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove-profiles': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (list ID)' }; break }
|
||||||
|
const profileIds = args.profiles?.split(',')
|
||||||
|
if (!profileIds) { result = { error: '--profiles required (comma-separated profile IDs)' }; break }
|
||||||
|
result = await api('DELETE', `/lists/${id}/relationships/profiles/`, {
|
||||||
|
data: profileIds.map(pid => ({ type: 'profile', id: pid }))
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown lists subcommand. Use: list, get, create, delete, add-profiles, remove-profiles' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.filter) params.set('filter', args.filter)
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/events/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/events/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const metric = args.metric
|
||||||
|
const email = args.email
|
||||||
|
if (!metric) { result = { error: '--metric required (metric name)' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const properties = {}
|
||||||
|
if (args.value) properties.value = Number(args.value)
|
||||||
|
if (args.property) {
|
||||||
|
const pairs = args.property.split(',')
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const [k, v] = pair.split(':')
|
||||||
|
if (k && v) properties[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = await api('POST', '/events/', {
|
||||||
|
data: {
|
||||||
|
type: 'event',
|
||||||
|
attributes: {
|
||||||
|
metric: { data: { type: 'metric', attributes: { name: metric } } },
|
||||||
|
profile: { data: { type: 'profile', attributes: { email } } },
|
||||||
|
properties,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, get, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.filter) params.set('filter', args.filter)
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/campaigns/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'flows':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.filter) params.set('filter', args.filter)
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/flows/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/flows/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const attributes = {}
|
||||||
|
if (args.status) attributes.status = args.status
|
||||||
|
result = await api('PATCH', `/flows/${id}/`, {
|
||||||
|
data: { type: 'flow', id, attributes }
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown flows subcommand. Use: list, get, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'metrics':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/metrics/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/metrics/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown metrics subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'segments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/segments/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/segments/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown segments subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'templates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.filter) params.set('filter', args.filter)
|
||||||
|
if (args['page-size']) params.set('page[size]', args['page-size'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/templates/${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/templates/${id}/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown templates subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
profiles: 'profiles [list | get --id <id> | create --email <email> | update --id <id>]',
|
||||||
|
lists: 'lists [list | get --id <id> | create --name <name> | delete --id <id> | add-profiles --id <list-id> --profiles <id1,id2> | remove-profiles --id <list-id> --profiles <id1,id2>]',
|
||||||
|
events: 'events [list | get --id <id> | create --metric <name> --email <email>]',
|
||||||
|
campaigns: 'campaigns [list | get --id <id>]',
|
||||||
|
flows: 'flows [list | get --id <id> | update --id <id> --status <status>]',
|
||||||
|
metrics: 'metrics [list | get --id <id>]',
|
||||||
|
segments: 'segments [list | get --id <id>]',
|
||||||
|
templates: 'templates [list | get --id <id>]',
|
||||||
|
options: '--filter <filter> --page-size <n> --page-cursor <cursor>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
221
tools/clis/lemlist.js
Executable file
221
tools/clis/lemlist.js
Executable file
|
|
@ -0,0 +1,221 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.LEMLIST_API_KEY
|
||||||
|
const BASE_URL = 'https://api.lemlist.com/api'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'LEMLIST_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH = 'Basic ' + Buffer.from(`:${API_KEY}`).toString('base64')
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': AUTH,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const offset = args.offset ? Number(args.offset) : 0
|
||||||
|
const limit = args.limit ? Number(args.limit) : 100
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'team':
|
||||||
|
switch (sub) {
|
||||||
|
case 'info':
|
||||||
|
result = await api('GET', '/team')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown team subcommand. Use: info' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
result = await api('GET', `/campaigns?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${args.id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'stats': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${args.id}/stats`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'export': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${args.id}/export`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, stats, export' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leads':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
result = await api('GET', `/campaigns/${args['campaign-id']}/leads?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['first-name']) body.firstName = args['first-name']
|
||||||
|
if (args['last-name']) body.lastName = args['last-name']
|
||||||
|
if (args.company) body.companyName = args.company
|
||||||
|
result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown leads subcommand. Use: list, get, add, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'unsubscribes':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
result = await api('GET', `/unsubscribes?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('POST', `/unsubscribes/${encodeURIComponent(args.email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('DELETE', `/unsubscribes/${encodeURIComponent(args.email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown unsubscribes subcommand. Use: list, add, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'activities':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['campaign-id']) params.set('campaignId', args['campaign-id'])
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
params.set('offset', String(offset))
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
result = await api('GET', `/activities?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown activities subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'hooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/hooks')
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
if (!args['target-url']) { result = { error: '--target-url required' }; break }
|
||||||
|
if (!args.event) { result = { error: '--event required' }; break }
|
||||||
|
result = await api('POST', '/hooks', { targetUrl: args['target-url'], event: args.event })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/hooks/${args.id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown hooks subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
team: 'team info',
|
||||||
|
campaigns: 'campaigns [list | get --id <id> | stats --id <id> | export --id <id>] [--offset 0] [--limit 100]',
|
||||||
|
leads: 'leads [list | get --email <email> | add --email <email> | delete --email <email>] --campaign-id <id> [--first-name <name>] [--last-name <name>] [--company <name>]',
|
||||||
|
unsubscribes: 'unsubscribes [list | add --email <email> | delete --email <email>] [--offset 0] [--limit 100]',
|
||||||
|
activities: 'activities list [--campaign-id <id>] [--type emailsSent|emailsOpened|emailsClicked|emailsReplied|emailsBounced] [--offset 0] [--limit 100]',
|
||||||
|
hooks: 'hooks [list | create --target-url <url> --event <event> | delete --id <id>]',
|
||||||
|
options: '--dry-run --offset <n> --limit <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
185
tools/clis/linkedin-ads.js
Executable file
185
tools/clis/linkedin-ads.js
Executable file
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const TOKEN = process.env.LINKEDIN_ACCESS_TOKEN
|
||||||
|
const BASE_URL = 'https://api.linkedin.com/v2'
|
||||||
|
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'LINKEDIN_ACCESS_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${TOKEN}`,
|
||||||
|
'X-RestLi-Protocol-Version': '2.0.0',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'accounts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/adAccountsV2?q=search')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown accounts subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['account-id']) { result = { error: '--account-id required' }; break }
|
||||||
|
result = await api('GET', `/adCampaignsV2?q=search&search.account.values[0]=urn:li:sponsoredAccount:${args['account-id']}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
if (!args['account-id'] || !args.name) { result = { error: '--account-id and --name required' }; break }
|
||||||
|
if (!args['campaign-group-id']) { result = { error: '--campaign-group-id required' }; break }
|
||||||
|
const body = {
|
||||||
|
account: `urn:li:sponsoredAccount:${args['account-id']}`,
|
||||||
|
campaignGroup: `urn:li:sponsoredCampaignGroup:${args['campaign-group-id']}`,
|
||||||
|
name: args.name,
|
||||||
|
type: args.type || 'SPONSORED_UPDATES',
|
||||||
|
costType: args['cost-type'] || 'CPC',
|
||||||
|
unitCost: {
|
||||||
|
amount: parseFloat(args['unit-cost'] || '5.00'),
|
||||||
|
currencyCode: 'USD',
|
||||||
|
},
|
||||||
|
dailyBudget: {
|
||||||
|
amount: parseFloat(args['daily-budget'] || '100.00'),
|
||||||
|
currencyCode: 'USD',
|
||||||
|
},
|
||||||
|
status: 'PAUSED',
|
||||||
|
}
|
||||||
|
result = await api('POST', '/adCampaignsV2', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!args.id || !args.status) { result = { error: '--id and --status required' }; break }
|
||||||
|
result = await api('POST', `/adCampaignsV2/${args.id}`, {
|
||||||
|
patch: {
|
||||||
|
$set: {
|
||||||
|
status: args.status,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'analytics': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
if (!args['start-year'] || !args['start-month'] || !args['start-day'] || !args['end-year'] || !args['end-month'] || !args['end-day']) {
|
||||||
|
result = { error: '--start-year, --start-month, --start-day, --end-year, --end-month, --end-day required' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: 'analytics',
|
||||||
|
pivot: 'CAMPAIGN',
|
||||||
|
'dateRange.start.year': args['start-year'],
|
||||||
|
'dateRange.start.month': args['start-month'],
|
||||||
|
'dateRange.start.day': args['start-day'],
|
||||||
|
'dateRange.end.year': args['end-year'],
|
||||||
|
'dateRange.end.month': args['end-month'],
|
||||||
|
'dateRange.end.day': args['end-day'],
|
||||||
|
campaigns: `urn:li:sponsoredCampaign:${args.id}`,
|
||||||
|
fields: 'impressions,clicks,costInLocalCurrency,conversions',
|
||||||
|
})
|
||||||
|
result = await api('GET', `/adAnalyticsV2?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, create, update, analytics' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'creatives':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||||
|
result = await api('GET', `/adCreativesV2?q=search&search.campaign.values[0]=urn:li:sponsoredCampaign:${args['campaign-id']}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown creatives subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audiences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'count': {
|
||||||
|
if (!args.targeting) { result = { error: '--targeting required (JSON string)' }; break }
|
||||||
|
let targeting
|
||||||
|
try {
|
||||||
|
targeting = JSON.parse(args.targeting)
|
||||||
|
} catch {
|
||||||
|
result = { error: 'Invalid JSON for --targeting' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = await api('POST', '/audienceCountsV2', { audienceCriteria: targeting })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown audiences subcommand. Use: count' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
accounts: 'accounts [list]',
|
||||||
|
campaigns: 'campaigns [list|create|update|analytics] [--account-id <id>] [--name <name>] [--type SPONSORED_UPDATES] [--cost-type CPC] [--unit-cost 5.00] [--daily-budget 100.00] [--id <id>] [--status ACTIVE|PAUSED]',
|
||||||
|
creatives: 'creatives [list] --campaign-id <id>',
|
||||||
|
audiences: 'audiences [count] --targeting <json>',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
292
tools/clis/livestorm.js
Executable file
292
tools/clis/livestorm.js
Executable file
|
|
@ -0,0 +1,292 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_TOKEN = process.env.LIVESTORM_API_TOKEN
|
||||||
|
const BASE_URL = 'https://api.livestorm.co/v1'
|
||||||
|
|
||||||
|
if (!API_TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'LIVESTORM_API_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${API_TOKEN}`,
|
||||||
|
'Content-Type': 'application/vnd.api+json',
|
||||||
|
'Accept': 'application/vnd.api+json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const page = args.page ? Number(args.page) : 1
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : 25
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'ping':
|
||||||
|
result = await api('GET', '/ping')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
if (args.title) qs += `&filter[title]=${encodeURIComponent(args.title)}`
|
||||||
|
result = await api('GET', `/events${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/events/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const title = args.title
|
||||||
|
if (!title) { result = { error: '--title required' }; break }
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
type: 'events',
|
||||||
|
attributes: {
|
||||||
|
title,
|
||||||
|
slug: args.slug || title.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (args.description) payload.data.attributes.description = args.description
|
||||||
|
if (args['estimated-duration']) payload.data.attributes.estimated_duration = Number(args['estimated-duration'])
|
||||||
|
result = await api('POST', '/events', payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const attrs = {}
|
||||||
|
if (args.title) attrs.title = args.title
|
||||||
|
if (args.description) attrs.description = args.description
|
||||||
|
if (args.slug) attrs.slug = args.slug
|
||||||
|
result = await api('PATCH', `/events/${id}`, {
|
||||||
|
data: { type: 'events', id, attributes: attrs },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/events/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'people': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
result = await api('GET', `/events/${id}/people${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, get, create, update, delete, people' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'sessions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
result = await api('GET', `/sessions${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/sessions/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const eventId = args['event-id']
|
||||||
|
if (!eventId) { result = { error: '--event-id required' }; break }
|
||||||
|
const payload = {
|
||||||
|
data: {
|
||||||
|
type: 'sessions',
|
||||||
|
attributes: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (args['estimated-started-at']) payload.data.attributes.estimated_started_at = args['estimated-started-at']
|
||||||
|
if (args.timezone) payload.data.attributes.timezone = args.timezone
|
||||||
|
result = await api('POST', `/events/${eventId}/sessions`, payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/sessions/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'people': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
result = await api('GET', `/sessions/${id}/people${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'register': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id (session id) required' }; break }
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const fields = { email }
|
||||||
|
if (args['first-name']) fields.first_name = args['first-name']
|
||||||
|
if (args['last-name']) fields.last_name = args['last-name']
|
||||||
|
result = await api('POST', `/sessions/${id}/people`, {
|
||||||
|
data: {
|
||||||
|
type: 'people',
|
||||||
|
attributes: { fields },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'unregister': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id (session id) required' }; break }
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('DELETE', `/sessions/${id}/people?filter[email]=${encodeURIComponent(email)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'chat': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
result = await api('GET', `/sessions/${id}/chat-messages${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'questions': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
result = await api('GET', `/sessions/${id}/questions${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'recordings': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/sessions/${id}/recordings`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sessions subcommand. Use: list, get, create, delete, people, register, unregister, chat, questions, recordings' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'people':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
let qs = `?page[number]=${page}&page[size]=${perPage}`
|
||||||
|
if (args.email) qs += `&filter[email]=${encodeURIComponent(args.email)}`
|
||||||
|
result = await api('GET', `/people${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/people/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown people subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
result = await api('GET', '/webhooks')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const url = args.url
|
||||||
|
if (!url) { result = { error: '--url required' }; break }
|
||||||
|
const eventName = args.event || 'attendance'
|
||||||
|
result = await api('POST', '/webhooks', {
|
||||||
|
data: {
|
||||||
|
type: 'webhooks',
|
||||||
|
attributes: {
|
||||||
|
target_url: url,
|
||||||
|
event_name: eventName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/webhooks/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'organization':
|
||||||
|
result = await api('GET', '/organization')
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
ping: 'ping',
|
||||||
|
events: 'events [list | get --id <id> | create --title <t> | update --id <id> --title <t> | delete --id <id> | people --id <id>]',
|
||||||
|
sessions: 'sessions [list | get --id <id> | create --event-id <id> | delete --id <id> | people --id <id> | register --id <id> --email <e> | unregister --id <id> --email <e> | chat --id <id> | questions --id <id> | recordings --id <id>]',
|
||||||
|
people: 'people [list --email <e> | get --id <id>]',
|
||||||
|
webhooks: 'webhooks [list | create --url <url> --event <name> | delete --id <id>]',
|
||||||
|
organization: 'organization',
|
||||||
|
options: '--page <n> --per-page <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
220
tools/clis/mailchimp.js
Executable file
220
tools/clis/mailchimp.js
Executable file
|
|
@ -0,0 +1,220 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.MAILCHIMP_API_KEY
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'MAILCHIMP_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dc = API_KEY.split('-').pop()
|
||||||
|
const BASE_URL = `https://${dc}.api.mailchimp.com/3.0`
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const auth = 'Basic ' + Buffer.from(`anystring:${API_KEY}`).toString('base64')
|
||||||
|
const headers = {
|
||||||
|
'Authorization': auth,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
result = await api('GET', `/lists?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'List ID required' }; break }
|
||||||
|
result = await api('GET', `/lists/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown lists subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'members':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['list-id']) {
|
||||||
|
result = { error: '--list-id is required for members list' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/lists/${args['list-id']}/members?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
if (!rest[0]) { result = { error: 'List ID required' }; break }
|
||||||
|
if (!args.email) { result = { error: '--email required' }; break }
|
||||||
|
if (!args['list-id']) {
|
||||||
|
result = { error: '--list-id is required for members add' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
email_address: args.email,
|
||||||
|
status: args.status || 'subscribed',
|
||||||
|
}
|
||||||
|
if (args['first-name'] || args['last-name']) {
|
||||||
|
body.merge_fields = {}
|
||||||
|
if (args['first-name']) body.merge_fields.FNAME = args['first-name']
|
||||||
|
if (args['last-name']) body.merge_fields.LNAME = args['last-name']
|
||||||
|
}
|
||||||
|
if (args.tags) body.tags = args.tags.split(',')
|
||||||
|
result = await api('POST', `/lists/${args['list-id']}/members`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!args['list-id']) {
|
||||||
|
result = { error: '--list-id is required for members update' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const subscriberHash = rest[0]
|
||||||
|
const body = {}
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
if (args['first-name'] || args['last-name']) {
|
||||||
|
body.merge_fields = {}
|
||||||
|
if (args['first-name']) body.merge_fields.FNAME = args['first-name']
|
||||||
|
if (args['last-name']) body.merge_fields.LNAME = args['last-name']
|
||||||
|
}
|
||||||
|
if (args.tags) body.tags = args.tags.split(',')
|
||||||
|
result = await api('PATCH', `/lists/${args['list-id']}/members/${subscriberHash}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown members subcommand. Use: list, add, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
result = await api('GET', `/campaigns?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
if (!args['list-id']) { result = { error: '--list-id required' }; break }
|
||||||
|
const body = {
|
||||||
|
type: args.type || 'regular',
|
||||||
|
recipients: {
|
||||||
|
list_id: args['list-id'],
|
||||||
|
},
|
||||||
|
settings: {},
|
||||||
|
}
|
||||||
|
if (args.subject) body.settings.subject_line = args.subject
|
||||||
|
if (args['from-name']) body.settings.from_name = args['from-name']
|
||||||
|
if (args['reply-to']) body.settings.reply_to = args['reply-to']
|
||||||
|
if (args.title) body.settings.title = args.title
|
||||||
|
result = await api('POST', '/campaigns', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send':
|
||||||
|
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
|
||||||
|
result = await api('POST', `/campaigns/${rest[0]}/actions/send`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, create, send' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
|
||||||
|
result = await api('GET', `/reports/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reports subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'automations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.count) params.set('count', args.count)
|
||||||
|
if (args.offset) params.set('offset', args.offset)
|
||||||
|
result = await api('GET', `/automations?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown automations subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
lists: 'lists [list|get] [id] [--count <n>] [--offset <n>]',
|
||||||
|
members: 'members [list|add|update] [subscriber_hash] --list-id <id> [--email <email>] [--status <status>] [--first-name <name>] [--last-name <name>] [--tags <t1,t2>]',
|
||||||
|
campaigns: 'campaigns [list|get|create|send] [id] [--list-id <id>] [--subject <subject>] [--from-name <name>] [--reply-to <email>]',
|
||||||
|
reports: 'reports get <campaign_id>',
|
||||||
|
automations: 'automations list [--count <n>] [--offset <n>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
161
tools/clis/mention-me.js
Executable file
161
tools/clis/mention-me.js
Executable file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.MENTIONME_API_KEY
|
||||||
|
const BASE_URL = 'https://api.mention-me.com/api/v2'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'MENTIONME_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'offers':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.firstname) body.firstname = args.firstname
|
||||||
|
if (args.lastname) body.lastname = args.lastname
|
||||||
|
if (args['order-number']) body.order_number = args['order-number']
|
||||||
|
if (args['order-total']) body.order_total = Number(args['order-total'])
|
||||||
|
if (args['order-currency']) body.order_currency = args['order-currency']
|
||||||
|
result = await api('POST', '/referrer-offer', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown offers subcommand. Use: create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'referrals':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const refId = rest[0] || args.id
|
||||||
|
if (!refId) { result = { error: 'Referral ID required (positional arg or --id)' }; break }
|
||||||
|
result = await api('GET', `/referral/${refId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
|
||||||
|
result = await api('GET', `/referrer/${args['customer-id']}/referrals`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown referrals subcommand. Use: get, list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'share-links':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get':
|
||||||
|
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
|
||||||
|
result = await api('GET', `/referrer/${args['customer-id']}/share-links`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown share-links subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'rewards':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get':
|
||||||
|
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
|
||||||
|
result = await api('GET', `/referrer/${args['customer-id']}/rewards`)
|
||||||
|
break
|
||||||
|
case 'redeem': {
|
||||||
|
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['reward-id']) body.reward_id = args['reward-id']
|
||||||
|
if (args['order-number']) body.order_number = args['order-number']
|
||||||
|
result = await api('POST', `/referrer/${args['customer-id']}/rewards/redeem`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown rewards subcommand. Use: get, redeem' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'referee':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.firstname) body.firstname = args.firstname
|
||||||
|
if (args['referrer-code']) body.referrer_code = args['referrer-code']
|
||||||
|
if (args['order-number']) body.order_number = args['order-number']
|
||||||
|
if (args['order-total']) body.order_total = Number(args['order-total'])
|
||||||
|
result = await api('POST', '/referee', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown referee subcommand. Use: create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
offers: 'offers [create] [--email <email>] [--firstname <name>] [--lastname <name>] [--order-number <num>] [--order-total <total>] [--order-currency <currency>]',
|
||||||
|
referrals: 'referrals [get|list] [id] [--customer-id <id>]',
|
||||||
|
'share-links': 'share-links [get] [--customer-id <id>]',
|
||||||
|
rewards: 'rewards [get|redeem] [--customer-id <id>] [--reward-id <id>] [--order-number <num>]',
|
||||||
|
referee: 'referee [create] [--email <email>] [--firstname <name>] [--referrer-code <code>] [--order-number <num>] [--order-total <total>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
181
tools/clis/meta-ads.js
Executable file
181
tools/clis/meta-ads.js
Executable file
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const TOKEN = process.env.META_ACCESS_TOKEN
|
||||||
|
const DEFAULT_ACCOUNT_ID = process.env.META_AD_ACCOUNT_ID
|
||||||
|
const BASE_URL = 'https://graph.facebook.com/v18.0'
|
||||||
|
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'META_ACCESS_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const url = `${BASE_URL}${path}`
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: { 'Authorization': `Bearer ${TOKEN}` },
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
|
opts.headers['Content-Type'] = 'application/json'
|
||||||
|
opts.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url, headers: { ...opts.headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(url, opts)
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
function getAccountId() {
|
||||||
|
return args['account-id'] || DEFAULT_ACCOUNT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'accounts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/me/adaccounts?fields=id,name,account_status')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown accounts subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const accountId = getAccountId()
|
||||||
|
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
|
||||||
|
result = await api('GET', `/act_${accountId}/campaigns?fields=id,name,status,objective,daily_budget`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'insights': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
const datePreset = args['date-preset'] || 'last_30d'
|
||||||
|
result = await api('GET', `/${args.id}/insights?fields=impressions,clicks,spend,actions,cost_per_action_type&date_preset=${datePreset}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const accountId = getAccountId()
|
||||||
|
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
|
||||||
|
if (!args.name || !args.objective) { result = { error: '--name and --objective required' }; break }
|
||||||
|
const body = {
|
||||||
|
name: args.name,
|
||||||
|
objective: args.objective,
|
||||||
|
status: args.status || 'PAUSED',
|
||||||
|
special_ad_categories: [],
|
||||||
|
}
|
||||||
|
result = await api('POST', `/act_${accountId}/campaigns`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!args.id || !args.status) { result = { error: '--id and --status required' }; break }
|
||||||
|
result = await api('POST', `/${args.id}`, { status: args.status })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, insights, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'adsets':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const accountId = getAccountId()
|
||||||
|
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
|
||||||
|
result = await api('GET', `/act_${accountId}/adsets?fields=id,name,status,targeting,daily_budget,bid_amount`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown adsets subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'ads':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['adset-id']) { result = { error: '--adset-id required' }; break }
|
||||||
|
result = await api('GET', `/${args['adset-id']}/ads?fields=id,name,status,creative`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown ads subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audiences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const accountId = getAccountId()
|
||||||
|
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
|
||||||
|
result = await api('GET', `/act_${accountId}/customaudiences?fields=id,name,approximate_count`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create-lookalike': {
|
||||||
|
const accountId = getAccountId()
|
||||||
|
if (!accountId) { result = { error: '--account-id required (or set META_AD_ACCOUNT_ID)' }; break }
|
||||||
|
if (!args['source-id'] || !args.country) { result = { error: '--source-id and --country required' }; break }
|
||||||
|
result = await api('POST', `/act_${accountId}/customaudiences`, {
|
||||||
|
name: args.name || 'Lookalike Audience',
|
||||||
|
subtype: 'LOOKALIKE',
|
||||||
|
origin_audience_id: args['source-id'],
|
||||||
|
lookalike_spec: JSON.stringify({ type: 'similarity', country: args.country }),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown audiences subcommand. Use: list, create-lookalike' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
accounts: 'accounts [list]',
|
||||||
|
campaigns: 'campaigns [list|insights|create|update] [--account-id <id>] [--id <id>] [--date-preset last_30d] [--name <name>] [--objective <obj>] [--status <status>]',
|
||||||
|
adsets: 'adsets [list] [--account-id <id>]',
|
||||||
|
ads: 'ads [list] --adset-id <id>',
|
||||||
|
audiences: 'audiences [list|create-lookalike] [--account-id <id>] [--source-id <id>] [--country US]',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
248
tools/clis/mixpanel.js
Executable file
248
tools/clis/mixpanel.js
Executable file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const TOKEN = process.env.MIXPANEL_TOKEN
|
||||||
|
const API_KEY = process.env.MIXPANEL_API_KEY
|
||||||
|
const SECRET = process.env.MIXPANEL_SECRET
|
||||||
|
const INGESTION_URL = 'https://api.mixpanel.com'
|
||||||
|
const QUERY_URL = 'https://mixpanel.com/api/2.0'
|
||||||
|
const EXPORT_URL = 'https://data.mixpanel.com/api/2.0'
|
||||||
|
|
||||||
|
if (!TOKEN && !API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'MIXPANEL_TOKEN (for ingestion) or MIXPANEL_API_KEY + MIXPANEL_SECRET (for query/export) environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ingestApi(method, path, body) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' }
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedBody = body ? JSON.parse(JSON.stringify(body)) : undefined
|
||||||
|
if (Array.isArray(maskedBody)) maskedBody.forEach(item => {
|
||||||
|
if (item.properties && item.properties.token) item.properties.token = '***'
|
||||||
|
if (item.$token) item.$token = '***'
|
||||||
|
})
|
||||||
|
return { _dry_run: true, method, url: `${INGESTION_URL}${path}`, headers, body: maskedBody }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${INGESTION_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryApi(method, baseUrl, path, params) {
|
||||||
|
if (!API_KEY || !SECRET) {
|
||||||
|
return { error: 'MIXPANEL_API_KEY and MIXPANEL_SECRET required for query/export operations' }
|
||||||
|
}
|
||||||
|
const auth = Buffer.from(`${API_KEY}:${SECRET}`).toString('base64')
|
||||||
|
const url = params ? `${baseUrl}${path}?${params}` : `${baseUrl}${path}`
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url, headers: { ...headers, Authorization: '***' } }
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryApiPost(path, body) {
|
||||||
|
if (!API_KEY || !SECRET) {
|
||||||
|
return { error: 'MIXPANEL_API_KEY and MIXPANEL_SECRET required for query/export operations' }
|
||||||
|
}
|
||||||
|
const auth = Buffer.from(`${API_KEY}:${SECRET}`).toString('base64')
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method: 'POST', url: `${QUERY_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${QUERY_URL}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'track':
|
||||||
|
switch (sub) {
|
||||||
|
case 'event': {
|
||||||
|
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for tracking' }; break }
|
||||||
|
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
|
||||||
|
if (!args.event) { result = { error: '--event required' }; break }
|
||||||
|
let properties
|
||||||
|
try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break }
|
||||||
|
properties.token = TOKEN
|
||||||
|
properties.distinct_id = args['distinct-id']
|
||||||
|
result = await ingestApi('POST', '/track', [{
|
||||||
|
event: args.event,
|
||||||
|
properties,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown track subcommand. Use: event' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'profiles':
|
||||||
|
switch (sub) {
|
||||||
|
case 'set': {
|
||||||
|
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for profiles' }; break }
|
||||||
|
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
|
||||||
|
let properties
|
||||||
|
try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break }
|
||||||
|
result = await ingestApi('POST', '/engage', [{
|
||||||
|
$token: TOKEN,
|
||||||
|
$distinct_id: args['distinct-id'],
|
||||||
|
$set: properties,
|
||||||
|
}])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown profiles subcommand. Use: set' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'query':
|
||||||
|
switch (sub) {
|
||||||
|
case 'events': {
|
||||||
|
if (!args['project-id']) { result = { error: '--project-id required' }; break }
|
||||||
|
const body = {
|
||||||
|
project_id: parseInt(args['project-id']),
|
||||||
|
bookmark_id: null,
|
||||||
|
params: {
|
||||||
|
events: [{ event: args.event || 'all' }],
|
||||||
|
time_range: {
|
||||||
|
from_date: args['from-date'] || new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10),
|
||||||
|
to_date: args['to-date'] || new Date().toISOString().slice(0, 10),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result = await queryApiPost('/insights', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown query subcommand. Use: events' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'funnels':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
if (!args['funnel-id']) { result = { error: '--funnel-id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('funnel_id', args['funnel-id'])
|
||||||
|
if (args['from-date']) params.set('from_date', args['from-date'])
|
||||||
|
if (args['to-date']) params.set('to_date', args['to-date'])
|
||||||
|
result = await queryApi('GET', QUERY_URL, '/funnels', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown funnels subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'retention':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
if (!args['from-date'] || !args['to-date']) { result = { error: '--from-date and --to-date required (YYYY-MM-DD)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('from_date', args['from-date'])
|
||||||
|
params.set('to_date', args['to-date'])
|
||||||
|
params.set('retention_type', 'birth')
|
||||||
|
if (args['born-event']) params.set('born_event', args['born-event'])
|
||||||
|
result = await queryApi('GET', QUERY_URL, '/retention', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown retention subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'export':
|
||||||
|
switch (sub) {
|
||||||
|
case 'events': {
|
||||||
|
if (!args['from-date']) { result = { error: '--from-date required' }; break }
|
||||||
|
if (!args['to-date']) { result = { error: '--to-date required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('from_date', args['from-date'])
|
||||||
|
params.set('to_date', args['to-date'])
|
||||||
|
if (args.event) params.set('event', JSON.stringify([args.event]))
|
||||||
|
result = await queryApi('GET', EXPORT_URL, '/export', params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown export subcommand. Use: events' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
track: 'track event --distinct-id <id> --event <name> [--properties <json>]',
|
||||||
|
profiles: 'profiles set --distinct-id <id> [--properties <json>]',
|
||||||
|
query: 'query events --project-id <id> [--event <name>] [--from-date <date>] [--to-date <date>]',
|
||||||
|
funnels: 'funnels get --funnel-id <id> [--from-date <date>] [--to-date <date>]',
|
||||||
|
retention: 'retention get [--from-date <date>] [--to-date <date>] [--born-event <event>]',
|
||||||
|
export: 'export events --from-date <date> --to-date <date>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
241
tools/clis/onesignal.js
Executable file
241
tools/clis/onesignal.js
Executable file
|
|
@ -0,0 +1,241 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const REST_API_KEY = process.env.ONESIGNAL_REST_API_KEY
|
||||||
|
const APP_ID = process.env.ONESIGNAL_APP_ID
|
||||||
|
const BASE_URL = 'https://api.onesignal.com'
|
||||||
|
|
||||||
|
if (!REST_API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'ONESIGNAL_REST_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!APP_ID) {
|
||||||
|
console.error(JSON.stringify({ error: 'ONESIGNAL_APP_ID environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Basic ${REST_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { ...headers, Authorization: '***' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const limit = args.limit ? Number(args.limit) : 50
|
||||||
|
const offset = args.offset ? Number(args.offset) : 0
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'notifications':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
const message = args.message
|
||||||
|
if (!message) { result = { error: '--message required' }; break }
|
||||||
|
const payload = {
|
||||||
|
app_id: APP_ID,
|
||||||
|
contents: { en: message },
|
||||||
|
}
|
||||||
|
if (args.heading) payload.headings = { en: args.heading }
|
||||||
|
if (args.url) payload.url = args.url
|
||||||
|
if (args.data) {
|
||||||
|
try { payload.data = JSON.parse(args.data) } catch { payload.data = { value: args.data } }
|
||||||
|
}
|
||||||
|
if (args.segment) {
|
||||||
|
payload.included_segments = args.segment.split(',')
|
||||||
|
} else if (args.emails) {
|
||||||
|
payload.include_email_tokens = args.emails.split(',')
|
||||||
|
} else if (args['player-ids']) {
|
||||||
|
payload.include_player_ids = args['player-ids'].split(',')
|
||||||
|
} else if (args.aliases) {
|
||||||
|
try {
|
||||||
|
payload.include_aliases = JSON.parse(args.aliases)
|
||||||
|
} catch {
|
||||||
|
payload.include_aliases = { external_id: args.aliases.split(',') }
|
||||||
|
}
|
||||||
|
payload.target_channel = args.channel || 'push'
|
||||||
|
} else {
|
||||||
|
payload.included_segments = ['Subscribed Users']
|
||||||
|
}
|
||||||
|
if (args['send-after']) payload.send_after = args['send-after']
|
||||||
|
if (args.ttl) payload.ttl = Number(args.ttl)
|
||||||
|
result = await api('POST', '/api/v1/notifications', payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
result = await api('GET', `/api/v1/notifications?app_id=${APP_ID}&limit=${limit}&offset=${offset}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/api/v1/notifications/${id}?app_id=${APP_ID}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'cancel': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/api/v1/notifications/${id}?app_id=${APP_ID}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown notifications subcommand. Use: send, list, get, cancel' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'segments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
result = await api('GET', `/api/v1/apps/${APP_ID}/segments?offset=${offset}&limit=${limit}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
let filters
|
||||||
|
try { filters = args.filters ? JSON.parse(args.filters) : [{ field: 'session_count', relation: '>', value: '0' }] } catch { result = { error: 'Invalid JSON in --filters' }; break }
|
||||||
|
result = await api('POST', `/api/v1/apps/${APP_ID}/segments`, { name, filters })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/api/v1/apps/${APP_ID}/segments/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown segments subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'users':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const aliasLabel = args['alias-label'] || 'external_id'
|
||||||
|
const aliasId = args['alias-id']
|
||||||
|
if (!aliasId) { result = { error: '--alias-id required' }; break }
|
||||||
|
result = await api('GET', `/api/v1/apps/${APP_ID}/users/by/${aliasLabel}/${aliasId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const payload = {}
|
||||||
|
if (args['external-id']) {
|
||||||
|
payload.identity = { external_id: args['external-id'] }
|
||||||
|
}
|
||||||
|
if (args.email) {
|
||||||
|
payload.subscriptions = [{ type: 'Email', token: args.email }]
|
||||||
|
}
|
||||||
|
if (args.tags) {
|
||||||
|
try { payload.tags = JSON.parse(args.tags) } catch { result = { error: 'Invalid --tags JSON' }; break }
|
||||||
|
}
|
||||||
|
result = await api('POST', `/api/v1/apps/${APP_ID}/users`, payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const aliasLabel = args['alias-label'] || 'external_id'
|
||||||
|
const aliasId = args['alias-id']
|
||||||
|
if (!aliasId) { result = { error: '--alias-id required' }; break }
|
||||||
|
result = await api('DELETE', `/api/v1/apps/${APP_ID}/users/by/${aliasLabel}/${aliasId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown users subcommand. Use: get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'templates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
result = await api('GET', `/api/v1/templates?app_id=${APP_ID}&limit=${limit}&offset=${offset}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/api/v1/templates/${id}?app_id=${APP_ID}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const payload = { app_id: APP_ID, name }
|
||||||
|
if (args.message) payload.contents = { en: args.message }
|
||||||
|
if (args.heading) payload.headings = { en: args.heading }
|
||||||
|
result = await api('POST', '/api/v1/templates', payload)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown templates subcommand. Use: list, get, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'app':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
result = await api('GET', `/api/v1/apps/${APP_ID}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown app subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
notifications: 'notifications [send --message <msg> --segment <s> | list | get --id <id> | cancel --id <id>]',
|
||||||
|
segments: 'segments [list | create --name <n> --filters <json> | delete --id <id>]',
|
||||||
|
users: 'users [get --alias-id <id> | create --external-id <id> --email <e> | delete --alias-id <id>]',
|
||||||
|
templates: 'templates [list | get --id <id> | create --name <n> --message <msg>]',
|
||||||
|
app: 'app [get]',
|
||||||
|
options: '--limit <n> --offset <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
233
tools/clis/optimizely.js
Executable file
233
tools/clis/optimizely.js
Executable file
|
|
@ -0,0 +1,233 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.OPTIMIZELY_API_KEY
|
||||||
|
const BASE_URL = 'https://api.optimizely.com/v2'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'OPTIMIZELY_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const projectId = args['project-id']
|
||||||
|
const page = args.page ? Number(args.page) : 1
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : 25
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'projects':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/projects?page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/projects/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { name }
|
||||||
|
if (args.platform) body.platform = args.platform
|
||||||
|
result = await api('POST', '/projects', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown projects subcommand. Use: list, get, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'experiments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
const params = new URLSearchParams({ project_id: projectId, page: String(page), per_page: String(perPage) })
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/experiments?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/experiments/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = {
|
||||||
|
project_id: Number(projectId),
|
||||||
|
name,
|
||||||
|
type: args.type || 'a/b',
|
||||||
|
status: 'not_started',
|
||||||
|
}
|
||||||
|
if (args['traffic-allocation']) body.traffic_allocation = Number(args['traffic-allocation'])
|
||||||
|
result = await api('POST', '/experiments', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
if (args['traffic-allocation']) body.traffic_allocation = Number(args['traffic-allocation'])
|
||||||
|
result = await api('PATCH', `/experiments/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'results': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['start-time']) params.set('start_time', args['start-time'])
|
||||||
|
if (args['end-time']) params.set('end_time', args['end-time'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/experiments/${id}/results${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'archive': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('PATCH', `/experiments/${id}`, { status: 'archived' })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown experiments subcommand. Use: list, get, create, update, results, archive' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns?project_id=${projectId}&page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'results': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/campaigns/${id}/results`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get, results' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audiences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
result = await api('GET', `/audiences?project_id=${projectId}&page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/audiences/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown audiences subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
result = await api('GET', `/events?project_id=${projectId}&page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pages':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!projectId) { result = { error: '--project-id required' }; break }
|
||||||
|
result = await api('GET', `/pages?project_id=${projectId}&page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown pages subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
projects: 'projects [list | get --id <id> | create --name <name>]',
|
||||||
|
experiments: 'experiments [list --project-id <id> | get --id <id> | create --project-id <id> --name <name> | update --id <id> --status <status> | results --id <id> | archive --id <id>]',
|
||||||
|
campaigns: 'campaigns [list --project-id <id> | get --id <id> | results --id <id>]',
|
||||||
|
audiences: 'audiences [list --project-id <id> | get --id <id>]',
|
||||||
|
events: 'events list --project-id <id>',
|
||||||
|
pages: 'pages list --project-id <id>',
|
||||||
|
options: '--page <n> --per-page <n> --status <not_started|running|paused|archived>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
385
tools/clis/paddle.js
Executable file
385
tools/clis/paddle.js
Executable file
|
|
@ -0,0 +1,385 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.PADDLE_API_KEY
|
||||||
|
const BASE_URL = process.env.PADDLE_SANDBOX === 'true'
|
||||||
|
? 'https://sandbox-api.paddle.com'
|
||||||
|
: 'https://api.paddle.com'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'PADDLE_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
function buildQuery() {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args['per-page']) params.set('per_page', args['per-page'])
|
||||||
|
if (args['order-by']) params.set('order_by', args['order-by'])
|
||||||
|
return params.toString() ? `?${params.toString()}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'products':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/products${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/products/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
const taxCategory = args['tax-category']
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
if (!taxCategory) { result = { error: '--tax-category required (e.g. standard, digital-goods, saas)' }; break }
|
||||||
|
const body = { name, tax_category: taxCategory }
|
||||||
|
if (args.description) body.description = args.description
|
||||||
|
if (args['image-url']) body.image_url = args['image-url']
|
||||||
|
result = await api('POST', '/products', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.description) body.description = args.description
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
if (args['tax-category']) body.tax_category = args['tax-category']
|
||||||
|
result = await api('PATCH', `/products/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown products subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'prices':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/prices${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/prices/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const productId = args['product-id']
|
||||||
|
const amount = args.amount
|
||||||
|
const currency = args.currency || 'USD'
|
||||||
|
if (!productId) { result = { error: '--product-id required' }; break }
|
||||||
|
if (!amount) { result = { error: '--amount required (in lowest denomination, e.g. cents)' }; break }
|
||||||
|
const body = {
|
||||||
|
product_id: productId,
|
||||||
|
description: args.description || 'Price',
|
||||||
|
unit_price: { amount, currency_code: currency },
|
||||||
|
}
|
||||||
|
if (args.interval && args.frequency) {
|
||||||
|
body.billing_cycle = {
|
||||||
|
interval: args.interval,
|
||||||
|
frequency: Number(args.frequency),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = await api('POST', '/prices', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.description) body.description = args.description
|
||||||
|
if (args.amount && args.currency) {
|
||||||
|
body.unit_price = { amount: args.amount, currency_code: args.currency }
|
||||||
|
}
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
result = await api('PATCH', `/prices/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown prices subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'customers':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/customers${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/customers/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email }
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
result = await api('POST', '/customers', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
result = await api('PATCH', `/customers/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown customers subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'subscriptions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/subscriptions${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/subscriptions/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['proration-billing-mode']) body.proration_billing_mode = args['proration-billing-mode']
|
||||||
|
if (args['scheduled-change']) {
|
||||||
|
try { body.scheduled_change = JSON.parse(args['scheduled-change']) } catch { result = { error: 'Invalid JSON in --scheduled-change' }; break }
|
||||||
|
}
|
||||||
|
result = await api('PATCH', `/subscriptions/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'cancel': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = { effective_from: args['effective-from'] || 'next_billing_period' }
|
||||||
|
result = await api('POST', `/subscriptions/${id}/cancel`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pause': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['resume-at']) body.resume_at = args['resume-at']
|
||||||
|
result = await api('POST', `/subscriptions/${id}/pause`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'resume': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = { effective_from: args['effective-from'] || 'immediately' }
|
||||||
|
result = await api('POST', `/subscriptions/${id}/resume`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown subscriptions subcommand. Use: list, get, update, cancel, pause, resume' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'transactions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/transactions${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/transactions/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const items = args.items
|
||||||
|
if (!items) { result = { error: '--items required (JSON array of {price_id, quantity})' }; break }
|
||||||
|
let parsedItems
|
||||||
|
try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break }
|
||||||
|
const body = { items: parsedItems }
|
||||||
|
if (args['customer-id']) body.customer_id = args['customer-id']
|
||||||
|
result = await api('POST', '/transactions', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown transactions subcommand. Use: list, get, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'discounts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/discounts${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/discounts/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const amount = args.amount
|
||||||
|
const type = args.type
|
||||||
|
if (!amount) { result = { error: '--amount required' }; break }
|
||||||
|
if (!type) { result = { error: '--type required (flat, flat_per_seat, percentage)' }; break }
|
||||||
|
const body = {
|
||||||
|
amount,
|
||||||
|
type,
|
||||||
|
description: args.description || 'Discount',
|
||||||
|
}
|
||||||
|
if (args.code) body.code = args.code
|
||||||
|
if (args['max-uses']) body.maximum_recurring_intervals = Number(args['max-uses'])
|
||||||
|
if (args['currency-code']) body.currency_code = args['currency-code']
|
||||||
|
result = await api('POST', '/discounts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown discounts subcommand. Use: list, get, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'adjustments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/adjustments${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const transactionId = args['transaction-id']
|
||||||
|
const action = args.action
|
||||||
|
const items = args.items
|
||||||
|
const reason = args.reason
|
||||||
|
if (!transactionId) { result = { error: '--transaction-id required' }; break }
|
||||||
|
if (!action) { result = { error: '--action required (refund, credit, chargeback)' }; break }
|
||||||
|
if (!reason) { result = { error: '--reason required' }; break }
|
||||||
|
if (!items) { result = { error: '--items required (JSON array of {item_id, type, amount})' }; break }
|
||||||
|
let parsedItems
|
||||||
|
try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break }
|
||||||
|
result = await api('POST', '/adjustments', {
|
||||||
|
transaction_id: transactionId,
|
||||||
|
action,
|
||||||
|
reason,
|
||||||
|
items: parsedItems,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown adjustments subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/events${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'types':
|
||||||
|
result = await api('GET', '/event-types')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, types' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'notifications':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/notifications${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/notifications/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'replay': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/notifications/${id}/replay`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown notifications subcommand. Use: list, get, replay' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
products: 'products [list | get --id <id> | create --name <n> --tax-category <cat> | update --id <id>]',
|
||||||
|
prices: 'prices [list | get --id <id> | create --product-id <id> --amount <amt> [--currency USD] [--interval month --frequency 1] | update --id <id>]',
|
||||||
|
customers: 'customers [list | get --id <id> | create --email <email> [--name <name>] | update --id <id>]',
|
||||||
|
subscriptions: 'subscriptions [list | get --id <id> | update --id <id> | cancel --id <id> [--effective-from next_billing_period] | pause --id <id> | resume --id <id>]',
|
||||||
|
transactions: 'transactions [list | get --id <id> | create --items <json>]',
|
||||||
|
discounts: 'discounts [list | get --id <id> | create --amount <amt> --type <type> [--code <code>]]',
|
||||||
|
adjustments: 'adjustments [list | create --transaction-id <id> --action <action> --reason <reason> --items <json>]',
|
||||||
|
events: 'events [list | types]',
|
||||||
|
notifications: 'notifications [list | get --id <id> | replay --id <id>]',
|
||||||
|
env: 'Set PADDLE_SANDBOX=true for sandbox environment',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
382
tools/clis/partnerstack.js
Executable file
382
tools/clis/partnerstack.js
Executable file
|
|
@ -0,0 +1,382 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const PUBLIC_KEY = process.env.PARTNERSTACK_PUBLIC_KEY
|
||||||
|
const SECRET_KEY = process.env.PARTNERSTACK_SECRET_KEY
|
||||||
|
const BASE_URL = 'https://api.partnerstack.com/api/v2'
|
||||||
|
|
||||||
|
if (!PUBLIC_KEY || !SECRET_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'PARTNERSTACK_PUBLIC_KEY and PARTNERSTACK_SECRET_KEY environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH = 'Basic ' + Buffer.from(`${PUBLIC_KEY}:${SECRET_KEY}`).toString('base64')
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': AUTH,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
const limit = args.limit ? Number(args.limit) : 10
|
||||||
|
|
||||||
|
function buildQuery() {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.after) params.set('starting_after', args.after)
|
||||||
|
if (args.before) params.set('ending_before', args.before)
|
||||||
|
if (args['order-by']) params.set('order_by', args['order-by'])
|
||||||
|
return params.toString() ? `?${params.toString()}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'partnerships':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/partnerships${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (partnership key)' }; break }
|
||||||
|
result = await api('GET', `/partnerships/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
const group = args.group
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
if (!group) { result = { error: '--group required (group key)' }; break }
|
||||||
|
const body = { email, group_key: group }
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
result = await api('POST', '/partnerships', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (partnership key)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.group) body.group_key = args.group
|
||||||
|
result = await api('PATCH', `/partnerships/${key}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown partnerships subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'customers':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/customers${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (customer key)' }; break }
|
||||||
|
result = await api('GET', `/customers/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
const partnerKey = args['partner-key']
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
if (!partnerKey) { result = { error: '--partner-key required' }; break }
|
||||||
|
const body = { email, partner_key: partnerKey }
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args['customer-key']) body.customer_key = args['customer-key']
|
||||||
|
result = await api('POST', '/customers', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (customer key)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args['partner-key']) body.partner_key = args['partner-key']
|
||||||
|
result = await api('PATCH', `/customers/${key}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (customer key)' }; break }
|
||||||
|
result = await api('DELETE', `/customers/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown customers subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'transactions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/transactions${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (transaction key)' }; break }
|
||||||
|
result = await api('GET', `/transactions/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const customerKey = args['customer-key']
|
||||||
|
const amount = args.amount
|
||||||
|
if (!customerKey) { result = { error: '--customer-key required' }; break }
|
||||||
|
if (!amount) { result = { error: '--amount required (in cents)' }; break }
|
||||||
|
const body = {
|
||||||
|
customer_key: customerKey,
|
||||||
|
amount: Number(amount),
|
||||||
|
}
|
||||||
|
if (args.currency) body.currency = args.currency
|
||||||
|
if (args.category) body.category = args.category
|
||||||
|
if (args['product-key']) body.product_key = args['product-key']
|
||||||
|
result = await api('POST', '/transactions', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (transaction key)' }; break }
|
||||||
|
result = await api('DELETE', `/transactions/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown transactions subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deals':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/deals${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (deal key)' }; break }
|
||||||
|
result = await api('GET', `/deals/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const partnerKey = args['partner-key']
|
||||||
|
const name = args.name
|
||||||
|
if (!partnerKey) { result = { error: '--partner-key required' }; break }
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { partner_key: partnerKey, name }
|
||||||
|
if (args.amount) body.amount = Number(args.amount)
|
||||||
|
if (args.currency) body.currency = args.currency
|
||||||
|
if (args.stage) body.stage = args.stage
|
||||||
|
result = await api('POST', '/deals', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (deal key)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.amount) body.amount = Number(args.amount)
|
||||||
|
if (args.stage) body.stage = args.stage
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
result = await api('PATCH', `/deals/${key}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'archive': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (deal key)' }; break }
|
||||||
|
result = await api('DELETE', `/deals/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown deals subcommand. Use: list, get, create, update, archive' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'actions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/actions${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const customerKey = args['customer-key']
|
||||||
|
const actionKey = args['action-key']
|
||||||
|
if (!customerKey) { result = { error: '--customer-key required' }; break }
|
||||||
|
if (!actionKey) { result = { error: '--action-key required' }; break }
|
||||||
|
const body = {
|
||||||
|
customer_key: customerKey,
|
||||||
|
key: actionKey,
|
||||||
|
}
|
||||||
|
if (args.value) body.value = Number(args.value)
|
||||||
|
result = await api('POST', '/actions', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown actions subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'rewards':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/rewards${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const partnerKey = args['partner-key']
|
||||||
|
const amount = args.amount
|
||||||
|
if (!partnerKey) { result = { error: '--partner-key required' }; break }
|
||||||
|
if (!amount) { result = { error: '--amount required (in cents)' }; break }
|
||||||
|
const body = {
|
||||||
|
partner_key: partnerKey,
|
||||||
|
amount: Number(amount),
|
||||||
|
}
|
||||||
|
if (args.description) body.description = args.description
|
||||||
|
if (args.currency) body.currency = args.currency
|
||||||
|
result = await api('POST', '/rewards', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown rewards subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'leads':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/leads${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (lead key)' }; break }
|
||||||
|
result = await api('GET', `/leads/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const partnerKey = args['partner-key']
|
||||||
|
const email = args.email
|
||||||
|
if (!partnerKey) { result = { error: '--partner-key required' }; break }
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { partner_key: partnerKey, email }
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.company) body.company = args.company
|
||||||
|
result = await api('POST', '/leads', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (lead key)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.status) body.status = args.status
|
||||||
|
result = await api('PATCH', `/leads/${key}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown leads subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'groups':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/groups${buildQuery()}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown groups subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/webhooks${buildQuery()}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (webhook key)' }; break }
|
||||||
|
result = await api('GET', `/webhooks/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const target = args.target
|
||||||
|
if (!target) { result = { error: '--target required (webhook URL)' }; break }
|
||||||
|
const body = { target }
|
||||||
|
if (args.events) body.events = args.events.split(',')
|
||||||
|
result = await api('POST', '/webhooks', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (webhook key)' }; break }
|
||||||
|
result = await api('DELETE', `/webhooks/${key}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
partnerships: 'partnerships [list | get --key <key> | create --email <email> --group <group-key> | update --key <key>]',
|
||||||
|
customers: 'customers [list | get --key <key> | create --email <email> --partner-key <key> | update --key <key> | delete --key <key>]',
|
||||||
|
transactions: 'transactions [list | get --key <key> | create --customer-key <key> --amount <cents> | delete --key <key>]',
|
||||||
|
deals: 'deals [list | get --key <key> | create --partner-key <key> --name <name> | update --key <key> | archive --key <key>]',
|
||||||
|
actions: 'actions [list | create --customer-key <key> --action-key <key> [--value <n>]]',
|
||||||
|
rewards: 'rewards [list | create --partner-key <key> --amount <cents>]',
|
||||||
|
leads: 'leads [list | get --key <key> | create --partner-key <key> --email <email> | update --key <key>]',
|
||||||
|
groups: 'groups [list]',
|
||||||
|
webhooks: 'webhooks [list | get --key <key> | create --target <url> [--events <evt1,evt2>] | delete --key <key>]',
|
||||||
|
options: '--limit <n> --after <cursor> --before <cursor> --order-by <field>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
249
tools/clis/plausible.js
Executable file
249
tools/clis/plausible.js
Executable file
|
|
@ -0,0 +1,249 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.PLAUSIBLE_API_KEY
|
||||||
|
const BASE_URL = process.env.PLAUSIBLE_BASE_URL || 'https://plausible.io'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'PLAUSIBLE_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const siteId = args['site-id']
|
||||||
|
const dateRange = args['date-range'] || '30d'
|
||||||
|
const limit = args.limit ? Number(args.limit) : 100
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'stats':
|
||||||
|
if (!siteId) { result = { error: '--site-id required (your domain, e.g. example.com)' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'aggregate': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'pageviews', 'bounce_rate', 'visit_duration']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'timeseries': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'pageviews']
|
||||||
|
const period = args.period || 'time:day'
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: [period],
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'pages': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'pageviews']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: ['event:page'],
|
||||||
|
pagination: { limit },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'sources': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'bounce_rate']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: ['visit:source'],
|
||||||
|
pagination: { limit },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'countries': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'percentage']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: ['visit:country'],
|
||||||
|
pagination: { limit },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'devices': {
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'percentage']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: ['visit:device'],
|
||||||
|
pagination: { limit },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'utm': {
|
||||||
|
const param = args.param || 'utm_source'
|
||||||
|
const metrics = args.metrics?.split(',') || ['visitors', 'bounce_rate']
|
||||||
|
result = await api('POST', '/api/v2/query', {
|
||||||
|
site_id: siteId,
|
||||||
|
metrics,
|
||||||
|
date_range: dateRange,
|
||||||
|
dimensions: [`visit:${param}`],
|
||||||
|
pagination: { limit },
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'query': {
|
||||||
|
const metrics = args.metrics?.split(',')
|
||||||
|
if (!metrics) { result = { error: '--metrics required (comma-separated)' }; break }
|
||||||
|
const body = { site_id: siteId, metrics, date_range: dateRange }
|
||||||
|
if (args.dimensions) body.dimensions = args.dimensions.split(',')
|
||||||
|
if (args.filters) {
|
||||||
|
try { body.filters = JSON.parse(args.filters) } catch { result = { error: '--filters must be valid JSON' }; break }
|
||||||
|
}
|
||||||
|
body.pagination = { limit }
|
||||||
|
result = await api('POST', '/api/v2/query', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'realtime':
|
||||||
|
result = await api('GET', `/api/v1/stats/realtime/visitors?site_id=${encodeURIComponent(siteId)}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown stats subcommand. Use: aggregate, timeseries, pages, sources, countries, devices, utm, query, realtime' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'sites':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/api/v1/sites')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
result = await api('GET', `/api/v1/sites/${encodeURIComponent(siteId)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
const body = { domain }
|
||||||
|
if (args.timezone) body.timezone = args.timezone
|
||||||
|
result = await api('POST', '/api/v1/sites', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
result = await api('DELETE', `/api/v1/sites/${encodeURIComponent(siteId)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown sites subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'goals':
|
||||||
|
if (!siteId) { result = { error: '--site-id required' }; break }
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/api/v1/sites/goals?site_id=${encodeURIComponent(siteId)}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const goalType = args['goal-type']
|
||||||
|
if (!goalType) { result = { error: '--goal-type required (event or page)' }; break }
|
||||||
|
const body = { site_id: siteId, goal_type: goalType }
|
||||||
|
if (goalType === 'event') {
|
||||||
|
if (!args['event-name']) { result = { error: '--event-name required for event goals' }; break }
|
||||||
|
body.event_name = args['event-name']
|
||||||
|
} else if (goalType === 'page') {
|
||||||
|
if (!args['page-path']) { result = { error: '--page-path required for page goals' }; break }
|
||||||
|
body.page_path = args['page-path']
|
||||||
|
}
|
||||||
|
result = await api('PUT', '/api/v1/sites/goals', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const goalId = args['goal-id']
|
||||||
|
if (!goalId) { result = { error: '--goal-id required' }; break }
|
||||||
|
result = await api('DELETE', `/api/v1/sites/goals/${goalId}`, { site_id: siteId })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown goals subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
stats: {
|
||||||
|
aggregate: 'stats aggregate --site-id <domain> [--date-range <30d>] [--metrics <m1,m2>]',
|
||||||
|
timeseries: 'stats timeseries --site-id <domain> [--date-range <30d>] [--period <time:day>]',
|
||||||
|
pages: 'stats pages --site-id <domain> [--date-range <30d>] [--limit <n>]',
|
||||||
|
sources: 'stats sources --site-id <domain> [--date-range <30d>]',
|
||||||
|
countries: 'stats countries --site-id <domain> [--date-range <30d>]',
|
||||||
|
devices: 'stats devices --site-id <domain> [--date-range <30d>]',
|
||||||
|
utm: 'stats utm --site-id <domain> [--param <utm_source>] [--date-range <30d>]',
|
||||||
|
query: 'stats query --site-id <domain> --metrics <m1,m2> [--dimensions <d1,d2>] [--filters <json>]',
|
||||||
|
realtime: 'stats realtime --site-id <domain>',
|
||||||
|
},
|
||||||
|
sites: 'sites [list | get --site-id <domain> | create --domain <domain> | delete --site-id <domain>]',
|
||||||
|
goals: 'goals [list | create --goal-type <event|page> --event-name <name> | delete --goal-id <id>] --site-id <domain>',
|
||||||
|
options: '--date-range <day|7d|30d|month|6mo|12mo|year> --limit <n>',
|
||||||
|
env: 'PLAUSIBLE_BASE_URL for self-hosted instances (default: https://plausible.io)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
375
tools/clis/postmark.js
Executable file
375
tools/clis/postmark.js
Executable file
|
|
@ -0,0 +1,375 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.POSTMARK_API_KEY
|
||||||
|
const BASE_URL = 'https://api.postmarkapp.com'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'POSTMARK_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body, useAccountToken) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
|
||||||
|
if (useAccountToken) {
|
||||||
|
maskedHeaders['X-Postmark-Account-Token'] = '***'
|
||||||
|
} else {
|
||||||
|
maskedHeaders['X-Postmark-Server-Token'] = '***'
|
||||||
|
}
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: maskedHeaders, body: body || undefined }
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (useAccountToken) {
|
||||||
|
headers['X-Postmark-Account-Token'] = API_KEY
|
||||||
|
} else {
|
||||||
|
headers['X-Postmark-Server-Token'] = API_KEY
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'email':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
const from = args.from
|
||||||
|
const to = args.to
|
||||||
|
const subject = args.subject
|
||||||
|
if (!from) { result = { error: '--from required' }; break }
|
||||||
|
if (!to) { result = { error: '--to required' }; break }
|
||||||
|
if (!subject) { result = { error: '--subject required' }; break }
|
||||||
|
const body = {
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Subject: subject,
|
||||||
|
}
|
||||||
|
if (args.html) body.HtmlBody = args.html
|
||||||
|
if (args.text) body.TextBody = args.text
|
||||||
|
if (!args.html && !args.text) body.TextBody = ''
|
||||||
|
if (args.tag) body.Tag = args.tag
|
||||||
|
if (args.stream) body.MessageStream = args.stream
|
||||||
|
if (args['track-opens']) body.TrackOpens = true
|
||||||
|
if (args['track-links']) body.TrackLinks = args['track-links']
|
||||||
|
if (args.cc) body.Cc = args.cc
|
||||||
|
if (args.bcc) body.Bcc = args.bcc
|
||||||
|
if (args['reply-to']) body.ReplyTo = args['reply-to']
|
||||||
|
result = await api('POST', '/email', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send-template': {
|
||||||
|
const from = args.from
|
||||||
|
const to = args.to
|
||||||
|
const template = args.template
|
||||||
|
if (!from) { result = { error: '--from required' }; break }
|
||||||
|
if (!to) { result = { error: '--to required' }; break }
|
||||||
|
if (!template) { result = { error: '--template required (template ID or alias)' }; break }
|
||||||
|
const body = {
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
TemplateModel: {},
|
||||||
|
}
|
||||||
|
const templateNum = Number(template)
|
||||||
|
if (!isNaN(templateNum)) {
|
||||||
|
body.TemplateId = templateNum
|
||||||
|
} else {
|
||||||
|
body.TemplateAlias = template
|
||||||
|
}
|
||||||
|
if (args.model) {
|
||||||
|
const pairs = args.model.split(',')
|
||||||
|
for (const pair of pairs) {
|
||||||
|
const [k, v] = pair.split(':')
|
||||||
|
if (k && v) body.TemplateModel[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (args.stream) body.MessageStream = args.stream
|
||||||
|
if (args.tag) body.Tag = args.tag
|
||||||
|
result = await api('POST', '/email/withTemplate', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send-batch': {
|
||||||
|
const from = args.from
|
||||||
|
const to = args.to
|
||||||
|
const subject = args.subject
|
||||||
|
if (!from || !to || !subject) {
|
||||||
|
result = { error: '--from, --to (comma-separated), and --subject required' }; break
|
||||||
|
}
|
||||||
|
const recipients = to.split(',')
|
||||||
|
const messages = recipients.map(recipient => ({
|
||||||
|
From: from,
|
||||||
|
To: recipient.trim(),
|
||||||
|
Subject: subject,
|
||||||
|
TextBody: args.text || '',
|
||||||
|
HtmlBody: args.html || undefined,
|
||||||
|
MessageStream: args.stream || undefined,
|
||||||
|
Tag: args.tag || undefined,
|
||||||
|
}))
|
||||||
|
result = await api('POST', '/email/batch', messages)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown email subcommand. Use: send, send-template, send-batch' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'templates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('Count', args.count || '100')
|
||||||
|
params.set('Offset', args.offset || '0')
|
||||||
|
if (args.type) params.set('TemplateType', args.type)
|
||||||
|
result = await api('GET', `/templates?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (template ID or alias)' }; break }
|
||||||
|
result = await api('GET', `/templates/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = {
|
||||||
|
Name: name,
|
||||||
|
Subject: args.subject || '',
|
||||||
|
}
|
||||||
|
if (args.html) body.HtmlBody = args.html
|
||||||
|
if (args.text) body.TextBody = args.text
|
||||||
|
if (args.alias) body.Alias = args.alias
|
||||||
|
if (args.type) body.TemplateType = args.type
|
||||||
|
result = await api('POST', '/templates', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (template ID or alias)' }; break }
|
||||||
|
result = await api('DELETE', `/templates/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown templates subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'bounces':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('count', args.count || '50')
|
||||||
|
params.set('offset', args.offset || '0')
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
if (args.inactive) params.set('inactive', args.inactive)
|
||||||
|
if (args.email) params.set('emailFilter', args.email)
|
||||||
|
result = await api('GET', `/bounces?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/bounces/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'stats':
|
||||||
|
result = await api('GET', '/deliverystats')
|
||||||
|
break
|
||||||
|
case 'activate': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('PUT', `/bounces/${id}/activate`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown bounces subcommand. Use: list, get, stats, activate' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'messages':
|
||||||
|
switch (sub) {
|
||||||
|
case 'outbound': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('count', args.count || '50')
|
||||||
|
params.set('offset', args.offset || '0')
|
||||||
|
if (args.recipient) params.set('recipient', args.recipient)
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/messages/outbound?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'inbound': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('count', args.count || '50')
|
||||||
|
params.set('offset', args.offset || '0')
|
||||||
|
if (args.recipient) params.set('recipient', args.recipient)
|
||||||
|
if (args.status) params.set('status', args.status)
|
||||||
|
result = await api('GET', `/messages/inbound?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (message ID)' }; break }
|
||||||
|
result = await api('GET', `/messages/outbound/${id}/details`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown messages subcommand. Use: outbound, inbound, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stats':
|
||||||
|
switch (sub) {
|
||||||
|
case 'overview': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'sends': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound/sends?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'bounces': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound/bounces?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'opens': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound/opens?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'clicks': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound/clicks?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'spam': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.tag) params.set('tag', args.tag)
|
||||||
|
if (args.from) params.set('fromdate', args.from)
|
||||||
|
if (args.to) params.set('todate', args.to)
|
||||||
|
result = await api('GET', `/stats/outbound/spam?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown stats subcommand. Use: overview, sends, bounces, opens, clicks, spam' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'server':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', '/server')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown server subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'suppressions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.stream) params.set('MessageStream', args.stream)
|
||||||
|
result = await api('GET', `/message-streams/${args.stream || 'outbound'}/suppressions/dump`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const stream = args.stream || 'outbound'
|
||||||
|
result = await api('POST', `/message-streams/${stream}/suppressions`, {
|
||||||
|
Suppressions: email.split(',').map(e => ({ EmailAddress: e.trim() }))
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const stream = args.stream || 'outbound'
|
||||||
|
result = await api('POST', `/message-streams/${stream}/suppressions/delete`, {
|
||||||
|
Suppressions: email.split(',').map(e => ({ EmailAddress: e.trim() }))
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown suppressions subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
email: 'email [send --from <from> --to <to> --subject <subj> | send-template --from <from> --to <to> --template <id> | send-batch --from <from> --to <to1,to2> --subject <subj>]',
|
||||||
|
templates: 'templates [list | get --id <id> | create --name <name> | delete --id <id>]',
|
||||||
|
bounces: 'bounces [list | get --id <id> | stats | activate --id <id>]',
|
||||||
|
messages: 'messages [outbound | inbound | get --id <id>]',
|
||||||
|
stats: 'stats [overview | sends | bounces | opens | clicks | spam]',
|
||||||
|
server: 'server [get]',
|
||||||
|
suppressions: 'suppressions [list | create --email <email> | delete --email <email>]',
|
||||||
|
options: '--tag <tag> --from <date> --to <date> --stream <stream-id>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
370
tools/clis/resend.js
Executable file
370
tools/clis/resend.js
Executable file
|
|
@ -0,0 +1,370 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.RESEND_API_KEY
|
||||||
|
const BASE_URL = 'https://api.resend.com'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'RESEND_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'send': {
|
||||||
|
if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break }
|
||||||
|
const body = { from: args.from, to: args.to?.split(','), subject: args.subject }
|
||||||
|
if (args.html) body.html = args.html
|
||||||
|
if (args.text) body.text = args.text
|
||||||
|
if (args.cc) body.cc = args.cc.split(',')
|
||||||
|
if (args.bcc) body.bcc = args.bcc.split(',')
|
||||||
|
if (args['reply-to']) body.reply_to = args['reply-to']
|
||||||
|
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
|
||||||
|
if (args.tags) body.tags = args.tags.split(',').map(t => {
|
||||||
|
const [name, value] = t.split(':')
|
||||||
|
return { name, value }
|
||||||
|
})
|
||||||
|
result = await api('POST', '/emails', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'emails':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/emails?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/emails/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'cancel':
|
||||||
|
result = await api('POST', `/emails/${rest[0]}/cancel`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown emails subcommand. Use: list, get, cancel' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'domains':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/domains?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/domains/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create':
|
||||||
|
result = await api('POST', '/domains', { name: args.name, region: args.region })
|
||||||
|
break
|
||||||
|
case 'verify':
|
||||||
|
result = await api('POST', `/domains/${rest[0]}/verify`)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/domains/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domains subcommand. Use: list, get, create, verify, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'api-keys':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/api-keys')
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const body = { name: args.name }
|
||||||
|
if (args.permission) body.permission = args.permission
|
||||||
|
if (args.domain_id) body.domain_id = args.domain_id
|
||||||
|
result = await api('POST', '/api-keys', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/api-keys/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown api-keys subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audiences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/audiences')
|
||||||
|
break
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/audiences/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create':
|
||||||
|
result = await api('POST', '/audiences', { name: args.name })
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/audiences/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown audiences subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'contacts': {
|
||||||
|
const audienceId = sub
|
||||||
|
if (!audienceId) { result = { error: 'Audience ID required as subcommand arg' }; break }
|
||||||
|
const action = rest[0]
|
||||||
|
const contactId = rest[1]
|
||||||
|
switch (action) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/audiences/${audienceId}/contacts?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
|
||||||
|
result = await api('GET', `/audiences/${audienceId}/contacts/${contactId}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const body = { email: args.email }
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args.unsubscribed) body.unsubscribed = args.unsubscribed === 'true'
|
||||||
|
result = await api('POST', `/audiences/${audienceId}/contacts`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args.unsubscribed !== undefined) body.unsubscribed = args.unsubscribed === 'true'
|
||||||
|
result = await api('PATCH', `/audiences/${audienceId}/contacts/${contactId}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
|
||||||
|
result = await api('DELETE', `/audiences/${audienceId}/contacts/${contactId}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown contacts action. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/webhooks')
|
||||||
|
break
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/webhooks/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
if (!args.url) { result = { error: '--url required (webhook URL)' }; break }
|
||||||
|
const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced']
|
||||||
|
result = await api('POST', '/webhooks', { url: args.url, events })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/webhooks/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'batch': {
|
||||||
|
let emails
|
||||||
|
try {
|
||||||
|
emails = JSON.parse(args.emails || '[]')
|
||||||
|
} catch (e) {
|
||||||
|
result = { error: 'Invalid JSON for --emails: ' + e.message }; break
|
||||||
|
}
|
||||||
|
result = await api('POST', '/emails/batch', emails)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'templates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args.before) params.set('before', args.before)
|
||||||
|
result = await api('GET', `/templates?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/templates/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const body = { name: args.name, html: args.html }
|
||||||
|
if (args.alias) body.alias = args.alias
|
||||||
|
if (args.from) body.from = args.from
|
||||||
|
if (args.subject) body.subject = args.subject
|
||||||
|
if (args['reply-to']) body.reply_to = args['reply-to']
|
||||||
|
if (args.text) body.text = args.text
|
||||||
|
if (args.variables) {
|
||||||
|
try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break }
|
||||||
|
}
|
||||||
|
result = await api('POST', '/templates', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.html) body.html = args.html
|
||||||
|
if (args.alias) body.alias = args.alias
|
||||||
|
if (args.from) body.from = args.from
|
||||||
|
if (args.subject) body.subject = args.subject
|
||||||
|
if (args['reply-to']) body.reply_to = args['reply-to']
|
||||||
|
if (args.text) body.text = args.text
|
||||||
|
if (args.variables) {
|
||||||
|
try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break }
|
||||||
|
}
|
||||||
|
result = await api('PATCH', `/templates/${rest[0]}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/templates/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'publish':
|
||||||
|
result = await api('POST', `/templates/${rest[0]}/publish`)
|
||||||
|
break
|
||||||
|
case 'duplicate':
|
||||||
|
result = await api('POST', `/templates/${rest[0]}/duplicate`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown templates subcommand. Use: list, get, create, update, delete, publish, duplicate' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'broadcasts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/broadcasts?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/broadcasts/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create': {
|
||||||
|
const body = { segment_id: args['segment-id'], from: args.from, subject: args.subject }
|
||||||
|
if (args.html) body.html = args.html
|
||||||
|
if (args.text) body.text = args.text
|
||||||
|
if (args['reply-to']) body.reply_to = args['reply-to']
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
result = await api('POST', '/broadcasts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'send': {
|
||||||
|
const body = {}
|
||||||
|
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
|
||||||
|
result = await api('POST', `/broadcasts/${rest[0]}/send`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/broadcasts/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown broadcasts subcommand. Use: list, get, create, send, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'segments':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/segments?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
result = await api('GET', `/segments/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'create':
|
||||||
|
result = await api('POST', '/segments', { name: args.name })
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
result = await api('DELETE', `/segments/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown segments subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
send: 'send --from <email> --to <email> --subject <subject> --html <html>',
|
||||||
|
emails: 'emails [list|get|cancel] [id]',
|
||||||
|
domains: 'domains [list|get|create|verify|delete] [id] [--name <name>]',
|
||||||
|
'api-keys': 'api-keys [list|create|delete] [id] [--name <name>]',
|
||||||
|
audiences: 'audiences [list|get|create|delete] [id] [--name <name>]',
|
||||||
|
contacts: 'contacts <audience_id> [list|get|create|update|delete] [contact_id] [--email <email>]',
|
||||||
|
webhooks: 'webhooks [list|get|create|delete] [id] [--endpoint <url>]',
|
||||||
|
batch: 'batch --emails <json_array>',
|
||||||
|
templates: 'templates [list|get|create|update|delete|publish|duplicate] [id] [--name <name>] [--html <html>] [--variables <json>]',
|
||||||
|
broadcasts: 'broadcasts [list|get|create|send|delete] [id] [--segment-id <id>] [--from <email>] [--subject <subject>]',
|
||||||
|
segments: 'segments [list|get|create|delete] [id] [--name <name>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
160
tools/clis/rewardful.js
Executable file
160
tools/clis/rewardful.js
Executable file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.REWARDFUL_API_KEY
|
||||||
|
const BASE_URL = 'https://api.getrewardful.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'REWARDFUL_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64')
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'affiliates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
result = await api('GET', `/affiliates?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Affiliate ID required' }; break }
|
||||||
|
result = await api('GET', `/affiliates/${rest[0]}`)
|
||||||
|
break
|
||||||
|
case 'search': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.email) params.set('email', args.email)
|
||||||
|
result = await api('GET', `/affiliates?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!rest[0]) { result = { error: 'Affiliate ID required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['first-name']) body.first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.last_name = args['last-name']
|
||||||
|
if (args['paypal-email']) body.paypal_email = args['paypal-email']
|
||||||
|
result = await api('PUT', `/affiliates/${args.id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown affiliates subcommand. Use: list, get, search, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'referrals':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
|
||||||
|
result = await api('GET', `/referrals?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['stripe-customer-id']) params.set('stripe_customer_id', args['stripe-customer-id'])
|
||||||
|
result = await api('GET', `/referrals?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown referrals subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'commissions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
|
||||||
|
result = await api('GET', `/commissions?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Commission ID required' }; break }
|
||||||
|
result = await api('GET', `/commissions/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown commissions subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'links':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
if (!args['affiliate-id']) { result = { error: '--affiliate-id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.token) body.token = args.token
|
||||||
|
if (args.url) body.url = args.url
|
||||||
|
result = await api('POST', `/affiliates/${args['affiliate-id']}/links`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown links subcommand. Use: create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
affiliates: 'affiliates [list|get|search|update] [id] [--email <email>] [--id <id>] [--first-name <name>] [--last-name <name>] [--paypal-email <email>]',
|
||||||
|
referrals: 'referrals [list|get] [--affiliate-id <id>] [--stripe-customer-id <id>]',
|
||||||
|
commissions: 'commissions [list|get] [id] [--affiliate-id <id>]',
|
||||||
|
links: 'links [create] [--affiliate-id <id>] [--token <token>] [--url <url>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
223
tools/clis/savvycal.js
Executable file
223
tools/clis/savvycal.js
Executable file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.SAVVYCAL_API_KEY
|
||||||
|
const BASE_URL = 'https://api.savvycal.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'SAVVYCAL_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const limit = args.limit ? Number(args.limit) : 20
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'me':
|
||||||
|
result = await api('GET', '/me')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'links':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args.before) params.set('before', args.before)
|
||||||
|
result = await api('GET', `/scheduling-links?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/scheduling-links/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { name }
|
||||||
|
if (args.slug) body.slug = args.slug
|
||||||
|
if (args.duration) body.duration_minutes = Number(args.duration)
|
||||||
|
result = await api('POST', '/scheduling-links', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.slug) body.slug = args.slug
|
||||||
|
if (args.duration) body.duration_minutes = Number(args.duration)
|
||||||
|
result = await api('PATCH', `/scheduling-links/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/scheduling-links/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'duplicate': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/scheduling-links/${id}/duplicate`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'toggle': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/scheduling-links/${id}/toggle`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'slots': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['start-time']) params.set('start_time', args['start-time'])
|
||||||
|
if (args['end-time']) params.set('end_time', args['end-time'])
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/scheduling-links/${id}/slots${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown links subcommand. Use: list, get, create, update, delete, duplicate, toggle, slots' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'events':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args.before) params.set('before', args.before)
|
||||||
|
result = await api('GET', `/events?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/events/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const linkId = args['link-id']
|
||||||
|
const startAt = args['start-at']
|
||||||
|
const name = args.name
|
||||||
|
const email = args.email
|
||||||
|
if (!linkId || !startAt || !name || !email) {
|
||||||
|
result = { error: '--link-id, --start-at, --name, and --email required' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = await api('POST', '/events', {
|
||||||
|
scheduling_link_id: linkId,
|
||||||
|
start_at: startAt,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'cancel': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/events/${id}/cancel`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown events subcommand. Use: list, get, create, cancel' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.set('limit', String(limit))
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args.before) params.set('before', args.before)
|
||||||
|
result = await api('GET', `/webhooks?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const url = args.url
|
||||||
|
const events = args.events?.split(',')
|
||||||
|
if (!url || !events) { result = { error: '--url and --events (comma-separated) required' }; break }
|
||||||
|
result = await api('POST', '/webhooks', { url, events })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/webhooks/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
me: 'me',
|
||||||
|
links: 'links [list | get --id <id> | create --name <name> | update --id <id> | delete --id <id> | duplicate --id <id> | toggle --id <id> | slots --id <id>]',
|
||||||
|
events: 'events [list | get --id <id> | create --link-id <id> --start-at <iso> --name <name> --email <email> | cancel --id <id>]',
|
||||||
|
webhooks: 'webhooks [list | create --url <url> --events <e1,e2> | delete --id <id>]',
|
||||||
|
options: '--limit <n> --after <cursor> --before <cursor>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
192
tools/clis/segment.js
Executable file
192
tools/clis/segment.js
Executable file
|
|
@ -0,0 +1,192 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const WRITE_KEY = process.env.SEGMENT_WRITE_KEY
|
||||||
|
const ACCESS_TOKEN = process.env.SEGMENT_ACCESS_TOKEN
|
||||||
|
const TRACKING_URL = 'https://api.segment.io/v1'
|
||||||
|
const PROFILE_URL = 'https://profiles.segment.com/v1'
|
||||||
|
|
||||||
|
if (!WRITE_KEY && !ACCESS_TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'SEGMENT_WRITE_KEY (for tracking) or SEGMENT_ACCESS_TOKEN (for profiles) environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trackApi(method, path, body) {
|
||||||
|
if (!WRITE_KEY) {
|
||||||
|
return { error: 'SEGMENT_WRITE_KEY required for tracking operations' }
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${TRACKING_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const auth = Buffer.from(`${WRITE_KEY}:`).toString('base64')
|
||||||
|
const res = await fetch(`${TRACKING_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function profileApi(method, path) {
|
||||||
|
if (!ACCESS_TOKEN) {
|
||||||
|
return { error: 'SEGMENT_ACCESS_TOKEN required for profile operations' }
|
||||||
|
}
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${PROFILE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' } }
|
||||||
|
}
|
||||||
|
const auth = Buffer.from(`${ACCESS_TOKEN}:`).toString('base64')
|
||||||
|
const res = await fetch(`${PROFILE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'track':
|
||||||
|
switch (sub) {
|
||||||
|
case 'event': {
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
if (!args.event) { result = { error: '--event required' }; break }
|
||||||
|
const body = {
|
||||||
|
userId: args['user-id'],
|
||||||
|
event: args.event,
|
||||||
|
}
|
||||||
|
if (args.properties) {
|
||||||
|
try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break }
|
||||||
|
}
|
||||||
|
result = await trackApi('POST', '/track', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown track subcommand. Use: event' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'identify':
|
||||||
|
switch (sub) {
|
||||||
|
case 'user': {
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
const body = { userId: args['user-id'] }
|
||||||
|
if (args.traits) {
|
||||||
|
try { body.traits = JSON.parse(args.traits) } catch { result = { error: 'Invalid JSON in --traits' }; break }
|
||||||
|
}
|
||||||
|
result = await trackApi('POST', '/identify', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown identify subcommand. Use: user' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'page':
|
||||||
|
switch (sub) {
|
||||||
|
case 'view': {
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
const body = { userId: args['user-id'] }
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.properties) {
|
||||||
|
try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break }
|
||||||
|
}
|
||||||
|
result = await trackApi('POST', '/page', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown page subcommand. Use: view' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'batch':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
if (!args.events) { result = { error: '--events required (JSON array)' }; break }
|
||||||
|
let batch
|
||||||
|
try { batch = JSON.parse(args.events) } catch { result = { error: 'Invalid JSON in --events' }; break }
|
||||||
|
result = await trackApi('POST', '/batch', { batch })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown batch subcommand. Use: send' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'profiles':
|
||||||
|
switch (sub) {
|
||||||
|
case 'traits': {
|
||||||
|
if (!args['space-id']) { result = { error: '--space-id required' }; break }
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
result = await profileApi('GET', `/spaces/${args['space-id']}/collections/users/profiles/user_id:${args['user-id']}/traits`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'events': {
|
||||||
|
if (!args['space-id']) { result = { error: '--space-id required' }; break }
|
||||||
|
if (!args['user-id']) { result = { error: '--user-id required' }; break }
|
||||||
|
result = await profileApi('GET', `/spaces/${args['space-id']}/collections/users/profiles/user_id:${args['user-id']}/events`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown profiles subcommand. Use: traits, events' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
track: 'track event --user-id <id> --event <name> [--properties <json>]',
|
||||||
|
identify: 'identify user --user-id <id> [--traits <json>]',
|
||||||
|
page: 'page view --user-id <id> [--name <name>] [--properties <json>]',
|
||||||
|
batch: 'batch send --events <json_array>',
|
||||||
|
profiles: 'profiles [traits|events] --space-id <id> --user-id <id>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
207
tools/clis/semrush.js
Executable file
207
tools/clis/semrush.js
Executable file
|
|
@ -0,0 +1,207 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.SEMRUSH_API_KEY
|
||||||
|
const BASE_URL = 'https://api.semrush.com/'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'SEMRUSH_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCSV(text) {
|
||||||
|
const lines = text.trim().split('\n')
|
||||||
|
if (lines.length < 2) return []
|
||||||
|
const headers = lines[0].split(';')
|
||||||
|
const rows = []
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
if (!lines[i].trim()) continue
|
||||||
|
const values = lines[i].split(';')
|
||||||
|
const row = {}
|
||||||
|
for (let j = 0; j < headers.length; j++) {
|
||||||
|
row[headers[j]] = values[j] || ''
|
||||||
|
}
|
||||||
|
rows.push(row)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(params) {
|
||||||
|
params.set('key', API_KEY)
|
||||||
|
params.set('export_escape', '1')
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedParams = new URLSearchParams(params)
|
||||||
|
maskedParams.set('key', '***')
|
||||||
|
return { _dry_run: true, method: 'GET', url: `${BASE_URL}?${maskedParams}`, headers: {}, body: undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}?${params}`)
|
||||||
|
const text = await res.text()
|
||||||
|
if (!res.ok) {
|
||||||
|
return { error: text.trim(), status: res.status }
|
||||||
|
}
|
||||||
|
if (text.startsWith('ERROR')) {
|
||||||
|
return { error: text.trim() }
|
||||||
|
}
|
||||||
|
return parseCSV(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const database = args.database || 'us'
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'domain':
|
||||||
|
switch (sub) {
|
||||||
|
case 'overview': {
|
||||||
|
if (!args.domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'domain_ranks',
|
||||||
|
export_columns: 'Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac',
|
||||||
|
domain: args.domain,
|
||||||
|
})
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'organic': {
|
||||||
|
if (!args.domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'domain_organic',
|
||||||
|
export_columns: 'Ph,Po,Pp,Pd,Nq,Cp,Ur,Tr,Tc,Co,Nr',
|
||||||
|
domain: args.domain,
|
||||||
|
database,
|
||||||
|
})
|
||||||
|
if (args.limit) params.set('display_limit', args.limit)
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'competitors': {
|
||||||
|
if (!args.domain) { result = { error: '--domain required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'domain_organic_organic',
|
||||||
|
export_columns: 'Dn,Cr,Np,Or,Ot,Oc,Ad',
|
||||||
|
domain: args.domain,
|
||||||
|
database,
|
||||||
|
})
|
||||||
|
if (args.limit) params.set('display_limit', args.limit)
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domain subcommand. Use: overview, organic, competitors' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'keywords':
|
||||||
|
switch (sub) {
|
||||||
|
case 'overview': {
|
||||||
|
if (!args.phrase) { result = { error: '--phrase required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'phrase_all',
|
||||||
|
export_columns: 'Ph,Nq,Cp,Co,Nr',
|
||||||
|
phrase: args.phrase,
|
||||||
|
database,
|
||||||
|
})
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'related': {
|
||||||
|
if (!args.phrase) { result = { error: '--phrase required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'phrase_related',
|
||||||
|
export_columns: 'Ph,Nq,Cp,Co,Nr,Td',
|
||||||
|
phrase: args.phrase,
|
||||||
|
database,
|
||||||
|
})
|
||||||
|
if (args.limit) params.set('display_limit', args.limit)
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'difficulty': {
|
||||||
|
if (!args.phrase) { result = { error: '--phrase required' }; break }
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'phrase_kdi',
|
||||||
|
export_columns: 'Ph,Kd',
|
||||||
|
phrase: args.phrase,
|
||||||
|
database,
|
||||||
|
})
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown keywords subcommand. Use: overview, related, difficulty' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'backlinks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'overview': {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'backlinks_overview',
|
||||||
|
target: args.target,
|
||||||
|
target_type: 'root_domain',
|
||||||
|
})
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
type: 'backlinks',
|
||||||
|
target: args.target,
|
||||||
|
target_type: 'root_domain',
|
||||||
|
export_columns: 'source_url,source_title,target_url,anchor',
|
||||||
|
})
|
||||||
|
if (args.limit) params.set('display_limit', args.limit)
|
||||||
|
result = await api(params)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown backlinks subcommand. Use: overview, list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
'domain overview': 'domain overview --domain <domain>',
|
||||||
|
'domain organic': 'domain organic --domain <domain> [--database <db>] [--limit <n>]',
|
||||||
|
'domain competitors': 'domain competitors --domain <domain> [--database <db>] [--limit <n>]',
|
||||||
|
'keywords overview': 'keywords overview --phrase <phrase> [--database <db>]',
|
||||||
|
'keywords related': 'keywords related --phrase <phrase> [--database <db>] [--limit <n>]',
|
||||||
|
'keywords difficulty': 'keywords difficulty --phrase <phrase> [--database <db>]',
|
||||||
|
'backlinks overview': 'backlinks overview --target <domain>',
|
||||||
|
'backlinks list': 'backlinks list --target <domain> [--limit <n>]',
|
||||||
|
'databases': 'us (default), uk, de, fr, ca, au, etc.',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
211
tools/clis/sendgrid.js
Executable file
211
tools/clis/sendgrid.js
Executable file
|
|
@ -0,0 +1,211 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.SENDGRID_API_KEY
|
||||||
|
const BASE_URL = 'https://api.sendgrid.com/v3'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'SENDGRID_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'send': {
|
||||||
|
if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break }
|
||||||
|
const body = {
|
||||||
|
personalizations: [{
|
||||||
|
to: args.to.split(',').map(e => ({ email: e.trim() })),
|
||||||
|
}],
|
||||||
|
from: { email: args.from },
|
||||||
|
subject: args.subject,
|
||||||
|
}
|
||||||
|
if (args['template-id']) {
|
||||||
|
body.template_id = args['template-id']
|
||||||
|
if (args['template-data']) {
|
||||||
|
try { body.personalizations[0].dynamic_template_data = JSON.parse(args['template-data']) } catch (e) { result = { error: 'Invalid JSON for --template-data: ' + e.message }; break }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const content = []
|
||||||
|
if (args.text) content.push({ type: 'text/plain', value: args.text })
|
||||||
|
if (args.html) content.push({ type: 'text/html', value: args.html })
|
||||||
|
if (content.length > 0) body.content = content
|
||||||
|
}
|
||||||
|
if (args.cc) body.personalizations[0].cc = args.cc.split(',').map(e => ({ email: e.trim() }))
|
||||||
|
if (args.bcc) body.personalizations[0].bcc = args.bcc.split(',').map(e => ({ email: e.trim() }))
|
||||||
|
if (args['reply-to']) body.reply_to = { email: args['reply-to'] }
|
||||||
|
result = await api('POST', '/mail/send', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'contacts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/marketing/contacts')
|
||||||
|
break
|
||||||
|
case 'add': {
|
||||||
|
const body = {
|
||||||
|
contacts: [{
|
||||||
|
email: args.email,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
if (args['first-name']) body.contacts[0].first_name = args['first-name']
|
||||||
|
if (args['last-name']) body.contacts[0].last_name = args['last-name']
|
||||||
|
if (args['list-ids']) body.list_ids = args['list-ids'].split(',')
|
||||||
|
result = await api('PUT', '/marketing/contacts', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'search': {
|
||||||
|
const body = { query: args.query }
|
||||||
|
result = await api('POST', '/marketing/contacts/search', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown contacts subcommand. Use: list, add, search' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.limit) params.set('page_size', args.limit)
|
||||||
|
result = await api('GET', `/marketing/campaigns?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get':
|
||||||
|
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
|
||||||
|
result = await api('GET', `/marketing/campaigns/${rest[0]}`)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stats': {
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['start-date']) params.set('start_date', args['start-date'])
|
||||||
|
if (args['end-date']) params.set('end_date', args['end-date'])
|
||||||
|
result = await api('GET', `/stats?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown stats subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'bounces':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['start-time']) params.set('start_time', args['start-time'])
|
||||||
|
if (args['end-time']) params.set('end_time', args['end-time'])
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/suppression/bounces?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown bounces subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'spam-reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['start-time']) params.set('start_time', args['start-time'])
|
||||||
|
if (args['end-time']) params.set('end_time', args['end-time'])
|
||||||
|
if (args.limit) params.set('limit', args.limit)
|
||||||
|
result = await api('GET', `/suppression/spam_reports?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown spam-reports subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'validate':
|
||||||
|
switch (sub) {
|
||||||
|
case 'email': {
|
||||||
|
if (!args.email && !rest[0]) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email: args.email || rest[0] }
|
||||||
|
result = await api('POST', '/validations/email', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const body = { email: sub }
|
||||||
|
result = await api('POST', '/validations/email', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
send: 'send --from <email> --to <email> --subject <subject> --html <html> [--text <text>] [--template-id <id>] [--template-data <json>]',
|
||||||
|
contacts: 'contacts [list|add|search] [--email <email>] [--first-name <name>] [--last-name <name>] [--list-ids <ids>] [--query <sgql>]',
|
||||||
|
campaigns: 'campaigns [list|get] [id] [--limit <n>]',
|
||||||
|
stats: 'stats get [--start-date <YYYY-MM-DD>] [--end-date <YYYY-MM-DD>]',
|
||||||
|
bounces: 'bounces list [--start-time <ts>] [--end-time <ts>] [--limit <n>]',
|
||||||
|
'spam-reports': 'spam-reports list [--start-time <ts>] [--end-time <ts>] [--limit <n>]',
|
||||||
|
validate: 'validate <email> OR validate email --email <email>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
237
tools/clis/snov.js
Executable file
237
tools/clis/snov.js
Executable file
|
|
@ -0,0 +1,237 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const CLIENT_ID = process.env.SNOV_CLIENT_ID
|
||||||
|
const CLIENT_SECRET = process.env.SNOV_CLIENT_SECRET
|
||||||
|
const BASE_URL = 'https://api.snov.io/v1'
|
||||||
|
|
||||||
|
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||||
|
console.error(JSON.stringify({ error: 'SNOV_CLIENT_ID and SNOV_CLIENT_SECRET environment variables required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedToken = null
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
if (cachedToken) return cachedToken
|
||||||
|
const res = await fetch('https://api.snov.io/v1/oauth/access_token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ grant_type: 'client_credentials', client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!data.access_token) {
|
||||||
|
throw new Error(data.error_description || data.error || 'Failed to obtain access token')
|
||||||
|
}
|
||||||
|
cachedToken = data.access_token
|
||||||
|
return cachedToken
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json', Accept: 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const token = await getToken()
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (body) opts.body = JSON.stringify(body)
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, opts)
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'domain':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
const body = { domain, type: args.type || 'all', limit: Number(args.limit || 100), lastId: 0 }
|
||||||
|
result = await api('POST', '/get-domain-emails-with-info', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'count': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get-domain-emails-count', { domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown domain subcommand. Use: search, count' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'email':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const domain = args.domain
|
||||||
|
const firstName = args['first-name']
|
||||||
|
const lastName = args['last-name']
|
||||||
|
if (!domain || !firstName || !lastName) { result = { error: '--domain, --first-name, and --last-name required' }; break }
|
||||||
|
result = await api('POST', '/get-emails-from-names', { firstName, lastName, domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'verify': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('POST', '/get-emails-verification-status', { emails: [email] })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown email subcommand. Use: find, verify' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'prospect':
|
||||||
|
switch (sub) {
|
||||||
|
case 'find': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
result = await api('POST', '/get-prospect-by-email', { email })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
const email = args.email
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
const body = { email }
|
||||||
|
if (args['first-name']) body.firstName = args['first-name']
|
||||||
|
if (args['last-name']) body.lastName = args['last-name']
|
||||||
|
if (args['list-id']) body.listId = args['list-id']
|
||||||
|
result = await api('POST', '/add-prospect-to-list', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown prospect subcommand. Use: find, add' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'lists':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/get-user-lists')
|
||||||
|
break
|
||||||
|
case 'prospects': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
const body = { listId: id }
|
||||||
|
if (args.page) body.page = Number(args.page)
|
||||||
|
if (args['per-page']) body.perPage = Number(args['per-page'])
|
||||||
|
result = await api('POST', '/prospect-list', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown lists subcommand. Use: list, prospects' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'technology':
|
||||||
|
switch (sub) {
|
||||||
|
case 'check': {
|
||||||
|
const domain = args.domain
|
||||||
|
if (!domain) { result = { error: '--domain required' }; break }
|
||||||
|
result = await api('POST', '/get-technology-checker', { domain })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown technology subcommand. Use: check' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'drips':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/get-user-campaigns')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/get-emails-from-campaign?id=${encodeURIComponent(id)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add-prospect': {
|
||||||
|
const campaignId = args['campaign-id']
|
||||||
|
const email = args.email
|
||||||
|
if (!campaignId || !email) { result = { error: '--campaign-id and --email required' }; break }
|
||||||
|
const body = { campaignId, email }
|
||||||
|
if (args['first-name']) body.firstName = args['first-name']
|
||||||
|
if (args['last-name']) body.lastName = args['last-name']
|
||||||
|
result = await api('POST', '/add-prospect-to-email-campaign', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown drips subcommand. Use: list, get, add-prospect' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
domain: {
|
||||||
|
search: 'domain search --domain <domain> [--type all|personal|generic] [--limit <n>]',
|
||||||
|
count: 'domain count --domain <domain>',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
find: 'email find --domain <domain> --first-name <name> --last-name <name>',
|
||||||
|
verify: 'email verify --email <email>',
|
||||||
|
},
|
||||||
|
prospect: {
|
||||||
|
find: 'prospect find --email <email>',
|
||||||
|
add: 'prospect add --email <email> [--first-name <name>] [--last-name <name>] [--list-id <id>]',
|
||||||
|
},
|
||||||
|
lists: {
|
||||||
|
list: 'lists list',
|
||||||
|
prospects: 'lists prospects --id <id> [--page <n>] [--per-page <n>]',
|
||||||
|
},
|
||||||
|
technology: 'technology check --domain <domain>',
|
||||||
|
drips: {
|
||||||
|
list: 'drips list',
|
||||||
|
get: 'drips get --id <id>',
|
||||||
|
'add-prospect': 'drips add-prospect --campaign-id <id> --email <email> [--first-name <name>] [--last-name <name>]',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
190
tools/clis/tiktok-ads.js
Executable file
190
tools/clis/tiktok-ads.js
Executable file
|
|
@ -0,0 +1,190 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const TOKEN = process.env.TIKTOK_ACCESS_TOKEN
|
||||||
|
const ADVERTISER_ID = process.env.TIKTOK_ADVERTISER_ID
|
||||||
|
const BASE_URL = 'https://business-api.tiktok.com/open_api/v1.3'
|
||||||
|
|
||||||
|
if (!TOKEN) {
|
||||||
|
console.error(JSON.stringify({ error: 'TIKTOK_ACCESS_TOKEN environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Access-Token': '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Access-Token': TOKEN,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (body && method === 'POST') {
|
||||||
|
opts.body = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, opts)
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
function getAdvertiserId() {
|
||||||
|
return args['advertiser-id'] || ADVERTISER_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'advertiser':
|
||||||
|
switch (sub) {
|
||||||
|
case 'info': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
const advParams = new URLSearchParams({ advertiser_ids: JSON.stringify([advId]) })
|
||||||
|
result = await api('GET', `/advertiser/info/?${advParams}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown advertiser subcommand. Use: info' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'campaigns':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
const campParams = new URLSearchParams({ advertiser_id: advId, page: '1', page_size: '20' })
|
||||||
|
result = await api('GET', `/campaign/get/?${campParams}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
if (!args.name || !args.objective) { result = { error: '--name and --objective required' }; break }
|
||||||
|
const body = {
|
||||||
|
advertiser_id: advId,
|
||||||
|
campaign_name: args.name,
|
||||||
|
objective_type: args.objective,
|
||||||
|
budget_mode: args['budget-mode'] || 'BUDGET_MODE_DAY',
|
||||||
|
}
|
||||||
|
if (args.budget) body.budget = parseFloat(args.budget)
|
||||||
|
result = await api('POST', '/campaign/create/', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update-status': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
if (!args.ids || !args.status) { result = { error: '--ids and --status required' }; break }
|
||||||
|
result = await api('POST', '/campaign/status/update/', {
|
||||||
|
advertiser_id: advId,
|
||||||
|
campaign_ids: args.ids.split(','),
|
||||||
|
opt_status: args.status,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown campaigns subcommand. Use: list, create, update-status' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'adgroups':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
const agParams = new URLSearchParams({ advertiser_id: advId })
|
||||||
|
if (args['campaign-id']) agParams.set('campaign_ids', JSON.stringify([args['campaign-id']]))
|
||||||
|
result = await api('GET', `/adgroup/get/?${agParams}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown adgroups subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reports':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
if (!args['start-date'] || !args['end-date']) { result = { error: '--start-date and --end-date required (YYYY-MM-DD)' }; break }
|
||||||
|
const body = {
|
||||||
|
advertiser_id: advId,
|
||||||
|
report_type: 'BASIC',
|
||||||
|
dimensions: args.dimensions ? args.dimensions.split(',') : ['campaign_id'],
|
||||||
|
metrics: args.metrics ? args.metrics.split(',') : ['spend', 'impressions', 'clicks', 'conversion'],
|
||||||
|
data_level: args['data-level'] || 'AUCTION_CAMPAIGN',
|
||||||
|
start_date: args['start-date'],
|
||||||
|
end_date: args['end-date'],
|
||||||
|
}
|
||||||
|
result = await api('POST', '/report/integrated/get/', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reports subcommand. Use: get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'audiences':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const advId = getAdvertiserId()
|
||||||
|
if (!advId) { result = { error: 'TIKTOK_ADVERTISER_ID env or --advertiser-id required' }; break }
|
||||||
|
const audParams = new URLSearchParams({ advertiser_id: advId })
|
||||||
|
result = await api('GET', `/dmp/custom_audience/list/?${audParams}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown audiences subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
advertiser: 'advertiser [info]',
|
||||||
|
campaigns: 'campaigns [list|create|update-status] [--name <name>] [--objective <obj>] [--budget-mode BUDGET_MODE_DAY] [--budget <amount>] [--ids <id1,id2>] [--status ENABLE|DISABLE]',
|
||||||
|
adgroups: 'adgroups [list] [--campaign-id <id>]',
|
||||||
|
reports: 'reports [get] --start-date YYYY-MM-DD --end-date YYYY-MM-DD [--dimensions campaign_id] [--metrics spend,impressions,clicks,conversion] [--data-level AUCTION_CAMPAIGN]',
|
||||||
|
audiences: 'audiences [list]',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
153
tools/clis/tolt.js
Executable file
153
tools/clis/tolt.js
Executable file
|
|
@ -0,0 +1,153 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.TOLT_API_KEY
|
||||||
|
const BASE_URL = 'https://api.tolt.io/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'TOLT_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'affiliates':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/affiliates')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = rest[0] || args.id
|
||||||
|
if (!id) { result = { error: 'Affiliate ID required (positional arg or --id)' }; break }
|
||||||
|
result = await api('GET', `/affiliates/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const body = {}
|
||||||
|
if (args.email) body.email = args.email
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
result = await api('POST', '/affiliates', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
if (!args.id) { result = { error: '--id required (affiliate ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['commission-rate']) body.commission_rate = Number(args['commission-rate'])
|
||||||
|
if (args['payout-method']) body.payout_method = args['payout-method']
|
||||||
|
if (args['paypal-email']) body.paypal_email = args['paypal-email']
|
||||||
|
result = await api('PATCH', `/affiliates/${args.id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown affiliates subcommand. Use: list, get, create, update' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'referrals':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
|
||||||
|
result = await api('GET', `/referrals?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['customer-id']) params.set('customer_id', args['customer-id'])
|
||||||
|
result = await api('GET', `/referrals?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown referrals subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'commissions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
|
||||||
|
result = await api('GET', `/commissions?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown commissions subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'payouts':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args['affiliate-id']) params.set('affiliate_id', args['affiliate-id'])
|
||||||
|
result = await api('GET', `/payouts?${params}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown payouts subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
affiliates: 'affiliates [list|get|create|update] [id] [--email <email>] [--name <name>] [--id <id>] [--commission-rate <rate>] [--payout-method <method>] [--paypal-email <email>]',
|
||||||
|
referrals: 'referrals [list|get] [--affiliate-id <id>] [--customer-id <id>]',
|
||||||
|
commissions: 'commissions [list] [--affiliate-id <id>]',
|
||||||
|
payouts: 'payouts [list] [--affiliate-id <id>]',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
276
tools/clis/trustpilot.js
Executable file
276
tools/clis/trustpilot.js
Executable file
|
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.TRUSTPILOT_API_KEY
|
||||||
|
const API_SECRET = process.env.TRUSTPILOT_API_SECRET
|
||||||
|
const BUSINESS_UNIT_ID = process.env.TRUSTPILOT_BUSINESS_UNIT_ID
|
||||||
|
const BASE_URL = 'https://api.trustpilot.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'TRUSTPILOT_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let accessToken = null
|
||||||
|
|
||||||
|
async function getAccessToken() {
|
||||||
|
if (accessToken) return accessToken
|
||||||
|
if (!API_SECRET) return null
|
||||||
|
const res = await fetch(`${BASE_URL}/oauth/oauth-business-users-for-applications/accesstoken`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(`${API_KEY}:${API_SECRET}`).toString('base64'),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: 'grant_type=client_credentials',
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.access_token) {
|
||||||
|
accessToken = data.access_token
|
||||||
|
return accessToken
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body, auth = 'apikey') {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
const maskedHeaders = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
|
||||||
|
if (auth === 'bearer') {
|
||||||
|
maskedHeaders['Authorization'] = '***'
|
||||||
|
} else {
|
||||||
|
maskedHeaders['apikey'] = '***'
|
||||||
|
}
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: maskedHeaders, body: body || undefined }
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
if (auth === 'bearer') {
|
||||||
|
const token = await getAccessToken()
|
||||||
|
if (!token) {
|
||||||
|
return { error: 'TRUSTPILOT_API_SECRET required for private API endpoints' }
|
||||||
|
}
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
} else {
|
||||||
|
headers['apikey'] = API_KEY
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const businessUnitId = args['business-unit'] || BUSINESS_UNIT_ID
|
||||||
|
const limit = args.limit ? Number(args.limit) : 20
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'business':
|
||||||
|
switch (sub) {
|
||||||
|
case 'search': {
|
||||||
|
const query = args.query
|
||||||
|
if (!query) { result = { error: '--query required' }; break }
|
||||||
|
result = await api('GET', `/business-units/search?query=${encodeURIComponent(query)}&limit=${limit}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
result = await api('GET', `/business-units/${businessUnitId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'profile': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
result = await api('GET', `/business-units/${businessUnitId}/profileinfo`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'categories': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
result = await api('GET', `/business-units/${businessUnitId}/categories`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'web-links': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
const locale = args.locale || 'en-US'
|
||||||
|
result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${encodeURIComponent(locale)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown business subcommand. Use: search, get, profile, categories, web-links' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'reviews':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
const reviewParams = new URLSearchParams({ perPage: String(limit), orderBy: args['order-by'] || 'createdat.desc' })
|
||||||
|
if (args.stars) reviewParams.set('stars', args.stars)
|
||||||
|
if (args.language) reviewParams.set('language', args.language)
|
||||||
|
result = await api('GET', `/business-units/${businessUnitId}/reviews?${reviewParams}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const reviewId = args.id
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/reviews/${reviewId}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'private': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
const privateParams = new URLSearchParams({ perPage: String(limit) })
|
||||||
|
if (args.stars) privateParams.set('stars', args.stars)
|
||||||
|
result = await api('GET', `/private/business-units/${businessUnitId}/reviews?${privateParams}`, null, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'latest':
|
||||||
|
result = await api('GET', `/reviews/latest?count=${limit}`)
|
||||||
|
break
|
||||||
|
case 'reply': {
|
||||||
|
const reviewId = args.id
|
||||||
|
const message = args.message
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
if (!message) { result = { error: '--message required' }; break }
|
||||||
|
result = await api('POST', `/private/reviews/${reviewId}/reply`, { message }, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete-reply': {
|
||||||
|
const reviewId = args.id
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('DELETE', `/private/reviews/${reviewId}/reply`, null, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown reviews subcommand. Use: list, get, private, latest, reply, delete-reply' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'invitations':
|
||||||
|
switch (sub) {
|
||||||
|
case 'create': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
const email = args.email
|
||||||
|
const name = args.name
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const templateId = args.template
|
||||||
|
const redirectUri = args['redirect-uri'] || 'https://trustpilot.com'
|
||||||
|
const payload = {
|
||||||
|
consumerEmail: email,
|
||||||
|
consumerName: name,
|
||||||
|
referenceNumber: args.reference || '',
|
||||||
|
senderEmail: args['sender-email'] || undefined,
|
||||||
|
replyTo: args['reply-to'] || undefined,
|
||||||
|
templateId: templateId || undefined,
|
||||||
|
redirectUri,
|
||||||
|
}
|
||||||
|
result = await api('POST', `/private/business-units/${businessUnitId}/email-invitations`, payload, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'link': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
const email = args.email
|
||||||
|
const name = args.name
|
||||||
|
if (!email) { result = { error: '--email required' }; break }
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
result = await api('POST', `/private/business-units/${businessUnitId}/invitation-links`, {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
referenceId: args.reference || '',
|
||||||
|
redirectUri: args['redirect-uri'] || 'https://trustpilot.com',
|
||||||
|
}, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'templates': {
|
||||||
|
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||||
|
result = await api('GET', `/private/business-units/${businessUnitId}/templates`, null, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown invitations subcommand. Use: create, link, templates' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tags':
|
||||||
|
switch (sub) {
|
||||||
|
case 'get': {
|
||||||
|
const reviewId = args.id
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/private/reviews/${reviewId}/tags`, null, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add': {
|
||||||
|
const reviewId = args.id
|
||||||
|
const group = args.group
|
||||||
|
const value = args.value
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
if (!group || !value) { result = { error: '--group and --value required' }; break }
|
||||||
|
result = await api('PUT', `/private/reviews/${reviewId}/tags`, {
|
||||||
|
tags: [{ group, value }],
|
||||||
|
}, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'remove': {
|
||||||
|
const reviewId = args.id
|
||||||
|
const group = args.group
|
||||||
|
const value = args.value
|
||||||
|
if (!reviewId) { result = { error: '--id required' }; break }
|
||||||
|
if (!group || !value) { result = { error: '--group and --value required' }; break }
|
||||||
|
result = await api('DELETE', `/private/reviews/${reviewId}/tags?group=${encodeURIComponent(group)}&value=${encodeURIComponent(value)}`, null, 'bearer')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tags subcommand. Use: get, add, remove' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
business: 'business [search --query <q> | get | profile | categories | web-links]',
|
||||||
|
reviews: 'reviews [list | get --id <id> | private | latest | reply --id <id> --message <msg> | delete-reply --id <id>]',
|
||||||
|
invitations: 'invitations [create --email <e> --name <n> | link --email <e> --name <n> | templates]',
|
||||||
|
tags: 'tags [get --id <id> | add --id <id> --group <g> --value <v> | remove --id <id> --group <g> --value <v>]',
|
||||||
|
options: '--business-unit <id> --limit <n> --stars <1-5> --language <code>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
269
tools/clis/typeform.js
Executable file
269
tools/clis/typeform.js
Executable file
|
|
@ -0,0 +1,269 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.TYPEFORM_API_KEY
|
||||||
|
const BASE_URL = 'https://api.typeform.com'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'TYPEFORM_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
const pageSize = args['page-size'] ? Number(args['page-size']) : undefined
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'forms':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (pageSize) params.set('page_size', String(pageSize))
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
if (args['workspace-id']) params.set('workspace_id', args['workspace-id'])
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/forms${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
result = await api('GET', `/forms/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const title = args.title
|
||||||
|
if (!title) { result = { error: '--title required' }; break }
|
||||||
|
const body = { title }
|
||||||
|
if (args['workspace-id']) {
|
||||||
|
body.workspace = { href: `${BASE_URL}/workspaces/${args['workspace-id']}` }
|
||||||
|
}
|
||||||
|
result = await api('POST', '/forms', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.title) body.title = args.title
|
||||||
|
result = await api('PUT', `/forms/${id}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
result = await api('DELETE', `/forms/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown forms subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'responses':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (pageSize) params.set('page_size', String(pageSize))
|
||||||
|
if (args.since) params.set('since', args.since)
|
||||||
|
if (args.until) params.set('until', args.until)
|
||||||
|
if (args.after) params.set('after', args.after)
|
||||||
|
if (args.before) params.set('before', args.before)
|
||||||
|
if (args['response-type']) params.set('response_type', args['response-type'])
|
||||||
|
if (args.query) params.set('query', args.query)
|
||||||
|
if (args.fields) params.set('fields', args.fields)
|
||||||
|
if (args.sort) params.set('sort', args.sort)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/forms/${id}/responses${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
const responseIds = args['response-ids']
|
||||||
|
if (!responseIds) { result = { error: '--response-ids required (comma-separated)' }; break }
|
||||||
|
result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${encodeURIComponent(responseIds)}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown responses subcommand. Use: list, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'webhooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||||
|
result = await api('GET', `/forms/${id}/webhooks`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
const tag = args.tag
|
||||||
|
if (!id || !tag) { result = { error: '--id (form ID) and --tag required' }; break }
|
||||||
|
result = await api('GET', `/forms/${id}/webhooks/${tag}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const id = args.id
|
||||||
|
const tag = args.tag
|
||||||
|
const url = args.url
|
||||||
|
if (!id || !tag || !url) { result = { error: '--id (form ID), --tag, and --url required' }; break }
|
||||||
|
const body = { url, enabled: args.enabled !== 'false' }
|
||||||
|
result = await api('PUT', `/forms/${id}/webhooks/${tag}`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
const tag = args.tag
|
||||||
|
if (!id || !tag) { result = { error: '--id (form ID) and --tag required' }; break }
|
||||||
|
result = await api('DELETE', `/forms/${id}/webhooks/${tag}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'themes':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (pageSize) params.set('page_size', String(pageSize))
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/themes${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (theme ID)' }; break }
|
||||||
|
result = await api('GET', `/themes/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { name }
|
||||||
|
if (args.font) body.font = args.font
|
||||||
|
result = await api('POST', '/themes', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (theme ID)' }; break }
|
||||||
|
result = await api('DELETE', `/themes/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown themes subcommand. Use: list, get, create, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'images':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/images')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (image ID)' }; break }
|
||||||
|
result = await api('GET', `/images/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown images subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'workspaces':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (pageSize) params.set('page_size', String(pageSize))
|
||||||
|
if (args.page) params.set('page', args.page)
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
const qs = params.toString()
|
||||||
|
result = await api('GET', `/workspaces${qs ? '?' + qs : ''}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (workspace ID)' }; break }
|
||||||
|
result = await api('GET', `/workspaces/${id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown workspaces subcommand. Use: list, get' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
forms: 'forms [list | get --id <id> | create --title <title> | update --id <id> --title <title> | delete --id <id>]',
|
||||||
|
responses: 'responses [list --id <form_id> | delete --id <form_id> --response-ids <id1,id2>]',
|
||||||
|
webhooks: 'webhooks [list --id <form_id> | get --id <form_id> --tag <tag> | create --id <form_id> --tag <tag> --url <url> | delete --id <form_id> --tag <tag>]',
|
||||||
|
themes: 'themes [list | get --id <id> | create --name <name> | delete --id <id>]',
|
||||||
|
images: 'images [list | get --id <id>]',
|
||||||
|
workspaces: 'workspaces [list | get --id <id>]',
|
||||||
|
options: '--page-size <n> --page <n> --since <iso> --until <iso> --query <text>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
256
tools/clis/wistia.js
Executable file
256
tools/clis/wistia.js
Executable file
|
|
@ -0,0 +1,256 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.WISTIA_API_KEY
|
||||||
|
const BASE_URL = 'https://api.wistia.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'WISTIA_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64')
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Authorization': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
const page = args.page ? Number(args.page) : 1
|
||||||
|
const perPage = args['per-page'] ? Number(args['per-page']) : 25
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'projects':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', `/projects.json?page=${page}&per_page=${perPage}`)
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (project hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/projects/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const name = args.name
|
||||||
|
if (!name) { result = { error: '--name required' }; break }
|
||||||
|
const body = { name }
|
||||||
|
if (args.public) body.public = true
|
||||||
|
result = await api('POST', '/projects.json', body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (project hashed ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
result = await api('PUT', `/projects/${id}.json`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (project hashed ID)' }; break }
|
||||||
|
result = await api('DELETE', `/projects/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown projects subcommand. Use: list, get, create, update, delete' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'medias':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) })
|
||||||
|
if (args.project) params.set('project_id', args.project)
|
||||||
|
if (args.name) params.set('name', args.name)
|
||||||
|
if (args.type) params.set('type', args.type)
|
||||||
|
result = await api('GET', `/medias.json?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'get': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/medias/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args.name) body.name = args.name
|
||||||
|
if (args.description) body.description = args.description
|
||||||
|
result = await api('PUT', `/medias/${id}.json`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('DELETE', `/medias/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'copy': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
const body = {}
|
||||||
|
if (args['target-project']) body.project_id = args['target-project']
|
||||||
|
result = await api('POST', `/medias/${id}/copy.json`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'stats': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/medias/${id}/stats.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown medias subcommand. Use: list, get, update, delete, copy, stats' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stats':
|
||||||
|
switch (sub) {
|
||||||
|
case 'account':
|
||||||
|
result = await api('GET', '/stats/account.json')
|
||||||
|
break
|
||||||
|
case 'project': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (project hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/stats/projects/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'media': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/stats/medias/${id}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'media-by-date': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (args.start) params.set('start_date', args.start)
|
||||||
|
if (args.end) params.set('end_date', args.end)
|
||||||
|
const qs = params.toString() ? `?${params.toString()}` : ''
|
||||||
|
result = await api('GET', `/stats/medias/${id}/by_date.json${qs}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'engagement': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/stats/medias/${id}/engagement.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'visitors': {
|
||||||
|
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) })
|
||||||
|
if (args.search) params.set('search', args.search)
|
||||||
|
result = await api('GET', `/stats/visitors.json?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'visitor': {
|
||||||
|
const key = args.key
|
||||||
|
if (!key) { result = { error: '--key required (visitor key)' }; break }
|
||||||
|
result = await api('GET', `/stats/visitors/${key}.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'events': {
|
||||||
|
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) })
|
||||||
|
if (args['media-id']) params.set('media_id', args['media-id'])
|
||||||
|
result = await api('GET', `/stats/events.json?${params.toString()}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown stats subcommand. Use: account, project, media, media-by-date, engagement, visitors, visitor, events' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'account':
|
||||||
|
result = await api('GET', '/account.json')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'captions':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
const id = args.id
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
result = await api('GET', `/medias/${id}/captions.json`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'create': {
|
||||||
|
const id = args.id
|
||||||
|
const language = args.language
|
||||||
|
if (!id) { result = { error: '--id required (media hashed ID)' }; break }
|
||||||
|
if (!language) { result = { error: '--language required (e.g. eng)' }; break }
|
||||||
|
const body = { language }
|
||||||
|
if (args['srt-file']) {
|
||||||
|
const fs = require('fs')
|
||||||
|
body.caption_file = fs.readFileSync(args['srt-file'], 'utf8')
|
||||||
|
}
|
||||||
|
result = await api('POST', `/medias/${id}/captions.json`, body)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown captions subcommand. Use: list, create' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
projects: 'projects [list | get --id <id> | create --name <name> | update --id <id> --name <name> | delete --id <id>]',
|
||||||
|
medias: 'medias [list [--project <id>] | get --id <id> | update --id <id> --name <name> | delete --id <id> | copy --id <id> [--target-project <id>] | stats --id <id>]',
|
||||||
|
stats: 'stats [account | project --id <id> | media --id <id> | media-by-date --id <id> [--start <date> --end <date>] | engagement --id <id> | visitors | visitor --key <key> | events [--media-id <id>]]',
|
||||||
|
account: 'account',
|
||||||
|
captions: 'captions [list --id <media-id> | create --id <media-id> --language <lang>]',
|
||||||
|
options: '--page <n> --per-page <n>',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
160
tools/clis/zapier.js
Executable file
160
tools/clis/zapier.js
Executable file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const API_KEY = process.env.ZAPIER_API_KEY
|
||||||
|
const BASE_URL = 'https://api.zapier.com/v1'
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error(JSON.stringify({ error: 'ZAPIER_API_KEY environment variable required' }))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function api(method, path, body) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'X-API-Key': '***', 'Content-Type': 'application/json' }, body: body || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(`${BASE_URL}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function webhookPost(url, data) {
|
||||||
|
if (args['dry-run']) {
|
||||||
|
return { _dry_run: true, method: 'POST', url, headers: { 'Content-Type': 'application/json' }, body: data || undefined }
|
||||||
|
}
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return { status: res.status, body: text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(args) {
|
||||||
|
const result = { _: [] }
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i]
|
||||||
|
if (arg.startsWith('--')) {
|
||||||
|
const key = arg.slice(2)
|
||||||
|
const next = args[i + 1]
|
||||||
|
if (next && !next.startsWith('--')) {
|
||||||
|
result[key] = next
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
result[key] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result._.push(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = parseArgs(process.argv.slice(2))
|
||||||
|
const [cmd, sub, ...rest] = args._
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let result
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'zaps':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list':
|
||||||
|
result = await api('GET', '/zaps')
|
||||||
|
break
|
||||||
|
case 'get': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('GET', `/zaps/${args.id}`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'on': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/zaps/${args.id}/on`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'off': {
|
||||||
|
if (!args.id) { result = { error: '--id required' }; break }
|
||||||
|
result = await api('POST', `/zaps/${args.id}/off`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown zaps subcommand. Use: list, get, on, off' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tasks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'list': {
|
||||||
|
if (!args['zap-id']) { result = { error: '--zap-id required' }; break }
|
||||||
|
result = await api('GET', `/zaps/${args['zap-id']}/tasks`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown tasks subcommand. Use: list' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'profile':
|
||||||
|
switch (sub) {
|
||||||
|
case 'me':
|
||||||
|
result = await api('GET', '/profiles/me')
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown profile subcommand. Use: me' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'hooks':
|
||||||
|
switch (sub) {
|
||||||
|
case 'send': {
|
||||||
|
if (!args.url) { result = { error: '--url required' }; break }
|
||||||
|
if (!args.data) { result = { error: '--data required (JSON string)' }; break }
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = JSON.parse(args.data)
|
||||||
|
} catch {
|
||||||
|
result = { error: 'Invalid JSON for --data' }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
result = await webhookPost(args.url, data)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
result = { error: 'Unknown hooks subcommand. Use: send' }
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = {
|
||||||
|
error: 'Unknown command',
|
||||||
|
usage: {
|
||||||
|
zaps: 'zaps [list|get|on|off] [--id <zap_id>]',
|
||||||
|
tasks: 'tasks [list] --zap-id <zap_id>',
|
||||||
|
profile: 'profile [me]',
|
||||||
|
hooks: 'hooks [send] --url <webhook_url> --data <json>',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(JSON.stringify({ error: err.message }))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
337
tools/integrations/activecampaign.md
Normal file
337
tools/integrations/activecampaign.md
Normal file
|
|
@ -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
|
||||||
148
tools/integrations/apollo.md
Normal file
148
tools/integrations/apollo.md
Normal file
|
|
@ -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
|
||||||
157
tools/integrations/beehiiv.md
Normal file
157
tools/integrations/beehiiv.md
Normal file
|
|
@ -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
|
||||||
268
tools/integrations/brevo.md
Normal file
268
tools/integrations/brevo.md
Normal file
|
|
@ -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
|
||||||
138
tools/integrations/buffer.md
Normal file
138
tools/integrations/buffer.md
Normal file
|
|
@ -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
|
||||||
161
tools/integrations/calendly.md
Normal file
161
tools/integrations/calendly.md
Normal file
|
|
@ -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
|
||||||
142
tools/integrations/clearbit.md
Normal file
142
tools/integrations/clearbit.md
Normal file
|
|
@ -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
|
||||||
165
tools/integrations/dataforseo.md
Normal file
165
tools/integrations/dataforseo.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# DataForSEO
|
||||||
|
|
||||||
|
Comprehensive SEO data API for SERP results, keyword research, backlinks, and on-page analysis.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | SERP, Keywords Data, Backlinks, On-Page, Labs |
|
||||||
|
| MCP | - | Not available |
|
||||||
|
| CLI | ✓ | [dataforseo.js](../clis/dataforseo.js) |
|
||||||
|
| SDK | ✓ | Python, TypeScript, PHP, Java, C# |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: Basic Auth
|
||||||
|
- **Header**: `Authorization: Basic {base64(login:password)}`
|
||||||
|
- **Get credentials**: API Access tab at https://app.dataforseo.com/api-access
|
||||||
|
- **Note**: API password is auto-generated, different from account password
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### SERP - Google organic (live)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/serp/google/organic/live/regular
|
||||||
|
|
||||||
|
[{
|
||||||
|
"keyword": "marketing automation",
|
||||||
|
"location_name": "United States",
|
||||||
|
"language_name": "English"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keywords - Search volume (live)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/keywords_data/google_ads/search_volume/live
|
||||||
|
|
||||||
|
[{
|
||||||
|
"keywords": ["email marketing", "marketing automation", "crm software"],
|
||||||
|
"location_code": 2840,
|
||||||
|
"language_code": "en"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keywords - Keywords for site (live)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/keywords_data/google_ads/keywords_for_site/live
|
||||||
|
|
||||||
|
[{
|
||||||
|
"target": "example.com",
|
||||||
|
"location_code": 2840,
|
||||||
|
"language_code": "en"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backlinks - Summary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/backlinks/summary/live
|
||||||
|
|
||||||
|
[{
|
||||||
|
"target": "example.com",
|
||||||
|
"internal_list_limit": 10,
|
||||||
|
"backlinks_status_type": "live"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backlinks - List
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/backlinks/backlinks/live
|
||||||
|
|
||||||
|
[{
|
||||||
|
"target": "example.com",
|
||||||
|
"mode": "as_is",
|
||||||
|
"limit": 100,
|
||||||
|
"backlinks_status_type": "live"
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backlinks - Referring domains
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/backlinks/referring_domains/live
|
||||||
|
|
||||||
|
[{
|
||||||
|
"target": "example.com",
|
||||||
|
"limit": 100
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backlinks - Index (database stats)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.dataforseo.com/v3/backlinks/index
|
||||||
|
```
|
||||||
|
|
||||||
|
### On-Page - Instant pages audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.dataforseo.com/v3/on_page/instant_pages
|
||||||
|
|
||||||
|
[{
|
||||||
|
"url": "https://example.com/page",
|
||||||
|
"enable_javascript": true
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
### SERP - Locations list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.dataforseo.com/v3/serp/google/locations
|
||||||
|
```
|
||||||
|
|
||||||
|
### SERP - Languages list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.dataforseo.com/v3/serp/google/languages
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Pattern
|
||||||
|
|
||||||
|
DataForSEO uses two methods for most endpoints:
|
||||||
|
- **Live** (`/live`) - Synchronous, results in same response
|
||||||
|
- **Task-based** (`/task_post` + `/task_get/$id`) - Async for large requests
|
||||||
|
|
||||||
|
Request bodies are always JSON arrays (even for single requests).
|
||||||
|
|
||||||
|
## Key Metrics
|
||||||
|
|
||||||
|
### Keyword Metrics
|
||||||
|
- `search_volume` - Monthly search volume
|
||||||
|
- `competition` - Competition level (0-1)
|
||||||
|
- `cpc` - Cost per click
|
||||||
|
- `monthly_searches` - Monthly breakdown array
|
||||||
|
|
||||||
|
### Backlink Metrics
|
||||||
|
- `total_backlinks` - Total backlink count
|
||||||
|
- `referring_domains` - Unique referring domains
|
||||||
|
- `domain_rank` - Domain authority score
|
||||||
|
- `backlinks_spam_score` - Spam score
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Programmatic SERP tracking at scale
|
||||||
|
- Keyword research with search volume data
|
||||||
|
- Backlink analysis and monitoring
|
||||||
|
- On-page SEO audits
|
||||||
|
- Competitor analysis
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- Rate limit headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`
|
||||||
|
- Backlinks API: 2000 requests/minute, 30 simultaneous
|
||||||
|
- Varies by endpoint and plan
|
||||||
|
|
||||||
|
## Relevant Skills
|
||||||
|
|
||||||
|
- seo-audit
|
||||||
|
- programmatic-seo
|
||||||
|
- content-strategy
|
||||||
|
- competitor-alternatives
|
||||||
182
tools/integrations/demio.md
Normal file
182
tools/integrations/demio.md
Normal file
|
|
@ -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
|
||||||
179
tools/integrations/g2.md
Normal file
179
tools/integrations/g2.md
Normal file
|
|
@ -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
|
||||||
147
tools/integrations/hotjar.md
Normal file
147
tools/integrations/hotjar.md
Normal file
|
|
@ -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
|
||||||
90
tools/integrations/hunter.md
Normal file
90
tools/integrations/hunter.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Hunter.io
|
||||||
|
|
||||||
|
Email finding and verification platform for outreach and link building.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | REST API for domain search, email finder, verification |
|
||||||
|
| MCP | - | Not available |
|
||||||
|
| CLI | [✓](../clis/hunter.js) | Zero-dependency Node.js CLI |
|
||||||
|
| SDK | - | API-only |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: API Key (query parameter)
|
||||||
|
- **Parameter**: `api_key={key}`
|
||||||
|
- **Env var**: `HUNTER_API_KEY`
|
||||||
|
- **Get key**: [Hunter dashboard > API](https://hunter.io/api-keys)
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### Find emails for a domain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/hunter.js domain search --domain example.com --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find a specific person's email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/hunter.js email find --domain example.com --first-name John --last-name Doe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify an email address
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/hunter.js email verify --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Count emails available for a domain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/hunter.js domain count --domain example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage leads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List leads
|
||||||
|
node tools/clis/hunter.js leads list --limit 20
|
||||||
|
|
||||||
|
# Create a lead
|
||||||
|
node tools/clis/hunter.js leads create --email john@example.com --first-name John --last-name Doe --company "Example Inc"
|
||||||
|
|
||||||
|
# Delete a lead
|
||||||
|
node tools/clis/hunter.js leads delete --id 12345
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage campaigns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List campaigns
|
||||||
|
node tools/clis/hunter.js campaigns list
|
||||||
|
|
||||||
|
# Get campaign details
|
||||||
|
node tools/clis/hunter.js campaigns get --id 12345
|
||||||
|
|
||||||
|
# Start/pause a campaign
|
||||||
|
node tools/clis/hunter.js campaigns start --id 12345
|
||||||
|
node tools/clis/hunter.js campaigns pause --id 12345
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check account usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/hunter.js account info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- Free plan: 25 searches/month, 50 verifications/month
|
||||||
|
- Paid plans scale with tier
|
||||||
|
- API rate limit: 10 requests/second
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Link building**: Find email contacts at target domains for outreach
|
||||||
|
- **Prospecting**: Build lead lists from company domains
|
||||||
|
- **Verification**: Clean email lists before sending campaigns
|
||||||
104
tools/integrations/instantly.md
Normal file
104
tools/integrations/instantly.md
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# Instantly.ai
|
||||||
|
|
||||||
|
Cold email platform with built-in email warmup and campaign management at scale.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | REST API for campaigns, leads, accounts, analytics |
|
||||||
|
| MCP | - | Not available |
|
||||||
|
| CLI | [✓](../clis/instantly.js) | Zero-dependency Node.js CLI |
|
||||||
|
| SDK | - | API-only |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: API Key (query parameter)
|
||||||
|
- **Parameter**: `api_key={key}`
|
||||||
|
- **Env var**: `INSTANTLY_API_KEY`
|
||||||
|
- **Get key**: [Instantly Settings > Integrations > API](https://app.instantly.ai/app/settings/integrations)
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### Manage campaigns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List campaigns
|
||||||
|
node tools/clis/instantly.js campaigns list --limit 20
|
||||||
|
|
||||||
|
# Get campaign details
|
||||||
|
node tools/clis/instantly.js campaigns get --id cam_abc123
|
||||||
|
|
||||||
|
# Check campaign status
|
||||||
|
node tools/clis/instantly.js campaigns status --id cam_abc123
|
||||||
|
|
||||||
|
# Launch a campaign
|
||||||
|
node tools/clis/instantly.js campaigns launch --id cam_abc123
|
||||||
|
|
||||||
|
# Pause a campaign
|
||||||
|
node tools/clis/instantly.js campaigns pause --id cam_abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage leads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List leads in a campaign
|
||||||
|
node tools/clis/instantly.js leads list --campaign-id cam_abc123 --limit 50
|
||||||
|
|
||||||
|
# Add a lead
|
||||||
|
node tools/clis/instantly.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc"
|
||||||
|
|
||||||
|
# Delete a lead
|
||||||
|
node tools/clis/instantly.js leads delete --campaign-id cam_abc123 --email john@example.com
|
||||||
|
|
||||||
|
# Check lead status
|
||||||
|
node tools/clis/instantly.js leads status --campaign-id cam_abc123 --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage email accounts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List connected accounts
|
||||||
|
node tools/clis/instantly.js accounts list --limit 20
|
||||||
|
|
||||||
|
# Check account status
|
||||||
|
node tools/clis/instantly.js accounts status --account-id me@example.com
|
||||||
|
|
||||||
|
# Check warmup status
|
||||||
|
node tools/clis/instantly.js accounts warmup-status --account-id me@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### View analytics
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Campaign analytics
|
||||||
|
node tools/clis/instantly.js analytics campaign --campaign-id cam_abc123 --start 2024-01-01 --end 2024-01-31
|
||||||
|
|
||||||
|
# Step-by-step analytics
|
||||||
|
node tools/clis/instantly.js analytics steps --campaign-id cam_abc123
|
||||||
|
|
||||||
|
# Account-level analytics
|
||||||
|
node tools/clis/instantly.js analytics account --start 2024-01-01 --end 2024-01-31
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage blocklist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List blocked emails/domains
|
||||||
|
node tools/clis/instantly.js blocklist list
|
||||||
|
|
||||||
|
# Add to blocklist
|
||||||
|
node tools/clis/instantly.js blocklist add --entries "competitor.com,spam@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- API rate limits vary by plan
|
||||||
|
- Recommended: stay under 10 requests/second
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Link building at scale**: Run large-volume outreach campaigns with built-in warmup
|
||||||
|
- **Campaign management**: Launch, pause, and monitor cold email campaigns
|
||||||
|
- **Account health**: Monitor email account warmup and deliverability
|
||||||
|
- **Analytics**: Track open rates, reply rates, and campaign performance
|
||||||
292
tools/integrations/intercom.md
Normal file
292
tools/integrations/intercom.md
Normal file
|
|
@ -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
|
||||||
207
tools/integrations/keywords-everywhere.md
Normal file
207
tools/integrations/keywords-everywhere.md
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
# Keywords Everywhere
|
||||||
|
|
||||||
|
Keyword research API for search volume, CPC, competition, related keywords, and traffic data.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | REST API for keyword data, related keywords, traffic |
|
||||||
|
| MCP | - | Community MCP server available |
|
||||||
|
| CLI | ✓ | [keywords-everywhere.js](../clis/keywords-everywhere.js) |
|
||||||
|
| SDK | - | API-only |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: API Key (Bearer token)
|
||||||
|
- **Header**: `Authorization: Bearer {api_key}`
|
||||||
|
- **Get key**: https://keywordseverywhere.com/first-install-addon.html
|
||||||
|
- **Limit**: 100 keywords per request
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### Get keyword data (volume, CPC, competition)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_keyword_data
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "gkp",
|
||||||
|
"kw": ["email marketing", "marketing automation", "crm software"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get related keywords
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_related_keywords
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "gkp",
|
||||||
|
"kw": ["email marketing"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get "People Also Search For" keywords
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_pasf_keywords
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"currency": "USD",
|
||||||
|
"dataSource": "gkp",
|
||||||
|
"kw": ["email marketing"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get domain keywords (what a domain ranks for)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_domain_keywords
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"currency": "USD",
|
||||||
|
"domain": "example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get URL keywords (what a specific URL ranks for)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_url_keywords
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"currency": "USD",
|
||||||
|
"url": "https://example.com/page"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get domain traffic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_domain_traffic
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"domain": "example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get URL traffic
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_url_traffic
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"country": "us",
|
||||||
|
"url": "https://example.com/page"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get domain backlinks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_domain_backlinks
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"domain": "example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get page backlinks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
POST https://api.keywordseverywhere.com/v1/get_page_backlinks
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
|
||||||
|
{
|
||||||
|
"url": "https://example.com/page"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check credits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.keywordseverywhere.com/v1/get_credits
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get supported countries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.keywordseverywhere.com/v1/get_countries
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get supported currencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GET https://api.keywordseverywhere.com/v1/get_currencies
|
||||||
|
|
||||||
|
Authorization: Bearer {api_key}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Metrics
|
||||||
|
|
||||||
|
### Keyword Data
|
||||||
|
- `vol` - Monthly search volume
|
||||||
|
- `cpc.value` - Cost per click
|
||||||
|
- `competition` - Competition score
|
||||||
|
- `trend` - 12-month trend data
|
||||||
|
|
||||||
|
### Traffic Data
|
||||||
|
- `estimated_traffic` - Estimated monthly traffic
|
||||||
|
- `keywords_count` - Number of ranking keywords
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `country` - Country code (us, uk, de, fr, etc.)
|
||||||
|
- `currency` - Currency code (USD, GBP, EUR, etc.)
|
||||||
|
- `dataSource` - Data source, default `gkp` (Google Keyword Planner)
|
||||||
|
- `kw` - Array of keywords (max 100 per request)
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Quick keyword research with volume and CPC
|
||||||
|
- Finding related keywords and PASF suggestions
|
||||||
|
- Analyzing domain/URL keyword rankings
|
||||||
|
- Traffic estimation for domains and pages
|
||||||
|
- Backlink discovery
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- 100 keywords per request
|
||||||
|
- Credit-based pricing (1 credit per keyword)
|
||||||
|
|
||||||
|
## Relevant Skills
|
||||||
|
|
||||||
|
- seo-audit
|
||||||
|
- content-strategy
|
||||||
|
- programmatic-seo
|
||||||
|
- competitor-alternatives
|
||||||
228
tools/integrations/klaviyo.md
Normal file
228
tools/integrations/klaviyo.md
Normal file
|
|
@ -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
|
||||||
110
tools/integrations/lemlist.md
Normal file
110
tools/integrations/lemlist.md
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Lemlist
|
||||||
|
|
||||||
|
Cold email outreach platform with personalization and campaign management.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | REST API for campaigns, leads, activities, webhooks |
|
||||||
|
| MCP | - | Not available |
|
||||||
|
| CLI | [✓](../clis/lemlist.js) | Zero-dependency Node.js CLI |
|
||||||
|
| SDK | - | API-only |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: Basic Auth (empty username, API key as password)
|
||||||
|
- **Header**: `Authorization: Basic base64(:api_key)`
|
||||||
|
- **Env var**: `LEMLIST_API_KEY`
|
||||||
|
- **Get key**: [Lemlist Settings > Integrations](https://app.lemlist.com/settings/integrations)
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### List campaigns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/lemlist.js campaigns list --offset 0 --limit 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get campaign details and stats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get campaign
|
||||||
|
node tools/clis/lemlist.js campaigns get --id cam_abc123
|
||||||
|
|
||||||
|
# Get campaign stats
|
||||||
|
node tools/clis/lemlist.js campaigns stats --id cam_abc123
|
||||||
|
|
||||||
|
# Export campaign data
|
||||||
|
node tools/clis/lemlist.js campaigns export --id cam_abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage leads in a campaign
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List leads
|
||||||
|
node tools/clis/lemlist.js leads list --campaign-id cam_abc123
|
||||||
|
|
||||||
|
# Add a lead
|
||||||
|
node tools/clis/lemlist.js leads add --campaign-id cam_abc123 --email john@example.com --first-name John --last-name Doe --company "Example Inc"
|
||||||
|
|
||||||
|
# Get lead details
|
||||||
|
node tools/clis/lemlist.js leads get --campaign-id cam_abc123 --email john@example.com
|
||||||
|
|
||||||
|
# Remove a lead
|
||||||
|
node tools/clis/lemlist.js leads delete --campaign-id cam_abc123 --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage unsubscribes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List unsubscribed emails
|
||||||
|
node tools/clis/lemlist.js unsubscribes list
|
||||||
|
|
||||||
|
# Add to unsubscribe list
|
||||||
|
node tools/clis/lemlist.js unsubscribes add --email john@example.com
|
||||||
|
|
||||||
|
# Remove from unsubscribe list
|
||||||
|
node tools/clis/lemlist.js unsubscribes delete --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### View activities
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All activities
|
||||||
|
node tools/clis/lemlist.js activities list
|
||||||
|
|
||||||
|
# Filter by campaign and type
|
||||||
|
node tools/clis/lemlist.js activities list --campaign-id cam_abc123 --type emailsOpened
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage webhooks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List hooks
|
||||||
|
node tools/clis/lemlist.js hooks list
|
||||||
|
|
||||||
|
# Create a webhook
|
||||||
|
node tools/clis/lemlist.js hooks create --target-url https://example.com/webhook --event emailsOpened
|
||||||
|
|
||||||
|
# Delete a webhook
|
||||||
|
node tools/clis/lemlist.js hooks delete --id hook_123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team info
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/lemlist.js team info
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- API rate limits vary by plan
|
||||||
|
- Recommended: stay under 10 requests/second
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Link building outreach**: Add prospects to campaigns for backlink requests
|
||||||
|
- **Campaign management**: Monitor open/reply rates across outreach campaigns
|
||||||
|
- **Lead management**: Add, remove, and track leads across campaigns
|
||||||
|
- **Webhook integration**: Get real-time notifications for email events
|
||||||
313
tools/integrations/livestorm.md
Normal file
313
tools/integrations/livestorm.md
Normal file
|
|
@ -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
|
||||||
229
tools/integrations/onesignal.md
Normal file
229
tools/integrations/onesignal.md
Normal file
|
|
@ -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
|
||||||
171
tools/integrations/optimizely.md
Normal file
171
tools/integrations/optimizely.md
Normal file
|
|
@ -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
|
||||||
212
tools/integrations/paddle.md
Normal file
212
tools/integrations/paddle.md
Normal file
|
|
@ -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
|
||||||
222
tools/integrations/partnerstack.md
Normal file
222
tools/integrations/partnerstack.md
Normal file
|
|
@ -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
|
||||||
177
tools/integrations/plausible.md
Normal file
177
tools/integrations/plausible.md
Normal file
|
|
@ -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
|
||||||
234
tools/integrations/postmark.md
Normal file
234
tools/integrations/postmark.md
Normal file
|
|
@ -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
|
||||||
181
tools/integrations/savvycal.md
Normal file
181
tools/integrations/savvycal.md
Normal file
|
|
@ -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
|
||||||
94
tools/integrations/snov.md
Normal file
94
tools/integrations/snov.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Snov.io
|
||||||
|
|
||||||
|
Email finding, verification, and drip campaign platform for outreach.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
| Integration | Available | Notes |
|
||||||
|
|-------------|-----------|-------|
|
||||||
|
| API | ✓ | REST API for email finding, verification, prospects, drip campaigns |
|
||||||
|
| MCP | - | Not available |
|
||||||
|
| CLI | [✓](../clis/snov.js) | Zero-dependency Node.js CLI |
|
||||||
|
| SDK | - | API-only |
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- **Type**: OAuth2 client credentials
|
||||||
|
- **Flow**: POST to `/oauth/access_token` with client_id + client_secret
|
||||||
|
- **Env vars**: `SNOV_CLIENT_ID`, `SNOV_CLIENT_SECRET`
|
||||||
|
- **Get keys**: [Snov.io > Integration > API](https://app.snov.io/integration/api)
|
||||||
|
|
||||||
|
The CLI handles token acquisition automatically.
|
||||||
|
|
||||||
|
## Common Agent Operations
|
||||||
|
|
||||||
|
### Search emails by domain
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js domain search --domain example.com --type all --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find a specific person's email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js email find --domain example.com --first-name John --last-name Doe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify an email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js email verify --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find prospect by email
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js prospect find --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add prospect to a list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js prospect add --email john@example.com --first-name John --last-name Doe --list-id 12345
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage prospect lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all lists
|
||||||
|
node tools/clis/snov.js lists list
|
||||||
|
|
||||||
|
# Get prospects in a list
|
||||||
|
node tools/clis/snov.js lists prospects --id 12345 --page 1 --per-page 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check domain technology stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/clis/snov.js technology check --domain example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage drip campaigns
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List campaigns
|
||||||
|
node tools/clis/snov.js drips list
|
||||||
|
|
||||||
|
# Get campaign details
|
||||||
|
node tools/clis/snov.js drips get --id 12345
|
||||||
|
|
||||||
|
# Add prospect to drip campaign
|
||||||
|
node tools/clis/snov.js drips add-prospect --id 12345 --email john@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
- Rate limits vary by plan
|
||||||
|
- OAuth tokens expire after a set period; CLI handles refresh automatically
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Link building**: Find contacts and run automated drip outreach
|
||||||
|
- **Prospecting**: Build and manage prospect lists
|
||||||
|
- **Technology research**: Check what tech stack a target domain uses
|
||||||
|
- **Email verification**: Clean lists before sending
|
||||||
191
tools/integrations/trustpilot.md
Normal file
191
tools/integrations/trustpilot.md
Normal file
|
|
@ -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
|
||||||
190
tools/integrations/typeform.md
Normal file
190
tools/integrations/typeform.md
Normal file
|
|
@ -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
|
||||||
164
tools/integrations/wistia.md
Normal file
164
tools/integrations/wistia.md
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue