@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,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Publish a GEO content item (or raw markdown) to WordPress.
|
|
3
|
+
|
|
4
|
+
Two modes:
|
|
5
|
+
--content-id <uuid> — publish an existing system article by ID
|
|
6
|
+
--title <t> --content-body <md>
|
|
7
|
+
— publish raw title + markdown body
|
|
8
|
+
|
|
9
|
+
Optional:
|
|
10
|
+
--category-ids 1,2,3 — WP category IDs (omitted → Uncategorized)
|
|
11
|
+
--status publish|draft — publish state (default: publish)
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
python3 scripts/publish.py --content-id <uuid> --category-ids 2,5
|
|
15
|
+
python3 scripts/publish.py --title "My Post" --content-body "# Hello..." --status draft
|
|
16
|
+
|
|
17
|
+
Run list_publishable.py first to find content_id candidates.
|
|
18
|
+
"""
|
|
19
|
+
import sys
|
|
20
|
+
import os
|
|
21
|
+
import argparse
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
24
|
+
from _client import (
|
|
25
|
+
get_api_config, get_project_id,
|
|
26
|
+
api_post, extract_data, print_json,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
parser = argparse.ArgumentParser(description="Publish content to WordPress")
|
|
32
|
+
parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
|
|
33
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
34
|
+
parser.add_argument("--content-id", help="System article UUID (content_id mode)")
|
|
35
|
+
parser.add_argument("--title", help="Article title (direct mode)")
|
|
36
|
+
parser.add_argument("--content-body", help="Markdown body (direct mode)")
|
|
37
|
+
parser.add_argument("--category-ids", help="Comma-separated WP category IDs")
|
|
38
|
+
parser.add_argument("--status", default="publish",
|
|
39
|
+
choices=["publish", "draft"],
|
|
40
|
+
help="Publish state (default: publish)")
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
if not args.content_id and not (args.title and args.content_body):
|
|
44
|
+
print("ERROR: provide either --content-id OR both --title and --content-body.")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
body = {"status": args.status}
|
|
48
|
+
if args.content_id:
|
|
49
|
+
body["content_id"] = args.content_id
|
|
50
|
+
else:
|
|
51
|
+
body["title"] = args.title
|
|
52
|
+
body["content_body"] = args.content_body
|
|
53
|
+
if args.category_ids:
|
|
54
|
+
body["category_ids"] = [
|
|
55
|
+
int(c.strip()) for c in args.category_ids.split(",") if c.strip()
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
key, base = get_api_config()
|
|
59
|
+
pid = get_project_id(args.project_id)
|
|
60
|
+
|
|
61
|
+
result = api_post(
|
|
62
|
+
f"/api/projects/{pid}/integrations/wordpress/publish",
|
|
63
|
+
key, base, body=body,
|
|
64
|
+
)
|
|
65
|
+
data = extract_data(result) or {}
|
|
66
|
+
|
|
67
|
+
if args.json:
|
|
68
|
+
print_json(data)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
print("Published to WordPress.")
|
|
72
|
+
for k in ("record_id", "wp_post_id", "wp_post_url", "status",
|
|
73
|
+
"published_at"):
|
|
74
|
+
if k in data:
|
|
75
|
+
print(f" {k}: {data.get(k)}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Check whether a newer version of adgine-geo-skills is available on GitHub.
|
|
4
|
+
|
|
5
|
+
The canonical VERSION source of truth is:
|
|
6
|
+
https://github.com/adgine-ai/adgine-geo-skills/blob/main/VERSION
|
|
7
|
+
fetched via the raw endpoint.
|
|
8
|
+
|
|
9
|
+
Two output modes:
|
|
10
|
+
(default) Print a JSON object describing the version state.
|
|
11
|
+
--notice Print a single `_notice: {...}` line IF (and only if) an update
|
|
12
|
+
is available. Prints nothing otherwise. This is what scripts emit
|
|
13
|
+
at startup so the AI agent sees it in the tool output and prompts
|
|
14
|
+
the user to update.
|
|
15
|
+
--footer Print a human-readable `---` / ⚠️ footer IF an update is available.
|
|
16
|
+
--human Print a one-line Chinese update hint for agents (WorkBuddy). Empty if up to date.
|
|
17
|
+
|
|
18
|
+
Exits 0 always — any error (network, parse, etc.) is silently suppressed so the
|
|
19
|
+
calling skill's main flow is never blocked.
|
|
20
|
+
|
|
21
|
+
Debug (opt-in, does not change exit code):
|
|
22
|
+
GEO_VERSION_CHECK_DEBUG=1 Log failures to stderr.
|
|
23
|
+
--verbose Same, with default or --notice mode.
|
|
24
|
+
|
|
25
|
+
JSON schema (default mode):
|
|
26
|
+
{
|
|
27
|
+
"current": "1.1.0",
|
|
28
|
+
"latest": "1.2.0",
|
|
29
|
+
"update_available": true,
|
|
30
|
+
"install_type": "git" | "package",
|
|
31
|
+
"update_command": "git -C /path/to/dir pull", // git only
|
|
32
|
+
"release_url": "https://github.com/..."
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import re
|
|
39
|
+
import subprocess
|
|
40
|
+
import sys
|
|
41
|
+
import tempfile
|
|
42
|
+
import time
|
|
43
|
+
import urllib.request
|
|
44
|
+
|
|
45
|
+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
46
|
+
VERSION_FILE = os.path.join(REPO_ROOT, "VERSION")
|
|
47
|
+
REMOTE_VERSION_URL = (
|
|
48
|
+
"https://raw.githubusercontent.com/adgine-ai/adgine-geo-skills/main/VERSION"
|
|
49
|
+
)
|
|
50
|
+
RELEASE_URL = "https://github.com/adgine-ai/adgine-geo-skills/releases/latest"
|
|
51
|
+
TIMEOUT = 5
|
|
52
|
+
# Cache the remote version lookup so a conversation that runs several scripts
|
|
53
|
+
# in one turn does not hammer GitHub. A fresh conversation later still re-checks
|
|
54
|
+
# once the cache expires.
|
|
55
|
+
CACHE_TTL = 120 # 2 minutes — dedupes a burst of scripts in one turn while
|
|
56
|
+
# still surfacing a freshly-pushed version within ~2 min.
|
|
57
|
+
CACHE_FILE = os.path.join(tempfile.gettempdir(), "adgine_geo_skills_version.json")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _debug_enabled():
|
|
61
|
+
return os.environ.get("GEO_VERSION_CHECK_DEBUG") == "1" or "--verbose" in sys.argv
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _debug(msg):
|
|
65
|
+
if _debug_enabled():
|
|
66
|
+
print(f"[check_version] {msg}", file=sys.stderr)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_version_from_skill_md():
|
|
70
|
+
"""Fallback when installers (e.g. WorkBuddy) omit the root VERSION file."""
|
|
71
|
+
skill_md = os.path.join(REPO_ROOT, "SKILL.md")
|
|
72
|
+
if not os.path.isfile(skill_md):
|
|
73
|
+
return None
|
|
74
|
+
in_frontmatter = False
|
|
75
|
+
with open(skill_md, encoding="utf-8") as f:
|
|
76
|
+
for line in f:
|
|
77
|
+
line = line.strip()
|
|
78
|
+
if line == "---":
|
|
79
|
+
in_frontmatter = not in_frontmatter
|
|
80
|
+
if not in_frontmatter:
|
|
81
|
+
break
|
|
82
|
+
continue
|
|
83
|
+
if in_frontmatter:
|
|
84
|
+
m = re.match(r'^version:\s*["\']?([^"\'#\s]+)', line)
|
|
85
|
+
if m:
|
|
86
|
+
return m.group(1).strip()
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_local_version():
|
|
91
|
+
if os.path.isfile(VERSION_FILE):
|
|
92
|
+
with open(VERSION_FILE) as f:
|
|
93
|
+
return f.read().strip()
|
|
94
|
+
fallback = _read_version_from_skill_md()
|
|
95
|
+
if fallback:
|
|
96
|
+
_debug(f"VERSION file missing; using SKILL.md frontmatter ({fallback})")
|
|
97
|
+
return fallback
|
|
98
|
+
raise FileNotFoundError(VERSION_FILE)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _fetch_remote_version():
|
|
102
|
+
"""Return the latest version string from GitHub, using a short-lived cache."""
|
|
103
|
+
# Serve from cache if fresh
|
|
104
|
+
try:
|
|
105
|
+
with open(CACHE_FILE) as f:
|
|
106
|
+
cached = json.load(f)
|
|
107
|
+
if time.time() - cached.get("ts", 0) < CACHE_TTL and cached.get("latest"):
|
|
108
|
+
return cached["latest"]
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
req = urllib.request.Request(
|
|
113
|
+
f"{REMOTE_VERSION_URL}?t={int(time.time())}",
|
|
114
|
+
headers={
|
|
115
|
+
"User-Agent": "adgine-geo-skills-version-check/1.0",
|
|
116
|
+
"Cache-Control": "no-cache",
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
|
120
|
+
latest = resp.read().decode().strip()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with open(CACHE_FILE, "w") as f:
|
|
124
|
+
json.dump({"ts": time.time(), "latest": latest}, f)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
return latest
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _parse_version(v):
|
|
131
|
+
"""Return tuple of ints for semver comparison, e.g. 'v1.2.3' → (1, 2, 3)."""
|
|
132
|
+
return tuple(int(x) for x in v.strip().lstrip("v").split("."))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _is_git_repo(path):
|
|
136
|
+
try:
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
["git", "-C", path, "rev-parse", "--git-dir"],
|
|
139
|
+
stdout=subprocess.DEVNULL,
|
|
140
|
+
stderr=subprocess.DEVNULL,
|
|
141
|
+
timeout=3,
|
|
142
|
+
)
|
|
143
|
+
return result.returncode == 0
|
|
144
|
+
except Exception:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_state():
|
|
149
|
+
"""Return the full version-state dict, or None on any error.
|
|
150
|
+
|
|
151
|
+
Returns None immediately when GEO_SKIP_VERSION_CHECK is set. The npm CLI
|
|
152
|
+
(adgine-geo) sets this so version updates are surfaced by npm instead, and
|
|
153
|
+
so stdout stays clean (no `_notice:`/footer lines) and no per-command
|
|
154
|
+
network round-trip to GitHub is incurred.
|
|
155
|
+
"""
|
|
156
|
+
if os.environ.get("GEO_SKIP_VERSION_CHECK"):
|
|
157
|
+
return None
|
|
158
|
+
try:
|
|
159
|
+
current = _read_local_version()
|
|
160
|
+
latest = _fetch_remote_version()
|
|
161
|
+
update_available = _parse_version(latest) > _parse_version(current)
|
|
162
|
+
is_git = _is_git_repo(REPO_ROOT)
|
|
163
|
+
return {
|
|
164
|
+
"current": current,
|
|
165
|
+
"latest": latest,
|
|
166
|
+
"update_available": update_available,
|
|
167
|
+
"install_type": "git" if is_git else "package",
|
|
168
|
+
"update_command": f"git -C {REPO_ROOT} pull" if is_git else "",
|
|
169
|
+
"release_url": RELEASE_URL,
|
|
170
|
+
}
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
_debug(f"check failed: {exc}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def format_user_footer(state):
|
|
177
|
+
"""Human-readable update block for stdout (WorkBuddy and similar agents).
|
|
178
|
+
|
|
179
|
+
Printed at the end of script output so agents that rewrite tool results
|
|
180
|
+
still surface the message when they quote trailing lines.
|
|
181
|
+
"""
|
|
182
|
+
if not state or not state.get("update_available"):
|
|
183
|
+
return ""
|
|
184
|
+
cur, lat = state["current"], state["latest"]
|
|
185
|
+
if state.get("install_type") == "git":
|
|
186
|
+
body = (
|
|
187
|
+
f"⚠️ adgine-geo-skills v{lat} 已发布(当前 v{cur})。"
|
|
188
|
+
"跟我说「请帮我更新 adgine-geo-skills 到最新版本」即可升级。"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
url = state.get("release_url", "")
|
|
192
|
+
body = (
|
|
193
|
+
f"⚠️ adgine-geo-skills v{lat} 已发布(当前 v{cur})。"
|
|
194
|
+
f"请前往 {url} 下载最新版并重新安装。"
|
|
195
|
+
)
|
|
196
|
+
return f"---\n{body}"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def format_user_inline(state):
|
|
200
|
+
"""One-line update hint (matches Hermes-style phrasing for WorkBuddy)."""
|
|
201
|
+
if not state or not state.get("update_available"):
|
|
202
|
+
return ""
|
|
203
|
+
cur, lat = state["current"], state["latest"]
|
|
204
|
+
if state.get("install_type") == "git":
|
|
205
|
+
return (
|
|
206
|
+
f"另外 adgine-geo-skills 又有新版本 v{lat}(当前 v{cur}),需要更新吗?"
|
|
207
|
+
"跟我说「请帮我更新 adgine-geo-skills 到最新版本」即可升级。"
|
|
208
|
+
)
|
|
209
|
+
url = state.get("release_url", "")
|
|
210
|
+
return (
|
|
211
|
+
f"另外 adgine-geo-skills 又有新版本 v{lat}(当前 v{cur}),需要更新吗?"
|
|
212
|
+
f"请前往 {url} 下载最新版并重新安装。"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def emit_human(stream=None):
|
|
217
|
+
"""Print a single-line human hint; empty when up to date."""
|
|
218
|
+
stream = stream or sys.stdout
|
|
219
|
+
line = format_user_inline(get_state())
|
|
220
|
+
if not line:
|
|
221
|
+
return
|
|
222
|
+
try:
|
|
223
|
+
stream.write(line + "\n")
|
|
224
|
+
stream.flush()
|
|
225
|
+
except Exception:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def emit_footer(stream=None):
|
|
230
|
+
"""Print the human-readable footer if an update is available."""
|
|
231
|
+
stream = stream or sys.stdout
|
|
232
|
+
footer = format_user_footer(get_state())
|
|
233
|
+
if not footer:
|
|
234
|
+
return
|
|
235
|
+
try:
|
|
236
|
+
stream.write(footer + "\n")
|
|
237
|
+
stream.flush()
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def emit_notice(stream=None):
|
|
243
|
+
"""Print a single `_notice:` line to `stream` if an update is available.
|
|
244
|
+
|
|
245
|
+
Writes to STDOUT by default so the agent harness reliably surfaces it in the
|
|
246
|
+
tool result (some harnesses only feed stdout to the model). The `_notice:`
|
|
247
|
+
prefix lets agents parse it before JSON on the same stream.
|
|
248
|
+
|
|
249
|
+
Silent when up to date or on any error. Safe to call at the top of any
|
|
250
|
+
skill script — never raises, never blocks.
|
|
251
|
+
"""
|
|
252
|
+
stream = stream or sys.stdout
|
|
253
|
+
state = get_state()
|
|
254
|
+
if not state or not state.get("update_available"):
|
|
255
|
+
return
|
|
256
|
+
cur, lat = state["current"], state["latest"]
|
|
257
|
+
if state.get("install_type") == "git":
|
|
258
|
+
msg = (f"adgine-geo-skills {lat} available (current {cur}). "
|
|
259
|
+
"Tell me: 请帮我更新 adgine-geo-skills 到最新版本")
|
|
260
|
+
else:
|
|
261
|
+
msg = (f"adgine-geo-skills {lat} available (current {cur}). "
|
|
262
|
+
f"Download: {state.get('release_url', '')}")
|
|
263
|
+
notice = {"update": {"current": cur, "latest": lat, "message": msg}}
|
|
264
|
+
try:
|
|
265
|
+
stream.write(f"_notice: {json.dumps(notice, ensure_ascii=False)}\n\n")
|
|
266
|
+
stream.flush()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def main():
|
|
272
|
+
if "--notice" in sys.argv:
|
|
273
|
+
emit_notice()
|
|
274
|
+
sys.exit(0)
|
|
275
|
+
if "--footer" in sys.argv:
|
|
276
|
+
emit_footer()
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
if "--human" in sys.argv:
|
|
279
|
+
emit_human()
|
|
280
|
+
sys.exit(0)
|
|
281
|
+
state = get_state()
|
|
282
|
+
if state is not None:
|
|
283
|
+
print(json.dumps(state, ensure_ascii=False))
|
|
284
|
+
elif _debug_enabled():
|
|
285
|
+
_debug("no state returned (see errors above)")
|
|
286
|
+
sys.exit(0)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
main()
|
package/skills/setup.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Setup helper for adgine-geo-skills.
|
|
3
|
+
|
|
4
|
+
Two ways to run:
|
|
5
|
+
|
|
6
|
+
Non-interactive (for AI agents / scripts):
|
|
7
|
+
python3 setup.py <GEO_API_KEY>
|
|
8
|
+
python3 setup.py --key <GEO_API_KEY>
|
|
9
|
+
|
|
10
|
+
Interactive wizard (for humans):
|
|
11
|
+
python3 setup.py
|
|
12
|
+
|
|
13
|
+
Behavior:
|
|
14
|
+
- Locates the skills repo root via this file's own absolute path,
|
|
15
|
+
so it works regardless of the caller's current working directory.
|
|
16
|
+
- Writes GEO_API_KEY to <repo_root>/.env (creating it from
|
|
17
|
+
.env.example if needed). Preserves other lines in .env.
|
|
18
|
+
- Verifies the key by calling /api/projects?limit=1 with Bearer auth.
|
|
19
|
+
Exit code 0 on success, non-zero on failure.
|
|
20
|
+
- Never writes to ~/.zshrc, ~/.bashrc, ~/.hermes/, or any other
|
|
21
|
+
location. The .env file is gitignored — the key stays local.
|
|
22
|
+
"""
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import shutil
|
|
26
|
+
import argparse
|
|
27
|
+
import atexit
|
|
28
|
+
import importlib.util
|
|
29
|
+
|
|
30
|
+
REPO_ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
31
|
+
ENV_PATH = os.path.join(REPO_ROOT, ".env")
|
|
32
|
+
ENV_EXAMPLE_PATH = os.path.join(REPO_ROOT, ".env.example")
|
|
33
|
+
PLACEHOLDER_VALUES = {"", "geo_sk_live_YOUR_KEY_HERE", "geo_sk...HERE"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _load_check_version_module():
|
|
37
|
+
path = os.path.join(REPO_ROOT, "scripts", "check_version.py")
|
|
38
|
+
if not os.path.isfile(path):
|
|
39
|
+
return None
|
|
40
|
+
spec = importlib.util.spec_from_file_location("geo_check_version", path)
|
|
41
|
+
mod = importlib.util.module_from_spec(spec)
|
|
42
|
+
spec.loader.exec_module(mod)
|
|
43
|
+
return mod
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _emit_version_notice():
|
|
47
|
+
try:
|
|
48
|
+
mod = _load_check_version_module()
|
|
49
|
+
if mod is not None:
|
|
50
|
+
mod.emit_notice()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _emit_version_footer():
|
|
56
|
+
try:
|
|
57
|
+
mod = _load_check_version_module()
|
|
58
|
+
if mod is not None:
|
|
59
|
+
mod.emit_footer()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
atexit.register(_emit_version_footer)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def read_env_key():
|
|
68
|
+
"""Return the current GEO_API_KEY from .env, or None if unset/placeholder."""
|
|
69
|
+
if not os.path.isfile(ENV_PATH):
|
|
70
|
+
return None
|
|
71
|
+
with open(ENV_PATH) as f:
|
|
72
|
+
for line in f:
|
|
73
|
+
line = line.strip()
|
|
74
|
+
if line.startswith("GEO_API_KEY="):
|
|
75
|
+
val = line.split("=", 1)[1].strip()
|
|
76
|
+
return val if val not in PLACEHOLDER_VALUES else None
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_env_key(key):
|
|
81
|
+
"""Write or update GEO_API_KEY in <repo_root>/.env, preserving other lines."""
|
|
82
|
+
if not os.path.isfile(ENV_PATH):
|
|
83
|
+
if os.path.isfile(ENV_EXAMPLE_PATH):
|
|
84
|
+
shutil.copy(ENV_EXAMPLE_PATH, ENV_PATH)
|
|
85
|
+
else:
|
|
86
|
+
with open(ENV_PATH, "w") as f:
|
|
87
|
+
f.write("# GEO Skills — Environment Variables\n")
|
|
88
|
+
|
|
89
|
+
with open(ENV_PATH) as f:
|
|
90
|
+
lines = f.readlines()
|
|
91
|
+
|
|
92
|
+
found = False
|
|
93
|
+
new_lines = []
|
|
94
|
+
for line in lines:
|
|
95
|
+
if line.strip().startswith("GEO_API_KEY="):
|
|
96
|
+
new_lines.append(f"GEO_API_KEY={key}\n")
|
|
97
|
+
found = True
|
|
98
|
+
else:
|
|
99
|
+
new_lines.append(line)
|
|
100
|
+
if not found:
|
|
101
|
+
if new_lines and not new_lines[-1].endswith("\n"):
|
|
102
|
+
new_lines.append("\n")
|
|
103
|
+
new_lines.append(f"GEO_API_KEY={key}\n")
|
|
104
|
+
|
|
105
|
+
with open(ENV_PATH, "w") as f:
|
|
106
|
+
f.writelines(new_lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def verify_key(key):
|
|
110
|
+
"""Quick auth check. Returns (ok: bool, message: str).
|
|
111
|
+
|
|
112
|
+
ok=True means the key is accepted (HTTP 200) OR the platform was
|
|
113
|
+
unreachable (we don't want to block setup on transient network issues).
|
|
114
|
+
ok=False means we got a definitive 401 Unauthorized.
|
|
115
|
+
"""
|
|
116
|
+
import urllib.request as req
|
|
117
|
+
import urllib.error as uerr
|
|
118
|
+
|
|
119
|
+
base = os.environ.get("GEO_API_BASE_URL", "https://platform.adgine.ai").rstrip("/")
|
|
120
|
+
url = f"{base}/api/projects?limit=1"
|
|
121
|
+
headers = {
|
|
122
|
+
"Authorization": f"Bearer {key}",
|
|
123
|
+
"Accept": "application/json",
|
|
124
|
+
"User-Agent": "geo-skills-setup/1.0",
|
|
125
|
+
}
|
|
126
|
+
request = req.Request(url, headers=headers, method="GET")
|
|
127
|
+
try:
|
|
128
|
+
with req.urlopen(request, timeout=10) as resp:
|
|
129
|
+
return (resp.status == 200, f"HTTP {resp.status}")
|
|
130
|
+
except uerr.HTTPError as e:
|
|
131
|
+
if e.code == 401:
|
|
132
|
+
return (False, "401 Unauthorized — key is invalid")
|
|
133
|
+
# Other errors (403, 5xx) don't necessarily mean the key is wrong.
|
|
134
|
+
return (True, f"HTTP {e.code} (non-auth error, treating as ok)")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return (True, f"network error: {e} (skipped verification)")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def print_next_steps():
|
|
140
|
+
print(" Next steps:")
|
|
141
|
+
print(" 1. List your projects:")
|
|
142
|
+
print(" python3 adgine-geo-projects/scripts/list_projects.py")
|
|
143
|
+
print(" 2. Ask your agent, for example:")
|
|
144
|
+
print(' "What\'s my AI visibility score this week?"')
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_noninteractive(key):
|
|
149
|
+
"""Install the key without prompting. Used by AI agents."""
|
|
150
|
+
if not key or key in PLACEHOLDER_VALUES:
|
|
151
|
+
print("ERROR: empty or placeholder key.", file=sys.stderr)
|
|
152
|
+
sys.exit(2)
|
|
153
|
+
|
|
154
|
+
if not key.startswith("geo_sk_"):
|
|
155
|
+
print(
|
|
156
|
+
f"WARNING: key does not start with 'geo_sk_' (got: {key[:8]}...). "
|
|
157
|
+
"Continuing anyway.",
|
|
158
|
+
file=sys.stderr,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
ok, detail = verify_key(key)
|
|
162
|
+
if not ok:
|
|
163
|
+
print(f"ERROR: key verification failed — {detail}", file=sys.stderr)
|
|
164
|
+
print(" Check the key at: https://platform.adgine.ai", file=sys.stderr)
|
|
165
|
+
sys.exit(1)
|
|
166
|
+
|
|
167
|
+
write_env_key(key)
|
|
168
|
+
print(f"OK: GEO_API_KEY written to {ENV_PATH}")
|
|
169
|
+
print(f" verification: {detail}")
|
|
170
|
+
sys.exit(0)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def run_interactive():
|
|
174
|
+
"""Wizard for human users."""
|
|
175
|
+
print()
|
|
176
|
+
print("=" * 52)
|
|
177
|
+
print(" adgine-geo-skills — Setup Wizard")
|
|
178
|
+
print("=" * 52)
|
|
179
|
+
print()
|
|
180
|
+
|
|
181
|
+
existing_key = read_env_key()
|
|
182
|
+
|
|
183
|
+
if existing_key:
|
|
184
|
+
print(f" GEO_API_KEY already set in {ENV_PATH}")
|
|
185
|
+
print()
|
|
186
|
+
answer = input(" Re-enter / update it? [y/N] ").strip().lower()
|
|
187
|
+
if answer != "y":
|
|
188
|
+
print()
|
|
189
|
+
print(" No changes made. You're ready to go!")
|
|
190
|
+
print()
|
|
191
|
+
print_next_steps()
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
print(" Get your API key at: https://platform.adgine.ai")
|
|
195
|
+
print()
|
|
196
|
+
key = input(" Paste your GEO_API_KEY: ").strip()
|
|
197
|
+
|
|
198
|
+
if not key or key in PLACEHOLDER_VALUES:
|
|
199
|
+
print()
|
|
200
|
+
print(" No key entered. Run setup.py again when you have your key.")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
print()
|
|
204
|
+
print(" Verifying key...", end="", flush=True)
|
|
205
|
+
ok, detail = verify_key(key)
|
|
206
|
+
if ok:
|
|
207
|
+
print(f" OK ({detail})")
|
|
208
|
+
else:
|
|
209
|
+
print(f" FAILED ({detail})")
|
|
210
|
+
print()
|
|
211
|
+
print(" Double-check at: https://platform.adgine.ai")
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
write_env_key(key)
|
|
215
|
+
print()
|
|
216
|
+
print(f" Key saved to: {ENV_PATH}")
|
|
217
|
+
print(" (This file is gitignored — your key stays local and private.)")
|
|
218
|
+
print()
|
|
219
|
+
print_next_steps()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main():
|
|
223
|
+
_emit_version_notice()
|
|
224
|
+
parser = argparse.ArgumentParser(
|
|
225
|
+
description="Configure GEO_API_KEY for adgine-geo-skills.",
|
|
226
|
+
add_help=True,
|
|
227
|
+
)
|
|
228
|
+
parser.add_argument(
|
|
229
|
+
"key",
|
|
230
|
+
nargs="?",
|
|
231
|
+
help="GEO_API_KEY to install. Omit for interactive mode.",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--key",
|
|
235
|
+
dest="key_flag",
|
|
236
|
+
help="GEO_API_KEY (alternative to positional argument).",
|
|
237
|
+
)
|
|
238
|
+
args = parser.parse_args()
|
|
239
|
+
|
|
240
|
+
key = args.key_flag or args.key
|
|
241
|
+
if key:
|
|
242
|
+
run_noninteractive(key)
|
|
243
|
+
else:
|
|
244
|
+
run_interactive()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
main()
|