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.
- kctl_claude/__init__.py +3 -0
- kctl_claude/__main__.py +5 -0
- kctl_claude/cli.py +137 -0
- kctl_claude/commands/__init__.py +0 -0
- kctl_claude/commands/api_cmd.py +97 -0
- kctl_claude/commands/backup_cmd.py +53 -0
- kctl_claude/commands/config_cmd.py +436 -0
- kctl_claude/commands/doctor_cmd.py +207 -0
- kctl_claude/commands/env_cmd.py +83 -0
- kctl_claude/commands/setup_cmd.py +170 -0
- kctl_claude/commands/skill_cmd.py +76 -0
- kctl_claude/commands/status_cmd.py +244 -0
- kctl_claude/commands/sync_cmd.py +83 -0
- kctl_claude/commands/verify_cmd.py +111 -0
- kctl_claude/core/__init__.py +0 -0
- kctl_claude/core/callbacks.py +55 -0
- kctl_claude/core/checks.py +249 -0
- kctl_claude/core/config.py +21 -0
- kctl_claude/core/exceptions.py +17 -0
- kctl_claude/core/output.py +39 -0
- kctl_claude/core/paths.py +47 -0
- kctl_claude/core/plugins.py +7 -0
- kctl_claude/core/runner.py +48 -0
- kctl_claude-0.2.0.dist-info/METADATA +17 -0
- kctl_claude-0.2.0.dist-info/RECORD +27 -0
- kctl_claude-0.2.0.dist-info/WHEEL +4 -0
- kctl_claude-0.2.0.dist-info/entry_points.txt +2 -0
kctl_claude/__init__.py
ADDED
kctl_claude/__main__.py
ADDED
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)
|