npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reopt-ai/data-cli

v0.2.0

Published

reopt-data command line: provision organizations and projects, run analytics queries, and upload source maps — built for CI and AI agents

Downloads

311

Readme

@reopt-ai/data-cli

reopt-data — the command line for a reopt-data deployment. Provision organizations and projects, run analytics queries, manage the event catalogue, and upload source maps from CI. Built to be driven by scripts and AI agents as much as by people.

For the guided Korean documentation, see CLI 사용하기.

npm install -g @reopt-ai/data-cli     # or: npx @reopt-ai/data-cli --help
reopt-data --help

Node 22 or newer. Runtime dependencies: @reopt-ai/data-contract, zod, and @modelcontextprotocol/server (loaded only by reopt-data mcp). Two capabilities need optional packages installed next to the CLI, and only those commands load them:

  • platform authority (org *, sourcemap upload --platform, sourcemap list-platform / delete-platform): @reopt-ai/brandapp-sdk (^4.7.0-rc.6, an optional peer) and @better-auth/core (>=1.7.7, which brandapp-sdk's machine signing needs) — npm install -g @reopt-ai/data-cli "@reopt-ai/brandapp-sdk@^4.7.0-rc.6" "@better-auth/core@>=1.7.7" (or npx -p @reopt-ai/data-cli -p "@reopt-ai/brandapp-sdk@^4.7.0-rc.6" -p "@better-auth/core@>=1.7.7" reopt-data …). Give the ranges: until brandapp-sdk 4.7.0 is released, its latest tag has no machine signing. Without them those commands exit 4 with this same install line; every other command works.
  • direct source-map uploads: @vercel/blob.

Commands

| Group | Command | Needs | Effect | | ----------- | -------------------------------------------------------------------------- | -------------------------- | ------------------- | | top-level | login, status, logout | browser-approved login | session lifecycle | | top-level | tools, mcp, completion | none | discovery / server | | account | show | browser-approved login | read-only | | org | create, update | machine client (platform) | writes | | org | rotate-key | machine client (platform) | destructive --yes | | project | list, get | org key | read-only | | project | create, update | org key | writes | | project | delete, rotate-secret | org key | destructive --yes | | event | set-meta | org key | writes | | query | timeseries, funnel, retention, traffic-overview, traffic-sources | client credential | read-only | | segment | list, get, preview | client credential | read-only | | sourcemap | inject, upload | key by target | writes, --dry-run | | sourcemap | list, delete | org key | read / destructive | | sourcemap | list-platform, delete-platform | machine client (platform) | read / destructive | | event | init, pull, diff, verify, push, types, history | org key (init/types: none) | see below | | top-level | pull, diff, verify, push, import, unlink (Project as Code) | org key | see below | | config | link (also reopt-data link), show | none | local writes |

reopt-data <group> <command> --help prints every flag with its type, default and whether it is required — generated from the same schema the parser validates against. Shell completion is generated from that registry too:

reopt-data completion bash
reopt-data completion zsh
reopt-data completion fish

Apply it to the current shell with source <(reopt-data completion bash), source <(reopt-data completion zsh), or reopt-data completion fish | source.

Credentials

Browser user session

Use the browser flow for a personal Reopt Data session:

reopt-data login
reopt-data status
reopt-data account show
reopt-data logout

login prints an 8-character code, opens /cli-auth, and polls until the signed-in browser approves it. On a remote machine use --no-browser and open the printed URL elsewhere. --force replaces a still-valid saved login. The password never enters the terminal, the browser URL contains only the display code, and the separate 256-bit device secret stays in CLI memory.

The resulting session is stored atomically at ~/.reopt/data/auth.json; the directory is owner-only (0700) and the file is 0600 on POSIX. REOPT_DATA_AUTH_FILE can redirect this path for an isolated runtime. REOPT_DATA_USER_TOKEN supplies an environment-owned session for headless use; logout never revokes that value and tells you to unset it.

login --format json is the one streaming command: it writes NDJSON events (verification_uri, polling, success) so an agent can approve the flow while the process is still running. The token is never included. status validates the session with the server, silently persists an extended expiry, and reports only identity/expiry plus booleans for deployment credentials. account show uses the scoped session to list the signed-in user and only their organization/project memberships.

The status.session value is valid, missing, invalid, corrupt, or unreachable. An unreachable server leaves authenticated as null instead of claiming that a cached session is valid. Use login --force to replace an invalid or corrupt session. logout removes the local file even when the server cannot confirm revocation and reports that condition on stderr.

Personal login and deployment credentials are intentionally separate. Browser cookies and CLI bearer tokens have different server-enforced audiences; neither can be replayed as the other. The CLI token currently carries only account:read and does not replace the platform/org/client credentials required by provisioning, catalogue, query, or source-map commands.

Deployment credentials

Set them in the environment. Flags (--machine-client-id, --org-key, --client-id, --client-secret) exist for one-off use; they land in shell history. The machine private key is never a flag. A variable exported empty (an unset CI secret) counts as unset everywhere. The machine credential is read only by the commands that need it: a malformed REOPT_CLIENT_PRIVATE_KEY_JWK fails those with exit 4 and nothing else, and a token endpoint that refuses the client is exit 2 (not retried).

| Variable | Issued by | Used by | | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | REOPT_DATA_API_URL | you; default https://data.reopt.ai | everything | | REOPT_DATA_USER_TOKEN | external secret injection (optional) | status, account show; never persisted or revoked by logout | | REOPT_DATA_MACHINE_CLIENT_ID + REOPT_CLIENT_PRIVATE_KEY_JWK (+ REOPT_ID_BASE_URL) | the deployment operator — its reopt machine OAuth client (RFC-0034: private_key_jwt + DPoP, scopes svc:reopt-data:*); generate the key with npx @reopt-ai/brandapp-sdk reopt-keygen | org *, platform uploads | | REOPT_DATA_ORG_KEY | org create (once) / org rotate-key | project *, event *, project & host uploads | | REOPT_DATA_CLIENT_ID + REOPT_DATA_CLIENT_SECRET | project create (once) / project rotate-secret | query * |

A command that lacks its credential exits 2 before making any request and names the variable to set.

Output contract

  • stdout carries the result and nothing else. Default --format json prints the server's response verbatim — the shape documented in @reopt-ai/data-contract. --format text gives a one-line human summary where one exists; table, csv, and yaml provide inspectable or pipeline-friendly views.

  • --fields project.id,project.name,clients.server.id selects comma-separated dotted fields before rendering; segments[].id,segments[].name gives one row per array item, and on a list such as project list plain names (--fields id,name) select the rows' fields. It works with every format.

  • Cursor-list commands accept --page-all, --page-limit <n>, and --page-delay <ms>. JSON streams one selected item per line (NDJSON); the other formats aggregate items before rendering. Reaching --page-limit while another cursor exists exits 6 so automation cannot mistake a truncated list for a complete one.

  • stderr carries progress (--quiet silences it) and, on failure, one envelope:

    { "error": "auth_error", "message": "…", "exitCode": 2, "status": 401, "apiCode": "invalid_key", "requestId": "…" }
  • Exit codes name the caller's next move:

    | Code | Name | Do | | ---- | ----------------------- | ----------------------------------------------------------- | | 0 | success | | | 1 | api_error | server or network failed; retryAfterMs if the server said | | 2 | auth_error | set or rotate the credential named in message | | 3 | validation_error | fix the input; details.issues lists each field | | 4 | config_error | environment incomplete (URL, directory, peer dependency) | | 5 | internal_error | CLI bug — please report with requestId if present | | 6 | partial_failure | some items failed; the result on stdout lists them | | 7 | confirmation_required | destructive command; re-run with --yes | | 8 | drift_detected | event verify: file and server disagree; pull or push |

Giving input

Three equivalent ways, merged (later wins):

# flags, kebab-case, one per field
reopt-data query funnel --project-id prj_1 --start-date 2026-08-01 --end-date 2026-08-31 \
  --steps view_item --steps add_to_cart --steps purchase

# one JSON object — inline, from a file, or from stdin
reopt-data query funnel --input '{"projectId":"prj_1","steps":["view_item","purchase"],…}'
reopt-data query funnel --input @funnel.json
echo '{…}' | reopt-data query funnel --input @-

# positionals for ids
reopt-data project get prj_1

Arrays of scalars repeat the flag; nested objects take JSON (--property-filters '[{"key":"plan","value":"pro"}]'). null clears a nullable field (--symbol-host-app null). Booleans stand alone (--conversion, --no-conversion).

For agents

reopt-data tools --json

returns every command with its inputSchema (JSON Schema 2020-12), flags, which credential it needs, whether it mutates, its confirmation policy, optional cursor pagination metadata, supportsDryRun, and ready-to-run examples. Read it once; the schemas are the contract package's own, so what validates here validates at the server.

Rules an agent can rely on:

  • A destructive command never runs without --yes — it exits 7 first, before any network call.
  • Validation and credential checks happen before any request; exit 2 and 3 mean nothing was sent.
  • --dry-run on sourcemap upload and sourcemap inject touches nothing and needs no key. On any other command it is refused (exit 3) rather than ignored.
  • Creating with the same --external-id twice is safe: the second call answers created:false and returns no secret.
  • stdout is always parseable when the exit code is 0 or 6.

MCP

reopt-data mcp

serves the same tools over the Model Context Protocol on stdio — one entry per command, named reopt_data_<group>_<command> (reopt_data_project_create, reopt_data_query_funnel, …). A call behaves like the same CLI command run in the server's directory: deployment credentials come from the flags given to mcp (--api-url, --org-key, --client-id/--client-secret, --machine-client-id), then the environment; --timeout, --config and --env (else REOPT_DATA_ENV) apply to every call; and the repository config is read on each call, so its defaults — projectId, events.file, definition.dir, sourcemaps.dir — fill omitted arguments and a config link takes effect on the next call. Per-command flags (--yes, --dry-run, --page-all, --fields, --format, --json) are refused on mcp, whatever their value; tools take confirmed / dryRun instead. A cursor list (project list, segment list, …) returns one page per call, and when another page exists the result's second content block names the cursor to pass for it. account show can use the saved browser-approved login or REOPT_DATA_USER_TOKEN. Destructive tools take a required confirmed: true argument instead of --yes; conditionally risky tools such as event push expose it as optional and require it only when the computed plan needs confirmation. Dry-runnable tools take dryRun. Annotations (readOnlyHint, destructiveHint) are derived from the same definitions, and every nonzero result returns the CLI's error envelope as its text with isError: true.

{
  "mcpServers": {
    "reopt-data": {
      "command": "npx",
      "args": ["-y", "@reopt-ai/data-cli", "mcp"],
      "env": { "REOPT_DATA_ORG_KEY": "…" }
    }
  }
}

The same registry is importable for in-process use:

import { allTools, run, toolCatalog } from "@reopt-ai/data-cli";

Segments

Segments are the saved audiences somebody defined in the console. The CLI reads them; it does not create them. Defining one means validating a condition tree against the project's catalogue and then computing it — expensive work that belongs behind a console somebody is looking at, not behind an unattended script.

reopt-data segment list --project-id prj_1 --format table
reopt-data segment list --project-id prj_1 --kind static --limit 10
reopt-data segment get --segment-id seg_1
reopt-data segment preview --project-id prj_1 --segment-id seg_1

list omits the condition tree — summary carries one rendered line per condition, which is what a listing needs — and get returns the tree in full. A segment id that does not exist and one that belongs to another project both answer 404 (exit 3), for get and for preview --segment-id alike, so the answer does not tell a credential which ids exist elsewhere.

preview takes either a saved --segment-id or a definition through --input, and answers with three numbers:

  • deviceCount is the segment. Actors are devices, so anonymous visitors count; a segment like "visited but never purchased" is mostly them.
  • profileCount is how many of those devices have been identified.
  • totalDevices is the whole project, as a denominator.

deviceCount is null on a listed segment that has never been calculated. That is not the same as a segment with no members, and a script that treats it as zero will report a healthy audience as an empty one.

Event catalogue as code

Declare the project's event catalogue in reopt-data.events.json next to the code that emits the events, and let CI keep the server in step with the commit that ships:

{
  "$schema": "https://data.reopt.ai/schemas/event-catalog.v1.json",
  "version": 1,
  "projectId": "prj_123",
  "events": {
    "purchase": { "displayName": "Purchase", "conversion": true, "rollupProperties": ["plan", "currency"] },
    "healthcheck": { "status": "internal" },
    "$pageview": {},
  },
}

The file is the truth: a field you leave out is the default, and an event you remove is archived (never deleted). A lock file beside it (reopt-data.events.lock.json, commit it) records the last sync so diff can tell your edits from console edits.

reopt-data event init --project-id prj_123    # or: event pull, to start from what the server has
reopt-data event diff --format text           # + create  ~ update  - archive  ! changed on the server
reopt-data event push                         # plan only
reopt-data event push --apply --yes           # write; --yes covers metric-changing and expensive changes
reopt-data event verify                       # CI gate: exit 8 on drift
reopt-data event types --out src/reopt-events.d.ts   # ReoptEventName / ReoptEventProperties for a typed track()

Changes are graded: safe (display fields), metric-changing (conversion, status) and expensive (rollupProperties, which queues a rollup rebuild); the last two need --yes. A push sends the last sync's updatedAt with every event, so a console edit made in between is refused as a whole (409) instead of half-applied — event pull first, or --force to overwrite. event pull refuses while the file has unpushed edits — events and, in a version 2 file, properties (archived events are not edits). In a version 2 file the property half is a second request: if it fails after the events were written the push exits 6, the lock records the event half, and re-running finishes the job.

Project as Code

The same treatment for the rest of a project's console settings. Three files under reopt-data/ declare the project's settings, the error-tracking rules and the alerts the repository owns; one lock beside the directory records the last sync:

// reopt-data/project.json
{
  "$schema": "https://data.reopt.ai/schemas/project-settings.v1.json",
  "version": 1,
  "projectId": "prj_123",
  "settings": { "timezone": "Asia/Seoul", "retentionDays": 90, "repositoryBranch": "main" },
}
// reopt-data/error-rules.json — array order is evaluation order
{
  "$schema": "https://data.reopt.ai/schemas/error-rules.v1.json",
  "version": 1,
  "projectId": "prj_123",
  "rules": [
    {
      "key": "ignore-vendor",
      "name": "Ignore vendor bundles",
      "action": { "kind": "suppression" },
      "filters": [{ "field": "path", "operator": "startsWith", "value": "/vendor/" }],
    },
    {
      "key": "checkout-owner",
      "name": "Checkout errors",
      "action": { "kind": "assignment", "assigneeUserId": "usr_9" },
    },
  ],
}
// reopt-data/alerts.json — no order; the destination is a secret, so it is a reference
{
  "$schema": "https://data.reopt.ai/schemas/alerts.v1.json",
  "version": 1,
  "projectId": "prj_123",
  "alerts": [
    {
      "key": "purchase-drop",
      "name": "Purchases below 10/h",
      "eventName": "purchase",
      "condition": { "kind": "event_count", "operator": "lt", "threshold": 10, "timeWindow": "1h" },
      "channel": "slack",
      "destination": "${env:SLACK_ALERTS_URL}",
    },
  ],
}

pull also writes the three files' JSON Schemas to reopt-data/.schemas/ and points each file's $schema at its local copy, so an editor validates and completes them without the network (commit that directory too; the same schemas are served at https://data.reopt.ai/schemas/). reopt-data.lock.json (commit it) holds the server's updatedAt and a hash of the resolved fields for the project and every keyed rule and alert, so diff can tell your edits from console edits. The files are the truth: a setting you leave out is the default, and a keyed rule or alert you remove is deleted. Ownership is the key — an item with one belongs to the repository, an item without one belongs to the console and is never touched (it is counted, not changed).

reopt-data pull                                  # server → files + lock; refuses over local edits (--force discards them)
reopt-data diff --format text                    # kind / key / operation / grade table; always exit 0
reopt-data verify                                # CI gate: exit 8 when either side moved
reopt-data push                                  # plan only
reopt-data push --apply --yes                    # write; approve metric-changing/expensive changes
reopt-data import error-rule rule_abc --key ignore-vendor   # give a console rule a key and add it to the file
reopt-data import alert alr_123 --key purchase-drop         # same for an alert
reopt-data unlink alert purchase-drop            # drop the key and the file entry; the alert stays on the server

Grades: retentionDays shortened, newly set or cleared is destructive (the inherited limit may purge rows); timezone and the lifecycle / power-user thresholds are metric-changing; creating, editing, removing or reordering a suppression or grouping rule is metric-changing (it changes which issues exist); alert changes and the remaining declared fields are safe; unknown fields default to destructive. --yes approves a risky grade; destructive changes additionally require --allow-destructive. --force skips compare-and-set for deliberate overwrites. --prune-imported separately permits deleting keyed items imported since the last pull. The directory defaults to ./reopt-data and can be set once with definition.dir in reopt-data.config.mjs.

${env:NAME} references

An alert's destination (a webhook URL, a Slack hook, an address) must not be committed. Write ${env:NAME} instead — upper-case, as CI secret stores spell it — and the CLI substitutes it from the environment before comparing or sending anything. The rules:

  • diff, verify and push fail with exit 3 when any referenced name is unset, listing every missing name at once (unresolved ${env:…} references in reopt-data/alerts.json: SLACK_ALERTS_URL, PAGER_URL — set them in the environment), and send nothing. The server only ever sees the resolved string.
  • The lock hashes the resolved value, so ${env:SLACK_ALERTS_URL} and the literal it stands for are the same alert. Rotating the secret in the environment is a change; reformatting the reference is not.
  • Reports show the reference, never its value, and never the server's current destination either ("from": "<redacted>").
  • pull keeps a reference only when the existing alerts.json declares it for the same key and it resolves to exactly the server's value. Otherwise the server's literal is written and the key is listed under secretsWritten — replace it with a reference before you commit. A reference the current environment cannot resolve proves nothing and is replaced too.

Environments

One repository, several deployments: name them in the config and pick one with --env <name> (or REOPT_DATA_ENV). The files are shared and carry no projectId; each environment keeps its own lock, reopt-data.<env>.lock.json.

// reopt-data.config.mjs — imports nothing, so it loads wherever the CLI runs from (global install, npx)
/** @type {import("@reopt-ai/data-cli").Config} */
export default {
  environments: {
    staging: { projectId: "prj_staging" },
    production: { projectId: "prj_production", apiUrl: "https://data.example.com" },
  },
};
reopt-data pull --env staging          # writes reopt-data/*.json without projectId and reopt-data.staging.lock.json
reopt-data verify --env production     # the same files against the production project

Precedence: the project is the environment's projectId; without --env it is the files' projectId, then --project-id, then config.projectId. --env also makes the environment's projectId the default for every other command (query, project update, …), not only these. A file pinned to a different project than the active environment is refused (exit 3) naming the file and both ids. The API URL is --api-url > REOPT_DATA_API_URL > environments[env].apiUrl > config.apiUrl. An --env the config does not define exits 4 listing the known names.

Project as Code concurrency

push --apply sends settings, error rules and alerts in one atomic request. Any validation or compare-and-set conflict rolls back all submitted changes. The returned transaction snapshot becomes the lock baseline; unrelated later edits remain visible in the next diff. Locally detected conflicts exit 3 before writing; HTTP 409 races also exit 3 with apiCode: "conflict".

Rule order is stored as an explicit errorRuleOrder array in lock v1, including numeric keys. pull protects local reordering. A legacy lock with multiple rules and no order requires reviewing local order before pull --force refreshes it. --prune-imported includes known imported deletions in the diff and grade gate, so deleting suppression/grouping rules requires --yes. Without a lock (a fresh CI checkout, the first run under a new --env) a keyed item the files do not declare is reported as imported, never planned as a deletion: --force only skips compare-and-set and cannot delete it. An item whose tracked fields the diff shows unchanged is sent with the updatedAt this run read, so a console save that changed nothing the files track (a rename, a mute, a reorder of other rules) does not fail the push. diff and verify report a declaration the console would not save as a warning; push refuses only the ones it would write. Imports arriving after that read still cause a conflict. Malformed or nested ${env:...} references fail validation.

A push creates a private <lock-path>.pending.json before sending its UUID operationId. If the response or local lock write fails, rerun the same push --apply command. It recovers the original committed receipt before attempting another write; later console edits remain drift. If no receipt exists, the original files, lock, target, environment values and flags must match before that operation can be retried. Do not delete the pending file while the result is unknown. The lock is replaced atomically. Ignore pending and temporary files in version control; commit the lock. Server receipts are retained until project deletion and require the project's org key.

The server also rejects a direct project PATCH or definition PUT that could shorten retention without allowDestructive: true (409 destructive_blocked). Error envelopes include classification, retryable, and resolution.action/message. Full control API schemas, approvals and receipt recovery are available at /openapi.json. The console displays repository keys and the last definition push with an edit warning.

Source maps in CI

# after `next build`
reopt-data sourcemap inject --dir .next/static
reopt-data sourcemap upload --dir .next/static \
  --url-prefix https://shop.example.com/_next/static --project-id "$REOPT_DATA_PROJECT_ID" \
  --release "$GIT_SHA" --delete-after-upload

--delete-after-upload deletes every map the server now holds — uploaded by this run or already there unchanged — and keeps any map with a failed or too-large ref so the next run can retry it. A network error — including a start or finish request that gets no answer within --timeout — is retried and then fails only the refs of that request; the summary always prints, and failures exit 6. The bytes of a map are not held to --timeout, since a large map on a slow link can legitimately take longer. The target resolves flag > REOPT_DATA_PROJECT_ID / REOPT_DATA_ORGANIZATION_ID > the --env entry > the config, and a config prefix or host app is only a default for the target the flags chose.

One bundle serving many projects uploads once per host app, keyed by path:

reopt-data sourcemap upload --dir .next/static --path-prefix /_next/static \
  --host-app studio-web --organization-id org_abc --release "$GIT_SHA"

Inspect or retire uploaded symbol sets without leaving the CLI:

reopt-data sourcemap list --project-id prj_123
reopt-data sourcemap delete sym_123 --project-id prj_123 --yes

reopt-data sourcemap list --scope host --organization-id org_abc --host-app studio-web
reopt-data sourcemap list-platform --host-app studio-web
reopt-data sourcemap delete-platform sym_123 --yes

inject-chunk-ids and upload-sourcemaps — the spellings from @reopt-ai/data-sdk-server's bin — still work, as do its --api-key (either upload key) and --silent (= --quiet) flags. Note the exit code for a partial failure is 6, where the old bin used 1.

License

MIT