Metadata-Version: 2.5
Name: kater-cli
Version: 0.2.271
Summary: Kater's resource-first command-line interface
Project-URL: Homepage, https://kater.ai
Project-URL: Documentation, https://landing.kater.ai/docs/cli-reference/install/
Requires-Python: >=3.14
Requires-Dist: aiohttp>=3.13
Requires-Dist: anyio>=4.0
Requires-Dist: asyncpg>=0.30
Requires-Dist: croniter<7,>=6
Requires-Dist: duckdb==1.5.5
Requires-Dist: email-validator>=2.3.0
Requires-Dist: fastmcp==3.4.4
Requires-Dist: filelock<3.26,>=3.20
Requires-Dist: httpx<1,>=0.28
Requires-Dist: jsonschema>=4.26.0
Requires-Dist: keyring<26,>=25.6
Requires-Dist: lsprotocol==2025.0.0
Requires-Dist: openai>=2.46.0
Requires-Dist: opentelemetry-api==1.44.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.44.0
Requires-Dist: opentelemetry-instrumentation-fastapi==0.65b0
Requires-Dist: opentelemetry-instrumentation-httpx==0.65b0
Requires-Dist: opentelemetry-instrumentation-sqlalchemy==0.65b0
Requires-Dist: opentelemetry-instrumentation-system-metrics==0.65b0
Requires-Dist: opentelemetry-sdk==1.44.0
Requires-Dist: polars>=1.37.1
Requires-Dist: psutil>=7.2.2
Requires-Dist: pyarrow==25.0.1
Requires-Dist: pydantic-settings>=2.11
Requires-Dist: pydantic<3,>=2.12
Requires-Dist: pydantic>=2.13
Requires-Dist: pydantic>=2.13.4
Requires-Dist: pygit2>=1.20.1
Requires-Dist: pygls==2.1.1
Requires-Dist: pyroscope-io==1.2.5; sys_platform != 'win32'
Requires-Dist: python-dotenv>=1.2
Requires-Dist: pyyaml<7,>=6.0.3
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: questionary>=2.1
Requires-Dist: rich>=14
Requires-Dist: snowflake-connector-python[secure-local-storage]>=4.7.1
Requires-Dist: sqlalchemy[asyncio]>=2.0.44
Requires-Dist: sqlglot>=30.17.0
Requires-Dist: textual<9,>=8.2.8
Requires-Dist: typer==0.27.2
Requires-Dist: watchfiles>=1.2.0
Description-Content-Type: text/markdown

# Kater CLI

`kater` works the way dbt does: a project file, a profile with targets, `--select` and `--exclude`, one line as each object starts and ends, `target/run_results.json`, and dbt's exit codes. Every warehouse call goes through your Kater API, which signs in with the organisation's stored credential, so your computer never holds one (ADR 0042, ADR 0043).

## Quick start

### 1. Install

On macOS or Linux:

```sh
curl -fsSL https://landing.kater.ai/install.sh | sh
```

The script installs uv, Python 3.14 and the `kater-cli` package from PyPI in `~/.kater`, links `kater` in `~/.local/bin`, and puts that folder on your PATH in your shell's profile when it is not there. With Python 3.14 on your computer, `uv tool install --python 3.14 kater-cli` installs the same package.

In a checkout of the Kater repository, from its root:

```sh
uv sync --package kater-cli
uv run --package kater-cli kater --version
```

The Release publishes kater-cli to PyPI as one wheel, which carries each workspace project it needs, when prod runs the same commit (`scripts/build_cli_packages.py`). PyPI has only the project kater-cli.

`kater --help` lists the commands you use each day. `kater help --all` lists every other command.

### 2. Set up with `kater init`

An admin who sets up a new organisation runs `kater init`. It signs you in, connects the warehouse, syncs its tables, runs a first query, and writes a profile with a target, step by step (see Onboarding below).

To work in an organisation that is already set up, sign in and make a local copy of its content:

