@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,108 @@
1
+ #!/usr/bin/env python3
2
+ """Generate a full article from an approved outline (async job).
3
+
4
+ The content item must have status='outline' and a completed outline.
5
+ Polls until the article is done (~5–10 min), then prints a summary.
6
+
7
+ Usage:
8
+ python3 scripts/generate_article.py --content-id <cid> [--project-id <id>] [--json]
9
+ """
10
+ import sys
11
+ import os
12
+ import argparse
13
+
14
+ sys.path.insert(0, os.path.dirname(__file__))
15
+ from _client import (
16
+ get_api_config, get_project_id,
17
+ api_get, api_post,
18
+ extract_data, poll_job, print_json,
19
+ )
20
+
21
+ parser = argparse.ArgumentParser(description="Generate a full article from outline")
22
+ parser.add_argument("--content-id", required=True,
23
+ help="Content item ID (must have status=outline)")
24
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
25
+ parser.add_argument("--json", action="store_true", help="Output raw job result as JSON")
26
+ parser.add_argument("--show-article", action="store_true",
27
+ help="Print the full article text (can be long)")
28
+ args = parser.parse_args()
29
+
30
+ key, base = get_api_config()
31
+ pid = get_project_id(args.project_id)
32
+
33
+ # Quick pre-check: verify content exists and has outline status
34
+ content_result = api_get(f"/api/projects/{pid}/content/{args.content_id}", key, base)
35
+ content = extract_data(content_result)
36
+ status = content.get("status", "")
37
+ if status not in ("outline", "article"):
38
+ print(f"WARNING: Content status is '{status}'. Expected 'outline' to generate an article.")
39
+ if status == "draft":
40
+ print(" Generate an outline first: python3 scripts/generate_outline.py --topic-id <tid> --prompt-ids <ids>")
41
+ sys.exit(1)
42
+
43
+ title = content.get("article_title") or "(untitled)"
44
+ print(f"Generating article for: {title}")
45
+ print(f" Content ID : {args.content_id}")
46
+ print()
47
+
48
+ body = {"content_id": args.content_id}
49
+ result = api_post(f"/api/projects/{pid}/content/generate-article", key, base, body)
50
+ job_data = extract_data(result)
51
+
52
+ job_id = job_data.get("job_id") or job_data.get("id") or job_data.get("article_job_id")
53
+ if not job_id:
54
+ print("ERROR: Article generate endpoint did not return a job ID.")
55
+ print_json(job_data)
56
+ sys.exit(1)
57
+
58
+ print(f"Job ID: {job_id}")
59
+ print("Polling for completion (this typically takes 5–10 minutes)...")
60
+
61
+ # Job status polled via: api_get(f"/api/projects/{pid}/content/jobs/{job_id}", key, base)
62
+ final_job = poll_job(
63
+ f"/api/projects/{pid}/content/jobs/{job_id}",
64
+ key, base,
65
+ interval=10,
66
+ max_wait=900,
67
+ )
68
+
69
+ if args.json:
70
+ print_json(final_job)
71
+ sys.exit(0)
72
+
73
+ if final_job.get("status") == "failed":
74
+ print(f"ERROR: Article generation failed — {final_job.get('error', 'unknown error')}")
75
+ sys.exit(1)
76
+
77
+ output = final_job.get("output") or {}
78
+ content_id_out = output.get("content_id") or args.content_id
79
+
80
+ # Retrieve the final content
81
+ final_content_result = api_get(f"/api/projects/{pid}/content/{content_id_out}", key, base)
82
+ article = extract_data(final_content_result)
83
+
84
+ print(f"\nArticle generation completed!")
85
+ print(f" Content ID : {article.get('id')}")
86
+ print(f" Title : {article.get('article_title') or '(untitled)'}")
87
+ print(f" Status : {article.get('status')}")
88
+ print(f" Publish status : {article.get('publish_status') or '—'}")
89
+ print(f" Word count : {article.get('word_count', 0)}")
90
+ print(f" Meta title : {(article.get('meta_title') or '—')[:80]}")
91
+ print(f" Meta desc : {(article.get('meta_description') or '—')[:120]}")
92
+ print(f" Slug : {article.get('meta_slug') or '—'}")
93
+
94
+ if article.get("faq_section"):
95
+ print(f"\n[FAQ Section — first 500 chars]")
96
+ faq = article["faq_section"]
97
+ print(faq[:500] + ("..." if len(faq) > 500 else ""))
98
+
99
+ if args.show_article:
100
+ full = article.get("full_content") or article.get("article_body") or ""
101
+ if full:
102
+ print(f"\n{'='*60}")
103
+ print(full)
104
+ else:
105
+ print("\nno full_content returned yet")
106
+ else:
107
+ print(f"\nTo print the full article, re-run with --show-article")
108
+ print(f" python3 scripts/generate_article.py --content-id {content_id_out} --show-article")
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """Generate an AI-optimized article outline (async job).
3
+
4
+ Starts an outline generation job, polls until completed (~10–15 min), then prints
5
+ the outline and the content_id needed for article generation.
6
+
7
+ Usage:
8
+ python3 scripts/generate_outline.py \
9
+ --topic-id <tid> --prompt-ids <id1,id2,...> \
10
+ [--project-id <id>] [--title "Your Article Title"] \
11
+ [--reference-urls "https://url1,https://url2"] \
12
+ [--instructions "Target CMO audience, emphasize ROI"] \
13
+ [--json]
14
+ """
15
+ import sys
16
+ import os
17
+ import argparse
18
+
19
+ sys.path.insert(0, os.path.dirname(__file__))
20
+ from _client import (
21
+ get_api_config, get_project_id,
22
+ api_post, extract_data, poll_job, print_json,
23
+ )
24
+
25
+ parser = argparse.ArgumentParser(description="Generate GEO article outline")
26
+ parser.add_argument("--topic-id", required=True, help="Topic ID")
27
+ parser.add_argument("--prompt-ids", required=True, help="Comma-separated prompt IDs")
28
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
29
+ parser.add_argument("--title", help="Article title (auto-generated if omitted)")
30
+ parser.add_argument("--reference-urls", help="Comma-separated reference/competitor URLs")
31
+ parser.add_argument("--instructions", help="Additional AI guidance (audience, tone, focus)")
32
+ parser.add_argument("--json", action="store_true", help="Output raw job result as JSON")
33
+ args = parser.parse_args()
34
+
35
+ key, base = get_api_config()
36
+ pid = get_project_id(args.project_id)
37
+
38
+ prompt_ids = [p.strip() for p in args.prompt_ids.split(",") if p.strip()]
39
+ if not prompt_ids:
40
+ print("ERROR: --prompt-ids must contain at least one ID")
41
+ sys.exit(1)
42
+
43
+ body = {
44
+ "topic_id": args.topic_id,
45
+ "prompt_ids": prompt_ids,
46
+ }
47
+ if args.title:
48
+ body["article_title"] = args.title
49
+ if args.reference_urls:
50
+ body["reference_urls"] = [u.strip() for u in args.reference_urls.split(",") if u.strip()]
51
+ if args.instructions:
52
+ body["custom_instructions"] = args.instructions
53
+
54
+ print(f"Starting outline generation...")
55
+ print(f" Topic : {args.topic_id}")
56
+ print(f" Prompts : {len(prompt_ids)}")
57
+ if args.title:
58
+ print(f" Title : {args.title}")
59
+ print()
60
+
61
+ result = api_post(f"/api/projects/{pid}/content/generate-outline", key, base, body)
62
+ job_data = extract_data(result)
63
+
64
+ # The response may contain the job directly (id = job id) or have separate job_id / content_id fields
65
+ job_id = job_data.get("job_id") or job_data.get("outline_job_id") or job_data.get("id")
66
+ content_id = job_data.get("content_id") # may be None; resolved from job output after polling
67
+
68
+ if not job_id:
69
+ print("ERROR: Outline endpoint did not return a job ID.")
70
+ print_json(job_data)
71
+ sys.exit(1)
72
+
73
+ print(f"Job ID: {job_id}")
74
+ print("Polling for completion (this typically takes 10–15 minutes)...")
75
+
76
+ # Job status polled via: api_get(f"/api/projects/{pid}/content/jobs/{job_id}", key, base)
77
+ final_job = poll_job(
78
+ f"/api/projects/{pid}/content/jobs/{job_id}",
79
+ key, base,
80
+ interval=10,
81
+ max_wait=1200,
82
+ )
83
+
84
+ if args.json:
85
+ print_json(final_job)
86
+ sys.exit(0)
87
+
88
+ status = final_job.get("status", "")
89
+ if status == "failed":
90
+ print(f"ERROR: Outline generation failed — {final_job.get('error', 'unknown error')}")
91
+ sys.exit(1)
92
+
93
+ output = final_job.get("output") or {}
94
+ content_id = output.get("content_id") or content_id
95
+ title = output.get("article_title") or args.title or "(auto-generated)"
96
+ outline = output.get("page_outline") or ""
97
+
98
+ print(f"\nOutline generation completed!")
99
+ print(f" Content ID : {content_id}")
100
+ print(f" Title : {title}")
101
+ print()
102
+
103
+ if outline:
104
+ # Print first 2000 chars of outline
105
+ if len(outline) > 2000:
106
+ print(outline[:2000])
107
+ print(f"\n [... outline continues — {len(outline)} chars total ...]")
108
+ else:
109
+ print(outline)
110
+
111
+ print()
112
+ print("Next: generate the full article with:")
113
+ print(f" python3 scripts/generate_article.py --content-id {content_id}")
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Suggest article titles for a given topic and set of prompts (sync, fast).
3
+
4
+ Usage:
5
+ python3 scripts/generate_titles.py --topic-id <tid> --prompt-ids <id1,id2,...>
6
+ [--project-id <id>] [--json]
7
+ """
8
+ import sys
9
+ import os
10
+ import argparse
11
+
12
+ sys.path.insert(0, os.path.dirname(__file__))
13
+ from _client import get_api_config, get_project_id, api_post, extract_data, print_json
14
+
15
+ parser = argparse.ArgumentParser(description="Suggest article titles for a topic")
16
+ parser.add_argument("--topic-id", required=True, help="Topic ID")
17
+ parser.add_argument("--prompt-ids", required=True, help="Comma-separated prompt IDs")
18
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
19
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
20
+ args = parser.parse_args()
21
+
22
+ key, base = get_api_config()
23
+ pid = get_project_id(args.project_id)
24
+
25
+ prompt_ids = [p.strip() for p in args.prompt_ids.split(",") if p.strip()]
26
+ if not prompt_ids:
27
+ print("ERROR: --prompt-ids must contain at least one prompt ID")
28
+ sys.exit(1)
29
+
30
+ body = {
31
+ "topic_id": args.topic_id,
32
+ "prompt_ids": prompt_ids,
33
+ }
34
+
35
+ result = api_post(f"/api/projects/{pid}/content/generate-titles", key, base, body)
36
+ data = extract_data(result)
37
+
38
+ if args.json:
39
+ print_json(data)
40
+ sys.exit(0)
41
+
42
+ titles = data.get("titles") or (data if isinstance(data, list) else [])
43
+ print(f"Suggested titles ({len(titles)}):")
44
+ print()
45
+ for i, title in enumerate(titles, 1):
46
+ print(f" {i:>2}. {title}")
47
+
48
+ print()
49
+ print("Use a title with generate_outline.py:")
50
+ print(f" python3 scripts/generate_outline.py --topic-id {args.topic_id} \\")
51
+ print(f" --prompt-ids {args.prompt_ids} --title \"<chosen title>\"")
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """List GEO content items for a project.
3
+
4
+ Usage:
5
+ python3 scripts/list_content.py [--project-id <id>] [--status draft|outline|article]
6
+ [--publish-status unpublished|published]
7
+ [--topic-id <id>] [--page 1] [--limit 20] [--json]
8
+ """
9
+ import sys
10
+ import os
11
+ import argparse
12
+
13
+ sys.path.insert(0, os.path.dirname(__file__))
14
+ from _client import get_api_config, get_project_id, api_get, extract_data, print_json
15
+
16
+ parser = argparse.ArgumentParser(description="List GEO content items")
17
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
18
+ parser.add_argument("--status", choices=["draft", "outline", "article"],
19
+ help="Filter by content status")
20
+ parser.add_argument("--publish-status", choices=["unpublished", "published"],
21
+ help="Filter by WordPress publish status")
22
+ parser.add_argument("--topic-id", help="Filter by topic ID")
23
+ parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
24
+ parser.add_argument("--limit", type=int, default=20, help="Results per page (default: 20)")
25
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
26
+ args = parser.parse_args()
27
+
28
+ key, base = get_api_config()
29
+ pid = get_project_id(args.project_id)
30
+
31
+ params = {"page": args.page, "limit": args.limit}
32
+ if args.status:
33
+ params["status"] = args.status
34
+ if args.publish_status:
35
+ params["publish_status"] = args.publish_status
36
+ if args.topic_id:
37
+ params["topic_id"] = args.topic_id
38
+
39
+ result = api_get(f"/api/projects/{pid}/content", key, base, params=params)
40
+ data = extract_data(result)
41
+ items = data if isinstance(data, list) else data.get("items") or data.get("content") or []
42
+
43
+ if args.json:
44
+ print_json(data)
45
+ sys.exit(0)
46
+
47
+ total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
48
+ filter_desc = f" status={args.status}" if args.status else ""
49
+ filter_desc += f" publish_status={args.publish_status}" if args.publish_status else ""
50
+ print(f"Content items ({len(items)} of {total}){filter_desc} | project: {pid}")
51
+ print()
52
+
53
+ if not items:
54
+ print(" No content found. Generate an outline with: python3 scripts/generate_outline.py")
55
+ sys.exit(0)
56
+
57
+ print(f" {'ID':<38} {'Status':<10} {'Words':>5} {'Pub.Status':<10} Title")
58
+ print(f" {'-'*38} {'-'*10} {'-'*5} {'-'*10} {'-'*50}")
59
+ for item in items:
60
+ cid = item.get("id", "")[:36]
61
+ status = item.get("status", "draft")
62
+ words = item.get("word_count", 0) or 0
63
+ pub_st = item.get("publish_status") or "—"
64
+ title = (item.get("article_title") or "(untitled)")[:60]
65
+ print(f" {cid:<38} {status:<10} {words:>5} {pub_st:<10} {title}")
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """Inspect, edit, or delete a single GEO content item.
3
+
4
+ Usage:
5
+ python3 scripts/manage_content.py get --content-id <id> [--project-id <id>] [--json]
6
+ python3 scripts/manage_content.py edit --content-id <id> [--title "..."] [--body-file path.md]
7
+ python3 scripts/manage_content.py delete --content-id <id>
8
+
9
+ The `get` action returns the full content detail (title, status, word count,
10
+ body, outline, metadata) for one record. Use `list_content.py` to discover IDs.
11
+ """
12
+ import sys
13
+ import os
14
+ import argparse
15
+
16
+ sys.path.insert(0, os.path.dirname(__file__))
17
+ from _client import (
18
+ get_api_config, get_project_id,
19
+ api_get, api_patch, api_delete,
20
+ extract_data, print_json,
21
+ )
22
+
23
+ parser = argparse.ArgumentParser(description="Manage a single GEO content item")
24
+ parser.add_argument("action", choices=["get", "edit", "delete"])
25
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
26
+ parser.add_argument("--content-id", required=True, help="Content ID")
27
+ parser.add_argument("--title", help="(edit) New article title")
28
+ parser.add_argument("--body-file", help="(edit) Path to a markdown file with the new body")
29
+ parser.add_argument("--status", help="(edit) New status (draft|outline|article|published)")
30
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
31
+ args = parser.parse_args()
32
+
33
+ key, base = get_api_config()
34
+ pid = get_project_id(args.project_id)
35
+
36
+ # ── GET ───────────────────────────────────────────────────────────────────────
37
+ if args.action == "get":
38
+ result = api_get(f"/api/projects/{pid}/content/{args.content_id}", key, base)
39
+ item = extract_data(result) or {}
40
+ if args.json:
41
+ print_json(item)
42
+ sys.exit(0)
43
+ print(f"Content {args.content_id}")
44
+ print()
45
+ print(f" Title : {item.get('article_title') or item.get('title') or '(untitled)'}")
46
+ print(f" Status : {item.get('status', '--')}")
47
+ print(f" Publish status: {item.get('publish_status') or '—'}")
48
+ print(f" Words : {item.get('word_count', 0)}")
49
+ print(f" Topic ID : {item.get('topic_id', '--')}")
50
+ print(f" Created : {item.get('created_at', '--')}")
51
+ print(f" Updated : {item.get('updated_at', '--')}")
52
+ body = item.get("article_body") or item.get("body") or ""
53
+ if body:
54
+ preview = body[:600]
55
+ print()
56
+ print(" --- body preview ---")
57
+ for line in preview.splitlines():
58
+ print(f" {line}")
59
+ if len(body) > 600:
60
+ print(f" [... truncated, {len(body)} chars total ...]")
61
+ sys.exit(0)
62
+
63
+ # ── EDIT ──────────────────────────────────────────────────────────────────────
64
+ if args.action == "edit":
65
+ body = {}
66
+ if args.title:
67
+ body["article_title"] = args.title
68
+ if args.body_file:
69
+ with open(args.body_file, "r", encoding="utf-8") as fh:
70
+ body["article_body"] = fh.read()
71
+ if args.status:
72
+ body["status"] = args.status
73
+ if not body:
74
+ print("ERROR: provide at least one of --title, --body-file or --status")
75
+ sys.exit(1)
76
+ result = api_patch(f"/api/projects/{pid}/content/{args.content_id}", key, base, body)
77
+ updated = extract_data(result) or {}
78
+ if args.json:
79
+ print_json(updated)
80
+ sys.exit(0)
81
+ print(f"✓ Updated content {args.content_id}")
82
+ print(f" Title : {updated.get('article_title') or updated.get('title', '')}")
83
+ print(f" Status : {updated.get('status', '--')}")
84
+ sys.exit(0)
85
+
86
+ # ── DELETE ────────────────────────────────────────────────────────────────────
87
+ if args.action == "delete":
88
+ api_delete(f"/api/projects/{pid}/content/{args.content_id}", key, base)
89
+ print(f"✓ Deleted content {args.content_id}")
90
+ sys.exit(0)
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """List, inspect and retry content generation jobs (outline + article workflows).
3
+
4
+ Subcommands:
5
+ list-outline — list outline-generation jobs
6
+ list-article — list article-generation jobs
7
+ list-workflow — list combined workflow jobs (jobs)
8
+ get-outline --job-id <id> — outline-jobs/{id}
9
+ get-article --job-id <id> — article-jobs/{id}
10
+ get-workflow --job-id <id> — jobs/{id} (overall workflow)
11
+ retry --job-id <id> — retry a failed workflow job
12
+
13
+ Usage examples:
14
+ python3 scripts/manage_jobs.py list-outline
15
+ python3 scripts/manage_jobs.py get-article --job-id <id>
16
+ python3 scripts/manage_jobs.py retry --job-id <id>
17
+ """
18
+ import sys
19
+ import os
20
+ import argparse
21
+
22
+ sys.path.insert(0, os.path.dirname(__file__))
23
+ from _client import (
24
+ get_api_config, get_project_id,
25
+ api_get, api_post,
26
+ extract_data, print_json, truncate,
27
+ pad,
28
+ )
29
+
30
+
31
+ def _norm_status(s):
32
+ s = (s or "").lower()
33
+ if s in ("completed", "complete", "success", "done"):
34
+ return "Completed"
35
+ if s in ("failed", "error"):
36
+ return "Failed"
37
+ if s in ("running", "generating", "in_progress"):
38
+ return "Generating"
39
+ if s in ("pending", "queued", "created"):
40
+ return "Pending"
41
+ return s.title() if s else "--"
42
+
43
+
44
+ def _print_list(items, args, title):
45
+ if args.json:
46
+ print_json(items)
47
+ return
48
+ print(f"{title} (page {args.page}, limit {args.limit})")
49
+ print()
50
+ if not items:
51
+ print("No jobs found.")
52
+ return
53
+ print("```")
54
+ print("┌──────────────────────────────────────┬──────────────┬──────────────────────┐")
55
+ print("│ Job ID │ Status │ Created at │")
56
+ print("├──────────────────────────────────────┼──────────────┼──────────────────────┤")
57
+ for j in items:
58
+ jid = truncate(j.get("id") or j.get("job_id"), 36)
59
+ st = _norm_status(j.get("status"))
60
+ created = truncate(j.get("created_at") or "--", 20)
61
+ print(f"│ {pad(jid, 36)} │ {pad(st, 12)} │ {pad(created, 20)} │")
62
+ print("└──────────────────────────────────────┴──────────────┴──────────────────────┘")
63
+ print("```")
64
+
65
+
66
+ def _print_detail(data, args, title):
67
+ if args.json:
68
+ print_json(data)
69
+ return
70
+ print(f"{title}: {args.job_id}")
71
+ print()
72
+ print("```")
73
+ print("┌────────────────────┬──────────────────────────────────────┐")
74
+ print("│ Field │ Value │")
75
+ print("├────────────────────┼──────────────────────────────────────┤")
76
+ for k in ("status", "current_phase", "progress", "content_id",
77
+ "topic_id", "created_at", "started_at", "completed_at",
78
+ "error_message"):
79
+ if k in data:
80
+ val = data.get(k)
81
+ if k == "status":
82
+ val = _norm_status(val)
83
+ print(f"│ {pad(k, 18)} │ {pad(truncate(val, 36), 36)} │")
84
+ print("└────────────────────┴──────────────────────────────────────┘")
85
+ print("```")
86
+
87
+
88
+ def cmd_list_outline(args, key, base, pid):
89
+ result = api_get(f"/api/projects/{pid}/content/outline-jobs", key, base,
90
+ params={"page": args.page, "limit": args.limit})
91
+ data = extract_data(result)
92
+ items = data if isinstance(data, list) else (data or {}).get("jobs") or (data or {}).get("items", [])
93
+ _print_list(items, args, "Outline jobs")
94
+
95
+
96
+ def cmd_list_article(args, key, base, pid):
97
+ result = api_get(f"/api/projects/{pid}/content/article-jobs", key, base,
98
+ params={"page": args.page, "limit": args.limit})
99
+ data = extract_data(result)
100
+ items = data if isinstance(data, list) else (data or {}).get("jobs") or (data or {}).get("items", [])
101
+ _print_list(items, args, "Article jobs")
102
+
103
+
104
+ def cmd_list_workflow(args, key, base, pid):
105
+ result = api_get(f"/api/projects/{pid}/content/jobs", key, base,
106
+ params={"page": args.page, "limit": args.limit})
107
+ data = extract_data(result)
108
+ items = data if isinstance(data, list) else (data or {}).get("jobs") or (data or {}).get("items", [])
109
+ _print_list(items, args, "Workflow jobs")
110
+
111
+
112
+ def cmd_get_outline(args, key, base, pid):
113
+ result = api_get(f"/api/projects/{pid}/content/outline-jobs/{args.job_id}", key, base)
114
+ data = extract_data(result) or {}
115
+ _print_detail(data, args, "Outline job")
116
+
117
+
118
+ def cmd_get_article(args, key, base, pid):
119
+ result = api_get(f"/api/projects/{pid}/content/article-jobs/{args.job_id}", key, base)
120
+ data = extract_data(result) or {}
121
+ _print_detail(data, args, "Article job")
122
+
123
+
124
+ def cmd_get_workflow(args, key, base, pid):
125
+ result = api_get(f"/api/projects/{pid}/content/jobs/{args.job_id}", key, base)
126
+ data = extract_data(result) or {}
127
+ _print_detail(data, args, "Workflow job")
128
+
129
+
130
+ def cmd_retry(args, key, base, pid):
131
+ result = api_post(f"/api/projects/{pid}/content/jobs/{args.job_id}/retry", key, base)
132
+ data = extract_data(result)
133
+ if args.json:
134
+ print_json(data if data is not None else {"ok": True})
135
+ return
136
+ print(f"Retried job: {args.job_id}")
137
+ if isinstance(data, dict) and data.get("status"):
138
+ print(f" Status: {_norm_status(data.get('status'))}")
139
+
140
+
141
+ def main():
142
+ parser = argparse.ArgumentParser(description="Manage content generation jobs")
143
+ parser.add_argument("--project-id", help="Project ID (or set GEO_PROJECT_ID env var)")
144
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
145
+ sub = parser.add_subparsers(dest="command", required=True)
146
+
147
+ for sub_name, _path, _title in [
148
+ ("list-outline", "/content/outline-jobs", "Outline jobs"),
149
+ ("list-article", "/content/article-jobs", "Article jobs"),
150
+ ("list-workflow", "/content/jobs", "Workflow jobs"),
151
+ ]:
152
+ p = sub.add_parser(sub_name)
153
+ p.add_argument("--page", type=int, default=1)
154
+ p.add_argument("--limit", type=int, default=20)
155
+
156
+ for sub_name, _path, _title in [
157
+ ("get-outline", "/content/outline-jobs", "Outline job"),
158
+ ("get-article", "/content/article-jobs", "Article job"),
159
+ ("get-workflow", "/content/jobs", "Workflow job"),
160
+ ]:
161
+ p = sub.add_parser(sub_name)
162
+ p.add_argument("--job-id", required=True)
163
+
164
+ p_retry = sub.add_parser("retry", help="Retry a failed workflow job")
165
+ p_retry.add_argument("--job-id", required=True)
166
+
167
+ args = parser.parse_args()
168
+ key, base = get_api_config()
169
+ pid = get_project_id(args.project_id)
170
+
171
+ handlers = {
172
+ "list-outline": cmd_list_outline,
173
+ "list-article": cmd_list_article,
174
+ "list-workflow": cmd_list_workflow,
175
+ "get-outline": cmd_get_outline,
176
+ "get-article": cmd_get_article,
177
+ "get-workflow": cmd_get_workflow,
178
+ "retry": cmd_retry,
179
+ }
180
+ handlers[args.command](args, key, base, pid)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()