@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,278 @@
1
+ #!/usr/bin/env python3
2
+ """AI bot/crawler traffic tracking.
3
+
4
+ Subcommands:
5
+ overview [--start <>] [--end <>] — 5 KPI cards + dual-period daily trend
6
+ (citation / training / index / agent / total)
7
+ platforms — ranking: which AI platforms crawl most
8
+ (OpenAI / Anthropic / Google / Perplexity ...)
9
+ by-platform — bot list grouped by AI platform
10
+ types — distribution by bot purpose
11
+ (index / training / assistant / agent)
12
+ useragents — detailed access by specific User-Agent
13
+ (GPTBot, ClaudeBot, GoogleBot, PerplexityBot...)
14
+ pages-by-bot [--limit 5] — per-bot top pages
15
+
16
+ Common opts: --start --end --platform
17
+
18
+ Usage:
19
+ python3 scripts/bot_traffic.py overview
20
+ python3 scripts/bot_traffic.py platforms
21
+ python3 scripts/bot_traffic.py useragents --start 2025-11-01
22
+ """
23
+ import sys
24
+ import os
25
+ import argparse
26
+
27
+ sys.path.insert(0, os.path.dirname(__file__))
28
+ from _client import (
29
+ get_api_config, get_project_id,
30
+ api_get,
31
+ extract_data, print_json, truncate,
32
+ pad,
33
+ )
34
+
35
+
36
+ def _fmt_num(n):
37
+ if n is None:
38
+ return "--"
39
+ try:
40
+ f = float(n)
41
+ if f == int(f):
42
+ return f"{int(f):,}"
43
+ return f"{f:,.1f}"
44
+ except (TypeError, ValueError):
45
+ return str(n)
46
+
47
+
48
+ def _fmt_change(c):
49
+ if c is None:
50
+ return "--"
51
+ try:
52
+ f = float(c)
53
+ sign = "+" if f >= 0 else ""
54
+ return f"{sign}{f:.1f}%"
55
+ except (TypeError, ValueError):
56
+ return str(c)
57
+
58
+
59
+ def _date_params(args):
60
+ p = {}
61
+ if args.start:
62
+ p["start_date"] = args.start
63
+ if args.end:
64
+ p["end_date"] = args.end
65
+ if getattr(args, "platform", None):
66
+ p["platform"] = args.platform
67
+ return p or None
68
+
69
+
70
+ def cmd_overview(args, key, base, pid):
71
+ result = api_get(f"/api/projects/{pid}/ai-agent/bot-traffic-overview",
72
+ key, base, params=_date_params(args))
73
+ data = extract_data(result) or {}
74
+ if args.json:
75
+ print_json(data)
76
+ return
77
+ kpis_raw = data.get("kpis") or data
78
+ # API returns kpis as a list of {key, label, current, prev, delta}
79
+ # Build a lookup dict keyed by the "key" field
80
+ if isinstance(kpis_raw, list):
81
+ kpis = {item["key"]: item for item in kpis_raw if "key" in item}
82
+ else:
83
+ kpis = kpis_raw if isinstance(kpis_raw, dict) else {}
84
+ print("AI Bot Traffic Overview")
85
+ print()
86
+ print("```")
87
+ print("┌────────────────────┬──────────────┬──────────────┐")
88
+ print("│ KPI │ Current │ Change │")
89
+ print("├────────────────────┼──────────────┼──────────────┤")
90
+ for k, label in [
91
+ ("ai_citation", "AI Citation"),
92
+ ("ai_training", "AI Training"),
93
+ ("ai_search", "AI Index"),
94
+ ("ai_agent", "AI Agent"),
95
+ ("all_bots", "All bot visits"),
96
+ ]:
97
+ v = kpis.get(k)
98
+ if isinstance(v, dict):
99
+ cur = v.get("current")
100
+ delta = v.get("delta")
101
+ prev = v.get("prev")
102
+ if delta is not None and prev:
103
+ pct = (delta / prev * 100)
104
+ ch_str = f"+{pct:.1f}%" if pct >= 0 else f"{pct:.1f}%"
105
+ else:
106
+ ch_str = _fmt_change(None)
107
+ else:
108
+ cur, ch_str = v, "--"
109
+ print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {ch_str:>12} │")
110
+ print("└────────────────────┴──────────────┴──────────────┘")
111
+ print("```")
112
+
113
+
114
+ def cmd_platforms(args, key, base, pid):
115
+ result = api_get(f"/api/projects/{pid}/ai-agent/platforms",
116
+ key, base, params=_date_params(args))
117
+ data = extract_data(result)
118
+ items = data if isinstance(data, list) else (data or {}).get("platforms", [])
119
+ if args.json:
120
+ print_json(items)
121
+ return
122
+ if not items:
123
+ print("No AI platform crawler data.")
124
+ return
125
+ print(f"AI Platforms ranked by bot visits ({len(items)})")
126
+ print()
127
+ print("```")
128
+ print("┌────────────────────┬──────────────┬──────────────┐")
129
+ print("│ Platform │ Requests │ Share │")
130
+ print("├────────────────────┼──────────────┼──────────────┤")
131
+ for p in items:
132
+ name = truncate(p.get("name") or p.get("platform"), 18)
133
+ req = _fmt_num(p.get("requests") or p.get("count") or p.get("total"))
134
+ pct = p.get("share") or p.get("percentage") or p.get("pct")
135
+ pct_str = (f"{float(pct):.1f}%" if pct is not None else "--")
136
+ print(f"│ {pad(name, 18)} │ {req:>12} │ {pct_str:>12} │")
137
+ print("└────────────────────┴──────────────┴──────────────┘")
138
+ print("```")
139
+
140
+
141
+ def cmd_by_platform(args, key, base, pid):
142
+ result = api_get(f"/api/projects/{pid}/ai-agent/bot-platforms",
143
+ key, base, params=_date_params(args))
144
+ data = extract_data(result)
145
+ items = data if isinstance(data, list) else (data or {}).get("platforms", [])
146
+ if args.json:
147
+ print_json(items)
148
+ return
149
+ if not items:
150
+ print("No bot-platform data.")
151
+ return
152
+ print(f"Bots grouped by AI platform ({len(items)})")
153
+ print()
154
+ for p in items[:10]:
155
+ name = p.get("name") or p.get("platform")
156
+ total = _fmt_num(p.get("total") or p.get("requests"))
157
+ print(f"• {name} — total {total}")
158
+ bots = p.get("bots") or []
159
+ for b in bots[:5]:
160
+ bn = truncate(b.get("name") or b.get("bot"), 28)
161
+ br = _fmt_num(b.get("requests") or b.get("count"))
162
+ print(f" - {pad(bn, 28)} {br:>10}")
163
+ print()
164
+
165
+
166
+ def cmd_types(args, key, base, pid):
167
+ result = api_get(f"/api/projects/{pid}/ai-agent/bot-types",
168
+ key, base, params=_date_params(args))
169
+ data = extract_data(result)
170
+ items = data if isinstance(data, list) else (data or {}).get("types", [])
171
+ if args.json:
172
+ print_json(items)
173
+ return
174
+ if not items:
175
+ print("No bot-type data.")
176
+ return
177
+ print("AI Bot Type Distribution")
178
+ print()
179
+ print("```")
180
+ print("┌────────────────────┬──────────────┬──────────────┐")
181
+ print("│ Type │ Requests │ Share │")
182
+ print("├────────────────────┼──────────────┼──────────────┤")
183
+ for t in items:
184
+ name = truncate(t.get("name") or t.get("type"), 18)
185
+ req = _fmt_num(t.get("requests") or t.get("count"))
186
+ pct = t.get("share") or t.get("percentage")
187
+ pct_str = (f"{float(pct):.1f}%" if pct is not None else "--")
188
+ print(f"│ {pad(name, 18)} │ {req:>12} │ {pct_str:>12} │")
189
+ print("└────────────────────┴──────────────┴──────────────┘")
190
+ print("```")
191
+
192
+
193
+ def cmd_useragents(args, key, base, pid):
194
+ result = api_get(f"/api/projects/{pid}/ai-agent/bot-useragents",
195
+ key, base, params=_date_params(args))
196
+ data = extract_data(result)
197
+ items = data if isinstance(data, list) else (data or {}).get("useragents") or (data or {}).get("bots", [])
198
+ if args.json:
199
+ print_json(items)
200
+ return
201
+ if not items:
202
+ print("No user-agent data.")
203
+ return
204
+ print(f"AI Bot User-Agents ({len(items)})")
205
+ print()
206
+ print("```")
207
+ print("┌──────────────────────────────┬────────────────────┬──────────────┐")
208
+ print("│ Bot name │ Platform │ Requests │")
209
+ print("├──────────────────────────────┼────────────────────┼──────────────┤")
210
+ for u in items:
211
+ name = truncate(u.get("bot_name") or u.get("name") or u.get("user_agent"), 28)
212
+ plat = truncate(u.get("platform") or "--", 18)
213
+ req = _fmt_num(u.get("requests") or u.get("count"))
214
+ print(f"│ {pad(name, 28)} │ {pad(plat, 18)} │ {req:>12} │")
215
+ print("└──────────────────────────────┴────────────────────┴──────────────┘")
216
+ print("```")
217
+
218
+
219
+ def cmd_pages_by_bot(args, key, base, pid):
220
+ params = _date_params(args) or {}
221
+ params["limit"] = args.limit
222
+ result = api_get(f"/api/projects/{pid}/ai-agent/pages-by-bot",
223
+ key, base, params=params)
224
+ data = extract_data(result)
225
+ items = data if isinstance(data, list) else (data or {}).get("bots", [])
226
+ if args.json:
227
+ print_json(items)
228
+ return
229
+ if not items:
230
+ print("No pages-by-bot data.")
231
+ return
232
+ print(f"Top pages per AI bot ({len(items)} bots)")
233
+ print()
234
+ for b in items[:10]:
235
+ bn = b.get("bot_name") or b.get("name")
236
+ print(f"• {bn}")
237
+ for p in (b.get("pages") or [])[:args.limit]:
238
+ path = truncate(p.get("path") or p.get("url"), 50)
239
+ hits = _fmt_num(p.get("requests") or p.get("count"))
240
+ print(f" {pad(path, 50)} {hits:>8}")
241
+ print()
242
+
243
+
244
+ def main():
245
+ parser = argparse.ArgumentParser(description="AI bot/crawler traffic tracking")
246
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
247
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
248
+ sub = parser.add_subparsers(dest="command", required=True)
249
+
250
+ for name in ("overview", "platforms", "by-platform", "types", "useragents"):
251
+ p = sub.add_parser(name)
252
+ p.add_argument("--start", help="Start date YYYY-MM-DD")
253
+ p.add_argument("--end", help="End date YYYY-MM-DD")
254
+ p.add_argument("--platform", help="Filter by platform code")
255
+
256
+ p_pbb = sub.add_parser("pages-by-bot")
257
+ p_pbb.add_argument("--start")
258
+ p_pbb.add_argument("--end")
259
+ p_pbb.add_argument("--platform")
260
+ p_pbb.add_argument("--limit", type=int, default=5)
261
+
262
+ args = parser.parse_args()
263
+ key, base = get_api_config()
264
+ pid = get_project_id(args.project_id)
265
+
266
+ handlers = {
267
+ "overview": cmd_overview,
268
+ "platforms": cmd_platforms,
269
+ "by-platform": cmd_by_platform,
270
+ "types": cmd_types,
271
+ "useragents": cmd_useragents,
272
+ "pages-by-bot": cmd_pages_by_bot,
273
+ }
274
+ handlers[args.command](args, key, base, pid)
275
+
276
+
277
+ if __name__ == "__main__":
278
+ main()
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ """Real human visits driven by AI platforms (UTM + referral + GA4).
3
+
4
+ Subcommands:
5
+ overview — 3 KPI cards: UTM AI visits / referral AI
6
+ visits / AI share of total + daily trend
7
+ platforms — by AI platform (UTM + referral) detail table
8
+ pages — by page path: human AI visits per page
9
+ platform-flow — Sankey: AI platform → landing page (real humans)
10
+ referral — referral-only traffic by AI source
11
+
12
+ GA4-based (require GA4 connected):
13
+ ga-overview — GA4 sessions/revenue from AI sources (trend)
14
+ ga-platforms — GA4 sessions/transactions/revenue by AI platform
15
+ ga-landing-pages [--limit 20] — GA4 top landing pages from AI traffic
16
+ ga-landing-flow — GA4 Sankey: AI platform → landing page
17
+
18
+ Common opts: --start --end --platform
19
+
20
+ Usage:
21
+ python3 scripts/human_traffic.py overview
22
+ python3 scripts/human_traffic.py ga-platforms
23
+ python3 scripts/human_traffic.py pages --start 2025-11-01
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,
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 _date_params(args):
62
+ p = {}
63
+ if args.start:
64
+ p["start_date"] = args.start
65
+ if args.end:
66
+ p["end_date"] = args.end
67
+ if getattr(args, "platform", None):
68
+ p["platform"] = args.platform
69
+ return p or None
70
+
71
+
72
+ def cmd_overview(args, key, base, pid):
73
+ result = api_get(f"/api/projects/{pid}/ai-agent/human-traffic-overview",
74
+ key, base, params=_date_params(args))
75
+ data = extract_data(result) or {}
76
+ if args.json:
77
+ print_json(data)
78
+ return
79
+ kpis_raw = data.get("kpis") or data
80
+ # API returns kpis as a list of {key, label, current, prev, delta, delta_pct}
81
+ # Build a lookup dict keyed by the "key" field
82
+ if isinstance(kpis_raw, list):
83
+ kpis = {item["key"]: item for item in kpis_raw if "key" in item}
84
+ else:
85
+ kpis = kpis_raw if isinstance(kpis_raw, dict) else {}
86
+ print("AI Human Traffic Overview")
87
+ print()
88
+ print("```")
89
+ print("┌────────────────────┬──────────────┬──────────────┐")
90
+ print("│ KPI │ Current │ Change │")
91
+ print("├────────────────────┼──────────────┼──────────────┤")
92
+ for k, label in [
93
+ ("utm", "UTM AI visits"),
94
+ ("referral", "Referral visits"),
95
+ ("ai_ratio", "AI share %"),
96
+ ]:
97
+ v = kpis.get(k)
98
+ if isinstance(v, dict):
99
+ cur = v.get("current")
100
+ delta_pct = v.get("delta_pct")
101
+ if delta_pct is not None:
102
+ ch_str = f"+{delta_pct:.1f}%" if delta_pct >= 0 else f"{delta_pct:.1f}%"
103
+ else:
104
+ ch_str = "--"
105
+ else:
106
+ cur, ch_str = v, "--"
107
+ print(f"│ {pad(label, 18)} │ {_fmt_num(cur):>12} │ {ch_str:>12} │")
108
+ print("└────────────────────┴──────────────┴──────────────┘")
109
+ print("```")
110
+
111
+
112
+ def cmd_platforms(args, key, base, pid):
113
+ result = api_get(f"/api/projects/{pid}/ai-agent/human-platforms",
114
+ key, base, params=_date_params(args))
115
+ data = extract_data(result)
116
+ items = data if isinstance(data, list) else (data or {}).get("platforms", [])
117
+ if args.json:
118
+ print_json(items)
119
+ return
120
+ if not items:
121
+ print("No human-platform data.")
122
+ return
123
+ print(f"AI Platforms — real human visits ({len(items)})")
124
+ print()
125
+ print("```")
126
+ print("┌────────────────────┬──────────────┬──────────────┐")
127
+ print("│ Platform │ Visits │ Change │")
128
+ print("├────────────────────┼──────────────┼──────────────┤")
129
+ for p in items:
130
+ name = truncate(p.get("name") or p.get("platform"), 18)
131
+ v = _fmt_num(p.get("visits") or p.get("sessions") or p.get("count"))
132
+ ch = _fmt_change(p.get("change") or p.get("delta"))
133
+ print(f"│ {pad(name, 18)} │ {v:>12} │ {ch:>12} │")
134
+ print("└────────────────────┴──────────────┴──────────────┘")
135
+ print("```")
136
+
137
+
138
+ def cmd_pages(args, key, base, pid):
139
+ params = _date_params(args) or {}
140
+ params["page"] = args.page
141
+ params["limit"] = args.limit
142
+ result = api_get(f"/api/projects/{pid}/ai-agent/human-pages",
143
+ key, base, params=params)
144
+ data = extract_data(result)
145
+ items = data if isinstance(data, list) else (data or {}).get("pages", [])
146
+ if args.json:
147
+ print_json(items)
148
+ return
149
+ if not items:
150
+ print("No human-pages data.")
151
+ return
152
+ print(f"Human AI visits per page ({len(items)})")
153
+ print()
154
+ print("```")
155
+ print("┌────────────────────────────────────────────┬──────────┐")
156
+ print("│ Page │ Visits │")
157
+ print("├────────────────────────────────────────────┼──────────┤")
158
+ for p in items:
159
+ path = truncate(p.get("path") or p.get("url"), 42)
160
+ v = _fmt_num(p.get("visits") or p.get("count"))
161
+ print(f"│ {pad(path, 42)} │ {v:>8} │")
162
+ print("└────────────────────────────────────────────┴──────────┘")
163
+ print("```")
164
+
165
+
166
+ def cmd_platform_flow(args, key, base, pid):
167
+ result = api_get(f"/api/projects/{pid}/ai-agent/human-platform-flow",
168
+ key, base, params=_date_params(args))
169
+ data = extract_data(result) or {}
170
+ if args.json:
171
+ print_json(data)
172
+ return
173
+ nodes = data.get("nodes", [])
174
+ links = data.get("links", [])
175
+ print(f"Sankey (real human): {len(nodes)} nodes, {len(links)} links")
176
+ print()
177
+ print("Top links:")
178
+ sorted_links = sorted(links, key=lambda l: l.get("value", 0), reverse=True)[:10]
179
+ for ln in sorted_links:
180
+ s = ln.get("source")
181
+ t = ln.get("target")
182
+ v = _fmt_num(ln.get("value"))
183
+ print(f" {s} → {t} ({v})")
184
+
185
+
186
+ def cmd_referral(args, key, base, pid):
187
+ result = api_get(f"/api/projects/{pid}/ai-agent/referral-traffic",
188
+ key, base, params=_date_params(args))
189
+ data = extract_data(result)
190
+ items = data if isinstance(data, list) else (data or {}).get("sources", [])
191
+ if args.json:
192
+ print_json(items)
193
+ return
194
+ if not items:
195
+ print("No referral data.")
196
+ return
197
+ print("AI Referral Traffic by source")
198
+ print()
199
+ print("```")
200
+ print("┌────────────────────┬──────────────┐")
201
+ print("│ Source │ Sessions │")
202
+ print("├────────────────────┼──────────────┤")
203
+ for s in items:
204
+ name = truncate(s.get("name") or s.get("source"), 18)
205
+ v = _fmt_num(s.get("sessions") or s.get("visits"))
206
+ print(f"│ {pad(name, 18)} │ {v:>12} │")
207
+ print("└────────────────────┴──────────────┘")
208
+ print("```")
209
+
210
+
211
+ def cmd_ga_overview(args, key, base, pid):
212
+ result = api_get(f"/api/projects/{pid}/ai-agent/ga-overview",
213
+ key, base, params=_date_params(args))
214
+ data = extract_data(result) or {}
215
+ if args.json:
216
+ print_json(data)
217
+ return
218
+ print("GA4 — AI source overview")
219
+ print()
220
+ print("```")
221
+ print("┌────────────────────┬──────────────┐")
222
+ print("│ Metric │ Value │")
223
+ print("├────────────────────┼──────────────┤")
224
+ for k, label in [
225
+ ("sessions", "Sessions"),
226
+ ("users", "Users"),
227
+ ("revenue", "Revenue"),
228
+ ("transactions", "Transactions"),
229
+ ]:
230
+ if k in data:
231
+ print(f"│ {pad(label, 18)} │ {_fmt_num(data.get(k)):>12} │")
232
+ print("└────────────────────┴──────────────┘")
233
+ print("```")
234
+
235
+
236
+ def cmd_ga_platforms(args, key, base, pid):
237
+ result = api_get(f"/api/projects/{pid}/ai-agent/ga-platforms",
238
+ key, base, params=_date_params(args))
239
+ data = extract_data(result)
240
+ items = data if isinstance(data, list) else (data or {}).get("platforms", [])
241
+ if args.json:
242
+ print_json(items)
243
+ return
244
+ if not items:
245
+ print("No GA4 platform data — is GA4 connected?")
246
+ return
247
+ print(f"GA4 by AI Platform ({len(items)})")
248
+ print()
249
+ print("```")
250
+ print("┌────────────────────┬──────────┬──────────┬──────────┬──────────┐")
251
+ print("│ Platform │ Sessions │ Trans │ Revenue │ Conv % │")
252
+ print("├────────────────────┼──────────┼──────────┼──────────┼──────────┤")
253
+ for p in items:
254
+ name = truncate(p.get("name") or p.get("platform"), 18)
255
+ s = _fmt_num(p.get("sessions"))
256
+ t = _fmt_num(p.get("transactions"))
257
+ r = _fmt_num(p.get("revenue"))
258
+ c = p.get("conversion_rate")
259
+ c_str = (f"{float(c):.2f}%" if c is not None else "--")
260
+ print(f"│ {pad(name, 18)} │ {s:>8} │ {t:>8} │ {r:>8} │ {c_str:>8} │")
261
+ print("└────────────────────┴──────────┴──────────┴──────────┴──────────┘")
262
+ print("```")
263
+
264
+
265
+ def cmd_ga_landing_pages(args, key, base, pid):
266
+ params = _date_params(args) or {}
267
+ params["limit"] = args.limit
268
+ result = api_get(f"/api/projects/{pid}/ai-agent/ga-landing-pages",
269
+ key, base, params=params)
270
+ data = extract_data(result)
271
+ items = data if isinstance(data, list) else (data or {}).get("pages", [])
272
+ if args.json:
273
+ print_json(items)
274
+ return
275
+ if not items:
276
+ print("No GA4 landing page data.")
277
+ return
278
+ print(f"GA4 Top Landing Pages from AI ({len(items)})")
279
+ print()
280
+ print("```")
281
+ print("┌──────────────────────────────────────┬──────────┬──────────┐")
282
+ print("│ Landing page │ Sessions │ Revenue │")
283
+ print("├──────────────────────────────────────┼──────────┼──────────┤")
284
+ for p in items:
285
+ path = truncate(p.get("path") or p.get("url") or p.get("page_path"), 36)
286
+ s = _fmt_num(p.get("sessions"))
287
+ r = _fmt_num(p.get("revenue"))
288
+ print(f"│ {pad(path, 36)} │ {s:>8} │ {r:>8} │")
289
+ print("└──────────────────────────────────────┴──────────┴──────────┘")
290
+ print("```")
291
+
292
+
293
+ def cmd_ga_landing_flow(args, key, base, pid):
294
+ result = api_get(f"/api/projects/{pid}/ai-agent/ga-platform-landing-flow",
295
+ key, base, params=_date_params(args))
296
+ data = extract_data(result) or {}
297
+ if args.json:
298
+ print_json(data)
299
+ return
300
+ nodes = data.get("nodes", [])
301
+ links = data.get("links", [])
302
+ print(f"GA4 Sankey: {len(nodes)} nodes, {len(links)} links")
303
+ print()
304
+ sorted_links = sorted(links, key=lambda l: l.get("value", 0), reverse=True)[:10]
305
+ for ln in sorted_links:
306
+ s = ln.get("source")
307
+ t = ln.get("target")
308
+ v = _fmt_num(ln.get("value"))
309
+ print(f" {s} → {t} ({v})")
310
+
311
+
312
+ def main():
313
+ parser = argparse.ArgumentParser(description="Real human AI-driven traffic tracking")
314
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
315
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
316
+ sub = parser.add_subparsers(dest="command", required=True)
317
+
318
+ for name in ("overview", "platforms", "platform-flow", "referral",
319
+ "ga-overview", "ga-platforms", "ga-landing-flow"):
320
+ p = sub.add_parser(name)
321
+ p.add_argument("--start")
322
+ p.add_argument("--end")
323
+ p.add_argument("--platform")
324
+
325
+ p_pages = sub.add_parser("pages")
326
+ p_pages.add_argument("--start")
327
+ p_pages.add_argument("--end")
328
+ p_pages.add_argument("--platform")
329
+ p_pages.add_argument("--page", type=int, default=1)
330
+ p_pages.add_argument("--limit", type=int, default=20)
331
+
332
+ p_glp = sub.add_parser("ga-landing-pages")
333
+ p_glp.add_argument("--start")
334
+ p_glp.add_argument("--end")
335
+ p_glp.add_argument("--platform")
336
+ p_glp.add_argument("--limit", type=int, default=20)
337
+
338
+ args = parser.parse_args()
339
+ key, base = get_api_config()
340
+ pid = get_project_id(args.project_id)
341
+
342
+ handlers = {
343
+ "overview": cmd_overview,
344
+ "platforms": cmd_platforms,
345
+ "pages": cmd_pages,
346
+ "platform-flow": cmd_platform_flow,
347
+ "referral": cmd_referral,
348
+ "ga-overview": cmd_ga_overview,
349
+ "ga-platforms": cmd_ga_platforms,
350
+ "ga-landing-pages": cmd_ga_landing_pages,
351
+ "ga-landing-flow": cmd_ga_landing_flow,
352
+ }
353
+ handlers[args.command](args, key, base, pid)
354
+
355
+
356
+ if __name__ == "__main__":
357
+ main()