The treg CLI
September 5, 2026 · View on GitHub
Instagram grants
TREG_OAUTH_REVIEW_PENDING and provider-registry metadata control the CLI's effective default and
guidance. With both Instagram keys pending, treg connections connect --provider instagram starts
the approved Facebook Page core flow. Direct Instagram Login remains explicit with --capability manage, and Page messaging remains explicit with --capability page-messages. Removing the Page
message key makes the full Page grant the default. Removing the direct key restores direct Instagram
Login as the default. Before consent starts, POST /oauth/start returns the effective guidance and
the CLI prints it. The CLI contains no provider-specific review logic.
For a catalog endpoint that supports both grants, treg call <endpoint> --authorization-method <method> selects the grant, upstream host, route, and method-specific
required inputs. The CLI accepts any method name; the API validates it against endpoint metadata.
Omitting it uses the endpoint's first declared method.
For a dual-method endpoint, omitting it first uses the sole connected grant when there is one;
otherwise the ordered default is Instagram Login. Static catalog templates therefore omit the flag
for dual-method endpoints, while dashboard-generated commands include the user's resolved choice.
A thin client over the API in src/treg/cli.py — every command is one HTTP call, no logic of its own
(stdlib argparse, reuses httpx). Entry point main() → build_parser() → dispatch to a cmd_*.
Every command/subcommand carries a description + help on each argument + a copy-paste Examples
epilog (a mk() helper + _ex() + RawDescriptionHelpFormatter), so treg <cmd> -h is self-teaching.
treg --version / treg version print cli_version() (package metadata); treg update (cmd_update)
re-runs the server's install.sh to upgrade the CLI in place. A global --json flag (stripped in
main like --org) makes the human-table commands (org ls, agents ls, catalog in all its forms)
emit raw JSON instead — one stable contract for agents; commands that already print JSON are unaffected.
TREG_CONFIG points the CLI at an alternate config file (CI/agents/tests; default
~/.treg/config.json). org use validates the slug against /orgs before persisting (a typo'd slug
exits naming your real teams; offline degrades to set + warn), and the server's "choose an org" 400 is
followed by a stderr line naming the bad --org/active-org value.
Every command builds its client via _client(cfg), which returns a _RegistryClient (an
httpx.Client subclass). It survives an upstream WAF: when a request's body is 403'd by an edge (a
403 whose response body is an HTML block page, never treg's own JSON 403s), it re-sends the request
once, base64-encoded with X-Treg-Body-Encoding: base64, which the server decodes (see
api). This is what lets treg upload push SQL-bearing recipes and treg call proxy
SQL/HTML bodies through Cloudflare/Render. Transparent — no effect on any request that isn't blocked.
_active_org_id(cfg, c, strict=True) resolves the active org's numeric id via GET /orgs, falling
back to /auth/me when that 403s — which is what a MACHINE identity gets, since the server refuses it
there on purpose. An invalid token now exits naming the real problem instead of the bare "no active
org" that 21 commands used to print. strict=False is for callers that only ENRICH output (the pin
marker on catalog get): the catalog is public, and sys.exit raises SystemExit, which
except Exception does NOT catch — so a try/except at the call site would not have saved that page.
Config + client (identity-first)
~/.treg/config.json (CONFIG_PATH) is v2: {base_url, token, email, active_org, identity, admin_token}
— one bearer token + an active org slug (_load_config migrates a legacy multi-org or flat config on
read, and tolerates a corrupt file as empty so a half-written config can't brick every command).
_save_config writes atomically (temp + os.replace); login persists the token before the
best-effort _pick_active_org lookup, so a transient /orgs failure can't discard a freshly-minted
token. _pick_active_org prefers the server's active flag, then the org a team-pinned identity token
bakes into its claim (_token_org_claim decodes it locally, unverified — covers older servers that mark
nothing active for such a token), and only then the first membership — an arbitrary team for a
multi-team user, so it is the last resort, not the default. call --query is relayed as a list of pairs (duplicate keys survive) and rejects a value with no
=; contract_to_skill_payload/load_contract raise clear errors (naming the entry/file) for a
stale/malformed treg.json instead of a bare traceback. Every "read a file / parse inline JSON the
user pointed at" path (oauth connect, tool add/update --binding|--health, skill push|scaffold|init,
secret add --dir) exits cleanly instead of tracebacking (_load_json_arg + guards); _parse_bind
rejects a non-int secret=; a one-shot --org <slug> override no longer wipes the stored active org on
leave/delete (_clear_active_if_targeted); oauth connect exits non-zero on a failed/timed-out
connect. _client(cfg) sends X-Treg-Token: token plus X-Treg-Org: <active_org> (the header is ignored
for a per-org token, and picks the org for an identity token). _effective_org applies the global
--org override; _active_org_id resolves the active org's numeric id via GET /orgs (for
/orgs/{id}/... endpoints). _admin_client uses admin_token else the bearer. _show pretty-prints +
exits non-zero on HTTP >= 400, and on >= 400 first prints one stderr line (_show_failure_diagnostics,
2026-09-05): the HTTP status, whose answer it is (X-Treg-Error: 1 = treg refused; absent = the
provider answered and treg relayed it unchanged), the X-Treg-Call-Id to quote to support, and the
X-Treg-Cost-Micro charge when the header is present. stdout stays the exact body — a runner that
saved only stdout filed 115 relayed Moz quota 403s as a bare "cli_error" with no status or id. A
metered 2xx gets the matching line (_show_charge_line: treg: charged \$0.006667 · call id …,
replay-aware); no cost header (own key, non-call response) → nothing extra.
Per-process identity: TREG_TOKEN (+ optional TREG_ORG) in the environment beat
~/.treg/config.json, so each coding agent on one machine can run as its own scoped agent —
set them in the runtime's env (Claude Code settings env, Codex config, a project .env) and the
config file stays the human's. This is what makes the dashboard's "Scope this agent" promotion
real on a shared machine (the same per-process pattern other agent credential gateways use).
The override never touches the config file, so treg login can't accidentally persist it.
The top-level IA (treg --help)
build_parser() returns a _GroupedHelpParser whose format_help renders the front page from the
HELP_GROUPS table (five groups, fixed order) instead of argparse's flat alphabetical wall — so the
listing is curated copy, not registration order. Anything not in HELP_GROUPS still parses; it is
simply absent from every listing, which is how the back-compat aliases survive. A subparser is hidden
by the alias() helper, which just omits help= (argparse only lists a subparser that has one);
mk() is the visible form. Subcommand -h is untouched — the top-level add_subparsers pins
parser_class=argparse.ArgumentParser, because argparse otherwise clones the parent's class into
every subparser and treg call -h would print the grouped front page instead of its own help.
THE CATALOG — tools you don't have a key for catalog · call · balance · topup
YOUR OWN TOOLS — what your team already has tool · skill · secret · connections
ON YOUR MACHINE — team credentials, locally cli · with · serve
BULK UPLOAD scan · upload
TEAM MANAGEMENT audit · org · invites · accept · agents · admin
CONFIG config · login · logout · onboard · update · version
The order is the pitch: what you can do with no setup comes before what you have to register
yourself, and balance/topup sit next to the thing that spends them rather than under team
management. test_help_is_grouped_and_hides_aliases pins both the order and that catalog precedes
tool, so a drift back to vault-first fails the suite.
Old → new. Every one of these still parses and routes exactly as before — hidden, not removed:
| old spelling | canonical now | note |
|---|---|---|
treg add … | treg tool add … | one spelling for registering a tool; cmd_add's name-or-id --secret sugar is unchanged |
treg oauth connect … | treg connections connect … | same flags (name, --provider, --capability, --client-secret, --scopes) |
treg oauth providers | treg connections providers | |
treg run … | treg cli run … | |
treg runs | treg cli runs (or treg audit --runs) | |
treg calls | treg audit --calls | |
treg shell start|stop | treg cli shell start|stop | |
sudo treg setup-local-run | sudo treg cli setup | |
treg import | treg upload | pre-existing alias |
treg health | — | still works; dropped from the front page, connections carries the health story |
Bare treg connections now lists (the subparser is required=False with a parent
set_defaults(fn=cmd_connections_ls)); every other namespace still requires a subcommand.
Commands
- Team policy + scoping (all under
org, all admin+):org agent-new <name>(--role,--cap,--tools,--all-tools,--local-run,--projects a,b/--all-projects— only sent when given, so a rotate never widens scope) mints or rotates an agent's token — a member identity for a machine caller (POST /orgs/{id}/agents). Re-running the same name rotates: the previous token dies there.org agentslists them with today's usage;org agent-rm <user_id>revokes. Nested underorgon purpose — the top-leveltreg agentsalready means "which coding agents can I install skills for", an unrelated concept (agents.py). See multi-tenancy.org pin <capability> --provider <p>/org pins/org unpin <capability>— the team's provider choice per job, enforced server-side (a gate, not a hint).catalog getshows the pin and lists only that provider's endpoints, so an agent does not learn the policy by being refused. See catalog.org deny(--host,--path,--method,--user,--project <slug|id>,--note) blocks calls for the whole team, one member/agent, and/or one project's tools;org deny-ls/org deny-rm <id>. An empty field means any, so--method DELETEalone blocks every delete. Enforced on the proxy AND both run tiers — see proxy-model.org project-new <name>/org projects/org project-rm <id>manage the optional sub-scope inside a team.org accessandorg inviteadditionally take--projects a,b/--all-projects, which ride alongside the existing--toolsflags (the two ACL axes compose as AND). Deleting a project frees its tools back to team-wide rather than hiding them.
config(--base-url; shows email + active org + logged-in) ·login— three doors in onecmd_login: default browser handshake —POST /auth/cli/startmints thelogin_idand a short pairing code, opens the universal/login?cli=<id>#code=<code>page — the code rides in the URL fragment (a fragment is never sent to the server, so it stays out of request logs) and the/loginpage displays it so the user just confirms it matches the terminal instead of typing it (the anti-phishing guard: a login you didn't start can't be approved into a token, and the server still validates the code at approve time — the guard itself is unchanged). The page reuses an existing dashboard session via a team picker, else offers GitHub / Google / email-code, then polls/auth/cli/pollwith no code; the poll result may carryactive_org= the team picked in the browser, whichcmd_loginadopts directly, falling back to_pick_active_orgonly against an older server (where/start404s → a locally-mintedlogin_id, no code)),login --email you@x.com(terminal-only email OTP:POST /auth/email/start→ prompts for the 6-digit code →/auth/email/verify, storing the identity token), orlogin --token <t>for agents/CI — which now verifies the token via/auth/mebefore saving (a rejected token exits loudly instead of the old misleading "Token saved"; a valid token whose user has no team yet prints a "create a team first" hint rather than a silentActive org: None). First login by any door also registers you (the user only — no auto personal org) ·logout(clears creds). After a first human login,_maybe_offer_onboardingprompts[Y/n]then shows the 3-path menu (TTY-only).onboard(cmd_onboard,--path setup|access|demo/--source local|global|both/--name/--yes/--reset;--modehidden, back-compatquick→demo) — a TTY run opens with a one-second_splashdecrypt animation (the wordmark reveals behind a ░▒▓ wavefront; any key skips; off-TTY/NO_COLOR/dumb terminals never see it), then_pick_pathpresents an arrow-key menu (_menu— ↑↓/jk move, ↵ confirm, 1-9 jump-pick; falls back to questionary where raw-key mode is unavailable). The interactive default is Set up; the smart org-based default (team-with-tools → Access, empty admin team → Set up, else Demo) applies only non-interactively. Set up (_run_setup) asks "Import skill/secret from where?" — this project / global agent folders~/.claude/skillsetc. / both / an other project repo typed inline (a_menutype-in row with fish-style folder autosuggestion; "this project" is hidden from a root-ish folder via_is_rootishso it can't sweep$HOME), unless--sourcepins it — then imports the chosen.env+ skills and runs a batchedhealth --run. Access (_run_access, list tools+skills → multi-selectskill install→ a no-key test call). Demo (_run_demo) is now purely illustrative — no team is created, nothing is uploaded — showing the loop across four beats (scan preview → roles → a real no-key call if the active team has a callable tool → the audit log). See onboarding.invites(cmd_invites→GET /invites/mine) lists invites addressed to your proven email;accept <org-slug>(cmd_accept) accepts one code-free (finds it in/invites/mine,POST /invites/{id}/accept, sets it active). The code path stays asorg join <code>.org—create "<name>"(become owner),ls(marks the active one),use <slug>(switch active),invite <email> [--role viewer|member|admin] [--expires-days N] [--tools a,b | --all-tools] [--local-run on|off](admin+; prints the one-time code;_resolve_tool_accessoffers an all-or-customise checklist prompt when neither flag is given on a TTY),access <user_id> [--tools a,b | --all-tools] [--local-run on|off](cmd_org_access, admin+; sets which tools a member may use + the local-run toggle, keeping the unspecified field's current value →PATCH /orgs/{id}/members/{user}/access),invites(admin+; lists live pending, purges expired),revoke <invite_id>(admin+),members(admin+; each row now carriestool_access+local_run_enabled),set-role <user_id> <role>(owner-only),join <code> --email you@…,leave,delete <slug>(owner-only; must name the org — confirm-by-name). A global--org <slug>flag (stripped inmainvia_pop_org_flag, applied through_ORG_OVERRIDE/_effective_org) runs any command in that org instead of the active one. See multi-tenancy.secret add(name;--value|--env-var VAR [--env-file PATH]|--file|--dir;--kind) ·secret ls·secret rm·secret update ID(--name/--value/--kind→PATCH /secrets/{id}; only the given fields).--dirauto-discovers the file viaconvert.find_secret_file; a file-sourced value (--dir/--file, and thetreg.jsoncontract secret read incontract_to_skill_payload) is now.strip()ed, so a trailing newline can't become an illegal header/env value downstream.--env-varreads ONE named var from an.env(default./.env) viaproviders.env_values— the correct, value-internal way to register an unmatched key: it strips a balanced quote pair (soKEY="v"storesv, not"v"— the malformation agents hit hand-extracting with grep/cut) and the value never lands on the command line.add(hidden alias) — the old friendly shortcut fortool add:treg add <name> --base-url URL [--secret <name|id>](cmd_add).--secretaccepts a secret name (resolved to its id via_resolve_secret_ref) or an id; default injection is a Bearer token in theAuthorizationheader.--header/--formatoverride it;--basealiases--base-url. Kept for old scripts and cached agent instructions;tool addis what we teach.tool add(name;--base-url; single-binding--secret/--injector/--auth-*/--secret-field; friendly multi-binding--bind 'secret=ID,injector=oauth,name=...,format=...'parsed by_parse_bind; raw--binding '<json>';--health '<json>') ·tool ls·tool rm·tool update ID(--base-url/--bind/--binding/--health→PATCH /tools/{id}).import [env|skills|clis](--dir/--env-file/--skills-dir;--select a,b|--all|--dry-run;--status;--replace;--no-oauth;--llm…) — scan a directory/machine and register what it finds; bare = all three (env + skills + clis). env: detect provider keys → secrets + tools (bearer/api-key/query/basic auto, OAuth pairs a per-provider connect,--llmfor unknowns). skills: each skill subdir → a tool (fromtreg.jsonor generated from its script/secret) or a recipe-only bundle. clis: scan the machine for INSTALLED catalog CLIs (shutil.which), classify each (providers.classify_cli), auto-register the ready ones on the right tier (server-injected key / local secret-less, stored as an explicitinject: []so the catalog's inject can't merge back at grant time), and print an actionable, plain-text gap report (no emoji/colour, one CLI per line) — every missing piece names the fix (set STRIPE_API_KEY/run gcloud auth login); fix + re-run (idempotent;--status/--dry-runreport only).--add BINregisters an INSTALLED cli that's NOT in the catalog (prompts for its key env var + API base_url) and prints a catalog-entry snippet to share; an unknown bin isn't server-allow-listed, so it runs locally until an admin allow-lists it. Brains inproviders.py+skills.py; see env-import.call(target, optionalpath;--method,--query K=Vrepeatable,--data,--file,--content-type,--header 'K: V'repeatable) → three shapes: namedcall <tool> <path>, agent-native single URLcall https://host/full/path(path omitted), or a catalog endpoint idcall tikhub.tiktok.video.comments— all hit/call/<rest>.--headeradds an extra request header the binding can't know (e.g. Google Ads' per-calllogin-customer-id); an injected credential always wins, so a--headercan never overwrite the secret the proxy injects. The endpoint-id shape (dotted, slash-free; an org tool with the same name always wins) walks the marketplace credential ladder server-side (_resolve_marketplace_callin api.py): ① an org tool for the provider (via passthrough resolution, so ACLs apply unchanged), ② an org secret tagged with or NAMED for the provider, injected through a virtual, never-persisted tool named after the endpoint (audit records the endpoint id;tool lsstays clean), ③ an actionable 404 naming theconnections connect/secret addfix — reached only after ④ treg's own key, which serves anyplatform_eligibleendpoint (priced + provenanced + live-verified) of an allow-listed provider (TREG_PLATFORM_PROVIDERS, the kill switch) with no credential at all, metering it against the team's prepaid balance: reserve before the request, settle on a billable answer (a provider-reported cost wins over our estimate), release on a 5xx/network failure, per-org daily ceiling, and a 402 carryingbalance_micro/estimated_cost_micro/topup_urlwhen the balance is short. Tier ④ is shadowed by ① and ② (an org that brought its own key is billed by the provider, not by us) and never resolves for a demo org.treg catalog getprints which tier would serve you. The server validates method + required params BEFORE money is spent, and fills{placeholder}path params from--query(consumed — dropped from the relayed query viarelay(drop_params=…)). Members restricted via--toolsget no marketplace calls; a bare provider name (call tikhub /path) still 404s but points at the endpoint-id form. See cli-audit-2026-07-28 (design section).-pis a short alias for--query.catalog getprints the whole contract an agent needs to build the cheapest valid request: the PARAMS table flattens nested objects to dotted names and its NOTE column carries the prose rule, the enum (one of:), the default, the numeric range and the example; acost.tableendpoint gets a PRICE TABLE section (_print_price_table: the rows,× field (from times_min)for linear rows, the fallback ceiling, and whether the row or the provider's usage settles); an async endpoint gets an ASYNC TASK section (_print_async: id field, poll command and interval, terminal values, result location or retrieval command, lifetime) and its RUN IT template ends in--await --timeout 900.--await [--timeout 900]readsX-Treg-Async; without the header it is a no-op. Descriptor semantics come fromtreg.domain.asynctasks(stdlib-only, see the import-boundaries fragment), not a CLI-side copy. With the header it prints the task id and a resumabletreg callcommand to stderr, polls static catalog ids or allow-listed dynamic URLs through/call/, retries network/5xx failures with backoff up to five consecutive failures, and keeps waiting on unknown status values after one warning. Stdout contains only the terminal polling response bytes. Exit codes are 0 for success, 2 for a provider terminal failure, 3 for timeout/interruption/recoverable polling failure, and 1 for malformed usage or metadata. Fetch-mode results print a retrieval command rather than downloading binary content; result URLs, reservations, progress, and TTL reminders stay on stderr. Running that command preserves a non-text response byte-for-byte on stdout (for example,treg call openrouter.video-gen.result.retrieve -p video_id=... > result.mp4); the upstreamContent-Typeremains authoritative, so the async descriptor does not duplicate a result format.audit(cmd_audit,--limit,--calls|--runs) — the single "who did what" view.--callsand--runsdelegate tocmd_calls/cmd_runsverbatim (the oldtreg calls/treg runsoutput, byte for byte). The default merged view is the only new behaviour in the consolidation: it fetches bothGET /callsandGET /runs(no new endpoint), normalises each row to{kind, id, user_email, tool, detail, result, where, created_at}(plustask-status,settled_micro,result_url,fetch_command,ttl_note- when/callsreports anasync_taskfor the row, i.e. a metered generation), sorts bycreated_atdescending and truncates to--limit. It drops thekind == "local_run"CallRecords, because/runsalready surfaces those same grants as itswhere: "local"rows — otherwise every local run would be listed twice. Call ids are prefixedc…; run ids keep/runs' owns…/l…prefixes, so nothing collides.cli run(treg cli run <tool> [--local|--server] [--] <cli args…>,cmd_run) — a dispatcher that picks a tier by flag:_run_local(default) or_run_server.argsis anargparse.REMAINDER, which silently swallows a treg flag typed AFTER the tool name;cmd_runguards against that by reading the realsys.argvand refusing a tier flag (--server/--local/--timeout) placed after the tool but before the--separator — while still letting a flag after--reach the vendor CLI (sotreg run db -- --timeout 30works and passes--timeoutto the CLI). Two execution tiers (see CLI-RUN-PLAN):--local(default,_run_local) — run the vendor CLI on THIS machine as a dedicatedtreg-runuser so the credential is unreadable by the member (see local-run). On Linux with local-run set up, the member hands off viasudo -u treg-run <runner>, passing its own token through the environment; the runner (cmd_run_helper, the hidden__run-helper, running as treg-run) fetches the grant (POST /tools/{name}/grant), runs the CLI with the credential, tees stderr to match the profile'serrors→ a translated message +run-report(verdict enum only), and passes the exit code through. Shared core_run_helper— which, on a shared-key run (grant setsredact_output), scrubs the injected value out of the CLI's stdout/stderr via_StreamRedactor(boundary-safe streaming). Without setup it runs as the member, best-effort, with a warning.cli setup(cmd_setup_local_run, run once with sudo — now Linux AND macOS; macOS creates the treg-run user viadscl/_create_run_user) creates the treg-run user, installs the runner (root-owned, can only invoke__run-helper), writes a narrow sudoers rule, and installs the egress allow-list (_install_egress→ local-run;--no-egressskips it,--refresh-egressre-resolves drifting IPs,--registrysets the host to allow). Its--run-proofflag installs the runner proof at/etc/treg-run/proof(root-owned, mode 0400, readable only by treg-run); the runner script exports it asTREG_RUN_PROOFand_run_helpersends it asX-Treg-Run-Proofon the grant call — which is how the server releases a SHARED (non-owned) key to the isolated runner but refuses a direct member call (without--run-proof, only owned-key tools run locally).treg tool update <id> --local-run on|offflipscli.enabled.--fs-jail(opt-in, macOS) confines the CLI's file writes to a private scratch (fsjail.macos_profile+sandbox-exec, forwarded viaTREG_RUN_FSJAIL) so it can't drop the key in a member-readable file — see local-run.--server(Tier 0,_run_server) — run a runnable skill's CLI on the registry server (POST /run,--timeoutcap 600), secrets injected server-side, stdout/stderr + exit code streamed back.cli runs(cmd_runs,--limit) shows the run audit log — now BOTH tiers:GET /runsmerges server runs and local grants, each taggedwhere(server|local; a local success has a null exit code, since only failures report back).treg audit --callsshows the localGRANT/DENY/REPORTaudit rows.
cli shell(cmd_shell_start/cmd_shell_stop) —cli shell startopens a subshell where the team's registered CLIs run with the credential injected transparently (a shim dir first onPATHroutes each totreg run);cli shell stop(orexit) leaves.--server-for a,broutes named tools to the server (key never on the machine, ifserver_runnable),--ttl MINauto-closes.--proxy(opt-in, with--proxy-port/--renew-ca) additionally catches HTTPS calls the AGENT makes on its own to a registered host —_start_local_proxyseeds the allow-list from the tool listing already fetched for the shims and handsstart_sessionthe environment + a stop callback. See shell and local-proxy.treg <command>(cmd_with, reachable astreg withor bare) — run ONE command with the team's credentials:treg claude,treg node app.js,treg with -- npm test. treg is the parent, so only that process and its children are affected and nothing is written to any config file.main()routes a bare word here via_looks_like_a_program()— the word must not be a treg subcommand AND must exist onPATH, sotreg toool lsstays an argparse error and a straycallbinary cannot shadowtreg call. See local-proxy.serve(cmd_serve_start/_stop/_status/_env) — the same local proxy as a background service, for a member who wants their own shell rather than a subshell.startdetaches a child runningserve start --foreground(viasys.executable -c, never thetregonPATH, which may be an older build);eval "$(treg serve env)"points a terminal at it and--unsetreverses it (and works afterstop, which is exactly when it is needed);statusreads~/.treg/proxy/proxy.jsonand says whether THIS terminal is using the proxy._start_proxy_handleis the one code path all three front doors share.admin(super-admin, cross-tenant):login --token,stats,orgs,org <id>,users,tools,calls,health,grant/revoke <user_id>,suspend-user/rm-user <user_id>,suspend-org/rm-org <org_id>. Reconciliation (price drift, provider spend, repeat-query rate) is deliberately NOT a CLI command — queryGET /admin/reconcile/*directly or runscripts/provider_balances.py; seesrc/treg/reconcile.py._admin_clientsends the savedadmin_token(treg admin login) or falls back to the active org token (works for anis_superadminuser). See super-admin.skill(init --dir,add --dir,scaffold <dir> [--out],push <file>,ls,rm) — see below.health(--run) — still parses; off the front page since the consolidation.connections(cmd_connections_ls/_resources/_use/_rm, pluscmd_oauth_connect+cmd_oauth_providers, which kept their names when the namespace moved) — your connected accounts, and where you connect a new one. Baretreg connections(orls) →GET /connections(health + expiry).connect(cmd_oauth_connect, ex-oauth connect) has two modes: registry —--provider <service>(e.g.google-search-console), optional--capabilityto pick a scope set (default read) and optionalname, so treg's app supplies the client credentials; or bring-your-own —name --client-secret <file> --scopes …reads your own Google OAuth client JSON (_byo_body). Either posts/oauth/start, prints the consent URL, and polls/oauth/status~5 min.providers(cmd_oauth_providers→GET /oauth/providers, ex-oauth providers) lists the services treg holds its own approved OAuth app for.resources <id>(GET /connections/{id}/resources— the sites/properties/accounts it can act on),use <id> <resource_ref>(POST /connections/{id}/resource— select which one),rm <id>(DELETE /connections/{id}— disconnect). The oldoauthnamespace stays as a hidden alias ofconnect+providers.catalog [platform](cmd_catalog) — the endpoint catalog: what you can CALL, as opposed toconnections providers, which is what you can CONNECT. No arg →GET /catalog/platforms, one aligned row per platform (endpoints / verified / capabilities / the providers serving it), busiest first. With a slug →GET /catalog/platforms/{slug}, grouped by capability so the same job across providers sits under one heading (provider METHOD path cost verified-date|unverified tier), with any unmapped extended endpoints last._cost_labelrenders the catalog's cost block as a scannable$0.001/success. Unauthenticated (_client(cfg, auth=False)) — the catalog is public, and it's the one thing worth reading before signing up. The CLI never parses the YAML itself (that needs pyyaml, a server extra); see catalog.catalog search <query…> [--limit](_catalog_search) — find an endpoint by what it DOES when you don't know which shelf it sits on (GET /catalog/search). A compact ranked table: endpoint id, platform, provider, cost,✓/·verified, tier, clipped summary — footer hinttreg catalog get <id>. Cost prints in USD (_cost_usd, 3 significant digits) rather than_cost_label's source currency: the column is only worth reading if CNY and USD rows compare. The no-match message namestreg catalog request "<query>"— the empty search is the moment the filer exists.catalog request <what's missing…>(_catalog_request) — file a "the catalog doesn't have X" report (POST /tool-requests,source: cli). Open endpoint, rate-limited server-side; a configured token rides along as attribution only, never a requirement.catalog get <endpoint-id>(_catalog_get) — the last stop beforetreg call(GET /catalog/endpoints/{id}): summary, provider + limits/pricing_url, cost in USD and the original currency with its note, verified date, the capability with a siblings table (same job, other providers — price and verification side by side), aPARAMStable by location with required first (_print_params), the paste-readyRUN ITcommand, and the example response pretty-printed, clipped at 40 lines with a pointer to the full JSON.search/getare matched as positional verbs insidecmd_catalog, not argparse subcommands, sotreg catalog <platform>keeps working and a multi-word query needs no quoting. An id that misses prints the server'sdid_you_meanids and the exact command for the first one; the old "find one with: treg catalog search …" is the fallback for a miss that resembles nothing, since it sends the reader back to the step that produced the wrong id. The siblings table'sWORKScell shows a measured0%for an endpoint whose decided calls all failed; below the sample floor it stays the neutral— (n), because the floor publishes volume and never outcome.mcp grants/mcp use-team <grant> <team>(cmd_mcp_grants,cmd_mcp_use_team) — which MCP connections this account has authorised, and which team's balance each one spends from; and moving one, without reconnecting the client. The grant id prints whole while every other column is clipped — it is the argumentuse-teamtakes, and clipping it made the one command this table exists to feed answer 404 for anything copied off the screen. The team was chosen once at a consent screen and then appeared nowhere: an agent reports a slug,treg org lslists the teams of whoever is signed in here, and those can be two different accounts. See mcp-oauth.
_parse_bind defaults every field to a bearer Authorization header; only secret= is required, so a
multi-credential tool needs no JSON.
Skill scaffolding + the treg.json contract (convert.py)
scaffold_skill(dir) walks a skill directory (_SECRET_DIRS = .secret/.secrets, _RECIPE_FILES =
SKILL.md/README.md) and emits a /skills manifest stub: the recipe + every credential file as a
secret (kind guessed by _guess_kind — JSON with refresh_token → oauth, other JSON → secret_file,
plain → env), and a tool with base_url and per-binding placement left as FILL for the agent to
complete. find_secret_file(dir, kind) (used by secret add --dir) matches by _matches_kind and
returns exactly one file or raises (none / ambiguous).
The treg.json sidecar contract (CONTRACT_FILE) lets a skill self-describe its registration so
treg is one command. generate_contract(dir) is the semi-automation helper: it auto-discovers secrets
(+ kinds), _guess_base_url(dir) scans SKILL.md + *.py for the upstream host (skipping doc hosts via
_DOC_HOST_HINTS), and emits non-colliding bindings via auto_bindings (shared with the dashboard's
_classify): the primary oauth/bearer token → Authorization: Bearer, each additional credential → its
own filename-derived header (developer_token → developer-token), and OAuth app config
(client_secret.json, _is_app_config) is skipped. _oauth_secret_field detects Google's token vs
access_token. base_url: a skill-name catalog match (providers.match_skill, e.g. google-ads/gsc) wins
over the _guess_base_url heuristic; whatever it still can't resolve confidently is listed in _fill for
the user. treg skill init --dir writes it; treg skill add --dir registers it
(load_contract + contract_to_skill_payload load the named secret files → POST /skills, so no secret
values live in the file; a file: path is resolved via resolve_secret_path, which swaps .secret↔.secrets
when the exact spelling is absent, so a shared contract survives the per-machine secret-dir spelling drift). secret add --dir syncs back into an existing treg.json
(_sync_contract_secret) so CLI-driven changes stay authoritative. treg skill install <name>
(or --all, --dir) does the reverse — pulls a bundle from the registry (GET /bundles/{id}) and
writes <dir>/<name>/SKILL.md PLUS its companion files (_write_bundle_files reconstructs the whole
folder from bundle.files — reference docs, scripts, nested subdirs; each path re-checked to stay inside
the skill dir, secrets never shipped), so a teammate installs a complete shared skill with one command;
a tool-backed skill notes its registered tools to call via treg call. A skill folder that already
exists on disk is kept, not overwritten (unless --force); the run ends with an actionable summary
of the kept skills + the --force hint, so a caller (agent or human) decides whether to overwrite —
the Access agent-instruction defers to this output rather than restating the rule. The push side (build_payload /
contract_to_skill_payload) collects those files via skills.collect_files (excludes .secret*,
SKILL.md, treg.json, VCS/build junk, binaries, oversized files).
Caller tags and per-tag budgets
For a builder reselling treg to their own users (see api and money).
# a token pinned to one customer — the pin beats whatever X-Treg-Meta the holder sends
treg org agent-new cust-a-bot --pin customer=cust_A
--pin is repeatable and survives a rotate: re-minting the same name replaces the token and keeps the
pin, because a rotate must never silently unpin a scoped token.
Two commands for a team reselling treg:
treg org budgets # every per-tag limit you've set
treg org budget-set customer cust_8123 --daily 5 # cap one of your users at \$5/day
treg org budget-set workspace ws_9 --daily 50 # budgets STACK — both apply to a call
treg org budget-set customer cust_8123 --block # cut one off; the caps survive
treg usage --by customer --days 30 # what each one consumed, from the ledger
budget-set only writes the limits you name, so --block never wipes a cap someone set last week.
Caps are advisory — concurrent calls can overshoot slightly — and the prepaid balance is the hard
limit; don't resell them to your users as exact.
treg login pins its token to your active team
The token login stores is an identity token — it names a person, not a team — but the CLI
re-mints it with the active org baked into the claim (GET /auth/cli-token with X-Treg-Org, the
same mechanism behind the dashboard's "your API key"). treg org use re-pins on every switch.
This matters because the token is the thing people copy out of the CLI: into curl, into an MCP
client's Authorization, into an agent's environment. Unpinned it fails there with
choose an org (send X-Treg-Org) — accurate, and useless, because the CLI had been supplying that
header invisibly all along.
Switching teams is unaffected: an explicit X-Treg-Org header always beats the claim.
treg org overflow [on|off] shows or sets the team's overflow-relay opt-out (PATCH /orgs/{id}/settings
platform_overflow); see ops/capacity.md.
treg catalog get <routed id> prints the ROUTING PLAN (order, accepted identity, price, HIT, expected
cost per hit) above the sibling table; the sibling table itself gained a HIT column (stats.observed
hit_rate). treg catalog <platform> rows lead with the endpoint id and show the unified USD price.