```sh
kater login
kater git clone kater
cd kater
```

The local copy holds `kater_project.yml`, which marks a project and names its profile. The profile is in `~/.kater/profiles.yml` (see Configuration below). A Kater engineer runs `pnpm dev:setup`, which writes both for the local stack.

### 3. Check the setup with `kater debug`

```sh
kater debug
```

```
15:19:40  Running with kater=0.1.0, profile=kater, target=dev (Kater API at http://127.0.0.1:8788)
15:19:40  Profile kater in ~/.kater/profiles.yml, target dev (Kater API at http://127.0.0.1:8788) ... [OK]
15:19:40  Project kater_project.yml at ~/kater ...................................... [OK]
15:19:41  Kater API 0.1.0 at http://127.0.0.1:8788 .................................. [OK]
15:19:41  Signed in as yvonne@kater.ai .............................................. [OK]
15:19:43  Warehouse snowflake_prod_connection (select 1) ............................ [OK]
15:19:43  All checks passed!
```

Each line that is not `[OK]` says what to do next. `kater debug` exits 1 when a check fails.

### 4. Build with `kater build`

```sh
kater build --select +connections/snowflake_prod_connection/workspaces/ad_reporting/query_contracts/spend_by_platform.sql
```

It compiles, runs and tests what you select and everything it reads, in the graph's order, as `dbt build` does: a model, a query, then each test of the model. What reads an object that failed is SKIP. It ends with `Done. PASS=… WARN=… ERROR=… SKIP=… TOTAL=…` and writes `target/run_results.json`. `kater retry` runs again only what failed or was skipped.

### The commands you use each day

| Command | What it does |
| --- | --- |
| `kater init`, `kater debug`, `kater login`, `kater logout` | Set up, check the setup, sign in and out. |
| `kater build`, `kater run`, `kater compile`, `kater test`, `kater retry` | Build, run, compile or test what `--select` selects, and run again what failed. |
| `kater show --select ONE --limit N` | Show a model's first rows, as `dbt show` does. |
| `kater ls --select …`, `kater parse` | List what a selection selects, one `TYPE.NAME` a line, and write `target/manifest.json`. |
| `kater new KIND` | Make a workspace, query, chart, app, field, test or model. |
| `kater compare`, `kater sync` | Compare two queries or an app's figures for two weeks, and update the datasets from the warehouse. |
| `kater git`, `kater admin` | Work on your local copy, and administer members, access and connections. |

Every command that works on objects selects them with dbt's grammar: `--select "one.sql two.sql"`, `--select tag:team=finance,resource_type:query`, `--select +semantic.example`, and `--exclude` to leave nodes out. A term that matches nothing prints a warning with the nearest name ("Warning: --select spend_by_platfrom matches no object. Did you mean spend_by_platform?"), as dbt warns, and `--warn-error` turns it into exit 1. The `--help` of each command of every day ends with examples. Other collection options take one value for each flag: `--sources one --sources two`. Common options (`--json`, `--kater-root`, `--scope`, `--target`) may come before the command or after it.

Three workflows, step by step with recordings, are in the docs: [check a query](../landing-page/src/docs/data/docs/en/developers/cli-check-a-query.mdx) (`debug`, `ls`, `show`), [change a query and build it](../landing-page/src/docs/data/docs/en/developers/cli-change-and-build.mdx) (`state:modified+`, `build`, `retry`, `kater git push`) and [test a shared model](../landing-page/src/docs/data/docs/en/developers/cli-test-a-model.mdx) (`test list`, `test`, `--update`, `new test --record`). `apps/landing-page/scripts/docs/recordings/record.mjs` records them again from real commands.

## How a command prints

