tripl (CLI)
Operator CLI for a running tripl instance — diagnostics, health and live
monitoring. Like tripl-mcp, it is a pure HTTP client of the
tripl REST API (/api/v1): it imports no backend code and never touches the
database.
The distribution is tripl, the console script is tripl, and the
import package is tripl_cli. The last one is deliberate: the service's own
source package is backend/src/tripl/, so a distribution that installed an
importable tripl would shadow it in any environment holding both — a
contributor's backend venv, for one. The service is packaged as the separate
tripl-server distribution (tripl-ey6j.6), so tripl names this CLI alone.
This package also owns the shared async REST client (tripl_cli.client) and
the shared request layer above it (tripl_cli.api): every REST path, query
parameter and response projection either surface uses is spelled once, here.
tripl-mcp depends on tripl and imports both rather than carrying a copy, and
a contract test in each package fails the build if a path literal appears
anywhere else.
Install
The only runtime dependency is httpx. Python 3.12+ is required for the
pip path only — uvx downloads a suitable interpreter itself, so a host
with uv and no Python at all can still run the CLI.
From PyPI — tripl is published, so this is the ordinary path:
uvx tripl --version
# tripl 0.1.0
pip install tripl
From a local checkout, when you are working on the CLI itself:
uv run --project /path/to/tripl/cli tripl --version
From git without a checkout — how you get a revision that is not released yet. It resolves, but it is not exercised by CI:
uvx --from "git+https://github.com/vladenisov/tripl.git#subdirectory=cli" tripl --version
Commands
A command acting on the instance as a whole is one word; a command acting on a
class of objects is <plural-noun> <verb>. Every verb takes --json and
--timeout SECONDS; the ones that report on many projects also take
--project SLUG (repeatable) and --include-demo. The ones that name a single
object require --project exactly once. A bare tripl scans or
tripl drifts prints that group's help on stderr and exits 2.
tripl doctor # check the instance and report what is broken
tripl doctor --json # one JSON document on stdout, human lines on stderr
tripl doctor --strict # exit 3 on warnings too (never on skipped checks)
tripl status # projects, events, scans, signals, coverage
tripl watch # follow jobs, signals and delivery failures live
tripl watch --json # JSON Lines on stdout, one object per event
tripl scans list # scan configs, their schedule, and whether they dispatch
tripl scans jobs <scan> --project SLUG # recent jobs, newest first
tripl scans run <scan> --project SLUG # trigger a run now (WRITE)
tripl scans cancel <scan> <job-id> --project SLUG # cancel an active job (WRITE)
tripl drifts list # schema drifts; untriaged by default
tripl drifts dismiss <drift-id> --project SLUG # false_positive or snooze (WRITE)
tripl drifts reopen <drift-id> --project SLUG # back to open; drops the note (WRITE)
tripl install --app-url https://tripl.example.com --version 1.5.0 # provision a stack and start it (HOST)
tripl install --app-url https://tripl.example.com --dry-run # print the plan, write nothing
tripl upgrade --to 1.6.0 # move an installed stack to a new image tag (HOST)
--version defaults to latest; pin a released tag in production so a re-run
cannot move you onto an image you have not read the notes for. The command says
so on stderr when you leave it at the default.
doctor, status, watch, scans list, scans jobs and drifts list are
read-only — a tk_r_ key is enough. The four marked WRITE need a tk_w_
key backed by an editor or owner, and the CLI does not pre-judge that: the key
prefix is derived from the scope's first letter server-side and says nothing
about the user's role, so the request is sent and the API's own 403 is printed.
scans cancel, drifts dismiss and drifts reopen prompt on a terminal and
take --yes; when stdin is not a terminal and --yes was not given they
refuse with exit 2 rather than hanging a cron job or proceeding silently.
scans run does not prompt and has no --yes at all — passing one is exit 2,
because a no-op flag here is a flag a script author will assume works on the next
command too. All four writes take --dry-run, which resolves everything, prints
the exact request (method, path, params, body — never a credential) and sends
nothing.
drifts reopen is the one whose prompt is worth reading: reopening clears the
drift's resolution_note, resolved_by and resolved_at, and dismissing it
again does not bring them back.
install and upgrade are the two marked HOST: they act on a directory and
the local Docker daemon, not on a running instance, so they take neither
--url nor --api-key — passing either explicitly is exit 2 rather than a
silently ignored flag. Both take --dir (default ./tripl, always reported
absolute), --wait SECONDS (default 300, 0 skips), --dry-run, --yes and
--json, and both are idempotent: install re-run converges, and upgrade to
the tag already pinned prints already at X; nothing to do. and exits 0.
install writes compose.yaml, infra/rabbitmq/rabbitmq.conf and a generated
0600 .env into --dir, then runs docker compose pull and
docker compose up -d in it and polls <--app-url>/health. It never
overwrites an existing .env: that file is created, or appended to with your
confirmation, or left alone. --force reaches compose.yaml and
rabbitmq.conf only, and there is no flag that reaches .env, because losing
ENCRYPTION_KEY permanently destroys every stored warehouse credential.
--no-start writes the files and runs nothing (and skips the Docker probe
entirely). Note that the health poll targets the public --app-url, so on a
host whose TLS terminator is not up yet, use --wait 0 and curl
http://127.0.0.1:8000/health from the box.
upgrade --to is required and a downgrade is refused outright with no
override flag; an unorderable pair (latest, sha-abc1234, 1.4) says so and
demands --yes. It pulls, then moves the TRIPL_VERSION pin in .env
keeping a 0600 .env.bak.<UTC> copy, then restarts — the pull is first so a
bad tag leaves .env untouched. The pg_dump backup command is printed for
you to run and always prompts: a dump this tool invoked and then called
"your backup" would be a promise it cannot keep, not least because the dump does
not contain ENCRYPTION_KEY. Your database lives in the named volume
pgdata18, not in --dir.
Neither creates the owner account, connects a warehouse or runs the first scan.
The first two are unreachable with an API key of any scope — they need an
interactive owner session — so install finishes by reading the instance's real
bootstrap state from the unauthenticated /auth/status and printing the URL to
open in a browser.
docker compose pull and up -d write straight to your terminal and are never
captured, so their progress and their errors are live; the exact invocation is
printed first and is safe to paste. That is why --json carries the argv, the
cwd and the returncode of each command rather than its output. No generated
secret is ever printed — the document lists secrets_generated by name.
Two operations are deliberately not offered here. A bounded metrics replay
is owner-session-only server-side (deps.get_owner_user rejects every
request carrying an API key scope), so no tripl command could reach it.
Accepting a schema drift deletes the field definition on a missing_field
drift — the damage doctor's schema_field_deleted_by_accept finding exists to
report — so that decision stays in the tripl UI. The API refuses the accept
outright (409) when a scan config's event name format builds event names from
that column, and its force override for that refusal is likewise not spelled
here.
doctor runs six checks, always in this order and always exactly once each:
connectivity, auth, projects, data_sources, scans, drifts.
Per-project results are findings inside a check, so a consumer selects by
id and gets one row.
tripl doctor - https://tripl.example.com (from $TRIPL_BASE_URL)
PASS connectivity Reached https://tripl.example.com (from $TRIPL_BASE_URL); the API and its database are up.
PASS auth The API key authenticates as an instance-wide key (role: owner).
PASS projects 1 project selected.
FAIL data_sources 1 referenced data source(s); see below.
- fail: data_source_probe_failed 'warehouse-prod'
Data source 'warehouse-prod' (used by scan config 'prod events', 'checkout funnel') last failed its connection test at 2026-07-29T19:08:09Z: 'FATAL: password authentication failed for user "tripl"'.
FAIL scans 1 of 2 scheduled scan configs is not collecting.
- fail: scan_config_failing [prod] 'prod events'
Scan config 'prod events' (1h) has failed 5 consecutive scheduled runs since 2026-07-31T14:08:09Z. Last error: 'Scan failed due to an internal error.' - that is the backend's generic fallback, not the real cause, so the cause is in the worker log for job job-0.
- warn: scan_backoff_active [prod] 'prod events'
The scheduler has deliberately deferred the next attempt to not before 2026-07-31T22:08:09Z (about 4h after the last failure): 3 or more consecutive failures trigger a backoff, so the worker is not stuck.
WARN drifts 1 event type(s) examined; see below.
- warn: schema_field_deleted_by_accept [prod] 'app.screen_view'
Field 'user_id' was deleted from event type 'app.screen_view' on 2026-07-26T19:08:09Z when a missing_field drift was accepted (by user uid-7).
- warn: schema_drift_open [prod]
Project 'prod' has 1 untriaged schema drifts (oldest detected 2026-07-28T19:08:09Z): app.screen_view.cart_value (type_changed)
6 checks: 3 pass, 1 warn, 2 fail. Exit 3.
Re-run with --json for the machine-readable form of every finding.
Output is ASCII only and byte-identical whether stdout is a TTY or a pipe, so
tripl doctor | tee incident.log and the terminal view are the same artifact.
Two behaviours are worth knowing before you read a report: a non-200 is never
treated as an empty list (it becomes endpoint_unexpected_status, because a
404 read as "no drifts" is the class of mistake this tool exists to remove), and
the scheduler's retry backoff is reported as expected behaviour rather than
as a hang.
watch answers the other question: not what is broken at one instant, but
what is happening right now. It polls (there is no daemon and no subscription)
and prints one line per change — a replay advancing a chunk, a job finishing, a
signal opening, an alert delivery failing to page anyone. It reaches no
verdict: a completed run exits 0 whatever it saw, and it never exits 3. The
same ASCII-only, pipe-identical rule applies, so tripl watch | tee incident.log is the artifact you actually saw.
2026-07-31T19:10:41Z watch.started 1 project, 1 scan config, poll 10s.
2026-07-31T19:10:51Z job.progress [prod] 'nightly replay' job job-91c2 chunk 4 of 18 (22.2%) collecting 2026-07-05T00:00:00Z..2026-07-06T00:00:00Z, 2m elapsed.
2026-07-31T19:11:11Z delivery.failed [prod] 'Checkout drop' -> slack 'oncall' failed: 'channel_not_found'. Nobody was paged; delivery del-4f21.
2026-07-31T19:11:21Z watch.stopped stopped (interrupted) after 40s, 5 ticks, 12 requests.
Useful flags beyond the shared three: --scan NAME_OR_ID (repeatable, exact
match, narrows the job lines only), --interval SECONDS (default 10),
--duration SECONDS (stop and exit 0; the default is to run until Ctrl-C) and
--stall-after SECONDS (default 120, report a running job whose progress has
not moved).
| Exit | Meaning |
|---|---|
| 0 | Every check passed, or only warned and --strict was not given. status, whenever it completed. watch, whenever the run completed — a failed job or a new signal is still 0. The scans / drifts verbs, whenever every read arrived or the write was accepted (--dry-run included). |
| 1 | The tool itself broke (doctor turns every API failure into a finding), or any other command could not complete a request — unreachable, or the API refused it. For watch this includes a key revoked mid-run. For scans list / drifts list it includes any failed read in the fan-out; for scans run, a job returned already failed; for scans cancel / drifts dismiss / drifts reopen, a declined prompt. |
| 2 | Usage or configuration error. For doctor and status that is resolved before any socket opens; watch also refuses after reading the listings, when --scan matches nothing or more than 24 scan configs are selected. The scans / drifts verbs add a bare group, a missing or repeated --project, an unresolved or ambiguous <scan>, and a prompting write on a non-TTY without --yes. Either way no JSON is emitted and no write is sent. |
| 3 | doctor only: at least one check failed, or --strict and at least one warning. Nothing else ever exits 3. |
| 130 | Interrupted (SIGINT). For watch this is the normal ending — a run without --duration has no other way to stop. |
An unreachable instance therefore exits 3 out of doctor, not 1 — it
becomes a finding like everything else doctor reads, which is what makes an exit
1 out of doctor a meaningful bug signal. Every other command exits 1 on an
unreachable instance, because none of them turns a failed read into a verdict.
doctor and status put exactly one JSON document on stdout, and so do the
scans / drifts verbs — but only when the command completes: a write the API
refused, a read that failed outright and a declined prompt all leave stdout
empty and put the reason on stderr, so a consumer checks the exit code
before it parses. watch --json
puts JSON Lines, one object per event, flushed as produced. Within one
schema_version, key names are never removed or retyped, and check ids,
finding codes, status/severity values and watch event tokens are never
renamed or repurposed. New keys, ids, codes and tokens may appear in any
release. title, summary and message are prose. Assert on code and
evidence — or, for watch, on event and data — never on prose.
Full reference — every check, every finding code with its evidence keys, every
watch event token and the JSON Lines envelope, and what an operator should
actually do about each one:
https://vladenisov.github.io/tripl/run/cli (source:
website/docs/run/cli.md).
Configuration
Resolved per field, highest precedence first:
- command-line flag —
--url/--base-url,--api-key - environment —
TRIPL_BASE_URL,TRIPL_API_KEY - config file —
base_url,api_key
Per field, not per source: --url https://staging with the key still coming
from the config file works.
| Variable | Meaning |
|---|---|
TRIPL_BASE_URL |
Base URL of the tripl instance (the same variable tripl-mcp reads) |
TRIPL_API_KEY |
API key — tk_r_ for read-only, tk_w_ for write |
XDG_CONFIG_HOME |
Overrides the config file location on every platform |
There is deliberately no TRIPL_URL: two supported spellings for one
setting is how configuration drifts. If it is set and TRIPL_BASE_URL is not,
the error says so by name.
Config file
| Platform | Path |
|---|---|
| Linux / BSD / macOS | $XDG_CONFIG_HOME/tripl/config.toml, else ~/.config/tripl/config.toml |
| Windows | %APPDATA%\tripl\config.toml, else the ~/.config fallback |
# ~/.config/tripl/config.toml
base_url = "https://tripl.example.com"
api_key = "tk_r_..."
TOML because tomllib is stdlib on the supported Pythons, so the file format
costs zero runtime dependencies — which matters, because tripl-mcp
inherits every dependency of this package. Unknown keys and unknown tables are
ignored, so an older CLI keeps working against a file written by a newer one.
--config PATH overrides discovery. A missing default file is fine; a missing
--config path is an error. On POSIX, a config file that holds an api_key and
is readable by other users gets one warning on stderr — chmod 600 it.
Create API keys in the tripl app under Settings → API keys.
Development
cd cli
uv sync
uv run --group dev pytest -q
uv run --group dev ruff check
uv run --group dev ruff format --check
uv run --group dev mypy src
mcp-server resolves this package from ../cli via [tool.uv.sources] rather
than from the published release, so a change here is picked up by
cd mcp-server && uv sync without any install step — and both suites test the
same source. Run both after touching client.py.
Metadata
Release files for tripl 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tripl-0.2.0.tar.gz | 294.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tripl-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 503.3 kB
Release files / tripl-0.2.0.tar.gz
| Download URL | tripl-0.2.0.tar.gz |
|---|---|
| Size | 294.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
53b2752b239d16dee4984dd5d292aa30de10fccbb7bd7540abbb702c73932d4e
|
|
BLAKE2b-256 checksum How to use checksums |
87e55aea22ca02c71850a494ef76ee2a76039104b95d6392c54291b6e9815baa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency logRelease files / tripl-0.2.0-py3-none-any.whl
| Download URL | tripl-0.2.0-py3-none-any.whl |
|---|---|
| Size | 208.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
27703a1d54b724bd07f8c397272f56783a8aaed5ee3ee4d6e35d2481c9407a4e
|
|
BLAKE2b-256 checksum How to use checksums |
653899d0146bdb14a88b3f32cd8d5674fe5416120b983f318d83bce4e20a595a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 9, 2026.
Transparency log