@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,212 @@
1
+ #!/usr/bin/env python3
2
+ """Cloudflare Worker management for AI-traffic tracking.
3
+
4
+ The Worker injects a script that logs AI crawler + referral visits. After
5
+ deployment, the Worker page queries (overview/pages) return AI traffic
6
+ analytics.
7
+
8
+ Subcommands:
9
+ config — get the Worker JS code + keys
10
+ deploy [--zone-id <id>] — deploy / re-deploy the Worker
11
+ undeploy [--keep-script] --yes — remove route (and optionally script) DESTRUCTIVE
12
+ deploy-status — check whether Worker is deployed
13
+ overview [--start <>] [--end <>] — Worker traffic overview (AI grouping)
14
+ pages [--page 1] [--limit 20] — Worker page-level AI traffic rankings
15
+
16
+ Usage:
17
+ python3 scripts/cloudflare_worker.py config
18
+ python3 scripts/cloudflare_worker.py deploy
19
+ python3 scripts/cloudflare_worker.py deploy-status
20
+ python3 scripts/cloudflare_worker.py overview
21
+ python3 scripts/cloudflare_worker.py undeploy --yes
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, api_post, api_delete,
31
+ extract_data, print_json, truncate,
32
+ pad,
33
+ )
34
+
35
+ def _fmt_num(n):
36
+ if n is None:
37
+ return "--"
38
+ try:
39
+ f = float(n)
40
+ if f == int(f):
41
+ return f"{int(f):,}"
42
+ return f"{f:,.1f}"
43
+ except (TypeError, ValueError):
44
+ return str(n)
45
+
46
+
47
+ def cmd_config(args, key, base, pid):
48
+ result = api_get(f"/api/projects/{pid}/integrations/cloudflare/worker-config", key, base)
49
+ data = extract_data(result) or {}
50
+ if args.json:
51
+ print_json(data)
52
+ return
53
+ print("Worker configuration")
54
+ print()
55
+ for k in ("worker_name", "receiver_url", "secret_key"):
56
+ if k in data:
57
+ print(f" {k}: {data.get(k)}")
58
+ script = data.get("script") or data.get("worker_script")
59
+ if script:
60
+ print()
61
+ print("Worker script:")
62
+ print("```javascript")
63
+ print(script)
64
+ print("```")
65
+
66
+
67
+ def cmd_deploy(args, key, base, pid):
68
+ body = {}
69
+ if args.zone_id:
70
+ body["zone_id"] = args.zone_id
71
+ result = api_post(f"/api/projects/{pid}/integrations/cloudflare/worker/deploy", key, base,
72
+ body=body or None)
73
+ data = extract_data(result) or {}
74
+ if args.json:
75
+ print_json(data)
76
+ return
77
+ print("Worker deployment started.")
78
+ for k in ("worker_name", "routes", "status"):
79
+ if k in data:
80
+ print(f" {k}: {data.get(k)}")
81
+
82
+
83
+ def cmd_undeploy(args, key, base, pid):
84
+ if not args.yes:
85
+ print(f"About to remove Worker route for project {pid}.")
86
+ print("Re-run with --yes to confirm.")
87
+ sys.exit(1)
88
+ qs = "?keep_script=true" if args.keep_script else ""
89
+ api_delete(f"/api/projects/{pid}/integrations/cloudflare/worker/deploy" + qs, key, base)
90
+ print("Worker route removed.")
91
+
92
+
93
+ def cmd_deploy_status(args, key, base, pid):
94
+ result = api_get(f"/api/projects/{pid}/integrations/cloudflare/worker/deploy-status", key, base)
95
+ data = extract_data(result) or {}
96
+ if args.json:
97
+ print_json(data)
98
+ return
99
+ deployed = data.get("deployed")
100
+ print("Worker deployment status")
101
+ print()
102
+ print("```")
103
+ print("┌────────────────────┬──────────────────────────────┐")
104
+ print("│ Field │ Value │")
105
+ print("├────────────────────┼──────────────────────────────┤")
106
+ print(f"│ {pad('deployed', 18)} │ {pad(('Yes' if deployed else 'No'), 28)} │")
107
+ for k in ("worker_name", "routes", "script_name"):
108
+ if k in data:
109
+ val = data.get(k)
110
+ if isinstance(val, list):
111
+ val = ", ".join(str(v) for v in val)
112
+ print(f"│ {pad(k, 18)} │ {pad(truncate(val, 28), 28)} │")
113
+ print("└────────────────────┴──────────────────────────────┘")
114
+ print("```")
115
+
116
+
117
+ def cmd_overview(args, key, base, pid):
118
+ params = {}
119
+ if args.start:
120
+ params["start_date"] = args.start
121
+ if args.end:
122
+ params["end_date"] = args.end
123
+ result = api_get(f"/api/projects/{pid}/integrations/cloudflare/worker/overview", key, base,
124
+ params=params or None)
125
+ data = extract_data(result) or {}
126
+ if args.json:
127
+ print_json(data)
128
+ return
129
+ print("Worker AI Traffic Overview")
130
+ print()
131
+ print("```")
132
+ print("┌────────────────────┬──────────────┐")
133
+ print("│ Metric │ Value │")
134
+ print("├────────────────────┼──────────────┤")
135
+ for k, label in [
136
+ ("ai_crawler_requests", "AI crawler reqs"),
137
+ ("ai_referral_visits", "AI referral hits"),
138
+ ("total_events", "Total events"),
139
+ ]:
140
+ if k in data:
141
+ print(f"│ {pad(label, 18)} │ {_fmt_num(data.get(k)):>12} │")
142
+ print("└────────────────────┴──────────────┘")
143
+ print("```")
144
+
145
+
146
+ def cmd_pages(args, key, base, pid):
147
+ result = api_get(f"/api/projects/{pid}/integrations/cloudflare/worker/pages", key, base,
148
+ params={"page": args.page, "limit": args.limit})
149
+ data = extract_data(result)
150
+ items = data if isinstance(data, list) else (data or {}).get("pages", [])
151
+ if args.json:
152
+ print_json(items)
153
+ return
154
+ if not items:
155
+ print("No Worker page-level AI traffic yet.")
156
+ return
157
+ print(f"Worker page-level AI traffic ({len(items)})")
158
+ print()
159
+ print("```")
160
+ print("┌────────────────────────────────────────────┬──────────┐")
161
+ print("│ Page │ AI Hits │")
162
+ print("├────────────────────────────────────────────┼──────────┤")
163
+ for p in items:
164
+ path = truncate(p.get("page_path") or p.get("path") or p.get("url"), 42)
165
+ hits = _fmt_num(p.get("ai_hits") or p.get("hits") or p.get("count"))
166
+ print(f"│ {pad(path, 42)} │ {hits:>8} │")
167
+ print("└────────────────────────────────────────────┴──────────┘")
168
+ print("```")
169
+
170
+
171
+ def main():
172
+ parser = argparse.ArgumentParser(description="Cloudflare Worker management for AI tracking")
173
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
174
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
175
+ sub = parser.add_subparsers(dest="command", required=True)
176
+
177
+ sub.add_parser("config")
178
+
179
+ p_d = sub.add_parser("deploy")
180
+ p_d.add_argument("--zone-id", help="Override zone ID (optional)")
181
+
182
+ p_u = sub.add_parser("undeploy", help="Remove route (DESTRUCTIVE)")
183
+ p_u.add_argument("--keep-script", action="store_true", help="Keep the script, remove only the route")
184
+ p_u.add_argument("--yes", action="store_true", help="Confirm")
185
+
186
+ sub.add_parser("deploy-status")
187
+
188
+ p_o = sub.add_parser("overview")
189
+ p_o.add_argument("--start", help="Start date YYYY-MM-DD")
190
+ p_o.add_argument("--end", help="End date YYYY-MM-DD")
191
+
192
+ p_p = sub.add_parser("pages")
193
+ p_p.add_argument("--page", type=int, default=1)
194
+ p_p.add_argument("--limit", type=int, default=20)
195
+
196
+ args = parser.parse_args()
197
+ key, base = get_api_config()
198
+ pid = get_project_id(args.project_id)
199
+
200
+ handlers = {
201
+ "config": cmd_config,
202
+ "deploy": cmd_deploy,
203
+ "undeploy": cmd_undeploy,
204
+ "deploy-status": cmd_deploy_status,
205
+ "overview": cmd_overview,
206
+ "pages": cmd_pages,
207
+ }
208
+ handlers[args.command](args, key, base, pid)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """Google Analytics 4: data sync + traffic/AI-referral/page/source queries.
3
+
4
+ Subcommands:
5
+ sync — pull latest GA4 data into the local DB (POST)
6
+ overview [--start <YYYY-MM-DD>] [--end <>] — sessions/users/pageviews + channel split
7
+ ai-referrals [--start <>] [--end <>] — AI-platform referral detail + trends
8
+ pages [--page 1] [--limit 20] — top pages by views
9
+ sources [--start <>] [--end <>] — traffic by channel group
10
+
11
+ Usage:
12
+ python3 scripts/ga4_data.py sync
13
+ python3 scripts/ga4_data.py overview --start 2025-11-01 --end 2025-11-30
14
+ python3 scripts/ga4_data.py ai-referrals --json
15
+ """
16
+ import sys
17
+ import os
18
+ import argparse
19
+
20
+ sys.path.insert(0, os.path.dirname(__file__))
21
+ from _client import (
22
+ get_api_config, get_project_id,
23
+ api_get, api_post,
24
+ extract_data, print_json, truncate,
25
+ pad,
26
+ )
27
+
28
+ def _fmt_num(n):
29
+ if n is None:
30
+ return "--"
31
+ try:
32
+ f = float(n)
33
+ if f == int(f):
34
+ return f"{int(f):,}"
35
+ return f"{f:,.1f}"
36
+ except (TypeError, ValueError):
37
+ return str(n)
38
+
39
+
40
+ def cmd_sync(args, key, base, pid):
41
+ result = api_post(f"/api/projects/{pid}/integrations/ga4/sync", key, base)
42
+ data = extract_data(result) or {}
43
+ if args.json:
44
+ print_json(data)
45
+ return
46
+ print("GA4 sync started.")
47
+ for k in ("status", "rows_synced", "started_at"):
48
+ if k in data:
49
+ print(f" {k}: {data.get(k)}")
50
+
51
+
52
+ def _date_params(args):
53
+ p = {}
54
+ if args.start:
55
+ p["start_date"] = args.start
56
+ if args.end:
57
+ p["end_date"] = args.end
58
+ return p or None
59
+
60
+
61
+ def cmd_overview(args, key, base, pid):
62
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/overview", key, base,
63
+ params=_date_params(args))
64
+ data = extract_data(result) or {}
65
+ if args.json:
66
+ print_json(data)
67
+ return
68
+ print("GA4 Overview")
69
+ print()
70
+ print("```")
71
+ print("┌────────────────────┬──────────────┐")
72
+ print("│ Metric │ Value │")
73
+ print("├────────────────────┼──────────────┤")
74
+ for k, label in [
75
+ ("sessions", "Sessions"),
76
+ ("active_users", "Active users"),
77
+ ("pageviews", "Pageviews"),
78
+ ("ai_referral_sessions", "AI ref sessions"),
79
+ ]:
80
+ if k in data:
81
+ print(f"│ {pad(label, 18)} │ {_fmt_num(data.get(k)):>12} │")
82
+ print("└────────────────────┴──────────────┘")
83
+ print("```")
84
+
85
+
86
+ def cmd_ai_referrals(args, key, base, pid):
87
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/ai-referrals", key, base,
88
+ params=_date_params(args))
89
+ data = extract_data(result) or {}
90
+ if args.json:
91
+ print_json(data)
92
+ return
93
+ plats = data.get("platforms") or data.get("by_platform") or []
94
+ print("GA4 AI Referrals")
95
+ print()
96
+ print("```")
97
+ print("┌────────────────────┬──────────────┬──────────────┐")
98
+ print("│ Platform │ Sessions │ Users │")
99
+ print("├────────────────────┼──────────────┼──────────────┤")
100
+ for p in plats[:10]:
101
+ name = truncate(p.get("name") or p.get("platform"), 18)
102
+ sess = _fmt_num(p.get("sessions"))
103
+ users = _fmt_num(p.get("users") or p.get("active_users"))
104
+ print(f"│ {pad(name, 18)} │ {sess:>12} │ {users:>12} │")
105
+ print("└────────────────────┴──────────────┴──────────────┘")
106
+ print("```")
107
+
108
+
109
+ def cmd_pages(args, key, base, pid):
110
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/pages", key, base,
111
+ params={"page": args.page, "limit": args.limit})
112
+ data = extract_data(result)
113
+ items = data if isinstance(data, list) else (data or {}).get("pages", [])
114
+ if args.json:
115
+ print_json(items)
116
+ return
117
+ if not items:
118
+ print("No page data. Run `sync` first or check the date range.")
119
+ return
120
+ print(f"GA4 Top Pages ({len(items)})")
121
+ print()
122
+ print("```")
123
+ print("┌────────────────────────────────────────────┬──────────┬──────────┐")
124
+ print("│ Page │ Views │ Sessions │")
125
+ print("├────────────────────────────────────────────┼──────────┼──────────┤")
126
+ for p in items:
127
+ path = truncate(p.get("page_path") or p.get("path") or p.get("url"), 42)
128
+ views = _fmt_num(p.get("views") or p.get("page_views"))
129
+ sess = _fmt_num(p.get("sessions"))
130
+ print(f"│ {pad(path, 42)} │ {views:>8} │ {sess:>8} │")
131
+ print("└────────────────────────────────────────────┴──────────┴──────────┘")
132
+ print("```")
133
+
134
+
135
+ def cmd_sources(args, key, base, pid):
136
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/sources", key, base,
137
+ params=_date_params(args))
138
+ data = extract_data(result)
139
+ items = data if isinstance(data, list) else (data or {}).get("sources") or (data or {}).get("channels", [])
140
+ if args.json:
141
+ print_json(items)
142
+ return
143
+ if not items:
144
+ print("No source data available.")
145
+ return
146
+ print("GA4 Traffic Sources")
147
+ print()
148
+ print("```")
149
+ print("┌────────────────────┬──────────────┐")
150
+ print("│ Channel │ Sessions │")
151
+ print("├────────────────────┼──────────────┤")
152
+ for s in items[:10]:
153
+ name = truncate(s.get("name") or s.get("channel") or s.get("source"), 18)
154
+ sess = _fmt_num(s.get("sessions"))
155
+ print(f"│ {pad(name, 18)} │ {sess:>12} │")
156
+ print("└────────────────────┴──────────────┘")
157
+ print("```")
158
+
159
+
160
+ def main():
161
+ parser = argparse.ArgumentParser(description="GA4 sync + data queries")
162
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
163
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
164
+ sub = parser.add_subparsers(dest="command", required=True)
165
+
166
+ sub.add_parser("sync", help="Pull latest GA4 data into local DB")
167
+
168
+ for name in ("overview", "ai-referrals", "sources"):
169
+ p = sub.add_parser(name)
170
+ p.add_argument("--start", help="Start date YYYY-MM-DD")
171
+ p.add_argument("--end", help="End date YYYY-MM-DD")
172
+
173
+ p_pages = sub.add_parser("pages")
174
+ p_pages.add_argument("--page", type=int, default=1)
175
+ p_pages.add_argument("--limit", type=int, default=20)
176
+
177
+ args = parser.parse_args()
178
+ key, base = get_api_config()
179
+ pid = get_project_id(args.project_id)
180
+
181
+ handlers = {
182
+ "sync": cmd_sync,
183
+ "overview": cmd_overview,
184
+ "ai-referrals": cmd_ai_referrals,
185
+ "pages": cmd_pages,
186
+ "sources": cmd_sources,
187
+ }
188
+ handlers[args.command](args, key, base, pid)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Google Analytics 4: OAuth setup, property selection, and connection state.
3
+
4
+ Subcommands:
5
+ auth-url — get the OAuth URL for the user to visit
6
+ connect --code <auth_code> — manually exchange an auth code (debug)
7
+ properties — list available GA4 properties
8
+ select --property-id <id> — select which GA4 property to use
9
+
10
+ After `select`, a 7-day backfill sync is triggered automatically by the API.
11
+
12
+ Usage:
13
+ python3 scripts/ga4_setup.py auth-url
14
+ python3 scripts/ga4_setup.py properties
15
+ python3 scripts/ga4_setup.py select --property-id 123456789
16
+ """
17
+ import sys
18
+ import os
19
+ import argparse
20
+
21
+ sys.path.insert(0, os.path.dirname(__file__))
22
+ from _client import (
23
+ get_api_config, get_project_id,
24
+ api_get, api_post,
25
+ extract_data, print_json, truncate,
26
+ pad,
27
+ )
28
+
29
+ def cmd_auth_url(args, key, base, pid):
30
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/auth-url", key, base)
31
+ data = extract_data(result) or {}
32
+ if args.json:
33
+ print_json(data)
34
+ return
35
+ url = data.get("url") or data.get("auth_url")
36
+ print("GA4 OAuth — open this URL in a browser:")
37
+ print(url or "(no URL returned)")
38
+
39
+
40
+ def cmd_connect(args, key, base, pid):
41
+ result = api_post(
42
+ f"/api/projects/{pid}/integrations/ga4/connect",
43
+ key, base, body={"code": args.code},
44
+ )
45
+ data = extract_data(result)
46
+ if args.json:
47
+ print_json(data if data is not None else {"ok": True})
48
+ return
49
+ print("GA4 connected.")
50
+
51
+
52
+ def cmd_properties(args, key, base, pid):
53
+ result = api_get(f"/api/projects/{pid}/integrations/ga4/properties", key, base)
54
+ data = extract_data(result)
55
+ items = data if isinstance(data, list) else (data or {}).get("properties", [])
56
+ if args.json:
57
+ print_json(items)
58
+ return
59
+ if not items:
60
+ print("No GA4 properties accessible with the current token.")
61
+ return
62
+ print(f"GA4 properties ({len(items)})")
63
+ print()
64
+ print("```")
65
+ print("┌──────────────────────┬────────────────────────────┬────────────────────┐")
66
+ print("│ Property ID │ Display name │ Web stream domain │")
67
+ print("├──────────────────────┼────────────────────────────┼────────────────────┤")
68
+ for p in items:
69
+ pid_ = truncate(p.get("id") or p.get("property_id"), 20)
70
+ name = truncate(p.get("display_name") or p.get("name"), 26)
71
+ domain = truncate(p.get("domain") or p.get("web_domain") or "--", 18)
72
+ print(f"│ {pad(pid_, 20)} │ {pad(name, 26)} │ {pad(domain, 18)} │")
73
+ print("└──────────────────────┴────────────────────────────┴────────────────────┘")
74
+ print("```")
75
+
76
+
77
+ def cmd_select(args, key, base, pid):
78
+ result = api_post(
79
+ f"/api/projects/{pid}/integrations/ga4/select-property",
80
+ key, base, body={"property_id": args.property_id},
81
+ )
82
+ data = extract_data(result)
83
+ if args.json:
84
+ print_json(data if data is not None else {"ok": True})
85
+ return
86
+ print(f"Selected GA4 property: {args.property_id}")
87
+ print("(API will trigger a 7-day backfill sync in the background.)")
88
+
89
+
90
+ def main():
91
+ parser = argparse.ArgumentParser(description="GA4 OAuth setup & property selection")
92
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
93
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
94
+ sub = parser.add_subparsers(dest="command", required=True)
95
+
96
+ sub.add_parser("auth-url", help="Get the OAuth URL")
97
+
98
+ p_c = sub.add_parser("connect", help="Manual auth-code exchange (debug)")
99
+ p_c.add_argument("--code", required=True, help="Google OAuth authorization code")
100
+
101
+ sub.add_parser("properties", help="List available GA4 properties")
102
+
103
+ p_s = sub.add_parser("select", help="Choose which GA4 property to bind")
104
+ p_s.add_argument("--property-id", required=True)
105
+
106
+ args = parser.parse_args()
107
+ key, base = get_api_config()
108
+ pid = get_project_id(args.project_id)
109
+
110
+ handlers = {
111
+ "auth-url": cmd_auth_url,
112
+ "connect": cmd_connect,
113
+ "properties": cmd_properties,
114
+ "select": cmd_select,
115
+ }
116
+ handlers[args.command](args, key, base, pid)
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: adgine/geo-opportunities
3
+ description: Discovers and displays GEO optimization opportunities for a project.
4
+ Shows AI-identified content gaps, topic opportunities, and actionable recommendations
5
+ ranked by potential impact (relevance, traffic, competition, urgency, AI citation).
6
+ Use when the user wants to find new content ideas, discover optimization opportunities,
7
+ see what topics they should target, or get prioritized recommendations for improving
8
+ their AI visibility. 触发词:查看机会、分析机会、优化机会、发现机会、网站机会、
9
+ 项目机会、机会列表、opportunity discovery、content gaps。
10
+ ---
11
+
12
+ # GEO Opportunity Discovery
13
+
14
+ ## Output rules — IDs (apply to every reply)
15
+
16
+ These rules apply to **every list, table, and confirmation message** in this skill. Their goal: keep user-facing output friendly while preserving the IDs the agent needs internally.
17
+
18
+ 1. **Lists & tables — never show raw UUIDs in cells.** Use a 1-based `#` index column instead. Keep a private mental mapping of `#N → actual UUID` so that follow-up commands like *"show details of #3"*, *"tell me more about the 2nd one"* resolve to the right entity.
19
+ - Index numbers restart from 1 in each new list — they are not stable across calls.
20
+ - If the user references *"the opportunity about X"*, match by visible content (title / category), not by ID.
21
+
22
+ 2. **Single-item operations — prefer a human name over an ID.**
23
+ - ✅ *"Opportunity **Optimize product comparison pages** — score 85/100."*
24
+ - ❌ *"Opportunity `a4305b57-1c79-4cec-a17c-16eb1d959ea6` — score 85."*
25
+ - If the entity has **no human-readable name**, use a short 8-character prefix: `2a2a8f4f…`
26
+
27
+ 3. **Always exception: `--json` mode.** When the user passes `--json` to a script or explicitly asks for raw JSON / debug output, print the script output verbatim — do not strip IDs.
28
+
29
+ 4. **Internally, the agent still uses full UUIDs** for every API call (`--project-id`, `--opportunity-id`, etc.). The display rules only affect what is shown back to the user.
30
+
31
+ ---
32
+
33
+ ## Step 1: Make sure GEO_API_KEY is configured
34
+
35
+ Scripts auto-load `GEO_API_KEY` from `<skills-root>/.env` on import — **no `export` needed, no shell restart needed**. To check the configuration, run any script (it prints the exact `.env` path if the key is missing).
36
+
37
+ - ✅ Key already in `<skills-root>/.env` → proceed.
38
+ - ❌ Key missing, or user just gave you a new key → go to the **adgine-geo-projects** skill, **Step 0**, which runs `python3 <skills-root>/setup.py <KEY>` to write the key into the correct `.env` file. **Never** write the key to `~/.zshrc`, `~/.bashrc`, Hermes global config, or any user-secrets store.
39
+
40
+ > ⚠️ **IMPORTANT:** In all shell/exec commands, always reference the key as `$GEO_API_KEY` (the environment variable). Never hardcode the key value or use placeholder strings like `API_KEY=***` or `API_KEY=geo_sk_live_xxx` directly in a command — this will cause authentication failures.
41
+
42
+
43
+ ## Project selection
44
+
45
+ ```bash
46
+ export GEO_PROJECT_ID=<project-id> # session shortcut — resets when terminal closes
47
+ # Run python3 scripts/list_projects.py from adgine-geo-projects skill to find your IDs
48
+ ```
49
+
50
+ ## What are opportunities?
51
+
52
+ Opportunities are AI-identified content gaps and optimization recommendations for your project. The system analyzes your current visibility, topics, competitors, and AI citation data to surface high-impact actions you can take to improve GEO performance.
53
+
54
+ Each opportunity includes:
55
+ - **Title** — a concise description of the opportunity
56
+ - **Total Score** — composite impact score (higher = more impactful)
57
+ - **Scores** — radar chart dimensions: relevance, traffic, competition, urgency, ai_citation
58
+ - **Category** — type of opportunity (e.g., content gap, optimization, new topic)
59
+ - **Rationale** — why this opportunity matters
60
+ - **Guidance** — strategic direction
61
+ - **Implementation** — concrete step-by-step actions
62
+ - **Coverage** — current coverage data
63
+ - **Source URLs** — reference links supporting the recommendation
64
+
65
+ ---
66
+
67
+ ## Commands
68
+
69
+ ### List project opportunities
70
+ ```bash
71
+ python3 scripts/list_opportunities.py [--project-id <id>] [--json]
72
+ ```
73
+ Returns the latest batch of opportunities for the project. Three possible states:
74
+ - **ready** — opportunities are available, displays ranked list
75
+ - **pending** — opportunities are currently being generated; check back in a few minutes
76
+ - **empty** — no opportunities yet; this is normal for new projects. Opportunity discovery requires approximately **1 week** of the website running in the platform before the first analysis cycle produces results. Relay this context to the user so they understand *why* there is nothing to show, and encourage them to check back after the weekly cycle.
77
+
78
+ ### Get opportunity detail
79
+ ```bash
80
+ python3 scripts/get_opportunity.py --opportunity-id <id> [--project-id <id>] [--json]
81
+ ```
82
+ Shows the full detail of a single opportunity including:
83
+ - Title & total score
84
+ - Score breakdown (radar dimensions)
85
+ - Category & rationale
86
+ - Guidance
87
+ - Implementation steps (numbered action items)
88
+ - Coverage details
89
+ - Source URLs
90
+
91
+ ---
92
+
93
+ ## Workflow
94
+
95
+ 1. List opportunities: `python3 scripts/list_opportunities.py`
96
+ 2. Review the ranked list — higher scores = higher potential impact
97
+ 3. Drill into specific opportunities: `python3 scripts/get_opportunity.py --opportunity-id <id>`
98
+ 4. Follow the implementation steps to act on the opportunity
99
+ 5. Use other skills to execute (e.g., `adgine-geo-content` for article generation, `adgine-geo-topics` for new topics)
100
+
101
+ ---
102
+
103
+ ## Output Format
104
+
105
+ > ⚠️ **CRITICAL — Table cell content rule (must follow exactly):**
106
+ > Tables use fenced code blocks with box-drawing borders. They only align correctly when **every cell contains ASCII characters exclusively**.
107
+ > - **NEVER** put emoji inside table cells.
108
+ > - Emoji go ONLY on the label line **above** the ` ``` ` fence.
109
+ > - Status values in cells: `Ready` / `Pending` / `Empty`
110
+ > - Use `[label](url)` for clickable URLs (outside fenced blocks).
111
+
112
+ ---
113
+
114
+ ### When listing opportunities (`list_opportunities.py`)
115
+
116
+ > 🔍 **Opportunities** — *N items found* (run date: YYYY-MM-DD)
117
+
118
+ ```
119
+ ┌────┬────────────────────────────────────┬───────┬──────────────┐
120
+ │ # │ Title │ Score │ Category │
121
+ ├────┼────────────────────────────────────┼───────┼──────────────┤
122
+ │ 1 │ Optimize product comparison pages │ 85 │ content_gap │
123
+ │ 2 │ Add FAQ schema to landing pages │ 72 │ optimization │
124
+ │ 3 │ Target "best X alternatives" topic │ 68 │ new_topic │
125
+ └────┴────────────────────────────────────┴───────┴──────────────┘
126
+ ```
127
+
128
+ > 💡 Use `get_opportunity.py --opportunity-id <id>` for full details on any item.
129
+
130
+ ---
131
+
132
+ ### When showing opportunity detail (`get_opportunity.py`)
133
+
134
+ > 🎯 **Opportunity Detail**
135
+ > Title: *"<title>"*
136
+ > Score: **N/100**
137
+
138
+ 📊 Score Breakdown
139
+ ```
140
+ ┌──────────────┬───────┐
141
+ │ Dimension │ Score │
142
+ ├──────────────┼───────┤
143
+ │ Relevance │ 18 │
144
+ │ Traffic │ 16 │
145
+ │ Competition │ 17 │
146
+ │ Urgency │ 15 │
147
+ │ AI Citation │ 19 │
148
+ └──────────────┴───────┘
149
+ ```
150
+
151
+ **Category:** content_gap
152
+ **Rationale:** "<rationale text>"
153
+ **Guidance:** "<guidance text>"
154
+
155
+ 📋 Implementation Steps:
156
+ 1. Step one description
157
+ 2. Step two description
158
+ 3. Step three description
159
+
160
+ 🔗 Source URLs:
161
+ - [source1.com/page](https://source1.com/page)
162
+ - [source2.com/article](https://source2.com/article)