Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ A fresh Windows install has no real Python either: `python` resolves to a Micros

Auto-updates kbagent on every launch; the self-update prefers the prebuilt wheel when available. Run `kbagent changelog` to see what changed. (Since 0.85.0 kbagent updates itself only -- if you also run `keboola-mcp-server` in Claude Desktop / Cursor, refresh it yourself with `uv tool install --upgrade --prerelease=allow keboola-mcp-server`.)

### Installed under the old package name

The package was named `keboola-agent-cli` up to 0.62.0. It is named `keboola-cli` from 0.63.0. Both packages install the `kbagent` executable. uv treats an update to `keboola-cli` as a new tool and does not overwrite the executable that the old tool owns.

To check which name you have, run `uv tool list`. An old install is listed as `keboola-agent-cli`.

- kbagent 0.59.x and older cannot update itself. Every command prints `Auto-update failed; continuing with current version.` and the version does not change. Without the `[server]` extras, the uv error behind it is ``error: Executable already exists: kbagent (use `--force` to overwrite)``. No release can change this, because the update code runs from the old install.
- kbagent 0.60.0 to 0.62.x updates through a compatibility wheel and stays on the old name.
- kbagent 0.63.1 and newer installs a second tool, `keboola-cli`, when it updates. The new tool takes over the `kbagent` executable and the old `keboola-agent-cli` entry stays in `uv tool list`.

To move to `keboola-cli`, run these two commands in this order, in a shell where no kbagent is running:

```bash
uv tool uninstall keboola-agent-cli
curl -LsSf https://raw.githubusercontent.com/keboola/cli/main/install.sh | sh
```

On Windows, run the PowerShell install from the Windows section above as the second step. Your configuration is not touched: the configuration folder keeps the name `keboola-agent-cli` on purpose.

