Skip to content
Like what we’re building? Star on GitHub

Cloud CLI — iterion remote ​

iterion remote turns the local CLI into a full client for a remote iterion instance (cloud or self-hosted iterion server): every operator and admin capability of the HTTP API is reachable as a typed subcommand. The raw escape hatch iterion remote api <METHOD> <path> remains for anything not wrapped; iterion remote routes / iterion remote openapi enumerate the live surface.

All commands honour the global --json flag: the output is then the server's response body verbatim (lossless, stable for jq).

The same surface is also exposable to MCP agents (Claude Code, desktop, Cursor): iterion mcp serves remote_* tools over the same stored credential, plus the remote_api escape hatch — see mcp-server.md.

Setup ​

Interactive (browser) ​

sh
iterion remote login https://iterion.example.com

Opens the instance's /cli-auth page; you approve in the studio and a personal access token (iap_…) is stored in ~/.iterion/cli-auth.json.

Headless alternatives:

sh
iterion remote login https://… --token iap_…          # existing PAT
iterion remote login https://… --email e@x --password …  # mints a CLI PAT
iterion remote login https://… --email e@x --password … --team <id|slug>  # pinned to a team

A --token (or ITERION_TOKEN) login is saved only when the instance answers GET /api/auth/me with that token's account — a JSON identity with a user id. A redirect is followed nowhere (the token is never sent to its target) — and when the target is another host the refusal names it as the URL to log in to instead — and a redirect, an HTML page, an unrelated JSON body or an error status refuses the login with nothing saved; a server error (5xx) says to retry once the server answers. --team pins the token a --email login mints — a team UUID or a team slug, resolved against your own org tree under the login's session, like teams switch, and saved as the team's UUID; it is refused with --token (a token's team is fixed at its mint) and with the browser flow (the approval pins the studio's active team).

iterion remote status shows the logged-in instance + account; iterion remote logout forgets the credential.

CI / scripting (environment only) ​

sh
export ITERION_REMOTE_URL=https://iterion.example.com
export ITERION_REMOTE_TOKEN=iap_…       # fallback: ITERION_TOKEN
iterion remote runs list --json | jq '.runs[].id'

When ITERION_REMOTE_URL is set the stored config file is ignored entirely (a stored token is never sent to a different host). ITERION_REMOTE_TEAM / ITERION_REMOTE_ORG set the tenant scope.

Tenant scope (teams / orgs) ​

Team-scoped commands (secrets, api-keys, webhooks, forge, audit team, bindings, and the run reads — runs list, runs stats, runs repos, resume --usage-blocked) resolve their team in this order:

  1. --team <id|slug> flag (a slug resolves against the org tree, an unknown one is refused naming it)
  2. ITERION_REMOTE_TEAM
  3. the default persisted by a --team login (or by teams switch)
  4. the account's active team (from /api/auth/me)

A PAT's identity team is pinned at mint time, and a PAT may not mint another credential (the instance answers 403). So teams switch re-opens the browser approval (the studio's /cli-auth page) with the team pinned: the approving live session mints the team-pinned token — never the stored PAT — and the CLI stores it and revokes the previous CLI token (matched by fingerprint — tokens you minted for other purposes are never touched). Headless, pin the team at login instead: iterion remote login <url> --email <you> --team <id>. From a session credential the switch still mints directly, stores, and revokes the same way. iterion remote tokens create is refused the same way from a stored token: create the token in the studio (Account → Access tokens) or from a fresh login.

The same rule reaches every command whose route mints a team token — a token that outlives the credential it is minted from. Three of them mint one on every call, so the CLI states the refusal itself, before anything is sent — not even the /api/auth/me the team and org scope resolution would read: webhooks create, webhooks rotate and teams|orgs invitations create. The advice names the studio, not a login: a login stores another personal access token, which mints no more than this one. Reads and the gestures that mint nothing stay available — webhooks list, the invitation list and DELETE, repo-bots preview|delete, orgs approvals reject. Org-scoped commands (--org / ITERION_REMOTE_ORG / orgs switch) need no re-minting (org scope is path-based).

The repo-bot provisioning is the one the CLI does not pre-judge, and must not: whether it mints depends on state only the instance holds. A repo's FIRST enable reserves its hook identity and mints the iwh_ token, so it is refused (403) from a stored token. A reconcile of a repo already provisioned, a settings-only repo-bots update, and an orgs approvals approve standing for such an update reuse the integration's sealed secret, mint nothing, and go through — which is why forge repo-bots create|update, orgs approvals approve and the same calls through remote api all send, and the instance decides. The question is asked where the mint is decided, so a refusal that does come back carries the same studio advice.

