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

@datarails-ai/dr-cli

v1.6.139

Published

Datarails CLI

Readme

dr-cli

Datarails CLI. Server keys: US, US2, UK, CA, EU, STUDIO, DEV, DEV-1, TEST — or pass a full https://<host>.datarails.com URL to reach an ad-hoc (e.g. PR-preview) environment. Credentials live in the OS keychain (Windows Credential Manager, macOS Keychain), or in a credential file when one is present — which wins over the keychain, and is how dr runs where no keychain exists (CI, headless hosts, containers). Binary is dr. Node.js 18+ / TypeScript / ESM.

A scriptable, diffable client for a customer's Datarails tenant — talks to the same APIs the web UI does. Most endpoints go directly against the datarails-server monolith; a few paths (e.g. templates sample / templates distinct / templates aggregate) route through the finance-os gateway at /finance-os/api/data_models/v1/raw/… because the monolith's public table API ignores limit/offset. Both accept the same user JWT, so there's nothing extra to configure.

What it does

  • Inspect a customer's environment — tables, dashboards, functions, dynamic ranges, datamappers, storyboards, reports, data flow, lookups, semantic tables, extables.
  • Sample + aggregate rows from any table via the finance-os gateway (templates sample, templates distinct, templates aggregate).
  • Trigger rescans, clone reports, prepare dashboards / storyboards.
  • IT triage — users, organizations, devices, request/receive/download device log bundles, feature flags, FX rates, metrics.
  • Search the customer Help Center — dr kb search, dr kb get, dr kb article-images, etc. Bridges product vocabulary ("reconciliation isn't pairing", "how does FX translation work") to dr-cli verbs.

Every top-level is a resource; subcommands are verbs. --json works everywhere; --server <KEY> is required on anything that hits an API (except dr kb, which is global Datarails content). A PR-preview or other ad-hoc environment is reachable by passing its full URL instead of a key, e.g. dr whoami --server https://<preview-host>.datarails.com (https + a *.datarails.com host only). Run dr <command> --help for the current flag list — the binary is the source of truth.

MCP integration with Claude Code

dr-cli ships an embedded MCP server (dr-zendesk-kb) that exposes the Help Center search/browse surface to Claude Code as native tools — Claude reaches for search_kb automatically when a request is phrased in product terms, no need to type a shell command first. fetch_article_media returns inline screenshots as image content blocks so the LLM can see what the product UI looks like.

Auto-wired on every npm install -g @datarails-ai/dr-cli (the postinstall hook adds an entry to ~/.claude.json). Manage with:

dr mcp list                          # show registered/unregistered status
dr mcp register dr-zendesk-kb        # add (idempotent; postinstall does this)
dr mcp unregister dr-zendesk-kb      # remove (postinstall re-adds on upgrade)

Other LLM assistants (Cursor, Codex, Gemini CLI) get the same capabilities via the shell route: dr kb search "<query>", dr kb get <id>, etc. — same surface, different transport.

Install

