A simple CLI to monitor search performance and detect issues from your terminal.
- Unified Data: Pulls GSC + GA4 metrics into a single view.
- Automated Checks: Flags cannibalization, content decay, and anomalies.
- Health Score: Summarizes site performance (0-100) based on traffic, rank, and issues.
- Shareable reports: Generates clean, shareable HTML reports via S3 with simple recommendations.
Designed for SEOs who prefer the terminal for quick SEO checks across one or many properties.
- Multi-account Connect multiple Google accounts and manage properties per project.
- Data Ownership: Backfill and store historical data locally, bypassing the 16-month GSC limit.
- Private Sharing: Publish reports to S3 with a single command.
- How It Works
- Reporting
- Commands Quick Reference
- Command Documentation
- Installation
- Metrics Reference
- Roadmap
A quick snapshot of overall site performance. It blends traffic trends (40%), average positions (30%), and open issues (30%) to show whether a site looks healthy or needs attention.
- >80 = Green (Healthy)
- 50-79 = Yellow (Warning)
- <50 = Red (Critical)
Runs a few checks on every data pull to catch common problems:
- Cannibalization: Flags pages competing with each other when secondary pages take more than 10% of impressions.
- Content Decay: Finds pages that are down more than 20% from their peak traffic.
- Anomalies: Uses statistical checks (z-scores) to catch unusual traffic spikes or drops.
Looks for easy wins and missed opportunities:
- Striking Distance: Keywords ranking on page two (positions 11–20) that could move up.
- Low CTR: Pages ranking well (top 15) but getting fewer clicks than expected.
See Metrics Reference below for formulas and thresholds.
Two simple ways to check your sites’ health:
Use this for a fast check while you’re working:
seo status <project-slug>
Displays:
- Health Score
- Traffic & Position Trends
- Top Issues
Generates a modern HTML file that can be shared via S3 privately:
seo report <project-slug>
The report includes 5 detailed tabs:
| Tab | What's In It |
|---|---|
| Overview | KPI cards, weekly trends chart, position distribution, branded vs non-branded, device/country breakdown |
| Content & Keywords | Top pages with GSC + GA4 metrics combined, top queries, engagement opportunities |
| Movements | Keyword flux (new/lost), top gainers/losers, detected anomalies |
| Action Plan | Cannibalization, Content Decay, Striking Distance, and CTR opportunities |
| Glossary | Built-in reference |
# Authentication
seo auth # Connect Google account
seo auth status # Show connected accounts
seo auth remove <email> # Remove account
# Properties
seo properties # List discovered properties
seo properties sync # Re-fetch properties from Google
# Projects
seo projects # List all projects
seo projects new # Create new project
seo projects show <slug> # Show project details
seo projects edit <slug> # Edit project settings
seo projects delete <slug> # Delete a project
seo projects brand <slug> list # Show brand keywords
seo projects brand <slug> add <keywords...> # Add keywords
seo projects brand <slug> remove <keywords...> # Remove keywords
seo projects brand <slug> clear # Remove all brand keywords
# Data
seo pull [slug] [--start <YYYY-MM-DD>] [--end <YYYY-MM-DD>] [--all]
seo backfill <slug> [--months <n>] # Fetch historical data (up to 16 months)
# Analysis
seo status [slug] [--days <n>] [--limit <n>] [--no-queries] [--no-pages] [--full]
seo report <slug> [--days <n>] [--pdf] # Generate HTML/PDF report
# Publish
seo publish <project-slug|file-name> # Upload report to S3 and get a shareable private link
# Advanced
seo dev analyze <slug> # Re-run checks on local dataOpens browser for Google OAuth. You approve permissions for Search Console and Analytics.
Shows connected accounts with token status.
Email Connected Token Status
user@example.com 2025-12-06 Valid
team@example.com 2025-12-06 Needs refresh
Disconnects the specified account and removes stored credentials.
Lists discovered GSC and GA4 properties for all connected accounts.
Google Search Console Properties:
┌─────┬────────────────────┬────────────────────┐
│ ID │ Property │ Account │
├─────┼────────────────────┼────────────────────┤
│ 101 │ mysite.com │ owner@example.com │
│ 102 │ acme.com │ owner@example.com │
└─────┴────────────────────┴────────────────────┘
Google Analytics 4 Properties:
┌─────┬────────────────────┬────────────────────┐
│ ID │ Property │ Account │
├─────┼────────────────────┼────────────────────┤
│ 201 │ MySite GA4 │ owner@example.com │
│ 202 │ Acme GA4 │ owner@example.com │
└─────┴────────────────────┴────────────────────┘
Re-fetches properties from Google for all connected accounts.
Lists all projects.
┌────────┬───────────────┬────────────────────┬────────────────────┬───────────┐
│ Slug │ Name │ GSC Property │ GA4 Property │ Created │
├────────┼───────────────┼────────────────────┼────────────────────┼───────────┤
│ mysite │ MySite │ mysite.com │ Not connected │ 12/7/2025 │
│ acme │ Acme Corp │ acme.com │ Acme GA4 │ 12/7/2025 │
└────────┴───────────────┴────────────────────┴────────────────────┴───────────┘
Creates a new project interactively. Choose one GSC property (required), optionally a GA4 property, and optionally add brand keywords.
? Project name: Demo Detailer
? Slug: demo
? Select GSC property: demo.com (owner@example.com)
? Select GA4 property (optional): Demo GA4
? Brand keywords (comma-separated, blank to skip): demo detailer, demo.com
✓ Project 'demo' created
• GSC: demo.com
• GA4: Demo GA4
• Brand: demo detailer, demo.com
Shows project details including connected properties and brand keywords.
Edit project name or GA4 property.
Delete project.
Manage brand keywords (add, remove, list, clear).
seo projects brand mysite add "brand name, brand variant"
seo projects brand mysite remove "old keyword"
seo projects brand mysite clearPulls data from GSC and GA4, aggregates weekly metrics, and runs analysis.
Options:
--allPull all projects--start <YYYY-MM-DD>Start date--end <YYYY-MM-DD>End date
Defaults to last 7 days ending 3 days ago.
Date range: 2025-12-01 to 2025-12-05
Demo
Fetching GSC data...
GSC: 1,234 records
Aggregating weekly...
Fetching GA4 data...
GA4: 567 records
Analyzing data...
Analyzed: 2 anomalies, 1 cannibalization, 0 decay
✓ Data pull complete
Fetch historical data up to 16 months. Processes monthly chunks.
Options:
--months <n>Number of months (default: 16)
Backfilling 16 months of data for Demo
Processing 16 date chunks...
[1/16] 520 records
[2/16] 480 records
...
Backfill complete: 8,900 total records
Quick SEO status summary for all projects.
┌──────────┬────────┬────────┬───────────┬────────┬────────┐
│ Project │ Clicks │ Change │ Position │ Issues │ Health │
├──────────┼────────┼────────┼───────────┼────────┼────────┤
│ demo │ 12.4k │ +15.2%↑│ 12.4 │ 4 ⚠ │ 82 │
└──────────┴────────┴────────┴───────────┴────────┴────────┘
Shows expanded metrics, issues, and top pages for one project.
Options:
-d, --days <n>Days to analyze (default: 7)--limit <n>Top queries/pages to show (default: 20)--no-queriesHide queries section--no-pagesHide pages section--fullShow full URLs without truncation
Demo Detailer
Last 30 days | Health Score: 82
Window: Nov 12 → Dec 12, 2025
Search Console
┌─────────────┬──────────────────┐
│ Clicks │ 12,450 (+15.2% ↑)│
├─────────────┼──────────────────┤
│ Impressions │ 345,210 (+8.4% ↑)│
├─────────────┼──────────────────┤
│ CTR │ 3.6% (+0.2% ↑) │
├─────────────┼──────────────────┤
│ Position │ 12.4 (-1.2) │
└─────────────┴──────────────────┘
Analytics
Sessions: 14,200 (+12.5% ↑) • Users: 11,500 • Engagement: 58.4% • Conversions: 342
Issues (4)
• 2 Anomaly Alerts
• 1 Cannibalization Issues
• 1 Content Decay
Run "seo report demo" for details.
Top Queries
┌──────────────────────────────┬────────┬────────┬───────┬──────┐
│ Query │ Clicks │ Impr │ CTR │ Pos │
├──────────────────────────────┼────────┼────────┼───────┼──────┤
│ demo detailer │ 1,850 │ 3,500 │ 52.8% │ 1.0 │
│ mobile detailing near me │ 1,240 │ 12,500 │ 9.9% │ 2.1 │
│ car detailing metro city │ 850 │ 5,600 │ 15.2% │ 1.4 │
│ demo.com reviews │ 620 │ 1,200 │ 51.6% │ 1.1 │
│ interior car cleaning cost │ 420 │ 3,100 │ 13.5% │ 3.2 │
└──────────────────────────────┴────────┴────────┴───────┴──────┘
Top Pages
┌──────────────────────────────┬────────┬────────┬──────┐
│ Page │ Clicks │ Impr │ Pos │
├──────────────────────────────┼────────┼────────┼──────┤
│ /services/interior-detailing │ 2,100 │ 45,000 │ 4.2 │
│ /services/ceramic-coating │ 1,850 │ 32,000 │ 3.5 │
│ /pricing │ 1,500 │ 12,000 │ 2.1 │
│ /blog/how-often-should-i-det │ 940 │ 28,000 │ 6.8 │
└──────────────────────────────┴────────┴────────┴──────┘
Generates HTML report (and optional PDF).
Options:
--days <n>Days to include (default: 30)--pdfAlso generate PDF
Fetching data for 2025-11-04 → 2025-12-04...
✓ Data ready (9,450 GSC records)
Generating report...
✓ Report generated: ./reports/demo-2025-12-07.html
Reports saved to ./reports/<slug>-<date>.html
Publishes an HTML report to AWS S3 and returns a time-limited shareable URL.
Arguments:
targetProject slug (e.g.,demo) to publish the latest report, or a specific file name (e.g.,demo-2025-12-07.html) in./reports/.
Examples:
seo publish demo
seo publish demo-2025-11-11.html
Output:
✔ Shareable Link:
https://s3-.../reports/demo/demo-2025-12-07.html?X-Amz-...
Link expires in 7 days.
Notes:
- Requires
.envvariables:AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION,AWS_BUCKET_NAME. - If the target cannot be resolved, ensure the project slug is correct or the file exists in
reports/.
Re-runs checks on existing data without pulling from Google. Useful for testing.
- Node.js 20+
- Google account with Search Console access
git clone https://github.com/rajakhoury/seo-cli.git
cd seo-cli
npm installCopy .env.example to .env and fill in your Google OAuth credentials.
Initialize the database:
npx prisma migrate dev --name initBuild:
npm run buildRun CLI globally (optional):
# Link the CLI to use 'seo' command anywhere
npm link
# Usage
seo auth
seo projects new
seo pull mysiteAlternatively, you can run (locally from project root) without linking using node dist/index.js <command>.
- Go to Google Cloud Console
- Create a new project (or use existing)
- Enable APIs:
- Google Search Console API
- Google Analytics Data API
- Go to APIs & Services → Credentials
- Create OAuth 2.0 Client ID → Select Desktop app type
- Download the JSON file
- Open OAuth consent screen → Add yourself as a Test user
- Add these scopes:
https://www.googleapis.com/auth/webmasters.readonlyhttps://www.googleapis.com/auth/analytics.readonlyhttps://www.googleapis.com/auth/userinfo.email
- Add the credentials to your
.envfile
If you want to share reports via private URLs:
- Create an S3 bucket (e.g.,
seo-cli-reports) - Create an IAM user with S3 permissions
- Add access keys to
.env:AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION,AWS_BUCKET_NAME
API Limits & Data Depth
Fetches up to the API maximum per request to capture long-tail query data.
| Service | Current Setting | API Maximum |
|---|---|---|
| GSC | 25,000 rows | 25,000 |
| GA4 | 25,000 rows | 100,000 |
Adjusting for Speed
If you prefer faster pulls over complete data depth, you can lower these values in src/services/gsc-service.ts (rowLimit) and src/services/ga4-service.ts (limit).
- Database: SQLite (stored in
data/seo-cli.db) - APIs: Google Search Console API, Google Analytics Data API (v1beta)
- Data freshness: 3-day stability window to align GSC and GA4 data
To start fresh:
rm -rf data prisma/migrations
npx prisma migrate dev --name init
npm run build| Metric | Source | Definition & Calculation |
|---|---|---|
| Clicks | GSC | Total clicks from search. Calculated as the sum of daily clicks. |
| Impressions | GSC | Total times the site appeared in search results. Sum of daily impressions. |
| CTR | GSC | (Total Clicks ÷ Total Impressions) × 100. Calculated from totals, never averaged. |
| Avg Position | GSC | Sum(Position × Impressions) ÷ Total Impressions. Impression-weighted average. |
| Sessions | GA4 | A group of user interactions. Ends after 30 minutes of inactivity. |
| Engagement Rate | GA4 | % of sessions lasting >10s, having 2+ pageviews, or a conversion. |
| Conversion Rate | GA4 | (Conversions ÷ Sessions) × 100. |
| Est. Search Vol | Derived | Impressions ÷ (Expected CTR ÷ 100). Estimated using position-based CTR curves. |
Formula: (Traffic Score × 0.4) + (Position Score × 0.3) + (Issues Score × 0.3)
| Component | Weight | Calculation (Capped 0-100) |
|---|---|---|
| Traffic Score | 40% | Base 50 + (Clicks Change %) |
| Position Score | 30% | Base 50 - (Position Change × 10) |
| Issues Score | 30% | Base 100 - (Count of Open Issues × 10) |
Keyword Cannibalization
Note: Not all overlap is bad. In Local SEO, having multiple pages ranking for "roof repair near me" (e.g.,
/austinand/dallaspages) is often desired. Always verify before merging.
- Trigger: Secondary page impressions > 10% of Primary page. Min 100 total impressions.
- Cannibalization Score:
Secondary Impressions ÷ Primary Impressions - Priority:
Total Impressions × Cannibalization Score
Content Decay
- Trigger: >20% decline from peak (peak must be >2 months ago and >50 clicks).
- Formula:
(Peak Clicks - Current Clicks) ÷ Peak Clicks - Likely Causes: Ranking Drop (Pos worsened 5+), CTR Decline (Impr stable, CTR down), or Competitor activity.
Anomaly Detection
- Method: Modified Z-Score (robust against outliers).
- Trigger:
|Z| > 3.5(roughly 99.7% confidence). - Baseline: Last 30 days of data (requires min 7 days).
| Type | Definition | Potential Impact Calculation |
|---|---|---|
| Striking Distance | Keywords in Pos 11-20 (Page 2). | Impressions × 8% (8% = estimated Page 1 CTR gain) |
| CTR Opportunity | High impressions (>500), low CTR. Pos ≤ 15. | Deserved Clicks - Actual Clicks (using Curve B below) |
| Content Refresh | Recovering lost traffic from decay. | Peak Clicks - Current Clicks |
Used for search volume estimation (Curve A) and CTR opportunity checks (Curve B).
| Position | Curve A (Vol Est) | Curve B (Opp Check) |
|---|---|---|
| 1 | 27.6% | 28% |
| 2 | 15.8% | 15% |
| 3 | 11.0% | 11% |
| 4 | 8.4% | 8% |
| 5 | 6.3% | 6% |
| 6-10 | 4.9% → 2.2% | 5% → 2% |
| 11-20 | 1.5% | - |
Future enhancements:
- Dependency upgrades: Upgrade outdated packages.
- Adjustable Data Depth: Add a
DATA_DEPTHenvironment variable to control how much data is pulled from APIs.- Lite: 5k rows (fast daily checks)
- Standard: 25k rows (default)
- Complete: 100k rows (GA4 max; GSC remains capped at 25k)
- Faster report generation: Add a
--no-fetchflag toseo reportto rebuild reports from existing data without pulling.
Pull requests welcome. For major changes, open an issue first.
MIT © Raja Khoury

