cometapi-cli 0.1.0__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.
- cometapi_cli/__init__.py +3 -0
- cometapi_cli/app.py +85 -0
- cometapi_cli/client.py +270 -0
- cometapi_cli/commands/__init__.py +1 -0
- cometapi_cli/commands/account.py +39 -0
- cometapi_cli/commands/balance.py +56 -0
- cometapi_cli/commands/chat.py +104 -0
- cometapi_cli/commands/chat_repl.py +229 -0
- cometapi_cli/commands/config_cmd.py +174 -0
- cometapi_cli/commands/doctor.py +144 -0
- cometapi_cli/commands/logs.py +326 -0
- cometapi_cli/commands/models.py +44 -0
- cometapi_cli/commands/repl.py +134 -0
- cometapi_cli/commands/stats.py +39 -0
- cometapi_cli/commands/tasks.py +130 -0
- cometapi_cli/commands/tokens.py +87 -0
- cometapi_cli/config.py +102 -0
- cometapi_cli/console.py +8 -0
- cometapi_cli/constants.py +55 -0
- cometapi_cli/errors.py +113 -0
- cometapi_cli/formatters.py +156 -0
- cometapi_cli/main.py +8 -0
- cometapi_cli-0.1.0.dist-info/METADATA +228 -0
- cometapi_cli-0.1.0.dist-info/RECORD +27 -0
- cometapi_cli-0.1.0.dist-info/WHEEL +4 -0
- cometapi_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cometapi_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Interactive multi-turn chat REPL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from prompt_toolkit import PromptSession
|
|
11
|
+
from prompt_toolkit.formatted_text import HTML
|
|
12
|
+
from prompt_toolkit.history import FileHistory
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from ..config import CONFIG_DIR, get_client, get_default_model
|
|
18
|
+
from ..console import console, err_console
|
|
19
|
+
|
|
20
|
+
SLASH_COMMANDS = {
|
|
21
|
+
"/model": "Switch model: /model <name>",
|
|
22
|
+
"/system": "Set system prompt: /system <prompt>",
|
|
23
|
+
"/clear": "Clear conversation history",
|
|
24
|
+
"/history": "Show conversation history",
|
|
25
|
+
"/save": "Save conversation: /save <file.json|file.md>",
|
|
26
|
+
"/tokens": "Show approximate token count",
|
|
27
|
+
"/help": "Show available commands",
|
|
28
|
+
"/exit": "Exit chat (also Ctrl+D)",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _banner(model: str) -> Panel:
|
|
33
|
+
text = Text.assemble(
|
|
34
|
+
("CometAPI Chat", "bold cyan"),
|
|
35
|
+
" • Model: ",
|
|
36
|
+
(model, "bold green"),
|
|
37
|
+
"\n",
|
|
38
|
+
("Type /help for commands, /exit or Ctrl+D to quit.", "dim"),
|
|
39
|
+
)
|
|
40
|
+
return Panel(text, border_style="cyan", padding=(0, 1))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _estimate_tokens(messages: list[dict[str, str]]) -> int:
|
|
44
|
+
"""Rough token estimate (~4 chars per token)."""
|
|
45
|
+
total_chars = sum(len(m.get("content", "")) for m in messages)
|
|
46
|
+
return total_chars // 4
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_history(messages: list[dict[str, str]]) -> None:
|
|
50
|
+
for msg in messages:
|
|
51
|
+
role = msg["role"]
|
|
52
|
+
content = msg["content"]
|
|
53
|
+
if role == "system":
|
|
54
|
+
console.print(f"[bold magenta]system:[/] {content}")
|
|
55
|
+
elif role == "user":
|
|
56
|
+
console.print(f"[bold blue]you:[/] {content}")
|
|
57
|
+
elif role == "assistant":
|
|
58
|
+
console.print(f"[bold green]assistant:[/] {content}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _save_conversation(messages: list[dict[str, str]], filepath: str, model: str) -> None:
|
|
62
|
+
path = Path(filepath)
|
|
63
|
+
if path.suffix in (".json",):
|
|
64
|
+
data = {
|
|
65
|
+
"model": model,
|
|
66
|
+
"exported_at": datetime.now(tz=timezone.utc).isoformat(),
|
|
67
|
+
"messages": messages,
|
|
68
|
+
}
|
|
69
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
70
|
+
console.print(f"[green]Saved to {path}[/]")
|
|
71
|
+
elif path.suffix in (".md", ".markdown"):
|
|
72
|
+
lines = [f"# Chat Export — {model}\n"]
|
|
73
|
+
for msg in messages:
|
|
74
|
+
role = msg["role"].capitalize()
|
|
75
|
+
lines.append(f"## {role}\n")
|
|
76
|
+
lines.append(msg["content"])
|
|
77
|
+
lines.append("")
|
|
78
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
79
|
+
console.print(f"[green]Saved to {path}[/]")
|
|
80
|
+
else:
|
|
81
|
+
err_console.print("[yellow]Unsupported format. Use .json or .md[/]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _send_message(
|
|
85
|
+
client: object,
|
|
86
|
+
messages: list[dict[str, str]],
|
|
87
|
+
model: str,
|
|
88
|
+
stream: bool = True,
|
|
89
|
+
temperature: float | None = None,
|
|
90
|
+
max_tokens: int | None = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Send messages to the API and return the assistant response."""
|
|
93
|
+
kwargs: dict = {"model": model, "messages": messages, "stream": stream}
|
|
94
|
+
if temperature is not None:
|
|
95
|
+
kwargs["temperature"] = temperature
|
|
96
|
+
if max_tokens is not None:
|
|
97
|
+
kwargs["max_tokens"] = max_tokens
|
|
98
|
+
|
|
99
|
+
if stream:
|
|
100
|
+
response = client.chat.completions.create(**kwargs) # type: ignore[union-attr]
|
|
101
|
+
full_text = ""
|
|
102
|
+
for chunk in response:
|
|
103
|
+
if not chunk.choices:
|
|
104
|
+
continue
|
|
105
|
+
delta = chunk.choices[0].delta.content or ""
|
|
106
|
+
sys.stdout.write(delta)
|
|
107
|
+
sys.stdout.flush()
|
|
108
|
+
full_text += delta
|
|
109
|
+
sys.stdout.write("\n")
|
|
110
|
+
sys.stdout.flush()
|
|
111
|
+
return full_text
|
|
112
|
+
else:
|
|
113
|
+
response = client.chat.completions.create(**kwargs) # type: ignore[union-attr]
|
|
114
|
+
text = response.choices[0].message.content or ""
|
|
115
|
+
console.print(Markdown(text))
|
|
116
|
+
return text
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_chat_repl(
|
|
120
|
+
model: str | None = None,
|
|
121
|
+
system: str | None = None,
|
|
122
|
+
temperature: float | None = None,
|
|
123
|
+
max_tokens: int | None = None,
|
|
124
|
+
stream: bool = True,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Run interactive multi-turn chat REPL."""
|
|
127
|
+
client = get_client()
|
|
128
|
+
current_model = model or get_default_model()
|
|
129
|
+
|
|
130
|
+
messages: list[dict[str, str]] = []
|
|
131
|
+
if system:
|
|
132
|
+
messages.append({"role": "system", "content": system})
|
|
133
|
+
|
|
134
|
+
console.print(_banner(current_model))
|
|
135
|
+
|
|
136
|
+
# Set up prompt_toolkit session with persistent history
|
|
137
|
+
history_file = CONFIG_DIR / "chat_history"
|
|
138
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
session: PromptSession[str] = PromptSession(
|
|
140
|
+
history=FileHistory(str(history_file)),
|
|
141
|
+
multiline=False,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
try:
|
|
146
|
+
user_input = session.prompt(HTML("<b><ansiblue>you → </ansiblue></b>")).strip()
|
|
147
|
+
except (EOFError, KeyboardInterrupt):
|
|
148
|
+
console.print("\n[dim]Goodbye![/]")
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
if not user_input:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Handle slash commands
|
|
155
|
+
if user_input.startswith("/"):
|
|
156
|
+
cmd_parts = user_input.split(maxsplit=1)
|
|
157
|
+
cmd = cmd_parts[0].lower()
|
|
158
|
+
arg = cmd_parts[1] if len(cmd_parts) > 1 else ""
|
|
159
|
+
|
|
160
|
+
if cmd in ("/exit", "/quit", "/q"):
|
|
161
|
+
console.print("[dim]Goodbye![/]")
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
elif cmd == "/help":
|
|
165
|
+
for slash_cmd, desc in SLASH_COMMANDS.items():
|
|
166
|
+
console.print(f" [cyan]{slash_cmd:<12}[/] {desc}")
|
|
167
|
+
|
|
168
|
+
elif cmd == "/model":
|
|
169
|
+
if arg:
|
|
170
|
+
current_model = arg
|
|
171
|
+
console.print(f"[green]Model switched to {current_model}[/]")
|
|
172
|
+
else:
|
|
173
|
+
console.print(f"[cyan]Current model: {current_model}[/]")
|
|
174
|
+
|
|
175
|
+
elif cmd == "/system":
|
|
176
|
+
if arg:
|
|
177
|
+
# Remove existing system message if any
|
|
178
|
+
messages = [m for m in messages if m["role"] != "system"]
|
|
179
|
+
messages.insert(0, {"role": "system", "content": arg})
|
|
180
|
+
console.print(f"[green]System prompt set ({len(arg)} chars).[/]")
|
|
181
|
+
else:
|
|
182
|
+
sys_msg = next((m["content"] for m in messages if m["role"] == "system"), None)
|
|
183
|
+
if sys_msg:
|
|
184
|
+
console.print(f"[cyan]System prompt:[/] {sys_msg}")
|
|
185
|
+
else:
|
|
186
|
+
console.print("[dim]No system prompt set.[/]")
|
|
187
|
+
|
|
188
|
+
elif cmd == "/clear":
|
|
189
|
+
sys_msg = next((m for m in messages if m["role"] == "system"), None)
|
|
190
|
+
messages = [sys_msg] if sys_msg else []
|
|
191
|
+
console.print("[green]History cleared.[/]")
|
|
192
|
+
|
|
193
|
+
elif cmd == "/history":
|
|
194
|
+
if not messages:
|
|
195
|
+
console.print("[dim]No messages yet.[/]")
|
|
196
|
+
else:
|
|
197
|
+
_format_history(messages)
|
|
198
|
+
|
|
199
|
+
elif cmd == "/save":
|
|
200
|
+
if arg:
|
|
201
|
+
_save_conversation(messages, arg, current_model)
|
|
202
|
+
else:
|
|
203
|
+
err_console.print("[yellow]Usage: /save <filename.json|filename.md>[/]")
|
|
204
|
+
|
|
205
|
+
elif cmd == "/tokens":
|
|
206
|
+
tokens = _estimate_tokens(messages)
|
|
207
|
+
console.print(f"[cyan]~{tokens} tokens[/] in {len(messages)} messages")
|
|
208
|
+
|
|
209
|
+
else:
|
|
210
|
+
err_console.print(f"[yellow]Unknown command: {cmd}. Type /help for available commands.[/]")
|
|
211
|
+
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# Regular message — send to API
|
|
215
|
+
messages.append({"role": "user", "content": user_input})
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
reply = _send_message(
|
|
219
|
+
client, messages, current_model,
|
|
220
|
+
stream=stream, temperature=temperature, max_tokens=max_tokens,
|
|
221
|
+
)
|
|
222
|
+
messages.append({"role": "assistant", "content": reply})
|
|
223
|
+
except KeyboardInterrupt:
|
|
224
|
+
console.print("\n[dim]Response interrupted.[/]")
|
|
225
|
+
# Remove the user message that didn't get a response
|
|
226
|
+
messages.pop()
|
|
227
|
+
except Exception as e:
|
|
228
|
+
err_console.print(f"[red bold]Error:[/] {e}")
|
|
229
|
+
# Keep the user message in history for retry
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Config management and init wizard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
|
|
11
|
+
from ..config import CONFIG_FILE, VALID_KEYS, load_config, mask_secret, save_config
|
|
12
|
+
from ..console import console, err_console
|
|
13
|
+
from ..errors import handle_errors
|
|
14
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
15
|
+
|
|
16
|
+
config_app = typer.Typer(name="config", help="Manage CLI configuration.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@handle_errors
|
|
20
|
+
def init(ctx: typer.Context) -> None:
|
|
21
|
+
"""Interactive setup wizard — configure API keys and preferences."""
|
|
22
|
+
from ..config import get_default_model
|
|
23
|
+
|
|
24
|
+
console.print(
|
|
25
|
+
Panel(
|
|
26
|
+
"[bold blue]CometAPI CLI Setup[/bold blue]\n\n"
|
|
27
|
+
"Let's configure your CometAPI client.\n"
|
|
28
|
+
"You can change these settings later with [bold]cometapi config set[/bold].",
|
|
29
|
+
title="🚀 Welcome",
|
|
30
|
+
border_style="blue",
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
console.print(
|
|
35
|
+
Panel(
|
|
36
|
+
"[bold yellow]⚠ Security Notice[/bold yellow]\n\n"
|
|
37
|
+
"Your API key and access token are sensitive credentials.\n"
|
|
38
|
+
"• Never share keys in public repos, logs, or chat messages.\n"
|
|
39
|
+
"• You are responsible for all usage and charges incurred with your keys.\n\n"
|
|
40
|
+
"[bold]Create your API key at:[/bold] https://www.cometapi.com/console/token\n"
|
|
41
|
+
"[bold]Get your access token at:[/bold] https://www.cometapi.com/console/personal",
|
|
42
|
+
border_style="yellow",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
cfg = load_config()
|
|
47
|
+
|
|
48
|
+
# API Key
|
|
49
|
+
current_key = cfg.get("api_key", "")
|
|
50
|
+
default_display = mask_secret(current_key) if current_key else None
|
|
51
|
+
api_key = Prompt.ask(
|
|
52
|
+
"[bold]API Key[/bold] (COMETAPI_KEY)",
|
|
53
|
+
default=default_display,
|
|
54
|
+
)
|
|
55
|
+
if api_key and api_key != default_display:
|
|
56
|
+
cfg["api_key"] = api_key
|
|
57
|
+
|
|
58
|
+
# Validate connectivity
|
|
59
|
+
if cfg.get("api_key"):
|
|
60
|
+
console.print("[dim]Testing API connectivity...[/]", end=" ")
|
|
61
|
+
try:
|
|
62
|
+
from cometapi_cli.client import CometClient
|
|
63
|
+
|
|
64
|
+
test_client = CometClient(api_key=cfg["api_key"])
|
|
65
|
+
test_client.models.list()
|
|
66
|
+
console.print("[green]✓ Connected[/green]")
|
|
67
|
+
except Exception:
|
|
68
|
+
console.print("[yellow]⚠ Could not verify (check later with cometapi doctor)[/yellow]")
|
|
69
|
+
|
|
70
|
+
# Access Token (optional)
|
|
71
|
+
console.print()
|
|
72
|
+
current_token = cfg.get("access_token", "")
|
|
73
|
+
if Confirm.ask(
|
|
74
|
+
"[bold]Configure access token?[/bold] (for account/stats commands)",
|
|
75
|
+
default=bool(current_token),
|
|
76
|
+
):
|
|
77
|
+
default_token_display = mask_secret(current_token) if current_token else None
|
|
78
|
+
access_token = Prompt.ask(
|
|
79
|
+
"[bold]Access Token[/bold] (COMETAPI_ACCESS_TOKEN)",
|
|
80
|
+
default=default_token_display,
|
|
81
|
+
)
|
|
82
|
+
if access_token and access_token != default_token_display:
|
|
83
|
+
cfg["access_token"] = access_token
|
|
84
|
+
|
|
85
|
+
# Default model
|
|
86
|
+
console.print()
|
|
87
|
+
default_model = Prompt.ask(
|
|
88
|
+
"[bold]Default model[/bold]",
|
|
89
|
+
default=cfg.get("default_model", get_default_model()),
|
|
90
|
+
)
|
|
91
|
+
cfg["default_model"] = default_model
|
|
92
|
+
|
|
93
|
+
# Save
|
|
94
|
+
save_config(cfg)
|
|
95
|
+
|
|
96
|
+
console.print()
|
|
97
|
+
console.print(
|
|
98
|
+
Panel(
|
|
99
|
+
f"[green]Configuration saved to[/green] {CONFIG_FILE}\n"
|
|
100
|
+
"[dim]File permissions: 0600 (owner read/write only)[/dim]\n\n"
|
|
101
|
+
"[dim]Credentials are stored locally. Never commit this file to version control.[/dim]\n\n"
|
|
102
|
+
"Next steps:\n"
|
|
103
|
+
" [bold]cometapi doctor[/bold] — verify your setup\n"
|
|
104
|
+
" [bold]cometapi models[/bold] — list available models\n"
|
|
105
|
+
" [bold]cometapi chat[/bold] — start chatting",
|
|
106
|
+
title="✅ Setup Complete",
|
|
107
|
+
border_style="green",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@config_app.command("show")
|
|
113
|
+
@handle_errors
|
|
114
|
+
def config_show(
|
|
115
|
+
ctx: typer.Context,
|
|
116
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
117
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Display current configuration (secrets are masked)."""
|
|
120
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
121
|
+
cfg = load_config()
|
|
122
|
+
|
|
123
|
+
if not cfg:
|
|
124
|
+
console.print("[dim]No configuration file found. Run [bold]cometapi init[/bold] to create one.[/]")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
display = {}
|
|
128
|
+
for key, value in cfg.items():
|
|
129
|
+
if key in ("api_key", "access_token"):
|
|
130
|
+
display[key] = mask_secret(str(value))
|
|
131
|
+
else:
|
|
132
|
+
display[key] = value
|
|
133
|
+
|
|
134
|
+
output(display, fmt, title="CometAPI Configuration")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@config_app.command("set")
|
|
138
|
+
@handle_errors
|
|
139
|
+
def config_set(
|
|
140
|
+
key: Annotated[str, typer.Argument(help="Configuration key.")],
|
|
141
|
+
value: Annotated[str, typer.Argument(help="Configuration value.")],
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Set a configuration value."""
|
|
144
|
+
if key not in VALID_KEYS:
|
|
145
|
+
err_console.print(f"[red bold]Error:[/] Unknown key '{key}'. Valid keys: {', '.join(sorted(VALID_KEYS))}")
|
|
146
|
+
raise typer.Exit(code=2)
|
|
147
|
+
|
|
148
|
+
cfg = load_config()
|
|
149
|
+
cfg[key] = value
|
|
150
|
+
save_config(cfg)
|
|
151
|
+
display_value = mask_secret(value) if key in ("api_key", "access_token") else value
|
|
152
|
+
console.print(f"[green]✓[/green] Set [bold]{key}[/bold] = {display_value}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@config_app.command("unset")
|
|
156
|
+
@handle_errors
|
|
157
|
+
def config_unset(
|
|
158
|
+
key: Annotated[str, typer.Argument(help="Configuration key to remove.")],
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Remove a configuration value."""
|
|
161
|
+
cfg = load_config()
|
|
162
|
+
if key not in cfg:
|
|
163
|
+
err_console.print(f"[yellow]Key '{key}' is not set.[/yellow]")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
del cfg[key]
|
|
167
|
+
save_config(cfg)
|
|
168
|
+
console.print(f"[green]✓[/green] Removed [bold]{key}[/bold]")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@config_app.command("path")
|
|
172
|
+
def config_path() -> None:
|
|
173
|
+
"""Show the configuration file path."""
|
|
174
|
+
console.print(str(CONFIG_FILE))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Doctor command — diagnostics and health checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from ..config import CONFIG_FILE, get_client, load_config, mask_secret
|
|
12
|
+
from ..console import console
|
|
13
|
+
from ..errors import ConfigMissing, ExitCode, handle_errors
|
|
14
|
+
from ..formatters import OutputFormat, output, resolve_format
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _check_mark(ok: bool) -> str:
|
|
18
|
+
return "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _warn_mark() -> str:
|
|
22
|
+
return "[yellow]⚠[/yellow]"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@handle_errors
|
|
26
|
+
def doctor(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
output_format: Annotated[OutputFormat | None, typer.Option("--format", "-f", help="Output format.")] = None,
|
|
29
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Check CLI configuration and API connectivity."""
|
|
32
|
+
from .. import __version__
|
|
33
|
+
|
|
34
|
+
fmt = resolve_format(ctx, json_output, output_format)
|
|
35
|
+
cfg = load_config()
|
|
36
|
+
results: dict = {}
|
|
37
|
+
all_ok = True
|
|
38
|
+
|
|
39
|
+
# Check 1: Config file
|
|
40
|
+
config_exists = CONFIG_FILE.exists()
|
|
41
|
+
results["config_file"] = {
|
|
42
|
+
"status": "ok" if config_exists else "warning",
|
|
43
|
+
"path": str(CONFIG_FILE),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Check 2: API key
|
|
47
|
+
api_key = os.environ.get("COMETAPI_KEY") or cfg.get("api_key", "")
|
|
48
|
+
key_source = "environment" if os.environ.get("COMETAPI_KEY") else ("config" if cfg.get("api_key") else "none")
|
|
49
|
+
has_key = bool(api_key)
|
|
50
|
+
results["api_key"] = {
|
|
51
|
+
"status": "ok" if has_key else "error",
|
|
52
|
+
"masked": mask_secret(api_key) if api_key else "not set",
|
|
53
|
+
"source": key_source,
|
|
54
|
+
}
|
|
55
|
+
if not has_key:
|
|
56
|
+
all_ok = False
|
|
57
|
+
|
|
58
|
+
# Check 3: Connectivity
|
|
59
|
+
if has_key:
|
|
60
|
+
try:
|
|
61
|
+
client = get_client()
|
|
62
|
+
client.models.list()
|
|
63
|
+
base_url = os.environ.get("COMETAPI_BASE_URL") or cfg.get("base_url", "https://api.cometapi.com/v1")
|
|
64
|
+
results["connectivity"] = {"status": "ok", "base_url": base_url}
|
|
65
|
+
except ConfigMissing:
|
|
66
|
+
results["connectivity"] = {"status": "error", "message": "API key required"}
|
|
67
|
+
all_ok = False
|
|
68
|
+
except Exception as e:
|
|
69
|
+
exc_type = type(e).__name__
|
|
70
|
+
if "AuthenticationError" in exc_type:
|
|
71
|
+
results["connectivity"] = {"status": "warning", "message": "connected but auth failed"}
|
|
72
|
+
else:
|
|
73
|
+
results["connectivity"] = {"status": "error", "message": str(e)[:100]}
|
|
74
|
+
all_ok = False
|
|
75
|
+
else:
|
|
76
|
+
results["connectivity"] = {"status": "skipped", "message": "no API key"}
|
|
77
|
+
|
|
78
|
+
# Check 4: Access token
|
|
79
|
+
access_token = os.environ.get("COMETAPI_ACCESS_TOKEN") or cfg.get("access_token", "")
|
|
80
|
+
results["access_token"] = {
|
|
81
|
+
"status": "ok" if access_token else "warning",
|
|
82
|
+
"masked": mask_secret(access_token) if access_token else "not set (optional)",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Check 5: Versions
|
|
86
|
+
results["versions"] = {
|
|
87
|
+
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
88
|
+
"cometapi_cli": __version__,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# JSON / YAML output
|
|
92
|
+
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
|
|
93
|
+
output(results, fmt)
|
|
94
|
+
if not all_ok:
|
|
95
|
+
raise typer.Exit(code=ExitCode.CONFIG_MISSING)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Rich formatted output
|
|
99
|
+
console.print()
|
|
100
|
+
console.print("[bold]CometAPI Doctor[/bold]")
|
|
101
|
+
console.print("─" * 40)
|
|
102
|
+
|
|
103
|
+
# Config file
|
|
104
|
+
mark = _check_mark(config_exists) if config_exists else _warn_mark()
|
|
105
|
+
status_text = "found" if config_exists else "not found"
|
|
106
|
+
console.print(f" {mark} Config file: {status_text} ({CONFIG_FILE})")
|
|
107
|
+
|
|
108
|
+
# API key
|
|
109
|
+
mark = _check_mark(has_key)
|
|
110
|
+
key_display = mask_secret(api_key) if api_key else "not set"
|
|
111
|
+
console.print(f" {mark} API key: {key_display} [dim]({key_source})[/dim]")
|
|
112
|
+
if not has_key:
|
|
113
|
+
console.print(" [dim]Create one at: https://www.cometapi.com/console/token[/dim]")
|
|
114
|
+
|
|
115
|
+
# Connectivity
|
|
116
|
+
conn = results["connectivity"]
|
|
117
|
+
if conn["status"] == "ok":
|
|
118
|
+
console.print(f" {_check_mark(True)} API connectivity: {conn['base_url']}")
|
|
119
|
+
elif conn["status"] == "warning":
|
|
120
|
+
console.print(f" {_warn_mark()} API connectivity: {conn['message']}")
|
|
121
|
+
elif conn["status"] == "skipped":
|
|
122
|
+
console.print(" [dim]- API connectivity: skipped (no API key)[/dim]")
|
|
123
|
+
else:
|
|
124
|
+
console.print(f" {_check_mark(False)} API connectivity: {conn.get('message', 'failed')}")
|
|
125
|
+
|
|
126
|
+
# Access token
|
|
127
|
+
token_ok = bool(access_token)
|
|
128
|
+
mark = _check_mark(token_ok) if token_ok else _warn_mark()
|
|
129
|
+
token_display = mask_secret(access_token) if access_token else "not set (optional)"
|
|
130
|
+
console.print(f" {mark} Access token: {token_display}")
|
|
131
|
+
if not token_ok:
|
|
132
|
+
console.print(" [dim]Get one at: https://www.cometapi.com/console/personal[/dim]")
|
|
133
|
+
|
|
134
|
+
# Versions
|
|
135
|
+
v = results["versions"]
|
|
136
|
+
ver_line = f"Python {v['python']} · cometapi-cli {v['cometapi_cli']}"
|
|
137
|
+
console.print(f" {_check_mark(True)} {ver_line}")
|
|
138
|
+
|
|
139
|
+
console.print()
|
|
140
|
+
if all_ok:
|
|
141
|
+
console.print("[green bold]All checks passed![/green bold]")
|
|
142
|
+
else:
|
|
143
|
+
console.print("[yellow]Some checks need attention. Run [bold]cometapi init[/bold] to configure.[/yellow]")
|
|
144
|
+
raise typer.Exit(code=ExitCode.CONFIG_MISSING)
|