Docs

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.

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.

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.

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/.ts ()", 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.

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 linked.
  • netlify: netlify env:set MIRIO_SECRET_KEY … --context production and deploy-preview. Needs the Netlify CLI, logged in and netlify linked.
  • 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.

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 [email protected] --form demo-request      # workspace members only
npx mirio destinations add email [email protected] --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).

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.

--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. 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.

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.

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 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.

npx mirio turnstile set 0x4AAAAAAA…
npx mirio turnstile