bmad-plus 0.12.1 → 0.12.2

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.
Files changed (156) hide show
  1. package/CHANGELOG.md +612 -580
  2. package/README.md +90 -115
  3. package/osint-agent-package/agents/osint-investigator.md +12 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  10. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  11. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  12. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  13. package/package.json +104 -91
  14. package/readme-international/README.de.md +596 -594
  15. package/readme-international/README.es.md +613 -611
  16. package/readme-international/README.fr.md +611 -609
  17. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  18. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  19. package/src/bmad-plus/module.yaml +283 -283
  20. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  21. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  22. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  23. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  24. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  25. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  83. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  84. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  87. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  88. package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
  89. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  90. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  91. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  92. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  93. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  94. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  95. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  96. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  97. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  99. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  100. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +304 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +410 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  106. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  107. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  108. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  109. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  110. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  111. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  112. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  135. package/tools/build/check-counts.js +628 -0
  136. package/tools/build/generated-adapters/.codex/AGENTS.md +1 -1
  137. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +1 -1
  138. package/tools/build/generated-adapters/.opencode/AGENTS.md +1 -1
  139. package/tools/build/generated-adapters/AGENTS.md +1 -1
  140. package/tools/build/generated-adapters/CLAUDE.md +1 -1
  141. package/tools/build/generated-adapters/CONVENTIONS.md +1 -1
  142. package/tools/build/generated-adapters/GEMINI.md +1 -1
  143. package/tools/cli/commands/autoconfig.js +470 -470
  144. package/tools/cli/commands/doctor.js +233 -233
  145. package/tools/cli/commands/install.js +598 -501
  146. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  147. package/tools/cli/commands/memory.js +195 -195
  148. package/tools/cli/commands/scan.js +348 -348
  149. package/tools/cli/commands/uninstall.js +101 -101
  150. package/tools/cli/commands/update.js +134 -134
  151. package/tools/cli/i18n.js +845 -845
  152. package/tools/cli/lib/README-memory-journal.md +125 -125
  153. package/tools/cli/lib/ide-config.js +267 -259
  154. package/tools/cli/lib/python-provision.js +508 -508
  155. package/tools/cli/lib/stack-detect.js +102 -102
  156. package/tools/cli/lib/validate.js +50 -50
