@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-virtualThat 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 | bashEach 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.mjsThe 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 identityBrowser 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 USCredential 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> # overrideThe 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
0600and 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 whichdrdeletes on sight.
Upgrading from a pre-keychain build? On the first run after upgrade, dr-cli detects any legacy
~/.dr/credentials.jsonand deletes it. The plaintext tokens are removed; your existing sessions are NOT migrated. Rundr 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 viadr 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 # pipedPiped, -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-pluginsmarketplace lists three plugins built from this repo at thereleasebranch:dr-cli(install, doctor, call, the update hook, thedr-clicommand skill,dr-switch-user),dr-cli-integrations, anddr-cli-pat. Install with/plugin install dr-cli@dr-internal-plugins; a satellite pullsdr-cliin. Every plugin version equals the CLI version, and thedr-clihook installs that CLI. - The npm package —
npm install -g @datarails-ai/dr-cliembeds 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 thedr-cliplugin, 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 ~ --forcewrites 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.mdCommand 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.pemDo 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 itDevelopment
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.staticwas read from the call and linked to the leaf through the call graph;inferredis 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 literal1will 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.jsonunderendpoints:noHttpCommands(no Datarails call) andexternalCommands(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 driftThe 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.versionA 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
--jsonand check the error, or read the header comment of the matchingsrc/api/<area>.tsmodule — 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:
- Clone both repos — this
dr-clirepo and the service repo the failing command hits (from the table above; the monolith covers most cases). - Point Claude Code at both local checkouts and ask it to investigate —
e.g. "
dr functions createreturns 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 indr-cli, and the serializer / view / permission check on the service. - 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.
- Implement + open a PR. Build the feature on a branch and open a PR
against
master. Keep CI green (typecheck/lint/test/build). - 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.
- 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.
- 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.
- Request review. Assign a human reviewer (e.g. Aviad, Eyal).
- Approve + merge. After approval (or another round), merge to
master. Merging requires human approval and passing CI — never merge red. - 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 -gline 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.
- Open https://github.com/Datarails/dr-cli/actions/workflows/release.yml
- Click Run workflow
- Pick a bump level (
patch/minor/major) — or fill inversionfor an explicit override (e.g.1.0.0for a milestone cut) - 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.jsonis stamped to the same version aspackage.json. - Each plugin is tagged
<plugin>--v<version>, alongside the CLI's ownv<version>tag. - As the last step, the
releasebranch — the ref thedr-internal-pluginsmarketplace 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 JFrogWiring 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.
