Skip to main content

AD-013: Kapi CLI

Summary

kapi is a standalone file-processing CLI that demonstrates the neokapi framework. Most commands are one-shot and require no project; the -p flag enables project mode with a .kapi recipe. Kapi builds on the shared CLI base (cli/), stores config under ~/.config/kapi/, and uses an OS-keychain credential store. A kapi mcp subcommand exposes tools over stdio JSON-RPC for AI agents.

Context

The framework needs a first-class CLI for three audiences:

  1. Engineers running ad-hoc file processing — "translate this JSON bundle to French and write the output." No project, no state, one command.
  2. Teams running reproducible workflows — a saved .kapi recipe with flows, plugin pinning, language targets, and defaults. Shared via git.
  3. AI agents invoking tools programmatically — structured discovery and typed input/output over MCP.

These audiences overlap in capability (all use formats, tools, flows) but differ in invocation style. A single binary with progressive complexity covers all three: run a tool directly, run a flow from a recipe, or expose the same tools over MCP.

A platform plugin's CLI shares much of the same surface (formats, tools, flows, plugins, presets) but is project-sync-centric. The common surface lives in a shared CLI base; each CLI selects which commands to register and adds its own behavior.

Decision

Binary and module layout

kapi is a Go binary at kapi/cmd/kapi/, part of the kapi module. It depends on the framework and the shared CLI base (cli/). It has no dependency on any vendor-plugin code; vendor plugins (such as okapi-bridge) are discovered at runtime via their manifests rather than compiled in.

kapi/
├── go.mod # module github.com/neokapi/neokapi/kapi
├── cmd/kapi/ # thin root cmd wiring shared CLI commands
└── preset/ # built-in preset definitions

The shared CLI base provides command factories; kapi registers them and optionally extends with CLI-specific behavior.

Command surface

Kapi uses Cobra for hierarchical subcommands:

kapi
├── <tool> # run a tool directly (pseudo-translate, ai-translate, …)
├── run FLOW # execute a composed flow
├── extract # emit a bilingual file for a translator/reviewer — native .klz or XLIFF/PO (AD-017)
├── merge # apply a translator's returned bilingual file (AD-017)
├── init # scaffold a new .kapi project
├── add # add file patterns to the project's content
├── rm # remove a content pattern, or exclude matching files
├── ls # list tracked files (--stats for block/word counts)
├── tools # list available tools
├── flows # list available flows
├── formats
│ └── list # list available formats (built-in + plugin)
├── plugin # (alias: plugins)
│ ├── list # list installed plugins
│ ├── install # install a plugin
│ └── update # update a plugin
├── presets
│ └── list # list presets
├── termbase # terminology management
│ ├── list
│ ├── lookup
│ └── stats
├── tm # translation memory management
│ ├── list
│ ├── import
│ ├── export
│ ├── lookup
│ ├── search
│ ├── stats # TU counts, locale breakdown, provenance (AD-017)
│ └── audit # trace a merge batch's TM impact (AD-017)
├── version # version info (set via ldflags)
└── mcp # MCP server for AI agent integration

Commands fall into categories:

  • Format operationskapi formats, kapi extract, kapi merge
  • Toolskapi pseudo-translate, kapi word-count, kapi ai-translate; MT tools register per provider as kapi <provider>-translate (e.g. kapi deepl-translate); kapi run <flow>
  • Pluginskapi plugin list/install/update
  • Presetskapi presets list
  • Terminology and TMkapi termbase, kapi tm

Tools run as top-level commands (kapi pseudo-translate); composed multi-tool flows run under kapi run (kapi run ai-translate-qa). Both single tools and composed flows appear in kapi flows and kapi tools listings depending on where they were defined.

One-shot and project modes

Most commands are one-shot by default:

kapi ai-translate file.xliff --target-lang fr
kapi pseudo-translate file.json
kapi word-count file.json

On project-aware commands, the -p / --project flag switches into project mode, loading a .kapi recipe (AD-008: Kapi Project Model) for defaults:

kapi run translate -p myproject.kapi
kapi run ai-translate-qa --project myproject.kapi --target-lang de
kapi extract --project myproject.kapi

With --project:

  • The flow name is looked up in the project's flows map.
  • sourceLocale and targetLocales[0] provide defaults (CLI flags override).
  • Project defaults (concurrency, parallel_blocks, encoding) apply unless overridden.
  • Plugin scoping from AllowedSources narrows format detection to the project's declared plugins.

Every project-aware command resolves -p in this order, matching the git-style semantics a localization engineer expects when running commands from inside a project tree:

  1. Explicit -p <path> flag.
  2. KAPI_PROJECT env var (CI escape hatch).
  3. project.ResolveLayout(cwd) — walk upward for the {name}.kapi recipe + adjacent .kapi/ state directory.
  4. Fall through to one-shot mode (for commands that support it) or return a "not a kapi project" error (for commands that require one — e.g. kapi merge).

ErrAmbiguousLayout (multiple *.kapi files in the same directory) surfaces as a CLI error asking for an explicit --project. The resolution helper (AddProjectFlag / ResolveProjectPath) lives in cli/project.go once and is reused by the project-aware commands — run, extract, merge, brand, and verify — plus any future project-aware command.

-p means --project only on project-aware commands