While a command works, it prints its progress on stdout as events with the time, as dbt does (`kater_cli.events`). A wait says what it waits for after 1 s and again every 10 s, and has a limit (`kater_cli.waiting`); reading a large project before the first object is one too ("Waiting for Kater to read the project. 1s"). `--json` leaves only the answer on stdout, and `--log-format json` prints each event as one JSON line. Every event also goes to `logs/kater.log` at the content root, or `~/.kater/logs/kater.log` outside one. An error prints what Kater could not do, the error's words, a `Next:` line, and the request ID with where the log is (`kater_cli.failures`). An unexpected error writes its record and traceback to the log only. With `--json` an error is the `OperationError` JSON on stderr.

Exit codes follow dbt: 0 for success, warnings included unless `--warn-error`; 1 when an object or an operation fails; 2 when the command cannot run (a bad flag, profile or project, a Kater API that does not answer, an unexpected error); 130 when you stop it. A step of `kater new field` that waits for input exits 0. Help and version do not open a service or resolve credentials.

The text is for people, and it changes freely. A program reads one of three contracts (ADR 0043): `--json`, one JSON document on stdout and the error JSON as the last stderr line; `kater worker`'s JSON lines, which the VS Code extension sends its operations and command lines to; or `kater serve`'s stdout. A program in its own process calls `kater_cli.calls.run(words, service=...)`, which answers the result, the error, the exit code and the events as objects, or opens the operations with `kater_cli.service.open_service(root)`. Structured request commands accept `--request-file PATH`; `-` reads stdin.

## API login

Model commands still run locally, and a compile works offline. They act as the person who is signed in to the Kater API that the local copy names (ADR 0042). A permission check and a warehouse call go to that Kater API, and with no sign-in they stop with "Run `kater login`". This is separate from inbound PropelAuth protection on `kater serve --mode remote`.

```sh
kater login
kater auth status
kater logout
kater debug
```

The CLI keeps one token for each Kater API (ADR 0042). Every command, `kater login` and `kater logout` included, works on the Kater API of the profile's target (`kater_cli.profiles`, see Configuration below), and every target that names the same API shares its token. In a local copy, `.git/kater.json` must name the same API as the target. The target's `auth_url`, `client_id`, `redirect_uri` and `site_url` come first. Without them, the first time the CLI signs in to a Kater API, it gets the Auth URL and the client ID from that API's public `GET /v1/sign-in-config` (the API's `PROPELAUTH_AUTH_URL` and `KATER_CLI_CLIENT_ID`), accepts only an HTTPS Auth URL or a loopback one for a loopback API, and saves them for that API in `sign-ins.json` in the configuration directory. Kater's own API keeps the built-in values.

