@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,279 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adgine/geo-projects
|
|
3
|
+
description: Lists, creates, retrieves, updates, and deletes GEO platform website projects, manages a project's competitor list (add / list / remove), and verifies API authentication. Use when the user needs to see their website projects, select a project to work on, create a new website project, check which project is active, manage competitors (竞争对手 / 竞品 / competitors / competitor brands), or when any other GEO operation needs to identify a project ID before proceeding. Also use this skill first to verify authentication is configured correctly. Also load this skill whenever the user provides, pastes, sets, or wants to configure a GEO API key. Intent synonyms: 项目列表, project list, 创建项目, new project, 项目详情, project details, 竞品分析, competitor analysis, who are my competitors, 配置key, 设置key, 设置API key, 我的key是, set api key, configure api key, install api key, GEO_API_KEY, geo_sk_live_, api key setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GEO Projects
|
|
7
|
+
|
|
8
|
+
## 版本检测(加载本 skill 时执行)
|
|
9
|
+
|
|
10
|
+
作为最常用的入口技能,加载本 skill 处理 GEO 相关请求时,**先单独运行**(本轮第一个 bash 命令):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python3 <skills-root>/scripts/check_version.py --human
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`<skills-root>` 是本仓库根目录(本文件向上两级)。规则见根目录 `SKILL.md`
|
|
17
|
+
「版本检测」一节:有输出则在回复末尾追加该句(WorkBuddy 不得省略)。
|
|
18
|
+
|
|
19
|
+
## `_notice` 处理规则
|
|
20
|
+
|
|
21
|
+
本技能(及所有 adgine-geo-* 技能)的脚本运行时,若有新版本会在工具输出
|
|
22
|
+
(stdout)中打印一行 `_notice: {"update": {...}}`。看到它就在完成用户请求后,
|
|
23
|
+
于回复末尾追加更新提示(模板见根目录 `SKILL.md` 的「版本更新提示」一节)。
|
|
24
|
+
主动检测与 `_notice` 任一发现更新都应提示;均未发现则不提版本相关内容。
|
|
25
|
+
|
|
26
|
+
工具输出末尾若出现 `---` + `⚠️ adgine-geo-skills v... 已发布` 页脚,**必须原样**
|
|
27
|
+
展示给用户(WorkBuddy 等改写表格时也不得省略此段)。
|
|
28
|
+
|
|
29
|
+
## Step 0: First-time API key setup (when user provides a key)
|
|
30
|
+
|
|
31
|
+
If the user provides a GEO API key in chat (e.g. *"my key is geo_sk_live_xxx"* / *"帮我配置一下 key"* / *"set my api key to ..."*), this is the canonical procedure for the entire skills repo. **Do not invent your own path — run the helper.**
|
|
32
|
+
|
|
33
|
+
### 0-1. Locate the skills repo root
|
|
34
|
+
|
|
35
|
+
This `SKILL.md` lives at `<skills-root>/adgine-geo-projects/SKILL.md`. The repo root contains all `adgine-geo-*` folders, plus `setup.py`, `.env.example`, and `README.md`. Use whatever absolute path the agent already knows for this file and go up two directories — do NOT guess `~/.hermes/...` or `/usr/local/...`.
|
|
36
|
+
|
|
37
|
+
### 0-2. Run the setup helper (one command)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python3 <skills-root>/setup.py <THE_KEY_FROM_USER>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That single command:
|
|
44
|
+
- Locates the repo root itself (uses its own `__file__`), so you don't need to `cd` first.
|
|
45
|
+
- Writes `GEO_API_KEY=<key>` to `<skills-root>/.env`, creating it from `.env.example` if missing, preserving any other lines.
|
|
46
|
+
- Verifies the key against the GEO API.
|
|
47
|
+
- Exits `0` on success, non-zero on failure.
|
|
48
|
+
|
|
49
|
+
If the user is **updating** an existing key, run the same command — `setup.py` overwrites cleanly. No need to ask for confirmation.
|
|
50
|
+
|
|
51
|
+
> ⚠️ **CRITICAL — storage rules (apply every single time):**
|
|
52
|
+
> - ✅ The ONLY allowed destination is `<skills-root>/.env`.
|
|
53
|
+
> - ❌ NEVER write to `~/.hermes/.env`, `~/.hermes/config.*`, or any Hermes user-secrets directory.
|
|
54
|
+
> - ❌ NEVER write to `~/.zshrc`, `~/.bashrc`, `~/.profile`, or any shell rc file.
|
|
55
|
+
> - ❌ NEVER call `hermes config set`, `claude config`, or any agent-host secret store with this key.
|
|
56
|
+
> - ❌ NEVER hardcode the key into other scripts, SKILL.md files, or chat messages.
|
|
57
|
+
> - The `.env` file is `.gitignore`d — the key stays local and private.
|
|
58
|
+
|
|
59
|
+
### 0-3. Confirm to the user
|
|
60
|
+
|
|
61
|
+
On success (exit 0), reply with the exact path that `setup.py` printed:
|
|
62
|
+
|
|
63
|
+
> ✅ **GEO API Key 已配置** — 保存到 `<absolute path printed by setup.py>`
|
|
64
|
+
> 认证验证通过,现在可以使用所有 GEO skills 了。
|
|
65
|
+
> 下一步可以试试:*"列出我的所有项目"*
|
|
66
|
+
|
|
67
|
+
On failure (non-zero exit), do NOT retry silently. Reply:
|
|
68
|
+
|
|
69
|
+
> ❌ **Key 验证失败** — 请确认 key 是否正确,或到 [platform.adgine.ai](https://platform.adgine.ai) 重新生成后再发给我。
|
|
70
|
+
|
|
71
|
+
### 0-4. After Step 0 — no `export` needed
|
|
72
|
+
|
|
73
|
+
Every script's `_client.py` calls `_load_dot_env()` on import and automatically reads `<skills-root>/.env`. You do not need to `export GEO_API_KEY` in the shell, and the user does not need to restart any terminal.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Output rules — IDs (apply to every reply)
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
- Index numbers restart from 1 in each new list — they are not stable across calls.
|
|
83
|
+
- 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.
|
|
84
|
+
|
|
85
|
+
2. **Single-item operations — prefer a human name over an ID.**
|
|
86
|
+
- ✅ *"Project **Poki vs Competitors** deleted."*
|
|
87
|
+
- ✅ *"Topic **Brand mentions in 2024** updated — name → 'Brand mentions 2025'."*
|
|
88
|
+
- ❌ *"Project `a4305b57-1c79-4cec-a17c-16eb1d959ea6` deleted."*
|
|
89
|
+
- 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.
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Step 1: Confirm authentication is working
|
|
98
|
+
|
|
99
|
+
Scripts auto-load `GEO_API_KEY` from `<skills-root>/.env` on import — **you don't need to `export` it**. To verify the configuration is healthy:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python3 scripts/check_auth.py
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- ✅ Exits with "Authentication successful" → proceed.
|
|
106
|
+
- ❌ Exits with "GEO_API_KEY is not set" → the error message prints the exact `.env` path. Go to **Step 0** to install a key.
|
|
107
|
+
- ❌ Exits with 401 / auth failure → the key is invalid or expired. Ask the user for a new one and go to **Step 0**.
|
|
108
|
+
|
|
109
|
+
> ⚠️ **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.
|
|
110
|
+
|
|
111
|
+
## Determine project ID
|
|
112
|
+
|
|
113
|
+
One API key manages all your projects. Resolve the project for each operation in this order:
|
|
114
|
+
|
|
115
|
+
1. `--project-id` argument passed to the script
|
|
116
|
+
2. `GEO_PROJECT_ID` env var (session shortcut, if set)
|
|
117
|
+
3. User mentioned a domain or website name → run `list_projects.py` and match
|
|
118
|
+
4. Nothing known → run `list_projects.py`, show results, ask the user to pick
|
|
119
|
+
|
|
120
|
+
To avoid repeating `--project-id` within a terminal session:
|
|
121
|
+
```bash
|
|
122
|
+
export GEO_PROJECT_ID=<id-from-list> # resets when terminal closes
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Commands
|
|
126
|
+
|
|
127
|
+
### List all projects
|
|
128
|
+
```bash
|
|
129
|
+
python3 scripts/list_projects.py [--limit 20] [--json]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Get project details
|
|
133
|
+
```bash
|
|
134
|
+
python3 scripts/manage_project.py get --project-id <id>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Create a new project
|
|
138
|
+
```bash
|
|
139
|
+
python3 scripts/manage_project.py create --url https://example.com [--description "My site"]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Update a project
|
|
143
|
+
```bash
|
|
144
|
+
python3 scripts/manage_project.py update --project-id <id> [--name "New Name"] [--url https://new.com]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Delete a project
|
|
148
|
+
```bash
|
|
149
|
+
python3 scripts/manage_project.py delete --project-id <id>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Manage competitors
|
|
153
|
+
|
|
154
|
+
List the competitor brands tied to a project:
|
|
155
|
+
```bash
|
|
156
|
+
python3 scripts/manage_competitors.py list [--project-id <id>] [--json]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Add a competitor:
|
|
160
|
+
```bash
|
|
161
|
+
python3 scripts/manage_competitors.py add --name "Acme" [--domain acme.com] \
|
|
162
|
+
[--aliases "Acme Inc,Acme Corp"] [--source manual|brand_profile|ai_discovery]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Remove a competitor (DESTRUCTIVE — requires `--yes`):
|
|
166
|
+
```bash
|
|
167
|
+
python3 scripts/manage_competitors.py remove --competitor-id <id> --yes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Output format
|
|
171
|
+
|
|
172
|
+
Default: human-readable summary table.
|
|
173
|
+
Pass `--json` for raw JSON (useful for piping to other scripts).
|
|
174
|
+
|
|
175
|
+
## Output Format
|
|
176
|
+
|
|
177
|
+
> ⚠️ **CRITICAL — Table cell content rule (must follow exactly):**
|
|
178
|
+
> Tables use fenced code blocks with box-drawing borders. They only align correctly when **every cell contains ASCII characters exclusively**.
|
|
179
|
+
> - **NEVER** put emoji inside table cells. They are 2 display units wide but count as 1 character, permanently misaligning all following columns.
|
|
180
|
+
> - Emoji go ONLY on the label line **above** the ` ``` ` fence.
|
|
181
|
+
> - Integration status in cells: `Connected` / `---` (NOT ✅/❌)
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### When listing projects (`list_projects.py`)
|
|
186
|
+
|
|
187
|
+
> 📁 **Your GEO Projects** — **N** total
|
|
188
|
+
|
|
189
|
+
📋 Projects
|
|
190
|
+
```
|
|
191
|
+
┌────┬──────────────────┬─────┬─────┬────────────┐
|
|
192
|
+
│ # │ Name │ GA4 │ CF │ Created │
|
|
193
|
+
├────┼──────────────────┼─────┼─────┼────────────┤
|
|
194
|
+
│ 1 │ Example Site │ OK │ --- │ 2025-01-15 │
|
|
195
|
+
│ 2 │ My Blog │ --- │ --- │ 2025-03-22 │
|
|
196
|
+
└────┴──────────────────┴─────┴─────┴────────────┘
|
|
197
|
+
```
|
|
198
|
+
List project URLs below as clickable links:
|
|
199
|
+
- Example Site → [example.com](https://example.com)
|
|
200
|
+
- My Blog → [myblog.io](https://myblog.io)
|
|
201
|
+
|
|
202
|
+
If a selection is needed ask: *"Which project would you like to work with?"*
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### When showing project details (`manage_project.py get`)
|
|
207
|
+
|
|
208
|
+
📁 Project Details
|
|
209
|
+
```
|
|
210
|
+
┌────────────┬──────────────────────────────────────┐
|
|
211
|
+
│ Name │ Example Site │
|
|
212
|
+
│ URL │ https://example.com │
|
|
213
|
+
│ Created │ 2025-01-15 │
|
|
214
|
+
│ GA4 │ Connected │
|
|
215
|
+
│ Cloudflare │ --- │
|
|
216
|
+
└────────────┴──────────────────────────────────────┘
|
|
217
|
+
```
|
|
218
|
+
If any integration is `---`: > 🔌 Connect integrations at [platform.adgine.ai](https://platform.adgine.ai)
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### When creating a project
|
|
223
|
+
|
|
224
|
+
✅ Project Created
|
|
225
|
+
```
|
|
226
|
+
┌────────────┬──────────────────────────────────────┐
|
|
227
|
+
│ Name │ Example Site │
|
|
228
|
+
|
|
229
|
+
│ URL │ https://example.com │
|
|
230
|
+
└────────────┴──────────────────────────────────────┘
|
|
231
|
+
```
|
|
232
|
+
> **Next:** connect integrations at [platform.adgine.ai](https://platform.adgine.ai)
|
|
233
|
+
|
|
234
|
+
### When updating
|
|
235
|
+
|
|
236
|
+
> ✅ Project **<project name>** updated — **<field>** → `<new value>`
|
|
237
|
+
|
|
238
|
+
### When deleting
|
|
239
|
+
|
|
240
|
+
> 🗑️ Project **<project name>** deleted.
|
|
241
|
+
|
|
242
|
+
### After auth check (`check_auth.py`)
|
|
243
|
+
|
|
244
|
+
> ✅ **Authenticated** — key valid for `<email or user-id>`
|
|
245
|
+
|
|
246
|
+
On failure:
|
|
247
|
+
|
|
248
|
+
> ❌ **Authentication failed.** Check `GEO_API_KEY` — get a key at [platform.adgine.ai](https://platform.adgine.ai)
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Post-task recommendations
|
|
253
|
+
|
|
254
|
+
After completing project operations, always end with 2–3 actionable next steps
|
|
255
|
+
based on what the project still needs. To check current state, run:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
python3 <skills-root>/adgine-geo-dashboard/scripts/get_overview.py [--project-id <id>]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Read these indicators and suggest the earliest missing step in the GEO pipeline:
|
|
262
|
+
|
|
263
|
+
| Dashboard indicator | → use skill (agent-internal) |
|
|
264
|
+
|---|---|
|
|
265
|
+
| Topics count = 0 | 创建主题和提示词 *(→ adgine-geo-topics)*|
|
|
266
|
+
| Prompts count = 0 | 批量生成 AI 搜索提示词 *(→ adgine-geo-topics)*|
|
|
267
|
+
| Tests count = 0 | 运行引用测试 *(→ adgine-geo-citation)*|
|
|
268
|
+
| Articles count = 0 | 生成 GEO 优化文章 *(→ adgine-geo-content)*|
|
|
269
|
+
| GA4 not connected | 连接 GA4 获取流量数据 *(→ adgine-geo-integrations)*|
|
|
270
|
+
| Cloudflare not connected | 接入 Cloudflare 追踪 AI 爬虫 *(→ adgine-geo-integrations)*|
|
|
271
|
+
| Brand not generated | 生成品牌画像 *(→ adgine-geo-brand)*|
|
|
272
|
+
|
|
273
|
+
Always present suggestions as:
|
|
274
|
+
|
|
275
|
+
**⚠️ 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.
|
|
276
|
+
|
|
277
|
+
> 💡 **建议下一步:**
|
|
278
|
+
> 1. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
|
|
279
|
+
> 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,45 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify GEO_API_KEY is set and valid.
|
|
3
|
+
|
|
4
|
+
Run this once before using any other geo-skills scripts.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
11
|
+
from _client import get_api_config, api_get, extract_data
|
|
12
|
+
|
|
13
|
+
key, base = get_api_config()
|
|
14
|
+
|
|
15
|
+
if not key.startswith("geo_sk_"):
|
|
16
|
+
print(f"WARNING: Unexpected key format: {key[:14]}...")
|
|
17
|
+
print(" Expected: geo_sk_live_xxx or geo_sk_test_xxx")
|
|
18
|
+
|
|
19
|
+
# Verify by fetching projects list
|
|
20
|
+
print(f"Verifying API key...")
|
|
21
|
+
result = api_get("/api/projects", key, base, params={"limit": 1})
|
|
22
|
+
data = extract_data(result)
|
|
23
|
+
|
|
24
|
+
total = data.get("total", "?") if isinstance(data, dict) else "?"
|
|
25
|
+
|
|
26
|
+
# Also fetch current-user info
|
|
27
|
+
me_result = api_get("/api/auth/me", key, base)
|
|
28
|
+
me = extract_data(me_result) or {}
|
|
29
|
+
|
|
30
|
+
print(f"✓ Authentication successful")
|
|
31
|
+
print(f" Key prefix : {key[:18]}...")
|
|
32
|
+
print(f" API base : {base}")
|
|
33
|
+
print(f" Projects : {total} project(s) found")
|
|
34
|
+
if me:
|
|
35
|
+
who = me.get("email") or me.get("username") or me.get("name") or me.get("id")
|
|
36
|
+
if who:
|
|
37
|
+
print(f" User : {who}")
|
|
38
|
+
print()
|
|
39
|
+
|
|
40
|
+
project_id = os.environ.get("GEO_PROJECT_ID", "")
|
|
41
|
+
if project_id:
|
|
42
|
+
print(f" Active project: {project_id} (from GEO_PROJECT_ID env var)")
|
|
43
|
+
else:
|
|
44
|
+
print(" No active project set. Run list_projects.py to select one.")
|
|
45
|
+
print(" Then: export GEO_PROJECT_ID=<project-id>")
|