If `uv tool list` shows `keboola-cli` next to `keboola-agent-cli`, the update already worked. kbagent runs from `keboola-cli` and the old entry does no harm. Do not remove the old entry on its own: `uv tool uninstall keboola-agent-cli` also deletes the `kbagent` executable that `keboola-cli` uses, and the shell then reports `command not found`. To restore the executable, run `uv tool install --force --reinstall "keboola-cli[server] @ https://github.com/keboola/cli/releases/download/v<version>/keboola_cli-<version>-py3-none-any.whl"` with the latest version number from the [releases page](https://github.com/keboola/cli/releases/latest).

A failed startup update prints the last line of the uv output as `Cause:` (since vNEXT), then the recovery command.

## Web UI (optional)

Want a browser dashboard? One command:
Expand Down
12 changes: 12 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -6271,3 +6271,15 @@ WSL `open_browser` routes through `wslview`, because `webbrowser` keys its
Windows handling off `sys.platform == "win32"` -- `linux` inside WSL -- and
would otherwise find no handler or open a Linux-side browser that shares none
of the logins the flow needs.

## An install under the old package name `keboola-agent-cli` does not update to `keboola-cli`

*(since vNEXT, #771)*

The package was named `keboola-agent-cli` up to 0.62.0 and is named `keboola-cli` from 0.63.0. Both install the `kbagent` executable, so uv treats an update to `keboola-cli` as a new tool. If a user reports a kbagent that never updates, run `uv tool list` and look for a `keboola-agent-cli` entry.

- **kbagent 0.59.x and older cannot update itself.** Every command prints `Auto-update failed; continuing with current version.` with no cause. Without the `[server]` extras the uv error is ``error: Executable already exists: kbagent (use `--force` to overwrite)``. The update code runs from the old install, so no release changes this.
- **Do not tell the user to run `kbagent update` or the `upgrade_command` from that old `kbagent version --json`.** Both run the same failing command.
- **Move the user with two commands, in this order:** `uv tool uninstall keboola-agent-cli`, then the install one-liner from the README. The configuration folder keeps the name `keboola-agent-cli` and is not touched.
- **The order is load-bearing.** kbagent 0.63.1 and newer updates by installing a second tool, `keboola-cli`, that takes over the `kbagent` executable. `uv tool uninstall keboola-agent-cli` after that also deletes the executable, and the shell reports `command not found`. Reinstall with `uv tool install --force --reinstall "keboola-cli[server] @ <release wheel URL>"`.
- **Failure banner.** A failed startup update now prints a `Cause:` line with the last line of the uv output, then `Recover with: <command>`, on stderr. Before, it printed only `Auto-update failed; continuing with current version.` and the recovery command. `kbagent update` is unchanged: its `update FAILED:` summary already ends with the last uv line.
53 changes: 37 additions & 16 deletions src/keboola_agent_cli/auto_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import shutil
import sys
import time
from dataclasses import dataclass
from importlib.metadata import distribution
from pathlib import Path

Expand All @@ -39,6 +40,7 @@
get_update_timeout,
prepare_kbagent_update_plan,
resolve_kbagent_wheel_url,
summarize_install_failure,
)
from .update_runner import (
DeferredUpdateReport,
Expand Down Expand Up @@ -68,6 +70,14 @@ class UpdateOutcome(enum.Enum):
FAILED = "failed"


@dataclass(frozen=True)
class UpdateResult:
"""An :class:`UpdateOutcome` plus what the installer printed when it failed (issue #771)."""

outcome: UpdateOutcome
output: str = ""


# Process-level sentinel for the auto-update flow.
#
# Bug D fix from issue #263: ``kbagent repl`` re-enters the entire CLI
Expand Down Expand Up @@ -273,9 +283,7 @@ def _should_skip() -> bool:
return _should_skip_kbagent_stage() or _should_skip_all()


def _perform_update(
latest_version: str, *, command: tuple[str, ...] | None = None
) -> UpdateOutcome:
def _perform_update(latest_version: str, *, command: tuple[str, ...] | None = None) -> UpdateResult:
"""Download and install the latest version.

Delegates to :func:`build_kbagent_upgrade_command` so this path stays
Expand All @@ -290,10 +298,10 @@ def _perform_update(
latest_version: The version being updated to (for logging).

Returns:
:class:`UpdateOutcome`: ``SUCCESS`` on a clean install, ``TIMEOUT`` when
the install outran :func:`get_update_timeout` -- it is still running and
will finish on its own, so the next launch picks it up -- and ``FAILED``
otherwise.
:class:`UpdateResult` whose ``outcome`` is ``SUCCESS`` on a clean install,
``TIMEOUT`` when the install outran :func:`get_update_timeout` -- it is
still running and will finish on its own, so the next launch picks it up --
and ``FAILED`` otherwise, with the installer's output in ``output``.
"""
# ``command`` is supplied by the startup planner. Keep the fallback only
# for direct legacy callers; it must never be used after another stage has
Expand All @@ -304,17 +312,32 @@ def _perform_update(
else:
cmd = list(command)
if cmd is None:
return UpdateOutcome.FAILED
return UpdateResult(UpdateOutcome.FAILED)

# Deliberately does NOT kill the installer when the deadline passes -- see
# ``update_runner.run_install``. A terminated uv leaves the same
# half-removed venv a Windows file lock does (issue #528).
run = run_install(tuple(cmd), timeout=get_update_timeout())
if run.status is InstallStatus.SUCCEEDED:
return UpdateOutcome.SUCCESS
return UpdateResult(UpdateOutcome.SUCCESS)
if run.status is InstallStatus.STILL_RUNNING:
return UpdateOutcome.TIMEOUT
return UpdateOutcome.FAILED
return UpdateResult(UpdateOutcome.TIMEOUT)
return UpdateResult(UpdateOutcome.FAILED, run.output)


def _format_update_failure(output: str, recovery_command: str | None) -> str:
"""Render the startup banner for a failed install (issue #771).

Names the line the installer ended with, so the user can tell a network
error from a permanent one.
"""
lines = ["Auto-update failed; continuing with current version."]
cause = summarize_install_failure(output)
if cause:
lines.append(f"Cause: {cause}")
if recovery_command:
lines.append(f"Recover with: {recovery_command}")
return "\n".join(lines) + "\n"


def _re_exec() -> None:
Expand Down Expand Up @@ -594,9 +617,10 @@ def maybe_auto_update() -> None:
return

sys.stderr.write(f"Updating kbagent v{__version__} -> v{kbagent_plan.latest_version}...\n")
outcome = _perform_update(
result = _perform_update(
kbagent_plan.latest_version or __version__, command=kbagent_plan.command
)
outcome = result.outcome
if outcome is UpdateOutcome.SUCCESS:
sys.stderr.write(f"Updated to v{kbagent_plan.latest_version}. Re-launching...\n")
os.environ[ENV_UPDATED_FROM] = __version__
Expand All @@ -611,10 +635,7 @@ def maybe_auto_update() -> None:
"background and applies on the next launch.\n"
)
else:
sys.stderr.write(
"Auto-update failed; continuing with current version. Recover with: "
f"{kbagent_plan.recovery_command}\n"
)
sys.stderr.write(_format_update_failure(result.output, kbagent_plan.recovery_command))
except Exception:
# Blanket catch: auto-update must NEVER crash the CLI.
logger.debug("Auto-update check failed", exc_info=True)
14 changes: 12 additions & 2 deletions src/keboola_agent_cli/services/version_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ def _recovery_command(command: tuple[str, ...] | None, target_version: str | Non
)


def summarize_install_failure(output: str | None) -> str:
"""Return the last non-empty line of an installer transcript, or "".

uv and pip print their actionable line last (for example
``error: Executable already exists: kbagent (use `--force` to overwrite)``),
so this is the cause to show in a one-line failure message.
"""
lines = [line.strip() for line in (output or "").splitlines() if line.strip()]
return lines[-1] if lines else ""


def _fetch_kbagent_latest_version(
timeout: float = VERSION_CHECK_TIMEOUT, *, include_prerelease: bool = False
) -> str | None:
Expand Down Expand Up @@ -634,8 +645,7 @@ def _summarize_failure_tail(message: str | None) -> str:
surface only that tail in the one-line summary -- the full transcript
stays in the result's ``output`` for ``--json`` / ``--verbose``.
"""
lines = [ln.strip() for ln in (message or "").splitlines() if ln.strip()]
return lines[-1] if lines else "update failed"
return summarize_install_failure(message) or "update failed"

@classmethod
def _compose_update_summary(cls, kbagent_result: dict[str, Any]) -> str:
Expand Down
Loading
Loading