bmad-plus 0.3.3 โ 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/EXTENSION.md +79 -0
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/ga4_client.py +200 -0
- package/oveanet-pack/seo-audit-360/extensions/google-analytics/requirements.txt +4 -0
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/EXTENSION.md +109 -0
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/gsc_client.py +186 -0
- package/oveanet-pack/seo-audit-360/extensions/google-search-console/requirements.txt +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to BMAD+ will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] โ 2026-03-19
|
|
9
|
+
|
|
10
|
+
### ๐ข SEO Engine โ Enterprise Extensions (Sprint 4)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Google Search Console extension** โ OAuth2 client for organic search data (queries, pages, coverage, sitemaps)
|
|
14
|
+
- **Google Analytics 4 extension** โ GA4 Data API client for organic traffic, landing pages, and engagement metrics
|
|
15
|
+
- Both extensions include setup guides, Python clients, and separate requirements
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- Extensions are **optional** and require OAuth2 setup (see `EXTENSION.md` in each directory)
|
|
19
|
+
- Core SEO Engine (SKILL.md + 3 agents + Python toolkit) works without extensions
|
|
20
|
+
- GSC and GA4 share OAuth2 credentials for simplified auth flow
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
8
24
|
## [0.3.3] โ 2026-03-19
|
|
9
25
|
|
|
10
26
|
### ๐งช SEO Engine โ Quality & Security (Sprint 3)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Google Analytics 4 Extension โ BMAD+ SEO Engine
|
|
2
|
+
|
|
3
|
+
> Author: Laurent Rochetta | BMAD+ SEO Engine v2.1
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This extension connects to Google Analytics 4 (GA4) Data API for organic traffic analysis. Uses the same OAuth2 credentials as the Search Console extension.
|
|
8
|
+
|
|
9
|
+
## Setup Guide
|
|
10
|
+
|
|
11
|
+
### Prerequisites
|
|
12
|
+
- Google Cloud project with **GA4 Data API** enabled
|
|
13
|
+
- OAuth2 credentials (same `credentials.json` as GSC extension)
|
|
14
|
+
- GA4 property ID (find in GA4 Admin > Property Settings)
|
|
15
|
+
|
|
16
|
+
### First Run
|
|
17
|
+
```bash
|
|
18
|
+
python ga4_client.py --setup --property 123456789
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Organic traffic overview
|
|
25
|
+
python ga4_client.py --organic https://example.com --property 123456789 --days 30
|
|
26
|
+
|
|
27
|
+
# Top organic landing pages
|
|
28
|
+
python ga4_client.py --landing https://example.com --property 123456789 --days 30
|
|
29
|
+
|
|
30
|
+
# Conversions from organic
|
|
31
|
+
python ga4_client.py --conversions https://example.com --property 123456789 --days 30
|
|
32
|
+
|
|
33
|
+
# Full export
|
|
34
|
+
python ga4_client.py --all https://example.com --property 123456789 --json > ga4-data.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Output Examples
|
|
38
|
+
|
|
39
|
+
### Organic Traffic
|
|
40
|
+
```
|
|
41
|
+
Organic Traffic (30 days):
|
|
42
|
+
Sessions: 12,450
|
|
43
|
+
Users: 8,230
|
|
44
|
+
New Users: 6,120
|
|
45
|
+
Engagement Rate: 72.3%
|
|
46
|
+
Avg Duration: 2m 45s
|
|
47
|
+
Bounce Rate: 36.1%
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Top Landing Pages
|
|
51
|
+
```
|
|
52
|
+
Top Organic Landing Pages:
|
|
53
|
+
1. /blog/ai-development โ 2,340 sessions, 78% engagement
|
|
54
|
+
2. / โ 1,850 sessions, 65% engagement
|
|
55
|
+
3. /features โ 1,120 sessions, 82% engagement
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Integration with SEO Engine
|
|
59
|
+
|
|
60
|
+
When installed, the SEO Engine can:
|
|
61
|
+
- Correlate crawled pages with actual organic traffic
|
|
62
|
+
- Identify high-traffic pages that need SEO optimization
|
|
63
|
+
- Track organic conversion attribution
|
|
64
|
+
- Detect pages with high impressions but low engagement (content quality issues)
|
|
65
|
+
|
|
66
|
+
## Dependencies
|
|
67
|
+
|
|
68
|
+
Same Google Auth libraries as GSC extension:
|
|
69
|
+
```
|
|
70
|
+
google-auth>=2.0.0
|
|
71
|
+
google-auth-oauthlib>=1.0.0
|
|
72
|
+
google-analytics-data>=0.18.0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Security Notes
|
|
76
|
+
|
|
77
|
+
- Uses same `credentials.json` and `token.json` as GSC extension
|
|
78
|
+
- GA4 property ID is not sensitive but should be stored per-project
|
|
79
|
+
- Add credentials to `.gitignore`
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Google Analytics 4 Client โ GA4 Data API client for organic traffic analysis.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- Organic traffic metrics (sessions, users, engagement)
|
|
7
|
+
- Landing page performance
|
|
8
|
+
- Conversion attribution
|
|
9
|
+
- Custom date ranges
|
|
10
|
+
|
|
11
|
+
Author: Laurent Rochetta
|
|
12
|
+
License: MIT
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
|
|
21
|
+
SCOPES = ["https://www.googleapis.com/auth/analytics.readonly"]
|
|
22
|
+
CREDENTIALS_FILE = os.path.join(os.path.dirname(__file__), "..", "google-search-console", "credentials.json")
|
|
23
|
+
TOKEN_FILE = os.path.join(os.path.dirname(__file__), "..", "google-search-console", "token.json")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_client(property_id: str):
|
|
27
|
+
"""Authenticate and return a GA4 BetaAnalyticsData client."""
|
|
28
|
+
try:
|
|
29
|
+
from google.oauth2.credentials import Credentials
|
|
30
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
31
|
+
from google.auth.transport.requests import Request
|
|
32
|
+
from google.analytics.data_v1beta import BetaAnalyticsDataClient
|
|
33
|
+
from google.analytics.data_v1beta.types import (
|
|
34
|
+
DateRange, Dimension, Metric, RunReportRequest, FilterExpression,
|
|
35
|
+
Filter,
|
|
36
|
+
)
|
|
37
|
+
except ImportError:
|
|
38
|
+
print(
|
|
39
|
+
"Error: Missing dependencies. Install:\n"
|
|
40
|
+
" pip install google-auth google-auth-oauthlib google-analytics-data",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
creds = None
|
|
46
|
+
if os.path.exists(TOKEN_FILE):
|
|
47
|
+
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
|
|
48
|
+
|
|
49
|
+
if not creds or not creds.valid:
|
|
50
|
+
if creds and creds.expired and creds.refresh_token:
|
|
51
|
+
creds.refresh(Request())
|
|
52
|
+
else:
|
|
53
|
+
if not os.path.exists(CREDENTIALS_FILE):
|
|
54
|
+
print(f"Error: credentials.json not found. See EXTENSION.md for setup.", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
|
|
57
|
+
creds = flow.run_local_server(port=0)
|
|
58
|
+
|
|
59
|
+
with open(TOKEN_FILE, "w") as f:
|
|
60
|
+
f.write(creds.to_json())
|
|
61
|
+
|
|
62
|
+
return BetaAnalyticsDataClient(credentials=creds), property_id
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_organic_report(client, property_id: str, days: int = 30) -> dict:
|
|
66
|
+
"""Get organic traffic overview."""
|
|
67
|
+
from google.analytics.data_v1beta.types import (
|
|
68
|
+
DateRange, Metric, RunReportRequest, FilterExpression, Filter,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
end_date = datetime.now().date()
|
|
72
|
+
start_date = end_date - timedelta(days=days)
|
|
73
|
+
|
|
74
|
+
request = RunReportRequest(
|
|
75
|
+
property=f"properties/{property_id}",
|
|
76
|
+
date_ranges=[DateRange(start_date=start_date.isoformat(), end_date=end_date.isoformat())],
|
|
77
|
+
metrics=[
|
|
78
|
+
Metric(name="sessions"),
|
|
79
|
+
Metric(name="totalUsers"),
|
|
80
|
+
Metric(name="newUsers"),
|
|
81
|
+
Metric(name="engagementRate"),
|
|
82
|
+
Metric(name="averageSessionDuration"),
|
|
83
|
+
Metric(name="bounceRate"),
|
|
84
|
+
],
|
|
85
|
+
dimension_filter=FilterExpression(
|
|
86
|
+
filter=Filter(
|
|
87
|
+
field_name="sessionDefaultChannelGroup",
|
|
88
|
+
string_filter=Filter.StringFilter(value="Organic Search"),
|
|
89
|
+
)
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
response = client.run_report(request)
|
|
94
|
+
|
|
95
|
+
if not response.rows:
|
|
96
|
+
return {"error": "No organic data available for this period"}
|
|
97
|
+
|
|
98
|
+
row = response.rows[0]
|
|
99
|
+
return {
|
|
100
|
+
"sessions": int(row.metric_values[0].value),
|
|
101
|
+
"users": int(row.metric_values[1].value),
|
|
102
|
+
"new_users": int(row.metric_values[2].value),
|
|
103
|
+
"engagement_rate": round(float(row.metric_values[3].value) * 100, 1),
|
|
104
|
+
"avg_duration_seconds": round(float(row.metric_values[4].value)),
|
|
105
|
+
"bounce_rate": round(float(row.metric_values[5].value) * 100, 1),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def run_landing_page_report(client, property_id: str, days: int = 30, limit: int = 20) -> list:
|
|
110
|
+
"""Get top organic landing pages."""
|
|
111
|
+
from google.analytics.data_v1beta.types import (
|
|
112
|
+
DateRange, Dimension, Metric, RunReportRequest, FilterExpression, Filter,
|
|
113
|
+
OrderBy,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
end_date = datetime.now().date()
|
|
117
|
+
start_date = end_date - timedelta(days=days)
|
|
118
|
+
|
|
119
|
+
request = RunReportRequest(
|
|
120
|
+
property=f"properties/{property_id}",
|
|
121
|
+
date_ranges=[DateRange(start_date=start_date.isoformat(), end_date=end_date.isoformat())],
|
|
122
|
+
dimensions=[Dimension(name="landingPage")],
|
|
123
|
+
metrics=[
|
|
124
|
+
Metric(name="sessions"),
|
|
125
|
+
Metric(name="engagementRate"),
|
|
126
|
+
Metric(name="averageSessionDuration"),
|
|
127
|
+
],
|
|
128
|
+
dimension_filter=FilterExpression(
|
|
129
|
+
filter=Filter(
|
|
130
|
+
field_name="sessionDefaultChannelGroup",
|
|
131
|
+
string_filter=Filter.StringFilter(value="Organic Search"),
|
|
132
|
+
)
|
|
133
|
+
),
|
|
134
|
+
order_bys=[OrderBy(metric=OrderBy.MetricOrderBy(metric_name="sessions"), desc=True)],
|
|
135
|
+
limit=limit,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
response = client.run_report(request)
|
|
139
|
+
|
|
140
|
+
return [{
|
|
141
|
+
"page": row.dimension_values[0].value,
|
|
142
|
+
"sessions": int(row.metric_values[0].value),
|
|
143
|
+
"engagement_rate": round(float(row.metric_values[1].value) * 100, 1),
|
|
144
|
+
"avg_duration": round(float(row.metric_values[2].value)),
|
|
145
|
+
} for row in response.rows]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# โโ CLI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
149
|
+
|
|
150
|
+
def main():
|
|
151
|
+
parser = argparse.ArgumentParser(
|
|
152
|
+
description="Google Analytics 4 Client (BMAD+ SEO Engine)"
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument("--property", "-p", required=True, help="GA4 Property ID")
|
|
155
|
+
parser.add_argument("--organic", action="store_true", help="Organic traffic overview")
|
|
156
|
+
parser.add_argument("--landing", action="store_true", help="Top landing pages")
|
|
157
|
+
parser.add_argument("--all", action="store_true", help="All reports")
|
|
158
|
+
parser.add_argument("--days", type=int, default=30, help="Days lookback (default: 30)")
|
|
159
|
+
parser.add_argument("--limit", type=int, default=20, help="Max rows (default: 20)")
|
|
160
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
161
|
+
parser.add_argument("--setup", action="store_true", help="Run OAuth2 setup")
|
|
162
|
+
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
client, property_id = get_client(args.property)
|
|
166
|
+
|
|
167
|
+
if args.setup:
|
|
168
|
+
print("โ
OAuth2 setup complete for GA4.")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
if args.organic or args.all:
|
|
172
|
+
data = run_organic_report(client, property_id, args.days)
|
|
173
|
+
if args.json:
|
|
174
|
+
print(json.dumps({"organic": data}, indent=2))
|
|
175
|
+
else:
|
|
176
|
+
print(f"\nOrganic Traffic ({args.days} days):")
|
|
177
|
+
if "error" in data:
|
|
178
|
+
print(f" {data['error']}")
|
|
179
|
+
else:
|
|
180
|
+
mins = data["avg_duration_seconds"] // 60
|
|
181
|
+
secs = data["avg_duration_seconds"] % 60
|
|
182
|
+
print(f" Sessions: {data['sessions']:,}")
|
|
183
|
+
print(f" Users: {data['users']:,}")
|
|
184
|
+
print(f" New Users: {data['new_users']:,}")
|
|
185
|
+
print(f" Engagement: {data['engagement_rate']}%")
|
|
186
|
+
print(f" Avg Duration: {mins}m {secs}s")
|
|
187
|
+
print(f" Bounce Rate: {data['bounce_rate']}%")
|
|
188
|
+
|
|
189
|
+
if args.landing or args.all:
|
|
190
|
+
pages = run_landing_page_report(client, property_id, args.days, args.limit)
|
|
191
|
+
if args.json:
|
|
192
|
+
print(json.dumps({"landing_pages": pages}, indent=2))
|
|
193
|
+
else:
|
|
194
|
+
print(f"\nTop Organic Landing Pages:")
|
|
195
|
+
for i, p in enumerate(pages, 1):
|
|
196
|
+
print(f" {i:2}. {p['page'][:55]} โ {p['sessions']:,} sessions, {p['engagement_rate']}% engagement")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Google Search Console Extension โ BMAD+ SEO Engine
|
|
2
|
+
|
|
3
|
+
> Author: Laurent Rochetta | BMAD+ SEO Engine v2.1
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This extension connects the SEO Engine to Google Search Console API v3 for accessing real organic search performance data. Requires OAuth2 authentication with a Google Cloud project.
|
|
8
|
+
|
|
9
|
+
## Setup Guide
|
|
10
|
+
|
|
11
|
+
### 1. Create Google Cloud Project
|
|
12
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com)
|
|
13
|
+
2. Create a new project or select existing
|
|
14
|
+
3. Enable **Google Search Console API**
|
|
15
|
+
|
|
16
|
+
### 2. Create OAuth2 Credentials
|
|
17
|
+
1. Go to **APIs & Services > Credentials**
|
|
18
|
+
2. Click **Create Credentials > OAuth 2.0 Client ID**
|
|
19
|
+
3. Application type: **Desktop app**
|
|
20
|
+
4. Download the JSON โ save as `credentials.json` in this directory
|
|
21
|
+
|
|
22
|
+
### 3. First Run
|
|
23
|
+
```bash
|
|
24
|
+
python gsc_client.py --setup
|
|
25
|
+
```
|
|
26
|
+
This opens a browser for OAuth consent. The refresh token is saved to `token.json` for subsequent runs.
|
|
27
|
+
|
|
28
|
+
### 4. Verify Access
|
|
29
|
+
```bash
|
|
30
|
+
python gsc_client.py --sites
|
|
31
|
+
```
|
|
32
|
+
Should list all verified Search Console properties.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# List verified sites
|
|
38
|
+
python gsc_client.py --sites
|
|
39
|
+
|
|
40
|
+
# Top queries (default: 28 days)
|
|
41
|
+
python gsc_client.py --queries https://example.com --days 28
|
|
42
|
+
|
|
43
|
+
# Top pages by organic traffic
|
|
44
|
+
python gsc_client.py --pages https://example.com --days 28
|
|
45
|
+
|
|
46
|
+
# Index coverage (errors, valid, excluded)
|
|
47
|
+
python gsc_client.py --coverage https://example.com
|
|
48
|
+
|
|
49
|
+
# Sitemap status
|
|
50
|
+
python gsc_client.py --sitemaps https://example.com
|
|
51
|
+
|
|
52
|
+
# Full export (all data, JSON)
|
|
53
|
+
python gsc_client.py --all https://example.com --json > gsc-data.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Output Examples
|
|
57
|
+
|
|
58
|
+
### Queries
|
|
59
|
+
```
|
|
60
|
+
Top Organic Queries (28 days):
|
|
61
|
+
1. "bmad framework" โ Pos: 3.2, Clicks: 450, CTR: 12.3%, Imp: 3,658
|
|
62
|
+
2. "ai development tool" โ Pos: 8.1, Clicks: 120, CTR: 3.5%, Imp: 3,428
|
|
63
|
+
3. "multi-agent coding" โ Pos: 5.4, Clicks: 95, CTR: 7.8%, Imp: 1,218
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Coverage
|
|
67
|
+
```
|
|
68
|
+
Index Coverage:
|
|
69
|
+
โ
Valid: 142 pages
|
|
70
|
+
โ ๏ธ Valid with warnings: 8 pages
|
|
71
|
+
โ Error: 3 pages
|
|
72
|
+
โ Excluded: 45 pages (noindex, canonical, etc.)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Integration with SEO Engine
|
|
76
|
+
|
|
77
|
+
When this extension is installed, the SKILL.md orchestrator can:
|
|
78
|
+
- Include real organic data in Phase 4 scoring
|
|
79
|
+
- Compare GSC impressions with crawled pages to find content gaps
|
|
80
|
+
- Detect indexed pages with declining CTR โ priority optimization targets
|
|
81
|
+
- Cross-reference sitemap URLs with actually indexed pages
|
|
82
|
+
|
|
83
|
+
## Files
|
|
84
|
+
|
|
85
|
+
| File | Purpose |
|
|
86
|
+
|------|---------|
|
|
87
|
+
| `EXTENSION.md` | This documentation |
|
|
88
|
+
| `gsc_client.py` | OAuth2 flow + API calls |
|
|
89
|
+
| `requirements.txt` | Python dependencies |
|
|
90
|
+
|
|
91
|
+
## Dependencies SUPPLรMENTAIRES
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
google-auth>=2.0.0
|
|
95
|
+
google-auth-oauthlib>=1.0.0
|
|
96
|
+
google-api-python-client>=2.0.0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Security Notes
|
|
100
|
+
|
|
101
|
+
- `credentials.json` โ Contains client ID/secret. **Do not commit to Git.**
|
|
102
|
+
- `token.json` โ Contains refresh token. **Do not commit to Git.**
|
|
103
|
+
- Add both to `.gitignore`
|
|
104
|
+
|
|
105
|
+
## Rate Limits
|
|
106
|
+
|
|
107
|
+
- 1,200 queries per minute per project
|
|
108
|
+
- 25,000 rows per request maximum
|
|
109
|
+
- Data typically 2-3 days delayed
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Google Search Console Client โ OAuth2 API client for organic search data.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
- OAuth2 flow with credential persistence
|
|
7
|
+
- Query performance data (queries, pages, devices, countries)
|
|
8
|
+
- Index coverage status
|
|
9
|
+
- Sitemap submissions
|
|
10
|
+
|
|
11
|
+
Author: Laurent Rochetta
|
|
12
|
+
License: MIT
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
|
|
21
|
+
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
|
|
22
|
+
CREDENTIALS_FILE = os.path.join(os.path.dirname(__file__), "credentials.json")
|
|
23
|
+
TOKEN_FILE = os.path.join(os.path.dirname(__file__), "token.json")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_service():
|
|
27
|
+
"""Authenticate and return a Search Console service object."""
|
|
28
|
+
try:
|
|
29
|
+
from google.oauth2.credentials import Credentials
|
|
30
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
31
|
+
from googleapiclient.discovery import build
|
|
32
|
+
from google.auth.transport.requests import Request
|
|
33
|
+
except ImportError:
|
|
34
|
+
print(
|
|
35
|
+
"Error: Missing dependencies. Install:\n"
|
|
36
|
+
" pip install google-auth google-auth-oauthlib google-api-python-client",
|
|
37
|
+
file=sys.stderr,
|
|
38
|
+
)
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
|
|
41
|
+
creds = None
|
|
42
|
+
if os.path.exists(TOKEN_FILE):
|
|
43
|
+
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
|
|
44
|
+
|
|
45
|
+
if not creds or not creds.valid:
|
|
46
|
+
if creds and creds.expired and creds.refresh_token:
|
|
47
|
+
creds.refresh(Request())
|
|
48
|
+
else:
|
|
49
|
+
if not os.path.exists(CREDENTIALS_FILE):
|
|
50
|
+
print(
|
|
51
|
+
f"Error: {CREDENTIALS_FILE} not found.\n"
|
|
52
|
+
"Download OAuth2 credentials from Google Cloud Console.\n"
|
|
53
|
+
"See EXTENSION.md for setup guide.",
|
|
54
|
+
file=sys.stderr,
|
|
55
|
+
)
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
|
|
58
|
+
creds = flow.run_local_server(port=0)
|
|
59
|
+
|
|
60
|
+
with open(TOKEN_FILE, "w") as f:
|
|
61
|
+
f.write(creds.to_json())
|
|
62
|
+
|
|
63
|
+
return build("searchconsole", "v1", credentials=creds)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_sites(service):
|
|
67
|
+
"""List all verified Search Console properties."""
|
|
68
|
+
result = service.sites().list().execute()
|
|
69
|
+
sites = result.get("siteEntry", [])
|
|
70
|
+
return [{"url": s["siteUrl"], "level": s["permissionLevel"]} for s in sites]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def query_performance(service, site_url: str, days: int = 28, dimensions: list = None, row_limit: int = 25):
|
|
74
|
+
"""Query Search Analytics for organic performance data."""
|
|
75
|
+
if dimensions is None:
|
|
76
|
+
dimensions = ["query"]
|
|
77
|
+
|
|
78
|
+
end_date = datetime.now().date()
|
|
79
|
+
start_date = end_date - timedelta(days=days)
|
|
80
|
+
|
|
81
|
+
body = {
|
|
82
|
+
"startDate": start_date.isoformat(),
|
|
83
|
+
"endDate": end_date.isoformat(),
|
|
84
|
+
"dimensions": dimensions,
|
|
85
|
+
"rowLimit": row_limit,
|
|
86
|
+
"dataState": "all",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
result = service.searchanalytics().query(siteUrl=site_url, body=body).execute()
|
|
90
|
+
rows = result.get("rows", [])
|
|
91
|
+
|
|
92
|
+
return [{
|
|
93
|
+
"keys": row["keys"],
|
|
94
|
+
"clicks": row["clicks"],
|
|
95
|
+
"impressions": row["impressions"],
|
|
96
|
+
"ctr": round(row["ctr"] * 100, 1),
|
|
97
|
+
"position": round(row["position"], 1),
|
|
98
|
+
} for row in rows]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_sitemaps(service, site_url: str):
|
|
102
|
+
"""Get sitemap submission status."""
|
|
103
|
+
result = service.sitemaps().list(siteUrl=site_url).execute()
|
|
104
|
+
sitemaps = result.get("sitemap", [])
|
|
105
|
+
return [{
|
|
106
|
+
"path": s["path"],
|
|
107
|
+
"type": s.get("type", ""),
|
|
108
|
+
"submitted": s.get("lastSubmitted", ""),
|
|
109
|
+
"warnings": s.get("warnings", 0),
|
|
110
|
+
"errors": s.get("errors", 0),
|
|
111
|
+
} for s in sitemaps]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# โโ CLI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
115
|
+
|
|
116
|
+
def main():
|
|
117
|
+
parser = argparse.ArgumentParser(
|
|
118
|
+
description="Google Search Console Client (BMAD+ SEO Engine)"
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument("site", nargs="?", help="Site URL (e.g. https://example.com)")
|
|
121
|
+
parser.add_argument("--setup", action="store_true", help="Run OAuth2 setup flow")
|
|
122
|
+
parser.add_argument("--sites", action="store_true", help="List verified sites")
|
|
123
|
+
parser.add_argument("--queries", action="store_true", help="Top queries")
|
|
124
|
+
parser.add_argument("--pages", action="store_true", help="Top pages")
|
|
125
|
+
parser.add_argument("--sitemaps", action="store_true", help="Sitemap status")
|
|
126
|
+
parser.add_argument("--all", action="store_true", help="All data")
|
|
127
|
+
parser.add_argument("--days", type=int, default=28, help="Days lookback (default: 28)")
|
|
128
|
+
parser.add_argument("--limit", type=int, default=25, help="Max rows (default: 25)")
|
|
129
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
130
|
+
|
|
131
|
+
args = parser.parse_args()
|
|
132
|
+
|
|
133
|
+
service = get_service()
|
|
134
|
+
|
|
135
|
+
if args.setup:
|
|
136
|
+
print("โ
OAuth2 setup complete. Token saved.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if args.sites:
|
|
140
|
+
sites = list_sites(service)
|
|
141
|
+
if args.json:
|
|
142
|
+
print(json.dumps(sites, indent=2))
|
|
143
|
+
else:
|
|
144
|
+
print("\nVerified Sites:")
|
|
145
|
+
for s in sites:
|
|
146
|
+
print(f" {s['url']} ({s['level']})")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if not args.site:
|
|
150
|
+
parser.print_help()
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if args.queries or args.all:
|
|
154
|
+
data = query_performance(service, args.site, args.days, ["query"], args.limit)
|
|
155
|
+
if args.json:
|
|
156
|
+
print(json.dumps({"queries": data}, indent=2))
|
|
157
|
+
else:
|
|
158
|
+
print(f"\nTop Queries ({args.days} days):")
|
|
159
|
+
for i, row in enumerate(data, 1):
|
|
160
|
+
q = row["keys"][0]
|
|
161
|
+
print(f" {i:2}. \"{q[:50]}\" โ Pos: {row['position']}, "
|
|
162
|
+
f"Clicks: {row['clicks']:,}, CTR: {row['ctr']}%, Imp: {row['impressions']:,}")
|
|
163
|
+
|
|
164
|
+
if args.pages or args.all:
|
|
165
|
+
data = query_performance(service, args.site, args.days, ["page"], args.limit)
|
|
166
|
+
if args.json:
|
|
167
|
+
print(json.dumps({"pages": data}, indent=2))
|
|
168
|
+
else:
|
|
169
|
+
print(f"\nTop Pages ({args.days} days):")
|
|
170
|
+
for i, row in enumerate(data, 1):
|
|
171
|
+
page = row["keys"][0]
|
|
172
|
+
print(f" {i:2}. {page[:60]} โ Clicks: {row['clicks']:,}, CTR: {row['ctr']}%")
|
|
173
|
+
|
|
174
|
+
if args.sitemaps or args.all:
|
|
175
|
+
data = get_sitemaps(service, args.site)
|
|
176
|
+
if args.json:
|
|
177
|
+
print(json.dumps({"sitemaps": data}, indent=2))
|
|
178
|
+
else:
|
|
179
|
+
print(f"\nSitemaps:")
|
|
180
|
+
for s in data:
|
|
181
|
+
status = "โ
" if s["errors"] == 0 else "โ"
|
|
182
|
+
print(f" {status} {s['path']} (type: {s['type']}, errors: {s['errors']})")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-plus",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "BMAD+ โ Augmented AI-Driven Development Framework with multi-role agents, autopilot, and parallel execution",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|