@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,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()
@@ -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()