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 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,4 @@
1
+ # Google Analytics 4 Extension โ€” Dependencies
2
+ google-auth>=2.0.0
3
+ google-auth-oauthlib>=1.0.0
4
+ google-analytics-data>=0.18.0
@@ -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()
@@ -0,0 +1,4 @@
1
+ # Google Search Console Extension โ€” Dependencies
2
+ google-auth>=2.0.0
3
+ google-auth-oauthlib>=1.0.0
4
+ google-api-python-client>=2.0.0
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.3.3",
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",