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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.43.2",
"version": "0.43.3",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
47 changes: 45 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,46 @@ All three inherit from `BaseHttpClient` (`http_base.py`) which provides shared r

**When bumping the version**: edit `pyproject.toml`, add a changelog entry to `src/keboola_agent_cli/changelog.py`, then run `make version-sync`. Do not edit `__init__.py` or `plugin.json` manually. CI enforces changelog completeness via `make changelog-check`.

### Beta / pre-release versions (since 0.43.3)

Beta and release-candidate versions follow **PEP 440**: `0.44.0b1`, `0.44.0rc1`, ... -- **not** the SemVer `-beta.1` form (hatchling + uv require PEP 440 syntax in `pyproject.toml`). Three independent gates keep stable users safe from accidentally landing on a beta:

1. **PEP 440 pre-release suffix.** pip / uv default to **skipping** pre-releases unless told otherwise (`--pre` for pip, `--prerelease=allow` for uv).
2. **GitHub Release `prerelease: true` flag.** The auto-update startup hook calls `/releases/latest`, which GitHub defines as "the most recent non-prerelease, non-draft release". Marking the release `--prerelease` makes it invisible to the auto-update path.
3. **Tag-pinned install URL.** When `--beta` opts into a pre-release, the install command appends `@v<version>` to the git+ source URL so uv pulls the **exact commit** the tag points to. Without this, uv would resolve the default branch (`main`) and -- if the beta lives on a feature branch -- silently install the stale main HEAD even though the version fetcher advertised the beta tag.

**Author workflow (release a beta from a feature branch):**

```bash
# 1. Bump pyproject.toml to PEP 440 pre-release form on the feature branch
# version = "0.44.0b1"
make version-sync # propagates to plugin.json / marketplace.json

# 2. Add a changelog entry under that key in src/keboola_agent_cli/changelog.py
# 3. Commit + push to PR (NOT to main -- main stays on the stable channel)
git push origin feat/my-feature

# 4. Tag the PR head SHA + push tag
git tag v0.44.0b1 && git push origin v0.44.0b1

# 5. KEY STEP: create the GitHub Release WITH --prerelease pointing at the tag
gh release create v0.44.0b1 --prerelease \
--title "v0.44.0 — Beta 1" \
--notes-file release-notes-0.44.0b1.md
```

When the beta cooks long enough, merge the PR (stable squash) and ship `0.44.0` from main with a normal release **without** `--prerelease` -- auto-update picks it up on next startup.

**User opt-in (consume betas):**

- One-shot: `kbagent update --beta` -- resolver opts into pre-releases for this invocation only.
- Per-shell: `export KBAGENT_INCLUDE_PRERELEASE=1` -- every `kbagent update` / `kbagent version` in that shell treats betas as installable.
- **No persistent setting.** Each beta install is an active choice; never a forgotten "I once typed --beta six months ago" foot-gun.

The startup auto-update hook is **never** affected by `--beta` / env opt-in -- it always uses `/releases/latest` (stable channel). Beta installs only come from explicit `kbagent update --beta`.

Full author checklist: see `CONTRIBUTING.md` > "Releasing a beta (pre-release) version".

## Coding Conventions

