@reopt-ai/cli
v0.9.2
Published
Reopt CLI — manage Brandapp resources, EAV schemas, and AI tooling, with a built-in MCP server for agents.
Maintainers
Readme
@reopt-ai/cli
Reopt CLI — manage Brandapp resources, EAV schemas, and AI tooling.
reopt is built for both humans and agents:
- JSON-first output —
--format jsonis the default, perfect for CI and LLMs. - Pipeable — quiet, deterministic stdout; logs go to stderr.
- Schema-as-Code — define EAV schemas in TypeScript, sync to server, generate types.
- MCP server built-in — expose tools to Claude/Cursor/etc. with
reopt mcp.
Installation
npm install -D @reopt-ai/cli @reopt-ai/brandapp-sdkRequires Node.js ≥ 18.
Quick start
# 1. Authenticate (browser-based)
npx reopt login
# 2. Link the current project to a brandapp
npx reopt brandapp link
# 3. Define your schema, then sync
npx reopt brandapp eav sync -s ./eav.schema.ts -o ./generated/eav.tsSchema-as-Code
// eav.schema.ts
import { defineSchema, defineEntity } from "@reopt-ai/brandapp-sdk/eav/schema";
const contacts = defineEntity({
name: "contacts",
description: "Contact list",
attributes: {
name: { dataType: "string", label: "Name", isRequired: true },
email: {
dataType: "email",
label: "Email",
isRequired: true,
isUnique: true,
},
phone: { dataType: "string", label: "Phone" },
status: {
dataType: "select",
label: "Status",
options: [
{ value: "active", label: "Active" },
{ value: "inactive", label: "Inactive" },
] as const,
},
},
});
export default defineSchema({ entities: { contacts } });npx reopt brandapp eav sync -s ./eav.schema.ts -o ./generated/eav.tsimport type { ContactsValues } from "./generated/eav";
import { ENTITY_IDS } from "./generated/eav";
const records = await eav.records.list(ENTITY_IDS.contacts);
const values = records[0].values as ContactsValues;
// values.email → string (required)
// values.phone → string | undefined
// values.status → "active" | "inactive" | undefinedOutput and global flags
| Flag | Default | Notes |
| ------------------------------ | ------- | ------------------------------------------------------------------------- |
| --format <type> | json | json, table, csv, yaml |
| --fields <a,b> | — | Restrict output to listed columns (applies to every format, incl. NDJSON) |
| --limit <n> / --offset <n> | — | Client-side window into a result list |
| --page-all | off | Auto-paginate; emits NDJSON in JSON mode |
| --page-limit <n> | 10 | Max pages with --page-all (a truncation warning is printed to stderr) |
| --page-delay <ms> | 100 | Delay between pages with --page-all |
| --env <name> | — | Named environment from .reopt.config.mjs (falls back to BRANDAPP_ENV) |
| --timeout <ms> | 30000 | Per-request timeout |
| --max-retries <n> | 1 | Retry budget for idempotent (GET) requests |
| -q, --quiet | off | Suppress non-essential output (errors still print) |
| -v, --verbose | off | Include stack traces / debug info |
| --no-interactive | auto | Never prompt for missing args (default when stdin/stderr is not a TTY) |
# Default JSON, agent-friendly
$ reopt brand list
[
{ "id": "br_123", "name": "...", ... }
]
# Human view
$ reopt brand list --format table
# YAML
$ reopt brand list --format yaml
# Stream every record as NDJSON for streaming consumers
$ reopt brand list --page-allErrors in JSON mode go to stderr as a single line:
{ "error": "auth_error", "message": "Session expired ...", "exitCode": 2 }Authentication
Reopt CLI separates two credential types:
- User session (
~/.reopt/auth.json) — your interactive login, used byreopt login,reopt status, and registry-driven commands likereopt brand list. - Brandapp credentials (
~/.reopt/credentials.json) — OAuth client id/secret per linked brandapp, used byreopt brandapp eav *and similar SDK-backed flows.
Both files are written with 0o600 permissions; the parent directory uses 0o700.
Sign in
$ reopt login
$ reopt login --server https://reopt.example.com
$ REOPT_CLI_SERVER=https://staging.reopt.ai reopt loginlogin --format json emits NDJSON events on stdout — useful for automation:
{"event":"verification_uri","url":"https://id.reopt.ai/api/cli/auth/verify?code=...","code":"..."}
{"event":"polling"}
{"event":"success","email":"[email protected]","name":"You","expiresAt":"2026-06-01T..."}Sign out
$ reopt logout # remove user session, leave brandapp credentials
$ reopt logout --all # also remove every stored brandapp credentialEnvironment variables
| Variable | Purpose |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| BRANDAPP_CLIENT_ID / BRANDAPP_CLIENT_SECRET / BRANDAPP_ID | OAuth client credentials for SDK-backed commands (EAV, token, env). Highest priority. |
| BRANDAPP_CLIENT_TOKEN | Short-lived service JWT (from reopt token mint). When set, brandapp requests send Authorization: Bearer instead of Basic auth. |
| BRANDAPP_ENV | Default named environment from .reopt.config.mjs (overridden by --env). |
| REOPT_BASE_URL | Override the API base URL used by the SDK (default https://brandapp.reopt.ai). Point this at a local dev server to target it. |
| REOPT_CLI_SERVER | Override the auth/CLI server URL (default https://reopt.ai). |
| REOPT_ID_SERVER | Override the derived id (auth) server URL — needed only when running apps/id directly on :3004. |
| REOPT_USER_TOKEN | Use a pre-issued session token instead of running reopt login (CI/headless). Requires REOPT_CLI_SERVER or REOPT_BASE_URL to be set. |
| REOPT_USER_EMAIL | Optional label for REOPT_USER_TOKEN users. |
| REOPT_CREDENTIALS_FILE | Path to a JSON file containing {clientId, clientSecret, brandappId, baseUrl?}. |
| REOPT_DEV_AI_PROXY_URL | Legacy alias for REOPT_BASE_URL used by reopt brandapp dev --proxy-ai. |
Resolution order for SDK credentials:
BRANDAPP_CLIENT_ID+BRANDAPP_CLIENT_SECRET+BRANDAPP_IDREOPT_CREDENTIALS_FILE.reopt.json+~/.reopt/credentials.json(set byreopt brandapp link)~/.reopt/config.json(legacy)
Resolution order for the user session:
REOPT_USER_TOKEN(+REOPT_CLI_SERVER)~/.reopt/auth.json(set byreopt login)
Preferences
Persist defaults in ~/.reopt/preferences.json so you don't have to set env
vars or pass flags every time:
$ reopt config set server https://reopt.example.com
$ reopt config set format table # use table view by default
$ reopt config set interactive false # never prompt
$ reopt config list # show all
$ reopt config get format # show one
$ reopt config unset server # removeResolution order for the active value: explicit flag > env var > preference > built-in default.
| Key | Type | Notes |
| ------------- | ------------------------------ | ----------------------------------------------------------------------------------------------- |
| server | URL | Default for reopt login and getAuthState (overridden by REOPT_CLI_SERVER and --server). |
| format | json \| table \| csv \| yaml | Default for --format. |
| interactive | boolean | Force interactive prompts on/off; otherwise auto-detected from TTY. |
Shell completion
# Bash — drop into ~/.bashrc:
eval "$(reopt completion bash)"
# Zsh — append to ~/.zshrc (after `compinit`):
eval "$(reopt completion zsh)"
# Fish — write the file once:
reopt completion fish > ~/.config/fish/completions/reopt.fishThe script enumerates the full command tree (including aliases like
reopt ws ls, reopt ba eav up) so subcommands tab-complete after the parent.
Status and diagnostics
$ reopt status # human-readable status table
$ reopt status --format json # full JSON report (auth, brandapps, env overrides)
$ reopt brandapp doctor # step-by-step ✓/✖ checks for the local setupstatus --format json returns a structured report:
{
"version": "0.6.0",
"build": "2026-07-24T00:00:00.000Z",
"server": { "url": "https://reopt.ai", "source": "default" },
"auth": { "status": "ok", "email": "...", "expiresAt": "..." },
"brandapps": [
{ "cwd": "apps/web", "brandappId": "...", "hasCredentials": true }
],
"envOverrides": {
"REOPT_BASE_URL": "https://...",
"BRANDAPP_CLIENT_ID": true
}
}Commands
Brandapp setup
| Command | Purpose |
| ----------------------------------- | ----------------------------------------------------------------------------------------------- |
| reopt brandapp link [--dry-run] | Link the current directory to a brandapp; writes .reopt.json and ~/.reopt/credentials.json. |
| reopt brandapp unlink [--dry-run] | Remove the link for this directory. |
| reopt brandapp doctor | Step-by-step health check (auth, project config, server, per-entry credentials). |
| reopt brandapp init [--force] | Scaffold dev-mode files into the current project. |
EAV (schema-as-code)
Two workflows: immediate sync (fast, dev) or file-based migrations
(plan → migrate → verify, recommended for prod). See the
reopt-eav skill for the full guide.
| Command | Purpose |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| reopt brandapp eav sync | Apply local schema to the server and regenerate types. --dry-run, --watch, --delete-orphans, --force. |
| reopt brandapp eav status | Diff local schema vs server (concise). |
| reopt brandapp eav pull | Generate a local schema file from the server state. |
| reopt brandapp eav diff | Render the schema diff as a markdown report (--out <file>, PR-attachable). |
| reopt brandapp eav plan <name> | Scaffold a migration file (--from-diff fills the up() body + per-op risk). |
| reopt brandapp eav migrate | Apply pending migrations sequentially under an advisory lock. --dry-run, --target <name>. |
| reopt brandapp eav history | Show pending / applied / drift / missing-file state. |
| reopt brandapp eav verify | CI-friendly checksum validation against applied migrations. |
| reopt brandapp eav records list | List records. --entity <name\|id>, --filter <json>, --auth-user, --sort, --order, --select, --limit, --page. |
| reopt brandapp eav records get <id> | Show one record (--entity). |
| reopt brandapp eav records count | Count records matching a filter, without fetching bodies. |
| reopt brandapp eav records delete-where | Delete every record matching a filter. Reports the match count and exits 7 unless --force. |
Workspace as code (RFC-0030)
Declare the reopt resources a product needs in reopt.workspace.ts and reconcile them through the platform API.
Credentials come from the same --env / BRANDAPP_CLIENT_* chain as the EAV commands (scope workspace:admin).
Six resource kinds, applied in this order (references flow one way, so the order is the dependency order); creates and updates run forward, deletes run in reverse and always last:
brand → brandapp → oauth-client → eav.schema → works.pipeline → webhook
// reopt.workspace.ts
import { defineWorkspace } from "@reopt-ai/cli/workspace";
export default defineWorkspace({
schemaVersion: 1,
brands: {
data: { name: "Reopt Data", domain: "data.example.com" },
},
brandapps: {
"reopt-data": {
name: "Reopt Data",
brand: "data",
eav: { schema: "./eav/reopt-data.schema.ts" },
},
},
oauthClients: {
"data-sdk": {
brandapp: "reopt-data",
scopes: ["works:read", "works:write"],
grantTypes: ["client_credentials"],
},
},
works: {
pipelines: {
"error-triage": {
name: "Error triage",
stages: [
{ key: "new", label: "New", isEntry: true },
{ key: "doing", label: "In progress" },
{ key: "done", label: "Done", outcome: "completed" },
],
},
},
},
webhooks: {
"work-sync": {
url: "https://example.com/api/webhooks/reopt",
triggers: ["workTaskStageChanged"],
// optional — omit and `apply` issues one for a new endpoint (shown once, `--secrets-out`)
secret: process.env.REOPT_WEBHOOK_WORK_SYNC_SECRET,
},
},
});| Command | Description |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| reopt workspace plan [--out <file>] [--exit-code] | Read the workspace and print the plan (+ create, ~ update, - delete, ! destructive / import-required, · unchanged). Never writes. --exit-code → 6 on changes. |
| reopt workspace apply [--plan <file>] [--allow-destructive] [--yes] | Apply under the server advisory lock. Exit 7 on destructive changes without --allow-destructive, 9 when --plan is stale, 10 when the lock is held, 3 when a prompt is needed but stdin is not a TTY. |
| reopt workspace import <kind>/<key> <remoteId> | Bind an existing UI-created resource to a declaration key, write reopt.workspace.lock, print a stub to paste. |
| reopt workspace export [--out <file>] [--all] | Write reopt.workspace.ts, the per-brandapp EAV schema files and the lock from the current server state. The migration path for a workspace built by hand. Refuses to overwrite without --force-overwrite. |
| reopt workspace rotate-secret oauth-client/<key> | Issue a new client secret. Shown once — capture it with --secrets-out <file> (written 0600) or --format json. |
| reopt workspace drift | CI alias for plan --exit-code. |
An absent section means "not managed"; works: { pipelines: {} } means "declared empty" and deletes code-managed
pipelines. reopt.workspace.lock (per --env: reopt.workspace.<env>.lock) binds declaration keys to server ids —
commit it.
Two things the tool refuses rather than guesses. Destructive changes — deleting a pipeline or stage that still holds
tasks, an EAV entity that still has records, an OAuth client whose tokens are live — need --allow-destructive, and
the server refuses them a second time on its own. Brands and brandapps have no delete path at all: one that leaves
the declaration is reported and left alone (remove it in superadmin). A resource that exists on the server but was not
created by code is reported as import-required, never overwritten — run import first.
New OAuth secrets exist only in the response that creates or rotates them. apply --secrets-out <file> and
rotate-secret --secrets-out <file> write them with mode 0600; human-readable output only ever shows a mask.
Resource lookups (registry-driven)
reopt workspace list | get [id]
reopt brand list
reopt domain list
reopt post list | post-group list
reopt member list
reopt brandapp list | term list
reopt schema [resource] # inspect available resource shapesAll registry commands honour --format, --fields, --limit, --offset, --page-all.
Customers (CRM)
reopt customer (alias cu) manages the workspace CRM with the same rules as Studio and the
remote MCP — plan limits, duplicate detection, idempotency, timeline events and workflow
triggers all run server-side in one shared core. Every command takes --workspace <id>.
reopt customer list --workspace <id> [--search <text>] [--tag <t>] [--brand-id <id>] [--journey-id <id>] [--stage-id <id>]
reopt customer count --workspace <id> [same filters]
reopt customer get <customerId> --workspace <id> [--unmask]
reopt customer timeline <customerId> --workspace <id>
reopt customer create --workspace <id> --name <name> [--email --phone --address --record]
[--tag <t>] [--attributes <json>] [--brand <idOrName>] [--journey <idOrName> --stage <idKeyOrLabel>]
[--note <text> | --note-file <path>] [--key <key>] [--on-conflict fail|link]
reopt customer update <customerId> --workspace <id> [--name --email --phone --address --attributes] [--clear-email|--clear-phone|--clear-address]
reopt customer import --workspace <id> --file <path|-> [--dry-run] [--on-conflict fail|link] [--brand --journey --stage --tag]
reopt customer brand link|unlink <customerId> --workspace <id> --brand <idOrName>
reopt customer journey list | set <customerId> --journey <idOrName> [--stage <idKeyOrLabel>] | remove <customerId> --journey <idOrName>
reopt customer tag list | add <customerId> --tag <t>[,<t>] | remove <customerId> --tag <t>
reopt customer note list <customerId> | add <customerId> --text|--file | update <noteId> --text|--file | delete <noteId>
reopt customer field listRules worth knowing:
- Lists mask email and phone (
pii: "masked").customer get --unmaskreturns one customer's raw contact details and is recorded in the workspace audit log. - "Register as a lead of a brand" is one command:
create(or link to the existing customer when the email/phone is already known, with--on-conflict link) →--brand→--journey/--stage→--note, in a single transaction. Nothing is merged by name. - Idempotency: a customer without email or phone needs
--key; otherwise the key derives from the contact. Re-sending the same key never creates a second customer, note or event (status: skipped). - Brand, journey and stage accept an id or an exact name (stage: id, key or label). An ambiguous name is not guessed — the result lists the candidates and the command exits 3.
importtakes a JSON array,{"rows": [...], "defaults": {...}}or NDJSON (--file -for stdin), at most 200 rows, one transaction per row. The result lists every row ascreated / linked / skipped / conflict / unresolved / invalid-attributes / failedwith the customer id, Studio URL and the next action; re-sending the same file only retries the rows that failed.--dry-runvalidates references, duplicates and plan capacity without writing — the real run validates again.updatechanges only the fields you pass (--clear-*removes a value); tags are never touched byupdate— usetag add/tag remove, which keep the other tags.- Registering a customer or linking a brand is not a messaging consent and sends nothing.
$ reopt customer import --workspace <id> --file ./agency-leads.json --dry-run
$ reopt customer import --workspace <id> --file ./agency-leads.json
$ reopt customer create --workspace <id> --name "쇼엠" --key showm --brand "Reopt Studio" \
--journey "영업 파이프라인" --stage lead --note-file ./showm.md
$ reopt customer tag add <customerId> --workspace <id> --tag "마케팅 에이전시"
$ reopt customer journey set <customerId> --workspace <id> --journey "영업 파이프라인" --stage wonSandbox environments [experimental]
reopt brandapp env list
reopt brandapp env create -n <name> [--ttl 7d]
reopt brandapp env use <envId>
reopt brandapp env destroy <envId> [--force]env create saves only a name → sandbox brandappId mapping to
.reopt/env-credentials.json (mode 0o600) — it does not issue or write
OAuth client credentials. Create the sandbox's OAuth client in Studio and export
BRANDAPP_CLIENT_ID / BRANDAPP_CLIENT_SECRET yourself. (--ai-limit and
--type are accepted for forward-compat but currently ignored by the
environments API — the CLI warns when you pass them.)
Local dev server [experimental]
reopt brandapp dev [-s <schema>] [-p <port>] [--persist] [--seed <path>] [-w] [--proxy-ai]
reopt brandapp seed [-f <file>] [-t <url>] [--reset]Service tokens (CI)
reopt token mint [--ttl 15m] [--scope eav:migrate,eav:read,eav:write] [--env <name>]Exchanges the long-lived Basic-Auth client credentials for a short-lived HS256 JWT (default 15m, capped 1h) scoped to the active brandapp. Use it in CI so a leaked variable expires before it can do harm:
export BRANDAPP_CLIENT_TOKEN=$(reopt token mint --quiet)
# Subsequent EAV / migration commands now send Authorization: Bearer instead
# of Basic auth. `--quiet` emits the bare token on stdout; the summary goes to
# stderr. Without --quiet the token is one field of the JSON envelope.MCP server
reopt mcp # stdio MCP server — exposes the same tools as the CLI
# (every tool ships a title + read-only annotations, so MCP clients
# can auto-approve reads instead of prompting on every call)The stdio server advertises the registry list/get tools, the CRM customer tools
(reopt_customer_* and reopt_journey_list — the same contracts as the remote
MCP, served through the logged-in session), plus reopt_status, reopt_schema,
reopt_brandapp_doctor, reopt_sdk_inspect, and reopt_schema_validate. The
EAV entity/record, segment and feedback/proposal tools are served by the remote
(web) MCP endpoint, not stdio. Write tools carry readOnlyHint: false and an
explicit destructiveHint, so MCP clients can ask before overwriting or
deleting.
Environments and config (.reopt.config.mjs)
Project metadata and per-environment credential routing live in a
.reopt.config.mjs at the project root. Secrets never go in the file — only
the names of the env vars that carry them.
// .reopt.config.mjs
import { defineConfig } from "@reopt-ai/cli/config";
export default defineConfig({
brandapps: [{ cwd: ".", brandappId: "abc-123", brandappName: "store" }],
environments: {
staging: {
clientIdEnv: "BRANDAPP_CLIENT_ID_STAGING",
clientSecretEnv: "BRANDAPP_CLIENT_SECRET_STAGING",
baseUrlEnv: "REOPT_BASE_URL_STAGING",
},
},
});Then every EAV / token / env command accepts --env <name> (falling back
to BRANDAPP_ENV) to route credentials to that environment's env vars:
reopt brandapp eav migrate --env stagingTrust model. Because
.reopt.config.mjsexecutes JavaScript on load, the CLI records a hash of each file the first time it runs one and prompts before executing an untrusted or changed file. SetREOPT_TRUST_CONFIG=1to trust and record non-interactively (CI / headless). The legacy.reopt.jsonshape still loads (withoutenvironments) and is exempt from the trust check.
Lockfile
reopt brandapp eav sync writes eav.lock next to your schema. The lockfile maps
entity/attribute names to their server IDs and survives renames. Commit it.
Add generated/eav.ts to .gitignore if desired.
Exit codes
| Code | Meaning |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | Success |
| 1 | API / network error |
| 2 | Auth error (missing/expired session, 401) |
| 3 | Validation error (bad input, unknown resource; workspace apply needing a prompt with no TTY; a customer result that is a conflict, an unresolved name or a failed import row) |
| 4 | Config error (missing credentials, malformed .reopt.json) |
| 5 | Internal error (uncaught) |
| 6 | Drift detected (eav history saw drift; workspace plan --exit-code / drift saw changes) |
| 7 | Destructive blocked (eav sync / eav records delete-where without --force; workspace apply without --allow-destructive) |
| 8 | Checksum mismatch (eav migrate / verify — local file ≠ applied row) |
| 9 | Checksum conflict (same migration name applied with a different checksum); workspace apply --plan when the saved plan is stale |
| 10 | Lock held — another eav migrate / eav sync / workspace apply runner holds the advisory lock, or the lock store is unreachable |
CI / automation
# Provide credentials non-interactively
export BRANDAPP_CLIENT_ID=...
export BRANDAPP_CLIENT_SECRET=...
export BRANDAPP_ID=...
# Sync the schema, fail the job if anything goes wrong
reopt brandapp eav sync --dry-run --format json | jq -e '.success'For interactive tools that should never block on stdin, pass --no-interactive.
Agent plugin (skills + MCP server)
The package is an Agent Plugins 1.0.0 plugin, so an agent client that speaks the standard picks up both halves from the installed package — no manual wiring:
- Skills (
skills/) —reopt-shared(auth, global flags, exit codes),reopt-brandapp(linking, listing, health checks), andreopt-eav(schema-as-code, migrations). - MCP server (
mcp.json) — the hosted server athttps://mcp.reopt.ai, which carries the full tool set (including the EAV entity/record tools) and authenticates over OAuth.
Point your client at the installed package root
(node_modules/@reopt-ai/cli); it reads plugin.json there and discovers the
rest from the standard's fixed locations.
For a client that does not implement Agent Plugins, the same two pieces are
still usable directly: load skill markdown from
node_modules/@reopt-ai/cli/skills/, and register https://mcp.reopt.ai as a
remote MCP server yourself.
The plugin deliberately ships only the hosted server. The local stdio server
(reopt mcp, see MCP server) overlaps it by 10 tools and needs a
separate CLI login, so bundling both would hand the client two copies of the same
tools with one copy failing until you log in. Use the stdio server for work that
needs your local project — reopt_schema_validate, reopt_brandapp_doctor,
reopt_sdk_inspect, reopt_status — and register it manually as documented.
License
MIT
