@adgine/geo-cli 1.2.8

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 (115) hide show
  1. package/README.md +93 -0
  2. package/bin/adgine-geo.js +224 -0
  3. package/package.json +38 -0
  4. package/skills/.env.example +11 -0
  5. package/skills/README.md +247 -0
  6. package/skills/SKILL.md +117 -0
  7. package/skills/VERSION +1 -0
  8. package/skills/adgine-geo-aiagent/SKILL.md +200 -0
  9. package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
  10. package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
  11. package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
  12. package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
  13. package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
  14. package/skills/adgine-geo-analytics/API.md +89 -0
  15. package/skills/adgine-geo-analytics/SKILL.md +191 -0
  16. package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
  17. package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
  18. package/skills/adgine-geo-billing/SKILL.md +118 -0
  19. package/skills/adgine-geo-billing/scripts/_client.py +341 -0
  20. package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
  21. package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
  22. package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
  23. package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
  24. package/skills/adgine-geo-brand/SKILL.md +193 -0
  25. package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
  26. package/skills/adgine-geo-brand/scripts/_client.py +341 -0
  27. package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
  28. package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
  29. package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
  30. package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
  31. package/skills/adgine-geo-citation/SKILL.md +220 -0
  32. package/skills/adgine-geo-citation/scripts/_client.py +341 -0
  33. package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
  34. package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
  35. package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
  36. package/skills/adgine-geo-content/SKILL.md +224 -0
  37. package/skills/adgine-geo-content/WORKFLOW.md +100 -0
  38. package/skills/adgine-geo-content/scripts/_client.py +341 -0
  39. package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
  40. package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
  41. package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
  42. package/skills/adgine-geo-content/scripts/list_content.py +65 -0
  43. package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
  44. package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
  45. package/skills/adgine-geo-dashboard/SKILL.md +181 -0
  46. package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
  47. package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
  48. package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
  49. package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
  50. package/skills/adgine-geo-docs/SKILL.md +60 -0
  51. package/skills/adgine-geo-domains/SKILL.md +223 -0
  52. package/skills/adgine-geo-domains/scripts/_client.py +341 -0
  53. package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
  54. package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
  55. package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
  56. package/skills/adgine-geo-integrations/SKILL.md +188 -0
  57. package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
  58. package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
  59. package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
  60. package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
  61. package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
  62. package/skills/adgine-geo-opportunities/SKILL.md +162 -0
  63. package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
  64. package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
  65. package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
  66. package/skills/adgine-geo-performance/SKILL.md +177 -0
  67. package/skills/adgine-geo-performance/scripts/_client.py +341 -0
  68. package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
  69. package/skills/adgine-geo-projects/SKILL.md +279 -0
  70. package/skills/adgine-geo-projects/scripts/_client.py +341 -0
  71. package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
  72. package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
  73. package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
  74. package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
  75. package/skills/adgine-geo-saas/SKILL.md +144 -0
  76. package/skills/adgine-geo-saas/scripts/_client.py +341 -0
  77. package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
  78. package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
  79. package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
  80. package/skills/adgine-geo-site-audit/README.md +281 -0
  81. package/skills/adgine-geo-site-audit/SKILL.md +449 -0
  82. package/skills/adgine-geo-site-audit/requirements.txt +5 -0
  83. package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
  84. package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
  85. package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
  86. package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
  87. package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
  88. package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
  89. package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
  90. package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
  91. package/skills/adgine-geo-topics/SKILL.md +232 -0
  92. package/skills/adgine-geo-topics/scripts/_client.py +341 -0
  93. package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
  94. package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
  95. package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
  96. package/skills/adgine-geo-visibility/SKILL.md +168 -0
  97. package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
  98. package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
  99. package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
  100. package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
  101. package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
  102. package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
  103. package/skills/adgine-geo-wordpress/SKILL.md +151 -0
  104. package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
  105. package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
  106. package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
  107. package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
  108. package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
  109. package/skills/scripts/check_version.py +290 -0
  110. package/skills/setup.py +248 -0
  111. package/src/config.js +126 -0
  112. package/src/doctor.js +137 -0
  113. package/src/install.js +90 -0
  114. package/src/manifest.js +99 -0
  115. package/src/runner.js +159 -0
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env python3
2
+ """Site-wide AI traffic analytics — overall KPI, page rankings, Sankey, raw logs.
3
+
4
+ Subcommands:
5
+ overview-kpi — site overall KPIs (5 cards + trends):
6
+ citation / index / training / agent / referral
7
+ pages [--page 1] [--limit 20] — top pages by AI references
8
+ pages-detail [--limit 20] — 5-metric table per page (citation/index/training/
9
+ agent/referral) with previous-period delta
10
+ pages-export [--format csv|json] — download pages-detail as CSV/JSON
11
+ platform-flow — Sankey: AI platform → page path
12
+ logs [--limit 50] — raw AI event logs (bot + human, paginated)
13
+
14
+ Common opts: --start --end [--platform]
15
+
16
+ Usage:
17
+ python3 scripts/page_analytics.py overview-kpi
18
+ python3 scripts/page_analytics.py pages-detail --start 2025-11-01
19
+ python3 scripts/page_analytics.py pages-export --format csv > pages.csv
20
+ """
21
+ import sys
22
+ import os
23
+ import argparse
24
+
25
+ sys.path.insert(0, os.path.dirname(__file__))
26
+ from _client import (
27
+ get_api_config, get_project_id,
28
+ api_get,
29
+ extract_data, print_json, truncate,
30
+ pad,
31
+ )
32
+
33
+
34
+ def _fmt_num(n):
35
+ if n is None:
36
+ return "--"
37
+ try:
38
+ f = float(n)
39
+ if f == int(f):
40
+ return f"{int(f):,}"
41
+ return f"{f:,.1f}"
42
+ except (TypeError, ValueError):
43
+ return str(n)
44
+
45
+
46
+ def _fmt_change(c):
47
+ if c is None:
48
+ return "--"
49
+ try:
50
+ f = float(c)
51
+ sign = "+" if f >= 0 else ""
52
+ return f"{sign}{f:.1f}%"
53
+ except (TypeError, ValueError):
54
+ return str(c)
55
+
56
+
57
+ def _date_params(args):
58
+ p = {}
59
+ if args.start:
60
+ p["start_date"] = args.start
61
+ if args.end:
62
+ p["end_date"] = args.end
63
+ if getattr(args, "platform", None):
64
+ p["platform"] = args.platform
65
+ return p or None
66
+
67
+
68
+ def cmd_overview_kpi(args, key, base, pid):
69
+ result = api_get(f"/api/projects/{pid}/ai-agent/overview-kpi",
70
+ key, base, params=_date_params(args))
71
+ data = extract_data(result) or {}
72
+ if args.json:
73
+ print_json(data)
74
+ return
75
+ # This endpoint returns a flat dict:
76
+ # {ai_citations, ai_index, ai_training, ai_agent, human_referrals, ai_human_ratio, ...}
77
+ # Each value is a {current, prev, delta, delta_pct, ...} dict.
78
+
79
+ def _get_kpi(d, k):
80
+ v = d.get(k)
81
+ if isinstance(v, dict):
82
+ cur = v.get("current")
83
+ delta_pct = v.get("delta_pct")
84
+ if delta_pct is not None:
85
+ ch_str = f"+{delta_pct:.1f}%" if delta_pct >= 0 else f"{delta_pct:.1f}%"
86
+ else:
87
+ ch_str = "--"
88
+ else:
89
+ cur, ch_str = v, "--"
90
+ return cur, ch_str
91
+
92
+ print("Site AI Overview — 5 KPI cards")
93
+ print()
94
+ print("```")
95
+ print("┌────────────────────┬──────────────┬──────────────┐")
96
+ print("│ KPI │ Current │ Change │")
97
+ print("├────────────────────┼──────────────┼──────────────┤")
98
+ for k, label in [
99
+ ("ai_citations", "AI Citation"),
100
+ ("ai_index", "AI Index"),
101
+ ("ai_training", "AI Training"),
102
+ ("ai_agent", "AI Agent"),
103
+ ("human_referrals","AI Referral"),
104
+ ("ai_human_ratio", "AI Referral %"),
105
+ ]:
106
+ cur, ch_str = _get_kpi(data, k)
107
+ print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {ch_str:>12} │")
108
+ print("└────────────────────┴──────────────┴──────────────┘")
109
+ print("```")
110
+
111
+
112
+ def cmd_pages(args, key, base, pid):
113
+ params = _date_params(args) or {}
114
+ params["page"] = args.page
115
+ params["limit"] = args.limit
116
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages",
117
+ key, base, params=params)
118
+ data = extract_data(result)
119
+ # API returns {items, total, ...} not a bare list or {"pages": [...]}
120
+ if isinstance(data, list):
121
+ items = data
122
+ elif isinstance(data, dict):
123
+ items = data.get("items") or data.get("pages", [])
124
+ else:
125
+ items = []
126
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
127
+ if args.json:
128
+ print_json(items)
129
+ return
130
+ if not items:
131
+ print("No page data.")
132
+ return
133
+ print(f"Top AI-referenced pages ({total} total, showing {len(items)})")
134
+ print()
135
+ print("```")
136
+ print("┌────────────────────────────────────────────┬──────────┬────────┬──────────┐")
137
+ print("│ Page │ Bot Hits │ Human │ Total AI │")
138
+ print("├────────────────────────────────────────────┼──────────┼────────┼──────────┤")
139
+ for p in items:
140
+ path = truncate(p.get("path") or p.get("url"), 42)
141
+ bot = _fmt_num(p.get("bot_requests") or p.get("ai_hits") or p.get("count"))
142
+ human = _fmt_num(p.get("human_referral_requests"))
143
+ total_ai = _fmt_num(p.get("total_ai_requests") or p.get("references"))
144
+ print(f"│ {pad(path, 42)} │ {bot:>8} │ {human:>6} │ {total_ai:>8} │")
145
+ print("└────────────────────────────────────────────┴──────────┴────────┴──────────┘")
146
+ print("```")
147
+
148
+
149
+ def cmd_pages_detail(args, key, base, pid):
150
+ params = _date_params(args) or {}
151
+ params["page"] = args.page
152
+ params["limit"] = args.limit
153
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages-detail",
154
+ key, base, params=params)
155
+ data = extract_data(result)
156
+ # API returns {items, total, ...} not a bare list or {"pages": [...]}
157
+ if isinstance(data, list):
158
+ items = data
159
+ elif isinstance(data, dict):
160
+ items = data.get("items") or data.get("pages", [])
161
+ else:
162
+ items = []
163
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
164
+ if args.json:
165
+ print_json(items)
166
+ return
167
+ if not items:
168
+ print("No page detail data.")
169
+ return
170
+ print(f"Pages — 5-metric detail ({total} total, showing {len(items)})")
171
+ print()
172
+ print("```")
173
+ print("┌────────────────────────────────┬──────┬──────┬──────┬──────┬──────┐")
174
+ print("│ Page │ Cite │ Indx │ Trn │ Agnt │ Ref │")
175
+ print("├────────────────────────────────┼──────┼──────┼──────┼──────┼──────┤")
176
+ for p in items:
177
+ path = truncate(p.get("path") or p.get("url"), 30)
178
+ c = _fmt_num(p.get("ai_citation"))
179
+ i = _fmt_num(p.get("ai_search") or p.get("ai_index"))
180
+ t = _fmt_num(p.get("ai_training"))
181
+ a = _fmt_num(p.get("ai_agent"))
182
+ r = _fmt_num(p.get("ai_referral") or p.get("ai_referral_visits"))
183
+ print(f"│ {pad(path, 30)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {r:>4} │")
184
+ print("└────────────────────────────────┴──────┴──────┴──────┴──────┴──────┘")
185
+ print("```")
186
+
187
+
188
+ def cmd_pages_export(args, key, base, pid):
189
+ params = _date_params(args) or {}
190
+ params["format"] = args.format
191
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages-detail/export",
192
+ key, base, params=params)
193
+ # Export returns raw CSV or JSON — pass through.
194
+ if isinstance(result, (dict, list)):
195
+ if args.format == "json":
196
+ print_json(result)
197
+ else:
198
+ print(result)
199
+ else:
200
+ print(result)
201
+
202
+
203
+ def cmd_platform_flow(args, key, base, pid):
204
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages-platform-flow",
205
+ key, base, params=_date_params(args))
206
+ data = extract_data(result) or {}
207
+ if args.json:
208
+ print_json(data)
209
+ return
210
+ nodes = data.get("nodes", [])
211
+ links = data.get("links", [])
212
+ print(f"Sankey AI platform → page: {len(nodes)} nodes, {len(links)} links")
213
+ print()
214
+ sorted_links = sorted(links, key=lambda l: l.get("value", 0), reverse=True)[:15]
215
+ for ln in sorted_links:
216
+ s = ln.get("source")
217
+ t = ln.get("target")
218
+ v = _fmt_num(ln.get("value"))
219
+ print(f" {s} → {t} ({v})")
220
+
221
+
222
+ def cmd_logs(args, key, base, pid):
223
+ params = _date_params(args) or {}
224
+ params["page"] = args.page
225
+ params["limit"] = args.limit
226
+ if args.traffic_type:
227
+ params["traffic_type"] = args.traffic_type
228
+ result = api_get(f"/api/projects/{pid}/ai-agent/logs",
229
+ key, base, params=params)
230
+ data = extract_data(result)
231
+ # API returns {items, total, page, limit, pages}
232
+ if isinstance(data, list):
233
+ items = data
234
+ total = len(data)
235
+ elif isinstance(data, dict):
236
+ items = data.get("items") or data.get("logs") or data.get("events", [])
237
+ total = data.get("total", len(items))
238
+ else:
239
+ items = []
240
+ total = 0
241
+ if args.json:
242
+ print_json(items)
243
+ return
244
+ if not items:
245
+ print("No log events.")
246
+ return
247
+ print(f"Raw AI event logs (total {total}, showing {len(items)})")
248
+ print()
249
+ print("```")
250
+ print("┌────────────────────┬────────────────────┬────────────┬──────────────────────────┐")
251
+ print("│ Time (UTC) │ Type │ Platform │ Path │")
252
+ print("├────────────────────┼────────────────────┼────────────┼──────────────────────────┤")
253
+ for e in items:
254
+ t = truncate(
255
+ (e.get("occurred_at") or e.get("timestamp") or e.get("created_at") or "")[:19].replace("T", " "),
256
+ 18)
257
+ ttype = truncate(
258
+ e.get("traffic_type_label") or e.get("bot_name") or e.get("traffic_type") or "(human)",
259
+ 18)
260
+ plat = truncate(e.get("platform_name") or e.get("platform") or "--", 10)
261
+ path = truncate(e.get("path") or e.get("url"), 24)
262
+ print(f"│ {pad(t, 18)} │ {pad(ttype, 18)} │ {pad(plat, 10)} │ {pad(path, 24)} │")
263
+ print("└────────────────────┴────────────────────┴────────────┴──────────────────────────┘")
264
+ print("```")
265
+
266
+
267
+ def main():
268
+ parser = argparse.ArgumentParser(description="Site-wide AI traffic analytics")
269
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
270
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
271
+ sub = parser.add_subparsers(dest="command", required=True)
272
+
273
+ for name in ("overview-kpi", "platform-flow"):
274
+ p = sub.add_parser(name)
275
+ p.add_argument("--start")
276
+ p.add_argument("--end")
277
+ p.add_argument("--platform")
278
+
279
+ for name in ("pages", "pages-detail"):
280
+ p = sub.add_parser(name)
281
+ p.add_argument("--start")
282
+ p.add_argument("--end")
283
+ p.add_argument("--platform")
284
+ p.add_argument("--page", type=int, default=1)
285
+ p.add_argument("--limit", type=int, default=20)
286
+
287
+ p_ex = sub.add_parser("pages-export")
288
+ p_ex.add_argument("--start")
289
+ p_ex.add_argument("--end")
290
+ p_ex.add_argument("--platform")
291
+ p_ex.add_argument("--format", choices=["csv", "json"], default="csv")
292
+
293
+ p_lg = sub.add_parser("logs")
294
+ p_lg.add_argument("--start")
295
+ p_lg.add_argument("--end")
296
+ p_lg.add_argument("--platform")
297
+ p_lg.add_argument("--page", type=int, default=1)
298
+ p_lg.add_argument("--limit", type=int, default=50)
299
+ p_lg.add_argument("--traffic-type", choices=["bot", "human"])
300
+
301
+ args = parser.parse_args()
302
+ key, base = get_api_config()
303
+ pid = get_project_id(args.project_id)
304
+
305
+ handlers = {
306
+ "overview-kpi": cmd_overview_kpi,
307
+ "pages": cmd_pages,
308
+ "pages-detail": cmd_pages_detail,
309
+ "pages-export": cmd_pages_export,
310
+ "platform-flow": cmd_platform_flow,
311
+ "logs": cmd_logs,
312
+ }
313
+ handlers[args.command](args, key, base, pid)
314
+
315
+
316
+ if __name__ == "__main__":
317
+ main()
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ """Deep-dive analytics for a specific page path.
3
+
4
+ All subcommands require --path (URL path on the project's website).
5
+
6
+ Subcommands:
7
+ kpi --path <p> — 5 KPI cards for this page (citation/index/training/
8
+ agent/total bots) with delta + daily trend
9
+ logs --path <p> — recent AI access events on this exact path
10
+ [--limit 50] [--traffic-type bot|human]
11
+ platforms --path <p> — per-platform 4-AI + human-referral + share table
12
+ related --path <p> — sibling pages under the same parent path, with
13
+ 5-metric comparison
14
+ health --path <p> — PageSpeed Insights report (cached). Returns
15
+ report=null if never analyzed.
16
+ health-refresh --path <p> — synchronously refresh PSI report (15–60s blocking)
17
+
18
+ Common opts: --start --end
19
+
20
+ Usage:
21
+ python3 scripts/page_detail.py kpi --path /blog/my-article
22
+ python3 scripts/page_detail.py logs --path /blog/my-article --limit 20
23
+ python3 scripts/page_detail.py health --path /blog/my-article
24
+ """
25
+ import sys
26
+ import os
27
+ import argparse
28
+
29
+ sys.path.insert(0, os.path.dirname(__file__))
30
+ from _client import (
31
+ get_api_config, get_project_id,
32
+ api_get, api_post,
33
+ extract_data, print_json, truncate,
34
+ pad,
35
+ )
36
+
37
+
38
+ def _fmt_num(n):
39
+ if n is None:
40
+ return "--"
41
+ try:
42
+ f = float(n)
43
+ if f == int(f):
44
+ return f"{int(f):,}"
45
+ return f"{f:,.1f}"
46
+ except (TypeError, ValueError):
47
+ return str(n)
48
+
49
+
50
+ def _fmt_change(c):
51
+ if c is None:
52
+ return "--"
53
+ try:
54
+ f = float(c)
55
+ sign = "+" if f >= 0 else ""
56
+ return f"{sign}{f:.1f}%"
57
+ except (TypeError, ValueError):
58
+ return str(c)
59
+
60
+
61
+ def _base_params(args):
62
+ p = {"path": args.path}
63
+ if args.start:
64
+ p["start_date"] = args.start
65
+ if args.end:
66
+ p["end_date"] = args.end
67
+ return p
68
+
69
+
70
+ def cmd_kpi(args, key, base, pid):
71
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/kpi",
72
+ key, base, params=_base_params(args))
73
+ data = extract_data(result) or {}
74
+ if args.json:
75
+ print_json(data)
76
+ return
77
+ kpis = data.get("kpis") or data
78
+ print(f"Page KPIs — {args.path}")
79
+ print()
80
+ print("```")
81
+ print("┌────────────────────┬──────────────┬──────────────┐")
82
+ print("│ KPI │ Current │ Change │")
83
+ print("├────────────────────┼──────────────┼──────────────┤")
84
+ for k, label in [
85
+ ("ai_citation", "AI Citation"),
86
+ ("ai_index", "AI Index"),
87
+ ("ai_training", "AI Training"),
88
+ ("ai_agent", "AI Agent"),
89
+ ("total_bots", "All bots"),
90
+ ]:
91
+ v = kpis.get(k) if isinstance(kpis, dict) else None
92
+ if isinstance(v, dict):
93
+ cur, ch = v.get("current"), v.get("change")
94
+ else:
95
+ cur, ch = v, None
96
+ print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {_fmt_change(ch):>12} │")
97
+ print("└────────────────────┴──────────────┴──────────────┘")
98
+ print("```")
99
+
100
+
101
+ def cmd_logs(args, key, base, pid):
102
+ params = _base_params(args)
103
+ params["limit"] = args.limit
104
+ if args.traffic_type:
105
+ params["traffic_type"] = args.traffic_type
106
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/logs",
107
+ key, base, params=params)
108
+ data = extract_data(result)
109
+ items = data if isinstance(data, list) else (data or {}).get("logs") or (data or {}).get("events", [])
110
+ if args.json:
111
+ print_json(items)
112
+ return
113
+ if not items:
114
+ print(f"No log events for {args.path}.")
115
+ return
116
+ print(f"Logs for {args.path} ({len(items)})")
117
+ print()
118
+ print("```")
119
+ print("┌────────────────────┬────────────────────┬────────────┐")
120
+ print("│ Time │ Bot / UA │ Platform │")
121
+ print("├────────────────────┼────────────────────┼────────────┤")
122
+ for e in items:
123
+ t = truncate((e.get("timestamp") or e.get("created_at") or "")[:19], 18)
124
+ bot = truncate(e.get("bot_name") or e.get("user_agent") or "(human)", 18)
125
+ plat = truncate(e.get("platform") or "--", 10)
126
+ print(f"│ {pad(t, 18)} │ {pad(bot, 18)} │ {pad(plat, 10)} │")
127
+ print("└────────────────────┴────────────────────┴────────────┘")
128
+ print("```")
129
+
130
+
131
+ def cmd_platforms(args, key, base, pid):
132
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/platforms",
133
+ key, base, params=_base_params(args))
134
+ data = extract_data(result)
135
+ items = data if isinstance(data, list) else (data or {}).get("platforms", [])
136
+ if args.json:
137
+ print_json(items)
138
+ return
139
+ if not items:
140
+ print(f"No platform breakdown for {args.path}.")
141
+ return
142
+ print(f"Per-platform metrics for {args.path} ({len(items)})")
143
+ print()
144
+ print("```")
145
+ print("┌────────────────────┬──────┬──────┬──────┬──────┬────────┬────────┐")
146
+ print("│ Platform │ Cite │ Indx │ Trn │ Agnt │ Human │ % │")
147
+ print("├────────────────────┼──────┼──────┼──────┼──────┼────────┼────────┤")
148
+ for p in items:
149
+ name = truncate(p.get("name") or p.get("platform"), 18)
150
+ c = _fmt_num(p.get("ai_citation"))
151
+ i = _fmt_num(p.get("ai_index"))
152
+ t = _fmt_num(p.get("ai_training"))
153
+ a = _fmt_num(p.get("ai_agent"))
154
+ h = _fmt_num(p.get("human_referral") or p.get("human"))
155
+ pct = p.get("share") or p.get("percentage")
156
+ pct_s = (f"{float(pct):.1f}%" if pct is not None else "--")
157
+ print(f"│ {pad(name, 18)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {h:>6} │ {pct_s:>6} │")
158
+ print("└────────────────────┴──────┴──────┴──────┴──────┴────────┴────────┘")
159
+ print("```")
160
+
161
+
162
+ def cmd_related(args, key, base, pid):
163
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/related",
164
+ key, base, params=_base_params(args))
165
+ data = extract_data(result)
166
+ items = data if isinstance(data, list) else (data or {}).get("pages", [])
167
+ if args.json:
168
+ print_json(items)
169
+ return
170
+ if not items:
171
+ print(f"No sibling pages under parent of {args.path}.")
172
+ return
173
+ print(f"Sibling pages of {args.path} ({len(items)})")
174
+ print()
175
+ print("```")
176
+ print("┌────────────────────────────────┬──────┬──────┬──────┬──────┬──────┐")
177
+ print("│ Page │ Cite │ Indx │ Trn │ Agnt │ Cit% │")
178
+ print("├────────────────────────────────┼──────┼──────┼──────┼──────┼──────┤")
179
+ for p in items:
180
+ path = truncate(p.get("path") or p.get("url"), 30)
181
+ c = _fmt_num(p.get("ai_citation"))
182
+ i = _fmt_num(p.get("ai_index"))
183
+ t = _fmt_num(p.get("ai_training"))
184
+ a = _fmt_num(p.get("ai_agent"))
185
+ cp = p.get("ai_citation_pct")
186
+ cp_s = (f"{float(cp):.1f}" if cp is not None else "--")
187
+ print(f"│ {pad(path, 30)} │ {c:>4} │ {i:>4} │ {t:>4} │ {a:>4} │ {cp_s:>4} │")
188
+ print("└────────────────────────────────┴──────┴──────┴──────┴──────┴──────┘")
189
+ print("```")
190
+
191
+
192
+ def cmd_health(args, key, base, pid):
193
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages/by-path/health",
194
+ key, base, params={"path": args.path})
195
+ data = extract_data(result) or {}
196
+ if args.json:
197
+ print_json(data)
198
+ return
199
+ report = data.get("report") or data
200
+ if not report or report == {}:
201
+ print(f"No PageSpeed report cached for {args.path}.")
202
+ print("Run `python3 scripts/page_detail.py health-refresh --path ...` to generate one.")
203
+ return
204
+ print(f"PageSpeed Insights — {args.path}")
205
+ print()
206
+ perf = report.get("performance_score") or report.get("performance")
207
+ print(f" Performance score: {perf}")
208
+ cwv = report.get("core_web_vitals") or report.get("cwv") or {}
209
+ if cwv:
210
+ print(" Core Web Vitals:")
211
+ for k, v in cwv.items():
212
+ print(f" {k}: {v}")
213
+ fetched = report.get("fetched_at") or report.get("updated_at")
214
+ if fetched:
215
+ print(f" Fetched at: {fetched}")
216
+
217
+
218
+ def cmd_health_refresh(args, key, base, pid):
219
+ print(f"Refreshing PageSpeed report for {args.path} (15–60s blocking)...")
220
+ result = api_post(f"/api/projects/{pid}/ai-agent/pages/by-path/health/refresh",
221
+ key, base, body={"path": args.path})
222
+ data = extract_data(result) or {}
223
+ if args.json:
224
+ print_json(data)
225
+ return
226
+ print("Refresh completed.")
227
+ perf = data.get("performance_score") or data.get("performance")
228
+ if perf is not None:
229
+ print(f" Performance score: {perf}")
230
+
231
+
232
+ def main():
233
+ parser = argparse.ArgumentParser(description="Page-level deep-dive AI analytics")
234
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
235
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
236
+ sub = parser.add_subparsers(dest="command", required=True)
237
+
238
+ for name in ("kpi", "platforms", "related"):
239
+ p = sub.add_parser(name)
240
+ p.add_argument("--path", required=True)
241
+ p.add_argument("--start")
242
+ p.add_argument("--end")
243
+
244
+ p_lg = sub.add_parser("logs")
245
+ p_lg.add_argument("--path", required=True)
246
+ p_lg.add_argument("--start")
247
+ p_lg.add_argument("--end")
248
+ p_lg.add_argument("--limit", type=int, default=50)
249
+ p_lg.add_argument("--traffic-type", choices=["bot", "human"])
250
+
251
+ p_h = sub.add_parser("health")
252
+ p_h.add_argument("--path", required=True)
253
+
254
+ p_hr = sub.add_parser("health-refresh")
255
+ p_hr.add_argument("--path", required=True)
256
+
257
+ args = parser.parse_args()
258
+ key, base = get_api_config()
259
+ pid = get_project_id(args.project_id)
260
+
261
+ handlers = {
262
+ "kpi": cmd_kpi,
263
+ "logs": cmd_logs,
264
+ "platforms": cmd_platforms,
265
+ "related": cmd_related,
266
+ "health": cmd_health,
267
+ "health-refresh": cmd_health_refresh,
268
+ }
269
+ handlers[args.command](args, key, base, pid)
270
+
271
+
272
+ if __name__ == "__main__":
273
+ main()
@@ -0,0 +1,89 @@
1
+ # GEO Analytics API Reference
2
+
3
+ ## Endpoint
4
+
5
+ ```
6
+ GET /api/public/projects/{project_id}/dashboard/overview
7
+ ```
8
+
9
+ ### Query parameters
10
+
11
+ | Param | Values | Default | Description |
12
+ |---|---|---|---|
13
+ | `period` | `7d` `14d` `30d` `90d` | `30d` | Analysis time window |
14
+
15
+ ### Response envelope
16
+
17
+ ```json
18
+ {
19
+ "code": 0,
20
+ "message": "success",
21
+ "data": {
22
+ "period": "30d",
23
+ "date_range": { "start": "2026-03-19", "end": "2026-04-18" },
24
+ "compare_range": { "start": "2026-02-17", "end": "2026-03-18" },
25
+ "integrations": { "gsc": true, "ga4": true, "cloudflare": false },
26
+ "search": { ... },
27
+ "traffic": { ... },
28
+ "ai_impact": { ... },
29
+ "infrastructure":{ ... }
30
+ }
31
+ }
32
+ ```
33
+
34
+ Sections that are `null` → integration not connected.
35
+
36
+ ---
37
+
38
+ ## Section: `search` (Google Search Console)
39
+
40
+ | Field | Type | Description |
41
+ |---|---|---|
42
+ | `clicks` | int | Total organic clicks |
43
+ | `impressions` | int | Total impressions |
44
+ | `avg_ctr` | float | Average click-through rate |
45
+ | `avg_position` | float | Average ranking position |
46
+ | `clicks_trend` | array | Daily clicks `[{ date, clicks }]` |
47
+ | `top_queries` | array | Top 5 search queries `[{ query, clicks, impressions, ctr, position }]` |
48
+ | `prev_clicks` | int | Previous period clicks (for comparison) |
49
+ | `prev_impressions` | int | Previous period impressions |
50
+
51
+ ---
52
+
53
+ ## Section: `traffic` (Google Analytics 4)
54
+
55
+ | Field | Type | Description |
56
+ |---|---|---|
57
+ | `sessions` | int | Total sessions |
58
+ | `active_users` | int | Active users |
59
+ | `sessions_trend` | array | Daily sessions |
60
+ | `top_sources` | array | Top traffic sources |
61
+ | `prev_sessions` | int | Previous period sessions |
62
+ | `prev_active_users` | int | Previous period active users |
63
+
64
+ ---
65
+
66
+ ## Section: `ai_impact`
67
+
68
+ | Field | Type | Description |
69
+ |---|---|---|
70
+ | `ai_referral_sessions` | int | Sessions from AI platforms (ChatGPT, Perplexity, etc.) |
71
+ | `ai_crawler_requests` | int | Requests from AI crawlers via Cloudflare |
72
+ | `ai_referral_trend` | array | Daily AI referral sessions |
73
+ | `ai_crawler_trend` | array | Daily AI crawler requests |
74
+ | `top_ai_sources` | array | Top AI referral sources |
75
+ | `top_ai_bots` | array | Top AI crawlers |
76
+
77
+ **Tracked AI sources**: chat.openai.com, chatgpt.com, perplexity.ai, claude.ai,
78
+ copilot.microsoft.com, gemini.google.com, you.com, phind.com, poe.com
79
+
80
+ ---
81
+
82
+ ## Section: `infrastructure` (Cloudflare)
83
+
84
+ | Field | Type | Description |
85
+ |---|---|---|
86
+ | `total_requests` | int | Total HTTP requests |
87
+ | `bandwidth_bytes` | int | Bandwidth consumed |
88
+ | `threats_blocked` | int | Blocked threats |
89
+ | `requests_trend` | array | Daily request counts |