kctl-claude 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.
@@ -0,0 +1,3 @@
1
+ """kctl-claude — Kodemeio Claude Code CLI."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m kctl_claude."""
2
+
3
+ from kctl_claude.cli import _run
4
+
5
+ _run()
kctl_claude/cli.py ADDED
@@ -0,0 +1,137 @@
1
+ """Main CLI entry point for kctl-claude."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ import sys
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from kctl_claude import __version__
12
+ from kctl_claude.commands.api_cmd import app as api_app
13
+ from kctl_claude.commands.backup_cmd import app as backup_app
14
+ from kctl_claude.commands.config_cmd import app as config_app
15
+ from kctl_claude.commands.doctor_cmd import app as doctor_app
16
+ from kctl_claude.commands.env_cmd import app as env_app
17
+ from kctl_claude.commands.setup_cmd import app as setup_app
18
+ from kctl_claude.commands.status_cmd import app as status_app
19
+ from kctl_claude.commands.sync_cmd import app as sync_app
20
+ from kctl_claude.commands.verify_cmd import app as verify_app
21
+ from kctl_claude.core.callbacks import AppContext
22
+ from kctl_claude.core.plugins import ENTRY_POINT_GROUP, discover_and_load_plugins
23
+ from kctl_claude.commands.skill_cmd import app as skill_app
24
+ from kctl_lib.self_update import notify_if_outdated
25
+
26
+ app = typer.Typer(
27
+ name="kctl-claude",
28
+ help="Kodemeio Claude Code CLI — manage local and remote Claude Code environments.",
29
+ invoke_without_command=True,
30
+ rich_markup_mode="rich",
31
+ pretty_exceptions_enable=False,
32
+ )
33
+
34
+ # Register command groups
35
+ app.add_typer(config_app, name="config")
36
+ app.add_typer(status_app, name="status")
37
+ app.add_typer(sync_app, name="sync")
38
+ app.add_typer(verify_app, name="verify")
39
+ app.add_typer(api_app, name="api")
40
+ app.add_typer(env_app, name="env")
41
+ app.add_typer(backup_app, name="backup")
42
+ app.add_typer(setup_app, name="setup")
43
+ app.add_typer(doctor_app, name="doctor")
44
+ app.add_typer(skill_app, name="skill", hidden=True)
45
+
46
+ # Discover and load plugins
47
+ discover_and_load_plugins(app, ENTRY_POINT_GROUP)
48
+
49
+
50
+ @app.callback()
51
+ def main(
52
+ ctx: typer.Context,
53
+ json_output: Annotated[bool, typer.Option("--json", help="JSON output")] = False,
54
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
55
+ verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Show debug output")] = False,
56
+ version: Annotated[bool, typer.Option("--version", "-V", help="Show version")] = False,
57
+ ) -> None:
58
+ """Global options applied to all commands."""
59
+ if version:
60
+ typer.echo(f"kctl-claude {__version__}")
61
+ raise typer.Exit()
62
+
63
+ ctx.ensure_object(dict)
64
+ ctx.obj = AppContext(json_mode=json_output, quiet=quiet, verbose=verbose)
65
+ notify_if_outdated(ctx.obj.output, "kctl-claude", __version__)
66
+
67
+ # Record command in history (non-blocking, best-effort)
68
+ try:
69
+ from kctl_lib.history import HistoryStore
70
+
71
+ store = HistoryStore("kctl-claude")
72
+ store.ensure_schema(
73
+ [
74
+ """CREATE TABLE IF NOT EXISTS commands (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ command TEXT NOT NULL,
77
+ timestamp TEXT NOT NULL
78
+ )"""
79
+ ]
80
+ )
81
+ store.record(table="commands", command=" ".join(sys.argv[1:]))
82
+ except Exception:
83
+ pass # History recording is best-effort
84
+
85
+ # If no subcommand, show help
86
+ if ctx.invoked_subcommand is None and not version:
87
+ typer.echo(ctx.get_help())
88
+ raise typer.Exit()
89
+
90
+
91
+ @app.command()
92
+ def completions(
93
+ shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
94
+ install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
95
+ ) -> None:
96
+ """Generate or install shell completions."""
97
+ from kctl_lib.completions import get_completion_script, install_completions
98
+
99
+ if install:
100
+ path = install_completions("kctl-claude", shell)
101
+ if path:
102
+ typer.echo(f"Completions installed to {path}")
103
+ else:
104
+ typer.echo(f"Could not install completions for {shell}", err=True)
105
+ raise typer.Exit(code=1)
106
+ else:
107
+ script = get_completion_script("kctl-claude", shell)
108
+ typer.echo(script)
109
+
110
+
111
+ @app.command("update")
112
+ def update_cmd(ctx: typer.Context) -> None:
113
+ """Check for updates and upgrade kctl-claude."""
114
+ actx: AppContext = ctx.obj
115
+ out = actx.output
116
+
117
+ from kctl_lib.self_update import check_update
118
+ from kctl_lib.self_update import update as do_update
119
+
120
+ latest = check_update("kctl-claude", __version__)
121
+ if latest:
122
+ out.info(f"Updating to {latest}...")
123
+ do_update("kctl-claude")
124
+ out.success(f"Updated to {latest}")
125
+ else:
126
+ out.success("Already up to date")
127
+
128
+
129
+ def _run() -> None:
130
+ """Entry point with error handling."""
131
+ try:
132
+ app()
133
+ except KeyboardInterrupt:
134
+ sys.exit(130)
135
+ except Exception as e:
136
+ typer.echo(f"Error: {e}", err=True)
137
+ sys.exit(1)
File without changes
@@ -0,0 +1,97 @@
1
+ """kctl-claude api — interact with the SDK REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import httpx
8
+ import typer
9
+
10
+ from kctl_claude.core.callbacks import AppContext
11
+
12
+ app = typer.Typer(help="SDK REST API operations.")
13
+
14
+ DEFAULT_URL = "http://localhost:3100"
15
+
16
+
17
+ def _base_url() -> str:
18
+ import os
19
+
20
+ return os.environ.get("SDK_BASE_URL", DEFAULT_URL)
21
+
22
+
23
+ def _api_key() -> str:
24
+ import os
25
+
26
+ return os.environ.get("SDK_API_KEY", "")
27
+
28
+
29
+ @app.command()
30
+ def health(ctx: typer.Context) -> None:
31
+ """Check SDK API health."""
32
+ actx: AppContext = ctx.obj
33
+ out = actx.output
34
+ try:
35
+ resp = httpx.get(f"{_base_url()}/health", timeout=5)
36
+ try:
37
+ data = resp.json()
38
+ except Exception:
39
+ data = {}
40
+ if actx.json_mode:
41
+ out.raw_json(data)
42
+ else:
43
+ out.success(f"Status: {data.get('status', 'unknown')}")
44
+ out.success(f"Tasks: {data.get('inFlight', 0)}/{data.get('maxConcurrent', 3)} in flight")
45
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
46
+ out.error(f"SDK API unreachable: {e}")
47
+ raise typer.Exit(code=1) from None
48
+
49
+
50
+ @app.command()
51
+ def task(
52
+ ctx: typer.Context,
53
+ prompt: Annotated[str, typer.Argument(help="Task prompt for Claude Code")],
54
+ workspace: Annotated[str, typer.Option("--workspace", "-w", help="Workspace name")] = "",
55
+ bare: Annotated[bool, typer.Option("--bare", help="Skip auto-discovery")] = False,
56
+ ) -> None:
57
+ """Send a task to Claude Code via SDK API."""
58
+ actx: AppContext = ctx.obj
59
+ out = actx.output
60
+
61
+ # Validate workspace path to prevent traversal attacks
62
+ if workspace and (".." in workspace or workspace.startswith("/")):
63
+ out.error("Invalid workspace path")
64
+ raise typer.Exit(code=1)
65
+
66
+ headers = {"Content-Type": "application/json"}
67
+ key = _api_key()
68
+ if key:
69
+ headers["Authorization"] = f"Bearer {key}"
70
+
71
+ body: dict = {"prompt": prompt}
72
+ if workspace:
73
+ body["workspace"] = workspace
74
+ if bare:
75
+ body["bare"] = True
76
+
77
+ try:
78
+ resp = httpx.post(f"{_base_url()}/task", json=body, headers=headers, timeout=310)
79
+ try:
80
+ data = resp.json()
81
+ except Exception:
82
+ data = {}
83
+ if actx.json_mode:
84
+ out.raw_json(data)
85
+ else:
86
+ if resp.status_code == 200:
87
+ out.success("Task completed")
88
+ out.text(data.get("result", str(data)))
89
+ else:
90
+ out.error(f"Error {resp.status_code}: {data.get('error', str(data))}")
91
+ raise typer.Exit(code=1)
92
+ except httpx.TimeoutException:
93
+ out.error("Task timed out (310s)")
94
+ raise typer.Exit(code=1) from None
95
+ except httpx.ConnectError as e:
96
+ out.error(f"SDK API unreachable: {e}")
97
+ raise typer.Exit(code=1) from None
@@ -0,0 +1,53 @@
1
+ """kctl-claude backup — runtime volume backup/restore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from kctl_lib.exceptions import CommandError
10
+
11
+ from kctl_claude.core.callbacks import AppContext
12
+ from kctl_claude.core.runner import run_script
13
+
14
+ app = typer.Typer(help="Backup and restore Claude Code runtime.")
15
+
16
+
17
+ @app.command("create")
18
+ def create_backup(ctx: typer.Context) -> None:
19
+ """Backup container runtime volume."""
20
+ actx: AppContext = ctx.obj
21
+ out = actx.output
22
+ repo = actx.repo_dir
23
+ if not repo:
24
+ out.error("Repo not found.")
25
+ raise typer.Exit(code=1)
26
+ script = repo / "scripts" / "backup-runtime.sh"
27
+ try:
28
+ run_script(script, capture=False)
29
+ out.success("Backup created")
30
+ except CommandError as e:
31
+ out.error(f"Backup failed (exit {e.returncode}): {e.stderr}")
32
+ raise typer.Exit(code=1) from None
33
+
34
+
35
+ @app.command()
36
+ def restore(
37
+ ctx: typer.Context,
38
+ backup_dir: Annotated[str, typer.Argument(help="Path to backup directory")],
39
+ ) -> None:
40
+ """Restore runtime from backup."""
41
+ actx: AppContext = ctx.obj
42
+ out = actx.output
43
+ out.info(f"Restoring from {backup_dir}...")
44
+ result = subprocess.run(
45
+ ["docker", "cp", f"{backup_dir}/.", "kodemeio-claude:/home/dev/.claude/"],
46
+ capture_output=True,
47
+ text=True,
48
+ )
49
+ if result.returncode == 0:
50
+ out.success("Restore complete")
51
+ else:
52
+ out.error(f"Restore failed (exit {result.returncode}): {result.stderr.strip()}")
53
+ raise typer.Exit(code=1)