Gå til hovedinnhold

CLI conventions

Every kapi command answers the same four questions the same way: how it takes input, how it emits output, what its exit code means, and what it does inside a project. These are contracts enforced by tests (cli/conventions_test.go, host/inputs_test.go), not per-command choices — a new command that breaks one fails the build.

1. Input

One resolver, host.App.ResolveInputs (host/inputs.go), backs every command that takes file inputs. Three rules:

Globs expand in-process. kapi stats 'src/**' behaves identically on zsh, bash and PowerShell, and whether or not the shell expanded the pattern first — a shell-expanded list of concrete paths is just the resolved form of the same input. Expansion uses doublestar, so ** is recursive (which filepath.Glob cannot express) and {a,b} alternates. A wildcard never descends into a dot-directory; naming one (.github/**/*.yml) opts back in.

Directories are content, not errors. A directory argument expands to the regular files beneath it, skipping hidden directories and editor junk (~$… Office lock files, ._… AppleDouble stubs). The toolbox utilities that emulate a Unix filter (kgrep, kcat, ksed) keep the POSIX -r requirement, because they are deliberate emulations of tools whose muscle memory includes it.

No input never means "block on the terminal". Given nothing, a content verb uses the project's tracked content set when a recipe is in scope; otherwise it reads standard input only when stdin is actually redirected. On an interactive terminal it reports how to give it input and exits 2. Standard input is always available explicitly as -.

2. Output

One axis, --output-format, with the values text (alias table), json and yaml; --json and --text are shorthands, and --jq <expr> implies JSON. Resolution lives in host/output.ResolveFormat, and every structured result type implements FormatText for the human rendering. text is a table for listings and a labeled summary for single records.

-o is not the format flag. Across the tree -o means an output path or path template (kapi extract -o work.kpz, kapi memory export -o tm.tmx, the {dir}{name}{ext}{lang} template on kapi exec <tool>), and that meaning is consistent, so the format axis keeps the unambiguous long spelling.

Progress for anything that can exceed a second goes to stderr (host/progressstep.go), so stdout stays exactly the machine-readable result: kapi stats --output-format json 'src/**' > out.json writes only JSON. It stays silent below ProgressDelay, is suppressed by --quiet, and degrades to plain append-only lines on a pipe.

3. Exit codes

host/exitcode.go defines the whole vocabulary; no command invents another.

CodeMeaning
0success
1operational error
2usage error — bad flags, unreadable input, no input
3quality gate unmet (ErrQualityGate)
130interrupted (SIGINT)

The grep-family utilities additionally use 1 for "no match", which is their namesakes' contract.

4. Project vs ad-hoc

ResolveProjectPath is the single discovery path: -p/--project flag → KAPI_NO_PROJECT opt-out → KAPI_PROJECT env → git-style upward walk for kapi.yaml. A command is one of:

  • project-required (up, status, ls, add, rm, extract, merge, check --ship) — errors without a recipe;
  • project-preferred (check, stats, inspect, translate, pseudo-translate, run) — uses the project's content when given nothing, works ad-hoc on named files;
  • ad-hoc (formats, tools, flows, plugin, models, config subcommands, the toolbox) — no project involved.

Command surface

CommandInputGlob / dirFormat axisProjectNon-zero exits
uptext·json·yamlrequired1
statustext·json·yamlrequired— (always 0)
check [files…]positionalyestext·json·yamlpreferred3 gate, 1 op
check --shippositionalyestext·json·yamlrequired3 gate
stats [files…]positional, stdinyestext·json·yamlpreferred2 per-file
inspect [files…]positional, stdinyestext·json·yaml (+--jsonl stream)preferred2 per-file
translate [files…]positional, -iyestext·json·yamlpreferred1
pseudo-translate [files…]positional, -iyestext·json·yamlpreferred1
run [flow]-iyestext·json·yamlpreferred1
extractproject content, or positional + -o <kpz>yestext·json·yamlrequired1
merge-i (file, glob, dir)yestext·json·yamlrequired1
apply [changeset]positional or stdintext·json·yamlpreferred3 drift
pack / unpack / infopositional archivetext·json·yamlpreferred / —1
add / ls / rmpositional patternsyestext·json·yamlrequired1
exec <tool> <files…>positionalyestext·json·yamlmemory + terms bound from project1
kcat / kgrep / ksed / kconvpositional, stdinglobs yes, dirs need -rtext·json·yaml2; kgrep 1 = no match
kdiff a [b]1–2 positionaltext·json·yaml1 differ
flows / tools / formatstext·json·yaml1
plugin …positional nametext·json·yaml1; doctor 1 unhealthy
models …positional modeltext·json·yaml1
memory … / terms …positional / --name,--local,--fileimport-dir walkstext·json·yamlresource flags1
brand …positional / --input-text / stdintext·json·yamlprofile flags3 min-score
credentials …positional nametext·json·yaml1
config …positional key/valuetext·json·yamlpositional form: required1
version / update / telemetry / completiontext·json·yaml1
mcp / engine serveprotocol streamsprotocolpreferred1

Deliberate exceptions

Three departures are intentional and documented rather than fixed:

  • kgrep/kcat/ksed require -r for directories. They are explicit emulations of POSIX tools; matching their contract is the point.
  • kgrep exits 1 on no-match. Same reason.
  • kapi status always exits 0. Target-language drift is pending work, not failure, so the reporting command never fails on it. kapi check --ship is the explicit enforcement point — see the convergence model.

One config verb

kapi config is the only configuration command. A plugin does not ship its own: it declares a key namespace in its manifest (capabilities.config_namespaces), and kapi routes kapi config set bowrain.server.url … to that plugin's own global config file with the prefix stripped — the same file and key the plugin already reads. kapi config list spans kapi's own keys and every installed plugin's namespace.

KAPI_CONFIG_DIR contains plugin config files too ($KAPI_CONFIG_DIR/<plugin>/<plugin>.yaml), so the in-repo isolation contract covers namespaced writes.