Find the Client ID in PropelAuth's **Frontend Integration → Advanced Settings → Edit OAuth Config**, in the same environment as the portal's Auth URL. Add the exact redirect URI `http://127.0.0.1:8765/callback` (override with the target's `redirect_uri`). These are the OAuth2 settings used by `/propelauth/oauth/authorize`, not the separate MCP OAuth Clients or Request Validation credentials. Login uses authorization code with S256 PKCE and state, a loopback-only callback with a three-minute wait, and no client secret. The API must accept that client's access tokens and expose authenticated `GET /v1/me` returning a nonempty `user_id`. Login saves credentials only after this check succeeds. The API origin defaults to Kater's production value (`kater_cli.config`: `https://api.kater.ai`, with `https://auth.kater.ai` and Kater's public client); the target's own values replace each one. See [PropelAuth's OAuth2 configuration guide](https://docs.propelauth.com/overview/authentication/oauth2#oauth2-configuration) and [PKCE token reference](https://docs.propelauth.com/reference/api/oauth2#token-endpoint).

Credentials live only in an allowlisted OS keychain backend: macOS Keychain, Windows Credential Locker, Linux Secret Service or KWallet. The backend must actually be available/unlocked; headless Linux often has none. No plaintext or plugin fallback is permitted. Credentials are isolated by configuration directory, API origin, auth issuer and client ID. Logout deletes the selected target's saved credentials, retaining non-secret configuration and other targets' credentials; it does not revoke the provider session.

For automation, give the target a `token`, as `"{{ env_var('KATER_CI_TOKEN') }}"`, and supply the variable through your environment/secret manager. This token overrides saved credentials, bypasses keychain access and refresh, and is never persisted. An invalid override fails rather than falling back to a saved login. Status verifies `/v1/me`, reports the target, user ID and credential source, and distinguishes authentication rejection from API unavailability. Logout cannot remove a profile token and reports when that override remains active (`profile_token_override`).

The reusable `kater_cli.client.api_session()` yields an authenticated client whose `request(method, "/v1/...", TypeAdapter(...), body=...)` validates API responses. Stored sessions refresh before expiry; a GET rejected with 401 may refresh and retry once. Mutations are never automatically replayed. Requests do not follow redirects or use ambient proxy settings. API origins require HTTPS except literal loopback development hosts; auth issuers always require HTTPS. Local runtime identity variables never grant API access.

## Set up Slack

```sh
kater admin slack setup
kater admin slack setup --target prod --no-browser
kater admin slack setup --guide
```

`setup` uses your API sign-in and requires `settings:manage`. It names missing server settings and prints the operator's setup guide, exiting with status 1 until the server is configured. Once configured, it prints and opens the server's Slack tab. Install and Update permissions stay in the portal's browser-bound OAuth flow; the CLI never takes an OAuth state or installation cookie into another browser. `--no-browser` prints the page only. `--guide` prints the steps offline, without authentication or an API call. It works on the target's Kater API, and the portal's copyable agent prompt names its own API for `kater debug` to check.

## Send an answer in Slack

```sh
kater send --to YOUR_KATER_EMAIL --answer RESULT_ID
kater send --to YOUR_KATER_EMAIL --path connections/CONNECTION/workspaces/WORKSPACE/query_contracts/QUERY.sql --note "Here are the figures."
```

Send accepts exactly one result id or saved query path. It uses your Kater API sign-in, and the Kater API runs a path fresh under your access. A standalone command sends only to yourself. An agent sends to another member through `kater_apply`, after approval. Kater checks the recipient's access before posting. A queued reply includes a job id; use `kater invoke message.status --request-file PATH` with `{"job":"JOB_ID"}` to read it before sending again. The sender can send 20 DMs in a rolling hour. See the [Slack delivery guide](../api/src/kater_api/slack/README.md).

## Local copy and write-back

`kater git` keeps a local copy of Kater's content store: an ordinary git repository holding only what you can read (see [ADR 0013](../../docs/adr/0013-kater-owns-content-github-write-back-only.md)). `kater git clone [DIR]` commits a snapshot onto the hidden `refs/kater/upstream` ref and checks out `main` at it; `.git/kater.json` records the API target and the store head the snapshot came from. `kater git pull` commits a new snapshot and runs a real `git merge refs/kater/upstream`, so conflicts are ordinary git conflicts. `kater git push [-m MESSAGE]` publishes the committed diff `refs/kater/upstream..HEAD` as one all-or-nothing change, each file carrying its snapshot blob as the expected revision; uncommitted changes are never published, and a push refuses during an unfinished merge. If anything changed in Kater meanwhile, nothing is published: the command pulls, you resolve and commit, and you push again. A successful push records the pushed tree as the new snapshot. A pull deletes a file only when an earlier snapshot sent it and the new one does not; Kater never names a file that you cannot read. A copy that `pnpm dev:setup` recorded (`"imported": true` in `.git/kater.json`) starts from the store's whole tree, so its first pull lists in `.git/kater-withheld` each file that Kater does not send, keeps those files as your branch has them on every pull, and a push leaves out each change to them and says how many. Every other subcommand, such as `kater git status` or `kater git log --oneline`, runs `git` unchanged.

```sh
kater git clone kater
cd kater && kater git pull
git commit -am "Rename revenue measure" && kater git push
```

`kater admin write-back status|connect|disconnect|run` manages the optional, admin-only write-back of the shared layer to a GitHub repository, as one pull request into main. `connect` authorizes Kater's GitHub App in your browser, then asks for the installation, repository, folder (default `kater/`) and schedule (after each publish, or a UTC cron expression); `--installation-id`, `--repository-id`, `--path-prefix`, `--schedule` and `--cron` answer those questions up front. `kater init` offers the same flow as its last, optional step, and `kater init --reset` clears onboarding progress at any time.

## Connect a warehouse

With a Kater API configured, `kater admin connection create` sends a Snowflake connection to `POST /v1/connections`, which tests the sign-in, stores the credential (ADR 0009) and commits `connection.yaml` to the content store. Then run `kater git pull` in your local copy. Without a Kater API, the command stops and says to run `kater login`. The CLI does not make a DuckDB connection. `--dry-run` prints `connection.yaml` and saves nothing.

```sh
kater admin connection create snowflake_prod --account myorg-myaccount --warehouse COMPUTE_WH --user KATER --private-key-file ~/.ssh/kater_key.p8 --schema ANALYTICS.PUBLIC
kater admin connection create snowflake_prod --account myorg-myaccount --warehouse COMPUTE_WH --user KATER --private-key-file ~/.ssh/kater_key.p8 --passphrase --schema ANALYTICS.PUBLIC
kater admin connection create snowflake_prod --account myorg-myaccount --warehouse COMPUTE_WH --user KATER --password --prompt --schema ANALYTICS.PUBLIC
```

The secret is never a flag's value or an environment variable, so an agent that runs the command never holds it ([ADR 0022](../../docs/adr/0022-an-agent-does-onboarding-through-the-cli-and-never-holds-the-warehouse-credential.md)). A key with no passphrase needs no secret, and the command asks for nothing. With `--password`, or `--passphrase` for a key that has one, the command serves the same `127.0.0.1` page as `kater init connection` (`secret_page.py`) and opens it in the browser. The person types the secret there, and each try goes to `POST /v1/connections`. A wrong secret, which stops the test at the sign-in, shows the API's error on the page, and the person tries again. Any other error, such as a name already in use, closes the page, and the command stops with it, because a new secret cannot fix it. A correct one connects, and the command prints the result. The command waits 10 minutes at most. `--prompt` asks at a hidden prompt in the terminal instead, for a machine with no browser, such as one you reach over SSH. When no browser opens, the command stops and says to add `--prompt`.

`kater admin connection sign-in` replaces a Snowflake connection's stored sign-in, or adds one to a connection that has none, as Replace sign-in does on the connection's page in the admin console. It sends `PUT /v1/connections/{name}/sign-in`, which tests the new sign-in with the account, the warehouse and the role in `connection.yaml` and stores it only when the test passes. A failed test changes nothing. A new sign-in method also changes `auth_method` in `connection.yaml`, as one commit.

```sh
kater admin connection sign-in snowflake_prod --user KATER --private-key-file ~/.ssh/kater_key.p8
kater admin connection sign-in snowflake_prod --user KATER --password
```

It asks for the password or the passphrase as `kater admin connection create` does: on the `127.0.0.1` page, or at a hidden prompt with `--prompt`. Each stage of the test prints as the server passes it, with `[OK]`, and a failed stage prints with `[ERROR]`. Without a Kater API, and for a DuckDB connection, the command stops before it asks for anything.

`kater admin connection objects --all` lists every database that the connection's role can read, and with `--database`, that database's schemas with their table and column counts. It reads `GET /v1/connections/{name}/listing`, which the Schemas step of New connection and Select schemas read too, so it needs a Kater API and the `settings:manage` permission. Kater keeps the listing, and its worker reads the warehouse again when the listing is more than an hour old. While the worker reads it, the command waits up to two minutes for what it asked for. Without `--all`, the command reads the databases that `connection.yaml` selects, through `connection.discover`.

```sh
kater admin connection objects --connection snowflake_prod --all
kater admin connection objects --connection snowflake_prod --all --database ANALYTICS
```

## Onboarding

`kater init` alone opens the screen in a terminal. Its subcommands do one step each and print one JSON object, for an admin's own agent that does onboarding for them (see [ADR 0022](../../docs/adr/0022-an-agent-does-onboarding-through-the-cli-and-never-holds-the-warehouse-credential.md)). Both work on the same record, lease and local defaults, so an admin can start with one and continue with the other.

`kater init` needs no `kater login` first. When no credentials are saved, it opens the browser and waits 15 minutes at most. The browser opens the website's trial sign-up flow (`/free-trial-request/` at `KATER_SITE_URL`): a person with no account makes one, a person with no organisation gets one, and a person who has both goes straight on. The flow ends at PropelAuth's authorize address, which returns to the CLI as `kater login` does.

```sh
kater init guide                 # the instructions an agent follows
kater init guide about           # how the agent drafts the company description with the admin
kater init status [--wait]       # every step's status and the next command
kater init connection --name snowflake_prod --account myorg-myaccount --warehouse COMPUTE_WH --user KATER --password
kater init schema-sync --schema ANALYTICS.PUBLIC --no-profile-columns
kater init tables                # the synced tables, their columns and the saved company description
kater init tables --values --table ANALYTICS.PUBLIC.ORDERS
kater init about --business "We sell outdoor equipment online." --audience "Finance and merchandising." --question "What is the revenue for each region?"
```

The other subcommands are `content-store`, `schemas`, `first-query`, `team`, `local-copy`, `vscode`, `agent`, `write-back`, `skip` and `reset`. Each answer holds `ok`, the step's result, `onboarding` (each step's status) and `next` (the command for the next step); a failure holds `error` with a `code` and a `message`, and the exit code is 1. A command claims onboarding's lease when it is free or the caller's own and keeps its id beside the local defaults, so the agent never handles it. `kater init --takeover COMMAND` continues another admin's onboarding.

