@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,191 @@
1
+ ---
2
+ name: adgine/geo-analytics
3
+ description: High-level traffic analytics overview for a GEO project — GA4 sessions,
4
+ active users, source breakdown, and aggregated AI-referral traffic in a single
5
+ call. Use when the user wants a broad traffic/analytics summary (流量概览,
6
+ analytics overview, traffic summary, AI 引荐汇总). NOT for deep per-bot crawl
7
+ breakdowns or per-page traffic logs — use adgine-geo-aiagent for those. NOT for
8
+ connecting/syncing GA4 or Cloudflare — use adgine-geo-integrations for that.
9
+ ---
10
+
11
+ # GEO Analytics
12
+
13
+ Fetches the full dashboard overview for a project — search, traffic, AI impact,
14
+ and infrastructure data in a single call.
15
+
16
+ ## 触发条件
17
+
18
+ 当用户说出以下意图时使用本 skill:
19
+ - “流量概览” / “流量汇总” / “traffic overview” / “analytics summary”
20
+ - “GA4 数据” / “访客数” / “会话数” / “sessions”
21
+ - “AI 引荐流量汇总” / “AI referral traffic”
22
+
23
+ **⛔ 以下意图不属于本 skill:**
24
+ - “哪些 AI bot 爬了我的网站” / “GPTBot 访问日志” / “某个页面的 AI 爬虫明细” → **adgine-geo-aiagent**
25
+ - “连接 GA4” / “同步数据” / “部署 Worker” → **adgine-geo-integrations**
26
+ - “项目总览” / “Dashboard” → **adgine-geo-dashboard**
27
+
28
+ ## Output rules — IDs (apply to every reply)
29
+
30
+ 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.
31
+
32
+ 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 *"delete #3"*, *"run citation test on #1 #2"*, *"show details of the 2nd one"* resolve to the right entity.
33
+ - Index numbers restart from 1 in each new list — they are not stable across calls.
34
+ - If the user references *"the topic about X"* / *"that Poki vs CrazyGames prompt"*, match by visible content (name / title / domain / prompt text), not by ID.
35
+
36
+ 2. **Single-item operations — prefer a human name over an ID.**
37
+ - ✅ *"Project **Poki vs Competitors** deleted."*
38
+ - ✅ *"Topic **Brand mentions in 2024** updated — name → 'Brand mentions 2025'."*
39
+ - ❌ *"Project `a4305b57-1c79-4cec-a17c-16eb1d959ea6` deleted."*
40
+ - If the entity has **no human-readable name** (e.g. an anonymous prompt or a job), use a short 8-character prefix: *"Prompt `2a2a8f4f…` deleted."* Never paste the full UUID.
41
+
42
+ 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.
43
+
44
+ 4. **Internally, the agent still uses full UUIDs** for every API call (`--project-id`, `--topic-id`, `--prompt-id`, etc.). The display rules only affect what is shown back to the user.
45
+
46
+ ---
47
+
48
+ ## Step 1: Make sure GEO_API_KEY is configured
49
+
50
+ 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).
51
+
52
+ - ✅ Key already in `<skills-root>/.env` → proceed.
53
+ - ❌ 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.
54
+
55
+ > ⚠️ **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.
56
+
57
+
58
+ ## Step 2: Identify project ID
59
+
60
+ Run `python3 scripts/list_projects.py` from the **adgine-geo-projects** skill if the project ID is unknown.
61
+
62
+ ## Fetch dashboard overview
63
+
64
+ ```bash
65
+ python3 scripts/get_overview.py --project-id <id> [--period 30d] [--json]
66
+ ```
67
+
68
+ **Period options:** `7d` · `14d` · `30d` (default) · `90d`
69
+
70
+ ## What the overview contains
71
+
72
+ | Section | Source | Key metrics |
73
+ |---|---|---|
74
+ | `traffic` | Google Analytics 4 | sessions, active users, top sources |
75
+ | `ai_impact` | GA4 + Cloudflare | AI referral sessions, AI crawler requests |
76
+ | `infrastructure` | Cloudflare | total requests, bandwidth, threat data |
77
+
78
+ Sections return `null` if the integration is not connected for that project.
79
+
80
+ ## Tips
81
+
82
+ - Use `--period 7d` for recent trends, `--period 90d` for long-term patterns
83
+ - If a section is `null`, suggest the user connect that integration in the GEO dashboard
84
+ - For detailed API field reference, see [API.md](API.md)
85
+
86
+ ## Output Format
87
+
88
+ > ⚠️ **CRITICAL — Table cell content rule (must follow exactly):**
89
+ > Use fenced code blocks with box-drawing border tables. They align perfectly — **only if every cell contains ASCII characters exclusively**.
90
+ > - **NEVER** put emoji (`✅` `❌` `📈` etc.) or non-ASCII text inside table cells. Emoji render as 2 display units but count as 1 character → all columns after them shift right permanently.
91
+ > - Emoji are allowed ONLY on the label line **immediately above** the opening ` ``` ` fence.
92
+ > - Status values in cells: `Connected` / `---` / `Pending` (NOT ✅/❌)
93
+ > - Change values in cells: `+1,200` / `-3,400` / `--` (NOT 📈/📉)
94
+ > - Box-drawing border chars (`┌─┬─┐ │ ├─┼─┤ └─┴─┘`) are fine — they are narrow 1-unit chars.
95
+
96
+ When presenting analytics results, follow this structure exactly.
97
+
98
+ ### 1. Header
99
+
100
+ > 📊 **Analytics Overview** — `<start>` → `<end>` (`<period>`)
101
+
102
+ ### 2. Integration status
103
+
104
+ 📡 Integrations
105
+ ```
106
+ ┌────────────────────────┬─────────────┐
107
+ │ Service │ Status │
108
+ ├────────────────────────┼─────────────┤
109
+ │ Google Analytics 4 │ Connected │
110
+ │ Cloudflare │ --- │
111
+ └────────────────────────┴─────────────┘
112
+ ```
113
+
114
+ ### 3. Traffic (GA4) — if available
115
+
116
+ 📈 Traffic (GA4)
117
+ ```
118
+ ┌────────────────────────┬──────────┬─────────┐
119
+ │ Metric │ Value │ vs Prev │
120
+ ├────────────────────────┼──────────┼─────────┤
121
+ │ Sessions │ 5,432 │ +320 │
122
+ │ Active Users │ 3,210 │ -- │
123
+ │ Pageviews │ 8,100 │ -- │
124
+ │ Bounce Rate │ 42.1% │ -- │
125
+ │ Avg Session Duration │ 2m 34s │ -- │
126
+ └────────────────────────┴──────────┴─────────┘
127
+ ```
128
+
129
+ 🌐 Top Sources (max 3 rows)
130
+ ```
131
+ ┌────────────────────┬──────────┬───────┐
132
+ │ Source / Medium │ Sessions │ Share │
133
+ ├────────────────────┼──────────┼───────┤
134
+ │ organic / google │ 2,100 │ 38.7% │
135
+ │ direct / none │ 1,200 │ 22.1% │
136
+ └────────────────────┴──────────┴───────┘
137
+ ```
138
+
139
+ ### 4. AI Impact — if available
140
+
141
+ 🤖 AI Impact
142
+ ```
143
+ ┌────────────────────────┬────────┬─────────┐
144
+ │ Metric │ Value │ vs Prev │
145
+ ├────────────────────────┼────────┼─────────┤
146
+ │ AI Referral Sessions │ 87 │ +12 │
147
+ │ AI Crawler Requests │ 4,320 │ -- │
148
+ └────────────────────────┴────────┴─────────┘
149
+ ```
150
+
151
+ ### 5. Infrastructure (Cloudflare) — if available
152
+
153
+ ☁️ Infrastructure (Cloudflare)
154
+ ```
155
+ ┌──────────────────┬─────────┐
156
+ │ Metric │ Value │
157
+ ├──────────────────┼─────────┤
158
+ │ Total Requests │ 120,000 │
159
+ │ Bandwidth │ 4.5 GB │
160
+ │ Threats Blocked │ 23 │
161
+ └──────────────────┴─────────┘
162
+ ```
163
+
164
+ ### Rules
165
+
166
+ - Status cells: `Connected` or `---` — **never** ✅/❌ inside cells
167
+ - Change cells: `+N` / `-N` / `--` — **never** 📈/📉 inside cells
168
+ - If a section is null/not connected: skip its table, show one line `🔌 **<Service>** — not connected. [Connect](https://platform.adgine.ai)`
169
+ - Keep total table width under ~60 chars (fits Telegram mobile without scroll)
170
+ - Each table in its own separate ` ``` ` fence
171
+
172
+ ---
173
+
174
+ ## Post-task recommendations
175
+
176
+ After reviewing analytics, suggest deeper or follow-up actions:
177
+
178
+ | What you saw | → use skill (agent-internal) |
179
+ |---|---|
180
+ | Traffic overview | 深入分析 AI 爬虫和 AI 引荐流量明细 *(→ adgine-geo-aiagent)*|
181
+ | GA4 data present | 查看 AI 驱动的真人访问来源 *(→ adgine-geo-aiagent)*|
182
+ | Cloudflare data present | 查看各 AI 平台的爬虫行为 *(→ adgine-geo-aiagent)*|
183
+ | AI referral sessions growing | 运行引用测试,量化 AI 可见性 *(→ adgine-geo-citation)*|
184
+ | Integration missing (null sections) | 连接 GA4 或 Cloudflare 获取完整数据 *(→ adgine-geo-integrations)*|
185
+ | Low traffic overall | 生成更多 GEO 优化内容吸引 AI 引荐流量 *(→ adgine-geo-content)*|
186
+
187
+ **⚠️ Output rule:** Do NOT write skill names (e.g. `adgine-geo-xxx`) in user-facing suggestions. Each suggestion must be phrased as a natural-language prompt the user can copy and send directly to the agent.
188
+
189
+ > 💡 **建议下一步:**
190
+ > 1. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
191
+ > 2. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ """Shared HTTP client utilities for geo-skills scripts.
3
+
4
+ Used by all scripts in this skill. Uses Python stdlib only — no external dependencies.
5
+
6
+ This file is the canonical source. It must be copied verbatim into every
7
+ adgine-geo-* skill folder's scripts/ directory. The sync linter at
8
+ scripts/sync-skills/sync-skills.sh (in the GEOAgent repo) flags any drift.
9
+ """
10
+ import os
11
+ import sys
12
+ import json
13
+ import time
14
+ import atexit
15
+ import unicodedata
16
+ import urllib.request as _req
17
+ import urllib.error as _uerr
18
+ import urllib.parse as _up
19
+
20
+ def _load_dot_env():
21
+ """Load .env from the repo root (adgine-geo-skills/) into os.environ.
22
+
23
+ Resolved relative to this file's location, so it works regardless of the
24
+ agent's working directory (e.g. Hermes, OpenClaw, or any other runtime).
25
+ Existing environment variables are never overwritten.
26
+ """
27
+ if os.environ.get("GEO_API_KEY"):
28
+ return
29
+ # _client.py is at <repo_root>/adgine-geo-<skill>/scripts/_client.py
30
+ repo_root = os.path.dirname(
31
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
32
+ )
33
+ env_path = os.path.join(repo_root, ".env")
34
+ if not os.path.isfile(env_path):
35
+ return
36
+ with open(env_path) as _f:
37
+ for _line in _f:
38
+ _line = _line.strip()
39
+ if not _line or _line.startswith("#") or "=" not in _line:
40
+ continue
41
+ _k, _, _v = _line.partition("=")
42
+ _k = _k.strip()
43
+ _v = _v.strip()
44
+ if _k and _k not in os.environ:
45
+ os.environ[_k] = _v
46
+
47
+
48
+ _load_dot_env()
49
+
50
+ _VERSION_UNCHECKED = object()
51
+ _version_state = _VERSION_UNCHECKED
52
+
53
+
54
+ def _repo_root():
55
+ return os.path.dirname(
56
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
57
+ )
58
+
59
+
60
+ def _load_check_version_module():
61
+ import importlib.util
62
+ path = os.path.join(_repo_root(), "scripts", "check_version.py")
63
+ if not os.path.isfile(path):
64
+ return None
65
+ spec = importlib.util.spec_from_file_location("geo_check_version", path)
66
+ mod = importlib.util.module_from_spec(spec)
67
+ spec.loader.exec_module(mod)
68
+ return mod
69
+
70
+
71
+ def _load_version_state():
72
+ global _version_state
73
+ if _version_state is not _VERSION_UNCHECKED:
74
+ return _version_state
75
+ _version_state = None
76
+ try:
77
+ mod = _load_check_version_module()
78
+ if mod is not None:
79
+ _version_state = mod.get_state()
80
+ except Exception:
81
+ pass
82
+ return _version_state
83
+
84
+
85
+ def _print_version_notice():
86
+ """Emit machine + human hints at import if a newer version exists."""
87
+ state = _load_version_state()
88
+ if not state or not state.get("update_available"):
89
+ return
90
+ cur, lat = state["current"], state["latest"]
91
+ try:
92
+ mod = _load_check_version_module()
93
+ if mod is not None:
94
+ inline = mod.format_user_inline(state)
95
+ if inline:
96
+ sys.stdout.write(inline + "\n\n")
97
+ sys.stdout.flush()
98
+ except Exception:
99
+ pass
100
+ if state.get("install_type") == "git":
101
+ msg = (f"adgine-geo-skills {lat} available (current {cur}). "
102
+ "Tell me: 请帮我更新 adgine-geo-skills 到最新版本")
103
+ else:
104
+ msg = (f"adgine-geo-skills {lat} available (current {cur}). "
105
+ f"Download: {state.get('release_url', '')}")
106
+ notice = {"update": {"current": cur, "latest": lat, "message": msg}}
107
+ try:
108
+ line = f"_notice: {json.dumps(notice, ensure_ascii=False)}\n\n"
109
+ sys.stdout.write(line)
110
+ sys.stdout.flush()
111
+ except Exception:
112
+ pass
113
+
114
+
115
+ def _print_version_footer():
116
+ """Emit human-readable footer at process exit (WorkBuddy-compatible)."""
117
+ state = _load_version_state()
118
+ if not state or not state.get("update_available"):
119
+ return
120
+ try:
121
+ mod = _load_check_version_module()
122
+ if mod is None:
123
+ return
124
+ footer = mod.format_user_footer(state)
125
+ if footer:
126
+ sys.stdout.write("\n" + footer + "\n")
127
+ sys.stdout.flush()
128
+ except Exception:
129
+ pass
130
+
131
+
132
+ _print_version_notice()
133
+ atexit.register(_print_version_footer)
134
+
135
+
136
+ def get_api_config():
137
+ """Read GEO_API_KEY and GEO_API_BASE_URL from environment.
138
+
139
+ Exits with a helpful message if GEO_API_KEY is not set.
140
+ Returns (key, base_url).
141
+ """
142
+ key = os.environ.get("GEO_API_KEY", "")
143
+ base = os.environ.get("GEO_API_BASE_URL", "https://platform.adgine.ai").rstrip("/")
144
+ if not key:
145
+ repo_root = os.path.dirname(
146
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
147
+ )
148
+ env_path = os.path.join(repo_root, ".env")
149
+ example_path = os.path.join(repo_root, ".env.example")
150
+ setup_path = os.path.join(repo_root, "setup.py")
151
+ print("ERROR: GEO_API_KEY is not set.")
152
+ print(f" Expected location: {env_path}")
153
+ print()
154
+ print(" One-shot fix (recommended):")
155
+ print(f" python3 {setup_path} <YOUR_KEY>")
156
+ print()
157
+ print(" Or set it manually:")
158
+ if not os.path.isfile(env_path):
159
+ print(f" cp {example_path} {env_path}")
160
+ print(f" edit {env_path} and set GEO_API_KEY=geo_sk_live_xxx")
161
+ print()
162
+ print(" Get your key at: https://platform.adgine.ai")
163
+ print(" The .env file is gitignored — your key stays local and private.")
164
+ print()
165
+ print(" NOTE: adgine-geo-site-audit does NOT require an API key.")
166
+ print(" Run directly: python3 adgine-geo-site-audit/scripts/geo_collect.py <url>")
167
+ sys.exit(1)
168
+ return key, base
169
+
170
+
171
+ def get_project_id(arg_value=None):
172
+ """Resolve project ID from argument or GEO_PROJECT_ID env var.
173
+
174
+ Exits with instructions if neither is set.
175
+ """
176
+ pid = arg_value or os.environ.get("GEO_PROJECT_ID", "")
177
+ if not pid:
178
+ print("ERROR: Project ID is required but not set.")
179
+ print(" Option 1: export GEO_PROJECT_ID=<project-id>")
180
+ print(" Option 2: pass --project-id <id> to this script")
181
+ print(" Run list_projects.py from the adgine-geo-projects skill to see your available projects.")
182
+ sys.exit(1)
183
+ return pid
184
+
185
+
186
+ def _do_request(method, url, key, body=None, timeout=30):
187
+ """Execute an HTTP request and return parsed JSON.
188
+
189
+ Exits with an error message on HTTP errors or network failures.
190
+ """
191
+ data = json.dumps(body).encode("utf-8") if body is not None else None
192
+ headers = {
193
+ "Authorization": f"Bearer {key}",
194
+ "Content-Type": "application/json",
195
+ "Accept": "application/json",
196
+ "User-Agent": "geo-skills/1.0",
197
+ }
198
+ request = _req.Request(url, data=data, headers=headers, method=method)
199
+ try:
200
+ with _req.urlopen(request, timeout=timeout) as resp:
201
+ raw = resp.read().decode("utf-8")
202
+ return json.loads(raw) if raw else {}
203
+ except _uerr.HTTPError as e:
204
+ raw = e.read().decode("utf-8")[:400]
205
+ try:
206
+ err_data = json.loads(raw)
207
+ msg = err_data.get("message") or err_data.get("detail") or raw
208
+ except Exception:
209
+ msg = raw
210
+ if e.code == 401:
211
+ print("ERROR: Unauthorized — API key is invalid or revoked.")
212
+ print(" Generate a new key: https://platform.adgine.ai")
213
+ elif e.code == 403:
214
+ print("ERROR: Forbidden — insufficient permissions for this operation.")
215
+ elif e.code == 404:
216
+ print(f"ERROR: Not found — {msg}")
217
+ else:
218
+ print(f"ERROR: HTTP {e.code} — {msg}")
219
+ sys.exit(1)
220
+ except Exception as e:
221
+ print(f"ERROR: Request failed — {e}")
222
+ sys.exit(1)
223
+
224
+
225
+ def api_get(path, key, base, params=None, timeout=30):
226
+ url = f"{base}{path}"
227
+ if params:
228
+ clean = {k: str(v) for k, v in params.items() if v is not None}
229
+ if clean:
230
+ url += "?" + _up.urlencode(clean)
231
+ return _do_request("GET", url, key, timeout=timeout)
232
+
233
+
234
+ def api_post(path, key, base, body=None, timeout=30):
235
+ return _do_request("POST", f"{base}{path}", key, body, timeout=timeout)
236
+
237
+
238
+ def api_patch(path, key, base, body=None, timeout=30):
239
+ return _do_request("PATCH", f"{base}{path}", key, body, timeout=timeout)
240
+
241
+
242
+ def api_put(path, key, base, body=None, timeout=30):
243
+ return _do_request("PUT", f"{base}{path}", key, body, timeout=timeout)
244
+
245
+
246
+ def api_delete(path, key, base, timeout=30):
247
+ return _do_request("DELETE", f"{base}{path}", key, timeout=timeout)
248
+
249
+
250
+ def extract_data(result):
251
+ """Extract the .data field from a standard geo-api envelope response.
252
+
253
+ On success the API returns {"code": 0, "data": <payload>, "message": "ok"}.
254
+ On non-zero code, this prints the message and exits(1).
255
+ """
256
+ if not isinstance(result, dict):
257
+ return result
258
+ if "code" in result and result.get("code") not in (0, None):
259
+ msg = result.get("message") or result.get("detail") or "request failed"
260
+ print(f"ERROR: API returned code={result.get('code')} — {msg}")
261
+ sys.exit(1)
262
+ return result.get("data", result)
263
+
264
+
265
+ def poll_job(status_path, key, base, interval=3, max_wait=300,
266
+ terminal=("completed", "failed", "done", "success", "error")):
267
+ """Poll a job/task endpoint until it reaches a terminal state.
268
+
269
+ Prints an inline spinner with the current phase. Returns the final job dict.
270
+ """
271
+ elapsed = 0
272
+ frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
273
+ idx = 0
274
+ last_job = {}
275
+ while elapsed < max_wait:
276
+ result = api_get(status_path, key, base)
277
+ last_job = extract_data(result) or {}
278
+ status = (last_job.get("status") or "").lower()
279
+ phase = last_job.get("current_phase") or last_job.get("phase") or status or "pending"
280
+ print(f"\r {frames[idx % len(frames)]} {phase}... ({elapsed}s)", end="", flush=True)
281
+ idx += 1
282
+ if status in terminal:
283
+ print() # newline after spinner
284
+ return last_job
285
+ time.sleep(interval)
286
+ elapsed += interval
287
+ print()
288
+ print(f"WARNING: Job is still running after {max_wait}s. Check status manually.")
289
+ return last_job
290
+
291
+
292
+ def print_json(data):
293
+ """Print data as formatted JSON (UTF-8 safe)."""
294
+ print(json.dumps(data, ensure_ascii=False, indent=2))
295
+
296
+
297
+ def fmt_change(value):
298
+ """Format a numeric change indicator per CONVENTIONS.md §4.
299
+
300
+ Returns: '+N' / '-N' / '0' / '--' (None becomes '--').
301
+ """
302
+ if value is None:
303
+ return "--"
304
+ try:
305
+ n = float(value)
306
+ except (TypeError, ValueError):
307
+ return "--"
308
+ if n == 0:
309
+ return "0"
310
+ sign = "+" if n > 0 else ""
311
+ if n == int(n):
312
+ return f"{sign}{int(n):,}"
313
+ return f"{sign}{n:,.1f}"
314
+
315
+
316
+ def truncate(text, n=60, ellipsis="…"):
317
+ """Truncate text to n characters, adding ellipsis if cut. ASCII-safe for tables."""
318
+ if text is None:
319
+ return "--"
320
+ s = str(text)
321
+ if len(s) <= n:
322
+ return s
323
+ return s[: max(0, n - len(ellipsis))] + ellipsis
324
+
325
+ def display_width(s):
326
+ """Return the display width of a string (CJK chars count as 2 columns)."""
327
+ w = 0
328
+ for c in str(s):
329
+ eaw = unicodedata.east_asian_width(c)
330
+ w += 2 if eaw in ('W', 'F') else 1
331
+ return w
332
+
333
+
334
+ def pad(s, width, align='left'):
335
+ """Pad string to display width for CJK-aware table alignment.
336
+ Replaces f'{s:<N}' (align='left') and f'{s:>N}' (align='right').
337
+ """
338
+ s = str(s)
339
+ dw = display_width(s)
340
+ spaces = max(0, width - dw) * ' '
341
+ return spaces + s if align == 'right' else s + spaces
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env python3
2
+ """Fetch dashboard analytics overview for a GEO project.
3
+
4
+ Usage:
5
+ python3 scripts/get_overview.py --project-id <id> [--period 30d] [--json]
6
+ """
7
+ import sys
8
+ import os
9
+ import argparse
10
+
11
+ sys.path.insert(0, os.path.dirname(__file__))
12
+ from _client import get_api_config, get_project_id, api_get, extract_data, print_json
13
+
14
+ parser = argparse.ArgumentParser(description="Fetch GEO analytics dashboard overview")
15
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
16
+ parser.add_argument("--period", default="30d", choices=["7d", "14d", "30d", "90d"],
17
+ help="Time period (default: 30d)")
18
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
19
+ args = parser.parse_args()
20
+
21
+ key, base = get_api_config()
22
+ pid = get_project_id(args.project_id)
23
+
24
+ result = api_get(
25
+ f"/api/projects/{pid}/dashboard/overview",
26
+ key, base,
27
+ params={"period": args.period}
28
+ )
29
+ data = extract_data(result)
30
+
31
+ if args.json:
32
+ print_json(data)
33
+ sys.exit(0)
34
+
35
+ # ── Human-readable output ─────────────────────────────────────────────────────
36
+ period = data.get("period", args.period)
37
+ dr = data.get("date_range", {})
38
+ integs = data.get("integrations", {})
39
+ search = data.get("search")
40
+ traffic = data.get("traffic")
41
+ ai_impact = data.get("ai_impact")
42
+ infra = data.get("infrastructure")
43
+
44
+ print(f"Analytics Overview — {dr.get('start','?')} to {dr.get('end','?')} ({period})")
45
+ print(f"Project: {pid}")
46
+ print()
47
+
48
+ # ── Integrations status ───────────────────────────────────────────────────────
49
+ statuses = []
50
+ for name, key_name in [("GSC", "gsc"), ("GA4", "ga4"), ("Cloudflare", "cloudflare")]:
51
+ statuses.append(f"{name} {'✓' if integs.get(key_name) else '✗'}")
52
+ print("Integrations: " + " | ".join(statuses))
53
+ print()
54
+
55
+ def _metric(obj, key, default=0):
56
+ """Extract .value from a MetricWithChange field, fallback to scalar."""
57
+ v = obj.get(key, {})
58
+ if isinstance(v, dict):
59
+ return v.get("value", default)
60
+ return v if v is not None else default
61
+
62
+ def _change(obj, key):
63
+ """Extract .change from a MetricWithChange field (may be None)."""
64
+ v = obj.get(key, {})
65
+ if isinstance(v, dict):
66
+ return v.get("change")
67
+ return None
68
+
69
+ def _change_str(change):
70
+ if change is None:
71
+ return ""
72
+ return f" ({'+' if change >= 0 else ''}{change:,.0f} vs prev period)"
73
+
74
+ # ── Search (GSC) ──────────────────────────────────────────────────────────────
75
+ if search:
76
+ print("── Search Performance (GSC) ─────────────────────────────────────────")
77
+ clicks = _metric(search, "clicks")
78
+ impressions = _metric(search, "impressions")
79
+ ctr = _metric(search, "avg_ctr")
80
+ position = _metric(search, "avg_position")
81
+ print(f" Clicks : {clicks:,.0f}{_change_str(_change(search, 'clicks'))}")
82
+ print(f" Impressions : {impressions:,.0f}{_change_str(_change(search, 'impressions'))}")
83
+ print(f" Avg CTR : {ctr:.2%}")
84
+ print(f" Avg Position: {position:.1f}")
85
+ top_q = search.get("top_queries", [])
86
+ if top_q:
87
+ print(f"\n Top queries:")
88
+ for q in top_q[:5]:
89
+ print(f" {q.get('name',''):<40} {q.get('value',0):>8,.0f} clicks")
90
+ else:
91
+ print("── Search Performance (GSC) ─────────────────────────────────────────")
92
+ print(" Not connected. Connect GSC at: https://platform.adgine.ai")
93
+ print()
94
+
95
+ # ── Traffic (GA4) ─────────────────────────────────────────────────────────────
96
+ if traffic:
97
+ print("── Website Traffic (GA4) ────────────────────────────────────────────")
98
+ sessions = _metric(traffic, "sessions")
99
+ users = _metric(traffic, "active_users")
100
+ print(f" Sessions : {sessions:,.0f}{_change_str(_change(traffic, 'sessions'))}")
101
+ print(f" Active users: {users:,.0f}{_change_str(_change(traffic, 'active_users'))}")
102
+ top_src = traffic.get("top_sources", [])
103
+ if top_src:
104
+ print(f"\n Top sources:")
105
+ for s in top_src[:5]:
106
+ print(f" {s.get('name',''):<30} {s.get('value',0):>8,.0f} sessions")
107
+ else:
108
+ print("── Website Traffic (GA4) ────────────────────────────────────────────")
109
+ print(" Not connected. Connect GA4 at: https://platform.adgine.ai")
110
+ print()
111
+
112
+ # ── AI Impact ─────────────────────────────────────────────────────────────────
113
+ if ai_impact:
114
+ print("── AI Impact ────────────────────────────────────────────────────────")
115
+ ai_sess = _metric(ai_impact, "ai_referral_sessions")
116
+ ai_reqs = _metric(ai_impact, "ai_crawler_requests")
117
+ print(f" AI referral sessions : {ai_sess:,.0f}{_change_str(_change(ai_impact, 'ai_referral_sessions'))}")
118
+ print(f" AI crawler requests : {ai_reqs:,.0f}{_change_str(_change(ai_impact, 'ai_crawler_requests'))}")
119
+ top_bots = ai_impact.get("top_ai_bots", [])
120
+ if top_bots:
121
+ print(f"\n Top AI crawlers:")
122
+ for b in top_bots[:3]:
123
+ print(f" {b.get('name',''):<30} {b.get('value',0):>10,.0f} requests")
124
+ top_src = ai_impact.get("top_ai_sources", [])
125
+ if top_src:
126
+ print(f"\n Top AI referral sources:")
127
+ for s in top_src[:3]:
128
+ print(f" {s.get('name',''):<30} {s.get('value',0):>8,.0f} sessions")
129
+ print()