diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7afe7b35..b7fbedc1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" diff --git a/plugins/kbagent/.claude-plugin/plugin.json b/plugins/kbagent/.claude-plugin/plugin.json index 2f400caa..bafaef42 100644 --- a/plugins/kbagent/.claude-plugin/plugin.json +++ b/plugins/kbagent/.claude-plugin/plugin.json @@ -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", diff --git a/plugins/kbagent/agents/keboola-expert.md b/plugins/kbagent/agents/keboola-expert.md index 7289be8a..c397ef5d 100644 --- a/plugins/kbagent/agents/keboola-expert.md +++ b/plugins/kbagent/agents/keboola-expert.md @@ -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); diff --git a/plugins/kbagent/skills/kbagent/references/gotchas.md b/plugins/kbagent/skills/kbagent/references/gotchas.md index 2f215c84..aecb33be 100644 --- a/plugins/kbagent/skills/kbagent/references/gotchas.md +++ b/plugins/kbagent/skills/kbagent/references/gotchas.md @@ -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 `/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 `/agents.json` + (or `kbagent --json agent show `) -- those are the values to carry over. + 3. `kbagent --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 `, then + `kbagent agent delete `. +- **`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 `, repoint it with + `kbagent agent update --trigger-task-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) diff --git a/pyproject.toml b/pyproject.toml index 3ef971df..063cfc9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/keboola_agent_cli/changelog.py b/src/keboola_agent_cli/changelog.py index 785eb7fc..a233146d 100644 --- a/src/keboola_agent_cli/changelog.py +++ b/src/keboola_agent_cli/changelog.py @@ -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 --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 " diff --git a/tests/test_mcp_migration_recipe.py b/tests/test_mcp_migration_recipe.py new file mode 100644 index 00000000..c25786a8 --- /dev/null +++ b/tests/test_mcp_migration_recipe.py @@ -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 --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() diff --git a/uv.lock b/uv.lock index 0eb9138e..bd7a4c0e 100644 --- a/uv.lock +++ b/uv.lock @@ -590,7 +590,7 @@ wheels = [ [[package]] name = "keboola-cli" -version = "0.82.0" +version = "0.83.0" source = { editable = "." } dependencies = [ { name = "croniter" },