The company description is the fifth step, after the first query, so the agent can draft it from the synced tables. `kater init tables` lists them from the sync's own record and costs nothing. It also gives the names of the groups and attributes the admin made, as a hint for who uses the data, and no person. With `--values` and one `--table` for each table, 20 at most, the Kater API counts each table's rows, the first and last value of each date column, and the 10 most frequent values of each column the profile marked categorical and not unique, in a sample of 100,000 rows. It leaves out a value that only one row holds. The statements are Kater's own, the admin approves each run, and Kater keeps none of the answer. `kater init about` refuses to replace a description that already has text unless it gets `--replace`; the terminal screen's form always replaces. Both commands work again after onboarding and after a skip, and neither changes onboarding's progress then.

The agent never holds the warehouse credential. A private key is given as a path. With `--password` or `--passphrase`, the command serves one page on `127.0.0.1` (`secret_page.py`) and opens it in the browser; the admin types the secret there, the page shows the test result, and the command waits 10 minutes at most. The `agent` subcommand starts no agent: it writes the MCP configuration into the local copy and names the model-table skill for the agent that runs to follow.

`kater init` opens a Textual screen with the steps on the left and mouse support. A first run starts on a welcome screen that lists the nine steps while Kater prepares the content store. The line at the bottom shows the keys for the focused control. Tab, Shift+Tab, Up and Down move between fields. Left and Right change a choice, and move the cursor in a text field. At the edge of a field, they move to the field beside it on the same line, and Left from the first field moves to the step list. Right moves from the step list back to the form. Enter moves to the next field, and on the last field it runs the primary action. Ctrl+S runs the primary action from any field. Escape or F6 moves to the step list, where the keys 1 to 9 open a step. F1 shows all keys. A locked step explains which prerequisite is incomplete. Only explicit actions, such as **Test and connect**, submit changes. Below 74 columns the step list becomes one line, and Escape opens the full list.