> **0. (BINDING) Follow [CONTRIBUTING.md](CONTRIBUTING.md) in full.** Every code change -- human or AI agent -- must satisfy the rules in `CONTRIBUTING.md`. Specifically, the "Code Quality Patterns" section is non-negotiable: dataclasses (not bare tuples) for multi-value returns; categorical arguments before variable ones; `ErrorCode` enum (never raw strings); file-size budgets; context managers over lambdas; named functions over assigned anonymous functions; `ty` clean for new code. The `.claude/settings.json` post-edit hooks run `ruff check --fix`, `ruff format`, and `ty check` after every edit -- when an AI agent edits a file in this repo, those checks fire automatically and any failure must be addressed before continuing. If a rule conflicts with an existing pattern in legacy code, **fix it in the PR you are touching** or open a follow-up issue; do not propagate the pattern.
Expand Down Expand Up @@ -447,8 +487,11 @@ kbagent schedule find [--cron-window START-END] [--not-run-since DAYS] [--projec
kbagent context
kbagent init [--from-global]
kbagent doctor [--fix]
kbagent version
kbagent update
kbagent version [--beta]
kbagent update [--beta]
# `--beta` (or env `KBAGENT_INCLUDE_PRERELEASE=1`) opts into pre-release versions
# (PEP 440 betas/rc, e.g. 0.43.0b1). Default (no flag) is stable-only -- auto-update
# startup hook never silently lands on a beta.
kbagent changelog [--limit N]
kbagent serve [--host HOST] [--port PORT] [--ui] [--ui-dist PATH] [--reload] [--log-level LVL] [--cors-origin ORIGIN] [--config-dir DIR]
```
58 changes: 58 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,13 @@ steps 5-8 is that **CI will not catch you** if you skip them; they are the
manual safety net for the silent-drift risks summarized in the
[Plugin synchronization map](#plugin-synchronization-map) above.

> **Want to ship a beta first?** You can. PEP 440 pre-release versions
> (`0.43.0b1`, `0.43.0rc1`) are fully supported by `kbagent update --beta`
> since v0.42.0. The startup auto-update hook never silently lands on a
> beta -- only explicit opt-in installs them. See
> [Releasing a beta (pre-release) version](#releasing-a-beta-pre-release-version)
> below for the workflow.

1. **Edit `pyproject.toml`** -- bump `version = "X.Y.Z"`. Single source of truth; everything else derives from it.
2. **Add a changelog entry** to `src/keboola_agent_cli/changelog.py` -- one entry per release, no exceptions. CI fails (`make changelog-check`) if this is missing.
3. **Run `make version-sync`** -- propagates the new version to `plugins/kbagent/.claude-plugin/plugin.json`. The pre-commit hook does this automatically on `git commit`, but running it explicitly lets you eyeball the diff.
Expand All @@ -547,6 +554,57 @@ If any of steps 5-8 reveal "I should have done this in the PR that introduced
the command, not at release time", **also patch the per-command checklist**
above so the next contributor catches the gap earlier.

### Releasing a beta (pre-release) version

Beta and release-candidate versions follow PEP 440: `X.Y.Zb1`, `X.Y.Zb2`,
`X.Y.Zrc1`, ... -- not the SemVer `-beta.1` form (hatchling and uv require
PEP 440 syntax in `pyproject.toml`'s `version` field). Two gates keep stable
users safe from accidentally landing on a beta:

1. **Version string itself.** PEP 440 marks any pre-release suffix as such;
`pip install keboola-agent-cli` and `uv tool install ...` default to
**skipping** pre-releases unless the resolver is told otherwise (`--pre`
for pip, `--prerelease=allow` for uv).
2. **GitHub Release `prerelease: true` flag.** The auto-update startup
hook calls `GET /releases/latest`, which GitHub explicitly defines as
"the most recent non-prerelease, non-draft release". Marking the release
`--prerelease` makes it invisible to the auto-update path.

**Workflow:**

1. Bump `pyproject.toml` to the PEP 440 pre-release version
(e.g. `0.43.0b1`).
2. Add a changelog entry under that key in `src/keboola_agent_cli/changelog.py`.
3. `make version-sync` propagates the version to `plugin.json` /
`marketplace.json`.
4. Tag and push: `git tag v0.43.0b1 && git push origin v0.43.0b1`.
5. Create the GitHub release **with the `--prerelease` flag**:
```bash
gh release create v0.43.0b1 --prerelease \
--title "v0.43.0 — Beta 1" \
--notes-file release-notes-0.43.0b1.md
```
6. Test by installing yourself: `kbagent update --beta` (or set
`KBAGENT_INCLUDE_PRERELEASE=1` in env). Users who do **not** opt in
keep getting the latest stable; the new beta is invisible to them.
7. Once the beta cooks long enough, bump to the stable equivalent
(`0.43.0`), retag, and create the release **without** `--prerelease`
so auto-update picks it up.

**Users opt in two ways:**

- One-shot: `kbagent update --beta` (resolver is told `--prerelease=allow`
/ `--pre`, GitHub query switches to `/releases` and picks the highest
PEP 440 version including pre-releases).
- Per-session env var: `export KBAGENT_INCLUDE_PRERELEASE=1` -- every
subsequent `kbagent update` / `kbagent version` in that shell treats
betas as installable.

**Never persists.** There is no `release_channel: beta` config setting --
each invocation has to opt in. This is deliberate: betas should always be
an active choice, never a forgotten "I once typed --beta six months ago"
foot-gun.

## Running CI Locally

```bash
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.43.2",
"version": "0.43.3",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
5 changes: 3 additions & 2 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ a critical failure.
user's local claude / codex / gemini CLI; backs the dashboard
Local AI tile that replaces Kai for non-master-token projects)
needs 0.41.9+,
data-app workspace + CLI sandbox annotation = 0.42.0+ (#304),
HTTP opt-in `?include_sandbox_annotation=true` = 0.43.1+ (#312),
data-app CLI sandbox annotation = 0.42.0+ (#304),
HTTP `?include_sandbox_annotation=true` = 0.43.1+ #312,
`kbagent update --beta` = 0.43.3+,
`storage retype` is a future composite), you
MUST refuse the task and return a handoff message to the parent:
`"Cannot proceed safely on kbagent <version>. Missing: <commands>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ All commands support `--json` for structured output. Multi-project flags (`--pro
## Setup & Info
- `init [--from-global]` -- create local `.kbagent/` workspace in current directory
- `doctor [--fix]` -- health check for CLI config and MCP server
- `version` -- show version info and dependency update status
- `update` -- self-update to latest version
- `version [--beta]` -- show version info and dependency update status. `--beta` (since v0.42.0) reports the latest pre-release (beta / rc) instead of the latest stable. Env override: `KBAGENT_INCLUDE_PRERELEASE=1`
- `update [--beta]` -- self-update to latest version. `--beta` (since v0.42.0) opts into pre-release versions (PEP 440 betas / rc, e.g. `0.43.0b1`). Default behaviour: GitHub's `/releases/latest` endpoint filters prereleases server-side, so the startup auto-update hook never silently lands on a beta. Resolver-level opt-in (`--prerelease=allow` for uv, `--pre` for pip) is added automatically when `--beta` is set
- `changelog [--limit N]` -- show recent changelog (default: last 5 versions). After auto-update, "What's new" is printed automatically. Manual trigger: `KBAGENT_UPDATED_FROM=0.17.0 kbagent version`
- `context` -- print full CLI reference for AI agents

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.43.2"
version = "0.43.3"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
3 changes: 3 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.43.3": [
"New: `kbagent update --beta` (alternatively `KBAGENT_INCLUDE_PRERELEASE=1` per-shell env var) opts into pre-release versions. Default behaviour is unchanged -- the startup auto-update hook hits GitHub's `/releases/latest` endpoint, which is defined as the latest non-prerelease, non-draft release; betas marked `--prerelease` are invisible. With `--beta`, the version fetcher switches to `/releases` (plural) and picks the highest PEP 440 version including pre-releases (e.g. `0.44.0b1` beats `0.43.3`). The install command additionally propagates `--prerelease=allow` (uv) / `--pre` (pip) so the resolver accepts PEP 440 pre-release tags that it would otherwise refuse by default, AND appends `@v<version>` to the git+ install URL so uv installs the exact commit pointed to by the tag rather than the default branch (this matters when beta tags live on a feature branch, not main -- without `@v<version>` uv would always install the latest main commit, even though `_fetch_kbagent_latest_prerelease` advertised a different version). `kbagent version --beta` mirrors the same lookup for inspection. No `release_channel: beta` persistent config setting -- each opt-in is ad-hoc and explicit so a beta install is never a forgotten preference. CONTRIBUTING.md gets a new 'Releasing a beta' workflow section documenting the PEP 440 + `gh release create --prerelease` convention. 12 unit tests in `test_version_service.py` (default uses /releases/latest, prerelease uses /releases with PEP 440 sort, skips drafts, falls back to stable, ignores invalid tags, HTTP failure returns None, `build_kbagent_upgrade_command` propagates `--prerelease=allow` for uv + `--pre` for pip, prerelease+target_version appends `@v<version>` to git URL, stable install URL is unchanged when target_version not provided).",
],
"0.43.2": [
"UX: `kbagent changelog` now renders entries with Rich-styled prefixes (`New:` bold green, `Fix:` bold yellow, `Change:` bold blue, `UX:` bold magenta, `Note:` bold cyan, `Security:` bold red, `Closed:` bold blue; `Tests:` / `Plugin docs:` / `Internal:` / `Observability:` / `E2E:` / `Review fixes:` / `Why:` dim), cyan inline backtick spans (e.g. `kbagent serve --ui`), dim bullets, and a 4-space continuation indent under the bullet on the first line only. Body text is word-wrapped to terminal width via `Text.wrap()` (Rich's span-preserving wrap) so long entries no longer render as one wall of unwrapped text. Renderer-only change in `commands/changelog.py` (~70 lines added); the `CHANGELOG` data dict in `changelog.py` is untouched. JSON envelope (`--json changelog`) is byte-for-byte unchanged: data shape stays 1:1 with prior releases so AI agents that consume `kbagent changelog` as context see zero diff. Two implementation details that mattered: (1) wrap-output `Text` lines keep a trailing word-break space that surfaces as visible trailing whitespace on copy/paste -- fixed by calling `Text.rstrip()` in place on each wrapped line before printing; (2) the gutter `Text` is built styleless and the dim attribute is appended only to the bullet glyph itself, otherwise Rich's parent-style inheritance would dim the whole body line including colored prefixes. Existing regression test `tests/test_auto_update.py::TestChangelogCommandConsumesWhatsNewTrigger` (assertion is content-based, not style-based) continues to pass without modification; full suite 3373 passed.",
],
Expand Down
16 changes: 14 additions & 2 deletions src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,16 +952,24 @@
kbagent doctor [--fix]
Health checks. --fix auto-installs MCP server binary.

kbagent version
kbagent version [--beta]
Version info for kbagent + keboola-mcp-server. Reports both the locally
installed version and the latest available; flags any staleness.
--beta (since 0.42.0) reports the latest pre-release (beta / rc) instead
of the latest stable. Same env override: KBAGENT_INCLUDE_PRERELEASE=1.

kbagent update
kbagent update [--beta]
Two-stage upgrade (since 0.30.1): kbagent itself AND keboola-mcp-server.
The MCP server is detected (uv tool / pip env / uvx) and bumped via the
matching command. Both stages always run, regardless of whether kbagent
itself needed an upgrade. The same flow runs automatically on every
kbagent startup -- the explicit `update` command forces a fresh check.
--beta (since 0.42.0) opts into pre-release versions (PEP 440 betas/rc,
e.g. 0.43.0b1). Without --beta the auto-update path uses GitHub's
/releases/latest endpoint, which excludes prereleases server-side --
stable users never silently land on a beta. Set
KBAGENT_INCLUDE_PRERELEASE=1 in env to make every update in the session
treat betas as installable without re-typing --beta.

kbagent changelog [--limit N]
Show recent changelog (what changed in each version). Default: last 5 versions.
Expand Down Expand Up @@ -1027,6 +1035,10 @@
KBAGENT_AUTO_UPDATE Set to "false" to disable automatic update on startup
KBAGENT_UPDATED_FROM Set to an older version to trigger "What's new" display on next run
KBAGENT_MCP_TRANSPORT MCP transport mode: "http" (default, persistent) or "stdio" (subprocess)
KBAGENT_INCLUDE_PRERELEASE Set to "1" (or "true"/"yes"/"on") to opt into pre-release versions for
`kbagent update` / `kbagent version` in this shell (equivalent to --beta flag,
since 0.43.3). NEVER affects the startup auto-update hook -- that path is
stable-channel-only by design so betas stay an explicit per-invocation choice.

8. Config resolution order:
--config-dir flag > KBAGENT_CONFIG_DIR env > .kbagent/ in CWD/parents > ~/.config/keboola-agent-cli/
Expand Down
50 changes: 46 additions & 4 deletions src/keboola_agent_cli/commands/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,37 @@ def _format_version_panel(console: Console, data: dict) -> None:
console.print(Panel(text, title="Version Info", border_style="blue"))


def version_command(ctx: typer.Context) -> None:
def version_command(
ctx: typer.Context,
beta: bool = typer.Option(
False,
"--beta",
help="Report the latest pre-release (beta / rc) instead of the latest stable.",
),
) -> None:
"""Show kbagent version and check for dependency updates."""
formatter = get_formatter(ctx)
version_service = get_service(ctx, "version_service")
result = version_service.get_versions()
include_prerelease = beta or _env_opted_into_prerelease()
result = version_service.get_versions(include_prerelease=include_prerelease)
formatter.output(result, _format_version_panel)


def update_command(ctx: typer.Context) -> None:
def update_command(
ctx: typer.Context,
beta: bool = typer.Option(
False,
"--beta",
help=(
"Opt into pre-release versions (beta / rc). Without this flag (the "
"default) and without KBAGENT_INCLUDE_PRERELEASE=1 in env, the "
"GitHub /releases/latest endpoint -- which filters out prereleases "
"server-side -- is used, so a beta release will never silently "
"install. With --beta, the resolver opts into PEP 440 prereleases "
"(``uv --prerelease=allow`` / ``pip --pre``)."
),
),
) -> None:
"""Update kbagent + keboola-mcp-server to the latest versions.

Two-stage upgrade (since v0.30.1):
Expand All @@ -130,10 +152,16 @@ def update_command(ctx: typer.Context) -> None:
JSON output reports both stages independently. Human mode prints a
one-line summary such as
``kbagent v0.30.0 -> v0.30.1 | keboola-mcp-server v1.49.0 -> v1.59.1``.

Pre-release channel (since v0.42.0): use ``--beta`` (or set
``KBAGENT_INCLUDE_PRERELEASE=1`` in env) to opt into beta / rc
releases. The startup auto-update hook never auto-installs a beta;
only this explicit command does.
"""
formatter = get_formatter(ctx)
version_service = get_service(ctx, "version_service")
result = version_service.self_update()
include_prerelease = beta or _env_opted_into_prerelease()
result = version_service.self_update(include_prerelease=include_prerelease)

if formatter.json_mode:
formatter.output(result)
Expand All @@ -142,3 +170,17 @@ def update_command(ctx: typer.Context) -> None:
formatter.success(result["message"])
else:
formatter.console.print(result["message"])


def _env_opted_into_prerelease() -> bool:
"""Honour ``KBAGENT_INCLUDE_PRERELEASE=1`` as a per-shell opt-in.

Some users want every kbagent invocation in a session to use the beta
channel without re-typing ``--beta`` (e.g. CI smoke-tests for an
upcoming release). Mirrors the truthy parse pattern used by
``KBAGENT_SKIP_UPDATE`` so the two env vars behave consistently.
"""
import os

raw = os.environ.get("KBAGENT_INCLUDE_PRERELEASE", "").strip().lower()
return raw in {"1", "true", "yes", "on"}
Loading
Loading