| title | The CLI (treg) + skill scaffolding | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| status | shipped | |||||||||
| sources |
|
|||||||||
| related |
|
cmd_feedback implements treg feedback submit <category> <message> [--call-id ID] [--endpoint-id ID].
The category choices and help description come from the lightweight feedback_contract module.
Both parent and submit help explain fields, category meanings, optional call-reference mapping,
privacy and receipt semantics; a reference mentioned only in prose is not automatically linked.
--call-id repeats; message - reads stdin. _client applies the configured registry and team,
and _feedback_request prints JSON or exits nonzero with an actionable error without echoing
rejected input. cmd_feedback_get implements treg feedback get <feedback_id>. Submission transport
failures report an unconfirmed outcome, not a definite failure. Bare treg feedback shows help;
main still accepts the original category-first submission shorthand. See feedback.
treg login --token <key> accepts an active managed human or agent key because the CLI verifies it
through /auth/me and uses the same bearer on later requests. Disable or revoke makes that check
fail before the CLI saves a new value. Complete credentials returned by login and key-minting
responses use Cache-Control: no-store. The CLI stores no key-management state. Its team selection,
org override, permissions, caps, and billing behavior still come from the live membership.
The MCP installer refuses a seven-day bootstrap credential before writing any client configuration;
the user must first choose/create/join a team and install its Default or Agent key.
cmd_review implements treg review <call_id> <usefulness> [--reason TEXT], sharing the light
contract's enum and description with MCP. It validates the reference and trimmed reason locally,
posts to /reviews, prints a receipt, and emits structured errors without echoing rejected input.
A transport failure explicitly leaves the outcome unconfirmed. _show_hint_line, beside the
charge line, prints the server's invitation (X-Treg-Hint: review|feedback; the older
X-Treg-Review: requested still means review) as one stderr line per kind. Call responses
retain the existing _show formatting on stdout, including pretty-printed JSON.
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; both last one main() call, reset on entry and exit) makes the human-table commands (org ls, agents ls, catalog in all its forms)
emit raw JSON instead — one stable contract for agents. On call (not --await) it prints one
compact envelope, {"result": <body>, "_treg": {http_status, call_id, charged_micro | reserved_micro, replay?, async?, hint?}} (_call_envelope; text as a string, binary as base64), and suppresses the
charge, hint and failure-diagnostic stderr lines: a script that merged the streams once discarded
every result it had paid for. Exit status is unchanged (1 on HTTP >= 400).
TREG_CONFIG points the CLI at an alternate config file (CI/agents/tests; default
~/.treg/config.json). org use validates the slug against /orgs, then gets that membership's
active Default key before it saves either value. If that exchange fails, the previous team and token
stay active. An Additional or Agent key cannot switch memberships; the user must first run treg login as a human. org rename --name/--slug sends PATCH /orgs/{id} (admin+); on a slug change it
rewrites active_org and leaves the token alone, since the server keeps the old slug as an alias. 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.
~/.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) and forces the credential file to owner-only
mode (0600); 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). An async submission instead says reserved up to $… for async settlement, because
its header is the open hold ceiling rather than the final charge. 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.
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 · hub
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.
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.
- 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.hub(cmd_hub_init/_run/_publish/_ls/_earnings/_price/_retire/_list/_unlist/_log) — publish a tool made of tools. See hub.whoami(cmd_whoami, visible but outside everyHELP_GROUPSrow) — the account, active team- role, and server this CLI talks to (
GET /auth/me+/orgs); added after an agent ran the systemwhoamithroughtreg withand printed the machine's user name, which reads like the treg identity.
- role, and server this CLI talks to (
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.
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).
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 ledgerbudget-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.
An org-less login result is a signed scp=bootstrap credential: it identifies the account for
onboarding, expires after seven days, and cannot call team resources. When browser login includes a
team selection, the poll result is already that membership's signed, non-expiring Default key. The
direct email-code CLI uses the response's HttpOnly browser session to request the selected team's
Default key; it does not let the bootstrap credential perform that exchange itself.
Creating a team, joining by invite, accepting a pending invite, or selecting a team replaces the
single token in ~/.treg/config.json with the returned/selected team Default key. That key is the
same deterministic credential shown for that human membership in the dashboard API Keys page, so
disable and rotation apply to CLI use and Activity attributes the real key. treg org use performs
the same authenticated Default-key exchange on every switch and writes the new team and token only
after the exchange succeeds. Old untyped login tokens remain usable during rollout; newly typed team
keys treat their signed team as authoritative for resource calls.
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.
For a typed Default key, a conflicting X-Treg-Org does not override the claim. The CLI replaces its
stored token when a deliberate team switch succeeds.
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.
main calls cli_analytics.track_command after dispatch, including failures and interrupts.
The PostHog Python SDK sends cli_command_completed with the handler's fixed command name,
exit code, success, duration in milliseconds, package version and OS. Help/version flags and
argument parsing failures exit before dispatch and emit nothing. No arguments, request/response
bodies, paths, tokens, emails or team identifiers are collected. A random UUID in analytics-id
beside TREG_CONFIG identifies an installation; it is independent of login/logout.
The public treg.to ingestion token is the default only for the hosted registry and its legacy
alias. Self-hosted URLs send nothing unless TREG_CLI_POSTHOG_KEY is set; the host override is
TREG_CLI_POSTHOG_HOST (default EU ingestion). TREG_TELEMETRY=0 or DO_NOT_TRACK=1 disables
all analytics and ID creation. SDK import and synchronous capture run in a daemon thread with
no retries, a 0.2-second request timeout and a 1-second caller wait budget. Slow delivery may
be dropped at exit; telemetry failures are silent and preserve command output and exit status.
_cost_label and _cost_usd consume the same computed display USD/unit/suffix fields as the web
pages. Grouped prices show the full block amount, and variable prices show a plus sign. The source
is provider-neutral cost.display catalog metadata. Missing metadata retains the existing format.
CLI call billing still uses the shared server call path.
resources list [--provider] [--kind] [--include-deleted] resolves the active organization and
calls GET /orgs/{id}/provider-resources. This is the platform-created resource inventory, distinct
from connections resources, which discovers accounts behind an OAuth connection. Binary treg call responses still write untouched bytes to stdout; Fish TTS examples redirect that stream to an
audio file. Existing --header and repeated --upload multipart behavior is unchanged.
The unmodified PyPI CLIs 0.16.0 and 0.19.0 can use existing saved tokens, complete browser login,
and exchange Default keys with org use. Their email flow discards the browser cookie and would
save a restricted bootstrap token. Their team-create and identity-mode invite flows keep the
previous token after selecting the new team. A scoped Default key must still reject that mismatch.
routers.auth_helpers.require_managed_cli stops these known old-client requests with HTTP 426
before issuing email credentials, creating a team, or consuming an invite. The response tells the
user to run treg update and retry. It sets X-Treg-Error: 1 so the released CLIs correctly
identify treg as the source of the refusal. Current CLI requests send X-Treg-Key-Protocol: 1 and save the
returned team's key. The legacy-client hint is the released CLI's python-httpx/ User-Agent plus
ngrok-skip-browser-warning: 1, without that protocol marker. It is a compatibility check, not an
authorization boundary or a universal client-version detector. Browsers and generic API clients
retain their API behavior; omitting or forging the hint never relaxes token restrictions.
Existing unscoped tokens retain their old team-create behavior. Fresh email login and team changes
with typed credentials require the updated CLI on the affected paths. This is a controlled upgrade
requirement, not full support for all fresh-login flows in old clients. The released-wheel test in
test_released_cli_compat checks that refusal preserves config bytes and the prior usable team.
cmd_host implements treg host <file> [--content-type TYPE] [--json]: one POST /media with the
file's bytes and a type guessed from the extension, printing the public URL alone on stdout (size
and expiry go to stderr) so $(treg host face.jpg) drops straight into a --data body. 30 MB per
file, 7-day TTL, image / audio / video only, free. See
media. treg call <catalog-id> <path> (the own-tool shape applied to a
catalog id) now answers 400 naming the endpoint's parameter slots instead of "no tool in this org".