End users: the polished install landing page is at docs/install.html (hosted at https://drcli.datarails.com/cli/install.html). It walks through Claude Code install, Node, dr-cli, and per-session Claude usage with copy-to-clipboard buttons. The instructions below are the same flow, just terser.

Requires Node.js 18+ (npm ships with Node — no extra tools needed). The package is published to the Datarails JFrog registry; no auth setup required.

npm install -g @datarails-ai/dr-cli --registry https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual

That puts dr on PATH globally. To update later, re-run the same command — it will pick up the latest version.

If you'd prefer not to type the registry every time, set it once:

npm config set registry https://datarails.jfrog.io/artifactory/api/npm/dr-cli-virtual
npm install -g @datarails-ai/dr-cli
npm update -g @datarails-ai/dr-cli           # later, to update

(That sets your default registry. To scope it only to the next install, drop npm config set and pass --registry inline as above.)

One-shot installer scripts

For non-developer machines, two scripts at the repo root do the whole thing — including installing Node 18+ if it's missing:

  • Windows (PowerShell): install.ps1
  • macOS / Linux (bash): install.sh

Both are also published at the fixed public URL, so users can run them without cloning anything:

# Windows
powershell -ExecutionPolicy Bypass -Command "irm https://drcli.datarails.com/cli/install.ps1 | iex"

# macOS / Linux
curl -fsSL https://drcli.datarails.com/cli/install.sh | bash

Each script checks Node, installs it via the platform's package manager if needed, then runs the npm install -g line above. Re-run any time to update.

Build from source (developers only)

git clone https://github.com/Datarails/dr-cli.git
cd dr-cli
node scripts/install.mjs

The bootstrap installer runs pnpm install, pnpm build, pnpm link --global (makes dr resolve to the workspace), and installs the embedded skills into ~/.claude/skills/ unless the dr-cli plugin manages them. Requires Node 18+ and pnpm.

If dr: command not found after pnpm link --global, run pnpm setup once (adds pnpm's global bin dir to your shell profile), open a new terminal, and dr will be there.

Run it without installing globally

From inside the repo, two equivalents exist:

pnpm dev login --server US           # runs src/cli.ts via tsx (no build needed)
node dist/cli.js login --server US   # runs the built bundle (needs `pnpm build` first)

First-time setup

dr login --server US
dr whoami --server US        # sanity-check identity

Browser opens, you complete the OAuth device-code flow, and credentials are stored in the OS keychain — Windows Credential Manager on Windows, Keychain on macOS. Each session is keyed by (server, email) so you can log in to multiple servers concurrently, and to multiple Datarails accounts on the same server.

Multi-identity per server: if more than one account is stored for a server, every command needs --account <email> to disambiguate. With a single stored identity, --account can be omitted. The error message when ambiguity is detected lists the stored emails.

JWT refresh is automatic. Re-run login only when the refresh token itself dies (days of idle, password change, admin revoke). Never paste the session/JWT — treat them like a password.

Device flow and credential scope. dr login uses the monolith's /oauth/v2/ device flow. The consent page lets you pick the credential's scope (below) before "Allow", and the login stores a cookie-less JWT pair, with no session, refreshed at the external refresh endpoint. The page offers the choice only where the org enforces it; elsewhere the token is unrestricted. This works with a credential file too (DR_CREDENTIALS_FILE), which records the login as "interactiveLogin": true so that dr login and dr logout still accept it.

DR_OAUTH_FLOW=legacy runs the old /oauth/device/code/ flow instead, for a server that does not serve /oauth/v2/ yet (a 404 from the v2 code endpoint says so). A legacy token carries no credential scope, whatever the person would have picked, and the login prints a line saying so. v2 or unset selects the default; any other value is refused. Log in again to switch an identity between flows.

DR_OAUTH_FLOW=legacy dr login --server US   # PowerShell: $env:DR_OAUTH_FLOW="legacy"; dr login --server US

Credential scope

A credential can carry a scope — a resource:action ceiling set by whoever approved it (an agent definition, an API key, a device-flow login), on top of the user's own permissions rather than instead of them. Commands outside the ceiling are hidden from dr --help, and running one anyway — or being refused by a server that enforces the same claim — exits 3 naming the resource:action that is missing, with 40301 meaning the credential's scope does not cover the call and 40302 meaning that endpoint is not scope-declared on the server yet (report those to #dr-cli; neither is fixed by logging in again). Run dr whoami to see the ceiling — it prints the preset name (Read only, Read and write, Full access), or a tree of the products, resources and actions a custom scope allows, or unrestricted (no ceiling on this credential) when the token carries no such claim; dr whoami --json adds the claim exactly as the token carries it, under scopes — an array, or the space-separated string an OAuth-shaped token uses, or null when there is none.

Credential file (instead of the keychain)

Where the OS keychain cannot be used — CI runners, headless Linux, containers — dr reads credentials from a file instead. The file wins over the keychain: if it is there, every command uses it, even on a host whose keychain works. Only when the path below is absent does dr touch the keychain.

~/.dr/session.json            # default location
DR_CREDENTIALS_FILE=<path>    # override

The file holds one identity:

{ "access": "<jwt>", "refresh": "<jwt>", "server": "DEV", "email": "[email protected]" }

It may instead hold a Datarails API key. dr exchanges it for a short-lived access JWT on demand, so such a file authenticates every command without ever holding a session cookie — the shape a CI provisioner writes. A key carries no refresh token, so the exchange runs again when the JWT goes stale; a refusal never deletes the stored key. dr login refuses the file — run dr login --api-key to rotate the key, and update apiKeyExpiresAt alongside it if the file carries one, since nothing derives that date — and dr logout removes it. The refusal stands even if the file also carries a session cookie, and applies to a keychain identity holding a key just the same: the key is what every later command authenticates as, so a browser login over it would succeed under one identity and run as another.

{ "apiKey": "<key>", "server": "DEV", "email": "[email protected]" }

The key may instead live in an environment variable, with the file carrying only the variable's name — the shape for CI (the key stays in the runner's secret env, never on disk) and for agent sandboxes (where the variable's in-sandbox value is an opaque vault placeholder substituted at egress, passed verbatim and never validated):

{ "apiKeyEnvVar": "DR_API_KEY", "server": "DEV", "email": "[email protected]" }

apiKey and apiKeyEnvVar are mutually exclusive — a file carrying both is refused rather than guessed about. An unset variable fails the command with an error naming it; the stored file is never modified by that failure.

email names the stored identity — what --account selects and what the keychain entry is keyed by. It need not match the principal the key is bound to; the token the exchange mints is what every request authenticates as, and dr whoami reports that token's own claims.

accessApiKeyFingerprint is written by dr, not by you: it records which key minted the access beside it, so replacing apiKey is picked up on the very next command rather than after the stale token expires. Leave it alone when you rotate — a fingerprint naming the old key is exactly what triggers the exchange.

apiKeyExpiresAt is optional and read strictly: YYYY-MM-DD, or a timestamp carrying an explicit offset (2026-12-01T09:00:00Z, …+02:00). An offset-less timestamp would mean a different day on a laptop than on a CI runner, so it is rejected — and a rejected value is simply never read, so the warning silently stops rather than erroring.

dr login works normally against a file store — the device flow writes the session into it, and --server defaults to the file's own server. It re-authenticates the identity the file names; logging in as a different account fails rather than silently writing nothing, since one file holds one identity. dr logout removes the file (--server and --all both mean that one identity, and neither flag is required). dr login-as is refused while a file is in use, for the same one-identity reason — it has no second slot to store the impersonated session in. dr whoami prints an Auth: line naming the file in use.

Credentials provisioned by something other than dr login carry no session cookie. For a bare token pair, dr refuses login and logout, since whatever provisioned it owns renewing it; remove the file to log in interactively instead. An API-key file is different: see below.

The file is written 0600 and holds live tokens — treat it like an SSH private key. Note it is not ~/.dr/credentials.json; that path is a legacy pre-keychain store which dr deletes on sight.

Upgrading from a pre-keychain build? On the first run after upgrade, dr-cli detects any legacy ~/.dr/credentials.json and deletes it. The plaintext tokens are removed; your existing sessions are NOT migrated. Run dr login --server <key> once per server.

Tip — using "Login As" (JWT). Log in once as the operator/support user with dr login --server US (browser device flow). Then impersonate any customer account without another browser round-trip: dr login-as <email> --server US. It resolves the email to a user id via dr users can-login-as, mints an impersonation JWT from the monolith (POST /jwt/api/loginas/), and stores it as a switchable identity keyed by (server, email) — so later commands target it with --account <email>. The impersonated token refreshes silently, and a dead refresh auto-remints from the operator, so you're never re-prompted until the operator's own login expires. Use --as <operator-email> to pick the operator when several are stored, or --user-id <n> to skip the lookup.

API keys (headless and CI)

A user-bound API key is a durable credential dr exchanges itself for a short-lived JWT at POST /jwt/api/apikey/token/. There is no refresh token and nothing to rotate on the client: the key is the only thing stored, and it is exchanged again whenever the JWT is past half its lifetime.

Today a key comes from an operator calling the platform's internal provisioning endpoint; there is no self-service dr command yet.

Save it once per person per machine:

dr login --api-key --server US          # hidden prompt on a terminal
printf '%s\n' "$DR_API_KEY" | dr login --api-key -s us   # piped

Piped, -s is required unless a credential file already names the server: the interactive picker reads stdin too, and would swallow the key.

Piping keeps the key out of argv and shell history, but not out of a trace: don't run the piped form with shell tracing (set -x), CI step-debug, or PowerShell script-block logging enabled — any of those persists the key verbatim in a log.

The key is never taken as an argument (it would land in shell history and ps). dr exchanges it once, reads your email from the returned JWT, and stores the key in the keychain, or in the credential file when one is in use. dr logout removes it. dr login-as cannot run from an API-key identity: the server refuses to mint further credentials from an exchanged token.

For CI, write the credential file by hand instead of running any command:

{ "apiKey": "<key>", "server": "US", "email": "[email protected]" }

chmod 600 it right after writing, and keep it that way: the mode stays yours. dr writes its own replacement at 0600 only when it saves a freshly exchanged JWT — which is deferred for as long as an access token you provisioned alongside the key is still fresh, and never happens at all if the key is bad, revoked, or API keys are off for the org. A file left at 0644 can stay 0644 indefinitely. Keep it out of the repo checkout and out of any directory a build-artifact or log-collection step will pick up.

Optionally add "apiKeyExpiresAt": "<ISO date>" — YYYY-MM-DD, or a full timestamp carrying an explicit offset. It is read strictly, in the same form the credential-file section above describes: an offset-less timestamp is rejected, and a rejected value is never read. When the date is within 14 days, dr prints a one-line warning on stderr, once per identity: from every command that authenticates, and from whoami, which is where an operator goes to inspect a credential. login, logout and login-as do not show it. Keys expire and cannot be extended, so rotation is create-new then revoke-old.

If the server answers 401, dr says the key may be revoked or expired, API keys may not be enabled for your organization, or the server may be temporarily unable to tell. It cannot distinguish these, and it never deletes the stored key on failure. If you suspect a key has leaked, ask an admin to revoke it and issue a new one — dr cannot do either itself.

Claude Code skills and plugins

Every skill lives under plugins/<plugin>/skills/<name>/ and reaches people two ways:

  • Claude Code plugins — the dr-internal-plugins marketplace lists three plugins built from this repo at the release branch: dr-cli (install, doctor, call, the update hook, the dr-cli command skill, dr-switch-user), dr-cli-integrations, and dr-cli-pat. Install with /plugin install dr-cli@dr-internal-plugins; a satellite pulls dr-cli in. Every plugin version equals the CLI version, and the dr-cli hook installs that CLI.
  • The npm package — npm install -g @datarails-ai/dr-cli embeds the same skills and its postinstall writes them to ~/.claude/skills/<name>/ (plus a Gemini mirror when ~/.gemini/ exists). On a machine where Claude Code has the dr-cli plugin, the Claude-side write is skipped, and the copies an earlier install wrote for skills an installed dr-cli plugin serves are removed (manifest-listed files only); dr skill install ~ --force writes them anyway.

The three wrapper skills (install, doctor, call) carry a .plugin-only marker and never embed into the binary.

Writing a new skill or a new plugin: CONTRIBUTING.md.

dr skill list             # every embedded skill
dr skill install ~        # user-space install (plugin-managed machine: skipped, and the copies the plugins serve are removed)
dr skill print <name>     # print an embedded skill's SKILL.md

Command surface

| Area | Commands | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Auth | login, login-as <email>, logout, whoami | | Templates (tables) | templates list/get/create/sample/distinct, templates aggregate | | Data models | data-models list/get | | Datamappers | datamappers list/get/create/update/delete | | Dashboards | dashboards list/get/create/delete/prepare, dashboards set-date-picker <id> --mode <preset> \| --year YYYY \| --quarter YYYY-Qn \| --month YYYY-MM \| --day YYYY-MM-DD | | Folders | folders list/create, folders reorder --container <root\|id> (--items folder:12,entity:87 \| --move <kind:id> --to-index <n>), folders move <id> --parent <root\|id> (dashboard / story picker tree, flag-gated; reorder and move are two-step confirm; a dashboard moves with dashboards set-folder) | | Functions | functions list/get/create/delete | | Dynamic ranges | dynamic-ranges list/get/data/create/delete | | Lookups (LUTs) | lut list/create/update/delete/reorder/unmapped (alias lookups) | | Documents | documents support-api set <id> --on/--off, documents calculate <id> --year/--quarter/--month/--day/--week ... [--backward] [--no-roll] | | Semantic tables | semantic-tables list/get | | OData (V1) | odata tables, odata metadata <table-id>, odata data <table-id> [--variant latest\|full\|cleared\|full-cleared] [--top N \| --all] [--csv\|--json] — reads a table exactly as Excel / Power BI / SQLConnector receive it | | Storyboards | storyboards list/get/slides/prepare/delete | | ExTables | extables list/get/clone/delete | | Reports (Financials) | reports list/get/clone/update/delete | | Data flow | dataflow get | | Rescan / scan | scans status <table_id>, rescan table (refuses UsageStats) | | Metrics / FX | metrics …, fx … | | Features | features … | | Users (IT) | users list/get/me/can-login-as/set-email/bulk-set-email/bulk-create [--no-register-identity]/register-identity | | Organizations (IT) | organizations list [--mini], organizations get (alias orgs) | | Support Manager | support-manager list [--search/--ordering/--page/--classification] (alias sm) | | Devices (IT) | devices list/get/logs/received-logs/commands/download-logs/request-logs/command-delete | | Fileboxes | filebox list/get/versions/sheets/preview/tags/create/upload/upload-version/update/archive/set-version-cycle/kind-explain/kind-summary (alias fileboxes; archive is the rename of the former delete — server-side it's a soft-delete; set-version-cycle tags a version in a forecast/budget filebox with its cycle date + scenario, and upload-version --date --scenario does it at upload time; kind-explain/kind-summary say why a filebox has its type — support seat only) | | Integrations | integrations list/get/types/status/query-status/errors/sync/test-connection/add/update/delete/share (alias integration) | | Integrations (IT) | integrations it list/get/add/update/delete/resync (file-sync surface — OneDrive/SharePoint/Egnyte) | | Widgets | widgets list/get/data/create/update/delete + flag-driven widgets chart/kpi/pie/table/text (alias widget; data applies the dashboard's saved filter bar + date picker by default so results match the product — --no-dashboard-context computes from the widget's own settings only) | | Forecast | forecast cycles list/get/documents/lock/unlock/set-default/roll-forward, forecast scenarios list/get/versions/create/delete/lock/unlock/set-default/clone/clone-from-budget/bulk-edit | | Collection processes | collection-processes list/get/create/set-tag-config/add-fileboxes/add-folders/calculate (alias collection-process; CPs are COLLECTION buckets containing N child fileboxes; set-tag-config updates the CP root document only — see --help for the per-child fan-out recipe; no delete verb) | | Spend Control | spend-control billing-frequencies list, spend-control contracts extract-from-files <files...>, spend-control contracts bulk-create --from <json>, spend-control vendors list/app-store/with-contracts/duplicates/categories, spend-control vendors owners <vendor_id>, spend-control vendors create <name>, spend-control vendors set-visibility <ids...> --visible <bool>, spend-control alerts list, spend-control alerts settings | | Workflows v2 | workflow list/get/create/update/activate/deactivate/archive/restore/delete/clone, workflow process list/get/delete, workflow step get/create/update/delete/reorder, workflow task list/get/add-file/add-checklist/update/update-one/delete/notify/approve/reject/sign-off, workflow member list/check/add/update/remove/grant-bulk, workflow group list/add/update/remove/grant-bulk (non-MEC; tenant must have enable_workflow_v2) | | MEC Workflows v2 | mec workflow list/get/create/update/activate/deactivate/archive/restore/delete/clone, mec workflow process list/get/delete, mec workflow step get/create/update/delete/reorder, mec workflow task list/get/add-file/add-checklist/update/update-one/delete/notify/approve/reject/sign-off (file + checklist + reconciliation_tags), mec workflow member list/check/add/update/remove/grant-bulk, mec workflow group list/add/update/remove/grant-bulk, mec workflow periods, mec workflow accounts tasks --date-tag <ts> --account-name <s> --account-group <s>, mec workflow accounts task-statuses --date-tag <ts> (tenant must have month_end_close; type "Month End Close" enforced) | | Desk | desk teams list, desk support-email set <team> <email>, desk smtp set/rotate/get/test <team>, desk sendgrid register <domain>/status/verify, desk ticket create <team> --title --requester-email [--attach <path> ...] [--no-thread] (<team> is an id, support email, or team name. ticket create emails the requester, so it is confirm-gated and keeps the word gate on prod — see docs/how-to-create-a-desk-ticket.md) | | Connectors | connectors publish <dataqueryFile>, connectors query repoint <queryId>, connectors binding get <fileboxUuid> (drives the connectors control-plane over VPN) | | Skill | skill — prints embedded SKILL.md |

Connectors: TLS over VPN

dr connectors … reaches the connectors control-plane (<env>-connectors-control-plane.datarails.io) over the corporate VPN. Its origin cert is a valid, publicly-trusted Let's Encrypt cert — but if your VPN performs TLS inspection (e.g. Cloudflare WARP / Zero-Trust), it re-signs the connection with a gateway CA that Node doesn't trust, and requests fail with self signed certificate in chain / unable to verify the first certificate.

Fix it while keeping verification on — trust the VPN gateway root CA via NODE_EXTRA_CA_CERTS (the CLI also appends these steps to the error inline):

# Windows — export the WARP/Gateway root CA from the trust store to a .pem, then:
setx NODE_EXTRA_CA_CERTS "C:\Users\<you>\gateway-ca.pem"   # reopen the terminal
# macOS / Linux
export NODE_EXTRA_CA_CERTS=/path/to/gateway-ca.pem

Do not use NODE_TLS_REJECT_UNAUTHORIZED=0 — it disables verification process-wide, including the monolith connection that carries your JWT and refresh token.

Usage examples

Inspect a customer's environment:

dr whoami --server US                              # who am I here?
dr templates list --server US                      # find the table
dr templates sample 3293 --rows 10 --server US     # peek at data
dr dataflow get 3293 --server US                   # see where it flows
dr reports list --table 3293 --server US           # which reports use it

Development

pnpm dev <args>           # run src/cli.ts under tsx
pnpm typecheck            # tsc --noEmit
pnpm lint
pnpm test                 # vitest
pnpm build                # tsup → dist/cli.js; scripts/embed-skills.mjs → dist/skills/

CI runs typecheck / lint / test / build on every push and PR (.github/workflows/ci.yml). node scripts/check-plugins.mjs gates the plugins tree (manifests, frontmatter, versions, the facts file, CONTRIBUTING.md); node --test plugins/dr-cli/hooks/tests/auto-update.test.js runs the hook's contract suite.

The endpoint inventory

scopes-endpoints.snapshot.json lists, for every leaf command, the HTTP endpoints it can reach — a top-level, non-empty JSON array of {command, method, url_template, service, certainty, non_integer_params?} records. It is the input of the scope contract gate in dr-datarails-server (HippiWeb/JWT_auth/scope_contract.py). Today that gate checks that each record's URL resolves to a monolith view and that the view declares its scope; holding a command's endpoints against its ceiling in scopes-map.snapshot.json is what DR-53487 may add. pnpm gen:endpoints also writes the same bytes to dist/scopes-endpoints.json.

  • Superset rule. The list may hold an endpoint a command never actually sends, but never omits one it can send: a gate built on it may over-report, never under-report.
  • certainty. static was read from the call and linked to the leaf through the call graph; inferred is a curated or fallback attribution.
  • url_template. A path only, no host and no query; a dynamic segment is {name}.
  • non_integer_params. The placeholders whose value is not a number (uuid, token, slug…), so substituting a literal 1 will not resolve that route. Absent means every placeholder takes an integer.
  • service. Which service serves the path: monolith, finance-os, spend-control, multi-product, desk, connectors, onboarding-validation. A consumer checking one service filters on it.
  • command: "*". A call any authenticated command may trigger (JWT mint/refresh, API-key and login-as re-exchange).
  • Leaves with no record are explained in scripts/scopes-map.sources.json under endpoints: noHttpCommands (no Datarails call) and externalCommands (the App Space leaves mounted from @datarails-ai/dr-appspace-cli, which owns its own contract).
pnpm gen:endpoints        # rewrite the snapshot (and dist/scopes-endpoints.json)
pnpm check:endpoints      # what CI runs: exit 1 on drift

The generator is static analysis plus the curated tables in scopes-map.sources.json; its header (scripts/gen-endpoints.mts) documents each step and every fatal case. scripts/gen-endpoints.spotcheck.test.mts runs a few leaves under msw and checks the requests they send are in the snapshot — a sample, not the mechanism. Adding a command or an api call changes the snapshot: run pnpm gen:endpoints and commit the diff.

Transferring it to dr-datarails-server. The snapshot is copied by hand, with the dr-cli version it was taken at (the version in package.json at that commit), assuming both repos are checked out side by side:

cp dr-cli/scopes-endpoints.snapshot.json dr-datarails-server/HippiWeb/JWT_auth/tests/fixtures/dr_cli_endpoints.json
node -p "require('./dr-cli/package.json').version" > dr-datarails-server/HippiWeb/JWT_auth/tests/fixtures/dr_cli_endpoints.version

A missing fixture makes the server test skip with a reason; a broken one is a hard failure. Lifting the skip is DR-53487. Two caveats for that work: the server's contract_violations currently resolves every record against the monolith URLconf and does not filter by service, so it must filter to service == "monolith", and it must decide how to treat command: "*" records and non_integer_params.

Adding a gated (destructive/sensitive) command

Sensitive writes route through twoStepConfirm (src/commands/_confirm.ts), which requires a two-step confirm-token and — on production tenants — an interactive challenge word for Tier A actions. Every gated action's (resource, verb) must be classified in src/commands/_prodGateTiers.ts:

  • Tier A — destructive / irreversible / org-wide → keeps the word gate on prod.
  • Tier B — reversible / additive / low-blast → confirm-token only, no word.

Add the new `${resource}|${verb}` key to PROD_GATE_TIER_A or PROD_GATE_TIER_B. If you skip it the test suite fails: twoStepConfirm throws on an unclassified action under test, and a completeness test flags any new gated resource or inline verb. (At runtime an unclassified action fail-closes to Tier A, so prod is never left un-gated — the tests just force the decision to be explicit.)

Note the flip side: any test that drives twoStepConfirm — even one unrelated to the gate — throws under this strict classification if its (resource, verb) is unclassified (confirm action "…|…" has no explicit production-gate tier). Usually that means you genuinely added a gated action and should classify its key. If instead you deliberately want an unclassified action in a test (e.g. exercising the fail-closed path itself), disable the check for that test with _setStrictClassification(false) from _confirm.ts and restore it (_setStrictClassification(undefined)) afterward.

Troubleshooting

dr-cli is a thin command-line client over the HTTP APIs of existing Datarails services — it holds almost no business logic of its own. Every command maps to one or more endpoints on a service you can read the source of. So when a command misbehaves, the bug is almost always on one of two sides: how dr-cli builds the request, or how the service handles it. Both are open to you.

The services dr-cli talks to:

| Service | What it serves | Repo | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Monolith (the big one) | Most endpoints — tables, dashboards, functions, ranges, datamappers, LUTs, documents, storyboards, extables, reports, dataflow, OData (odata), scans, fileboxes, integrations, forecast, collection-processes, workflows/MEC, cash, users/orgs/support-manager/devices, features, login/login-as/whoami | dr-datarails-server | | FinanceOS gateway | Paths under /finance-os/api/… — templates sample/distinct/aggregate, semantic-tables, metrics, fx | dr-finance-os | | Spend Control | dr spend-control … (/api/spend-control/v1) — a separate microservice reached through the monolith host | dr-spend-control | | Multi-Product | dr multi-product … (/api/multi-product/v1) — separate microservice, monolith host | dr-multi-product | | Desk | dr desk … (/api/desk/v1/…) — separate microservice, monolith host. One exception: desk support-email set writes to the MONOLITH's /bo/teams/<id>/, because that is where support_email lives | dr-desk-ai | | Connectors control-plane | dr connectors … (reached over the corporate VPN) | dr-connectors-control-plane | | Onboarding-validation | dr validate onboarding … (its own host, <env>-onboarding.datarails.com) | dr-onboarding-validation-re |

Not sure which service a command hits? Run it with --json and check the error, or read the header comment of the matching src/api/<area>.ts module — each one documents its endpoints. dr kb … is the exception: it talks to the Zendesk Help Center (plus an MCP server embedded in this repo), not a Datarails backend.

Before you report a bug to the channel, do this first:

  1. Clone both repos — this dr-cli repo and the service repo the failing command hits (from the table above; the monolith covers most cases).
  2. Point Claude Code at both local checkouts and ask it to investigate — e.g. "dr functions create returns a 400 on DEV; the CLI is in ~/src/dr-cli, the monolith is in ~/src/dr-datarails-server — trace the request from the CLI command through to the server view and find the mismatch." With both sides on disk, Claude can follow the request end to end: the flag parsing and payload shaping in dr-cli, and the serializer / view / permission check on the service.
  3. Ask it to implement or fix the issue right there — in whichever repo the root cause lives — rather than filing a report and waiting.

We believe in contribution. A reproduction with a root-cause pointer is good; a PR is better. If you can fix it — in dr-cli or in the service — open the PR (see Contributing below) instead of only reporting it. That's how the tool and the services it drives get better for the next person.

Contributing

Adding a skill or a plugin has its own checklist — see CONTRIBUTING.md. The loop below applies to every change.

The intended loop leans on Claude Code for review — a fresh session reviewing a PR catches issues the authoring session is blind to, so the review and the fix run in separate sessions.

  1. Implement + open a PR. Build the feature on a branch and open a PR against master. Keep CI green (typecheck / lint / test / build).
  2. Review in a clean session. Clear the Claude session (or open a second terminal) and ask for a code review of the PR — the clean context is what makes the review useful. It routinely finds real issues.
  3. Post the findings on the PR. Read the review, then have Claude post the issues as PR comments so there's a written record to work against.
  4. Fix in the original session. Back in the authoring terminal, clear the session and tell Claude to fix the PR against those comments. Push the fixes.
  5. Request review. Assign a human reviewer (e.g. Aviad, Eyal).
  6. Approve + merge. After approval (or another round), merge to master. Merging requires human approval and passing CI — never merge red.
  7. Cut a release + update locally. Run the release workflow (see Cutting a release) to bump the version, then update your own machine by re-running the npm install -g line from Install.

Cutting a release

The release workflow is .github/workflows/release.yml. It runs entirely from the GitHub Actions UI — nothing on your machine.

  1. Open https://github.com/Datarails/dr-cli/actions/workflows/release.yml
  2. Click Run workflow
  3. Pick a bump level (patch/minor/major) — or fill in version for an explicit override (e.g. 1.0.0 for a milestone cut)
  4. Click green

The workflow then bumps package.json, commits + tags master, builds, attaches dr-v<TAG>.js + SKILL.md + README.md to a new GitHub Release with auto-generated changelog.

  • Every plugins/*/.claude-plugin/plugin.json is stamped to the same version as package.json.
  • Each plugin is tagged <plugin>--v<version>, alongside the CLI's own v<version> tag.
  • As the last step, the release branch — the ref the dr-internal-plugins marketplace tracks — is fast-forwarded to the new commit.

App Space CLI bumps are automatic. @datarails-ai/dr-appspace-cli is pinned to an exact version. appspace-cli-bump.yml checks every 15 minutes for a newer one, pins it, runs the CI gates, and only when they pass pushes the pin to master and starts this workflow with patch. A manual release works as before.

JFrog publishing is currently manual — after the release run lands, build the npm tarball locally and upload it:

pnpm install && pnpm build && npm pack
# upload dr-cli-<version>.tgz to JFrog

Wiring npm publish into the release workflow is a follow-up — needs a JFrog publish token as a repo secret (separate from the anonymous read access end users use).

Auth used by the release workflow: the bump-and-push step uses secrets.BOT_PAT, a fine-grained PAT scoped to this repo with Contents: r/w. The PAT inherits the repo admin's branch-protection bypass, so git push origin master from CI lands cleanly without opening a PR.

License

Internal Datarails tool. Not for external distribution.