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,125 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cited_cli.output.formatter import OutputContext, print_error, print_result
9
+ from cited_cli.output.tables import render_bar
10
+ from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
11
+ from cited_core.api import endpoints
12
+ from cited_core.api.client import CitedClient
13
+ from cited_core.auth.store import TokenStore
14
+ from cited_core.config.manager import ConfigManager
15
+
16
+ hq_app = typer.Typer(name="hq", help="Business HQ dashboard.", invoke_without_command=True)
17
+
18
+
19
+ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
20
+ obj = ctx.obj or {}
21
+ out: OutputContext = obj.get("output", OutputContext())
22
+ cfg: ConfigManager = obj.get("config", ConfigManager())
23
+ profile = obj.get("profile", "default")
24
+ env = cfg.get_environment(profile, obj.get("env_override"))
25
+ api_url = cfg.get_api_url(profile, obj.get("env_override"))
26
+
27
+ store = TokenStore()
28
+ token = store.get_token(env)
29
+ if not token:
30
+ print_error(f"Not logged in to {env}. Run: cited login", out)
31
+ raise typer.Exit(ExitCode.AUTH_ERROR)
32
+
33
+ return out, CitedClient(base_url=api_url, token=token)
34
+
35
+
36
+ @hq_app.callback(invoke_without_command=True)
37
+ def hq_dashboard(
38
+ ctx: typer.Context,
39
+ business_id: Annotated[str, typer.Argument(help="Business ID")],
40
+ full: Annotated[bool, typer.Option("--full", help="Include all data (heavy)")] = False,
41
+ personas: Annotated[bool, typer.Option("--personas", help="Include personas")] = False,
42
+ products: Annotated[bool, typer.Option("--products", help="Include products")] = False,
43
+ intents: Annotated[bool, typer.Option("--intents", help="Include buyer intents")] = False,
44
+ actions: Annotated[bool, typer.Option("--actions", help="Include priority actions")] = False,
45
+ ) -> None:
46
+ """Show the Business HQ dashboard."""
47
+ if ctx.invoked_subcommand is not None:
48
+ return
49
+
50
+ out, client = _get_client(ctx)
51
+ try:
52
+ # Fetch base HQ data
53
+ if full:
54
+ path = endpoints.HQ_HEAVY.format(business_id=business_id)
55
+ else:
56
+ path = endpoints.HQ.format(business_id=business_id)
57
+ data = client.get(path)
58
+
59
+ # Fetch additional sections if requested
60
+ if personas and not full:
61
+ data["personas"] = client.get(endpoints.PERSONAS.format(business_id=business_id))
62
+ if products and not full:
63
+ data["products"] = client.get(endpoints.PRODUCTS.format(business_id=business_id))
64
+ if intents and not full:
65
+ path = endpoints.BUYER_INTENTS.format(business_id=business_id)
66
+ data["buyer_intents"] = client.get(path)
67
+ if actions and not full:
68
+ path = endpoints.HQ_PRIORITY.format(business_id=business_id)
69
+ data["priority_actions"] = client.get(path)
70
+
71
+ def _human(d: object, console: Console) -> None:
72
+ if not isinstance(d, dict):
73
+ return
74
+ name = d.get("business", {}).get("name", d.get("name", "Business HQ"))
75
+ console.print()
76
+ console.rule(f"[bold]{name}[/bold]")
77
+
78
+ # Health scores
79
+ scores = d.get("health_scores", d.get("scores", {}))
80
+ if scores:
81
+ console.print("\n[bold]Health Scores[/bold]")
82
+ if isinstance(scores, dict):
83
+ for label, val in scores.items():
84
+ if isinstance(val, (int, float)):
85
+ render_bar(label.replace("_", " ").title(), val, console=console)
86
+
87
+ # Personas
88
+ if d.get("personas"):
89
+ console.print("\n[bold]Personas[/bold]")
90
+ p_list = (
91
+ d["personas"] if isinstance(d["personas"], list)
92
+ else d["personas"].get("items", [])
93
+ )
94
+ for p in p_list:
95
+ console.print(f" - {p.get('name', 'Unknown')}")
96
+
97
+ # Products
98
+ if d.get("products"):
99
+ console.print("\n[bold]Products[/bold]")
100
+ prod_list = (
101
+ d["products"] if isinstance(d["products"], list)
102
+ else d["products"].get("items", [])
103
+ )
104
+ for p in prod_list:
105
+ console.print(f" - {p.get('name', 'Unknown')}")
106
+
107
+ # Priority actions
108
+ if d.get("priority_actions"):
109
+ console.print("\n[bold]Priority Actions[/bold]")
110
+ pa = d["priority_actions"]
111
+ act_list = (
112
+ pa if isinstance(pa, list)
113
+ else pa.get("items", [])
114
+ )
115
+ for a in act_list:
116
+ label = a.get('title', a.get('description', ''))
117
+ console.print(f" [{a.get('priority', '?')}] {label}")
118
+
119
+ console.print()
120
+
121
+ print_result(data, out, human_formatter=_human)
122
+ except CitedAPIError as e:
123
+ handle_api_error(e, out.json_mode)
124
+ finally:
125
+ client.close()
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from cited_cli.output.formatter import OutputContext, print_error, print_result, print_success
8
+ from cited_cli.output.progress import watch_job
9
+ from cited_cli.output.tables import render_kv
10
+ from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
11
+ from cited_cli.utils.interactive import confirm_action
12
+ from cited_core.api.client import CitedClient
13
+ from cited_core.auth.store import TokenStore
14
+ from cited_core.config.manager import ConfigManager
15
+
16
+ job_app = typer.Typer(name="job", help="Monitor and manage background jobs.")
17
+
18
+
19
+ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
20
+ obj = ctx.obj or {}
21
+ out: OutputContext = obj.get("output", OutputContext())
22
+ cfg: ConfigManager = obj.get("config", ConfigManager())
23
+ profile = obj.get("profile", "default")
24
+ env = cfg.get_environment(profile, obj.get("env_override"))
25
+ api_url = cfg.get_api_url(profile, obj.get("env_override"))
26
+
27
+ store = TokenStore()
28
+ token = store.get_token(env)
29
+ if not token:
30
+ print_error(f"Not logged in to {env}. Run: cited login", out)
31
+ raise typer.Exit(ExitCode.AUTH_ERROR)
32
+
33
+ return out, CitedClient(base_url=api_url, token=token)
34
+
35
+
36
+ def _guess_job_type(job_id: str, client: CitedClient) -> str:
37
+ """Try to determine job type by probing status endpoints."""
38
+ for prefix in ["/audit", "/recommendations", "/solutions"]:
39
+ try:
40
+ client.get(f"{prefix}/{job_id}/status")
41
+ return prefix.strip("/")
42
+ except CitedAPIError:
43
+ continue
44
+ return "audit" # default fallback
45
+
46
+
47
+ @job_app.command("watch")
48
+ def job_watch(
49
+ ctx: typer.Context,
50
+ job_id: Annotated[str, typer.Argument(help="Job ID to watch")],
51
+ job_type: Annotated[
52
+ str,
53
+ typer.Option("--type", "-t", help="Job type: audit, recommendations, solutions"),
54
+ ] = "",
55
+ interval: Annotated[
56
+ float,
57
+ typer.Option("--interval", "-i", help="Poll interval in seconds"),
58
+ ] = 2.0,
59
+ ) -> None:
60
+ """Watch a job's progress until completion."""
61
+ out, client = _get_client(ctx)
62
+ try:
63
+ if not job_type:
64
+ job_type = _guess_job_type(job_id, client)
65
+
66
+ status_path = f"/{job_type}/{job_id}/status"
67
+ result = watch_job(client, status_path, out.console, poll_interval=interval)
68
+
69
+ status = result.get("status", "unknown")
70
+ if status in ("completed", "complete", "done"):
71
+ print_success(f"Job {job_id[:8]} completed", out)
72
+ elif status in ("failed", "error"):
73
+ print_error(f"Job {job_id[:8]} failed: {result.get('error', '')}", out)
74
+ raise typer.Exit(ExitCode.ERROR)
75
+ else:
76
+ print_result(result, out, human_formatter=lambda d, c: render_kv("Job Result", d, c))
77
+ except CitedAPIError as e:
78
+ handle_api_error(e, out.json_mode)
79
+ finally:
80
+ client.close()
81
+
82
+
83
+ @job_app.command("cancel")
84
+ def job_cancel(
85
+ ctx: typer.Context,
86
+ job_id: Annotated[str, typer.Argument(help="Job ID to cancel")],
87
+ job_type: Annotated[
88
+ str,
89
+ typer.Option("--type", "-t", help="Job type: audit, recommendations, solutions"),
90
+ ] = "",
91
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
92
+ ) -> None:
93
+ """Cancel a running job."""
94
+ out, client = _get_client(ctx)
95
+ confirm_action(f"Cancel job {job_id[:8]}?", out, skip=yes)
96
+ try:
97
+ if not job_type:
98
+ job_type = _guess_job_type(job_id, client)
99
+
100
+ cancel_path = f"/{job_type}/{job_id}/cancel"
101
+ client.post(cancel_path)
102
+ print_success(f"Cancelled job {job_id[:8]}", out)
103
+ except CitedAPIError as e:
104
+ handle_api_error(e, out.json_mode)
105
+ finally:
106
+ client.close()
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ mcp_app = typer.Typer(help="MCP server for AI agent integration.", no_args_is_help=True)
10
+
11
+
12
+ @mcp_app.command("serve")
13
+ def mcp_serve(
14
+ env: Annotated[
15
+ str | None,
16
+ typer.Option("--env", "-e", help="Target environment (dev, prod, local)"),
17
+ ] = None,
18
+ ) -> None:
19
+ """Start the MCP server (stdio transport) for AI agent tool calls."""
20
+ try:
21
+ from cited_mcp.server import run_server
22
+ except ImportError:
23
+ print(
24
+ 'MCP dependencies not installed. Run: pip install "cited-cli[mcp]"',
25
+ file=sys.stderr,
26
+ )
27
+ raise typer.Exit(1) # noqa: B904
28
+ if env:
29
+ os.environ["CITED_ENV"] = env
30
+ run_server()
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cited_cli.output.formatter import OutputContext, print_error, print_result, print_success
9
+ from cited_cli.output.tables import render_kv, render_table
10
+ from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
11
+ from cited_cli.utils.interactive import can_prompt, prompt_if_missing
12
+ from cited_core.api import endpoints
13
+ from cited_core.api.client import CitedClient
14
+ from cited_core.auth.store import TokenStore
15
+ from cited_core.config.manager import ConfigManager
16
+
17
+ named_audit_app = typer.Typer(name="template", help="Manage audit templates.")
18
+
19
+
20
+ def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
21
+ obj = ctx.obj or {}
22
+ out: OutputContext = obj.get("output", OutputContext())
23
+ cfg: ConfigManager = obj.get("config", ConfigManager())
24
+ profile = obj.get("profile", "default")
25
+ env = cfg.get_environment(profile, obj.get("env_override"))
26
+ api_url = cfg.get_api_url(profile, obj.get("env_override"))
27
+
28
+ store = TokenStore()
29
+ token = store.get_token(env)
30
+ if not token:
31
+ print_error(f"Not logged in to {env}. Run: cited login", out)
32
+ raise typer.Exit(ExitCode.AUTH_ERROR)
33
+
34
+ return out, CitedClient(base_url=api_url, token=token)
35
+
36
+
37
+ @named_audit_app.command("list")
38
+ def template_list(
39
+ ctx: typer.Context,
40
+ business_id: Annotated[
41
+ str | None,
42
+ typer.Option("--business", "-b", help="Filter by business ID"),
43
+ ] = None,
44
+ ) -> None:
45
+ """List audit templates."""
46
+ out, client = _get_client(ctx)
47
+ try:
48
+ params = {}
49
+ if business_id:
50
+ params["business_id"] = business_id
51
+ data = client.get(endpoints.NAMED_AUDITS, params=params or None)
52
+ templates = (
53
+ data if isinstance(data, list)
54
+ else data.get("named_audits", data.get("items", []))
55
+ )
56
+
57
+ def _human(d: object, console: Console) -> None:
58
+ items = d if isinstance(d, list) else []
59
+ rows = []
60
+ for t in items:
61
+ questions = t.get("questions", [])
62
+ rows.append([
63
+ t.get("id", "")[:8],
64
+ t.get("name", ""),
65
+ t.get("business_name", t.get("business_id", "")),
66
+ str(len(questions)),
67
+ t.get("created_at", "")[:19] if t.get("created_at") else "",
68
+ ])
69
+ render_table(
70
+ "Audit Templates",
71
+ ["ID", "Name", "Business", "Questions", "Created"],
72
+ rows,
73
+ console,
74
+ )
75
+
76
+ print_result(templates, out, human_formatter=_human)
77
+ except CitedAPIError as e:
78
+ handle_api_error(e, out.json_mode)
79
+ finally:
80
+ client.close()
81
+
82
+
83
+ @named_audit_app.command("get")
84
+ def template_get(
85
+ ctx: typer.Context,
86
+ named_audit_id: Annotated[str, typer.Argument(help="Audit template ID")],
87
+ ) -> None:
88
+ """Get audit template details."""
89
+ out, client = _get_client(ctx)
90
+ try:
91
+ path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
92
+ data = client.get(path)
93
+
94
+ def _human(d: object, console: Console) -> None:
95
+ if not isinstance(d, dict):
96
+ console.print(d)
97
+ return
98
+ questions = d.pop("questions", [])
99
+ render_kv("Audit Template", d, console)
100
+ if questions:
101
+ console.print("\n[bold]Questions:[/bold]")
102
+ for i, q in enumerate(questions, 1):
103
+ text = q.get("question", q) if isinstance(q, dict) else q
104
+ console.print(f" {i}. {text}")
105
+
106
+ print_result(data, out, human_formatter=_human)
107
+ except CitedAPIError as e:
108
+ handle_api_error(e, out.json_mode)
109
+ finally:
110
+ client.close()
111
+
112
+
113
+ @named_audit_app.command("create")
114
+ def template_create(
115
+ ctx: typer.Context,
116
+ name: Annotated[str | None, typer.Option("--name", "-n", help="Template name")] = None,
117
+ business_id: Annotated[str | None, typer.Option("--business", "-b", help="Business ID")] = None,
118
+ description: Annotated[
119
+ str | None, typer.Option("--description", "-d", help="Template description")
120
+ ] = None,
121
+ questions: Annotated[
122
+ list[str] | None,
123
+ typer.Option("--question", "-q", help="Question to include (repeatable)"),
124
+ ] = None,
125
+ ) -> None:
126
+ """Create a new audit template."""
127
+ out, client = _get_client(ctx)
128
+ name = prompt_if_missing(name, "--name", "Template name", out)
129
+ business_id = prompt_if_missing(business_id, "--business", "Business ID", out)
130
+ if not questions and can_prompt(out):
131
+ out.console.print("\n[bold]Enter questions (one per line, empty line to finish):[/bold]")
132
+ entered: list[str] = []
133
+ while True:
134
+ q = typer.prompt(f" Q{len(entered) + 1}", default="")
135
+ if not q:
136
+ break
137
+ entered.append(q)
138
+ if entered:
139
+ questions = entered
140
+ try:
141
+ payload: dict[str, object] = {
142
+ "name": name,
143
+ "business_id": business_id,
144
+ }
145
+ if description:
146
+ payload["description"] = description
147
+ if questions:
148
+ payload["questions"] = [{"question": q} for q in questions]
149
+
150
+ data = client.post(endpoints.NAMED_AUDITS, json=payload)
151
+ template_id = data.get("id", "")
152
+ print_result(data, out, human_formatter=lambda d, c: render_kv("Template Created", d, c))
153
+ if not out.json_mode and template_id:
154
+ out.console.print(
155
+ f"\nTemplate ID: [bold]{template_id}[/bold]"
156
+ f"\nRun audit: [bold]cited audit start {template_id}"
157
+ f" --business {business_id}[/bold]"
158
+ )
159
+ except CitedAPIError as e:
160
+ handle_api_error(e, out.json_mode)
161
+ finally:
162
+ client.close()
163
+
164
+
165
+ @named_audit_app.command("update")
166
+ def template_update(
167
+ ctx: typer.Context,
168
+ named_audit_id: Annotated[str, typer.Argument(help="Audit template ID to update")],
169
+ name: Annotated[
170
+ str | None, typer.Option("--name", "-n", help="New template name")
171
+ ] = None,
172
+ description: Annotated[
173
+ str | None, typer.Option("--description", "-d", help="New description")
174
+ ] = None,
175
+ questions: Annotated[
176
+ list[str] | None,
177
+ typer.Option(
178
+ "--question", "-q",
179
+ help="Replacement question (repeatable — replaces all existing)",
180
+ ),
181
+ ] = None,
182
+ ) -> None:
183
+ """Update an audit template's name, description, or questions.
184
+
185
+ When --question flags are provided, they replace ALL existing questions.
186
+ Omit --question to keep existing questions unchanged.
187
+ """
188
+ out, client = _get_client(ctx)
189
+ try:
190
+ payload: dict[str, object] = {}
191
+ if name is not None:
192
+ payload["name"] = name
193
+ if description is not None:
194
+ payload["description"] = description
195
+ if questions is not None:
196
+ payload["questions"] = [{"question": q} for q in questions]
197
+
198
+ if not payload:
199
+ print_error("Nothing to update. Provide --name, --description, or --question.", out)
200
+ raise typer.Exit(ExitCode.VALIDATION_ERROR)
201
+
202
+ path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
203
+ data = client.put(path, json=payload)
204
+
205
+ def _human(d: object, console: Console) -> None:
206
+ if not isinstance(d, dict):
207
+ console.print(d)
208
+ return
209
+ questions_list = d.pop("questions", [])
210
+ render_kv("Template Updated", d, console)
211
+ if questions_list:
212
+ console.print("\n[bold]Questions:[/bold]")
213
+ for i, q in enumerate(questions_list, 1):
214
+ text = q.get("question", q) if isinstance(q, dict) else q
215
+ console.print(f" {i}. {text}")
216
+
217
+ print_result(data, out, human_formatter=_human)
218
+ except CitedAPIError as e:
219
+ handle_api_error(e, out.json_mode)
220
+ finally:
221
+ client.close()
222
+
223
+
224
+ @named_audit_app.command("delete")
225
+ def template_delete(
226
+ ctx: typer.Context,
227
+ named_audit_id: Annotated[str, typer.Argument(help="Audit template ID to delete")],
228
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
229
+ ) -> None:
230
+ """Delete an audit template."""
231
+ out, client = _get_client(ctx)
232
+ try:
233
+ if not yes:
234
+ confirmed = typer.confirm(f"Delete template {named_audit_id}?")
235
+ if not confirmed:
236
+ raise typer.Abort()
237
+
238
+ path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
239
+ client.delete(path)
240
+ print_success(f"Template {named_audit_id} deleted.", out)
241
+ except CitedAPIError as e:
242
+ handle_api_error(e, out.json_mode)
243
+ finally:
244
+ client.close()