@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,193 @@
1
+ ---
2
+ name: adgine/geo-brand
3
+ description: Views, generates (async AI job), edits and inspects the GEO brand cognition profile for a website project — brand introduction, ideal customer profile, competitor analysis, brand perspective, author persona, voice and tone, writing guidelines — and lists/starts/inspects brand generation background jobs. Use when the user wants to build or update their AI-facing brand identity, generate brand cognition content (品牌画像 / 品牌认知 / brand profile), review existing brand positioning, refine writing style guidelines, or check the status of running/failed brand generation jobs (任务列表 / job status / 重新生成). Intent synonyms: brand profile, 品牌画像, 品牌认知, brand cognition, brand identity, generate brand, brand job status, retry brand generation.
4
+ ---
5
+
6
+ # GEO Brand
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
+ All commands require a project ID. Resolve in this order:
41
+ 1. `--project-id` argument
42
+ 2. `GEO_PROJECT_ID` env var (session shortcut)
43
+ 3. Run `python3 scripts/list_projects.py` from the **adgine-geo-projects** skill and ask the user to pick
44
+
45
+ ```bash
46
+ export GEO_PROJECT_ID=<project-id> # session shortcut — resets when terminal closes
47
+ ```
48
+
49
+ ## Commands
50
+
51
+ ### View current brand profile
52
+ ```bash
53
+ python3 scripts/get_brand.py [--project-id <id>] [--json]
54
+ ```
55
+ Shows: status (`none` / `generating` / `completed`), all brand fields.
56
+
57
+ ### Generate brand profile (AI-powered, async ~2–5 min)
58
+ ```bash
59
+ python3 scripts/generate_brand.py [--project-id <id>] [--language English] [--region US]
60
+ ```
61
+ Starts an AI generation job, polls automatically, then prints the completed profile.
62
+
63
+ > ⏳ **Expected duration: 2–5 minutes.** The script polls automatically (interval 8 s, timeout 10 min). Do NOT cancel early.
64
+
65
+ ### Update individual brand fields
66
+ ```bash
67
+ python3 scripts/update_brand.py [--project-id <id>] --field <field_name> --value "<text>"
68
+ ```
69
+
70
+ **Updatable fields:**
71
+
72
+ | Field | Description |
73
+ |---|---|
74
+ | `brand_introduction` | Core brand description for AI audiences |
75
+ | `ideal_customer` | Who you serve (demographics, needs, pain points) |
76
+ | `competitors` | Direct competitors (for context/differentiation) |
77
+ | `brand_perspective` | Unique differentiators and positioning |
78
+ | `author_persona` | The narrative voice persona |
79
+ | `voice_and_tone` | Writing style guidelines |
80
+ | `writing_rules` | Specific do's and don'ts |
81
+ | `cta_text` | Call-to-action text |
82
+ | `cta_landing_page` | CTA URL |
83
+ | `language` | Brand content language (default: English) |
84
+ | `region` | Target region (default: US) |
85
+
86
+ ### Brand generation jobs
87
+
88
+ List recent brand jobs:
89
+ ```bash
90
+ python3 scripts/list_jobs.py list [--project-id <id>] [--page 1] [--limit 20] [--json]
91
+ ```
92
+
93
+ Inspect one job in detail (status, phases, error message):
94
+ ```bash
95
+ python3 scripts/list_jobs.py get --job-id <id>
96
+ ```
97
+
98
+ Manually start a job that was created with `auto_start=false`:
99
+ ```bash
100
+ python3 scripts/list_jobs.py start --job-id <id>
101
+ ```
102
+
103
+ ## Workflow
104
+
105
+ See `WORKFLOW.md` for the recommended first-time setup sequence.
106
+
107
+ ## Output Format
108
+
109
+ > ⚠️ **CRITICAL — Table cell content rule (must follow exactly):**
110
+ > Tables use fenced code blocks with box-drawing borders. They only align correctly when **every cell contains ASCII characters exclusively**.
111
+ > - **NEVER** put emoji inside table cells — they are 2 display units wide but count as 1 character, permanently misaligning all following columns.
112
+ > - Emoji go ONLY on the label line **above** the ` ``` ` fence.
113
+ > - Status values in cells: `Completed` / `Generating` / `Not set` / `Failed` (NOT ✅/⏳/❌)
114
+
115
+ ---
116
+
117
+ ### When showing an existing brand profile (`get_brand.py`)
118
+
119
+ **1. Status table:**
120
+
121
+ 🏷️ Brand Profile
122
+ ```
123
+ ┌────────────┬──────────────────────────────────────┐
124
+ │ Status │ Completed │
125
+ │ Language │ English │
126
+ │ Region │ US │
127
+ │ CTA Text │ <cta_text> │
128
+ │ CTA URL │ <cta_landing_page> │
129
+ └────────────┴──────────────────────────────────────┘
130
+ ```
131
+ Status values in cell: `Completed` / `Generating` / `Not set` / `Failed`
132
+
133
+ **2. Content fields** — use `###` headings + full text (never truncate). Skip empty with *"not set yet"*:
134
+
135
+ ### Brand Introduction
136
+ <brand_introduction text>
137
+
138
+ ### Ideal Customer
139
+ <ideal_customer text>
140
+
141
+ ### Competitors
142
+ <competitors text>
143
+
144
+ ### Brand Perspective
145
+ <brand_perspective text>
146
+
147
+ ### Author Persona
148
+ <author_persona text>
149
+
150
+ ### Voice & Tone
151
+ <voice_and_tone text>
152
+
153
+ ### Writing Rules
154
+ <writing_rules text>
155
+
156
+ ---
157
+
158
+ ### When generating a brand profile (`generate_brand.py`)
159
+
160
+ - Initial line: `⏳ **Generating brand profile…** (~2–5 min)`
161
+ - On completion, show the status table + all content sections above.
162
+
163
+ ---
164
+
165
+ ### When updating a field (`update_brand.py`)
166
+
167
+ ✅ Field Updated
168
+ ```
169
+ ┌────────────┬──────────────────────────────────────┐
170
+ │ Field │ <field_name> │
171
+ │ New Value │ <preview, first 60 chars> │
172
+ └────────────┴──────────────────────────────────────┘
173
+ ```
174
+ Then show the full new value below the table if it was truncated.
175
+
176
+ ---
177
+
178
+ ## Post-task recommendations
179
+
180
+ After brand operations, suggest 2–3 next steps based on brand status:
181
+
182
+ | Scenario | → use skill (agent-internal) |
183
+ |---|---|
184
+ | Brand just generated / completed | 基于品牌画像创建主题和提示词 *(→ adgine-geo-topics)*|
185
+ | Brand exists (any state) | 运行引用测试,验证 AI 是否识别品牌 *(→ adgine-geo-citation)*|
186
+ | Brand fields updated | 生成 GEO 文章(会使用更新后的品牌画像) *(→ adgine-geo-content)*|
187
+ | No brand generated yet | 先生成品牌画像,这是所有 GEO 操作的基础 *(→ adgine-geo-brand)*|
188
+
189
+ **⚠️ 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.
190
+
191
+ > 💡 **建议下一步:**
192
+ > 1. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
193
+ > 2. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
@@ -0,0 +1,66 @@
1
+ # Brand Cognition Generation Workflow
2
+
3
+ Brand cognition is how AI search engines (ChatGPT, Perplexity, Gemini, etc.) understand and represent your brand. This workflow covers initial brand setup and ongoing refinement.
4
+
5
+ ---
6
+
7
+ ## First-time setup
8
+
9
+ ### Step 1 — Check current status
10
+ ```bash
11
+ python3 scripts/get_brand.py --project-id <id>
12
+ ```
13
+ If status is already `completed` and the fields look accurate, no further action needed.
14
+
15
+ ### Step 2 — Generate brand profile (if missing or outdated)
16
+ ```bash
17
+ python3 scripts/generate_brand.py --project-id <id> --language English --region US
18
+ ```
19
+ The AI scrapes your website and auto-generates all brand fields. Typically 30–90 seconds.
20
+
21
+ **What gets generated:**
22
+ - `brand_introduction` — concise, authoritative brand description
23
+ - `ideal_customer` — ICP based on your product/service
24
+ - `competitors` — detected competitor landscape
25
+ - `brand_perspective` — unique angles and differentiators
26
+ - `author_persona` — narrative voice for content
27
+ - `voice_and_tone` — communication style
28
+ - `writing_rules` — usage guidelines
29
+
30
+ ### Step 3 — Review each field
31
+ ```bash
32
+ python3 scripts/get_brand.py --project-id <id>
33
+ ```
34
+ Read through each generated field and decide what to refine.
35
+
36
+ ### Step 4 — Refine specific fields
37
+ ```bash
38
+ python3 scripts/update_brand.py --project-id <id> \
39
+ --field voice_and_tone \
40
+ --value "Professional but approachable. Rarely uses jargon. Uses data to support claims."
41
+ ```
42
+ Repeat for each field that needs adjustment.
43
+
44
+ ---
45
+
46
+ ## Re-generation triggers
47
+
48
+ Re-run `generate_brand.py` when:
49
+ - Major website or product rebrand
50
+ - Significant new product / service launch
51
+ - Quarterly refresh for active content strategies
52
+ - Expanding to a new language or region
53
+
54
+ ```bash
55
+ # Regenerate for a new market
56
+ python3 scripts/generate_brand.py --project-id <id> --language "Spanish" --region "MX"
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Integration with other skills
62
+
63
+ Brand cognition feeds into:
64
+ - **adgine-geo-content** — articles are written with your voice and tone
65
+ - **adgine-geo-topics** — generated prompts reflect your brand's ICP
66
+ - **adgine-geo-citation** — AI platforms cite you using your brand introduction
@@ -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,95 @@
1
+ #!/usr/bin/env python3
2
+ """Generate an AI brand cognition profile for a GEO project (async job).
3
+
4
+ Starts a brand generation job, polls until completed (~2–5 min), and prints
5
+ the resulting brand profile.
6
+
7
+ Usage:
8
+ python3 scripts/generate_brand.py [--project-id <id>] [--language English] [--region US]
9
+ """
10
+ import sys
11
+ import os
12
+ import argparse
13
+
14
+ sys.path.insert(0, os.path.dirname(__file__))
15
+ from _client import (
16
+ get_api_config, get_project_id,
17
+ api_get, api_post,
18
+ extract_data, poll_job, print_json,
19
+ )
20
+
21
+ parser = argparse.ArgumentParser(description="Generate GEO brand cognition profile")
22
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
23
+ parser.add_argument("--language", default="English", help="Brand content language (default: English)")
24
+ parser.add_argument("--region", default="US", help="Target region code, e.g. US, UK, CN (default: US)")
25
+ parser.add_argument("--json", action="store_true", help="Output final job result as raw JSON")
26
+ args = parser.parse_args()
27
+
28
+ key, base = get_api_config()
29
+ pid = get_project_id(args.project_id)
30
+
31
+ print(f"Starting brand cognition generation for project: {pid}")
32
+ print(f" Language : {args.language} | Region : {args.region}")
33
+ print()
34
+
35
+ body = {
36
+ "language": args.language,
37
+ "region": args.region,
38
+ "auto_start": True,
39
+ }
40
+ result = api_post(f"/api/projects/{pid}/brand/generate", key, base, body)
41
+ job_data = extract_data(result)
42
+
43
+ job_id = job_data.get("id") or job_data.get("job_id")
44
+ if not job_id:
45
+ print("ERROR: Brand generate endpoint did not return a job ID.")
46
+ print_json(job_data)
47
+ sys.exit(1)
48
+
49
+ print(f"Job ID: {job_id}")
50
+ print("Polling for completion (this typically takes 2–5 minutes)...")
51
+
52
+ # Job status polled via: api_get(f"/api/projects/{pid}/brand/jobs/{job_id}", key, base)
53
+ final_job = poll_job(
54
+ f"/api/projects/{pid}/brand/jobs/{job_id}",
55
+ key, base,
56
+ interval=8,
57
+ max_wait=600,
58
+ )
59
+
60
+ if args.json:
61
+ print_json(final_job)
62
+ sys.exit(0)
63
+
64
+ status = final_job.get("status", "")
65
+ if status == "failed":
66
+ print(f"ERROR: Brand generation failed — {final_job.get('error', 'unknown error')}")
67
+ sys.exit(1)
68
+
69
+ print(f"\nBrand generation completed!")
70
+
71
+ # Fetch the final brand profile from the brand endpoint
72
+ brand_result = api_get(f"/api/projects/{pid}/brand", key, base)
73
+ brand_data = extract_data(brand_result)
74
+ profile = brand_data.get("profile") or {}
75
+
76
+ FIELDS = [
77
+ ("brand_introduction", "Brand Introduction"),
78
+ ("ideal_customer", "Ideal Customer"),
79
+ ("competitors", "Competitors"),
80
+ ("brand_perspective", "Brand Perspective"),
81
+ ("author_persona", "Author Persona"),
82
+ ("voice_and_tone", "Voice & Tone"),
83
+ ("writing_rules", "Writing Rules"),
84
+ ]
85
+
86
+ for field_key, label in FIELDS:
87
+ value = profile.get(field_key, "")
88
+ if value:
89
+ display = value[:400] + "\n [truncated...]" if len(value) > 400 else value
90
+ print(f"\n[{label}]")
91
+ print(f" {display}")
92
+
93
+ print(f"\n---")
94
+ print(f"To refine any field, run:")
95
+ print(f" python3 scripts/update_brand.py --project-id {pid} --field <field> --value \"...\"")