antigravity-seo-kit 2.0.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.

Potentially problematic release.


This version of antigravity-seo-kit might be problematic. Click here for more details.

Files changed (135) hide show
  1. package/.agent/agent.md +96 -0
  2. package/.agent/skills/seo/SKILL.md +153 -0
  3. package/.agent/skills/seo/references/cwv-thresholds.md +108 -0
  4. package/.agent/skills/seo/references/eeat-framework.md +214 -0
  5. package/.agent/skills/seo/references/local-schema-types.md +230 -0
  6. package/.agent/skills/seo/references/local-seo-signals.md +218 -0
  7. package/.agent/skills/seo/references/maps-api-endpoints.md +160 -0
  8. package/.agent/skills/seo/references/maps-free-apis.md +176 -0
  9. package/.agent/skills/seo/references/maps-gbp-checklist.md +150 -0
  10. package/.agent/skills/seo/references/maps-geo-grid.md +154 -0
  11. package/.agent/skills/seo/references/quality-gates.md +155 -0
  12. package/.agent/skills/seo/references/schema-types.md +118 -0
  13. package/.agent/skills/seo/schema/templates.json +213 -0
  14. package/.agent/skills/seo/scripts/analyze_visual.py +217 -0
  15. package/.agent/skills/seo/scripts/capture_screenshot.py +181 -0
  16. package/.agent/skills/seo/scripts/fetch_page.py +196 -0
  17. package/.agent/skills/seo/scripts/parse_html.py +201 -0
  18. package/.agent/skills/seo-audit/SKILL.md +278 -0
  19. package/.agent/skills/seo-competitor-pages/SKILL.md +212 -0
  20. package/.agent/skills/seo-content/SKILL.md +230 -0
  21. package/.agent/skills/seo-dataforseo/SKILL.md +418 -0
  22. package/.agent/skills/seo-geo/SKILL.md +305 -0
  23. package/.agent/skills/seo-google/SKILL.md +405 -0
  24. package/.agent/skills/seo-google/assets/templates/cwv-audit-report.md +48 -0
  25. package/.agent/skills/seo-google/assets/templates/gsc-performance-report.md +44 -0
  26. package/.agent/skills/seo-google/assets/templates/indexation-status-report.md +43 -0
  27. package/.agent/skills/seo-google/references/auth-setup.md +154 -0
  28. package/.agent/skills/seo-google/references/ga4-data-api.md +184 -0
  29. package/.agent/skills/seo-google/references/indexing-api.md +107 -0
  30. package/.agent/skills/seo-google/references/keyword-planner-api.md +66 -0
  31. package/.agent/skills/seo-google/references/nlp-api.md +55 -0
  32. package/.agent/skills/seo-google/references/pagespeed-crux-api.md +204 -0
  33. package/.agent/skills/seo-google/references/rate-limits-quotas.md +75 -0
  34. package/.agent/skills/seo-google/references/search-console-api.md +156 -0
  35. package/.agent/skills/seo-google/references/supplementary-apis.md +99 -0
  36. package/.agent/skills/seo-google/references/youtube-api.md +49 -0
  37. package/.agent/skills/seo-google/scripts/crux_history.py +321 -0
  38. package/.agent/skills/seo-google/scripts/ga4_report.py +478 -0
  39. package/.agent/skills/seo-google/scripts/google_auth.py +795 -0
  40. package/.agent/skills/seo-google/scripts/google_report.py +2273 -0
  41. package/.agent/skills/seo-google/scripts/gsc_inspect.py +340 -0
  42. package/.agent/skills/seo-google/scripts/gsc_query.py +378 -0
  43. package/.agent/skills/seo-google/scripts/indexing_notify.py +313 -0
  44. package/.agent/skills/seo-google/scripts/keyword_planner.py +297 -0
  45. package/.agent/skills/seo-google/scripts/nlp_analyze.py +309 -0
  46. package/.agent/skills/seo-google/scripts/pagespeed_check.py +649 -0
  47. package/.agent/skills/seo-google/scripts/youtube_search.py +355 -0
  48. package/.agent/skills/seo-hreflang/SKILL.md +192 -0
  49. package/.agent/skills/seo-image-gen/SKILL.md +211 -0
  50. package/.agent/skills/seo-image-gen/references/cost-tracking.md +47 -0
  51. package/.agent/skills/seo-image-gen/references/gemini-models.md +200 -0
  52. package/.agent/skills/seo-image-gen/references/mcp-tools.md +115 -0
  53. package/.agent/skills/seo-image-gen/references/post-processing.md +192 -0
  54. package/.agent/skills/seo-image-gen/references/presets.md +69 -0
  55. package/.agent/skills/seo-image-gen/references/prompt-engineering.md +411 -0
  56. package/.agent/skills/seo-image-gen/references/seo-image-presets.md +137 -0
  57. package/.agent/skills/seo-image-gen/scripts/batch.py +97 -0
  58. package/.agent/skills/seo-image-gen/scripts/cost_tracker.py +191 -0
  59. package/.agent/skills/seo-image-gen/scripts/edit.py +141 -0
  60. package/.agent/skills/seo-image-gen/scripts/generate.py +149 -0
  61. package/.agent/skills/seo-image-gen/scripts/presets.py +153 -0
  62. package/.agent/skills/seo-image-gen/scripts/setup_mcp.py +151 -0
  63. package/.agent/skills/seo-image-gen/scripts/validate_setup.py +133 -0
  64. package/.agent/skills/seo-images/SKILL.md +176 -0
  65. package/.agent/skills/seo-local/SKILL.md +381 -0
  66. package/.agent/skills/seo-maps/SKILL.md +328 -0
  67. package/.agent/skills/seo-page/SKILL.md +86 -0
  68. package/.agent/skills/seo-plan/SKILL.md +118 -0
  69. package/.agent/skills/seo-plan/assets/agency.md +175 -0
  70. package/.agent/skills/seo-plan/assets/ecommerce.md +167 -0
  71. package/.agent/skills/seo-plan/assets/generic.md +144 -0
  72. package/.agent/skills/seo-plan/assets/local-service.md +160 -0
  73. package/.agent/skills/seo-plan/assets/publisher.md +153 -0
  74. package/.agent/skills/seo-plan/assets/saas.md +135 -0
  75. package/.agent/skills/seo-programmatic/SKILL.md +171 -0
  76. package/.agent/skills/seo-schema/SKILL.md +223 -0
  77. package/.agent/skills/seo-sitemap/SKILL.md +180 -0
  78. package/.agent/skills/seo-technical/SKILL.md +211 -0
  79. package/.agent/workflows/seo-audit.md +17 -0
  80. package/.agent/workflows/seo-competitor-pages.md +12 -0
  81. package/.agent/workflows/seo-content.md +14 -0
  82. package/.agent/workflows/seo-geo.md +12 -0
  83. package/.agent/workflows/seo-google.md +12 -0
  84. package/.agent/workflows/seo-hreflang.md +12 -0
  85. package/.agent/workflows/seo-images.md +13 -0
  86. package/.agent/workflows/seo-local.md +12 -0
  87. package/.agent/workflows/seo-maps.md +11 -0
  88. package/.agent/workflows/seo-page.md +13 -0
  89. package/.agent/workflows/seo-plan.md +13 -0
  90. package/.agent/workflows/seo-programmatic.md +12 -0
  91. package/.agent/workflows/seo-schema.md +11 -0
  92. package/.agent/workflows/seo-sitemap.md +9 -0
  93. package/.agent/workflows/seo-technical.md +18 -0
  94. package/LICENSE +88 -0
  95. package/README.md +122 -0
  96. package/bin/cli.js +117 -0
  97. package/docs/ARCHITECTURE.md +218 -0
  98. package/docs/COMMANDS.md +184 -0
  99. package/docs/INSTALLATION.md +100 -0
  100. package/docs/MCP-INTEGRATION.md +153 -0
  101. package/docs/TROUBLESHOOTING.md +151 -0
  102. package/docs/superpowers/plans/2026-03-13-github-audit-fixes.md +511 -0
  103. package/extensions/banana/README.md +95 -0
  104. package/extensions/banana/docs/BANANA-SETUP.md +86 -0
  105. package/extensions/banana/install.sh +170 -0
  106. package/extensions/banana/references/cost-tracking.md +47 -0
  107. package/extensions/banana/references/gemini-models.md +200 -0
  108. package/extensions/banana/references/mcp-tools.md +115 -0
  109. package/extensions/banana/references/post-processing.md +192 -0
  110. package/extensions/banana/references/presets.md +69 -0
  111. package/extensions/banana/references/prompt-engineering.md +411 -0
  112. package/extensions/banana/references/seo-image-presets.md +137 -0
  113. package/extensions/banana/scripts/batch.py +97 -0
  114. package/extensions/banana/scripts/cost_tracker.py +191 -0
  115. package/extensions/banana/scripts/edit.py +141 -0
  116. package/extensions/banana/scripts/generate.py +149 -0
  117. package/extensions/banana/scripts/presets.py +153 -0
  118. package/extensions/banana/scripts/setup_mcp.py +151 -0
  119. package/extensions/banana/scripts/validate_setup.py +133 -0
  120. package/extensions/banana/uninstall.sh +43 -0
  121. package/extensions/dataforseo/README.md +169 -0
  122. package/extensions/dataforseo/docs/DATAFORSEO-SETUP.md +74 -0
  123. package/extensions/dataforseo/field-config.json +280 -0
  124. package/extensions/dataforseo/install.ps1 +110 -0
  125. package/extensions/dataforseo/install.sh +161 -0
  126. package/extensions/dataforseo/uninstall.ps1 +35 -0
  127. package/extensions/dataforseo/uninstall.sh +39 -0
  128. package/lib/api.js +190 -0
  129. package/lib/fingerprint.js +68 -0
  130. package/lib/installer.js +486 -0
  131. package/lib/utils.js +254 -0
  132. package/package.json +40 -0
  133. package/pyproject.toml +11 -0
  134. package/requirements-google.txt +15 -0
  135. package/requirements.txt +11 -0
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Google Ads API - Keyword Planner for SEO keyword research.
4
+
5
+ Gold-standard source for keyword search volume, CPC, and competition data.
6
+ Requires a Google Ads Manager account with a developer token.
7
+
8
+ Usage:
9
+ python keyword_planner.py ideas "seo tools" --json
10
+ python keyword_planner.py volume "seo tools,seo audit,seo checker" --json
11
+ python keyword_planner.py forecast "seo tools" --json
12
+
13
+ Prerequisites:
14
+ - Google Ads Manager account (can be free)
15
+ - Developer Token (apply at Google Ads API Center)
16
+ - OAuth credentials or service account
17
+ - google-ads Python library: pip install google-ads
18
+ - Config: ~/.config/claude-seo/google-api.json with:
19
+ {
20
+ "ads_developer_token": "YOUR_DEV_TOKEN",
21
+ "ads_customer_id": "123-456-7890",
22
+ "ads_login_customer_id": "123-456-7890"
23
+ }
24
+
25
+ Note: Accounts without active ad spend receive bucketed volume ranges
26
+ (e.g., "1K-10K") instead of exact numbers.
27
+ """
28
+
29
+ import argparse
30
+ import json
31
+ import sys
32
+ from typing import Optional
33
+
34
+ try:
35
+ from google.ads.googleads.client import GoogleAdsClient
36
+ from google.ads.googleads.errors import GoogleAdsException
37
+ HAS_GOOGLE_ADS = True
38
+ except ImportError:
39
+ HAS_GOOGLE_ADS = False
40
+
41
+ try:
42
+ from google_auth import load_config
43
+ except ImportError:
44
+ import os
45
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
46
+ from google_auth import load_config
47
+
48
+
49
+ def _build_ads_client() -> Optional[object]:
50
+ """Build Google Ads client from config."""
51
+ if not HAS_GOOGLE_ADS:
52
+ print(
53
+ "Error: google-ads library required. Install with: pip install google-ads",
54
+ file=sys.stderr,
55
+ )
56
+ return None
57
+
58
+ config = load_config()
59
+ dev_token = config.get("ads_developer_token")
60
+ customer_id = config.get("ads_customer_id", "").replace("-", "")
61
+ login_customer_id = config.get("ads_login_customer_id", "").replace("-", "")
62
+ oauth_client_path = config.get("oauth_client_path")
63
+
64
+ if not dev_token:
65
+ print(
66
+ "Error: No Google Ads developer token configured. "
67
+ "Add 'ads_developer_token' to ~/.config/claude-seo/google-api.json. "
68
+ "Get a token at: https://ads.google.com/aw/apicenter",
69
+ file=sys.stderr,
70
+ )
71
+ return None
72
+
73
+ if not customer_id:
74
+ print(
75
+ "Error: No Google Ads customer ID configured. "
76
+ "Add 'ads_customer_id' (format: 123-456-7890) to config.",
77
+ file=sys.stderr,
78
+ )
79
+ return None
80
+
81
+ try:
82
+ # Build from dict configuration
83
+ ads_config = {
84
+ "developer_token": dev_token,
85
+ "use_proto_plus": True,
86
+ }
87
+ if login_customer_id:
88
+ ads_config["login_customer_id"] = login_customer_id
89
+
90
+ # Try to use OAuth token if available
91
+ token_path = os.path.expanduser("~/.config/claude-seo/oauth-token.json")
92
+ if os.path.exists(token_path):
93
+ with open(token_path) as f:
94
+ token_data = json.load(f)
95
+ if oauth_client_path:
96
+ with open(os.path.expanduser(oauth_client_path)) as f:
97
+ client_data = json.load(f)
98
+ client_info = client_data.get("web", client_data.get("installed", {}))
99
+ ads_config["client_id"] = client_info.get("client_id")
100
+ ads_config["client_secret"] = client_info.get("client_secret")
101
+ ads_config["refresh_token"] = token_data.get("refresh_token")
102
+
103
+ client = GoogleAdsClient.load_from_dict(ads_config)
104
+ return client, customer_id
105
+
106
+ except Exception as e:
107
+ print(f"Error building Google Ads client: {e}", file=sys.stderr)
108
+ return None
109
+
110
+
111
+ def generate_keyword_ideas(
112
+ seed_keywords: list,
113
+ language_id: str = "1000",
114
+ location_id: str = "2840",
115
+ limit: int = 50,
116
+ ) -> dict:
117
+ """
118
+ Generate keyword ideas from seed keywords.
119
+
120
+ Args:
121
+ seed_keywords: List of seed keyword strings.
122
+ language_id: Language ID (1000 = English).
123
+ location_id: Location ID (2840 = United States).
124
+ limit: Max results.
125
+
126
+ Returns:
127
+ Dictionary with keyword ideas and metrics.
128
+ """
129
+ result = {
130
+ "seed_keywords": seed_keywords,
131
+ "ideas": [],
132
+ "error": None,
133
+ }
134
+
135
+ client_data = _build_ads_client()
136
+ if not client_data:
137
+ result["error"] = "Could not build Google Ads client. Check config."
138
+ return result
139
+
140
+ client, customer_id = client_data
141
+
142
+ try:
143
+ kp_service = client.get_service("KeywordPlanIdeaService")
144
+ request = client.get_type("GenerateKeywordIdeasRequest")
145
+ request.customer_id = customer_id
146
+ request.language = f"languageConstants/{language_id}"
147
+ request.geo_target_constants.append(f"geoTargetConstants/{location_id}")
148
+ request.keyword_plan_network = client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
149
+ request.keyword_seed.keywords.extend(seed_keywords)
150
+
151
+ response = kp_service.generate_keyword_ideas(request=request)
152
+
153
+ for idea in response.results:
154
+ metrics = idea.keyword_idea_metrics
155
+ monthly_volumes = []
156
+ for mv in metrics.monthly_search_volumes:
157
+ monthly_volumes.append({
158
+ "year": mv.year,
159
+ "month": mv.month,
160
+ "volume": mv.monthly_searches,
161
+ })
162
+
163
+ result["ideas"].append({
164
+ "keyword": idea.text,
165
+ "avg_monthly_searches": metrics.avg_monthly_searches,
166
+ "competition": metrics.competition.name if metrics.competition else "UNSPECIFIED",
167
+ "competition_index": metrics.competition_index,
168
+ "low_top_of_page_bid": metrics.low_top_of_page_bid_micros / 1_000_000 if metrics.low_top_of_page_bid_micros else None,
169
+ "high_top_of_page_bid": metrics.high_top_of_page_bid_micros / 1_000_000 if metrics.high_top_of_page_bid_micros else None,
170
+ "monthly_volumes": monthly_volumes[-12:] if monthly_volumes else [],
171
+ })
172
+
173
+ if len(result["ideas"]) >= limit:
174
+ break
175
+
176
+ # Sort by volume descending
177
+ result["ideas"].sort(key=lambda k: k.get("avg_monthly_searches", 0) or 0, reverse=True)
178
+
179
+ except GoogleAdsException as e:
180
+ errors = [err.message for err in e.failure.errors]
181
+ result["error"] = f"Google Ads API error: {'; '.join(errors)}"
182
+ except Exception as e:
183
+ result["error"] = f"Keyword Planner error: {e}"
184
+
185
+ return result
186
+
187
+
188
+ def get_keyword_volumes(
189
+ keywords: list,
190
+ language_id: str = "1000",
191
+ location_id: str = "2840",
192
+ ) -> dict:
193
+ """
194
+ Get search volume for specific keywords.
195
+
196
+ Args:
197
+ keywords: List of keywords to check.
198
+ language_id: Language ID.
199
+ location_id: Location ID.
200
+
201
+ Returns:
202
+ Dictionary with keyword metrics.
203
+ """
204
+ result = {
205
+ "keywords": [],
206
+ "error": None,
207
+ }
208
+
209
+ client_data = _build_ads_client()
210
+ if not client_data:
211
+ result["error"] = "Could not build Google Ads client."
212
+ return result
213
+
214
+ client, customer_id = client_data
215
+
216
+ try:
217
+ kp_service = client.get_service("KeywordPlanIdeaService")
218
+ request = client.get_type("GenerateKeywordHistoricalMetricsRequest")
219
+ request.customer_id = customer_id
220
+ request.keywords.extend(keywords)
221
+ request.language = f"languageConstants/{language_id}"
222
+ request.geo_target_constants.append(f"geoTargetConstants/{location_id}")
223
+ request.keyword_plan_network = client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
224
+
225
+ response = kp_service.generate_keyword_historical_metrics(request=request)
226
+
227
+ for kw_result in response.results:
228
+ metrics = kw_result.keyword_metrics
229
+ result["keywords"].append({
230
+ "keyword": kw_result.text,
231
+ "avg_monthly_searches": metrics.avg_monthly_searches,
232
+ "competition": metrics.competition.name if metrics.competition else "UNSPECIFIED",
233
+ "competition_index": metrics.competition_index,
234
+ "low_top_of_page_bid": metrics.low_top_of_page_bid_micros / 1_000_000 if metrics.low_top_of_page_bid_micros else None,
235
+ "high_top_of_page_bid": metrics.high_top_of_page_bid_micros / 1_000_000 if metrics.high_top_of_page_bid_micros else None,
236
+ })
237
+
238
+ except GoogleAdsException as e:
239
+ errors = [err.message for err in e.failure.errors]
240
+ result["error"] = f"Google Ads API error: {'; '.join(errors)}"
241
+ except Exception as e:
242
+ result["error"] = f"Keyword volume error: {e}"
243
+
244
+ return result
245
+
246
+
247
+ def main():
248
+ parser = argparse.ArgumentParser(
249
+ description="Google Ads Keyword Planner - SEO keyword research"
250
+ )
251
+ parser.add_argument(
252
+ "command",
253
+ choices=["ideas", "volume"],
254
+ help="Command: ideas (keyword suggestions), volume (search volume lookup)",
255
+ )
256
+ parser.add_argument("keywords", help="Seed keyword(s), comma-separated for volume")
257
+ parser.add_argument("--limit", type=int, default=50, help="Max results for ideas (default: 50)")
258
+ parser.add_argument("--language", default="1000", help="Language ID (default: 1000 = English)")
259
+ parser.add_argument("--location", default="2840", help="Location ID (default: 2840 = US)")
260
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
261
+
262
+ args = parser.parse_args()
263
+
264
+ if args.command == "ideas":
265
+ seeds = [k.strip() for k in args.keywords.split(",")]
266
+ result = generate_keyword_ideas(seeds, language_id=args.language, location_id=args.location, limit=args.limit)
267
+ elif args.command == "volume":
268
+ kws = [k.strip() for k in args.keywords.split(",")]
269
+ result = get_keyword_volumes(kws, language_id=args.language, location_id=args.location)
270
+
271
+ if result.get("error"):
272
+ print(f"Error: {result['error']}", file=sys.stderr)
273
+ if not args.json:
274
+ sys.exit(1)
275
+
276
+ if args.json:
277
+ print(json.dumps(result, indent=2, default=str))
278
+ else:
279
+ if args.command == "ideas":
280
+ print(f"=== Keyword Ideas ===")
281
+ for i, idea in enumerate(result.get("ideas", [])[:20], 1):
282
+ vol = idea.get("avg_monthly_searches", "?")
283
+ comp = idea.get("competition", "?")
284
+ bid_low = idea.get("low_top_of_page_bid")
285
+ bid_high = idea.get("high_top_of_page_bid")
286
+ bid_str = f"${bid_low:.2f}-${bid_high:.2f}" if bid_low and bid_high else "N/A"
287
+ print(f" {i:2d}. {idea['keyword']:40s} | Vol: {vol:>8} | Comp: {comp:8s} | CPC: {bid_str}")
288
+ elif args.command == "volume":
289
+ print(f"=== Keyword Volumes ===")
290
+ for kw in result.get("keywords", []):
291
+ vol = kw.get("avg_monthly_searches", "?")
292
+ comp = kw.get("competition", "?")
293
+ print(f" {kw['keyword']:40s} | Vol: {vol:>8} | Comp: {comp}")
294
+
295
+
296
+ if __name__ == "__main__":
297
+ main()
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Google Cloud Natural Language API - Entity, sentiment, and content analysis.
4
+
5
+ Enhances E-E-A-T scoring with NLP entity coverage, sentiment analysis,
6
+ and Google's own content classification taxonomy.
7
+
8
+ Usage:
9
+ python nlp_analyze.py --text "Your content here" --json
10
+ python nlp_analyze.py --url https://example.com --json
11
+ python nlp_analyze.py --text "Your content" --features entities,sentiment,classify
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from typing import Optional
18
+
19
+ try:
20
+ import requests
21
+ except ImportError:
22
+ print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ try:
26
+ from google_auth import get_api_key, validate_url
27
+ except ImportError:
28
+ import os
29
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
30
+ from google_auth import get_api_key, validate_url
31
+
32
+ NLP_ENDPOINT = "https://language.googleapis.com/v2/documents:annotateText"
33
+
34
+ # Free tier: 5,000 units/month per feature
35
+ # Paid: $0.001 per 1,000-character unit for entity/sentiment
36
+ FEATURES = {
37
+ "entities": "extractEntities",
38
+ "sentiment": "extractDocumentSentiment",
39
+ "classify": "classifyText",
40
+ "categories": "classifyText",
41
+ "moderate": "moderateText",
42
+ }
43
+
44
+
45
+ def analyze_text(
46
+ text: str,
47
+ features: Optional[list] = None,
48
+ api_key: Optional[str] = None,
49
+ language: str = "en",
50
+ ) -> dict:
51
+ """
52
+ Analyze text using Google Cloud Natural Language API.
53
+
54
+ Args:
55
+ text: Text content to analyze (max 1M characters).
56
+ features: List of features: entities, sentiment, classify, moderate.
57
+ api_key: Google API key.
58
+ language: Language code (default: en).
59
+
60
+ Returns:
61
+ Dictionary with entities, sentiment, categories, and moderation results.
62
+ """
63
+ result = {
64
+ "text_length": len(text),
65
+ "language": language,
66
+ "entities": [],
67
+ "sentiment": None,
68
+ "categories": [],
69
+ "moderation": [],
70
+ "error": None,
71
+ }
72
+
73
+ key = api_key or get_api_key()
74
+ if not key:
75
+ result["error"] = "No API key. Set GOOGLE_API_KEY or add 'api_key' to config."
76
+ return result
77
+
78
+ if features is None:
79
+ features = ["entities", "sentiment", "classify"]
80
+
81
+ # Build request
82
+ feature_map = {}
83
+ for f in features:
84
+ api_feature = FEATURES.get(f)
85
+ if api_feature:
86
+ feature_map[api_feature] = True
87
+
88
+ body = {
89
+ "document": {
90
+ "type": "PLAIN_TEXT",
91
+ "content": text[:100000], # API limit
92
+ "languageCode": language,
93
+ },
94
+ "features": feature_map,
95
+ "encodingType": "UTF8",
96
+ }
97
+
98
+ try:
99
+ resp = requests.post(
100
+ f"{NLP_ENDPOINT}?key={key}",
101
+ json=body,
102
+ timeout=30,
103
+ )
104
+
105
+ if resp.status_code == 403:
106
+ result["error"] = (
107
+ "Cloud Natural Language API access denied. Enable it in "
108
+ "GCP Console: APIs & Services > Library > Cloud Natural Language API. "
109
+ "Billing must be enabled on the project."
110
+ )
111
+ return result
112
+
113
+ if resp.status_code == 429:
114
+ result["error"] = "NLP API quota exceeded. Free tier: 5,000 units/month."
115
+ return result
116
+
117
+ resp.raise_for_status()
118
+ data = resp.json()
119
+ except requests.exceptions.RequestException as e:
120
+ result["error"] = f"NLP API request failed: {e}"
121
+ return result
122
+
123
+ # Entities
124
+ for entity in data.get("entities", []):
125
+ mentions = entity.get("mentions", [])
126
+ result["entities"].append({
127
+ "name": entity.get("name", ""),
128
+ "type": entity.get("type", "UNKNOWN"),
129
+ "salience": round(entity.get("salience", 0), 4),
130
+ "sentiment_score": entity.get("sentiment", {}).get("score"),
131
+ "sentiment_magnitude": entity.get("sentiment", {}).get("magnitude"),
132
+ "mention_count": len(mentions),
133
+ "metadata": entity.get("metadata", {}),
134
+ })
135
+
136
+ # Sort by salience (most important first)
137
+ result["entities"].sort(key=lambda e: e["salience"], reverse=True)
138
+
139
+ # Document sentiment
140
+ doc_sentiment = data.get("documentSentiment", {})
141
+ if doc_sentiment:
142
+ score = doc_sentiment.get("score", 0)
143
+ magnitude = doc_sentiment.get("magnitude", 0)
144
+ if score > 0.25:
145
+ tone = "positive"
146
+ elif score < -0.25:
147
+ tone = "negative"
148
+ else:
149
+ tone = "neutral"
150
+
151
+ result["sentiment"] = {
152
+ "score": round(score, 3),
153
+ "magnitude": round(magnitude, 3),
154
+ "tone": tone,
155
+ "interpretation": (
156
+ f"{'Positive' if score > 0 else 'Negative' if score < 0 else 'Neutral'} "
157
+ f"(score: {score:.2f}) with "
158
+ f"{'high' if magnitude > 2 else 'moderate' if magnitude > 0.5 else 'low'} "
159
+ f"emotional content (magnitude: {magnitude:.2f})"
160
+ ),
161
+ }
162
+
163
+ # Sentence-level sentiment
164
+ sentences = data.get("sentences", [])
165
+ if sentences:
166
+ result["sentiment"]["sentence_count"] = len(sentences)
167
+ sent_scores = [s.get("sentiment", {}).get("score", 0) for s in sentences]
168
+ result["sentiment"]["most_positive"] = max(sent_scores) if sent_scores else 0
169
+ result["sentiment"]["most_negative"] = min(sent_scores) if sent_scores else 0
170
+
171
+ # Categories (content classification)
172
+ for cat in data.get("categories", []):
173
+ result["categories"].append({
174
+ "name": cat.get("name", ""),
175
+ "confidence": round(cat.get("confidence", 0), 4),
176
+ })
177
+
178
+ # Moderation categories
179
+ for mod in data.get("moderationCategories", []):
180
+ if mod.get("confidence", 0) > 0.5:
181
+ result["moderation"].append({
182
+ "name": mod.get("name", ""),
183
+ "confidence": round(mod.get("confidence", 0), 4),
184
+ })
185
+
186
+ return result
187
+
188
+
189
+ def analyze_url(
190
+ url: str,
191
+ features: Optional[list] = None,
192
+ api_key: Optional[str] = None,
193
+ ) -> dict:
194
+ """
195
+ Fetch a URL's text content and analyze it.
196
+
197
+ Args:
198
+ url: URL to fetch and analyze.
199
+ features: NLP features to extract.
200
+ api_key: API key override.
201
+
202
+ Returns:
203
+ Dictionary with NLP analysis results.
204
+ """
205
+ if not validate_url(url):
206
+ return {"error": "Invalid URL. Only http/https URLs to public hosts are accepted."}
207
+
208
+ # Fetch the page text
209
+ try:
210
+ resp = requests.get(url, timeout=30, headers={
211
+ "User-Agent": "Mozilla/5.0 (compatible; ClaudeSEO/1.7 NLP Analyzer)"
212
+ })
213
+ resp.raise_for_status()
214
+ html = resp.text
215
+ except requests.exceptions.RequestException as e:
216
+ return {"error": f"Could not fetch URL: {e}"}
217
+
218
+ # Extract text from HTML (simple approach)
219
+ try:
220
+ from bs4 import BeautifulSoup
221
+ soup = BeautifulSoup(html, "html.parser")
222
+ # Remove script and style
223
+ for tag in soup(["script", "style", "nav", "footer", "header"]):
224
+ tag.decompose()
225
+ text = soup.get_text(separator=" ", strip=True)
226
+ except ImportError:
227
+ # Fallback: regex-based text extraction
228
+ import re
229
+ text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
230
+ text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.IGNORECASE)
231
+ text = re.sub(r"<[^>]+>", " ", text)
232
+ text = re.sub(r"\s+", " ", text).strip()
233
+
234
+ if not text or len(text) < 50:
235
+ return {"error": "Extracted text too short for meaningful NLP analysis."}
236
+
237
+ result = analyze_text(text, features=features, api_key=api_key)
238
+ result["source_url"] = url
239
+ result["extracted_text_length"] = len(text)
240
+ return result
241
+
242
+
243
+ def main():
244
+ parser = argparse.ArgumentParser(
245
+ description="Google Cloud Natural Language API - Entity/sentiment/classification for SEO"
246
+ )
247
+ parser.add_argument("--text", "-t", help="Text to analyze")
248
+ parser.add_argument("--url", "-u", help="URL to fetch and analyze")
249
+ parser.add_argument(
250
+ "--features", "-f",
251
+ default="entities,sentiment,classify",
252
+ help="Comma-separated features: entities, sentiment, classify, moderate (default: entities,sentiment,classify)",
253
+ )
254
+ parser.add_argument("--api-key", help="API key override")
255
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
256
+
257
+ args = parser.parse_args()
258
+
259
+ if not args.text and not args.url:
260
+ print("Error: Provide --text or --url to analyze.", file=sys.stderr)
261
+ sys.exit(1)
262
+
263
+ features = [f.strip() for f in args.features.split(",")]
264
+
265
+ if args.url:
266
+ result = analyze_url(args.url, features=features, api_key=args.api_key)
267
+ else:
268
+ result = analyze_text(args.text, features=features, api_key=args.api_key)
269
+
270
+ if result.get("error"):
271
+ print(f"Error: {result['error']}", file=sys.stderr)
272
+ if not args.json:
273
+ sys.exit(1)
274
+
275
+ if args.json:
276
+ print(json.dumps(result, indent=2))
277
+ else:
278
+ if result.get("source_url"):
279
+ print(f"=== NLP Analysis: {result['source_url']} ===")
280
+ print(f"Text extracted: {result.get('extracted_text_length', 0):,} chars")
281
+ else:
282
+ print(f"=== NLP Analysis ({result.get('text_length', 0):,} chars) ===")
283
+
284
+ sent = result.get("sentiment")
285
+ if sent:
286
+ print(f"\nSentiment: {sent['tone'].upper()} (score: {sent['score']}, magnitude: {sent['magnitude']})")
287
+ print(f" {sent['interpretation']}")
288
+
289
+ entities = result.get("entities", [])
290
+ if entities:
291
+ print(f"\nTop Entities ({len(entities)} total):")
292
+ for e in entities[:15]:
293
+ print(f" [{e['type']:12s}] {e['name']} (salience: {e['salience']:.3f})")
294
+
295
+ categories = result.get("categories", [])
296
+ if categories:
297
+ print(f"\nContent Categories:")
298
+ for c in categories:
299
+ print(f" {c['name']} ({c['confidence']:.1%})")
300
+
301
+ moderation = result.get("moderation", [])
302
+ if moderation:
303
+ print(f"\nModeration Flags:")
304
+ for m in moderation:
305
+ print(f" {m['name']} ({m['confidence']:.1%})")
306
+
307
+
308
+ if __name__ == "__main__":
309
+ main()