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 spellingcanonical nownote
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 providerstreg connections providers
treg run …treg cli run …
treg runstreg cli runs (or treg audit --runs)
treg callstreg audit --calls
treg shell start|stoptreg cli shell start|stop
sudo treg setup-local-runsudo treg cli setup
treg importtreg uploadpre-existing alias
treg healthstill 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 agents lists them with today's usage; org agent-rm <user_id> revokes. Nested under org on purpose — the top-level treg agents already 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 get shows 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 DELETE alone 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 access and org invite additionally take --projects a,b / --all-projects, which ride alongside the existing --tools flags (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 one cmd_login: default browser handshake — POST /auth/cli/start mints the login_id and 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 /login page 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/poll with no code; the poll result may carry active_org = the team picked in the browser, which cmd_login adopts directly, falling back to _pick_active_org only against an older server (where /start 404s → a locally-minted login_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), or login --token <t> for agents/CI — which now verifies the token via /auth/me before 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 silent Active 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_onboarding prompts [Y/n] then shows the 3-path menu (TTY-only).
  • onboard (cmd_onboard, --path setup|access|demo/--source local|global|both/--name/--yes/ --reset; --mode hidden, back-compat quick→demo) — a TTY run opens with a one-second _splash decrypt animation (the wordmark reveals behind a ░▒▓ wavefront; any key skips; off-TTY/NO_COLOR/dumb terminals never see it), then _pick_path presents 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/skills etc. / both / an other project repo typed inline (a _menu type-in row with fish-style folder autosuggestion; "this project" is hidden from a root-ish folder via _is_rootish so it can't sweep $HOME), unless --source pins it — then imports the chosen .env + skills and runs a batched health --run. Access (_run_access, list tools+skills → multi-select skill 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_invitesGET /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 as org join <code>.
  • orgcreate "<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_access offers 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 carries tool_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 in main via _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/--kindPATCH /secrets/{id}; only the given fields). --dir auto-discovers the file via convert.find_secret_file; a file-sourced value (--dir/--file, and the treg.json contract secret read in contract_to_skill_payload) is now .strip()ed, so a trailing newline can't become an illegal header/env value downstream. --env-var reads ONE named var from an .env (default ./.env) via providers.env_values — the correct, value-internal way to register an unmatched key: it strips a balanced quote pair (so KEY="v" stores v, 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 for tool add: treg add <name> --base-url URL [--secret <name|id>] (cmd_add). --secret accepts a secret name (resolved to its id via _resolve_secret_ref) or an id; default injection is a Bearer token in the Authorization header. --header/--format override it; --base aliases --base-url. Kept for old scripts and cached agent instructions; tool add is 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/--healthPATCH /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, --llm for unknowns). skills: each skill subdir → a tool (from treg.json or 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 explicit inject: [] 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-run report only). --add BIN registers 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 in providers.py + skills.py; see env-import.
  • call (target, optional path; --method, --query K=V repeatable, --data, --file, --content-type, --header 'K: V' repeatable) → three shapes: named call <tool> <path>, agent-native single URL call https://host/full/path (path omitted), or a catalog endpoint id call tikhub.tiktok.video.comments — all hit /call/<rest>. --header adds an extra request header the binding can't know (e.g. Google Ads' per-call login-customer-id); an injected credential always wins, so a --header can 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_call in 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 ls stays clean), ③ an actionable 404 naming the connections connect / secret add fix — reached only after ④ treg's own key, which serves any platform_eligible endpoint (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 carrying balance_micro / estimated_cost_micro / topup_url when 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 get prints 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 via relay(drop_params=…)). Members restricted via --tools get 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). -p is a short alias for --query. catalog get prints 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; a cost.table endpoint 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] reads X-Treg-Async; without the header it is a no-op. Descriptor semantics come from treg.domain.asynctasks (stdlib-only, see the import-boundaries fragment), not a CLI-side copy. With the header it prints the task id and a resumable treg call command 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 upstream Content-Type remains authoritative, so the async descriptor does not duplicate a result format.
  • audit (cmd_audit, --limit, --calls | --runs) — the single "who did what" view. --calls and --runs delegate to cmd_calls / cmd_runs verbatim (the old treg calls / treg runs output, byte for byte). The default merged view is the only new behaviour in the consolidation: it fetches both GET /calls and GET /runs (no new endpoint), normalises each row to {kind, id, user_email, tool, detail, result, where, created_at} (plus task - status, settled_micro, result_url, fetch_command, ttl_note - when /calls reports an async_task for the row, i.e. a metered generation), sorts by created_at descending and truncates to --limit. It drops the kind == "local_run" CallRecords, because /runs already surfaces those same grants as its where: "local" rows — otherwise every local run would be listed twice. Call ids are prefixed c…; run ids keep /runs' own s…/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. args is an argparse.REMAINDER, which silently swallows a treg flag typed AFTER the tool name; cmd_run guards against that by reading the real sys.argv and 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 (so treg run db -- --timeout 30 works and passes --timeout to the CLI). Two execution tiers (see CLI-RUN-PLAN):
    • --local (default, _run_local) — run the vendor CLI on THIS machine as a dedicated treg-run user so the credential is unreadable by the member (see local-run). On Linux with local-run set up, the member hands off via sudo -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's errors → 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 sets redact_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 via dscl/_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_egresslocal-run; --no-egress skips it, --refresh-egress re-resolves drifting IPs, --registry sets the host to allow). Its --run-proof flag installs the runner proof at /etc/treg-run/proof (root-owned, mode 0400, readable only by treg-run); the runner script exports it as TREG_RUN_PROOF and _run_helper sends it as X-Treg-Run-Proof on 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|off flips cli.enabled. --fs-jail (opt-in, macOS) confines the CLI's file writes to a private scratch (fsjail.macos_profile + sandbox-exec, forwarded via TREG_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, --timeout cap 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 /runs merges server runs and local grants, each tagged where (server|local; a local success has a null exit code, since only failures report back). treg audit --calls shows the local GRANT/DENY/REPORT audit rows.
  • cli shell (cmd_shell_start/cmd_shell_stop) — cli shell start opens a subshell where the team's registered CLIs run with the credential injected transparently (a shim dir first on PATH routes each to treg run); cli shell stop (or exit) leaves. --server-for a,b routes named tools to the server (key never on the machine, if server_runnable), --ttl MIN auto-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_proxy seeds the allow-list from the tool listing already fetched for the shims and hands start_session the environment + a stop callback. See shell and local-proxy.
  • treg <command> (cmd_with, reachable as treg with or 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 on PATH, so treg toool ls stays an argparse error and a stray call binary cannot shadow treg 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. start detaches a child running serve start --foreground (via sys.executable -c, never the treg on PATH, which may be an older build); eval "$(treg serve env)" points a terminal at it and --unset reverses it (and works after stop, which is exactly when it is needed); status reads ~/.treg/proxy/proxy.json and says whether THIS terminal is using the proxy. _start_proxy_handle is 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 — query GET /admin/reconcile/* directly or run scripts/provider_balances.py; see src/treg/reconcile.py. _admin_client sends the saved admin_token (treg admin login) or falls back to the active org token (works for an is_superadmin user). 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, plus cmd_oauth_connect + cmd_oauth_providers, which kept their names when the namespace moved) — your connected accounts, and where you connect a new one. Bare treg connections (or ls) → GET /connections (health + expiry). connect (cmd_oauth_connect, ex-oauth connect) has two modes: registry--provider <service> (e.g. google-search-console), optional --capability to pick a scope set (default read) and optional name, so treg's app supplies the client credentials; or bring-your-ownname --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_providersGET /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 old oauth namespace stays as a hidden alias of connect + providers.
  • catalog [platform] (cmd_catalog) — the endpoint catalog: what you can CALL, as opposed to connections 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_label renders 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 hint treg 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 names treg 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 before treg 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), a PARAMS table by location with required first (_print_params), the paste-ready RUN IT command, and the example response pretty-printed, clipped at 40 lines with a pointer to the full JSON. search/get are matched as positional verbs inside cmd_catalog, not argparse subcommands, so treg catalog <platform> keeps working and a multi-word query needs no quoting. An id that misses prints the server's did_you_mean ids 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's WORKS cell shows a measured 0% 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 argument use-team takes, 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 ls lists 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_tokenoauth, 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_tokendeveloper-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.