cited-cli 0.3.2__py3-none-any.whl

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 (48) hide show
  1. cited_cli/__init__.py +1 -0
  2. cited_cli/__main__.py +3 -0
  3. cited_cli/api/__init__.py +0 -0
  4. cited_cli/api/client.py +111 -0
  5. cited_cli/api/endpoints.py +88 -0
  6. cited_cli/app.py +199 -0
  7. cited_cli/auth/__init__.py +0 -0
  8. cited_cli/auth/oauth_server.py +123 -0
  9. cited_cli/auth/store.py +83 -0
  10. cited_cli/commands/__init__.py +0 -0
  11. cited_cli/commands/agent.py +129 -0
  12. cited_cli/commands/analytics.py +83 -0
  13. cited_cli/commands/audit.py +177 -0
  14. cited_cli/commands/auth.py +353 -0
  15. cited_cli/commands/business.py +217 -0
  16. cited_cli/commands/config_cmd.py +111 -0
  17. cited_cli/commands/hq.py +125 -0
  18. cited_cli/commands/job.py +106 -0
  19. cited_cli/commands/mcp_cmd.py +30 -0
  20. cited_cli/commands/named_audit.py +244 -0
  21. cited_cli/commands/recommend.py +211 -0
  22. cited_cli/commands/solution.py +154 -0
  23. cited_cli/config/__init__.py +0 -0
  24. cited_cli/config/constants.py +48 -0
  25. cited_cli/config/manager.py +69 -0
  26. cited_cli/mcp/__init__.py +1 -0
  27. cited_cli/mcp/context.py +12 -0
  28. cited_cli/mcp/server.py +54 -0
  29. cited_cli/mcp/tools/__init__.py +1 -0
  30. cited_cli/mcp/tools/_helpers.py +33 -0
  31. cited_cli/mcp/tools/audit.py +129 -0
  32. cited_cli/mcp/tools/auth.py +23 -0
  33. cited_cli/mcp/tools/business.py +96 -0
  34. cited_cli/mcp/tools/job.py +57 -0
  35. cited_cli/mcp/tools/recommend.py +96 -0
  36. cited_cli/mcp/tools/solution.py +66 -0
  37. cited_cli/output/__init__.py +0 -0
  38. cited_cli/output/formatter.py +61 -0
  39. cited_cli/output/progress.py +56 -0
  40. cited_cli/output/tables.py +49 -0
  41. cited_cli/utils/__init__.py +0 -0
  42. cited_cli/utils/errors.py +31 -0
  43. cited_cli/utils/interactive.py +83 -0
  44. cited_cli-0.3.2.dist-info/METADATA +515 -0
  45. cited_cli-0.3.2.dist-info/RECORD +48 -0
  46. cited_cli-0.3.2.dist-info/WHEEL +4 -0
  47. cited_cli-0.3.2.dist-info/entry_points.txt +2 -0
  48. cited_cli-0.3.2.dist-info/licenses/LICENSE +4 -0
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+
14
+ @mcp.tool()
15
+ async def list_audit_templates(ctx: Context[Any, CitedContext, Any]) -> Any:
16
+ """List all audit templates (named audits) for the user."""
17
+ cited_ctx = _get_ctx(ctx)
18
+ if err := _auth_check(cited_ctx):
19
+ return err
20
+ try:
21
+ return cited_ctx.client.get(endpoints.NAMED_AUDITS)
22
+ except CitedAPIError as e:
23
+ return _api_error_response(e)
24
+
25
+
26
+ @mcp.tool()
27
+ async def get_audit_template(ctx: Context[Any, CitedContext, Any], named_audit_id: str) -> Any:
28
+ """Get a specific audit template by ID."""
29
+ cited_ctx = _get_ctx(ctx)
30
+ if err := _auth_check(cited_ctx):
31
+ return err
32
+ try:
33
+ return cited_ctx.client.get(
34
+ endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
35
+ )
36
+ except CitedAPIError as e:
37
+ return _api_error_response(e)
38
+
39
+
40
+ @mcp.tool()
41
+ async def create_audit_template(
42
+ ctx: Context[Any, CitedContext, Any],
43
+ name: str,
44
+ business_id: str,
45
+ description: str | None = None,
46
+ questions: list[str] | None = None,
47
+ ) -> Any:
48
+ """Create a new audit template.
49
+
50
+ Args:
51
+ ctx: MCP context
52
+ name: Template name
53
+ business_id: Business ID to associate with
54
+ description: Optional template description
55
+ questions: Optional list of audit questions
56
+ """
57
+ cited_ctx = _get_ctx(ctx)
58
+ if err := _auth_check(cited_ctx):
59
+ return err
60
+ payload: dict[str, Any] = {"name": name, "business_id": business_id}
61
+ if description is not None:
62
+ payload["description"] = description
63
+ if questions is not None:
64
+ payload["questions"] = questions
65
+ try:
66
+ return cited_ctx.client.post(endpoints.NAMED_AUDITS, json=payload)
67
+ except CitedAPIError as e:
68
+ return _api_error_response(e)
69
+
70
+
71
+ @mcp.tool()
72
+ async def start_audit(
73
+ ctx: Context[Any, CitedContext, Any],
74
+ named_audit_id: str,
75
+ business_id: str | None = None,
76
+ ) -> Any:
77
+ """Start an audit using a template. Returns a job_id to poll for status.
78
+
79
+ Args:
80
+ ctx: MCP context
81
+ named_audit_id: The audit template ID to run
82
+ business_id: Optional business ID override
83
+ """
84
+ cited_ctx = _get_ctx(ctx)
85
+ if err := _auth_check(cited_ctx):
86
+ return err
87
+ payload: dict[str, Any] = {"named_audit_id": named_audit_id}
88
+ if business_id is not None:
89
+ payload["business_id"] = business_id
90
+ try:
91
+ return cited_ctx.client.post(endpoints.AUDIT_START, json=payload)
92
+ except CitedAPIError as e:
93
+ return _api_error_response(e)
94
+
95
+
96
+ @mcp.tool()
97
+ async def get_audit_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
98
+ """Check the status of a running audit job."""
99
+ cited_ctx = _get_ctx(ctx)
100
+ if err := _auth_check(cited_ctx):
101
+ return err
102
+ try:
103
+ return cited_ctx.client.get(endpoints.AUDIT_STATUS.format(job_id=job_id))
104
+ except CitedAPIError as e:
105
+ return _api_error_response(e)
106
+
107
+
108
+ @mcp.tool()
109
+ async def get_audit_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
110
+ """Get the results of a completed audit job."""
111
+ cited_ctx = _get_ctx(ctx)
112
+ if err := _auth_check(cited_ctx):
113
+ return err
114
+ try:
115
+ return cited_ctx.client.get(endpoints.AUDIT_RESULT.format(job_id=job_id))
116
+ except CitedAPIError as e:
117
+ return _api_error_response(e)
118
+
119
+
120
+ @mcp.tool()
121
+ async def list_audits(ctx: Context[Any, CitedContext, Any]) -> Any:
122
+ """List audit history."""
123
+ cited_ctx = _get_ctx(ctx)
124
+ if err := _auth_check(cited_ctx):
125
+ return err
126
+ try:
127
+ return cited_ctx.client.get(endpoints.AUDIT_HISTORY)
128
+ except CitedAPIError as e:
129
+ return _api_error_response(e)
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+
14
+ @mcp.tool()
15
+ async def check_auth_status(ctx: Context[Any, CitedContext, Any]) -> Any:
16
+ """Check if the user is authenticated and return their account info."""
17
+ cited_ctx = _get_ctx(ctx)
18
+ if err := _auth_check(cited_ctx):
19
+ return err
20
+ try:
21
+ return cited_ctx.client.get(endpoints.ME)
22
+ except CitedAPIError as e:
23
+ return _api_error_response(e)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+
14
+ @mcp.tool()
15
+ async def list_businesses(ctx: Context[Any, CitedContext, Any]) -> Any:
16
+ """List all businesses for the authenticated user."""
17
+ cited_ctx = _get_ctx(ctx)
18
+ if err := _auth_check(cited_ctx):
19
+ return err
20
+ try:
21
+ return cited_ctx.client.get(endpoints.BUSINESSES)
22
+ except CitedAPIError as e:
23
+ return _api_error_response(e)
24
+
25
+
26
+ @mcp.tool()
27
+ async def get_business(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
28
+ """Get details for a specific business by ID."""
29
+ cited_ctx = _get_ctx(ctx)
30
+ if err := _auth_check(cited_ctx):
31
+ return err
32
+ try:
33
+ return cited_ctx.client.get(endpoints.BUSINESS.format(business_id=business_id))
34
+ except CitedAPIError as e:
35
+ return _api_error_response(e)
36
+
37
+
38
+ @mcp.tool()
39
+ async def create_business(
40
+ ctx: Context[Any, CitedContext, Any],
41
+ name: str,
42
+ website: str,
43
+ description: str,
44
+ industry: str = "technology",
45
+ ) -> Any:
46
+ """Create a new business.
47
+
48
+ Args:
49
+ ctx: MCP context
50
+ name: Business name
51
+ website: Business website (must be a publicly DNS-resolvable domain)
52
+ description: Business description (minimum ~50 characters)
53
+ industry: One of: automotive, beauty, consulting, education, entertainment,
54
+ finance, fitness, government, healthcare, home_services, hospitality,
55
+ legal, manufacturing, non_profit, real_estate, restaurant, retail,
56
+ technology, other
57
+ """
58
+ cited_ctx = _get_ctx(ctx)
59
+ if err := _auth_check(cited_ctx):
60
+ return err
61
+ try:
62
+ return cited_ctx.client.post(
63
+ endpoints.BUSINESSES,
64
+ json={
65
+ "name": name,
66
+ "website": website,
67
+ "description": description,
68
+ "industry": industry,
69
+ },
70
+ )
71
+ except CitedAPIError as e:
72
+ return _api_error_response(e)
73
+
74
+
75
+ @mcp.tool()
76
+ async def crawl_business(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
77
+ """Start a crawl job for a business. Returns a job_id you can poll with get_job_status."""
78
+ cited_ctx = _get_ctx(ctx)
79
+ if err := _auth_check(cited_ctx):
80
+ return err
81
+ try:
82
+ return cited_ctx.client.post(endpoints.CRAWL_START.format(business_id=business_id))
83
+ except CitedAPIError as e:
84
+ return _api_error_response(e)
85
+
86
+
87
+ @mcp.tool()
88
+ async def get_health_scores(ctx: Context[Any, CitedContext, Any], business_id: str) -> Any:
89
+ """Get GEO health scores for a business."""
90
+ cited_ctx = _get_ctx(ctx)
91
+ if err := _auth_check(cited_ctx):
92
+ return err
93
+ try:
94
+ return cited_ctx.client.get(endpoints.HEALTH_SCORES.format(business_id=business_id))
95
+ except CitedAPIError as e:
96
+ return _api_error_response(e)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+ _STATUS_ENDPOINTS = {
14
+ "audit": endpoints.AUDIT_STATUS,
15
+ "recommendation": endpoints.RECOMMEND_STATUS,
16
+ "solution": endpoints.SOLUTION_STATUS,
17
+ }
18
+
19
+
20
+ @mcp.tool()
21
+ async def get_job_status(
22
+ ctx: Context[Any, CitedContext, Any],
23
+ job_id: str,
24
+ job_type: str | None = None,
25
+ ) -> Any:
26
+ """Get the status of any job by ID.
27
+
28
+ Args:
29
+ ctx: MCP context
30
+ job_id: The job ID to check
31
+ job_type: Optional job type hint: audit, recommendation, or solution.
32
+ If not provided, will probe each type until one responds.
33
+ """
34
+ cited_ctx = _get_ctx(ctx)
35
+ if err := _auth_check(cited_ctx):
36
+ return err
37
+
38
+ if job_type and job_type in _STATUS_ENDPOINTS:
39
+ try:
40
+ result = cited_ctx.client.get(
41
+ _STATUS_ENDPOINTS[job_type].format(job_id=job_id)
42
+ )
43
+ result["job_type"] = job_type
44
+ return result
45
+ except CitedAPIError as e:
46
+ return _api_error_response(e)
47
+
48
+ # Probe each type
49
+ for jtype, endpoint_tpl in _STATUS_ENDPOINTS.items():
50
+ try:
51
+ result = cited_ctx.client.get(endpoint_tpl.format(job_id=job_id))
52
+ result["job_type"] = jtype
53
+ return result
54
+ except CitedAPIError:
55
+ continue
56
+
57
+ return {"error": True, "message": f"No job found with ID: {job_id}"}
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+
14
+ @mcp.tool()
15
+ async def start_recommendation(ctx: Context[Any, CitedContext, Any], audit_job_id: str) -> Any:
16
+ """Start a recommendation job based on a completed audit.
17
+
18
+ Args:
19
+ ctx: MCP context
20
+ audit_job_id: The completed audit job ID to generate recommendations for
21
+ """
22
+ cited_ctx = _get_ctx(ctx)
23
+ if err := _auth_check(cited_ctx):
24
+ return err
25
+ try:
26
+ return cited_ctx.client.post(
27
+ endpoints.RECOMMEND_START,
28
+ json={"audit_job_id": audit_job_id},
29
+ )
30
+ except CitedAPIError as e:
31
+ return _api_error_response(e)
32
+
33
+
34
+ @mcp.tool()
35
+ async def get_recommendation_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
36
+ """Check the status of a recommendation job."""
37
+ cited_ctx = _get_ctx(ctx)
38
+ if err := _auth_check(cited_ctx):
39
+ return err
40
+ try:
41
+ return cited_ctx.client.get(endpoints.RECOMMEND_STATUS.format(job_id=job_id))
42
+ except CitedAPIError as e:
43
+ return _api_error_response(e)
44
+
45
+
46
+ @mcp.tool()
47
+ async def get_recommendation_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
48
+ """Get the full results of a completed recommendation job."""
49
+ cited_ctx = _get_ctx(ctx)
50
+ if err := _auth_check(cited_ctx):
51
+ return err
52
+ try:
53
+ return cited_ctx.client.get(endpoints.RECOMMEND_RESULT.format(job_id=job_id))
54
+ except CitedAPIError as e:
55
+ return _api_error_response(e)
56
+
57
+
58
+ @mcp.tool()
59
+ async def get_recommendation_insights(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
60
+ """Get actionable insights from a recommendation, with source_type and source_id for solutions.
61
+
62
+ Returns question_insights, head_to_head_comparisons, strengthening_tips, and
63
+ priority_actions, each annotated with the source_type and source_id needed to
64
+ start a solution via start_solution.
65
+ """
66
+ cited_ctx = _get_ctx(ctx)
67
+ if err := _auth_check(cited_ctx):
68
+ return err
69
+ try:
70
+ result = cited_ctx.client.get(endpoints.RECOMMEND_RESULT.format(job_id=job_id))
71
+ except CitedAPIError as e:
72
+ return _api_error_response(e)
73
+
74
+ insights: dict[str, Any] = {}
75
+
76
+ for item in result.get("question_insights", []):
77
+ item["source_type"] = "question_insight"
78
+ item["source_id"] = item.get("question_id", "")
79
+ insights["question_insights"] = result.get("question_insights", [])
80
+
81
+ for item in result.get("head_to_head_comparisons", []):
82
+ item["source_type"] = "head_to_head"
83
+ item["source_id"] = item.get("competitor_domain", "")
84
+ insights["head_to_head_comparisons"] = result.get("head_to_head_comparisons", [])
85
+
86
+ for item in result.get("strengthening_tips", []):
87
+ item["source_type"] = "strengthening_tip"
88
+ item["source_id"] = item.get("category", "")
89
+ insights["strengthening_tips"] = result.get("strengthening_tips", [])
90
+
91
+ for item in result.get("priority_actions", []):
92
+ item["source_type"] = "priority_action"
93
+ item["source_id"] = item.get("id", item.get("category", ""))
94
+ insights["priority_actions"] = result.get("priority_actions", [])
95
+
96
+ return insights
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from mcp.server.fastmcp import Context
6
+
7
+ from cited_cli.api import endpoints
8
+ from cited_cli.mcp.context import CitedContext
9
+ from cited_cli.mcp.server import mcp
10
+ from cited_cli.mcp.tools._helpers import _api_error_response, _auth_check, _get_ctx
11
+ from cited_cli.utils.errors import CitedAPIError
12
+
13
+
14
+ @mcp.tool()
15
+ async def start_solution(
16
+ ctx: Context[Any, CitedContext, Any],
17
+ recommendation_job_id: str,
18
+ source_type: str,
19
+ source_id: str,
20
+ ) -> Any:
21
+ """Start generating a solution for a specific recommendation insight.
22
+
23
+ Args:
24
+ ctx: MCP context
25
+ recommendation_job_id: The recommendation job ID
26
+ source_type: One of: question_insight, head_to_head, strengthening_tip, priority_action
27
+ source_id: The source identifier (question_id, competitor_domain, category, etc.)
28
+ """
29
+ cited_ctx = _get_ctx(ctx)
30
+ if err := _auth_check(cited_ctx):
31
+ return err
32
+ try:
33
+ return cited_ctx.client.post(
34
+ endpoints.SOLUTION_REQUEST,
35
+ json={
36
+ "recommendation_job_id": recommendation_job_id,
37
+ "source_type": source_type,
38
+ "source_id": source_id,
39
+ },
40
+ )
41
+ except CitedAPIError as e:
42
+ return _api_error_response(e)
43
+
44
+
45
+ @mcp.tool()
46
+ async def get_solution_status(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
47
+ """Check the status of a solution generation job."""
48
+ cited_ctx = _get_ctx(ctx)
49
+ if err := _auth_check(cited_ctx):
50
+ return err
51
+ try:
52
+ return cited_ctx.client.get(endpoints.SOLUTION_STATUS.format(job_id=job_id))
53
+ except CitedAPIError as e:
54
+ return _api_error_response(e)
55
+
56
+
57
+ @mcp.tool()
58
+ async def get_solution_result(ctx: Context[Any, CitedContext, Any], job_id: str) -> Any:
59
+ """Get the results of a completed solution generation job."""
60
+ cited_ctx = _get_ctx(ctx)
61
+ if err := _auth_check(cited_ctx):
62
+ return err
63
+ try:
64
+ return cited_ctx.client.get(endpoints.SOLUTION_RESULT.format(job_id=job_id))
65
+ except CitedAPIError as e:
66
+ return _api_error_response(e)
File without changes
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from rich.console import Console
10
+
11
+
12
+ @dataclass
13
+ class OutputContext:
14
+ json_mode: bool = False
15
+ quiet: bool = False
16
+ no_color: bool = False
17
+ _console: Console | None = field(default=None, repr=False)
18
+
19
+ @property
20
+ def console(self) -> Console:
21
+ if self._console is None:
22
+ self._console = Console(
23
+ no_color=self.no_color,
24
+ quiet=self.quiet and not self.json_mode,
25
+ )
26
+ return self._console
27
+
28
+
29
+ def print_result(
30
+ data: Any,
31
+ ctx: OutputContext,
32
+ human_formatter: Callable[[Any, Console], None] | None = None,
33
+ ) -> None:
34
+ if ctx.json_mode:
35
+ sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
36
+ elif human_formatter:
37
+ human_formatter(data, ctx.console)
38
+ else:
39
+ ctx.console.print_json(json.dumps(data, default=str))
40
+
41
+
42
+ def print_success(message: str, ctx: OutputContext) -> None:
43
+ if ctx.json_mode:
44
+ sys.stdout.write(json.dumps({"status": "ok", "message": message}) + "\n")
45
+ elif not ctx.quiet:
46
+ ctx.console.print(f"[green]✓[/green] {message}")
47
+
48
+
49
+ def print_error(message: str, ctx: OutputContext, status_code: int | None = None) -> None:
50
+ if ctx.json_mode:
51
+ err: dict[str, Any] = {"error": True, "message": message}
52
+ if status_code:
53
+ err["status_code"] = status_code
54
+ sys.stderr.write(json.dumps(err) + "\n")
55
+ else:
56
+ Console(stderr=True).print(f"[red]Error:[/red] {message}")
57
+
58
+
59
+ def print_warning(message: str, ctx: OutputContext) -> None:
60
+ if not ctx.json_mode and not ctx.quiet:
61
+ ctx.console.print(f"[yellow]Warning:[/yellow] {message}")
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from rich.console import Console
6
+ from rich.live import Live
7
+ from rich.spinner import Spinner
8
+ from rich.text import Text
9
+
10
+ from cited_cli.utils.errors import CitedAPIError
11
+ from cited_core.api.client import CitedClient
12
+
13
+
14
+ def watch_job(
15
+ client: CitedClient,
16
+ status_path: str,
17
+ console: Console | None = None,
18
+ poll_interval: float = 2.0,
19
+ ) -> dict[str, object]:
20
+ """Poll a job status endpoint until completion. Returns the final status response."""
21
+ c = console or Console()
22
+
23
+ with Live(Spinner("dots", text="Starting..."), console=c, refresh_per_second=4) as live:
24
+ while True:
25
+ try:
26
+ data = client.get(status_path)
27
+ except CitedAPIError:
28
+ raise
29
+
30
+ status = data.get("status", "unknown")
31
+ progress = data.get("progress")
32
+ message = data.get("message", "")
33
+
34
+ if progress is not None:
35
+ pct = (
36
+ int(progress * 100)
37
+ if isinstance(progress, float) and progress <= 1
38
+ else int(progress)
39
+ )
40
+ bar_w = 20
41
+ filled = int((pct / 100) * bar_w)
42
+ bar = f"{'█' * filled}{'░' * (bar_w - filled)}"
43
+ text = Text.from_markup(
44
+ f"[cyan]{bar}[/cyan] {pct}% — {status} {message}"
45
+ )
46
+ else:
47
+ text = Text.from_markup(f"[cyan]⟳[/cyan] {status} {message}")
48
+
49
+ live.update(Spinner("dots", text=text))
50
+
51
+ if status in ("completed", "complete", "done", "failed", "error", "cancelled"):
52
+ break
53
+
54
+ time.sleep(poll_interval)
55
+
56
+ return data # type: ignore[no-any-return]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.table import Table
8
+
9
+
10
+ def render_table(
11
+ title: str,
12
+ columns: list[str],
13
+ rows: list[list[Any]],
14
+ console: Console | None = None,
15
+ ) -> None:
16
+ c = console or Console()
17
+ table = Table(title=title, show_header=True, header_style="bold cyan")
18
+ for col in columns:
19
+ table.add_column(col)
20
+ for row in rows:
21
+ table.add_row(*[str(v) for v in row])
22
+ c.print(table)
23
+
24
+
25
+ def render_kv(
26
+ title: str,
27
+ items: dict[str, Any],
28
+ console: Console | None = None,
29
+ ) -> None:
30
+ c = console or Console()
31
+ lines = []
32
+ for key, value in items.items():
33
+ lines.append(f"[bold]{key}:[/bold] {value}")
34
+ panel = Panel("\n".join(lines), title=title, border_style="cyan")
35
+ c.print(panel)
36
+
37
+
38
+ def render_bar(
39
+ label: str,
40
+ value: int | float,
41
+ max_value: int | float = 100,
42
+ console: Console | None = None,
43
+ ) -> None:
44
+ c = console or Console()
45
+ bar_width = 20
46
+ filled = int((value / max_value) * bar_width) if max_value > 0 else 0
47
+ empty = bar_width - filled
48
+ bar = f"[green]{'█' * filled}[/green][dim]{'░' * empty}[/dim]"
49
+ c.print(f" {label:<25} {bar} {value:.0f}%")
File without changes
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from rich.console import Console
6
+
7
+ from cited_core.errors import CitedAPIError as CitedAPIError # noqa: F401
8
+ from cited_core.errors import ExitCode as ExitCode # noqa: F401
9
+ from cited_core.errors import exit_code_for_status as exit_code_for_status # noqa: F401
10
+
11
+ err_console = Console(stderr=True)
12
+
13
+
14
+ def handle_api_error(error: CitedAPIError, json_mode: bool = False) -> None:
15
+ code = exit_code_for_status(error.status_code)
16
+ if json_mode:
17
+ import json
18
+
19
+ err_console.print(
20
+ json.dumps(
21
+ {
22
+ "error": True,
23
+ "status_code": error.status_code,
24
+ "message": error.message,
25
+ "error_code": error.error_code,
26
+ }
27
+ )
28
+ )
29
+ else:
30
+ err_console.print(f"[red]Error[/red] ({error.status_code}): {error.message}")
31
+ sys.exit(code)