From 54eac3f97a0d6a25157001e50846f39af4eeb6a0 Mon Sep 17 00:00:00 2001 From: Petr Date: Sat, 28 Mar 2026 21:07:02 +0100 Subject: [PATCH] add self-update command and version check for kbagent - kbagent version now checks GitHub releases for newer kbagent versions - kbagent update runs uv tool install --upgrade to self-update - Falls back to pip if uv not available - Shows update hint in version output when new version is available --- CLAUDE.md | 1 + plugins/kbagent/skills/kbagent/SKILL.md | 1 + src/keboola_agent_cli/cli.py | 3 +- src/keboola_agent_cli/commands/context.py | 5 +- src/keboola_agent_cli/commands/version.py | 29 ++++- src/keboola_agent_cli/constants.py | 2 + .../services/version_service.py | 102 ++++++++++++++++++ 7 files changed, 140 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 971555a8..54974925 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -256,4 +256,5 @@ kbagent context kbagent init [--from-global] kbagent doctor [--fix] kbagent version +kbagent update ``` diff --git a/plugins/kbagent/skills/kbagent/SKILL.md b/plugins/kbagent/skills/kbagent/SKILL.md index 4db47fd7..8a414eb4 100644 --- a/plugins/kbagent/skills/kbagent/SKILL.md +++ b/plugins/kbagent/skills/kbagent/SKILL.md @@ -40,6 +40,7 @@ If kbagent is not installed or you need the full standalone reference, run `kbag | Goal | Command | |------|---------| +| Update kbagent to the latest version | `kbagent update` | | Add a new Keboola project connection | `kbagent project add --project ALIAS` | | List all connected Keboola projects | `kbagent project list` | | Remove a Keboola project connection | `kbagent project remove --project ALIAS` | diff --git a/src/keboola_agent_cli/cli.py b/src/keboola_agent_cli/cli.py index 97cda256..5aa3679b 100644 --- a/src/keboola_agent_cli/cli.py +++ b/src/keboola_agent_cli/cli.py @@ -20,7 +20,7 @@ from .commands.storage import storage_app from .commands.sync import sync_app from .commands.tool import tool_app -from .commands.version import version_command +from .commands.version import update_command, version_command from .commands.workspace import workspace_app from .config_store import ConfigStore, resolve_config_dir from .output import OutputFormatter @@ -50,6 +50,7 @@ app.command("init", rich_help_panel=_SETUP)(init_command) app.command("doctor", rich_help_panel=_SETUP)(doctor_command) app.command("version", rich_help_panel=_SETUP)(version_command) +app.command("update", rich_help_panel=_SETUP)(update_command) app.command("context", rich_help_panel=_SETUP)(context_command) app.command("repl", rich_help_panel=_SETUP)(repl_command) diff --git a/src/keboola_agent_cli/commands/context.py b/src/keboola_agent_cli/commands/context.py index bd8914a3..2fa43ecd 100644 --- a/src/keboola_agent_cli/commands/context.py +++ b/src/keboola_agent_cli/commands/context.py @@ -235,7 +235,10 @@ Health checks. --fix auto-installs MCP server binary. kbagent version - Version info and MCP server update check. + Version info, update check for kbagent and MCP server. + + kbagent update + Self-update kbagent to latest version (via uv tool install --upgrade). ## Tips for AI Agents diff --git a/src/keboola_agent_cli/commands/version.py b/src/keboola_agent_cli/commands/version.py index 0473c9b3..fbbfdbf8 100644 --- a/src/keboola_agent_cli/commands/version.py +++ b/src/keboola_agent_cli/commands/version.py @@ -66,7 +66,15 @@ def _format_dep_auto_update(text: Text, dep: dict) -> None: def _format_version_panel(console: Console, data: dict) -> None: """Render version information as a Rich panel.""" text = Text() - text.append(f"kbagent v{data['kbagent']['version']}", style="bold") + kbagent = data["kbagent"] + text.append(f"kbagent v{kbagent['version']}", style="bold") + + if kbagent.get("up_to_date") is False and kbagent.get("latest_version"): + text.append(f" -> v{kbagent['latest_version']} available", style="yellow") + text.append(" (run: kbagent update)", style="dim") + elif kbagent.get("up_to_date") is True: + text.append(" up to date", style="green") + text.append("\n\nDependencies:\n") for dep in data["dependencies"]: @@ -84,3 +92,22 @@ def version_command(ctx: typer.Context) -> None: version_service = get_service(ctx, "version_service") result = version_service.get_versions() formatter.output(result, _format_version_panel) + + +def update_command(ctx: typer.Context) -> None: + """Update kbagent to the latest version. + + Uses 'uv tool install --upgrade' (preferred) or 'pip install --upgrade' + to install the latest version from the GitHub repository. + """ + formatter = get_formatter(ctx) + version_service = get_service(ctx, "version_service") + result = version_service.self_update() + + if formatter.json_mode: + formatter.output(result) + else: + if result["updated"]: + formatter.success(result["message"]) + else: + formatter.console.print(result["message"]) diff --git a/src/keboola_agent_cli/constants.py b/src/keboola_agent_cli/constants.py index 595b64cb..a46de98a 100644 --- a/src/keboola_agent_cli/constants.py +++ b/src/keboola_agent_cli/constants.py @@ -75,6 +75,8 @@ # --- Version Check --- VERSION_CHECK_TIMEOUT: float = 4.0 # seconds for fetching latest version from remote MCP_PYPI_URL: str = "https://pypi.org/pypi/keboola-mcp-server/json" +KBAGENT_GITHUB_REPO: str = "padak/keboola_agent_cli" +KBAGENT_INSTALL_SOURCE: str = "git+https://github.com/padak/keboola_agent_cli" # --- AI Service --- AI_SERVICE_TIMEOUT: httpx.Timeout = httpx.Timeout(connect=5.0, read=15.0, write=5.0, pool=5.0) diff --git a/src/keboola_agent_cli/services/version_service.py b/src/keboola_agent_cli/services/version_service.py index 892c75c4..f5ac8d8d 100644 --- a/src/keboola_agent_cli/services/version_service.py +++ b/src/keboola_agent_cli/services/version_service.py @@ -15,6 +15,8 @@ from .. import __version__ from ..constants import ( + KBAGENT_GITHUB_REPO, + KBAGENT_INSTALL_SOURCE, MCP_PYPI_URL, VERSION_CHECK_TIMEOUT, ) @@ -27,6 +29,34 @@ def _is_uvx_available() -> bool: return shutil.which("uvx") is not None +def _fetch_kbagent_latest_version(timeout: float = VERSION_CHECK_TIMEOUT) -> str | None: + """Fetch latest kbagent version from GitHub releases. + + Args: + timeout: HTTP request timeout in seconds. + + Returns: + Version string like '0.16.0', or None on failure. + """ + try: + response = httpx.get( + f"https://api.github.com/repos/{KBAGENT_GITHUB_REPO}/releases/latest", + timeout=timeout, + follow_redirects=True, + headers={"Accept": "application/vnd.github.v3+json"}, + ) + response.raise_for_status() + tag = response.json().get("tag_name", "") + # Strip leading 'v' from tag (e.g. 'v0.16.0' -> '0.16.0') + version = tag.lstrip("v") + if re.match(r"\d+\.\d+\.\d+", version): + return version + return None + except (httpx.HTTPError, KeyError, ValueError): + logger.debug("Failed to fetch latest kbagent version", exc_info=True) + return None + + def _fetch_mcp_latest_version(timeout: float = VERSION_CHECK_TIMEOUT) -> str | None: """Fetch latest keboola-mcp-server version from PyPI. @@ -90,6 +120,8 @@ def get_versions(self) -> dict[str, Any]: """ uvx_available = _is_uvx_available() mcp_latest = _fetch_mcp_latest_version() + kbagent_latest = _fetch_kbagent_latest_version() + kbagent_up_to_date = _is_up_to_date(__version__, kbagent_latest) mcp_entry: dict[str, Any] = { "name": "keboola-mcp-server", @@ -102,8 +134,78 @@ def get_versions(self) -> dict[str, Any]: return { "kbagent": { "version": __version__, + "latest_version": kbagent_latest, + "up_to_date": kbagent_up_to_date, + "upgrade_command": f"uv tool install --upgrade {KBAGENT_INSTALL_SOURCE}", }, "dependencies": [ mcp_entry, ], } + + def self_update(self) -> dict[str, Any]: + """Update kbagent to the latest version via uv tool install. + + Returns: + Dict with update result (old version, new version, output). + """ + import subprocess + + old_version = __version__ + kbagent_latest = _fetch_kbagent_latest_version() + up_to_date = _is_up_to_date(old_version, kbagent_latest) + + if up_to_date is True: + return { + "updated": False, + "current_version": old_version, + "latest_version": kbagent_latest, + "message": f"kbagent v{old_version} is already up to date.", + } + + # Try uv tool install --upgrade first, fall back to pip + uv_path = shutil.which("uv") + if uv_path: + cmd = [uv_path, "tool", "install", "--upgrade", KBAGENT_INSTALL_SOURCE] + else: + pip_path = shutil.which("pip") + if pip_path is None: + return { + "updated": False, + "current_version": old_version, + "latest_version": kbagent_latest, + "message": "Neither 'uv' nor 'pip' found on PATH. " + f"Install manually: uv tool install --upgrade {KBAGENT_INSTALL_SOURCE}", + } + cmd = [pip_path, "install", "--upgrade", KBAGENT_INSTALL_SOURCE] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=120, + ) + if result.returncode == 0: + return { + "updated": True, + "current_version": old_version, + "latest_version": kbagent_latest, + "message": f"Updated kbagent from v{old_version} to v{kbagent_latest}. " + "Restart your shell to use the new version.", + "output": result.stdout.strip(), + } + return { + "updated": False, + "current_version": old_version, + "latest_version": kbagent_latest, + "message": f"Update failed: {result.stderr.strip()}", + "output": result.stderr.strip(), + } + except subprocess.TimeoutExpired: + return { + "updated": False, + "current_version": old_version, + "latest_version": kbagent_latest, + "message": "Update timed out after 120 seconds.", + }