An error shows below the field that caused it, and focus moves to that field. A field is checked when focus leaves it and again when its action runs. A pasted Snowflake URL becomes the account identifier. Path fields suggest a value, and Right accepts the suggestion: the private key field suggests the `.p8` and `.pem` files in `~/.snowflake`, `~/.ssh` and the current folder. Ctrl+R shows or hides a secret. The connection test shows each stage as the server completes it. The CLI asks `POST /v1/admin/onboarding/connection` for `application/x-ndjson`, and the Kater API answers with one JSON line for each stage (`sign_in`, then `role`), then a `result` or `error` line. A server that does not stream answers with plain JSON, and the screen shows the result only. When the test fails, the error names the stage that failed (`account`, `sign_in`, `warehouse` or `role`) as `stage`, and the screen marks the field to correct.

The terminal uses app-kit's dark Kater palette. Its icon is a terminal-cell version of the SVG on the successful login page. Regenerate the terminal assets after either source changes with `uv run apps/cli/scripts/generate-onboarding-brand.py` from the repository root. Image libraries are build-only dependencies; the installed CLI does not need them.

The schema step loads databases automatically, in two panes. Up and Down in the database pane choose a database and load its schemas. Right moves to the schemas. Space selects a schema, A selects all or none, and `/` filters. Selections stay when you change databases, and each database shows its selected count. Reading options does not start a sync. A failed read shows an error and a **Retry loading** action. A running sync shows one bar for each schema, from the counts the sync job saves in `operation.progress.schemas` as `[tables finished, tables selected]`. The bars start when the warehouse has listed the tables. The result waits for Enter.