The -p shorthand binds to --project only on the project-aware commands listed above (run, extract, merge, brand, verify), where AddProjectFlag registers it. On ad-hoc tool commands (such as kapi ai-translate or kapi pseudo-translate), there is no --project flag — the same -p shorthand is already taken by --progress (the progress-bar flag). So kapi ai-translate -p my.kapi is parsed as --progress with my.kapi left as a positional argument, not as a load-project request. Tool commands pick up project context (TM, glossary, defaults) through git-style discovery instead — run them from inside the project tree, or point KAPI_PROJECT at the recipe.

Output format flags

All commands that produce output support consistent format flags through the shared cli/output/ package:

kapi tm stats --json # machine-readable JSON
kapi tm stats --text # human-readable (default)
kapi tm stats --output-format=json # explicit long form

Flag precedence:

  1. --json — highest precedence (shorthand for common case).
  2. --text — explicit text.
  3. --output-format=<fmt> — explicit selection.
  4. Default: text.

Supported formats: text (tables, formatted text, colors when the terminal supports them) and json (single JSON object or array per command).

Each command defines its JSON structure as a concrete Go type with json struct tags. Structures must be:

  • Consistent — the same command always produces the same structure.
  • Complete — all relevant data, not just what text mode displays.
  • Typed — correct JSON types (numbers, booleans, arrays; never map[string]any).

Types implement a TextFormatter interface for human-readable output:

type TextFormatter interface {
FormatText(w io.Writer) error
}

Commands without a TextFormatter fall back to formatted JSON.

Exit codes

CodeMeaning
0Success (ExitOK)
1Operational error (ExitError) — the default for a failed command
2Usage / toolbox trouble (ExitUsage) — e.g. kgrep/ksed/kcat on a bad pattern or unreadable file (grep convention)
3Quality/brand gate failed (ExitGate) — distinct from an error, so CI and skills can tell a sub-threshold gate from a crash
130Interrupted (ExitSignal) — 128 + SIGINT

Commands map errors to codes through cli.ExitCode; a command can request a specific code by returning an error tagged with cli.WithExitCode (the toolbox utilities use it for the grep-style 2), and ErrQualityGate maps to 3.

Exit codes are consistent across formats. In JSON mode, errors are structured objects:

{
"error": "failed to connect to server",
"code": "connection_failed"
}

Credential store

The credential store lives in cli/credentials/ and is shared by kapi and kapi-desktop. Non-secret provider configs live as JSON at ~/.config/kapi/providers.json; API keys are stored in the OS keychain under the service name "kapi".

Platform backends:

  • macOS — Keychain via /usr/bin/security.
  • Windows — Credential Manager via Wincred API.
  • Linuxlibsecret (GNOME Keyring, KWallet) via github.com/zalando/go-keyring.

A file-based fallback encrypts secrets with a machine-derived key for environments without an OS keychain (containers, headless CI).

App configuration

App configuration uses Viper:

~/.config/kapi/
├── kapi.yaml # global settings
├── providers.json # AI/MT provider configs (keys in keychain)
└── plugins/ # installed plugins

kapi.yaml holds defaults for output format, logging level, plugin directory, telemetry opt-in, and default provider. CLI flags override Viper values; environment variables (prefix KAPI_) override file values.

MCP server

The kapi mcp subcommand starts an MCP (Model Context Protocol) server over stdio JSON-RPC, exposing kapi's file-processing capabilities to AI agents (Claude Desktop, Cursor, VS Code, etc.).

Tools exposed:

ToolDescription
list_formatsList supported file formats
detect_formatDetect format from file path
extract_contentParse file, return translatable blocks
word_countCount translatable words
run_flowExecute a processing flow on a file
pseudo_translatePseudo-translate a file for QA
list_flowsList available processing flows
list_toolsList available processing tools

Each tool has typed Input and Output structs with json and jsonschema struct tags. The MCP SDK generates JSON Schema from these types automatically:

type ExtractContentInput struct {
Path string `json:"path" jsonschema:"File path to extract from"`
Format string `json:"format,omitempty" jsonschema:"Override format detection"`
SourceLang string `json:"source_lang,omitempty" jsonschema:"Source language (default: en)"`
}

Wiring pattern:

server := mcp.NewServer(&mcp.Implementation{Name: "kapi", Version: version.Version}, nil)
registerKapiTools(server, app)
return server.Run(cmd.Context(), &mcp.StdioTransport{})

PersistentPreRun initializes app.FormatReg and app.PluginHost before the MCP server starts. Stdout belongs to the MCP transport; Init only writes to stderr.

Client configuration (Claude Desktop):

{
"mcpServers": {
"kapi": {
"command": "kapi",
"args": ["mcp"]
}
}
}

MCP tools reuse the same infrastructure as CLI commands — FormatRegistry for format detection and reader/writer creation, Executor for pipeline orchestration, built-in tool constructors for flow chains. No parallel implementation.

Consequences

  • A single binary handles one-shot processing, project-based workflows, and AI-agent integration without feature flags or separate builds. The shared CLI base contributes the common commands for formats, tools, flows, plugins, presets, termbase, and version.
  • Output format consistency makes jq, yq, and language-specific parsers work uniformly across commands.
  • OS keychain storage keeps API keys out of environment variables, shell history, and project files.
  • The MCP server adds AI-agent support with ~5 lines of wiring; no shared MCP abstraction is needed across CLIs.
  • The absence of a project dependency in one-shot mode keeps kapi usable in CI pipelines with no state beyond the input file.
  • Exit-code stability means shell scripts can reliably distinguish success, failure, and conflict without parsing output.