Metadata-Version: 2.4
Name: meridian-governance-cli
Version: 0.9.0
Summary: Command-line client for Meridian Governance Router — not the unrelated 'meridian-cli' package also on PyPI
License-Expression: MIT
Project-URL: Homepage, https://github.com/knjpollard/meridian
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: meridian-client>=0.4.0
Requires-Dist: requests>=2.28.0
Dynamic: license-file

# meridian-governance-cli

Scriptable command-line client for [Meridian Governance Router](https://dev-meridian.mytrustworthy.ai/), over [`meridian-client`](https://pypi.org/project/meridian-client/). Installs as the `meridian` command — one short command, same shape as `mta` or `claude`.

The PyPI package is `meridian-governance-cli`; the command it installs is `meridian`. Not to be confused with the unrelated `meridian-cli` package also on PyPI.

Closer in spirit to the `aws` or `stripe` CLIs than to an app's own CLI:
subcommands over an API, scriptable, mostly no interactive loop required —
with one exception. `chat` absorbed
[Local-AI](https://github.com/knjpollard/Local-AI)'s reference
implementation upstream (`meridian-agent-v1.0.py`, decided 2026-10-18) so
customers get one governed resolve -> dispatch -> audit loop from Meridian
itself, interactively or scripted, rather than writing that sequence
themselves against the SDK.

## Install

```bash
curl -fsSL https://dev-meridian.mytrustworthy.ai/install.sh | bash
```

Detects Python 3.9+, installs via `pipx` if present, otherwise into an
isolated venv under `~/.local/share/meridian` with a shim on `~/.local/bin`. One
command, nothing else to figure out. It also drops a small manifest at
`~/.meridian/install-manifest.json` recording which path it took, so
uninstalling doesn't require remembering:

```bash
curl -fsSL https://dev-meridian.mytrustworthy.ai/uninstall.sh | bash
```

Reads that manifest to remove the right thing (`pipx uninstall` or the
venv it created), offers to revoke your API key on the server first
(`meridian logout --revoke`), and separately offers to delete
`~/.meridian` (saved credentials, chat session logs) — three
confirmations, nothing deleted without asking.

Or plain pip, if you'd rather manage the venv yourself (no manifest, so
`uninstall.sh` won't know about it — just `pip uninstall` when you're done):

```bash
pip install meridian-governance-cli
```

## Get credentials

```bash
meridian signup          # new org — prompts for org name, email, password
# or
meridian login           # existing account — authenticates, mints a new key
meridian whoami           # confirm what's saved
```

Credentials live in `~/.meridian/config.json` (mode 600 — `api_key` is in
there in plaintext, same tradeoff `mta-cli` makes for its own token). Override
the location with `MERIDIAN_CLI_HOME` if you want more than one profile.

## Commands

```bash
meridian resolve my_agent [--project-id ID] [--json]
meridian explain ROUTING_LOG_ID
meridian audit ROUTING_LOG_ID --input-tokens N --output-tokens N

meridian projects create --name NAME [--quality Q] [--cost C] [--risk R]
meridian projects list

meridian agents create PROJECT_ID AGENT_NAME [--pinned-model M] [--quality-override Q] [--allowed-hubs h1,h2]
meridian agents list PROJECT_ID

meridian hubs list
meridian hubs discover

meridian keys list [--json]
meridian keys create [--name NAME]
meridian keys revoke KEY_ID

meridian catalog
meridian logout [--revoke]

meridian python-path     # print the interpreter this command runs under —
                          # has `meridian` importable even inside a pipx venv.
                          # For a host program that wants `from meridian import
                          # Meridian` directly instead of shelling out: run this
                          # once to find the right interpreter, don't parse the
                          # `meridian` executable's shebang to guess it.

meridian chat [--agent NAME] [--code-agent NAME] [--project-id ID] [--max-tokens N]
meridian chat --one-shot "message" [--json] [--max-tokens N]
meridian chat --daemon [--host HOST] [--port PORT] [--refresh-interval SECS]
```

Every data-returning command without a dedicated human-readable format
(`explain`, `audit`, `catalog`, `projects list`, `agents list`) prints raw
JSON — pipe into `jq`.

### `chat`

Per-turn flow: `resolve(agent)` -> catalog/hubs join -> dispatch to the
chosen hub's OpenAI-compatible endpoint -> `audit`. Routing, scoring and
governance all live in Meridian; `chat` just drives the loop. OpenAI-compat
hubs only for now (vLLM, Ollama, OpenAI-shaped endpoints) — bedrock/
anthropic/etc. need a protocol translator, not yet added.

- No args: interactive REPL. `/help` inside it for slash commands
  (`/catalog`, `/hubs`, `/refresh`, `/history`, `/export`, `/window`, …).
- **Three REPL modes**, switched with `/chat`, `/code`, `/manage` (default `chat`):
  - `chat` — plain conversation, `resolve(--agent)` (default agent name
    `concierge` — every org gets a default project + a `concierge` agent
    provisioned automatically on signup, so this works immediately with
    no setup).
  - `code` — same loop, different binding: `resolve(--code-agent)` (default
    agent name `code`) plus a one-time system preamble. Its own history,
    separate from `chat` mode's. This is *not* a tool-using coding agent —
    Meridian's dispatch layer never sends `tools` to the hub, so this is "a
    different agent/model to resolve against," nothing more. A real
    tool-execution loop would be new dispatch-layer work, not a REPL mode.
  - `manage` — no LLM calls at all: `/projects`, `/agents`, `/keys` (list/create,
    `/keys revoke ID`) over the account you're logged into. Same functions
    `meridian projects`/`meridian agents`/`meridian keys` call outside the REPL
    (`meridian_cli/manage.py`) — one implementation, not two that can drift.
  - `/project [ID]` works in any mode: with no argument, shows the active
    project and lists your org's projects; with an ID, switches to it
    (validated against your org's actual projects first). Affects the next
    `resolve()` call in `chat`/`code` mode immediately.
- `--one-shot "message"`: single turn, prints the response, exits.
  `--json` emits structured output (`response`, `model`, `hub`,
  `routing_log_id`, `latency_ms`, `usage`) instead of plain text. Exit
  code: `0` an answer was produced (including a token-budget-exhausted
  message — that's a completed, audited call, just an unhelpful one),
  `1` a real failure (`resolve()` failed, or dispatch returned an error —
  unreachable hub, bad model id, HTTP error), `2` misuse (`--daemon` and
  `--one-shot` together — mutually exclusive, now rejected instead of
  silently running the daemon and dropping `--one-shot`).
- `--daemon`: long-lived local OpenAI-compatible endpoint (`POST
  /v1/chat/completions`, `GET /v1/models`, `GET /health`) — point an
  existing OpenAI-SDK-based app at `http://127.0.0.1:8700` and it works
  unchanged, with Meridian's governance and audit trail in the middle. The
  F9 "broker" shape from Local-AI's feedback. Stateless across requests,
  same as the real OpenAI API: your app holds history and sends the full
  `messages` array each call.
- `--max-tokens`: reasoning models can need a *much* larger budget than the
  default (8192) just to reach visible content — confirmed live against a
  real deployment where the same five-word prompt burned anywhere from
  ~2400 to the full 8192-token budget purely on reasoning, before any
  answer. If a response comes back as a token-budget notice instead of
  text, raise this — unless the prompt matches the pattern below, where
  raising it won't help.
- **Exact-count / constraint-satisfaction prompts can make reasoning never
  converge, at any budget.** Isolated live against `qwen3.6:35b`: `"Say
  hello in exactly five words."` burned the *entire* reasoning budget at
  8192, then again at 16384 — raising `--max-tokens` didn't make it
  converge faster, it just filled whatever ceiling it was given. The same
  model answers the unconstrained version (`"Say hello."`) normally in 186
  reasoning tokens, and a substantive non-counting prompt normally in ~700.
  So this isn't the model's reasoning budget being too small — it's a
  specific model failure mode where exact-count constraints seem to trap it
  in verifying the count. Not a Meridian or CLI bug; a model characteristic
  to know about before raising `--max-tokens` in response to a stuck
  "exactly N words/items" prompt — raising it will not fix that case.

### `hubs discover`

Probes `localhost` for a local Ollama (`:11434/api/tags`) or vLLM
(`:8000/v1/models`), proposes registering what it finds (models included),
and asks for confirmation before actually registering anything — it never
writes to your org's catalog without that confirmation. If nothing
responds on localhost, it falls back to prompting for a provider + URL
manually, probes that instead, and asks for confirmation the same way.
Same wizard from `meridian hubs discover` and `/discover` inside `meridian chat`'s
manage mode (`meridian_cli/manage.py` + `chat.py`'s `cmd_manage_discover`)
— one implementation either way.

Deliberately only probes `localhost`, never a LAN subnet: unlike a
single-household tool that can hardcode its own known boxes' IPs,
Meridian is multi-tenant — there's no fixed list of "this customer's
boxes" to guess at. `localhost` is the one address that means the same
thing for every caller; anything else goes through the manual-entry
fallback instead of a scan.

Also why this is a client-side probe at all, not a call to the backend's
own `/discover` endpoint: on a SaaS instance, "localhost" from the
*server's* perspective means the server's own container, not your
laptop. Discovery and model-listing both happen in the CLI process so
they can actually reach `localhost` on the machine `meridian` is running on;
the resulting model list is registered directly via `register_models`,
never by asking the server to re-discover something it structurally
cannot reach.

## What this deliberately doesn't do

- `hubs discover` only knows Ollama and vLLM, and only probes `localhost`.
  No cloud-hub registration commands (bedrock/anthropic/openrouter/etc.) —
  those need credentials entered deliberately, not something to probe for.
  Console-only for now.