The team step has three modes: invite people, import a CSV file, or SSO and SCIM. Enter adds an address to a list, a pasted line with many addresses adds each one, and one action sends all invitations. The write-back step shows one action for each stage. After you authorize GitHub in the browser, the screen polls and loads your accounts and repositories without another action.

Non-secret answers are saved as editable defaults under `$KATER_CONFIG_DIR/onboarding-drafts/`, or `~/.kater/onboarding-drafts/`. Each draft belongs to one API, organisation and user. Files are written atomically with owner-only permissions. Passwords, key passphrases and private-key paths stay in memory only. They remain available during retries and navigation, then clear after connection success or exit. The private key is read only when you submit the connection.

Ctrl+Q closes onboarding and prints what is saved and how to continue. A request already in progress finishes before exit. Server jobs continue afterward. Reopening onboarding shows their status. Errors stay beside the form without clearing its fields. After a network failure, the screen offers **Reload status** before you can repeat a mutation. A saved warehouse connection is read-only in onboarding. Applying a schema sync clears its earlier completion and the first-query completion, but keeps unrelated progress.

`kater init --reset` clears organisation progress and the current user's local defaults. It does not remove content, established connections, stored credentials, or GitHub write-back settings.

## Configuration

CLI-owned settings are Pydantic objects: local presentation/root/scope, serving options, explicit deployment inputs, and outbound API/login settings. Operation payloads and warehouse metadata remain request models; shared runtime identity, MCP authentication/branding and compiler settings stay with their owning packages.

Where a command runs comes from two files, in dbt's shape (CL7 of `brainstorm/kater-cli-plan.md`). Nothing reads a `config.toml`.

- **`kater_project.yml`** marks a project, the local copy, and names its profile. `kater git clone` and `kater git pull` write it, and `.git/info/exclude` keeps it out of git. A command that reads content looks for it from the working folder up, or takes `--kater-root`/`KATER_ROOT`, and exits 2 without one (`kater_core.scopes`, `project_required`).
- **`profiles.yml`** lists each profile's targets. It comes from `--profiles-dir`, else `KATER_PROFILES_DIR`, else the project folder, else the configuration directory (`~/.kater` or `KATER_CONFIG_DIR`). The profile is `--profile`, else the project's, else `kater`. The target is `--target`/`-t`, else `KATER_TARGET`, else the profile's `target`. These three flags may come anywhere on the line (`help_sections.hoisted`).
- **Every target is `type: kater`**, a Kater API: `api_url`, and optionally `auth_url`, `client_id`, `redirect_uri`, `site_url` and `token`. Warehouse calls go through that API with its stored credential (ADR 0042), so no target holds a warehouse credential. Any value can be `{{ env_var('NAME') }}` or `{{ env_var('NAME', 'default') }}`. A bad value exits 2 and names the file and the key, never the value.
- **`kater debug`** checks each part: the version, the profile and target, the project, the API's `/health`, the sign-in, a `select 1` on each connection through the API, the git state and the compile cache.

