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.82.0",
"version": "0.83.0",
"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
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.82.0",
"version": "0.83.0",
"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
3 changes: 3 additions & 0 deletions plugins/kbagent/agents/keboola-expert.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ One-line triggers only. Full prose, exact error strings, issue numbers, and
API quirks live in [`gotchas.md`](../skills/kbagent/references/gotchas.md) --
read it when a trigger fires. Each `(X.Y.Z+)` tag is the version floor.

**Migrating an `mcp_tool` agent task**
- No migration command exists; you do the argv mapping. Recipe in gotchas.md. (0.83.0+)

**Upgrading kbagent itself**
- `install_channel` in `kbagent --json version` => native binary; `kbagent
update` REFUSES by design. Quote `upgrade_command` (choco/winget/brew/apt/dnf);
Expand Down
29 changes: 27 additions & 2 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,39 @@ Versioning convention:
warns. Migrate to `--type cli_command` with the native command BEFORE
v0.85.0. These tasks are persisted in `<config_dir>/agents.json`, so
unlike an interactive `tool call` they get no warning at removal time: a
scheduled task simply starts failing on its next cron tick. Use the
`cli_equivalent` from `kbagent tool list` to rewrite each one.
scheduled task simply starts failing on its next cron tick.
- **There is NO migration command, by decision.** The parity map knows which
native command replaces a tool, but not how to map arguments: `mcp_tool`
params are the MCP tool's own `input` dict (`componentId`) while the CLI
takes flags (`--component-id`), and for several tools the shape differs too.
Guessing the argv of a scheduled WRITE task that then runs unattended is the
worst place to be wrong, so kbagent detects these tasks and leaves the
rewrite to you -- or to an AI agent, which can read `--help` and the task's
own input and does not have to guess.
- **Find the affected tasks (since v0.81.0)**: `kbagent doctor` has an
`mcp_tool_tasks` check that warns with the task ids, the tool each one
calls and its native replacement (`details.tasks[]` in `--json`, each with
`native_command`). `kbagent agent list` marks those rows DEPRECATED and its
`--json` adds an additive per-task `deprecation` key -- present ONLY on
affected tasks, so every other consumer sees a byte-identical payload.
- **Recipe for migrating one task** (this is the whole procedure):
1. `kbagent --json doctor` -> `checks[].details.tasks[]` for the affected
task ids and the `native_command` each one maps to.
2. Read that task's `action.params.input` from `<config_dir>/agents.json`
(or `kbagent --json agent show <id>`) -- those are the values to carry over.
3. `kbagent <native_command> --help` -> map each input key to its flag. This
is the step no tooling does for you; the naming differs per tool.
4. `kbagent agent create --name ... --cron ... --type cli_command --argv ...`
with the result, verify it once with `kbagent agent run <new-id>`, then
`kbagent agent delete <old-id>`.
- **`agent update` CANNOT change a task's action** -- it patches name, cron,
enabled/manual and the trigger only, so `--type` / `--argv` are not options
there and exit 2. Migration therefore means create-new + delete-old, which
has one trap: **the task id changes**. `Trigger.task_id` chains tasks by id,
so if any OTHER task has `--trigger-task-id <old-id>`, repoint it with
`kbagent agent update <that-task> --trigger-task-id <new-id>` BEFORE deleting
the old one, or the chain breaks silently. Check with
`kbagent --json agent list` and grep the `trigger` blocks for the old id.

## MCP tool classification is FAIL-CLOSED; parity commands replace `tool call` (since v0.73.0)

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-cli"
version = "0.82.0"
version = "0.83.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
18 changes: 18 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.83.0": [
"Note (#390): there will be NO `kbagent agent migrate-mcp-tasks` command. Migrating "
"a scheduled `--type mcp_tool` task to `--type cli_command` before v0.85.0 is a "
"manual step -- or an AI-assisted one, which is the point: the parity map knows "
"which native command replaces each tool, but not how to map arguments (`mcp_tool` "
"params are the MCP tool's own `input` dict, e.g. `componentId`, while the CLI takes "
"`--component-id`, and for several tools the shape differs too). Guessing the argv "
"of a scheduled WRITE task that then runs unattended is the worst place to be wrong, "
"and an agent reading `--help` alongside the task's own input does it better than "
"any generator we could ship. kbagent therefore finds the tasks and leaves the "
"rewrite to you. The full four-step recipe is in the skill's `gotchas.md`; the short "
"version is `kbagent --json doctor` for the task ids and their `native_command`, "
"`kbagent <cmd> --help` for the flag names, then `kbagent agent create --type "
"cli_command --argv ...`, one `kbagent agent run` to verify, and `agent delete` on "
"the old task. Note `agent update` cannot change a task's action -- migration is "
"create-new + delete-old, so the task ID changes: if another task chains to it via "
"`--trigger-task-id`, repoint that first or the chain breaks silently.",
],
"0.82.0": [
"Fix: kbagent is licensed under **Apache 2.0**, and the packaging metadata now says "
"so. #544 added an Apache 2.0 `LICENSE` file, but `pyproject.toml`, the deb/rpm "
Expand Down
67 changes: 67 additions & 0 deletions tests/test_mcp_migration_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""The documented migration recipe must name flags the CLI actually has.

#581 shipped a four-step recipe whose last step told the reader to run
`kbagent agent update --type cli_command --argv ...`. That command patches
name/cron/enabled/trigger only -- it explicitly cannot change a task's action --
so the documented path exited 2. A recipe that fails at its last step is worse
than no recipe, and nothing was checking that the docs matched the CLI.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import typer.main

from keboola_agent_cli.cli import app

GOTCHAS = Path(__file__).parent.parent / "plugins/kbagent/skills/kbagent/references/gotchas.md"


def _options_of(*command: str) -> set[str]:
"""Every option name a command declares, read from the command object.

Deliberately NOT scraped from ``--help``: rendered help is a presentation
concern -- Rich wraps and truncates it by terminal width and TTY detection,
so an option can be present in the interface and absent from the text. The
parameter list is the actual contract. An earlier version of this helper
scraped the help, passed locally, and failed on all three CI runners.
"""
node: Any = typer.main.get_command(app)
for name in command:
# Duck-typed rather than isinstance(click.Group): Typer builds its own
# Command/Group subclasses, so a click.Group check is False at runtime.
subcommands = getattr(node, "commands", None)
assert isinstance(subcommands, dict), f"{name!r} is not under a command group"
assert name in subcommands, f"no such command: {' '.join(command)}"
node = subcommands[name]
return {opt for param in node.params for opt in param.opts if opt.startswith("--")}


def test_agent_update_still_cannot_change_the_action() -> None:
"""Anchor for the recipe below; if this ever changes, simplify the docs."""
options = _options_of("agent", "update")
assert "--type" not in options
assert "--argv" not in options


def test_recipe_does_not_tell_agent_update_to_change_the_action() -> None:
text = GOTCHAS.read_text(encoding="utf-8")
assert "agent update <id> --type cli_command" not in text


def test_recipe_uses_flags_agent_create_really_has() -> None:
options = _options_of("agent", "create")
for flag in ("--type", "--argv", "--name", "--cron"):
assert flag in options, f"the migration recipe names {flag}, which agent create lacks"


def test_recipe_warns_about_the_changing_task_id() -> None:
"""Recreating breaks any task chained to the old id via Trigger.task_id."""
text = GOTCHAS.read_text(encoding="utf-8")
section = text.split("Recipe for migrating one task", 1)
assert len(section) == 2, "migration recipe section is gone"
body = section[1][:1600]
assert "--trigger-task-id" in body
assert "the task id changes" in body.lower()
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.