kctl-api 0.2.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.
- kctl_api/__init__.py +3 -0
- kctl_api/__main__.py +5 -0
- kctl_api/cli.py +238 -0
- kctl_api/commands/__init__.py +1 -0
- kctl_api/commands/ai.py +250 -0
- kctl_api/commands/aliases.py +84 -0
- kctl_api/commands/apps.py +172 -0
- kctl_api/commands/auth.py +313 -0
- kctl_api/commands/automation.py +242 -0
- kctl_api/commands/build_cmd.py +87 -0
- kctl_api/commands/clean.py +182 -0
- kctl_api/commands/config_cmd.py +443 -0
- kctl_api/commands/dashboard.py +139 -0
- kctl_api/commands/db.py +599 -0
- kctl_api/commands/deploy.py +84 -0
- kctl_api/commands/deps.py +289 -0
- kctl_api/commands/dev.py +136 -0
- kctl_api/commands/docker_cmd.py +252 -0
- kctl_api/commands/doctor_cmd.py +286 -0
- kctl_api/commands/env.py +289 -0
- kctl_api/commands/files.py +250 -0
- kctl_api/commands/fmt_cmd.py +58 -0
- kctl_api/commands/health.py +479 -0
- kctl_api/commands/jobs.py +169 -0
- kctl_api/commands/lint_cmd.py +81 -0
- kctl_api/commands/logs.py +258 -0
- kctl_api/commands/marketplace.py +316 -0
- kctl_api/commands/monitor_cmd.py +243 -0
- kctl_api/commands/notifications.py +132 -0
- kctl_api/commands/odoo_proxy.py +182 -0
- kctl_api/commands/openapi.py +299 -0
- kctl_api/commands/perf.py +307 -0
- kctl_api/commands/rate_limit.py +223 -0
- kctl_api/commands/realtime.py +100 -0
- kctl_api/commands/redis_cmd.py +609 -0
- kctl_api/commands/routes_cmd.py +277 -0
- kctl_api/commands/saas.py +145 -0
- kctl_api/commands/scaffold.py +362 -0
- kctl_api/commands/security_cmd.py +350 -0
- kctl_api/commands/services.py +191 -0
- kctl_api/commands/shell.py +197 -0
- kctl_api/commands/skill_cmd.py +58 -0
- kctl_api/commands/streams.py +309 -0
- kctl_api/commands/stripe_cmd.py +105 -0
- kctl_api/commands/tenant_ai.py +169 -0
- kctl_api/commands/test_cmd.py +95 -0
- kctl_api/commands/users.py +302 -0
- kctl_api/commands/webhooks.py +56 -0
- kctl_api/commands/workflows.py +127 -0
- kctl_api/commands/ws.py +323 -0
- kctl_api/core/__init__.py +1 -0
- kctl_api/core/async_client.py +120 -0
- kctl_api/core/callbacks.py +88 -0
- kctl_api/core/client.py +190 -0
- kctl_api/core/config.py +260 -0
- kctl_api/core/db.py +65 -0
- kctl_api/core/exceptions.py +43 -0
- kctl_api/core/output.py +5 -0
- kctl_api/core/plugins.py +26 -0
- kctl_api/core/redis.py +35 -0
- kctl_api/core/resolve.py +47 -0
- kctl_api/core/utils.py +109 -0
- kctl_api-0.2.0.dist-info/METADATA +34 -0
- kctl_api-0.2.0.dist-info/RECORD +66 -0
- kctl_api-0.2.0.dist-info/WHEEL +4 -0
- kctl_api-0.2.0.dist-info/entry_points.txt +2 -0
kctl_api/__init__.py
ADDED
kctl_api/__main__.py
ADDED
kctl_api/cli.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-api."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from kctl_lib import KctlError, handle_cli_error
|
|
9
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
10
|
+
|
|
11
|
+
from kctl_api import __version__
|
|
12
|
+
from kctl_api.core.callbacks import AppContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def version_callback(value: bool) -> None:
|
|
16
|
+
if value:
|
|
17
|
+
typer.echo(f"kctl-api {__version__}")
|
|
18
|
+
raise typer.Exit()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(
|
|
22
|
+
name="kctl-api",
|
|
23
|
+
help="Kodemeio API CLI - manage your FastAPI platform.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
rich_markup_mode="rich",
|
|
26
|
+
pretty_exceptions_enable=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback()
|
|
31
|
+
def main(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
34
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
35
|
+
output_format: Annotated[
|
|
36
|
+
str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")
|
|
37
|
+
] = "pretty",
|
|
38
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit table headers (for scripting)")] = False,
|
|
39
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
40
|
+
url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
|
|
41
|
+
ai_url: Annotated[str | None, typer.Option("--ai-url", help="AI API URL override")] = None,
|
|
42
|
+
api_key: Annotated[str | None, typer.Option("--api-key", help="API key override")] = None,
|
|
43
|
+
database_url: Annotated[str | None, typer.Option("--database-url", help="Database URL override")] = None,
|
|
44
|
+
redis_url: Annotated[str | None, typer.Option("--redis-url", help="Redis URL override")] = None,
|
|
45
|
+
version: Annotated[
|
|
46
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
47
|
+
] = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Kodemeio API CLI."""
|
|
50
|
+
ctx.ensure_object(dict)
|
|
51
|
+
ctx.obj = AppContext(
|
|
52
|
+
json_mode=json_output or output_format == "json",
|
|
53
|
+
quiet=quiet,
|
|
54
|
+
format=output_format,
|
|
55
|
+
no_header=no_header,
|
|
56
|
+
profile=profile,
|
|
57
|
+
url_override=url,
|
|
58
|
+
ai_url_override=ai_url,
|
|
59
|
+
api_key_override=api_key,
|
|
60
|
+
database_url_override=database_url,
|
|
61
|
+
redis_url_override=redis_url,
|
|
62
|
+
)
|
|
63
|
+
notify_if_outdated(ctx.obj.output, "kctl-api", __version__)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _register_commands() -> None:
|
|
67
|
+
"""Register all command groups (lazy imports for fast startup)."""
|
|
68
|
+
from kctl_api.commands.ai import app as ai_app
|
|
69
|
+
from kctl_api.commands.aliases import register_aliases
|
|
70
|
+
from kctl_api.commands.apps import app as apps_app
|
|
71
|
+
from kctl_api.commands.auth import app as auth_app
|
|
72
|
+
from kctl_api.commands.automation import app as automation_app
|
|
73
|
+
from kctl_api.commands.build_cmd import app as build_app
|
|
74
|
+
from kctl_api.commands.clean import app as clean_app
|
|
75
|
+
from kctl_api.commands.config_cmd import app as config_app
|
|
76
|
+
from kctl_api.commands.dashboard import app as dashboard_app
|
|
77
|
+
from kctl_api.commands.db import app as db_app
|
|
78
|
+
from kctl_api.commands.deploy import app as deploy_app
|
|
79
|
+
from kctl_api.commands.deps import app as deps_app
|
|
80
|
+
from kctl_api.commands.dev import app as dev_app
|
|
81
|
+
from kctl_api.commands.docker_cmd import app as docker_app
|
|
82
|
+
from kctl_api.commands.doctor_cmd import app as doctor_app
|
|
83
|
+
from kctl_api.commands.env import app as env_app
|
|
84
|
+
from kctl_api.commands.files import app as files_app
|
|
85
|
+
from kctl_api.commands.fmt_cmd import app as fmt_app
|
|
86
|
+
from kctl_api.commands.health import app as health_app
|
|
87
|
+
from kctl_api.commands.jobs import app as jobs_app
|
|
88
|
+
from kctl_api.commands.lint_cmd import app as lint_app
|
|
89
|
+
from kctl_api.commands.logs import app as logs_app
|
|
90
|
+
from kctl_api.commands.marketplace import app as marketplace_app
|
|
91
|
+
from kctl_api.commands.monitor_cmd import app as monitor_app
|
|
92
|
+
from kctl_api.commands.notifications import app as notifications_app
|
|
93
|
+
from kctl_api.commands.odoo_proxy import app as odoo_app
|
|
94
|
+
from kctl_api.commands.openapi import app as openapi_app
|
|
95
|
+
from kctl_api.commands.perf import app as perf_app
|
|
96
|
+
from kctl_api.commands.rate_limit import app as rate_limit_app
|
|
97
|
+
from kctl_api.commands.realtime import app as realtime_app
|
|
98
|
+
from kctl_api.commands.redis_cmd import app as redis_app
|
|
99
|
+
from kctl_api.commands.routes_cmd import app as routes_app
|
|
100
|
+
from kctl_api.commands.saas import app as saas_app
|
|
101
|
+
from kctl_api.commands.scaffold import app as scaffold_app
|
|
102
|
+
from kctl_api.commands.security_cmd import app as security_app
|
|
103
|
+
from kctl_api.commands.services import app as services_app
|
|
104
|
+
from kctl_api.commands.shell import app as shell_app
|
|
105
|
+
from kctl_api.commands.streams import app as streams_app
|
|
106
|
+
from kctl_api.commands.stripe_cmd import app as stripe_app
|
|
107
|
+
from kctl_api.commands.tenant_ai import app as tenant_ai_app
|
|
108
|
+
from kctl_api.commands.test_cmd import app as test_app
|
|
109
|
+
from kctl_api.commands.users import app as users_app
|
|
110
|
+
from kctl_api.commands.webhooks import app as webhooks_app
|
|
111
|
+
from kctl_api.commands.workflows import app as workflows_app
|
|
112
|
+
from kctl_api.commands.ws import app as ws_app
|
|
113
|
+
|
|
114
|
+
# Authentication & access
|
|
115
|
+
app.add_typer(config_app, name="config")
|
|
116
|
+
app.add_typer(auth_app, name="auth")
|
|
117
|
+
app.add_typer(health_app, name="health")
|
|
118
|
+
|
|
119
|
+
# API resources
|
|
120
|
+
app.add_typer(users_app, name="users")
|
|
121
|
+
app.add_typer(files_app, name="files")
|
|
122
|
+
app.add_typer(jobs_app, name="jobs")
|
|
123
|
+
app.add_typer(workflows_app, name="workflows")
|
|
124
|
+
app.add_typer(automation_app, name="automation")
|
|
125
|
+
app.add_typer(notifications_app, name="notifications")
|
|
126
|
+
app.add_typer(webhooks_app, name="webhooks")
|
|
127
|
+
app.add_typer(marketplace_app, name="marketplace")
|
|
128
|
+
app.add_typer(saas_app, name="saas")
|
|
129
|
+
app.add_typer(stripe_app, name="stripe")
|
|
130
|
+
app.add_typer(odoo_app, name="odoo")
|
|
131
|
+
app.add_typer(realtime_app, name="realtime")
|
|
132
|
+
|
|
133
|
+
# AI platform
|
|
134
|
+
app.add_typer(ai_app, name="ai")
|
|
135
|
+
app.add_typer(tenant_ai_app, name="tenant-ai")
|
|
136
|
+
|
|
137
|
+
# Infrastructure
|
|
138
|
+
app.add_typer(db_app, name="db")
|
|
139
|
+
app.add_typer(redis_app, name="redis")
|
|
140
|
+
app.add_typer(streams_app, name="streams")
|
|
141
|
+
app.add_typer(services_app, name="services")
|
|
142
|
+
app.add_typer(deploy_app, name="deploy")
|
|
143
|
+
app.add_typer(apps_app, name="apps")
|
|
144
|
+
app.add_typer(docker_app, name="docker")
|
|
145
|
+
|
|
146
|
+
# Development tools
|
|
147
|
+
app.add_typer(dev_app, name="dev")
|
|
148
|
+
app.add_typer(test_app, name="test")
|
|
149
|
+
app.add_typer(lint_app, name="lint")
|
|
150
|
+
app.add_typer(fmt_app, name="fmt")
|
|
151
|
+
app.add_typer(build_app, name="build")
|
|
152
|
+
app.add_typer(scaffold_app, name="scaffold")
|
|
153
|
+
app.add_typer(shell_app, name="shell")
|
|
154
|
+
|
|
155
|
+
# API analysis & testing
|
|
156
|
+
app.add_typer(openapi_app, name="openapi")
|
|
157
|
+
app.add_typer(routes_app, name="routes")
|
|
158
|
+
app.add_typer(rate_limit_app, name="rate-limit")
|
|
159
|
+
app.add_typer(ws_app, name="ws")
|
|
160
|
+
app.add_typer(perf_app, name="perf")
|
|
161
|
+
|
|
162
|
+
# Environment & security
|
|
163
|
+
app.add_typer(doctor_app, name="doctor")
|
|
164
|
+
app.add_typer(env_app, name="env")
|
|
165
|
+
app.add_typer(security_app, name="security")
|
|
166
|
+
app.add_typer(deps_app, name="deps")
|
|
167
|
+
app.add_typer(clean_app, name="clean")
|
|
168
|
+
|
|
169
|
+
# Observability & monitoring
|
|
170
|
+
app.add_typer(logs_app, name="logs")
|
|
171
|
+
app.add_typer(dashboard_app, name="dashboard")
|
|
172
|
+
app.add_typer(monitor_app, name="monitor")
|
|
173
|
+
|
|
174
|
+
# Skill generation
|
|
175
|
+
from kctl_api.commands.skill_cmd import app as skill_app
|
|
176
|
+
|
|
177
|
+
app.add_typer(skill_app, name="skill")
|
|
178
|
+
|
|
179
|
+
# Short aliases
|
|
180
|
+
register_aliases(app)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_register_commands()
|
|
184
|
+
|
|
185
|
+
# Load plugins via entry points
|
|
186
|
+
from kctl_api.core.plugins import discover_and_load_plugins # noqa: E402
|
|
187
|
+
|
|
188
|
+
discover_and_load_plugins(app)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command("self-update")
|
|
192
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
193
|
+
"""Check for updates and upgrade kctl-api."""
|
|
194
|
+
actx = ctx.obj
|
|
195
|
+
out = actx.output
|
|
196
|
+
|
|
197
|
+
from kctl_lib.self_update import check_update
|
|
198
|
+
from kctl_lib.self_update import update as do_update
|
|
199
|
+
|
|
200
|
+
latest = check_update("kctl-api", __version__)
|
|
201
|
+
if latest:
|
|
202
|
+
out.info(f"Updating to {latest}...")
|
|
203
|
+
do_update("kctl-api")
|
|
204
|
+
out.success(f"Updated to {latest}")
|
|
205
|
+
else:
|
|
206
|
+
out.success("Already up to date")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def completions(
|
|
211
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
212
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Generate or install shell completions."""
|
|
215
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
216
|
+
|
|
217
|
+
if install:
|
|
218
|
+
path = install_completions("kctl-api", shell)
|
|
219
|
+
if path:
|
|
220
|
+
typer.echo(f"Completions installed to {path}")
|
|
221
|
+
else:
|
|
222
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
223
|
+
raise typer.Exit(code=1)
|
|
224
|
+
else:
|
|
225
|
+
script = get_completion_script("kctl-api", shell)
|
|
226
|
+
typer.echo(script)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _run() -> None:
|
|
230
|
+
"""Entry point with error handling."""
|
|
231
|
+
try:
|
|
232
|
+
app()
|
|
233
|
+
except KctlError as e:
|
|
234
|
+
handle_cli_error(e)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
_run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command groups for kctl-api."""
|
kctl_api/commands/ai.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""AI platform commands for kctl-api.
|
|
2
|
+
|
|
3
|
+
Manage copilots, chat, conversations, and AI service health.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from kctl_api.core.callbacks import AppContext
|
|
13
|
+
from kctl_api.core.exceptions import APIError, AuthenticationError
|
|
14
|
+
from kctl_api.core.exceptions import ConnectionError as KctlConnectionError
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(name="ai", help="AI platform — copilots, chat, conversations.", no_args_is_help=True)
|
|
17
|
+
|
|
18
|
+
_BASE = "/api/v1/ai"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# copilots
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
@app.command()
|
|
25
|
+
def copilots(ctx: typer.Context) -> None:
|
|
26
|
+
"""List available AI copilots via GET /api/v1/ai/copilots."""
|
|
27
|
+
actx: AppContext = ctx.obj
|
|
28
|
+
out = actx.output
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
data = actx.ai_client.get(f"{_BASE}/copilots")
|
|
32
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
33
|
+
out.error(str(e))
|
|
34
|
+
raise typer.Exit(1) from None
|
|
35
|
+
|
|
36
|
+
items = data if isinstance(data, list) else data.get("items", []) if isinstance(data, dict) else []
|
|
37
|
+
|
|
38
|
+
rows: list[list[str]] = []
|
|
39
|
+
for cp in items:
|
|
40
|
+
rows.append(
|
|
41
|
+
[
|
|
42
|
+
str(cp.get("id", "")),
|
|
43
|
+
cp.get("name", ""),
|
|
44
|
+
cp.get("model", ""),
|
|
45
|
+
cp.get("description", "")[:50],
|
|
46
|
+
"[green]active[/green]" if cp.get("active", True) else "[dim]inactive[/dim]",
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
out.table(
|
|
51
|
+
title="AI Copilots",
|
|
52
|
+
columns=[
|
|
53
|
+
("ID", "bold"),
|
|
54
|
+
("Name", ""),
|
|
55
|
+
("Model", ""),
|
|
56
|
+
("Description", "dim"),
|
|
57
|
+
("Status", ""),
|
|
58
|
+
],
|
|
59
|
+
rows=rows,
|
|
60
|
+
data_for_json=items,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# health
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
@app.command()
|
|
68
|
+
def health(ctx: typer.Context) -> None:
|
|
69
|
+
"""Check AI service health via GET /api/v1/ai/health."""
|
|
70
|
+
actx: AppContext = ctx.obj
|
|
71
|
+
out = actx.output
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
data = actx.ai_client.get(f"{_BASE}/health")
|
|
75
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
76
|
+
out.error(str(e))
|
|
77
|
+
raise typer.Exit(1) from None
|
|
78
|
+
|
|
79
|
+
if not data:
|
|
80
|
+
out.error("No health data from ai-main.")
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
|
|
83
|
+
status = data.get("status", "unknown")
|
|
84
|
+
healthy = status == "ok"
|
|
85
|
+
|
|
86
|
+
out.detail(
|
|
87
|
+
title="AI Service Health",
|
|
88
|
+
sections=[
|
|
89
|
+
(
|
|
90
|
+
"Status",
|
|
91
|
+
[
|
|
92
|
+
("Status", "[green]ok[/green]" if healthy else f"[red]{status}[/red]"),
|
|
93
|
+
*[(k, str(v)) for k, v in data.items() if k != "status"],
|
|
94
|
+
],
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
data_for_json=data,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not healthy:
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# chat
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
@app.command()
|
|
108
|
+
def chat(
|
|
109
|
+
ctx: typer.Context,
|
|
110
|
+
message: Annotated[str, typer.Argument(help="Message to send to the copilot.")],
|
|
111
|
+
copilot: Annotated[str, typer.Option("--copilot", "-c", help="Copilot ID or name.")] = "default",
|
|
112
|
+
conversation_id: Annotated[
|
|
113
|
+
str | None, typer.Option("--conversation", help="Existing conversation ID to continue.")
|
|
114
|
+
] = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Send a chat message to an AI copilot via POST /api/v1/ai/chat."""
|
|
117
|
+
actx: AppContext = ctx.obj
|
|
118
|
+
out = actx.output
|
|
119
|
+
|
|
120
|
+
payload: dict = {"message": message, "copilot": copilot}
|
|
121
|
+
if conversation_id:
|
|
122
|
+
payload["conversation_id"] = conversation_id
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = actx.ai_client.post(f"{_BASE}/chat", json=payload)
|
|
126
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
127
|
+
out.error(str(e))
|
|
128
|
+
raise typer.Exit(1) from None
|
|
129
|
+
|
|
130
|
+
if actx.json_mode:
|
|
131
|
+
out.raw_json(result)
|
|
132
|
+
else:
|
|
133
|
+
reply = result.get("reply", result.get("message", "")) if isinstance(result, dict) else str(result)
|
|
134
|
+
out.text(reply)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# conversations
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
@app.command()
|
|
141
|
+
def conversations(
|
|
142
|
+
ctx: typer.Context,
|
|
143
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number.")] = 1,
|
|
144
|
+
per_page: Annotated[int, typer.Option("--per-page", "-n", help="Items per page.")] = 20,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""List AI conversations via GET /api/v1/ai/conversations."""
|
|
147
|
+
actx: AppContext = ctx.obj
|
|
148
|
+
out = actx.output
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
data = actx.ai_client.get(f"{_BASE}/conversations", params={"page": page, "per_page": per_page})
|
|
152
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
153
|
+
out.error(str(e))
|
|
154
|
+
raise typer.Exit(1) from None
|
|
155
|
+
|
|
156
|
+
items = data.get("items", []) if isinstance(data, dict) else []
|
|
157
|
+
total = data.get("total", len(items)) if isinstance(data, dict) else len(items)
|
|
158
|
+
|
|
159
|
+
rows: list[list[str]] = []
|
|
160
|
+
for c in items:
|
|
161
|
+
rows.append(
|
|
162
|
+
[
|
|
163
|
+
str(c.get("id", "")),
|
|
164
|
+
c.get("copilot", ""),
|
|
165
|
+
str(c.get("message_count", "")),
|
|
166
|
+
str(c.get("created_at", "")),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
out.table(
|
|
171
|
+
title=f"AI Conversations (page {page}, {total} total)",
|
|
172
|
+
columns=[
|
|
173
|
+
("ID", "bold"),
|
|
174
|
+
("Copilot", ""),
|
|
175
|
+
("Messages", ""),
|
|
176
|
+
("Created", "dim"),
|
|
177
|
+
],
|
|
178
|
+
rows=rows,
|
|
179
|
+
data_for_json=items,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# conversation
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
@app.command()
|
|
187
|
+
def conversation(
|
|
188
|
+
ctx: typer.Context,
|
|
189
|
+
conversation_id: Annotated[str, typer.Argument(help="Conversation ID.")],
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Get conversation details and messages via GET /api/v1/ai/conversations/{id}."""
|
|
192
|
+
actx: AppContext = ctx.obj
|
|
193
|
+
out = actx.output
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
data = actx.ai_client.get(f"{_BASE}/conversations/{conversation_id}")
|
|
197
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
198
|
+
out.error(str(e))
|
|
199
|
+
raise typer.Exit(1) from None
|
|
200
|
+
|
|
201
|
+
if not data:
|
|
202
|
+
out.error(f"Conversation not found: {conversation_id}")
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
|
|
205
|
+
out.detail(
|
|
206
|
+
title=f"Conversation: {conversation_id}",
|
|
207
|
+
sections=[
|
|
208
|
+
("Details", [(k, str(v)) for k, v in data.items() if k != "messages"]),
|
|
209
|
+
],
|
|
210
|
+
data_for_json=data,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Print messages if present
|
|
214
|
+
messages = data.get("messages", [])
|
|
215
|
+
if messages and not actx.json_mode:
|
|
216
|
+
out.header("Messages")
|
|
217
|
+
for msg in messages:
|
|
218
|
+
role = msg.get("role", "unknown")
|
|
219
|
+
content = msg.get("content", "")
|
|
220
|
+
out.text(f" [{role}] {content}")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
# delete-conversation
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
@app.command(name="delete-conversation")
|
|
227
|
+
def delete_conversation(
|
|
228
|
+
ctx: typer.Context,
|
|
229
|
+
conversation_id: Annotated[str, typer.Argument(help="Conversation ID to delete.")],
|
|
230
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation.")] = False,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""Delete an AI conversation via DELETE /api/v1/ai/conversations/{id}."""
|
|
233
|
+
actx: AppContext = ctx.obj
|
|
234
|
+
out = actx.output
|
|
235
|
+
|
|
236
|
+
if not force:
|
|
237
|
+
confirm = typer.confirm(f"Delete conversation {conversation_id}?", default=False)
|
|
238
|
+
if not confirm:
|
|
239
|
+
out.info("Cancelled.")
|
|
240
|
+
raise typer.Exit(0)
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
result = actx.ai_client.delete(f"{_BASE}/conversations/{conversation_id}")
|
|
244
|
+
except (AuthenticationError, KctlConnectionError, APIError) as e:
|
|
245
|
+
out.error(str(e))
|
|
246
|
+
raise typer.Exit(1) from None
|
|
247
|
+
|
|
248
|
+
out.success(f"Conversation {conversation_id} deleted.")
|
|
249
|
+
if actx.json_mode:
|
|
250
|
+
out.raw_json(result)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Short alias commands for kctl-api.
|
|
2
|
+
|
|
3
|
+
Hidden commands that delegate to longer command paths.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from kctl_api.core.callbacks import AppContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_aliases(app: typer.Typer) -> None:
|
|
16
|
+
"""Register hidden short-alias commands on the main app."""
|
|
17
|
+
|
|
18
|
+
def _base_cmd(ctx: typer.Context) -> list[str]:
|
|
19
|
+
cmd = ["kctl-api"]
|
|
20
|
+
actx: AppContext = ctx.obj
|
|
21
|
+
if actx.profile:
|
|
22
|
+
cmd.extend(["-p", actx.profile])
|
|
23
|
+
if actx.json_mode:
|
|
24
|
+
cmd.append("--json")
|
|
25
|
+
if actx.quiet:
|
|
26
|
+
cmd.append("-q")
|
|
27
|
+
if actx.format != "pretty" and not actx.json_mode:
|
|
28
|
+
cmd.extend(["-f", actx.format])
|
|
29
|
+
if actx.no_header:
|
|
30
|
+
cmd.append("--no-header")
|
|
31
|
+
if actx.url_override:
|
|
32
|
+
cmd.extend(["--url", actx.url_override])
|
|
33
|
+
if actx.ai_url_override:
|
|
34
|
+
cmd.extend(["--ai-url", actx.ai_url_override])
|
|
35
|
+
if actx.api_key_override:
|
|
36
|
+
cmd.extend(["--api-key", actx.api_key_override])
|
|
37
|
+
if actx.database_url_override:
|
|
38
|
+
cmd.extend(["--database-url", actx.database_url_override])
|
|
39
|
+
if actx.redis_url_override:
|
|
40
|
+
cmd.extend(["--redis-url", actx.redis_url_override])
|
|
41
|
+
return cmd
|
|
42
|
+
|
|
43
|
+
@app.command("hc", hidden=True, help="Alias: health all")
|
|
44
|
+
def hc(ctx: typer.Context) -> None:
|
|
45
|
+
subprocess.run([*_base_cmd(ctx), "health", "all"])
|
|
46
|
+
|
|
47
|
+
@app.command("dl", hidden=True, help="Alias: deploy logs <app>")
|
|
48
|
+
def dl(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
app_name: str = typer.Argument("api-main", help="App name."),
|
|
51
|
+
) -> None:
|
|
52
|
+
subprocess.run([*_base_cmd(ctx), "deploy", "logs", app_name])
|
|
53
|
+
|
|
54
|
+
@app.command("ds", hidden=True, help="Alias: deploy status")
|
|
55
|
+
def ds(ctx: typer.Context) -> None:
|
|
56
|
+
subprocess.run([*_base_cmd(ctx), "deploy", "status"])
|
|
57
|
+
|
|
58
|
+
@app.command("ul", hidden=True, help="Alias: users list")
|
|
59
|
+
def ul(ctx: typer.Context) -> None:
|
|
60
|
+
subprocess.run([*_base_cmd(ctx), "users", "list"])
|
|
61
|
+
|
|
62
|
+
@app.command("fl", hidden=True, help="Alias: files list")
|
|
63
|
+
def fl(ctx: typer.Context) -> None:
|
|
64
|
+
subprocess.run([*_base_cmd(ctx), "files", "list"])
|
|
65
|
+
|
|
66
|
+
@app.command("jo", hidden=True, help="Alias: jobs overview")
|
|
67
|
+
def jo(ctx: typer.Context) -> None:
|
|
68
|
+
subprocess.run([*_base_cmd(ctx), "jobs", "overview"])
|
|
69
|
+
|
|
70
|
+
@app.command("du", hidden=True, help="Alias: dev up")
|
|
71
|
+
def du(ctx: typer.Context) -> None:
|
|
72
|
+
subprocess.run([*_base_cmd(ctx), "dev", "up"])
|
|
73
|
+
|
|
74
|
+
@app.command("dd", hidden=True, help="Alias: dev down")
|
|
75
|
+
def dd(ctx: typer.Context) -> None:
|
|
76
|
+
subprocess.run([*_base_cmd(ctx), "dev", "down"])
|
|
77
|
+
|
|
78
|
+
@app.command("dr", hidden=True, help="Alias: dev rebuild")
|
|
79
|
+
def dr(ctx: typer.Context) -> None:
|
|
80
|
+
subprocess.run([*_base_cmd(ctx), "dev", "rebuild"])
|
|
81
|
+
|
|
82
|
+
@app.command("tr", hidden=True, help="Alias: test run all")
|
|
83
|
+
def tr(ctx: typer.Context) -> None:
|
|
84
|
+
subprocess.run([*_base_cmd(ctx), "test", "run", "all"])
|