# CLI

The CLI ships inside the `@trymirio/mirio` package. Run it with `npx @trymirio/mirio <command>` before the package is installed, or `npx mirio <command>` after. Every command accepts `--json` for machine-readable output on stdout. Errors say what went wrong and what to do next. Exit codes: 0 ok, 1 error, 2 `check` failed.

Configuration: `mirio.json` at the repo root (`projectId`, `apiUrl`, `formsDir`), secret key in `.mirio/secret` (gitignored). `MIRIO_SECRET_KEY` and `MIRIO_API_URL` override both, which is what CI uses.

## `mirio init`

Creates a sandbox project and sets up the repo.

```bash
npx @trymirio/mirio init [--agent claude-code] [--name "Marketing site"] [--api-url https://api.trymirio.com] [--forms-dir mirio/forms] [--force]
```

Writes `mirio.json`, `.mirio/secret`, `mirio/client.ts`, `mirio/forms/`, adds `.mirio/` to `.gitignore`, and appends a short "Mirio forms" section to `AGENTS.md` and `CLAUDE.md` (whichever exist; creates `AGENTS.md` if neither). Prints the claim link. Refuses to overwrite an existing `mirio.json` without `--force`. Provisioning is rate limited per IP. In a monorepo, run it in the app's folder: at a monorepo root (no `package.json`, a workspace setup, or no React) with an app below it, it stops before doing anything (a link code stays unused) and names the app folders, Next.js and Astro apps first; `--force` sets up the current folder anyway. The install hint matches your package manager.

`--agent <name>` tells Mirio which coding agent ran the command (agents should name themselves). Without it the CLI uses `MIRIO_AGENT`, then `AI_AGENT`, then `CLAUDECODE=1` (Claude Code); `init --link` sends it too.

### `mirio init --link <code>`

Connects the repo to a site that was started in the Mirio dashboard (New form). The dashboard puts the command, with a one-time code, at the top of the prompt it gives you for the form; codes work once and expire after 24 hours.

```bash
npx @trymirio/mirio init --link mil_XXXXX-XXXXX-XXXXX-XXXXX [--force]
```

It does everything `init` does (config, `.mirio/secret`, `mirio/client.ts`, the plugin, CSS and the AGENTS.md section) but uses the site created in the dashboard, which is already claimed, so there is no claim link. It prints `Connected to <site>` and the forms waiting for a file:

```
✓ Connected to Acme marketing (prj_…)
Forms waiting for a file:
  mirio/forms/waitlist.ts — Waitlist
      Template: waitlist
  mirio/forms/quote.ts — Quote request
      A short intake for people who want a custom quote: what they need built, rough budget…
```

