kennet
The command line for Kennet, Metaphi AI's RL infrastructure. Sign in and twin agent traces.
Install
brew install metaphi-labs/tap/kennet # macOS and Linux
No Homebrew? Download the binary for your platform from the
latest release, unpack it and put kennet on your
PATH. Each release lists its checksums.txt.
Calling it from Python instead: pip install metaphi-ai gives you the same command and the library (see below).
Quickstart (2 minutes)
kennet login # opens the browser; confirm the code you see in the terminal
kennet twin trace.json # writes trace.twin.json
Working through Claude Code? kennet skill install, then ask it to twin the file: see
Driving it from an agent.
kennet login signs you in as yourself and hands this machine your organization's keys: a live key (mai_sk_...)
and a test key (mai_sk_test_...). They are kept in the OS keychain when there is one (macOS Keychain, Windows
Credential Locker, Secret Service on Linux), otherwise in the config file, and are never printed. Logging in again
replaces them; kennet logout revokes them. If your organization has no twinning access yet, the sign-in still
works and says why no key was saved.
kennet twin trace.json --test runs on the test key. Test runs go through the same pipeline but cost nothing; files
can be at most 256 KiB and an organization gets 20 runs a day. Test twins and live twins never see each other.
Usage, keys, members and billing are in the console at https://app.metaphi.ai/console; the work is done here.
Driving it from an agent
kennet skill install # once per machine: Claude Code now knows this CLI
That writes the kennet skill to ~/.claude/skills/kennet (--project writes it to ./.claude/skills instead, to commit
for a team). Then ask Claude for what you want: "twin trace.jsonl before I send it to the vendor". The skill is a
pointer: it tells the agent to run kennet skill show, which prints the guide for the version installed, so the
guide never goes stale. Any other agent can be handed the output of kennet skill show.
The CLI is made to be run by a coding agent (Claude Code and the like) as much as typed. Away from a terminal
nothing waits on a question, and every command takes --json: stdout is then the result, or the error object,
alone, and progress goes to stderr. An error that one command clears names it: Next: <command> on stderr,
next_step in the JSON.
kennet login --json # {"verification_uri", "user_code", "expires_in", "next_step": "kennet login --complete"}
# the person opens the page and confirms the code
kennet login --complete # waits for the approval (60 s away from a terminal) and saves the keys; safe to run again
kennet whoami --json # asks the server: email, organizations (role, grants, balance), the key in use
kennet twin trace.json --no-wait --json # {"id", "status", "next_step": "kennet twins wait twin_..."}
kennet twins wait twin_... --json # {"output", "id"} once it ends
kennet twins list --json # the organization's twins, newest first
Away from a terminal kennet login is --no-wait by itself: it prints the page and the code and returns, so the
agent can show them to its person. --wait keeps the old behaviour, one call that waits for the approval.
A twin started is the service's, not the terminal's. Ctrl-C, a closed session or --timeout stop the waiting, never
the twin, and say how to come back: kennet twins wait <id> (add --test for a test twin), from any machine
holding the organization's key.
Use it from your servers
A server or CI job has no browser: make it a key of its own in the console and hand it over as a secret.
export METAPHI_API_KEY=mai_sk_...
kennet twin trace.json -o out.json --json
METAPHI_API_KEY overrides whatever is saved. A key you already have can also be saved with
kennet login --with-api-key, which reads it from stdin (or prompts without echo), so it never lands in your shell
history or the process list.
One trace file in, one twinned file out, in the same format. --json prints {"output", "id"} on stdout, with
progress on stderr. Or from Python:
from pathlib import Path
from metaphi.twin import twin
result = twin(Path("trace.json"), Path("out.json"), api_key="mai_sk_...")
result["output"] # where the twinned trace was written
One key per environment (prod, staging) or per customer is a good default. Revoking one stops only that key.
Twin options
| Flag | What it does |
|---|---|
-o, --output |
Where the twinned trace goes. Default: <name>.twin<ext>. |
--key <64 hex> |
Keys the stand-in values. The same key gives a person the same stand-in in every trace, so a corpus stays consistent. Default: a fresh key per trace. |
--confidential <org> |
Replaces an organization even though it is public, e.g. your customer. Repeatable. |
--test |
Runs on the test key kennet login saved: free, small, apart from live twins. |
--no-wait |
Starts the twin and returns its id; kennet twins wait <id> comes back for it. |
--timeout <seconds> |
How long to wait for the twin. The twin goes on past it. |
The organization key comes from METAPHI_API_KEY, then the saved key. (--api-key still works but is deprecated: an
argument is visible in shell history and to other processes.)
People, private organizations, accounts and secrets are always replaced, each with one consistent synthetic value of the same kind, and the trace keeps its structure. An adversarial pass then reads the output for anything still real.
Errors
Every error from the service carries a stable code. Switch on the code or the exception class, never on the
message. On a failure the CLI exits 1 and prints:
error: This organization's prepaid balance is used up; add credits to continue. Nothing was started. (code: out_of_credit, request: req_5f1c...)
With --json, stdout also gets the error object: {"error": {"type", "code", "message", "retryable", "request_id"}},
plus run_id when a run failed, action_url when a page fixes it and next_step when a command does. Quote the
request_id to support.
From Python, each error type raises its own class from metaphi.errors. All of them subclass MetaphiError:
from pathlib import Path
from metaphi.errors import BillingError, MetaphiError, RateLimitError
from metaphi.twin import twin
try:
twin(Path("trace.json"))
except BillingError: # out_of_credit
... # add credit: `kennet credits buy`
except RateLimitError as e: # rate_limited_concurrent, rate_limited_daily
... # e.retryable is True: try again when a run ends
except MetaphiError as e:
print(e.code, e.request_id, e.retryable)
| Class | type |
Codes, for example |
|---|---|---|
BillingError |
billing_error |
out_of_credit |
AuthError |
auth_error |
no_organization |
RateLimitError |
rate_limit_error |
rate_limited_concurrent, rate_limited_daily |
InvalidRequestError |
invalid_request_error |
invalid_key, upload_already_run, task_not_found |
RunError |
run_error |
twin_unclean, not_text, too_large, session_failed, budget_exceeded |
IdempotencyError |
idempotency_error |
idempotency_key_reused, idempotency_key_in_use |
APIError |
api_error |
unavailable, balance_unavailable, run_unavailable |
retryable says whether making the same call again later may succeed. For a failed run, it means uploading the same
trace again.
The CLI says a few errors itself, each with the command that clears it as next_step. They are auth_errors,
but for the last:
| Code | When | next_step |
|---|---|---|
not_signed_in |
no sign-in on this machine, or the server has ended it | kennet login |
authorization_pending |
login --complete returned before the code was approved (retryable) |
kennet login --complete |
no_api_key |
signed in, but the organization was handed no key; the message says why | kennet whoami |
login_expired, login_refused |
the sign-in code ran out, or was declined | kennet login |
timed_out (api_error) |
the wait ended before the twin did; it goes on | kennet twins wait <id> |
Any other error the CLI says itself, such as a file that is not there, is a plain MetaphiError with code None.
Your organization
kennet whoami asks the server who you are: your organizations, your role and what each may do, its prepaid balance
when you may see it, and which key calls run on (the saved one, or METAPHI_API_KEY), by its last four characters.
A sign-in the server has ended is an error, not a name read back from a file. People, roles and keys are managed in
the console at https://app.metaphi.ai/console.
Credits
Each completed trace draws your organization's prepaid credit; a run that fails costs nothing. When the balance reaches zero, new runs are refused until credit is added.
kennet credits # the balance (owners, admins and billing members)
kennet credits buy --amount 200 # pay through Stripe Checkout in the browser; waits until it lands
Only owners and billing members can buy; a billing member sees the balance and buys credit, nothing else.
Running out of credit
When kennet twin is refused with out_of_credit at a terminal, and you may buy for your organization, it offers to
add credit there and then, opens Stripe Checkout, and carries on with the same run once the payment lands:
$ kennet twin trace.json
acme is out of prepaid credit.
Add credit now? Amount in $ [100]:
Pay in the browser:
https://checkout.stripe.com/c/pay/...
Paid. acme: $100.00 prepaid credit
uploaded trace.json (48,112 bytes)
Enter takes $100; n or Ctrl-C declines. The run is retried once, under the Idempotency-Key of the first attempt, so
it can never start twice. If you may not buy, it names who can and where.
In CI, a pipe or with --json nothing is asked. The command exits 1 with the error and one more line on stderr:
Add credit: https://app.metaphi.ai/console?add=credit or run: kennet credits buy --amount <amount>
With --json the error object carries the same two things: action_url, the page, and next_step,
kennet credits buy, which prints a Stripe Checkout link and waits until it is paid.
The library never prompts: twin() raises BillingError, whose action_url is the add-credit page when the service
sent one.
Configuration
| Variable | Default |
|---|---|
METAPHI_API_KEY |
the key saved by kennet login (or kennet login --with-api-key) |
METAPHI_API |
https://api.metaphi.ai/v1/kennet, the Kennet API |
METAPHI_HOME |
~/.config/metaphi (directory mode 700, config file mode 600) |
METAPHI_TWIN_TIMEOUT |
7200: seconds kennet twin waits for its twin to end (--timeout per call) |
METAPHI_NO_KEYRING |
unset: secrets go to the OS keychain when one is available; 1 keeps them in the config file |
METAPHI_ALLOW_CUSTOM_HOST |
unset: the CLI talks only to https://*.metaphi.ai; 1 allows another https host |
Sign out with kennet logout. It ends this sign-in everywhere it is used, revokes the keys kennet login
handed out, and clears the saved keys from this machine.
Metadata
Release files for metaphi-ai 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| metaphi_ai-0.1.6.tar.gz | 29.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| metaphi_ai-0.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.1 kB
Release files / metaphi_ai-0.1.6.tar.gz
| Download URL | metaphi_ai-0.1.6.tar.gz |
|---|---|
| Size | 29.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5b580f529934bdd05710d4939a7c3b7c10b731c2ec7c9c0b41d4244d8196b286
|
|
BLAKE2b-256 checksum How to use checksums |
bcacfb47cbb3788d6756f55c047f30087d0c70ff65d88c61b4c8036e185eb542
|
| 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 Oct 12, 2026.
Transparency logRelease files / metaphi_ai-0.1.6-py3-none-any.whl
| Download URL | metaphi_ai-0.1.6-py3-none-any.whl |
|---|---|
| Size | 34.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
789baa6105e0a65087912e6db0ca86dceab7c61c9b1262e470920b626b9c8478
|
|
BLAKE2b-256 checksum How to use checksums |
b4e2fdad920d6c13beff73185e9d26e361a5f710ab75ac7b8b1f306d0ed39a3d
|
| 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 Oct 12, 2026.
Transparency log