The launch → follow → inspect recipe ​

sh
# Launch a local .bot file (its source is uploaded inline) and tail it.
# What travels is the bot written out as ONE file: a bot in several files
# (`import "lib/x.bot"`) is flattened, and every prompt `{{include "x.md"}}`
# is resolved beside the file that carries it, before the upload — a server
# pod has none of the files beside the source. A bot in one file with no
# include uploads byte-identical. The run's identity is that text: a resume
# (`runs resume --file`) re-flattens the same unit into the same text, an
# edited fragment or include into another, which the server refuses without
# --force.
iterion remote runs launch ./review.bot --var repo=org/app --follow

# Or launch a catalog bot by id:
iterion remote runs launch --bot whats-next --follow

# Separately:
id=$(iterion remote runs launch ./wf.bot --json | jq -r .run_id)
iterion remote runs follow "$id"          # exit 1 if the run fails
iterion remote runs log "$id"
iterion remote runs artifacts "$id"
iterion remote runs files "$id" src/main.go --diff
iterion remote runs merge "$id" --strategy squash

runs follow polls GET /api/runs/{id}/events by seq cursor (default every 2s, --interval to tune) — no WebSocket dependency, so it works through any proxy.

What a server relays — a run's error, a title, a body without a table — is shown as text: a control character, an escape sequence or a bidi override is printed escaped (\x1b, \u202e), never sent to the terminal raw. --json passes the server's body as it is, for a machine; runs log and runs workflow print the bytes the run holds, as they are (a log keeps its colors). A long run error is clipped on its failure's text: the remedy it ends on (— hint: …) is kept whole.

Attachments: --attach name=./file uploads via POST /api/runs/uploads and wires the returned id into the launch. runs upload <path> does the staging step alone and prints the upload id.

Command tree ​

GroupCommands
runslist · launch · get · events · follow · log · workflow · artifacts · files · commits · cancel · pause · resume · fork · send · merge · conflicts · rename · delete · preview-cost · upload · stats · repos
botslist · get · put · overlay · install · upload
marketplacelist · get · download · submit · install · uninstall · moderation
issueslist · get · create · update · delete · transition · comment · push · pulls
labels / boardlist · rename · merge · delete / get · set · bind · show · unbind
dispatcherstatus · state · start · stop · pause · resume · refresh · reload · config · issue · cancel
triggerslist · get · create · update · delete · emit
scheduleslist · create · delete (team-scoped, cloud recurring bots)
teamslist · create · switch · members · invitations · update · status · delete · add-member
orgslist · switch · members · invitations · usage · teams · add-member · approvals · credential-audience · oauth · settings
… members (both)members alone lists; add <user-id> <role> places an account that ALREADY exists (idempotent), set-role updates an existing membership, remove revokes. A team grant needs the user in the team's org first, so the order for someone new is orgs members add then teams members add. orgs members add is super-admin only — an org admin adds by orgs invitations create <email> (from the studio, or from a CLI holding a session credential: the invitation is a team token, which a stored personal access token does not mint) and keeps set-role/remove; teams members add stays theirs, its candidates being already in the org.
mepassword · sessions-revoke-all · sso-links
tokenslist · create · revoke
secrets / api-keyslist · set/create · rotate/update · delete (--scope team|me)
bindingsper-bot secret bindings (list · create · delete)
webhookslist · get · create · update · delete · rotate · deliveries (create and rotate mint a team token on every call: studio only from a stored token)
forgeconnections · refresh · repo-bots · oauth-apps · integrations
audit / usage / limitsaudit team|org|admin · org usage · cost limits
memorydocs · doc get|put|delete · export · import (--name space) — usage carries no space flags, so the server refuses it
admin usersusers alone lists (--q matches an email PREFIX or an exact id); get <user-id> returns one account's file — status, last sign-in, whether a password sign-in is possible at all, its SSO links, and the orgs/teams it was actually granted; update <user-id> --data; reset-password <user-id> mints a one-shot temporary password and revokes every session — refused (403) for your own account when the CLI presents a personal access token, which is what a login stores: change your own password from the studio (Account → Profile → Change password). See administering accounts.
adminorgs · users · dlq · llm · caps · bots · roles · sandbox (super-admin; llm api-keys/llm oauth = the platform fallback credentials — rotate without a redeploy, see cloud-llm-credentials.md; caps = the runtime usage-cap percentages — retune without a restart, see usage-caps.md; bots = platform bot overrides — push any bot without an image rollout, roles/sandbox = runtime webhook role bindings + sandbox: auto image, see platform-bots.md)
ssoproviders · domains (org-scoped)
pluginslist · enable · disable · install · uninstall · config
poolstatus · history · share · pause · resume · withdraw · donors · policy — lend your own LLM subscription or personal metered key to the shared credential pool, bounded by ceilings you set on share (--max-usd-day/-week, --max-runs-day, --max-concurrent, --from-hour/--to-hour, --bots). donors is the operator view of the pool's policy and its lenders; policy is the operator write side (--enabled, --name, audience flags — the audience is replaced whole).
serverinfo · health

