oppx client CLI: pairing, config, terminal UI, engine, –update
Part of the developer notes; the index is in CLAUDE.md.
Client (oppx)
cargo install --path client/oppx # or: cargo build -p oppx (binary in target/debug/oppx)
oppx pair <server> <code> [--fingerprint FP | --yes] [--as NAME] [--device-name NAME] [--force]
oppx pair self # the server on this machine: no code or fingerprint to copy
oppx status [NAME] # certificate pinned ✓, token valid ✓, model ready + context
oppx [PROMPT] [--no-web] [--classic] # chat frontend (--classic: Aider's UI); unpaired: banner + how to pair
oppx -c | -r [ID] | -p PROMPT # continue latest session · resume (picker or id) · print mode (one answer)
oppx --update # update oppx (release download, or source pull + rebuild) and its engine
oppx aider [--server NAME] [--no-web] [-- aider args] # local Aider via pinned loopback proxy; never commits
oppx proxy [NAME] [--port N] [--no-web] # proxy only; prints OPENAI_API_BASE and a per-run local key
oppx search "query" [-n N] # web search via the server's SearXNG (Markdown; use /run in Aider)
oppx mcp add NAME [-e KEY=VALUE] -- COMMAND [ARGS] # add a connector (MCP server) that runs on this machine
oppx mcp add NAME --url URL [-H 'Name: value'] # ... or one reached over HTTP
oppx mcp [list] | oppx mcp remove NAME # what's added (yours and this repo's) · drop one
oppx browser [on | off | install] # the chat's headless browser: setting and install (status without an argument)
oppx unpair [NAME] [--local-only] # self-revoke on the server, then forget locally
oppx servers | oppx use NAME
oppx pair self(local_pairing): reads the backend container’s admin token withdocker inspect, asks the loopback admin API for a pairing code and the certificate fingerprint, then runs the normal pairing against127.0.0.1with that fingerprint pinned; saved aslocal. It needs Docker access, which already lets a user read that token by hand, so it grants nothing new; without it the command says to pair with a code. The code it requests replaces the one the app is showing. Tested against the gateway alone.- Config:
~/.config/oppx/config.json(0600), orOPPX_CONFIG/--config. - Agent isolation: the agent sees only the per-run local key; the device token stays in
oppx. - Testing without a GUI: pairing codes can be issued through the admin API:
curl -X POST -H "x-admin-token: $ADMIN_TOKEN" http://127.0.0.1:9091/admin/pairing. The token is in the container env:docker inspect openphalanx-backend --format '{{json .Config.Env}}'. - Terminal UI (
client/oppx/src/ui.rs,console+indicatif):- There’s an OPENPHALANX banner: large 5-row block letters at 95 columns or more, a compact 2-row version below that, with a green-to-blue gradient. It also has spinners on stderr, rounded panels, and ✓ / ! / ✗ rows.
- Output is plain when stdout or stderr isn’t a terminal, or when
NO_COLORis set. This matters because/run oppx searchoutput goes into Aider’s chat. Keep it that way. - To preview in a pty, use
script -qec "stty rows 40 cols 120; oppx status" /dev/null. The rows matter: a pty with 0 rows reports no size, so the compact banner is used.
- Startup checks run together (
preflight_inner): health, whoami and/v1/infogo out at once, one round trip of waiting instead of three over a WAN. They are then checked in the old order, so the errors read the same. A certificate that doesn’t match the pin fails the TLS handshake before any request is written, so nothing, the token included, is sent to it either way. The connect line ends with “sees pictures” or “text only”. - Coding engine (
client/oppx/src/engine.rs): users never install Aider.oppxkeeps a private engine in~/.local/share/oppx/engine/(OPPX_ENGINE_DIRoverrides):bin/uv, a uv-managed Python 3.12 (UV_PYTHON_PREFERENCE=only-managed, so a system 3.13 can’t break it), andaider-chat==agent::AIDER_VERSIONinstalled withuv tool installintotools/, withengine::EXTRAS(pypdf, fpdf2, resvg-py; see thedocumentstopic) next to it. A private copy missing an extra counts as not current and is installed again. About 740 MB.- Lookup order: the private engine at the pinned version, then an
aideronPATHat exactly the pinned version (handy on dev machines), otherwise install the private one (engine::ensure). Versions are read from the venv’saider_chat-X.Y.Z.dist-infofolder, not by importing Aider (2 s). - uv is reused when present (the private copy,
PATH,~/.local/bin,~/.cargo/bin); otherwise Astral’s installer runs withUV_UNMANAGED_INSTALLintoengine/bin, with no shell-profile changes. - The frontend runs on the venv’s
pythonnext to the real launcher. uv writes a#!/bin/shtrampoline instead of a Python shebang when the path is long, so the shebang alone isn’t reliable. - To test a first install, run with
HOMEpointing to an empty folder andPATH=/usr/bin:/bin(verified: about 7 s on this network, uv download included).
--update(client/oppx/src/update.rs) has two modes:- Release builds (
OPPX_RELEASE_TARGETset at build time by the release workflow): read GitHub’s latest release, downloadoppx-<target>.tar.gz, check it againstSHA256SUMS, unpack next to the current binary and rename over it. A 404 means no release yet. - Source builds: work on the checkout
oppxwas built from (CARGO_MANIFEST_DIR/../..), and refuse if it has local changes. Fetch and fast-forward, then rebuild withcargo install --locked --path client/oppxwhen anything was pulled, or when the commit embedded at build time (build.rs, shown inoppx --version) differs from HEAD. To test in isolation, use a scratch clone with a local bare remote andCARGO_INSTALL_ROOTset to a scratch folder. - The binary that runs is the one updated.
cargo installwrites to cargo’s bin folder; when the runningoppxis a copy elsewhere (e.g.~/.local/bin, first onPATH), a source update used to say “installed” whileoppx --versionstayed at the old version. It now copies the new build over the running file (replace_running: written beside it, renamed over it) or fails saying where the new one is, and the last line gives the version read from the installed binary. Run as in the recipe above with the copy in another folder: 2d07f09 → c1d49fc in both places. A release binary updating itself was run too (the real 0.5.1 release → 0.6.1). - Both modes then install or refresh the private engine.
- Release builds (
- Stand-in agents: a fake
aiderscript onPATH(printing its args and env) is a quick way to testoppx aiderwithout the real agent.