Metadata-Version: 2.5
Name: lynk-cli
Version: 0.1.3
Summary: Command-line interface for the Lynk semantic layer platform.
Author: Lynk
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27.0
Requires-Dist: keyring>=25.0.0
Requires-Dist: logfire[httpx]>=3.0
Requires-Dist: pydantic-settings>=2.2.0
Requires-Dist: pydantic>=2.6.0
Requires-Dist: pyjwt>=2.8.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7.0
Requires-Dist: typer>=0.12.0
Description-Content-Type: text/markdown

# Lynk CLI

`lynk` is the command-line interface for the Lynk semantic layer platform. Log in once
with your browser, then build your semantic layer and query it over SQL — all from your
terminal.

## Install

The CLI is a standalone Python package. Install it as an isolated tool:

```bash
# with uv
uv tool install lynk-cli

# or with pipx
pipx install lynk-cli
```

## Quickstart

```bash
lynk auth login            # opens your browser to authenticate
lynk auth whoami           # confirm who you are and your active tenant
lynk build run             # build / validate your semantic layer
lynk setup --harness claude   # install the Lynk plugins into this project (Claude Code)
```

## Commands

`-h` works anywhere `--help` does, on the root app and on every group and command.

`-f json` always writes exactly one valid JSON document to stdout and nothing else, so it
pipes into `jq` unconditionally. Everything the human report shows as context is a field
in that document — `build local` carries its `archive` summary, and a branch with no
builds is `null` rather than empty output. Progress and warnings go to stderr.

### `lynk auth login`

Authenticates via your browser using OAuth (Authorization Code + PKCE) and stores your
tokens securely in the OS keyring. The active tenant is chosen during the browser login
flow, so login is single-tenant.

### `lynk auth logout`

Removes your stored credentials and revokes the session.

### `lynk auth whoami`

Shows the authenticated user, active tenant, and token expiry.
`-f json` prints the same fields as a JSON document for scripting.

### `lynk build`

A command group for building and inspecting the semantic layer.

`lynk build run` builds (validates) the semantic layer for a branch and prints a report —
a summary on success, a grouped issue list on failure. Exits non-zero when the build is
invalid (CI-friendly). A commit that was already built reuses its build.

```bash
lynk build run                 # build the current git branch
lynk build run -b main         # build a specific branch
lynk build latest              # show the report for the latest build (any status)
lynk build status              # show just the latest build's status / commit / time
lynk build run -f json         # raw JSON instead of the report (any build command)
```

For `run` / `latest` / `status` the branch defaults to your current git branch. `build
latest` / `build status` report the latest *remote* build — builds created by `build
local` are excluded from those lookups by the API.

`lynk build local` zips your `.lynk/` directory at HEAD and uploads it, so it needs no
git integration on the server side. It always builds the branch you have checked out, and
refuses to run while `.lynk/` has uncommitted changes — the build is recorded against a
commit sha, so the upload has to be what that sha names. Changes under `.lynk/evals/` are
the exception: a build never reads your test cases, so editing them doesn't block one.
Changes elsewhere in the repo don't matter either. `--dry-run` prints the archive summary
without uploading.

```bash
lynk build local               # zip and upload .lynk/ at HEAD
lynk build local --dry-run     # build the archive but don't send it
lynk build local -f json       # raw JSON instead of the report
```

### `lynk evals`

A command group for running the agent pipeline (explore -> text-to-sql) against a dataset
of test cases and grading the results, so you can catch regressions before they reach
customers. Requires an existing build for the checked-out commit — run `lynk build local`
or `lynk build run` first.

Test cases are authored as a flat YAML list under `.lynk/evals/`: `.lynk/evals/dataset.yml`
is the recommended single file, but every `*.yml`/`*.yaml` in that directory is read and
merged. Each case needs at minimum a `name` and a `domain`; everything else (the fields
that describe the question, the expected entities, `labels`, etc.) is forwarded to the API
as authored — run `lynk evals case-schema` to see the full field list the backend expects.

```yaml
# .lynk/evals/dataset.yml
- name: total-revenue-last-quarter
  domain: sales
  labels: [smoke]
  entities: [order]
  request: "What was our total revenue last quarter?"
```

Only `name` and `domain` are interpreted by the CLI; the remaining field names are the
backend's, so take them from `lynk evals case-schema` rather than from this example.

```bash
lynk evals case-schema           # print the case field schema (authoring reference)
lynk evals validate               # check every case: locally, then against the API
lynk evals run                    # run every case through the pipeline
lynk evals run --dry-run          # show which cases would run, without sending anything
lynk evals run -F smoke           # only cases labeled "smoke"
lynk evals run -F smoke -F entity:team  # only cases labeled BOTH (labels are AND-ed)
lynk evals run -d core -d finance   # cases in either domain (domains are OR-ed)
lynk evals run -F smoke -d core     # "smoke" cases in the core domain
lynk evals run --max-concurrency 10  # default 5, capped at 20
```

