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.
Files changed (48) hide show
  1. kctl_claw/__init__.py +3 -0
  2. kctl_claw/__main__.py +5 -0
  3. kctl_claw/cli.py +173 -0
  4. kctl_claw/commands/__init__.py +0 -0
  5. kctl_claw/commands/agents.py +323 -0
  6. kctl_claw/commands/agents_test.py +264 -0
  7. kctl_claw/commands/ai.py +122 -0
  8. kctl_claw/commands/aliases.py +63 -0
  9. kctl_claw/commands/backup.py +112 -0
  10. kctl_claw/commands/config_cmd.py +248 -0
  11. kctl_claw/commands/config_drift.py +169 -0
  12. kctl_claw/commands/cron.py +338 -0
  13. kctl_claw/commands/cron_debug.py +220 -0
  14. kctl_claw/commands/deploy.py +217 -0
  15. kctl_claw/commands/docker_cmd.py +198 -0
  16. kctl_claw/commands/doctor_cmd.py +222 -0
  17. kctl_claw/commands/env.py +154 -0
  18. kctl_claw/commands/health.py +94 -0
  19. kctl_claw/commands/lint_cmd.py +267 -0
  20. kctl_claw/commands/logs.py +71 -0
  21. kctl_claw/commands/mcp.py +322 -0
  22. kctl_claw/commands/mcp_test.py +250 -0
  23. kctl_claw/commands/memory.py +359 -0
  24. kctl_claw/commands/monitor_cmd.py +192 -0
  25. kctl_claw/commands/pipeline.py +200 -0
  26. kctl_claw/commands/prompts.py +286 -0
  27. kctl_claw/commands/security.py +169 -0
  28. kctl_claw/commands/skill_cmd.py +76 -0
  29. kctl_claw/commands/skills.py +144 -0
  30. kctl_claw/commands/skills_test.py +269 -0
  31. kctl_claw/commands/status.py +78 -0
  32. kctl_claw/commands/telegram.py +235 -0
  33. kctl_claw/commands/test_cmd.py +213 -0
  34. kctl_claw/commands/trading.py +269 -0
  35. kctl_claw/core/__init__.py +0 -0
  36. kctl_claw/core/callbacks.py +74 -0
  37. kctl_claw/core/config.py +118 -0
  38. kctl_claw/core/config_manager.py +123 -0
  39. kctl_claw/core/docker_client.py +191 -0
  40. kctl_claw/core/exceptions.py +38 -0
  41. kctl_claw/core/gateway_client.py +61 -0
  42. kctl_claw/core/models.py +133 -0
  43. kctl_claw/core/output.py +5 -0
  44. kctl_claw/core/resolve.py +63 -0
  45. kctl_claw-0.2.0.dist-info/METADATA +19 -0
  46. kctl_claw-0.2.0.dist-info/RECORD +48 -0
  47. kctl_claw-0.2.0.dist-info/WHEEL +4 -0
  48. kctl_claw-0.2.0.dist-info/entry_points.txt +2 -0
kctl_claw/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """kctl-claw: Kodemeio OpenClaw CLI."""
2
+
3
+ __version__ = "0.2.0"
kctl_claw/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m kctl_claw."""
2
+
3
+ from kctl_claw.cli import _run
4
+
5
+ _run()
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)