Skip to content

Wrapping a click CLI

If you already have a click CLI, you do not need to rewrite it:

import knaif.cli as nk
from myapp.cli import cli # your existing click.Group
nk.from_click(cli).run()

Each sub-command becomes a tool. Your command functions are untouched — knaif derives the tool description from the docstring, the argument schema from your click types, and the required/optional split from whether a parameter has a default.

Terminal window
$ python app.py "convert 2026-06-20T15:00 to Tokyo"
# calls: convert("2026-06-20T15:00", to_tz="Asia/Tokyo")

click.Choice([...]) maps to an enum schema, so choices you already declared become constraints the planner enforces for free.

from_click handles a flat click.Group with typed options and arguments. Everything below is a genuine gap, not a rough edge — check this table before assuming your CLI wraps cleanly.

click feature Status
Nested click.Group inside a group Not supported. Flatten to one group, or use @nk.command.
nargs=-1 / variadic arguments Not mapped. Declare the argument as array with @nk.command instead.
pass_context / click.Context params Ignored. Context objects cannot appear in a model-generated plan. Use nk.Ctx.
click.File / click.Path Mapped as string. Add path_role via @nk.command for sandbox validation.
Callback-based validation Not preserved. Move it into the function body, or express it declaratively as enum / min / max.
click.Choice(case_sensitive=False) Choices reach the schema, but case-insensitivity is not enforced at plan time.
Stacked command decorators Only the last-registered click.Group is walked.

from_click is not all-or-nothing. Where a command does not map — a variadic argument, a nested group — reimplement just that one with @nk.command and pass it alongside.

The natural-language path and your existing CLI stay independent, so an unmapped command is still reachable the normal way; it simply is not something a user can ask for in a sentence yet.