kctl-claw 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_claw/__init__.py +3 -0
- kctl_claw/__main__.py +5 -0
- kctl_claw/cli.py +173 -0
- kctl_claw/commands/__init__.py +0 -0
- kctl_claw/commands/agents.py +323 -0
- kctl_claw/commands/agents_test.py +264 -0
- kctl_claw/commands/ai.py +122 -0
- kctl_claw/commands/aliases.py +63 -0
- kctl_claw/commands/backup.py +112 -0
- kctl_claw/commands/config_cmd.py +248 -0
- kctl_claw/commands/config_drift.py +169 -0
- kctl_claw/commands/cron.py +338 -0
- kctl_claw/commands/cron_debug.py +220 -0
- kctl_claw/commands/deploy.py +217 -0
- kctl_claw/commands/docker_cmd.py +198 -0
- kctl_claw/commands/doctor_cmd.py +222 -0
- kctl_claw/commands/env.py +154 -0
- kctl_claw/commands/health.py +94 -0
- kctl_claw/commands/lint_cmd.py +267 -0
- kctl_claw/commands/logs.py +71 -0
- kctl_claw/commands/mcp.py +322 -0
- kctl_claw/commands/mcp_test.py +250 -0
- kctl_claw/commands/memory.py +359 -0
- kctl_claw/commands/monitor_cmd.py +192 -0
- kctl_claw/commands/pipeline.py +200 -0
- kctl_claw/commands/prompts.py +286 -0
- kctl_claw/commands/security.py +169 -0
- kctl_claw/commands/skill_cmd.py +76 -0
- kctl_claw/commands/skills.py +144 -0
- kctl_claw/commands/skills_test.py +269 -0
- kctl_claw/commands/status.py +78 -0
- kctl_claw/commands/telegram.py +235 -0
- kctl_claw/commands/test_cmd.py +213 -0
- kctl_claw/commands/trading.py +269 -0
- kctl_claw/core/__init__.py +0 -0
- kctl_claw/core/callbacks.py +74 -0
- kctl_claw/core/config.py +118 -0
- kctl_claw/core/config_manager.py +123 -0
- kctl_claw/core/docker_client.py +191 -0
- kctl_claw/core/exceptions.py +38 -0
- kctl_claw/core/gateway_client.py +61 -0
- kctl_claw/core/models.py +133 -0
- kctl_claw/core/output.py +5 -0
- kctl_claw/core/resolve.py +63 -0
- kctl_claw-0.2.0.dist-info/METADATA +19 -0
- kctl_claw-0.2.0.dist-info/RECORD +48 -0
- kctl_claw-0.2.0.dist-info/WHEEL +4 -0
- kctl_claw-0.2.0.dist-info/entry_points.txt +2 -0
kctl_claw/__init__.py
ADDED
kctl_claw/__main__.py
ADDED
kctl_claw/cli.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Main CLI entry point for kctl-claw."""
|
|
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
|
+
|
|
10
|
+
from kctl_claw import __version__
|
|
11
|
+
from kctl_claw.commands.agents import app as agents_app
|
|
12
|
+
from kctl_claw.commands.agents_test import app as agents_test_app
|
|
13
|
+
from kctl_claw.commands.ai import app as ai_app
|
|
14
|
+
from kctl_claw.commands.aliases import register_aliases
|
|
15
|
+
from kctl_claw.commands.backup import app as backup_app
|
|
16
|
+
from kctl_claw.commands.config_cmd import app as config_app
|
|
17
|
+
from kctl_claw.commands.config_drift import app as config_drift_app
|
|
18
|
+
from kctl_claw.commands.cron import app as cron_app
|
|
19
|
+
from kctl_claw.commands.cron_debug import app as cron_debug_app
|
|
20
|
+
from kctl_claw.commands.deploy import app as deploy_app
|
|
21
|
+
from kctl_claw.commands.docker_cmd import app as docker_app
|
|
22
|
+
from kctl_claw.commands.doctor_cmd import app as doctor_app
|
|
23
|
+
from kctl_claw.commands.env import app as env_app
|
|
24
|
+
from kctl_claw.commands.health import app as health_app
|
|
25
|
+
from kctl_claw.commands.lint_cmd import app as lint_app
|
|
26
|
+
from kctl_claw.commands.logs import app as logs_app
|
|
27
|
+
from kctl_claw.commands.mcp import app as mcp_app
|
|
28
|
+
from kctl_claw.commands.mcp_test import app as mcp_test_app
|
|
29
|
+
from kctl_claw.commands.memory import app as memory_app
|
|
30
|
+
from kctl_claw.commands.monitor_cmd import app as monitor_app
|
|
31
|
+
from kctl_claw.commands.pipeline import app as pipeline_app
|
|
32
|
+
from kctl_claw.commands.prompts import app as prompts_app
|
|
33
|
+
from kctl_claw.commands.security import app as security_app
|
|
34
|
+
from kctl_claw.commands.skill_cmd import app as skill_app
|
|
35
|
+
from kctl_claw.commands.skills import app as skills_app
|
|
36
|
+
from kctl_claw.commands.skills_test import app as skills_test_app
|
|
37
|
+
from kctl_claw.commands.status import app as status_app
|
|
38
|
+
from kctl_claw.commands.telegram import app as telegram_app
|
|
39
|
+
from kctl_claw.commands.test_cmd import app as test_app
|
|
40
|
+
from kctl_claw.commands.trading import app as trading_app
|
|
41
|
+
from kctl_claw.core.callbacks import AppContext
|
|
42
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def version_callback(value: bool) -> None:
|
|
46
|
+
if value:
|
|
47
|
+
typer.echo(f"kctl-claw {__version__}")
|
|
48
|
+
raise typer.Exit()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
app = typer.Typer(
|
|
52
|
+
name="kctl-claw",
|
|
53
|
+
help="Kodemeio OpenClaw CLI — manage AI agent gateway instances.",
|
|
54
|
+
no_args_is_help=True,
|
|
55
|
+
rich_markup_mode="rich",
|
|
56
|
+
pretty_exceptions_enable=False,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.callback()
|
|
61
|
+
def main(
|
|
62
|
+
ctx: typer.Context,
|
|
63
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
64
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
|
|
65
|
+
output_format: Annotated[
|
|
66
|
+
str, typer.Option("--format", "-f", help="Output format (pretty/json/csv/yaml)")
|
|
67
|
+
] = "pretty",
|
|
68
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit table headers")] = False,
|
|
69
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
|
|
70
|
+
root: Annotated[str | None, typer.Option("--root", help="Project root override")] = None,
|
|
71
|
+
live: Annotated[bool, typer.Option("--live", help="Push config changes and trigger reload")] = False,
|
|
72
|
+
version: Annotated[
|
|
73
|
+
bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
|
|
74
|
+
] = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Kodemeio OpenClaw CLI."""
|
|
77
|
+
ctx.ensure_object(dict)
|
|
78
|
+
ctx.obj = AppContext(
|
|
79
|
+
json_mode=json_output,
|
|
80
|
+
quiet=quiet,
|
|
81
|
+
format=output_format,
|
|
82
|
+
no_header=no_header,
|
|
83
|
+
profile=profile,
|
|
84
|
+
root_override=root,
|
|
85
|
+
live=live,
|
|
86
|
+
)
|
|
87
|
+
notify_if_outdated(ctx.obj.output, "kctl-claw", __version__)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Existing command groups
|
|
91
|
+
app.add_typer(agents_app, name="agents")
|
|
92
|
+
app.add_typer(ai_app, name="ai")
|
|
93
|
+
app.add_typer(backup_app, name="backup")
|
|
94
|
+
app.add_typer(config_app, name="config")
|
|
95
|
+
app.add_typer(cron_app, name="cron")
|
|
96
|
+
app.add_typer(deploy_app, name="deploy")
|
|
97
|
+
app.add_typer(env_app, name="env")
|
|
98
|
+
app.add_typer(health_app, name="health")
|
|
99
|
+
app.add_typer(logs_app, name="logs")
|
|
100
|
+
app.add_typer(memory_app, name="memory")
|
|
101
|
+
app.add_typer(mcp_app, name="mcp")
|
|
102
|
+
app.add_typer(security_app, name="security")
|
|
103
|
+
app.add_typer(skills_app, name="skills")
|
|
104
|
+
app.add_typer(status_app, name="status")
|
|
105
|
+
app.add_typer(telegram_app, name="telegram")
|
|
106
|
+
app.add_typer(skill_app, name="skill", hidden=True)
|
|
107
|
+
app.add_typer(trading_app, name="trading")
|
|
108
|
+
|
|
109
|
+
# New command groups (SP6)
|
|
110
|
+
app.add_typer(agents_test_app, name="agents-test")
|
|
111
|
+
app.add_typer(config_drift_app, name="config-drift")
|
|
112
|
+
app.add_typer(cron_debug_app, name="cron-debug")
|
|
113
|
+
app.add_typer(docker_app, name="docker")
|
|
114
|
+
app.add_typer(doctor_app, name="doctor")
|
|
115
|
+
app.add_typer(lint_app, name="lint")
|
|
116
|
+
app.add_typer(mcp_test_app, name="mcp-test")
|
|
117
|
+
app.add_typer(monitor_app, name="monitor")
|
|
118
|
+
app.add_typer(pipeline_app, name="pipeline")
|
|
119
|
+
app.add_typer(prompts_app, name="prompts")
|
|
120
|
+
app.add_typer(skills_test_app, name="skills-test")
|
|
121
|
+
app.add_typer(test_app, name="test")
|
|
122
|
+
|
|
123
|
+
register_aliases(app)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("self-update")
|
|
127
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
128
|
+
"""Check for updates and upgrade kctl-claw."""
|
|
129
|
+
actx = ctx.obj
|
|
130
|
+
out = actx.output
|
|
131
|
+
|
|
132
|
+
from kctl_lib.self_update import check_update
|
|
133
|
+
from kctl_lib.self_update import update as do_update
|
|
134
|
+
|
|
135
|
+
latest = check_update("kctl-claw", __version__)
|
|
136
|
+
if latest:
|
|
137
|
+
out.info(f"Updating to {latest}...")
|
|
138
|
+
do_update("kctl-claw")
|
|
139
|
+
out.success(f"Updated to {latest}")
|
|
140
|
+
else:
|
|
141
|
+
out.success("Already up to date")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def completions(
|
|
146
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
147
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Generate or install shell completions."""
|
|
150
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
151
|
+
|
|
152
|
+
if install:
|
|
153
|
+
path = install_completions("kctl-claw", shell)
|
|
154
|
+
if path:
|
|
155
|
+
typer.echo(f"Completions installed to {path}")
|
|
156
|
+
else:
|
|
157
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
158
|
+
raise typer.Exit(code=1)
|
|
159
|
+
else:
|
|
160
|
+
script = get_completion_script("kctl-claw", shell)
|
|
161
|
+
typer.echo(script)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _run() -> None:
|
|
165
|
+
"""Entry point with error handling."""
|
|
166
|
+
try:
|
|
167
|
+
app()
|
|
168
|
+
except KctlError as e:
|
|
169
|
+
handle_cli_error(e)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
_run()
|
|
File without changes
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Agent management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from kctl_claw.core.callbacks import AppContext
|
|
10
|
+
from kctl_claw.core.exceptions import GatewayError
|
|
11
|
+
from kctl_claw.core.resolve import get_all_agents, resolve_agent
|
|
12
|
+
|
|
13
|
+
_GATEWAY_HINT = "Start the gateway first: kctl-claw deploy up"
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Manage OpenClaw agents.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_(ctx: typer.Context) -> None:
|
|
20
|
+
"""List all configured agents."""
|
|
21
|
+
actx: AppContext = ctx.obj
|
|
22
|
+
out = actx.output
|
|
23
|
+
agents = get_all_agents(actx.config_mgr)
|
|
24
|
+
|
|
25
|
+
rows = []
|
|
26
|
+
json_data = []
|
|
27
|
+
for a in agents:
|
|
28
|
+
name = a["name"]
|
|
29
|
+
model = a.get("model", "default")
|
|
30
|
+
profile = a.get("profile", "default")
|
|
31
|
+
thinking = a.get("thinking", "adaptive")
|
|
32
|
+
rows.append([name, model, profile, thinking])
|
|
33
|
+
json_data.append({"name": name, "model": model, "profile": profile, "thinking": thinking})
|
|
34
|
+
|
|
35
|
+
out.table(
|
|
36
|
+
f"Agents ({len(agents)})",
|
|
37
|
+
[("Name", "cyan"), ("Model", ""), ("Profile", ""), ("Thinking", "dim")],
|
|
38
|
+
rows,
|
|
39
|
+
data_for_json=json_data,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def get(
|
|
45
|
+
ctx: typer.Context,
|
|
46
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Get detailed agent info."""
|
|
49
|
+
actx: AppContext = ctx.obj
|
|
50
|
+
out = actx.output
|
|
51
|
+
agent = resolve_agent(actx.config_mgr, name)
|
|
52
|
+
|
|
53
|
+
sections = [
|
|
54
|
+
(
|
|
55
|
+
"Config",
|
|
56
|
+
[
|
|
57
|
+
("Name", agent["name"]),
|
|
58
|
+
("Model", agent.get("model", "default")),
|
|
59
|
+
("Profile", agent.get("profile", "default")),
|
|
60
|
+
("Thinking", agent.get("thinking", "adaptive")),
|
|
61
|
+
("Sandbox", str(agent.get("sandbox", True))),
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
if agent.get("workspace"):
|
|
66
|
+
sections[0][1].append(("Workspace", agent["workspace"]))
|
|
67
|
+
|
|
68
|
+
out.detail(f"Agent: {name}", sections, data_for_json=agent)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("set-model")
|
|
72
|
+
def set_model(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
75
|
+
model: Annotated[str, typer.Argument(help="Model ID (e.g. claude-opus-4-6)")],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Change an agent's primary model."""
|
|
78
|
+
actx: AppContext = ctx.obj
|
|
79
|
+
out = actx.output
|
|
80
|
+
mgr = actx.config_mgr
|
|
81
|
+
|
|
82
|
+
from kctl_claw.core.config_manager import ConfigFile
|
|
83
|
+
|
|
84
|
+
agent = resolve_agent(mgr, name)
|
|
85
|
+
old_model = agent.get("model", "default")
|
|
86
|
+
|
|
87
|
+
mgr.backup_before_modify(ConfigFile.OPENCLAW)
|
|
88
|
+
data = mgr.read(ConfigFile.OPENCLAW)
|
|
89
|
+
for a in data["agents"]["list"]:
|
|
90
|
+
if a["name"] == name:
|
|
91
|
+
a["model"] = model
|
|
92
|
+
break
|
|
93
|
+
mgr.write(ConfigFile.OPENCLAW, data)
|
|
94
|
+
out.success(f"{name}: {old_model} -> {model}")
|
|
95
|
+
|
|
96
|
+
if actx.live:
|
|
97
|
+
out.info("Reloading gateway...")
|
|
98
|
+
# DockerClient.restart() would go here when available
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("set-profile")
|
|
102
|
+
def set_profile(
|
|
103
|
+
ctx: typer.Context,
|
|
104
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
105
|
+
profile: Annotated[str, typer.Argument(help="Tool profile name")],
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Change an agent's tool profile."""
|
|
108
|
+
actx: AppContext = ctx.obj
|
|
109
|
+
out = actx.output
|
|
110
|
+
mgr = actx.config_mgr
|
|
111
|
+
|
|
112
|
+
from kctl_claw.core.config_manager import ConfigFile
|
|
113
|
+
|
|
114
|
+
agent = resolve_agent(mgr, name)
|
|
115
|
+
old_profile = agent.get("profile", "default")
|
|
116
|
+
|
|
117
|
+
mgr.backup_before_modify(ConfigFile.OPENCLAW)
|
|
118
|
+
data = mgr.read(ConfigFile.OPENCLAW)
|
|
119
|
+
for a in data["agents"]["list"]:
|
|
120
|
+
if a["name"] == name:
|
|
121
|
+
a["profile"] = profile
|
|
122
|
+
break
|
|
123
|
+
mgr.write(ConfigFile.OPENCLAW, data)
|
|
124
|
+
out.success(f"{name}: profile {old_profile} -> {profile}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command("set-thinking")
|
|
128
|
+
def set_thinking(
|
|
129
|
+
ctx: typer.Context,
|
|
130
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
131
|
+
mode: Annotated[str, typer.Argument(help="Thinking mode (adaptive/medium/high/off)")],
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Change an agent's thinking mode."""
|
|
134
|
+
actx: AppContext = ctx.obj
|
|
135
|
+
out = actx.output
|
|
136
|
+
mgr = actx.config_mgr
|
|
137
|
+
|
|
138
|
+
valid_modes = ["adaptive", "medium", "high", "off"]
|
|
139
|
+
if mode not in valid_modes:
|
|
140
|
+
out.error(f"Invalid mode: {mode} (valid: {', '.join(valid_modes)})")
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
|
|
143
|
+
from kctl_claw.core.config_manager import ConfigFile
|
|
144
|
+
|
|
145
|
+
resolve_agent(mgr, name)
|
|
146
|
+
|
|
147
|
+
mgr.backup_before_modify(ConfigFile.OPENCLAW)
|
|
148
|
+
data = mgr.read(ConfigFile.OPENCLAW)
|
|
149
|
+
for a in data["agents"]["list"]:
|
|
150
|
+
if a["name"] == name:
|
|
151
|
+
a["thinking"] = mode
|
|
152
|
+
break
|
|
153
|
+
mgr.write(ConfigFile.OPENCLAW, data)
|
|
154
|
+
out.success(f"{name}: thinking -> {mode}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.command()
|
|
158
|
+
def workspace(
|
|
159
|
+
ctx: typer.Context,
|
|
160
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Show agent workspace structure."""
|
|
163
|
+
actx: AppContext = ctx.obj
|
|
164
|
+
out = actx.output
|
|
165
|
+
resolve_agent(actx.config_mgr, name)
|
|
166
|
+
|
|
167
|
+
root = actx.project_root
|
|
168
|
+
# Main agent uses config/workspace/, others use config/agents/<name>/workspace/
|
|
169
|
+
ws_dir = root / "config" / "workspace" if name == "kodemeiodev" else root / "config" / "agents" / name / "workspace"
|
|
170
|
+
|
|
171
|
+
if not ws_dir.exists():
|
|
172
|
+
out.warn(f"Workspace not found: {ws_dir}")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
nodes = []
|
|
176
|
+
for item in sorted(ws_dir.iterdir()):
|
|
177
|
+
if item.is_dir():
|
|
178
|
+
children = [{"name": f.name} for f in sorted(item.iterdir()) if not f.name.startswith(".")]
|
|
179
|
+
nodes.append({"name": f"{item.name}/", "children": children})
|
|
180
|
+
else:
|
|
181
|
+
size = f"{item.stat().st_size:,} bytes"
|
|
182
|
+
nodes.append({"name": item.name, "info": size})
|
|
183
|
+
|
|
184
|
+
out.tree(f"Workspace: {name}", nodes)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.command()
|
|
188
|
+
def test(
|
|
189
|
+
ctx: typer.Context,
|
|
190
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
191
|
+
prompt: Annotated[str, typer.Argument(help="Test prompt to send")],
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Quick test — send a prompt to an agent and show the response."""
|
|
194
|
+
actx: AppContext = ctx.obj
|
|
195
|
+
out = actx.output
|
|
196
|
+
|
|
197
|
+
resolve_agent(actx.config_mgr, name)
|
|
198
|
+
out.info(f"Testing agent {name!r} with: {prompt[:80]!r}")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
data = actx.gateway.post(f"/api/agents/{name}/message", {"content": prompt, "test": True})
|
|
202
|
+
if isinstance(data, dict):
|
|
203
|
+
sections = [
|
|
204
|
+
(
|
|
205
|
+
"Response",
|
|
206
|
+
[
|
|
207
|
+
("Agent", name),
|
|
208
|
+
("Status", str(data.get("status", ""))),
|
|
209
|
+
("Content", str(data.get("content", ""))[:300]),
|
|
210
|
+
("Model", str(data.get("model", ""))),
|
|
211
|
+
("Tokens", str(data.get("tokens", ""))),
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
out.detail(f"Agent Test: {name}", sections, data_for_json=data)
|
|
216
|
+
else:
|
|
217
|
+
out.text(str(data))
|
|
218
|
+
except GatewayError as e:
|
|
219
|
+
out.warn(f"Gateway not available: {e}")
|
|
220
|
+
out.info(_GATEWAY_HINT)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.command()
|
|
224
|
+
def replay(
|
|
225
|
+
ctx: typer.Context,
|
|
226
|
+
conversation_id: Annotated[str, typer.Argument(help="Conversation ID to replay")],
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Quick replay — replay a conversation and diff the output."""
|
|
229
|
+
actx: AppContext = ctx.obj
|
|
230
|
+
out = actx.output
|
|
231
|
+
|
|
232
|
+
out.info(f"Replaying conversation: {conversation_id!r}...")
|
|
233
|
+
try:
|
|
234
|
+
data = actx.gateway.post(f"/api/conversations/{conversation_id}/replay", {})
|
|
235
|
+
if isinstance(data, dict):
|
|
236
|
+
diffs = data.get("diffs", [])
|
|
237
|
+
rows = [[str(d.get("turn", "")), str(d.get("field", "")), str(d.get("delta", ""))] for d in diffs]
|
|
238
|
+
out.table(
|
|
239
|
+
f"Replay Diff ({len(diffs)} differences)",
|
|
240
|
+
[("Turn", "cyan"), ("Field", ""), ("Delta", "dim")],
|
|
241
|
+
rows,
|
|
242
|
+
data_for_json=data,
|
|
243
|
+
)
|
|
244
|
+
else:
|
|
245
|
+
out.text(str(data))
|
|
246
|
+
except GatewayError as e:
|
|
247
|
+
out.warn(f"Gateway not available: {e}")
|
|
248
|
+
out.info(_GATEWAY_HINT)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@app.command()
|
|
252
|
+
def stats(ctx: typer.Context) -> None:
|
|
253
|
+
"""Show per-agent stats: message count, token usage."""
|
|
254
|
+
actx: AppContext = ctx.obj
|
|
255
|
+
out = actx.output
|
|
256
|
+
|
|
257
|
+
out.info("Fetching agent stats from gateway...")
|
|
258
|
+
try:
|
|
259
|
+
data = actx.gateway.get("/api/agents/stats")
|
|
260
|
+
if isinstance(data, list):
|
|
261
|
+
rows = [
|
|
262
|
+
[
|
|
263
|
+
str(s.get("agent", "")),
|
|
264
|
+
str(s.get("messages", "")),
|
|
265
|
+
str(s.get("tokens_in", "")),
|
|
266
|
+
str(s.get("tokens_out", "")),
|
|
267
|
+
str(s.get("cost_usd", "")),
|
|
268
|
+
]
|
|
269
|
+
for s in data
|
|
270
|
+
]
|
|
271
|
+
out.table(
|
|
272
|
+
f"Agent Stats ({len(data)} agents)",
|
|
273
|
+
[("Agent", "cyan"), ("Messages", ""), ("Tokens In", ""), ("Tokens Out", ""), ("Cost USD", "dim")],
|
|
274
|
+
rows,
|
|
275
|
+
data_for_json=data,
|
|
276
|
+
)
|
|
277
|
+
elif isinstance(data, dict):
|
|
278
|
+
rows = [[k, str(v)] for k, v in data.items()]
|
|
279
|
+
out.table("Agent Stats", [("Key", "cyan"), ("Value", "")], rows)
|
|
280
|
+
else:
|
|
281
|
+
out.text(str(data))
|
|
282
|
+
except GatewayError as e:
|
|
283
|
+
out.warn(f"Gateway not available: {e}")
|
|
284
|
+
out.info(_GATEWAY_HINT)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@app.command("compare-models")
|
|
288
|
+
def compare_models(
|
|
289
|
+
ctx: typer.Context,
|
|
290
|
+
name: Annotated[str, typer.Argument(help="Agent name")],
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Compare model performance for a given agent."""
|
|
293
|
+
actx: AppContext = ctx.obj
|
|
294
|
+
out = actx.output
|
|
295
|
+
|
|
296
|
+
agent = resolve_agent(actx.config_mgr, name)
|
|
297
|
+
current_model = agent.get("model", "default")
|
|
298
|
+
|
|
299
|
+
out.info(f"Model performance comparison for agent {name!r} (current: {current_model!r})")
|
|
300
|
+
try:
|
|
301
|
+
data = actx.gateway.get(f"/api/agents/{name}/model-comparison")
|
|
302
|
+
if isinstance(data, list):
|
|
303
|
+
rows = [
|
|
304
|
+
[
|
|
305
|
+
str(m.get("model", "")),
|
|
306
|
+
str(m.get("avg_latency_ms", "")),
|
|
307
|
+
str(m.get("avg_tokens", "")),
|
|
308
|
+
str(m.get("avg_cost_usd", "")),
|
|
309
|
+
str(m.get("success_rate", "")),
|
|
310
|
+
]
|
|
311
|
+
for m in data
|
|
312
|
+
]
|
|
313
|
+
out.table(
|
|
314
|
+
f"Model Comparison: {name}",
|
|
315
|
+
[("Model", "cyan"), ("Avg Latency", ""), ("Avg Tokens", ""), ("Avg Cost", ""), ("Success %", "dim")],
|
|
316
|
+
rows,
|
|
317
|
+
data_for_json=data,
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
out.text(str(data))
|
|
321
|
+
except GatewayError as e:
|
|
322
|
+
out.warn(f"Gateway not available: {e}")
|
|
323
|
+
out.info(_GATEWAY_HINT)
|