Structured mutation payloads follow the --data '<json>' / --data @file.json / --data @- (stdin) convention shared with remote api.

Bind the team to a GitHub project board ​

Makes a Projects v2 board and the native board the same tickets: the board's Status column becomes two-way with the native columns, and its Area/Mode/Priority land as area:/mode:/prio: card labels.

sh
iterion remote forge connections                 # find the connection id
iterion remote board bind --project SocialGouv/203 --connection conn_123
iterion remote board show                        # the EFFECTIVE map + coverage

Field and option ids are discovered by name at bind time, never hardcoded. A board with different columns binds with --status-map "Todo=ready,Doing=in_progress,Shipped=done"; the map must be injective (two columns on one state is refused, naming the collision). --sync-every 2m (default; 0/off disables, floor 1m) sets the reconciliation interval, which the server runs elected per tenant. Full runbook, incl. the permissions the credential needs and what the sync deliberately refuses to do: github-board-sync.md.

Give a bot account the iterion-bot avatar ​

A GitLab group/project access token's bot user gets the mascot avatar at connect time. For a connection created before that, or a dedicated account the forge does not flag as a bot (Forgejo, a hand-made GitLab user), apply it once:

sh
iterion remote forge connections avatar <connection-id>            # a flagged bot account
iterion remote forge connections avatar <connection-id> --force    # a dedicated, unflagged account

Refused on an OAuth connection (a person's account) and on GitHub, which has no avatar or App-logo API — the error names where to upload it by hand. Full policy + the manual GitHub App upload: brand.md.

Refresh GitHub App grants ​

After changing a GitHub App installation's permissions on GitHub, refresh the connection immediately instead of waiting for a periodic worker or restarting the server:

sh
iterion remote forge refresh <connection-id>

This command applies only to GitHub-App connections. It re-probes the live installation, replaces the connection's stored grant map, and forces a fresh installation-token mint. The table deliberately shows GRANTED (what the installation allows) beside TOKEN (what the newly minted token actually carries); --json returns both maps plus missing-permission lists. The active team is resolved by the normal remote-team precedence.

Secrets hygiene ​

Secret values are never taken as command arguments (argv leaks via process lists). secrets set, secrets rotate and api-keys create read the value from --from-env VAR, --from-file path, or stdin:

sh
printf '%s' "$GITLAB_TOKEN" | iterion remote secrets set gitlab-token
iterion remote api-keys create --provider anthropic --name prod --from-env ANTHROPIC_API_KEY
# a tenant-owned OpenAI-compatible gateway, restricted to one repository (stored and
# listed; no run spends it until #2005 PR-C')
iterion remote api-keys create --provider openai_compatible --name pliage --base-url https://gateway.example.test --repos owner/repo --from-env PLIAGE_KEY
# moving a gateway key is a rotation: the secret rides the same write, never a JSON blob
iterion remote api-keys rotate <key-id> --base-url https://gateway-2.example.test --from-env PLIAGE_KEY

Errors ​

Any non-2xx response surfaces as HTTP <code> <METHOD> <path>: <what the server said> and a non-zero exit. A JSON error body is read field by field: its message (with message / detail when the server puts them apart), then in parentheses its error_code, also needs --force, retryable and reset_at when present, then its hint — the remedy a refusal names, such as the --accept-scratch-loss a resume that would lose its scratch needs. Any other body prints its first line. Admin commands do no client-side role check — a 403 from the server is the answer.

Everything else ​

The typed tree covers the operator surface; for the long tail (examples, filesystem browse, CDP proxies, anything new):

sh
iterion remote routes                 # live method+path inventory
iterion remote api GET /api/…        # authenticated raw call