@@ -0,0 +1,17 @@
1
+ # BMAD+ SEO Engine — Python Dependencies
2
+ # Install: pip install -r requirements.txt
3
+ # Author: Laurent Rochetta
4
+
5
+ # Core (required)
6
+ requests>=2.34.2 # CVE-2024-35195: require patched line
7
+ beautifulsoup4==4.15.0
8
+
9
+ # Hardened XML parsing (SSRF / XXE / billion-laughs defense)
10
+ defusedxml==0.7.1
11
+
12
+ # Fast HTML parser (recommended)
13
+ lxml==6.1.1
14
+
15
+ # Screenshot capture (optional — only for seo_screenshot.py)
16
+ # Uncomment and run: playwright install chromium
17
+ # playwright>=1.40.0
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SEO APIs — Google free API client for live SEO data.
4
+
5
+ Connects to:
6
+ - PageSpeed Insights API v5 (lab scores + audits)
7
+ - Chrome UX Report (CrUX) API (field CWV data)
8
+ - Rich Results Test API (schema validation)
9
+
10
+ Requires: GOOGLE_API_KEY environment variable (free, no OAuth).
11
+ Get one at: https://console.cloud.google.com/apis/credentials
12
+
13
+ Author: Laurent Rochetta
14
+ License: MIT
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from typing import Optional
22
+
23
+ try:
24
+ import requests
25
+ except ImportError:
26
+ print("Error: requests required. Install: pip install requests", file=sys.stderr)
27
+ sys.exit(1)
28
+
29
+
30
+ API_KEY = os.environ.get("GOOGLE_API_KEY", "")
31
+
32
+ PSI_ENDPOINT = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
33
+ CRUX_ENDPOINT = "https://chromeuxreport.googleapis.com/v1/records:queryRecord"
34
+ RICH_RESULTS_ENDPOINT = "https://searchconsole.googleapis.com/v1/urlTestingTools/mobileFriendlyTest:run"
35
+
36
+
37
+ # ── PageSpeed Insights ─────────────────────────────────────────────
38
+
39
+ def run_pagespeed(url: str, strategy: str = "mobile", categories: Optional[list] = None) -> dict:
40
+ """
41
+ Run PageSpeed Insights audit.
42
+
43
+ Args:
44
+ url: URL to audit
45
+ strategy: "mobile" or "desktop"
46
+ categories: List of categories (PERFORMANCE, ACCESSIBILITY, BEST_PRACTICES, SEO)
47
+
48
+ Returns:
49
+ Structured result with scores, audits, and opportunities
50
+ """
51
+ if not API_KEY:
52
+ return {"error": "GOOGLE_API_KEY not set. Get one at https://console.cloud.google.com/apis/credentials"}
53
+
54
+ if categories is None:
55
+ categories = ["PERFORMANCE", "ACCESSIBILITY", "BEST_PRACTICES", "SEO"]
56
+
57
+ params = {
58
+ "url": url,
59
+ "key": API_KEY,
60
+ "strategy": strategy,
61
+ "category": categories,
62
+ }
63
+
64
+ try:
65
+ response = requests.get(PSI_ENDPOINT, params=params, timeout=120)
66
+ response.raise_for_status()
67
+ data = response.json()
68
+ except requests.RequestException as e:
69
+ return {"error": f"PSI API request failed: {e}"}
70
+
71
+ # Extract scores
72
+ result = {
73
+ "url": url,
74
+ "strategy": strategy,
75
+ "scores": {},
76
+ "cwv": {},
77
+ "failing_audits": [],
78
+ "opportunities": [],
79
+ }
80
+
81
+ # Category scores
82
+ categories_data = data.get("lighthouseResult", {}).get("categories", {})
83
+ for cat_id, cat_data in categories_data.items():
84
+ score = cat_data.get("score", 0)
85
+ result["scores"][cat_id] = round(score * 100)
86
+
87
+ # Core Web Vitals from Lighthouse
88
+ audits = data.get("lighthouseResult", {}).get("audits", {})
89
+
90
+ cwv_metrics = {
91
+ "largest-contentful-paint": "LCP",
92
+ "interaction-to-next-paint": "INP",
93
+ "cumulative-layout-shift": "CLS",
94
+ "first-contentful-paint": "FCP",
95
+ "total-blocking-time": "TBT",
96
+ "speed-index": "SI",
97
+ }
98
+
99
+ for audit_id, label in cwv_metrics.items():
100
+ if audit_id in audits:
101
+ audit = audits[audit_id]
102
+ result["cwv"][label] = {
103
+ "value": audit.get("numericValue"),
104
+ "display": audit.get("displayValue", ""),
105
+ "score": round(audit.get("score", 0) * 100),
106
+ }
107
+
108
+ # Failing audits (score < 1.0)
109
+ for audit_id, audit in audits.items():
110
+ score = audit.get("score")
111
+ if score is not None and score < 0.9 and audit.get("title"):
112
+ severity = "critical" if score < 0.5 else "warning"
113
+ result["failing_audits"].append({
114
+ "id": audit_id,
115
+ "title": audit.get("title", ""),
116
+ "description": audit.get("description", "")[:200],
117
+ "score": round(score * 100),
118
+ "severity": severity,
119
+ "display_value": audit.get("displayValue", ""),
120
+ })
121
+
122
+ # Sort failures by score (worst first)
123
+ result["failing_audits"].sort(key=lambda x: x["score"])
124
+
125
+ # Opportunities (have savings)
126
+ for audit_id, audit in audits.items():
127
+ details = audit.get("details", {})
128
+ if details.get("type") == "opportunity" and details.get("overallSavingsMs", 0) > 0:
129
+ result["opportunities"].append({
130
+ "id": audit_id,
131
+ "title": audit.get("title", ""),
132
+ "savings_ms": details.get("overallSavingsMs", 0),
133
+ "savings_bytes": details.get("overallSavingsBytes", 0),
134
+ })
135
+
136
+ result["opportunities"].sort(key=lambda x: x["savings_ms"], reverse=True)
137
+
138
+ # Field data (CrUX from PSI response)
139
+ loading_exp = data.get("loadingExperience", {})
140
+ if loading_exp.get("metrics"):
141
+ result["field_data"] = {}
142
+ for metric_id, metric_data in loading_exp["metrics"].items():
143
+ result["field_data"][metric_id] = {
144
+ "percentile": metric_data.get("percentile"),
145
+ "category": metric_data.get("category"),
146
+ }
147
+
148
+ return result
149
+
150
+
151
+ # ── CrUX API ───────────────────────────────────────────────────────
152
+
153
+ def run_crux(url: str, form_factor: str = "PHONE") -> dict:
154
+ """
155
+ Query Chrome UX Report for real-world performance data.
156
+
157
+ Args:
158
+ url: URL or origin to query
159
+ form_factor: PHONE, DESKTOP, or ALL_FORM_FACTORS
160
+
161
+ Returns:
162
+ Field CWV data at 75th percentile
163
+ """
164
+ if not API_KEY:
165
+ return {"error": "GOOGLE_API_KEY not set"}
166
+
167
+ # Try URL-level first, fall back to origin
168
+ from urllib.parse import urlparse
169
+ parsed = urlparse(url)
170
+ origin = f"{parsed.scheme}://{parsed.netloc}"
171
+
172
+ payload = {
173
+ "url": url,
174
+ "formFactor": form_factor,
175
+ }
176
+
177
+ try:
178
+ response = requests.post(
179
+ f"{CRUX_ENDPOINT}?key={API_KEY}",
180
+ json=payload,
181
+ timeout=30,
182
+ )
183
+
184
+ if response.status_code == 404:
185
+ # No URL-level data, try origin
186
+ payload = {"origin": origin, "formFactor": form_factor}
187
+ response = requests.post(
188
+ f"{CRUX_ENDPOINT}?key={API_KEY}",
189
+ json=payload,
190
+ timeout=30,
191
+ )
192
+
193
+ if response.status_code == 404:
194
+ return {"error": f"No CrUX data available for {url} (not enough traffic)"}
195
+
196
+ response.raise_for_status()
197
+ data = response.json()
198
+ except requests.RequestException as e:
199
+ return {"error": f"CrUX API request failed: {e}"}
200
+
201
+ result = {
202
+ "url": url,
203
+ "form_factor": form_factor,
204
+ "metrics": {},
205
+ "collection_period": {},
206
+ }
207
+
208
+ # Extract metrics
209
+ metrics = data.get("record", {}).get("metrics", {})
210
+
211
+ metric_map = {
212
+ "largest_contentful_paint": "LCP",
213
+ "interaction_to_next_paint": "INP",
214
+ "cumulative_layout_shift": "CLS",
215
+ "first_contentful_paint": "FCP",
216
+ "experimental_time_to_first_byte": "TTFB",
217
+ }
218
+
219
+ for api_name, label in metric_map.items():
220
+ if api_name in metrics:
221
+ m = metrics[api_name]
222
+ p75 = m.get("percentiles", {}).get("p75")
223
+ histogram = m.get("histogram", [])
224
+
225
+ # Calculate good/needs-improvement/poor distribution
226
+ distribution = {}
227
+ for bucket in histogram:
228
+ density = bucket.get("density", 0)
229
+ if bucket.get("end"):
230
+ distribution["good"] = distribution.get("good", 0) + density
231
+ elif "start" in bucket and "end" not in bucket:
232
+ distribution["poor"] = density
233
+ else:
234
+ distribution["needs_improvement"] = density
235
+
236
+ result["metrics"][label] = {
237
+ "p75": p75,
238
+ "distribution": distribution,
239
+ }
240
+
241
+ # Collection period
242
+ period = data.get("record", {}).get("collectionPeriod", {})
243
+ result["collection_period"] = {
244
+ "first_date": period.get("firstDate", {}),
245
+ "last_date": period.get("lastDate", {}),
246
+ }
247
+
248
+ return result
249
+
250
+
251
+ # ── Rich Results Test ──────────────────────────────────────────────
252
+
253
+ def run_rich_results_test(url: str) -> dict:
254
+ """
255
+ Check if a URL is eligible for rich results.
256
+
257
+ Note: This uses the URL Testing Tools API (Mobile-Friendly Test)
258
+ which also returns rich results information. The dedicated Rich Results
259
+ Test API requires OAuth2, so we use this free alternative.
260
+
261
+ Args:
262
+ url: URL to test
263
+
264
+ Returns:
265
+ Mobile-friendly status and detected structured data
266
+ """
267
+ if not API_KEY:
268
+ return {"error": "GOOGLE_API_KEY not set"}
269
+
270
+ payload = {"url": url}
271
+
272
+ try:
273
+ response = requests.post(
274
+ f"{RICH_RESULTS_ENDPOINT}?key={API_KEY}",
275
+ json=payload,
276
+ timeout=60,
277
+ )
278
+ response.raise_for_status()
279
+ data = response.json()
280
+ except requests.RequestException as e:
281
+ return {"error": f"URL Testing API request failed: {e}"}
282
+
283
+ result = {
284
+ "url": url,
285
+ "mobile_friendly": data.get("mobileFriendliness") == "MOBILE_FRIENDLY",
286
+ "issues": [],
287
+ }
288
+
289
+ for issue in data.get("mobileFriendlyIssues", []):
290
+ result["issues"].append({
291
+ "rule": issue.get("rule", ""),
292
+ })
293
+
294
+ return result
295
+
296
+
297
+ # ── Unified Runner ─────────────────────────────────────────────────
298
+
299
+ def run_all(url: str) -> dict:
300
+ """Run all available API checks and merge results."""
301
+ print(f"Running PageSpeed Insights (mobile)...", file=sys.stderr)
302
+ psi_mobile = run_pagespeed(url, strategy="mobile")
303
+
304
+ print(f"Running PageSpeed Insights (desktop)...", file=sys.stderr)
305
+ psi_desktop = run_pagespeed(url, strategy="desktop")
306
+
307
+ print(f"Running CrUX API...", file=sys.stderr)
308
+ crux = run_crux(url)
309
+
310
+ print(f"Running Mobile-Friendly Test...", file=sys.stderr)
311
+ rich = run_rich_results_test(url)
312
+
313
+ return {
314
+ "url": url,
315
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
316
+ "pagespeed": {
317
+ "mobile": psi_mobile,
318
+ "desktop": psi_desktop,
319
+ },
320
+ "crux": crux,
321
+ "mobile_friendly": rich,
322
+ }
323
+
324
+
325
+ # ── CLI ────────────────────────────────────────────────────────────
326
+
327
+ def print_psi_summary(result: dict, label: str):
328
+ """Print a human-readable PSI summary."""
329
+ if result.get("error"):
330
+ print(f" Error: {result['error']}")
331
+ return
332
+
333
+ scores = result.get("scores", {})
334
+ print(f"\n {label} Scores:")
335
+ for cat, score in scores.items():
336
+ icon = "🟢" if score >= 90 else "🟡" if score >= 50 else "🔴"
337
+ cat_name = cat.replace("_", " ").replace("-", " ").title()
338
+ print(f" {icon} {cat_name}: {score}/100")
339
+
340
+ cwv = result.get("cwv", {})
341
+ if cwv:
342
+ print(f"\n Core Web Vitals:")
343
+ for metric, data in cwv.items():
344
+ icon = "🟢" if data["score"] >= 90 else "🟡" if data["score"] >= 50 else "🔴"
345
+ print(f" {icon} {metric}: {data['display']} ({data['score']}/100)")
346
+
347
+ failures = result.get("failing_audits", [])[:5]
348
+ if failures:
349
+ print(f"\n Top Failing Audits:")
350
+ for audit in failures:
351
+ icon = "🔴" if audit["severity"] == "critical" else "🟠"
352
+ print(f" {icon} {audit['title']} ({audit['score']}/100)")
353
+
354
+ opps = result.get("opportunities", [])[:3]
355
+ if opps:
356
+ print(f"\n Top Opportunities:")
357
+ for opp in opps:
358
+ print(f" 💡 {opp['title']} (save {opp['savings_ms']}ms)")
359
+
360
+
361
+ def main():
362
+ parser = argparse.ArgumentParser(
363
+ description="SEO APIs — Google free API client (BMAD+ SEO Engine)"
364
+ )
365
+ parser.add_argument("url", nargs="?", help="URL to analyze")
366
+ parser.add_argument("--pagespeed", action="store_true", help="Run PageSpeed Insights")
367
+ parser.add_argument("--crux", action="store_true", help="Run CrUX API")
368
+ parser.add_argument("--richtest", action="store_true", help="Run Rich Results Test")
369
+ parser.add_argument("--all", action="store_true", help="Run all APIs")
370
+ parser.add_argument("--strategy", choices=["mobile", "desktop"], default="mobile",
371
+ help="PSI strategy (default: mobile)")
372
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
373
+
374
+ args = parser.parse_args()
375
+
376
+ if not args.url:
377
+ parser.print_help()
378
+ sys.exit(1)
379
+
380
+ if not API_KEY:
381
+ print("⚠️ GOOGLE_API_KEY not set!", file=sys.stderr)
382
+ print(" Get a free key: https://console.cloud.google.com/apis/credentials", file=sys.stderr)
383
+ print(" Enable: PageSpeed Insights API + Chrome UX Report API", file=sys.stderr)
384
+ print(" Set: export GOOGLE_API_KEY=your_key", file=sys.stderr)
385
+ sys.exit(1)
386
+
387
+ if args.all:
388
+ result = run_all(args.url)
389
+ if args.json:
390
+ print(json.dumps(result, indent=2, ensure_ascii=False))
391
+ else:
392
+ print(f"\n{'='*60}")
393
+ print(f"SEO API Report: {args.url}")
394
+ print(f"{'='*60}")
395
+ print_psi_summary(result["pagespeed"]["mobile"], "📱 Mobile")
396
+ print_psi_summary(result["pagespeed"]["desktop"], "🖥️ Desktop")
397
+
398
+ crux = result["crux"]
399
+ if not crux.get("error"):
400
+ print(f"\n 📊 CrUX Field Data:")
401
+ for metric, data in crux.get("metrics", {}).items():
402
+ print(f" {metric}: p75 = {data['p75']}")
403
+ else:
404
+ print(f"\n 📊 CrUX: {crux['error']}")
405
+
406
+ mf = result["mobile_friendly"]
407
+ if not mf.get("error"):
408
+ icon = "✅" if mf["mobile_friendly"] else "❌"
409
+ print(f"\n 📱 Mobile-Friendly: {icon}")
410
+ else:
411
+ print(f"\n 📱 Mobile-Friendly: {mf['error']}")
412
+
413
+ elif args.pagespeed:
414
+ result = run_pagespeed(args.url, strategy=args.strategy)
415
+ if args.json:
416
+ print(json.dumps(result, indent=2, ensure_ascii=False))
417
+ else:
418
+ print_psi_summary(result, f"{'📱 Mobile' if args.strategy == 'mobile' else '🖥️ Desktop'}")
419
+
420
+ elif args.crux:
421
+ result = run_crux(args.url)
422
+ if args.json:
423
+ print(json.dumps(result, indent=2, ensure_ascii=False))
424
+ else:
425
+ if result.get("error"):
426
+ print(f"Error: {result['error']}")
427
+ else:
428
+ print(f"\nCrUX Field Data: {args.url}")
429
+ for metric, data in result.get("metrics", {}).items():
430
+ print(f" {metric}: p75 = {data['p75']}")
431
+
432
+ elif args.richtest:
433
+ result = run_rich_results_test(args.url)
434
+ if args.json:
435
+ print(json.dumps(result, indent=2, ensure_ascii=False))
436
+ else:
437
+ if result.get("error"):
438
+ print(f"Error: {result['error']}")
439
+ else:
440
+ icon = "✅" if result["mobile_friendly"] else "❌"
441
+ print(f"Mobile-Friendly: {icon}")
442
+ if result["issues"]:
443
+ for issue in result["issues"]:
444
+ print(f" ⚠️ {issue['rule']}")
445
+
446
+ else:
447
+ # Default: run pagespeed
448
+ result = run_pagespeed(args.url, strategy=args.strategy)
449
+ if args.json:
450
+ print(json.dumps(result, indent=2, ensure_ascii=False))
451
+ else:
452
+ print_psi_summary(result, f"{'📱 Mobile' if args.strategy == 'mobile' else '🖥️ Desktop'}")
453
+
454
+
455
+ if __name__ == "__main__":
456
+ main()