@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.
- package/README.md +93 -0
- package/bin/adgine-geo.js +224 -0
- package/package.json +38 -0
- package/skills/.env.example +11 -0
- package/skills/README.md +247 -0
- package/skills/SKILL.md +117 -0
- package/skills/VERSION +1 -0
- package/skills/adgine-geo-aiagent/SKILL.md +200 -0
- package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
- package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
- package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
- package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
- package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
- package/skills/adgine-geo-analytics/API.md +89 -0
- package/skills/adgine-geo-analytics/SKILL.md +191 -0
- package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
- package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
- package/skills/adgine-geo-billing/SKILL.md +118 -0
- package/skills/adgine-geo-billing/scripts/_client.py +341 -0
- package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
- package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
- package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
- package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
- package/skills/adgine-geo-brand/SKILL.md +193 -0
- package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
- package/skills/adgine-geo-brand/scripts/_client.py +341 -0
- package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
- package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
- package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
- package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
- package/skills/adgine-geo-citation/SKILL.md +220 -0
- package/skills/adgine-geo-citation/scripts/_client.py +341 -0
- package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
- package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
- package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
- package/skills/adgine-geo-content/SKILL.md +224 -0
- package/skills/adgine-geo-content/WORKFLOW.md +100 -0
- package/skills/adgine-geo-content/scripts/_client.py +341 -0
- package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
- package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
- package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
- package/skills/adgine-geo-content/scripts/list_content.py +65 -0
- package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
- package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
- package/skills/adgine-geo-dashboard/SKILL.md +181 -0
- package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
- package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
- package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
- package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
- package/skills/adgine-geo-docs/SKILL.md +60 -0
- package/skills/adgine-geo-domains/SKILL.md +223 -0
- package/skills/adgine-geo-domains/scripts/_client.py +341 -0
- package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
- package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
- package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
- package/skills/adgine-geo-integrations/SKILL.md +188 -0
- package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
- package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
- package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
- package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
- package/skills/adgine-geo-opportunities/SKILL.md +162 -0
- package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
- package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
- package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
- package/skills/adgine-geo-performance/SKILL.md +177 -0
- package/skills/adgine-geo-performance/scripts/_client.py +341 -0
- package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
- package/skills/adgine-geo-projects/SKILL.md +279 -0
- package/skills/adgine-geo-projects/scripts/_client.py +341 -0
- package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
- package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
- package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
- package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
- package/skills/adgine-geo-saas/SKILL.md +144 -0
- package/skills/adgine-geo-saas/scripts/_client.py +341 -0
- package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
- package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
- package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
- package/skills/adgine-geo-site-audit/README.md +281 -0
- package/skills/adgine-geo-site-audit/SKILL.md +449 -0
- package/skills/adgine-geo-site-audit/requirements.txt +5 -0
- package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
- package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
- package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
- package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
- package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
- package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
- package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
- package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
- package/skills/adgine-geo-topics/SKILL.md +232 -0
- package/skills/adgine-geo-topics/scripts/_client.py +341 -0
- package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
- package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
- package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
- package/skills/adgine-geo-visibility/SKILL.md +168 -0
- package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
- package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
- package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
- package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
- package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
- package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
- package/skills/adgine-geo-wordpress/SKILL.md +151 -0
- package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
- package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
- package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
- package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
- package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
- package/skills/scripts/check_version.py +290 -0
- package/skills/setup.py +248 -0
- package/src/config.js +126 -0
- package/src/doctor.js +137 -0
- package/src/install.js +90 -0
- package/src/manifest.js +99 -0
- 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)
|