@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,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adgine/geo-content
|
|
3
|
+
description: Generates AI-optimized article outlines and full articles for GEO content strategy, manages the content library, and inspects/retries the underlying outline and article generation jobs. Supports generating title suggestions, producing a keyword-optimized outline (async), writing a complete article from an approved outline (async), listing/editing/deleting content items (with optional publish-status filter), and managing the workflow / outline / article job lists with detail inspection and retry. Use when the user wants to create an article (写文章 / generate article), generate an outline (生成大纲 / outline), write GEO-optimized content, check content status, review generated articles, filter by publish status (已发布/未发布, published/unpublished), manage their content pipeline, check job progress (任务进度 / job status), or retry a failed content job (重试失败任务 / retry job). Intent synonyms: article generation, outline generation, content jobs, content workflow, retry failed job, 内容生成任务, 已发布文章, 未发布文章.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GEO Content
|
|
7
|
+
|
|
8
|
+
## Output rules — IDs (apply to every reply)
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
- Index numbers restart from 1 in each new list — they are not stable across calls.
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
2. **Single-item operations — prefer a human name over an ID.**
|
|
17
|
+
- ✅ *"Project **Poki vs Competitors** deleted."*
|
|
18
|
+
- ✅ *"Topic **Brand mentions in 2024** updated — name → 'Brand mentions 2025'."*
|
|
19
|
+
- ❌ *"Project `a4305b57-1c79-4cec-a17c-16eb1d959ea6` deleted."*
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Step 1: Make sure GEO_API_KEY is configured
|
|
29
|
+
|
|
30
|
+
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).
|
|
31
|
+
|
|
32
|
+
- ✅ Key already in `<skills-root>/.env` → proceed.
|
|
33
|
+
- ❌ 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.
|
|
34
|
+
|
|
35
|
+
> ⚠️ **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.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Project selection
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export GEO_PROJECT_ID=<project-id> # session shortcut — resets when terminal closes
|
|
42
|
+
# Run python3 scripts/list_projects.py from adgine-geo-projects skill to find your IDs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Content lifecycle
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Prompts selected → generate-titles → generate-outline (async) → approve outline → generate-article (async) → article ready → publish to WordPress
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Content items progress through statuses: `draft` → `outline` → `article`.
|
|
52
|
+
Each item also has a `publish_status`:
|
|
53
|
+
- `unpublished` — article not yet pushed to WordPress (default after generation)
|
|
54
|
+
- `published` — article successfully published to WordPress
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
### List content items
|
|
61
|
+
```bash
|
|
62
|
+
python3 scripts/list_content.py [--project-id <id>] [--status draft|outline|article] \
|
|
63
|
+
[--publish-status unpublished|published] [--topic-id <id>] [--page 1] [--limit 20] [--json]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Suggest article titles (quick, sync)
|
|
67
|
+
```bash
|
|
68
|
+
python3 scripts/generate_titles.py --topic-id <tid> --prompt-ids <id1,id2,...> \
|
|
69
|
+
[--project-id <id>]
|
|
70
|
+
```
|
|
71
|
+
Returns 5–10 AI-suggested article titles for the given topic and prompts.
|
|
72
|
+
|
|
73
|
+
### Generate article outline (async, ~10–15 min)
|
|
74
|
+
```bash
|
|
75
|
+
python3 scripts/generate_outline.py --topic-id <tid> --prompt-ids <id1,id2,...> \
|
|
76
|
+
[--project-id <id>] [--title "Your chosen title"] \
|
|
77
|
+
[--reference-urls "https://url1,https://url2"] \
|
|
78
|
+
[--instructions "Additional guidance for the AI"]
|
|
79
|
+
```
|
|
80
|
+
Creates a content item with status `outline` once complete.
|
|
81
|
+
|
|
82
|
+
> ⏳ **Expected duration: 10–15 minutes.** The script polls automatically (interval 10 s, timeout 20 min). Do NOT cancel early — this is a large LLM job.
|
|
83
|
+
|
|
84
|
+
### Generate full article from outline (async, ~5–10 min)
|
|
85
|
+
```bash
|
|
86
|
+
python3 scripts/generate_article.py --content-id <cid> [--project-id <id>]
|
|
87
|
+
```
|
|
88
|
+
Content item must have status `outline`. Produces the full article.
|
|
89
|
+
|
|
90
|
+
> ⏳ **Expected duration: 5–10 minutes.** The script polls automatically (interval 10 s, timeout 15 min). Do NOT cancel early.
|
|
91
|
+
|
|
92
|
+
### Inspect & retry generation jobs
|
|
93
|
+
|
|
94
|
+
The content pipeline runs three job types: the combined **workflow** job, the
|
|
95
|
+
**outline** job, and the **article** job. They each have their own list/detail
|
|
96
|
+
endpoints. Use `manage_jobs.py` to inspect or retry them.
|
|
97
|
+
|
|
98
|
+
List jobs:
|
|
99
|
+
```bash
|
|
100
|
+
python3 scripts/manage_jobs.py list-workflow [--page 1] [--limit 20] [--json]
|
|
101
|
+
python3 scripts/manage_jobs.py list-outline [--page 1] [--limit 20] [--json]
|
|
102
|
+
python3 scripts/manage_jobs.py list-article [--page 1] [--limit 20] [--json]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Get job detail:
|
|
106
|
+
```bash
|
|
107
|
+
python3 scripts/manage_jobs.py get-workflow --job-id <id>
|
|
108
|
+
python3 scripts/manage_jobs.py get-outline --job-id <id>
|
|
109
|
+
python3 scripts/manage_jobs.py get-article --job-id <id>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Retry a failed workflow job:
|
|
113
|
+
```bash
|
|
114
|
+
python3 scripts/manage_jobs.py retry --job-id <id>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Workflow
|
|
118
|
+
|
|
119
|
+
See `WORKFLOW.md` for the detailed step-by-step content creation flow.
|
|
120
|
+
|
|
121
|
+
## Output Format
|
|
122
|
+
|
|
123
|
+
> ⚠️ **CRITICAL — Table cell content rule (must follow exactly):**
|
|
124
|
+
> Tables use fenced code blocks with box-drawing borders. They only align correctly when **every cell contains ASCII characters exclusively**.
|
|
125
|
+
> - **NEVER** put emoji inside table cells. They are 2 display units wide but count as 1 character, permanently misaligning all following columns.
|
|
126
|
+
> - Emoji go ONLY on the label line **above** the ` ``` ` fence.
|
|
127
|
+
> - Status in cells: `Draft` / `Outline` / `Article` (NOT 📝/📋/✅)
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### When listing content (`list_content.py`)
|
|
132
|
+
|
|
133
|
+
> 📄 **Content Library** — (Page 1 / N)
|
|
134
|
+
|
|
135
|
+
📚 Items
|
|
136
|
+
```
|
|
137
|
+
┌────┬──────────┬───────┬────────────┬──────────────────────────────────────┐
|
|
138
|
+
│ # │ Status │ Words │ Pub.Status │ Title │
|
|
139
|
+
├────┼──────────┼───────┼────────────┼──────────────────────────────────────┤
|
|
140
|
+
│ 1 │ Draft │ — │ Unpub │ How to Improve Your SEO in 2025 │
|
|
141
|
+
│ 2 │ Outline │ — │ Unpub │ Top 10 GEO Strategies for SaaS │
|
|
142
|
+
│ 3 │ Article │ 1,200 │ Published │ What is Generative Engine Optimi... │
|
|
143
|
+
│ 4 │ Article │ 850 │ Unpub │ AI Search Trends for Marketers │
|
|
144
|
+
└────┴──────────┴───────┴────────────┴──────────────────────────────────────┘
|
|
145
|
+
```
|
|
146
|
+
- `Status`: content generation stage (`Draft` / `Outline` / `Article`)
|
|
147
|
+
- `Words`: word count (`—` if not available)
|
|
148
|
+
- `Pub.Status`: WordPress publish status (`Unpub` / `Published` / `—` if unknown)
|
|
149
|
+
- Truncate long titles to ~36 chars with `...`.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### When suggesting titles (`generate_titles.py`)
|
|
154
|
+
|
|
155
|
+
> 💡 **Suggested Titles** — pick one to generate an outline:
|
|
156
|
+
|
|
157
|
+
💡 Title Options
|
|
158
|
+
```
|
|
159
|
+
┌────┬──────────────────────────────────────────────────────┐
|
|
160
|
+
│ # │ Title │
|
|
161
|
+
├────┼──────────────────────────────────────────────────────┤
|
|
162
|
+
│ 1 │ How to Dominate AI Search in 2025 │
|
|
163
|
+
│ 2 │ The Complete Guide to GEO for SaaS Companies │
|
|
164
|
+
│ 3 │ Why Traditional SEO Is Not Enough Anymore │
|
|
165
|
+
└────┴──────────────────────────────────────────────────────┘
|
|
166
|
+
```
|
|
167
|
+
*Which title would you like to use for the outline?*
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### When generating an outline (`generate_outline.py`)
|
|
172
|
+
|
|
173
|
+
- Progress: `⏳ **Generating outline…** (~10–15 min)`
|
|
174
|
+
- On completion, show the outline as a nested numbered list, then:
|
|
175
|
+
|
|
176
|
+
✅ Outline Ready
|
|
177
|
+
```
|
|
178
|
+
┌────────────┬────────────────────────────────────┐
|
|
179
|
+
│ Content ID │ <id> │
|
|
180
|
+
│ Sections │ 6 │
|
|
181
|
+
│ Next step │ generate_article.py -c <id> │
|
|
182
|
+
└────────────┴────────────────────────────────────┘
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### When generating an article (`generate_article.py`)
|
|
188
|
+
|
|
189
|
+
- Progress: `⏳ **Writing article…** (~5–10 min)`
|
|
190
|
+
- On completion:
|
|
191
|
+
|
|
192
|
+
✅ Article Complete
|
|
193
|
+
```
|
|
194
|
+
┌────────────────┬────────────────────────────────────┐
|
|
195
|
+
│ Title │ <title> │
|
|
196
|
+
│ Content ID │ <id> │
|
|
197
|
+
│ Word Count │ ~1,200 │
|
|
198
|
+
│ Status │ Article │
|
|
199
|
+
│ Publish Status │ Unpub │
|
|
200
|
+
└────────────────┴────────────────────────────────────┘
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Then the **full article text** with `##`/`###` headings and bullet lists — never wrap article body in a code fence.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Post-task recommendations
|
|
208
|
+
|
|
209
|
+
After content operations, guide the user to the next stage:
|
|
210
|
+
|
|
211
|
+
| You just… | → use skill (agent-internal) |
|
|
212
|
+
|---|---|
|
|
213
|
+
| Generated an outline | Review outline, then 生成完整文章 *(→ adgine-geo-content)*|
|
|
214
|
+
| Generated a full article | 发布文章到 WordPress *(→ adgine-geo-wordpress)*|
|
|
215
|
+
| Listed content items | 选择一篇生成 outline 或 article *(→ adgine-geo-content)*|
|
|
216
|
+
| Checked job status / retried | 继续管理内容管线 *(→ adgine-geo-content)*|
|
|
217
|
+
| Article published-ready | 检查文章页面的 AI 优化健康度 *(→ adgine-geo-performance)*|
|
|
218
|
+
| Multiple articles ready | 对文章主题运行引用测试,验证效果 *(→ adgine-geo-citation)*|
|
|
219
|
+
|
|
220
|
+
**⚠️ 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.
|
|
221
|
+
|
|
222
|
+
> 💡 **建议下一步:**
|
|
223
|
+
> 1. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
|
|
224
|
+
> 2. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Content Generation Workflow
|
|
2
|
+
|
|
3
|
+
This guide walks through the complete GEO content creation pipeline, from prompts to a published-ready article.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **API key configured** — See [SETUP.md](../SETUP.md)
|
|
10
|
+
- **Prompts** — You need prompt IDs. Generate them first with the **adgine-geo-topics** skill.
|
|
11
|
+
- **Brand profile** — For best quality, ensure your brand profile is complete via the **adgine-geo-brand** skill.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Step-by-step
|
|
16
|
+
|
|
17
|
+
### Step 1 — Get topic and prompt IDs
|
|
18
|
+
|
|
19
|
+
Use the **adgine-geo-topics** skill to list topics and prompts:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# From adgine-geo-topics/ skill directory:
|
|
23
|
+
python3 scripts/manage_topics.py list
|
|
24
|
+
python3 scripts/manage_prompts.py list --topic-id <topic-id>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Note: gather the `topic_id` and the prompt `id` values you want to use.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### Step 2 — (Optional) Suggest article titles
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 scripts/generate_titles.py \
|
|
35
|
+
--topic-id <tid> \
|
|
36
|
+
--prompt-ids "prompt-id-1,prompt-id-2,prompt-id-3"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Returns AI-suggested titles. Pick one or write your own.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### Step 3 — Generate article outline
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 scripts/generate_outline.py \
|
|
47
|
+
--topic-id <tid> \
|
|
48
|
+
--prompt-ids "prompt-id-1,prompt-id-2,prompt-id-3" \
|
|
49
|
+
--title "Your Chosen Article Title" \
|
|
50
|
+
--instructions "Emphasize cost-saving benefits, target CMO audience"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This creates a content item (status: `outline`) and polls until complete (~30–90 s).
|
|
54
|
+
The script prints the outline on completion and shows the `content_id`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Step 4 — Review the outline
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 scripts/list_content.py --status outline
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Review the generated outline. If needed, you can edit it:
|
|
65
|
+
- Contact your geo-platform dashboard to edit outline content
|
|
66
|
+
- Or proceed to article generation using the existing outline
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Step 5 — Generate the full article
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 scripts/generate_article.py --content-id <content-id>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Polls until the article is complete (~60–180 s). Prints the article on completion.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### Step 6 — Review the article
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python3 scripts/list_content.py --status article
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The full article is ready in `full_content` (Markdown format). It includes:
|
|
87
|
+
- Article body
|
|
88
|
+
- FAQ section
|
|
89
|
+
- Citation opportunities analysis
|
|
90
|
+
- SEO metadata (title, description, slug)
|
|
91
|
+
- Word count
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Tips
|
|
96
|
+
|
|
97
|
+
- **Multiple articles**: Run steps 3–5 in parallel with different topics/prompts.
|
|
98
|
+
- **Re-generation**: Delete a content item with status `draft` or `outline` and start over.
|
|
99
|
+
- **Custom instructions**: Use `--instructions` in `generate_outline.py` to guide tone, audience, and structure.
|
|
100
|
+
- **Reference URLs**: Pass competitor or reference URLs via `--reference-urls` for better-informed outlines.
|
|
@@ -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
|