`--filter`/`-F` and `--domain`/`-d` are both repeatable: multiple `--filter` values must
*all* be present on a case's `labels` (AND), while multiple `--domain` values match a case
in *any* of them (OR). `validate` also accepts `-t/--test-case NAME` to check a single case.

`run` goes further: it validates every selected case against the API *before* starting, and
refuses to run at all if any case fails — a broken `expected_sql` would otherwise cost two
agent runs and a judge call, then be scored against SQL that was never valid. A rejected run
creates no run directory. That costs one cheap validation request per case up front.

Both commands check your dataset locally first — case names unique across files, and each
case's `domain` and `entities` actually present in your `.lynk` source — but they react
differently. **`validate` reports every case**: a case with a typo fails on its own (and is
not sent), while the rest are still validated against the API, so one mistake never hides
the state of the whole dataset. It exits non-zero if any case failed. **`run` stops before
sending anything** if any case has a problem, because it is about to spend real agent time.

The local check is a typo catcher, not a guarantee: the agents see the *compiled* layer, so
a name that exists in your source can still be missing from a build.

Each `run` writes its artifacts under `.lynk/evals/runs/{timestamp}/`: `run.json` (run
metadata and counts) and `raw.jsonl` (one line per case, with every stage's raw response).
Add `.lynk/evals/runs/` to your `.gitignore`.

A run reports each case as it finishes, with a spinner for the ones still going, so a long
run shows progress instead of sitting silent. It ends with a table of the scores each case
got, one column per evaluator.

Cases that errored have no scores, so they appear in the list above the table but not in it
— an infra failure is not a low score. `run` never exits non-zero for low scores or case
errors; those are results, not failures. It exits non-zero only when a run could not start:
no git repo, no dataset, a bad `--max-concurrency`, a pre-flight problem, or a case that
failed validation. Aggregation across a run is not implemented, so each score is a single
trial — small differences between cases are noise.

### `lynk marketplace`

A command group for the Lynk plugins, distributed as a plugin marketplace. Every command
names its coding harness explicitly — `--harness claude` is required, and `claude` is the
only value today — and that harness's own CLI has to be on your `PATH`, since these
commands drive it.

`lynk marketplace install --harness claude` (alias: `lynk setup --harness claude`) sets up
the current project: it downloads the latest marketplace release to
`$HOME/.lynk/marketplace`, registers it with Claude Code, installs the plugins it declares,
and wires a `SessionStart` hook into the project's `.claude/settings.json` so the plugins
stay current automatically. Start a new Claude Code session afterwards to load them.

```bash
lynk marketplace install --harness claude   # set up this project (same as `lynk setup`)
lynk marketplace update --harness claude    # refresh without waiting for a session start
```

`lynk marketplace update --harness claude` fetches the marketplace only when it has changed
(conditional request), updates the installed plugins, and tells you to run
`/reload-plugins` or relaunch when a new version lands. It prints nothing when everything
is already current, and **always exits 0** — a failed refresh (offline, expired login,
missing `claude`) reports a warning and leaves the working installation in place, so it can
never stop a session from starting. `--hook` is what the hook itself passes: it emits
Claude Code `SessionStart` JSON on stdout, which Claude Code reads to tell you the plugins
moved.

The marketplace lives at `$HOME/.lynk/marketplace` — machine-wide, not per project,
because Claude Code keeps a single global marketplace-name → path mapping. The extracted
release version is recorded in `$HOME/.lynk/marketplace.release`.

Commit `.claude/settings.json` (the hook plus your `enabledPlugins`) so everyone on the
repo gets the same setup; keep the generated, machine-specific
`.claude/settings.local.json` out of git.

## Configuration

### API token (non-interactive auth)

For CI and automation where a browser login isn't possible, authenticate with an **API
token** instead of `lynk auth login`. Provide it with `--api-token` or the
`LYNK_API_TOKEN` environment variable — it works on every command:

```bash
LYNK_API_TOKEN=lynk_… lynk build run        # via env var (best for CI)
lynk --api-token lynk_… build run           # global flag (before the command)
lynk build run --api-token lynk_…           # per-command flag (after the command)
```

When set, the token is sent as the `x-api-key` header on API requests, bypassing the
OAuth session entirely. An API token is a **secret**, so it is read **only** from
`--api-token` / `LYNK_API_TOKEN` — never from `~/.lynk/config.toml`. It takes precedence
over any stored browser login.

### Tracing

The CLI reports a trace of each invocation to Lynk, and API calls carry the trace context
so server-side spans join the same trace.

Set `TRACEPARENT` (a standard W3C traceparent) to have the CLI join a caller's trace
instead of starting its own — useful when a CI job or wrapper script invokes `lynk` and
wants its trace to include the invocation.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `Not logged in` / `Session expired` | Run `lynk auth login` (or pass `--api-token` / set `LYNK_API_TOKEN`). |
| `Authentication failed` with an API token | The `--api-token` / `LYNK_API_TOKEN` value is invalid or revoked — check it. |
| `Could not determine the branch` | Pass `-b <branch>` or run from a git repo. |
| `evals run`/`validate` fails because no build exists for this commit | Run `lynk build local` (or `lynk build run`) first. |