```yaml
# ~/.kater/profiles.yml
kater:
  target: dev
  outputs:
    dev:
      type: kater
      api_url: http://127.0.0.1:8788
    prod:
      type: kater
      api_url: https://api.kater.ai
    ci:
      type: kater
      api_url: https://api.kater.ai
      token: "{{ env_var('KATER_CI_TOKEN') }}"
```

`KATER_CONFIG_DIR` selects an alternate absolute directory for configuration and credential isolation (not the existing saved-scope location). Login saves each Kater API's sign-in settings in `sign-ins.json`. The other settings below come from a flag, else the environment, else the default.

| Setting | Environment | CLI flag |
| --- | --- | --- |
| Local JSON output | `KATER_JSON` | `--json` |
| Project folder | `KATER_ROOT` | `--kater-root` |
| Profiles folder | `KATER_PROFILES_DIR` | `--profiles-dir` |
| Profile | None | `--profile` |
| Target | `KATER_TARGET` | `--target`, `-t` |
| Local scope | `KATER_SCOPE` | `--scope` |
| Serve mode | `KATER_SERVE_MODE` | `serve --mode` |
| Local serve transport (`stdio`, or `http` on 127.0.0.1 behind `KATER_MCP_TOKEN`) | `KATER_SERVE_TRANSPORT` | `serve --transport` |
| Local serve port (`0` takes a free one) | `KATER_SERVE_PORT` | `serve --port` |
| Disable serve cache | `KATER_NO_CACHE` | `serve --no-cache` |
| Serve cache directory | `KATER_CACHE_DIR` | `serve --cache-dir` |
| Serve telemetry: `true` sends each MCP call's spans to the OTLP/HTTP receiver | `OTEL_ENABLED` | None |
| Serve telemetry receiver: an OTLP/HTTP base URL, such as `http://localhost:4318`. Required with `OTEL_ENABLED` | `OTEL_EXPORTER_OTLP_ENDPOINT` | None |

A target that names Kater's own API with no sign-in settings uses Kater's production values (`API_URL`, `AUTH_URL`, `CLIENT_ID` and `SITE_URL` in `src/kater_cli/config.py`). `kater init` writes the profile `kater` with the target `prod` at `API_URL` when there is none.

Settings validate when needed: local commands do not require API credentials; profile-token requests do not require OAuth settings; help/version do not read the profile. A saved scope in `~/.kater/scope.json` counts only inside its own project. Job commands keep their explicit root and scope rules rather than inheriting development defaults. Jobs keep explicit `--deployment` and `--env-file`; dotenv is never discovered automatically, and explicit job dotenv loading still does not override existing environment values.

## Structure

`commands/` contains ordinary annotated Typer callbacks grouped by resource. `dispatch.py` preserves omitted values and builds the execution adapter's request context. `connection.py` derives warehouse flags from the existing settings contract, and `lifecycle.py` adds a typed JSON command for each operation that `lifecycle_operations.py` lists from the named operation registry. `parse_arguments()` uses this same Typer tree without opening a service, for host-attached callers.

The root imports no command. `command_table.py` names each root command, its module and its short help, and the root group in `help_sections.py` lists them from it and imports a module only to run its command, so `kater --help`, `kater --version` and `kater <group> --help` answer in under half a second. A command module imports what is heavy, such as the client or the configuration, inside the function that uses it. After you add a root command, change one's help or add an operation to the chart, query, dashboard or insight family, run `python -m scripts.check_cli_manifest --write`; `pnpm check:cli-manifest` fails until then.
