@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,140 @@
1
+ #!/usr/bin/env python3
2
+ """AI-powered bulk prompt generation for a GEO topic (async job).
3
+
4
+ Starts a generate-prompts job, polls until completed (~1–3 min), and prints
5
+ the generated prompts.
6
+
7
+ Usage:
8
+ python3 scripts/generate_prompts.py --topic-id <id> [--project-id <id>] \
9
+ [--count 10] [--language "English (en-US)"] [--region US] \
10
+ [--platforms "openai,perplexity,google_aio"] \
11
+ [--instructions "Focus on enterprise buyers"] [--json]
12
+ """
13
+ import sys
14
+ import os
15
+ import time
16
+ import argparse
17
+
18
+ sys.path.insert(0, os.path.dirname(__file__))
19
+ from _client import (
20
+ get_api_config, get_project_id,
21
+ api_get, api_post,
22
+ extract_data, print_json,
23
+ )
24
+
25
+ DEFAULT_PLATFORMS = ["openai", "google_aio", "perplexity"]
26
+
27
+ parser = argparse.ArgumentParser(description="AI-generate prompts for a GEO topic")
28
+ parser.add_argument("--topic-id", required=True, help="Topic ID to generate prompts for")
29
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
30
+ parser.add_argument("--count", type=int, default=10,
31
+ help="Number of prompts to generate, 1–50 (default: 10)")
32
+ parser.add_argument("--language", default="English (en-US)",
33
+ help="Language for generated prompts (default: English (en-US))")
34
+ parser.add_argument("--region", default="US",
35
+ help="Target region code (default: US)")
36
+ parser.add_argument("--platforms",
37
+ help=f"Comma-separated platform IDs (default: {', '.join(DEFAULT_PLATFORMS)}). "
38
+ "Valid values: openai, google_aio, perplexity")
39
+ parser.add_argument("--instructions",
40
+ help="Additional instructions to guide prompt generation")
41
+ parser.add_argument("--json", action="store_true", help="Output raw JSON task result")
42
+ args = parser.parse_args()
43
+
44
+ key, base = get_api_config()
45
+ pid = get_project_id(args.project_id)
46
+
47
+ platforms = DEFAULT_PLATFORMS
48
+ if args.platforms:
49
+ platforms = [p.strip() for p in args.platforms.split(",") if p.strip()]
50
+
51
+ if not (1 <= args.count <= 50):
52
+ print("ERROR: --count must be between 1 and 50")
53
+ sys.exit(1)
54
+
55
+ print(f"Starting AI prompt generation for topic: {args.topic_id}")
56
+ print(f" Count : {args.count}")
57
+ print(f" Language : {args.language} | Region: {args.region}")
58
+ print(f" Platforms : {', '.join(platforms)}")
59
+ print()
60
+
61
+ body = {
62
+ "count": args.count,
63
+ "language": args.language,
64
+ "region": args.region,
65
+ "platforms": platforms,
66
+ }
67
+ if args.instructions:
68
+ body["additional_instructions"] = args.instructions
69
+
70
+ result = api_post(
71
+ f"/api/projects/{pid}/topics/{args.topic_id}/generate-prompts",
72
+ key, base, body
73
+ )
74
+ task_data = extract_data(result)
75
+ task_id = task_data.get("task_id") or task_data.get("id")
76
+
77
+ if not task_id:
78
+ print("ERROR: Generate-prompts endpoint did not return a task ID.")
79
+ print_json(task_data)
80
+ sys.exit(1)
81
+
82
+ print(f"Task ID: {task_id}")
83
+ print("Polling for completion (this typically takes 1–3 minutes)...")
84
+
85
+ # Poll the task status endpoint
86
+ frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
87
+ elapsed = 0
88
+ max_wait = 600
89
+ interval = 5
90
+ idx = 0
91
+ final_task = None
92
+
93
+ while elapsed < max_wait:
94
+ status_result = api_get(
95
+ f"/api/projects/{pid}/topics/{args.topic_id}/generate-prompts/{task_id}",
96
+ key, base
97
+ )
98
+ task = extract_data(status_result)
99
+ status = task.get("status", "")
100
+ phase = task.get("current_phase") or status
101
+ count_so_far = task.get("generated_count", "")
102
+ label = f"{phase} ({count_so_far} generated)" if count_so_far else phase
103
+ print(f"\r {frames[idx % len(frames)]} {label}...", end="", flush=True)
104
+ idx += 1
105
+
106
+ if status in ("completed", "failed", "done", "success", "error"):
107
+ print()
108
+ final_task = task
109
+ break
110
+ time.sleep(interval)
111
+ elapsed += interval
112
+
113
+ if final_task is None:
114
+ print()
115
+ print(f"WARNING: Task still running after {max_wait}s.")
116
+ print(f" Check status manually:")
117
+ print(f" GET /api/projects/{pid}/topics/{args.topic_id}/generate-prompts/{task_id}")
118
+ sys.exit(0)
119
+
120
+ if args.json:
121
+ print_json(final_task)
122
+ sys.exit(0)
123
+
124
+ if final_task.get("status") == "failed":
125
+ print(f"ERROR: Prompt generation failed — {final_task.get('error', 'unknown error')}")
126
+ sys.exit(1)
127
+
128
+ prompts = final_task.get("prompts") or []
129
+ print(f"\nGenerated {len(prompts)} prompts for topic {args.topic_id}:")
130
+ print()
131
+ for i, p in enumerate(prompts, 1):
132
+ content = p.get("content", "")
133
+ prompt_id = p.get("id", "")
134
+ print(f" {i:>2}. [{prompt_id[:36]}]")
135
+ print(f" {content}")
136
+ print()
137
+
138
+ print(f"These prompts are ready for:")
139
+ print(f" - Citation testing : use the geo-citation skill → python3 scripts/create_tests.py --prompt-ids <id1,id2,...>")
140
+ print(f" - Article outlines : use the geo-content skill → python3 scripts/generate_outline.py --topic-id {args.topic_id} --prompt-ids <id1,id2,...>")
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """List, create, update, or delete prompts within a GEO topic.
3
+
4
+ Usage:
5
+ python3 scripts/manage_prompts.py list --topic-id <tid> [--project-id <id>] [--json]
6
+ python3 scripts/manage_prompts.py list-all [--project-id <id>] [--json]
7
+ python3 scripts/manage_prompts.py create --topic-id <tid> --content "What is...?" \
8
+ [--language "English (en-US)"] [--region US] \
9
+ [--platforms "ChatGPT,Perplexity,Google AI Overviews"]
10
+ python3 scripts/manage_prompts.py update --topic-id <tid> --prompt-id <pid> --content "..."
11
+ python3 scripts/manage_prompts.py delete --topic-id <tid> --prompt-id <pid>
12
+ """
13
+ import sys
14
+ import os
15
+ import argparse
16
+
17
+ sys.path.insert(0, os.path.dirname(__file__))
18
+ from _client import (
19
+ get_api_config, get_project_id,
20
+ api_get, api_post, api_put, api_delete,
21
+ extract_data, print_json,
22
+ )
23
+
24
+ parser = argparse.ArgumentParser(description="Manage GEO prompts")
25
+ parser.add_argument("action", choices=["list", "list-all", "get", "create", "update", "delete"])
26
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
27
+ parser.add_argument("--topic-id", help="Topic ID")
28
+ parser.add_argument("--prompt-id", help="Prompt ID (required for update/delete)")
29
+ parser.add_argument("--content", help="Prompt text content")
30
+ parser.add_argument("--language", default="English (en-US)", help="Language (default: English (en-US))")
31
+ parser.add_argument("--region", default="US", help="Region code (default: US)")
32
+ parser.add_argument("--platforms", help="Comma-separated platform IDs (e.g. openai,perplexity,google_aio)")
33
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
34
+ args = parser.parse_args()
35
+
36
+ key, base = get_api_config()
37
+ pid = get_project_id(args.project_id)
38
+
39
+ def _fmt_platforms(platforms_list):
40
+ if not platforms_list:
41
+ return "—"
42
+ return ", ".join(platforms_list)[:60]
43
+
44
+ # ── LIST (by topic) ───────────────────────────────────────────────────────────
45
+ if args.action == "list":
46
+ if not args.topic_id:
47
+ print("ERROR: --topic-id is required for list")
48
+ sys.exit(1)
49
+ result = api_get(f"/api/projects/{pid}/topics/{args.topic_id}/prompts", key, base)
50
+ data = extract_data(result)
51
+ items = data if isinstance(data, list) else data.get("items") or data.get("prompts") or []
52
+ if args.json:
53
+ print_json(data)
54
+ sys.exit(0)
55
+ print(f"Prompts for topic {args.topic_id} ({len(items)} found)")
56
+ print()
57
+ for p in items:
58
+ print(f" ID: {p.get('id', '')[:36]}")
59
+ content = p.get("content", "")
60
+ print(f" {content[:120]}{'...' if len(content) > 120 else ''}")
61
+ print(f" Platforms: {_fmt_platforms(p.get('platforms'))}")
62
+ print()
63
+
64
+ # ── LIST-ALL (project-wide) ───────────────────────────────────────────────────
65
+ elif args.action == "list-all":
66
+ result = api_get(f"/api/projects/{pid}/prompts", key, base)
67
+ data = extract_data(result)
68
+ items = data if isinstance(data, list) else data.get("items") or data.get("prompts") or []
69
+ if args.json:
70
+ print_json(data)
71
+ sys.exit(0)
72
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
73
+ print(f"All prompts in project {pid} ({total} found)")
74
+ print()
75
+ for p in items:
76
+ content = p.get("content", "")
77
+ print(f" [{p.get('id', '')[:36]}] {content[:100]}{'...' if len(content) > 100 else ''}")
78
+
79
+ # ── GET (detail) ──────────────────────────────────────────────────────────────
80
+ elif args.action == "get":
81
+ if not args.topic_id or not args.prompt_id:
82
+ print("ERROR: --topic-id and --prompt-id are required for get")
83
+ sys.exit(1)
84
+ result = api_get(
85
+ f"/api/projects/{pid}/topics/{args.topic_id}/prompts/{args.prompt_id}",
86
+ key, base,
87
+ )
88
+ prompt = extract_data(result) or {}
89
+ if args.json:
90
+ print_json(prompt)
91
+ sys.exit(0)
92
+ print(f"Prompt detail: {args.prompt_id}")
93
+ print()
94
+ print(f" Content : {prompt.get('content', '')}")
95
+ print(f" Language : {prompt.get('language', '--')}")
96
+ print(f" Region : {prompt.get('region', '--')}")
97
+ print(f" Platforms : {_fmt_platforms(prompt.get('platforms'))}")
98
+ print(f" Created : {prompt.get('created_at', '--')}")
99
+
100
+ # ── CREATE ────────────────────────────────────────────────────────────────────
101
+ elif args.action == "create":
102
+ if not args.topic_id:
103
+ print("ERROR: --topic-id is required for create")
104
+ sys.exit(1)
105
+ if not args.content:
106
+ print("ERROR: --content is required for create")
107
+ sys.exit(1)
108
+ body = {
109
+ "content": args.content,
110
+ "language": args.language,
111
+ "region": args.region,
112
+ }
113
+ if args.platforms:
114
+ body["platforms"] = [p.strip() for p in args.platforms.split(",") if p.strip()]
115
+ result = api_post(f"/api/projects/{pid}/topics/{args.topic_id}/prompts", key, base, body)
116
+ prompt = extract_data(result)
117
+ if args.json:
118
+ print_json(prompt)
119
+ sys.exit(0)
120
+ print(f"✓ Created prompt")
121
+ print(f" ID : {prompt.get('id')}")
122
+ print(f" Content : {prompt.get('content', '')[:100]}")
123
+
124
+ # ── UPDATE ────────────────────────────────────────────────────────────────────
125
+ elif args.action == "update":
126
+ if not args.topic_id or not args.prompt_id:
127
+ print("ERROR: --topic-id and --prompt-id are required for update")
128
+ sys.exit(1)
129
+ if not args.content:
130
+ print("ERROR: --content is required for update")
131
+ sys.exit(1)
132
+ body = {"content": args.content}
133
+ result = api_put(
134
+ f"/api/projects/{pid}/topics/{args.topic_id}/prompts/{args.prompt_id}",
135
+ key, base, body
136
+ )
137
+ prompt = extract_data(result)
138
+ if args.json:
139
+ print_json(prompt)
140
+ sys.exit(0)
141
+ print(f"✓ Updated prompt {args.prompt_id}")
142
+ print(f" Content : {prompt.get('content', '')[:100]}")
143
+
144
+ # ── DELETE ────────────────────────────────────────────────────────────────────
145
+ elif args.action == "delete":
146
+ if not args.topic_id or not args.prompt_id:
147
+ print("ERROR: --topic-id and --prompt-id are required for delete")
148
+ sys.exit(1)
149
+ api_delete(
150
+ f"/api/projects/{pid}/topics/{args.topic_id}/prompts/{args.prompt_id}",
151
+ key, base
152
+ )
153
+ print(f"✓ Deleted prompt {args.prompt_id}")
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """List, create, batch-create, update, or delete GEO topics.
3
+
4
+ Usage:
5
+ python3 scripts/manage_topics.py list [--project-id <id>] [--page 1] [--limit 20] [--json]
6
+ python3 scripts/manage_topics.py create --name "Product Reviews" [--description "..."]
7
+ python3 scripts/manage_topics.py batch --names "Topic A,Topic B,Topic C"
8
+ python3 scripts/manage_topics.py update --topic-id <id> [--name "..."] [--description "..."]
9
+ python3 scripts/manage_topics.py delete --topic-id <id>
10
+ """
11
+ import sys
12
+ import os
13
+ import argparse
14
+
15
+ sys.path.insert(0, os.path.dirname(__file__))
16
+ from _client import (
17
+ get_api_config, get_project_id,
18
+ api_get, api_post, api_put, api_delete,
19
+ extract_data, print_json,
20
+ )
21
+
22
+ parser = argparse.ArgumentParser(description="Manage GEO topics")
23
+ parser.add_argument("action", choices=["list", "get", "create", "batch", "update", "delete"])
24
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
25
+ parser.add_argument("--topic-id", help="Topic ID (required for update/delete)")
26
+ parser.add_argument("--name", help="Topic name")
27
+ parser.add_argument("--description", help="Topic description")
28
+ parser.add_argument("--names", help="Comma-separated names for batch create")
29
+ parser.add_argument("--page", type=int, default=1, help="Page number (list only)")
30
+ parser.add_argument("--limit", type=int, default=20, help="Results per page (list only)")
31
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
32
+ args = parser.parse_args()
33
+
34
+ key, base = get_api_config()
35
+ pid = get_project_id(args.project_id)
36
+
37
+ # ── LIST ─────────────────────────────────────────────────────────────────────
38
+ if args.action == "list":
39
+ result = api_get(
40
+ f"/api/projects/{pid}/topics",
41
+ key, base,
42
+ params={"page": args.page, "limit": args.limit},
43
+ )
44
+ topics = extract_data(result)
45
+ items = topics if isinstance(topics, list) else topics.get("items") or topics.get("topics") or []
46
+
47
+ if args.json:
48
+ print_json(topics)
49
+ sys.exit(0)
50
+
51
+ total = topics.get("total", len(items)) if isinstance(topics, dict) else len(items)
52
+ print(f"Topics ({len(items)} of {total}) | project: {pid}")
53
+ print()
54
+ if not items:
55
+ print(" No topics found. Create one with: python3 scripts/manage_topics.py create --name \"...\"")
56
+ else:
57
+ print(f" {'ID':<38} {'Name':<30} {'Prompts':>7} Description")
58
+ print(f" {'-'*38} {'-'*30} {'-'*7} {'-'*30}")
59
+ for t in items:
60
+ tid = t.get("id", "")[:36]
61
+ name = (t.get("name") or "")[:30]
62
+ count = t.get("prompt_count", 0)
63
+ desc = (t.get("description") or "")[:40]
64
+ print(f" {tid:<38} {name:<30} {count:>7} {desc}")
65
+
66
+ # ── GET (detail) ─────────────────────────────────────────────────────────────
67
+ elif args.action == "get":
68
+ if not args.topic_id:
69
+ print("ERROR: --topic-id is required for get")
70
+ sys.exit(1)
71
+ result = api_get(f"/api/projects/{pid}/topics/{args.topic_id}", key, base)
72
+ topic = extract_data(result) or {}
73
+ if args.json:
74
+ print_json(topic)
75
+ sys.exit(0)
76
+ print(f"Topic detail: {args.topic_id}")
77
+ print()
78
+ print(f" Name : {topic.get('name', '')}")
79
+ print(f" Description : {topic.get('description', '') or '--'}")
80
+ print(f" Prompts : {topic.get('prompt_count', 0)}")
81
+ print(f" Created : {topic.get('created_at', '--')}")
82
+
83
+ # ── CREATE ────────────────────────────────────────────────────────────────────
84
+ elif args.action == "create":
85
+ if not args.name:
86
+ print("ERROR: --name is required for create")
87
+ sys.exit(1)
88
+ body = {"name": args.name}
89
+ if args.description:
90
+ body["description"] = args.description
91
+ result = api_post(f"/api/projects/{pid}/topics", key, base, body)
92
+ topic = extract_data(result)
93
+ if args.json:
94
+ print_json(topic)
95
+ sys.exit(0)
96
+ print(f"✓ Created topic: {topic.get('name')}")
97
+ print(f" ID: {topic.get('id')}")
98
+
99
+ # ── BATCH CREATE ──────────────────────────────────────────────────────────────
100
+ elif args.action == "batch":
101
+ if not args.names:
102
+ print("ERROR: --names is required (comma-separated list of topic names)")
103
+ sys.exit(1)
104
+ name_list = [n.strip() for n in args.names.split(",") if n.strip()]
105
+ if not name_list:
106
+ print("ERROR: --names must contain at least one topic name")
107
+ sys.exit(1)
108
+ body = {"topics": [{"name": n} for n in name_list]}
109
+ result = api_post(f"/api/projects/{pid}/topics/batch", key, base, body)
110
+ created = extract_data(result)
111
+ if args.json:
112
+ print_json(created)
113
+ sys.exit(0)
114
+ items = created if isinstance(created, list) else created.get("topics") or created.get("items") or []
115
+ print(f"✓ Created {len(items)} topics:")
116
+ for t in items:
117
+ print(f" {t.get('id', '')[:36]} {t.get('name', '')}")
118
+
119
+ # ── UPDATE ────────────────────────────────────────────────────────────────────
120
+ elif args.action == "update":
121
+ if not args.topic_id:
122
+ print("ERROR: --topic-id is required for update")
123
+ sys.exit(1)
124
+ body = {}
125
+ if args.name:
126
+ body["name"] = args.name
127
+ if args.description:
128
+ body["description"] = args.description
129
+ if not body:
130
+ print("ERROR: Provide at least one of --name or --description to update")
131
+ sys.exit(1)
132
+ result = api_put(f"/api/projects/{pid}/topics/{args.topic_id}", key, base, body)
133
+ topic = extract_data(result)
134
+ if args.json:
135
+ print_json(topic)
136
+ sys.exit(0)
137
+ print(f"✓ Updated topic: {topic.get('name')}")
138
+
139
+ # ── DELETE ────────────────────────────────────────────────────────────────────
140
+ elif args.action == "delete":
141
+ if not args.topic_id:
142
+ print("ERROR: --topic-id is required for delete")
143
+ sys.exit(1)
144
+ api_delete(f"/api/projects/{pid}/topics/{args.topic_id}", key, base)
145
+ print(f"✓ Deleted topic {args.topic_id}")
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: adgine/geo-visibility
3
+ description: Reads and analyzes already-collected AI visibility data for a GEO
4
+ project — Visibility Score, Share of Voice, Average Position, competitor matrix,
5
+ historical AI executions, full AI responses, brand mentions, cited URLs.
6
+ Use when the user wants to READ analytics results: AI visibility / 可见性得分 /
7
+ Visibility Score / 声量份额 / Share of Voice / 平均排名 / 竞品对比 / 平台矩阵 /
8
+ AI 回答历史 / prompt 历史测试 / AI 具体怎么回复的 / 品牌提及 / 引用链接.
9
+ NOT for running new citation tests (向 AI 平台发请求测试是否引用) — use
10
+ adgine-geo-citation to actively submit prompts and collect new results.
11
+ NOT for lightweight dashboard summary — use adgine-geo-dashboard instead.
12
+ ---
13
+
14
+ # GEO Visibility (Analytics)
15
+
16
+ Deep analytics over `/api/projects/{id}/analytics/*` — 11 endpoints covering
17
+ the AI visibility funnel from brand-level single metrics down to individual
18
+ prompt executions.
19
+
20
+ > Lightweight dashboard summary lives in **adgine-geo-dashboard**.
21
+ > Prompt CRUD / start-analysis actions live in **adgine-geo-topics**.
22
+
23
+ ## 触发条件
24
+
25
+ 当用户说出以下意图时使用本 skill:
26
+ - “可见性得分” / “Visibility Score” / “我的可见度”
27
+ - “声量份额” / “Share of Voice” / “平均排名” / “Average Position”
28
+ - “竞品对比” / “平台矩阵” / “competitor matrix”
29
+ - “AI 回答历史” / “AI 具体怎么回复的” / “prompt 历史测试结果”
30
+ - “品牌提及” / “引用链接”(查看已有数据)
31
+
32
+ **⛔ 以下意图不属于本 skill:**
33
+ - “跑一次引用测试” / “测试 AI 是否引用”(发起新测试)→ **adgine-geo-citation**
34
+ - “项目总览” / “Dashboard” / “近七天趋势” → **adgine-geo-dashboard**
35
+ - “创建提示词” / “管理主题” → **adgine-geo-topics**
36
+
37
+ ## Output rules — IDs (apply to every reply)
38
+
39
+ 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.
40
+
41
+ 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.
42
+ - Index numbers restart from 1 in each new list — they are not stable across calls.
43
+ - 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.
44
+
45
+ 2. **Single-item operations — prefer a human name over an ID.**
46
+ - ✅ *"Project **Poki vs Competitors** deleted."*
47
+ - ✅ *"Topic **Brand mentions in 2024** updated — name → 'Brand mentions 2025'."*
48
+ - ❌ *"Project `a4305b57-1c79-4cec-a17c-16eb1d959ea6` deleted."*
49
+ - 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.
50
+
51
+ 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.
52
+
53
+ 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.
54
+
55
+ ---
56
+
57
+ ## Step 1: Make sure GEO_API_KEY is configured
58
+
59
+ 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).
60
+
61
+ - ✅ Key already in `<skills-root>/.env` → proceed.
62
+ - ❌ 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.
63
+
64
+ > ⚠️ **IMPORTANT:** In all shell/exec commands, always reference the key as `$GEO_API_KEY` (the environment variable). Never hardcode the literal value.
65
+
66
+ ## Project selection
67
+
68
+ ```bash
69
+ export GEO_PROJECT_ID=<project-id>
70
+ ```
71
+
72
+ ## Scripts
73
+
74
+ ### Brand-level single metrics
75
+
76
+ ```bash
77
+ python3 scripts/get_visibility.py score
78
+ python3 scripts/get_visibility.py share-of-voice
79
+ python3 scripts/get_visibility.py average-position
80
+ # common opts: --platform openai|google_aio|perplexity|gemini --start ... --end ...
81
+ ```
82
+
83
+ ### Competitor × platform matrix
84
+
85
+ ```bash
86
+ python3 scripts/get_matrix.py # default: visibility metric
87
+ python3 scripts/get_matrix.py --metric sov
88
+ ```
89
+
90
+ ### Topic-level metrics
91
+
92
+ ```bash
93
+ python3 scripts/get_topic_metrics.py list # full Topic dimension
94
+ python3 scripts/get_topic_metrics.py visibility # lightweight picker list
95
+ python3 scripts/get_topic_metrics.py prompts --topic-id <id> # prompts under topic
96
+ python3 scripts/get_topic_metrics.py prompts-visibility --topic-id <id>
97
+ ```
98
+
99
+ ### Per-prompt overview
100
+
101
+ ```bash
102
+ python3 scripts/get_prompt_metrics.py --prompt-id <prompt_id>
103
+ # adds: previous-period delta + per-platform breakdown
104
+ ```
105
+
106
+ ### Prompt execution history & detail
107
+
108
+ ```bash
109
+ python3 scripts/get_execution.py list --prompt-id <prompt_id>
110
+ python3 scripts/get_execution.py list --prompt-id <pid> --platform openai
111
+ python3 scripts/get_execution.py get --prompt-id <pid> --execution-id <eid>
112
+ ```
113
+
114
+ ## Output Format
115
+
116
+ ASCII tables only inside fenced code blocks. Numeric formatting: 1 or 2
117
+ decimals depending on metric scale; "%" suffix for percentage metrics;
118
+ "--" for null. Change-from-previous-period is shown with explicit sign
119
+ ("+1.5" / "-0.3").
120
+
121
+ ## Metric vocabulary
122
+
123
+ | Metric | Unit | Typical range |
124
+ |---|---|---|
125
+ | Visibility Score | % | 0–100 |
126
+ | Share of Voice | % | 0–100 |
127
+ | Average Position | rank | 1+ (lower is better) |
128
+
129
+ ## Platform codes
130
+
131
+ `openai` · `google_aio` · `perplexity` · `gemini`
132
+
133
+ ## Related endpoints
134
+
135
+ | Method | Path |
136
+ |---|---|
137
+ | GET | `/api/projects/{id}/analytics/visibility/score` |
138
+ | GET | `/api/projects/{id}/analytics/visibility/share-of-voice` |
139
+ | GET | `/api/projects/{id}/analytics/visibility/average-position` |
140
+ | GET | `/api/projects/{id}/analytics/platforms/matrix` |
141
+ | GET | `/api/projects/{id}/analytics/topics` |
142
+ | GET | `/api/projects/{id}/analytics/topics/visibility` |
143
+ | GET | `/api/projects/{id}/analytics/topics/{topic_id}/prompts` |
144
+ | GET | `/api/projects/{id}/analytics/topics/{topic_id}/prompts/visibility` |
145
+ | GET | `/api/projects/{id}/analytics/prompts/{prompt_id}/overview` |
146
+ | GET | `/api/projects/{id}/analytics/prompts/{prompt_id}/executions` |
147
+ | GET | `/api/projects/{id}/analytics/prompts/{prompt_id}/executions/{execution_id}` |
148
+
149
+ ---
150
+
151
+ ## Post-task recommendations
152
+
153
+ After visibility analysis, suggest actions to improve or act on the results:
154
+
155
+ | What you saw | → use skill (agent-internal) |
156
+ |---|---|
157
+ | Low Visibility Score | 运行新引用测试,诊断哪些平台未引用 *(→ adgine-geo-citation)*|
158
+ | Low Share of Voice | 针对弱项话题生成更多 GEO 内容 *(→ adgine-geo-content)*|
159
+ | Competitor outperforming | 优化品牌画像,强化差异化定位 *(→ adgine-geo-brand)*|
160
+ | Specific prompt performing well | 基于该 prompt 生成扩展文章 *(→ adgine-geo-content)*|
161
+ | Specific prompt performing poorly | 优化或替换该 prompt *(→ adgine-geo-topics)*|
162
+ | Execution history reviewed | 查看对应页面的 AI 爬虫访问数据 *(→ adgine-geo-aiagent)*|
163
+
164
+ **⚠️ 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.
165
+
166
+ > 💡 **建议下一步:**
167
+ > 1. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*
168
+ > 2. **[行动标题]** — *"[可直接发送给 AI 的自然语言提示词]"*