Write each file with `defineForm` and the same slug; the dev server (or `mirio push`) registers it and the draft becomes the form. `--json` returns the project id and the drafts (never the key). An expired or used code asks you to copy a fresh prompt from the form's page; a site that is already connected to another repo returns the API's hint (copy `MIRIO_SECRET_KEY` from that repo, or create a reconnect code on the site's page under Sites). `--force` replaces an existing `mirio.json`.

### Setup edits

Both forms of `init` also set up the site, unless you pass `--no-setup`:

- **Framework plugin.** Next.js: wraps the default export of `next.config.(ts|mjs|js)` in `withMirio(...)` and adds the import (handles `export default nextConfig`, `export default { … }` and `module.exports = …`). Vite and React Router: adds `mirio()` to `plugins` in `vite.config.*`. Astro: adds `mirio()` to `vite.plugins` in `astro.config.*`. When the file has a shape it doesn't recognize, nothing is written and the two lines to add are printed.
- **CSS.** With Tailwind v4, adds `@import "@trymirio/mirio/theme.css"` and the `@source` line after `@import "tailwindcss"` in the global stylesheet; on shadcn/ui sites (`components.json`, whose `tailwind.css` path is used) also `@import "@trymirio/mirio/themes/shadcn.css"`. With Tailwind v3 it adds `@trymirio/mirio/styles.css` (and `themes/shadcn-v3.css`) at the top. Without Tailwind it prints `import '@trymirio/mirio/styles.css'`.

## `mirio push [slug] [--env production|preview|development]`

Loads every schema in the forms directory, validates it locally, and registers it. Prints the version number and the diff from the previous version. Warns while the project is unclaimed. With the framework plugin you rarely need it: the dev server registers a form when its file is saved, and builds run `sync`. The push is tagged with its source (see `sync`); from a laptop that is `development`, so it never changes the questions the dashboard shows for a live form. Pass `--env` to override.

```
✓ waitlist  v3 (new version)
    + role (dropdown)
    ~ name → full_name
⚠ Project is unclaimed. Public origins are rejected until it is claimed: https://trymirio.com/claim/…
```

## `mirio test [slug]`

Sends a sample submission built from the schema, with the secret key and a test flag, then reads it back. Test submissions are stored with status `test` and never count as responses or toward the response limit (triage still reviews them, at most 20 per form a day, and those reviews count toward the triage quota), and never trigger notifications.

## `mirio status`

Project mode (sandbox or live), claim link, allowed origins, delivery destinations, drafts created in Mirio that are still "Waiting for a file: mirio/forms/<slug>.ts (<name>)", and a table of forms with version, 7-day submissions, last submission, the agents policy, whether triage is on and whether an ideal response is set (read from Mirio). Warns when review is on without an ideal response (fit levels need one: `mirio review <slug> --ideal "…"`), prints one line per schema that still carries moved settings (`notifyEmails`, `allowedOrigins`, `review`), and flags forms whose local schema differs from what was registered.

**Updates:** `status` and `sync` say when a newer `@trymirio/mirio` is on npm ("@trymirio/mirio 0.1.5 is out (you have 0.1.4). What changed: https://trymirio.com/docs/changelog · Upgrade: pnpm add @trymirio/mirio@latest"; `--json` has it as `update`). They ask the registry at most once a day (cached in `~/.cache/@trymirio/mirio`), give up after 1.5 seconds and never fail a command; the dev server's sync and CI builds don't check. `MIRIO_NO_UPDATE_CHECK=1` turns it off. See the [changelog](/docs/changelog.md).

## `mirio sync [--env production|preview|development]`

The build step the framework plugin runs (`withMirio` during `next build`, `mirio()` during `vite build`); call it yourself in any other CI.

- **With a key** (`MIRIO_SECRET_KEY`, or `.mirio/secret` locally): registers every form, tagged with its source.
- **Without one**: checks the registered forms through the public schema endpoint. It **fails** (exit 2) only when visitors would be rejected: the project is unclaimed, or a form is not registered yet (a new form, or a draft created in Mirio whose file has not been registered). It **warns** when a form's questions changed since the registered version (those responses are accepted and flagged).

An unclaimed project fails builds on a deploy host or in CI and only warns on a local build. If Mirio can't be reached, the build continues with a warning.

The source comes from the host: Vercel (`VERCEL_ENV`, `VERCEL_GIT_COMMIT_REF`, `VERCEL_GIT_COMMIT_SHA`, `VERCEL_URL`), Netlify (`CONTEXT`, `BRANCH`, `COMMIT_REF`, `DEPLOY_URL`), Cloudflare Pages (`CF_PAGES_BRANCH`, `CF_PAGES_COMMIT_SHA`, `CF_PAGES_URL`; production on `MIRIO_PRODUCTION_BRANCH` or `main`), GitHub Actions (`GITHUB_REF_NAME`, `GITHUB_SHA`; same branch rule), Render (`RENDER`, `IS_PULL_REQUEST`, `RENDER_GIT_BRANCH`, `RENDER_GIT_COMMIT`), Railway (`RAILWAY_ENVIRONMENT_NAME`, `RAILWAY_GIT_BRANCH`, `RAILWAY_GIT_COMMIT_SHA`); GitLab, CircleCI, Buildkite, Jenkins, Bitbucket Pipelines, Azure Pipelines and Travis by their branch and pull/merge request variables (production on `MIRIO_PRODUCTION_BRANCH`, else the provider's default branch when it says, else `main`; preview otherwise); and any other build server that sets `CI` (production). `MIRIO_ENV` overrides. A build with no host signal and no `CI` is development, a local `next build` included, so building a branch on your laptop never changes production; if your build server doesn't set `CI`, set `MIRIO_ENV=production` (the build output says so). Production registrations set the questions the dashboard shows; preview and development ones never do.

```
mirio sync production (vercel, main@1a2b3c4) · 3 forms · registering
✓ demo-request v4 registered
✓ waitlist v2 unchanged
```

## `mirio env vercel|netlify|print`

Puts the project's secret key into the host so deploy builds can register forms.

- `vercel`: `vercel env add MIRIO_SECRET_KEY production` and `… preview`, with the key on stdin. Needs the Vercel CLI, logged in and `vercel link`ed.
- `netlify`: `netlify env:set MIRIO_SECRET_KEY … --context production` and `deploy-preview`. Needs the Netlify CLI, logged in and `netlify link`ed.
- `print`: prints `MIRIO_SECRET_KEY=sk_…` to your terminal so you can paste it into any host's settings.

Mirio never prints the key for `vercel` and `netlify`, and redacts it from the host CLI's output. When the host CLI is missing, not logged in, not linked, or the variable already exists, the error says what to run.

## `mirio check [--allow-sandbox]`

A strict local check. Exits 2 when the project is unclaimed or any local schema differs from the registered one (or was never registered). Builds don't need it any more: the framework plugin runs `sync`, which fails only when visitors would be rejected.

## `mirio claim [--open] [--wait]`

Prints the claim link. `--open` opens it in a browser. `--wait` polls until the project is live.

## `mirio submissions [slug] [--status all|complete|partial|spam|test] [--limit 20]`

Reads submissions through the project API, with each response's kind (complete, partial, spam, test), when it came in and its first answers. For sandbox projects this is the agent's own test data and expires after 72 hours.

## `mirio destinations …`

Wire delivery without the dashboard. Destinations belong to a form: pass `--form <slug>`, or `--all-forms` to create one per form.

```bash
npx mirio destinations list
npx mirio destinations add slack --form demo-request      # "Add to Slack": prints a link for a person, waits for the channel
npx mirio destinations add slack https://hooks.slack.com/services/… --all-forms
npx mirio destinations add postgres "postgres://…pooler.supabase.com:6543/postgres" [--table demo_requests] --form demo-request
npx mirio destinations add email founders@example.com --form demo-request      # workspace members only
npx mirio destinations add email sales@example.com --form demo-request --categories "Sales lead,Partnership" --fit high,medium
npx mirio destinations add slack https://hooks.slack.com/services/… --form demo-request --fit high
npx mirio destinations add slack https://hooks.slack.com/services/… --form demo-request --group "Urgency: This week"
npx mirio destinations add webhook https://example.com/hooks/mirio --form demo-request      # prints the signing secret once
npx mirio destinations remove dst_…
npx mirio destinations test dst_…
```

**Slack without a URL** prints a 30-minute link to "Add to Slack" for a person in the site's workspace (an agent can't click through Slack's install screen) and waits up to 10 minutes for the channel to appear; `--no-wait` prints the link and exits, and `--json` returns `{ connectUrl, expiresAt, destinations }` without waiting. Claimed sites only.

Webhook URLs must be public `https://` addresses, and databases public hosts: Mirio refuses localhost, private ranges and internal names (see [Destinations](/docs/destinations.md)).

Email goes only to workspace members (no verification step); for shared inboxes and aliases use Slack. Adding a non-member address fails with the API's hint. Connection strings are encrypted at rest and never printed back. See [Destinations](/docs/destinations.md).

`--categories` (comma-separated, matching the form's review categories) and `--fit` (`high`, `medium`, `low`) filter what an **email or Slack** destination sends, using the [AI review](/docs/ai-review.md). Responses without a review (review off, or the review failed) are always sent. The flags are rejected for webhooks and Postgres, which receive every response with the review attached. `destinations list` shows each filter in plain words ("Sales lead or Partnership, high or medium fit"); `--json` includes the `filter` object.

## `mirio domains list|add <domain>|remove <domain>`

The site's production domains (allowed origins). Visitors from them can submit once the project is claimed; localhost always works. Accepts `example.com`, `www.example.com`, `*.example.com` or a full origin with a port. A domain and its `www.` form count as one site: adding either allows both (most sites redirect one to the other). API: `GET/POST/DELETE /v1/project/domains`.

```bash
npx mirio domains add example.com
npx mirio domains list
npx mirio domains remove staging.example.com
```

## `mirio review <slug> [--ideal "…"] [--summary "…"] [--categories "A,B"] [--group "Name: A, B"] [--remove-group "Name"] [--off|--on]`

Triage for one form. With no flags it shows the settings; flags change them and keep the rest (so `--ideal` alone keeps the categories). The ideal response describes a great respondent in plain words and enables fit levels; `--summary` says what each summary should focus on (3 to 500 characters); categories are 2 to 12 labels; `--group "Urgency: This week, Later"` adds a group triage sorts into, or replaces the one with that name (repeatable, up to 5), and `--remove-group` drops one. API: `GET/PUT /v1/project/forms/:slug/review`. See [AI review](/docs/ai-review.md).

```bash
npx mirio review demo-request --ideal "Product or research leads at 20–500 person software companies who want to start this quarter."
```


## `mirio turnstile [set <secret key> [--site-key <key>]|remove]`

[Cloudflare Turnstile](/docs/spam.md#turnstile) with your own widget. `set` gives Mirio the widget's secret key so it can check visitors' tokens: the site key comes from `settings.turnstileSiteKey` in your schema files (`--site-key` when they use more than one), Mirio checks the secret key with Cloudflare, and it is stored encrypted and never printed back. With no action it lists the forms that set a site key and whether their tokens are checked; `remove` deletes the secret key. API: `GET/PUT/DELETE /v1/project/turnstile`.

```bash
npx mirio turnstile set 0x4AAAAAAA…
npx mirio turnstile
```
