@datarails-ai/dr-cli 0.0.1-locked → 1.6.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +608 -0
  2. package/dist/chunk-DWMCBGQH.js +242 -0
  3. package/dist/cli.js +64468 -0
  4. package/dist/kb-6UWI7Q32.js +21 -0
  5. package/dist/scopes-map.json +4412 -0
  6. package/dist/skills/connector-sandbox/SKILL.md +95 -0
  7. package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
  8. package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
  9. package/dist/skills/dr-cli/SKILL.md +659 -0
  10. package/dist/skills/dr-cli/reference/admin.md +796 -0
  11. package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
  12. package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
  13. package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
  14. package/dist/skills/dr-cli/reference/cash.md +61 -0
  15. package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
  16. package/dist/skills/dr-cli/reference/datamappers.md +150 -0
  17. package/dist/skills/dr-cli/reference/desk.md +472 -0
  18. package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
  19. package/dist/skills/dr-cli/reference/forecast.md +51 -0
  20. package/dist/skills/dr-cli/reference/integrations.md +57 -0
  21. package/dist/skills/dr-cli/reference/lookups.md +168 -0
  22. package/dist/skills/dr-cli/reference/metrics.md +512 -0
  23. package/dist/skills/dr-cli/reference/multi-product.md +180 -0
  24. package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
  25. package/dist/skills/dr-cli/reference/rh-config.md +84 -0
  26. package/dist/skills/dr-cli/reference/safety.md +143 -0
  27. package/dist/skills/dr-cli/reference/spend-control.md +206 -0
  28. package/dist/skills/dr-cli/reference/templates.md +163 -0
  29. package/dist/skills/dr-cli/reference/workflows.md +137 -0
  30. package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
  31. package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
  32. package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
  33. package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
  34. package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
  35. package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
  36. package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
  37. package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
  38. package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
  39. package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
  40. package/dist/skills/dr-switch-user/SKILL.md +395 -0
  41. package/dist/skills/dr-validate/SKILL.md +392 -0
  42. package/dist/skills/dr-validate/report-template.html +950 -0
  43. package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
  44. package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
  45. package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
  46. package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
  47. package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
  48. package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
  49. package/dist/skills/fx-onboard/SKILL.md +325 -0
  50. package/dist/skills/mec-table-validation/SKILL.md +383 -0
  51. package/dist/skills/plugins.json +24 -0
  52. package/dist/skills/validate-workato-migration/SKILL.md +167 -0
  53. package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
  54. package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
  55. package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
  56. package/dist/zendeskKb-JPSHVOPH.js +243 -0
  57. package/package.json +56 -9
  58. package/scripts/postinstall.mjs +91 -0
@@ -0,0 +1,796 @@
1
+ # IT / admin / misc
2
+
3
+ ## Auth / impersonation
4
+
5
+ - `login --server <key>` — browser device flow; the operator's own login.
6
+ - `login-as <email> --server <key>` — impersonate a user with **JWT only**
7
+ (no browser). Requires an operator `login` first. Resolves the email to a
8
+ user id via `can_login_as` (or pass `--user-id <n>`), mints an impersonation
9
+ JWT from the monolith (`POST /jwt/api/loginas/`) authorized by the operator,
10
+ and stores it as a switchable identity keyed by `(server, email)`. Target it
11
+ afterward with `--account <email>`. `--as <operator-email>` selects the
12
+ operator when several are stored. The token refreshes silently and
13
+ auto-remints from the operator if its refresh dies — no re-prompt until the
14
+ operator's own login expires. Denials surface as clear messages
15
+ (not permitted / inactive / unavailable). `whoami --account <email>` shows
16
+ `Impersonated by:`.
17
+ - **Skipping the email lookup.** The email→id resolution is an extra
18
+ round-trip against the user directory before the mint. If it errors or times
19
+ out, get the id once and bypass it:
20
+ `dr users can-login-as --search <email> --json` → `--user-id <n>`. Same for
21
+ scripted runs over many users: resolve the ids up front, then pass
22
+ `--user-id` so a directory hiccup can't fail the batch mid-way.
23
+ - **Requires dr ≥ 1.3.24.** `login-as` shipped in 1.3.24; older installs
24
+ answer `unknown command 'login-as'`. If the browser Login-As works but the
25
+ CLI "can't", check `dr --version` first and `dr update` — that, not a
26
+ permission block, is the most common cause of "the CLI won't let me".
27
+ - **The product `SUPER_ADMIN` role is NOT Django `is_superuser`.** The gate
28
+ (`IT.helpers.it_loginas`, wired as Django's `CAN_LOGIN_AS`; the JWT view
29
+ resolves it via `_loginas_permitted` — same gate as the browser Login-As)
30
+ blocks only genuine Django **`is_superuser`**, sync-service, or application
31
+ targets. A customer/demo user whose *product* role is `SUPER_ADMIN` is
32
+ still impersonable. **Verified on DEMO:** `login-as superadmin@demo.<org>`
33
+ (product role SUPER_ADMIN, `is_admin: true`) minted successfully, same as a
34
+ CONTRIBUTOR. So a `can_login_as` hit is generally reliable; there is **no**
35
+ "equal-or-higher privilege" rule.
36
+ - **When `not_permitted` really does fire:** the target is a true Django
37
+ superuser / sync-service / application user, **or** you (the operator)
38
+ lack scope — a Django superuser operator can impersonate any non-superuser;
39
+ a supporting-manager operator only users in their supported orgs
40
+ (`profile.get_supporting_login_as_organization_users()`). Note the CLI
41
+ `users get` payload exposes `profile.role` (product role) but **not**
42
+ `is_superuser`, so you can't confirm the Django flag from the CLI.
43
+
44
+ ## Users
45
+
46
+ - `users list`, `users get`, `users me`.
47
+ - `users can-login-as` — users the operator can impersonate; `--search`,
48
+ `--limit`, `--all`, `--json`. Feeds the email→id lookup for `login-as`.
49
+ It can list a genuine Django superuser / sync-service / application account
50
+ that the mint will still refuse (the gate is applied at mint time), but for
51
+ ordinary customer accounts — including product `SUPER_ADMIN`-role users — a
52
+ hit is reliable. See the gotcha above.
53
+ - `users set-password <user_id>` — set a new password on a **sync-service
54
+ account only** (`profile.is_sync_service === true`). Refuses every other user
55
+ up-front; regular humans authenticate via Descope/SSO and must use the
56
+ product's "Send a reset password link by email" flow instead. The new value
57
+ is read from an interactive hidden TTY prompt — there is no `--password` flag
58
+ and stdin is required (refuses non-TTY to avoid silent hangs); paste works.
59
+ Text mode prints the target's email + org for sanity-check before the prompt;
60
+ `--json` skips that preamble and emits `{"id":…,"email":"…","ok":true}` on
61
+ success. Server-side complexity rejections (HTTP 400 with
62
+ `{password:[…]}`) are surfaced verbatim as bullet lines so the operator sees
63
+ the exact policy that failed. PUTs `/api/it/users/<id>/set_password`,
64
+ mirroring the FE `dialog-user-set-password.component.ts` path.
65
+
66
+ ### `users bulk-create` — invite many users from a CSV
67
+
68
+ `users bulk-create --file <csv> [--organization <id>] [--no-register-identity] [--confirm] [--json] [--timeout <seconds>]`
69
+ automates the UI's **Invite Users → Set Permissions** flow for a whole list:
70
+ one backend call to create each user (`POST /api/it/users`), then one to scope
71
+ their data access (`POST /api/it/users/<id>/permission_groups`). Turns an
72
+ hours-long manual job into seconds.
73
+
74
+ **No passwords, SSO as normal.** This never sets or sends a password. The
75
+ server creates each user with a random throwaway password it never surfaces and
76
+ sends the standard invite (`send_invite_mail_or_ciam` — the Descope/CIAM path),
77
+ exactly like a UI invite. Invited users onboard through the org's **default
78
+ SSO/Descope login** — identical to inviting them one-by-one. There is no
79
+ `--password` here (that's `users set-password`, sync-service accounts only).
80
+
81
+ **CSV format** (header is exact; comma-delimited, so multi-group cells use `;`):
82
+
83
+ ```csv
84
+ email,first_name,last_name,role,permission_groups,department,job_role,register_identity
85
+ jane.doe@acme.com,Jane,Doe,VIEWER,US Sales;Marketing Reports,FINANCE_FPA,ANALYST_OR_SPECIALIST,true
86
+ john.roe@acme.com,John,Roe,VIEWER,EMEA Sales,SALES,MANAGER,
87
+ sam.lee@acme.com,Sam,Lee,VIEWER,US Sales;EMEA Sales;Exec Dashboards,,,false
88
+ ```
89
+
90
+ | Column | Required | Notes |
91
+ |--------|----------|-------|
92
+ | `email` | ✅ | Unique per file. The only truly required field. |
93
+ | `first_name` / `last_name` | – | Optional. |
94
+ | `role` | – | Blank ⇒ `VIEWER`. Accepts `VIEWER`, `COLLABORATOR`, `CONTRIBUTOR`, `ADMIN` (the privileged `SUPER_ADMIN`/`DR_CONNECT_ADMIN` are rejected on purpose). |
95
+ | `permission_groups` | – | `;`-separated group **names** (not ids), resolved case-insensitively against `dr organizations permission-groups`. This is the data-access column; blank = invited but sees nothing until scoped. |
96
+ | `department` / `job_role` | – | Optional enum names (e.g. `FINANCE_FPA`, `MANAGER`); validated server-side. |
97
+ | `register_identity` | – | **Optional 8th column.** Per-row control over identity-provider registration + invite email. `true`/blank ⇒ register + invite on create (the default). `false` ⇒ defer — create the user without IdP registration / invite (register later with `register-identity`). **Omit the column entirely and every row defaults to `true`** (back-compat). `--no-register-identity` forces `false` for the whole batch regardless of this column. |
98
+
99
+ > **`bulk-create` does not create groups — the name must already exist.** Any name
100
+ > in the `permission_groups` column is resolved against existing groups; an unknown
101
+ > name aborts the run (fail-fast) before any user is created. Pre-create groups
102
+ > with **`dr organizations create-group`** (see Organizations below) or in the web
103
+ > UI's Permission Groups settings. On a brand-new org with no groups yet, leave the
104
+ > column blank (users are invited unscoped) and pass `--organization <id>` since the
105
+ > target org can't be inferred without groups.
106
+
107
+ Ready-to-edit templates ship at `docs/examples/users-bulk-create.csv` (with
108
+ permission groups) and `docs/examples/users-bulk-create-no-groups.csv` (blank
109
+ `permission_groups` — invite unscoped, e.g. into a fresh org).
110
+
111
+ **Big CSVs finish in one run — do not chunk them.** The whole-command budget is
112
+ sized to the work: 30s of base, plus 5s per **round-trip** (not per row — one row
113
+ that fans out to N calls is sized as N), capped at 30 minutes. A 140-row CSV gets
114
+ ~12 minutes, which is far more than the ~5 minutes it actually needs.
115
+
116
+ `--timeout <seconds>` raises that budget for something unusually large or a very
117
+ slow tenant. It is a **floor, not a cap**: auto-sizing may still widen it, so a
118
+ value smaller than what the plan needs costs you nothing. Max 86400 (24h). Past
119
+ ~354 round-trips the 30-minute ceiling starts biting and the per-call allowance
120
+ decays — the command warns on stderr when that happens, and `--timeout` is the
121
+ answer if the run then aborts part-way.
122
+
123
+ > **If a run ever stops partway and the CLI says `session expired; re-login
124
+ > required`, do not conclude the JWT expired.** dr-cli auto-refreshes the JWT
125
+ > (proactively at a 60s buffer, and again on any 401), so a mid-batch expiry is
126
+ > not a thing you need to work around. That message was historically what a
127
+ > whole-command *timeout* looked like when the abort happened to land inside the
128
+ > auth pre-flight. Re-logging-in and re-running in smaller chunks "worked" only
129
+ > because each run restarted the clock. The fix is the sized budget above, or
130
+ > `--timeout`, not chunking and not a re-login.
131
+
132
+ **Deferring registration — create quietly, invite later.** By default each
133
+ create registers the user in the identity provider (Descope) and sends the
134
+ invite email — on prod, registration *is* the invite email (one atomic Descope
135
+ call). Two ways to defer that:
136
+ - **Per row:** set the `register_identity` column to `false` on the rows you
137
+ want deferred (blank/absent ⇒ `true`). Lets one CSV mix invited and deferred
138
+ users.
139
+ - **Whole batch:** pass **`--no-register-identity`**, which forces every row to
140
+ `false` regardless of the column.
141
+ Deferred users still get the row, async data-model provisioning, and seat-limit
142
+ alert — they just can't authenticate until registered. Stand up an environment
143
+ silently, then fire the invites with **`users register-identity --file <same
144
+ csv>`** (below). The command sends `register_identity: false` only for deferred
145
+ rows; a fully-invited run's create body is unchanged. See DR-52009.
146
+
147
+ **Safety model:**
148
+ - **Dry-run by default.** Without `--confirm` the command parses the CSV,
149
+ resolves every group name + the target org, prints the plan, and makes **zero
150
+ mutating calls**. Nothing is created (and no invite emails go out) until you
151
+ re-run with `--confirm`.
152
+ - **Fail-fast before any write.** A typo'd/unknown group name, an ambiguous
153
+ name, or a duplicate email aborts the whole run *before* the first user is
154
+ created — you never get a half-built batch.
155
+ - **Target org.** Auto-resolved from the tenant's permission groups (the create
156
+ endpoint requires an org id and the profile endpoint doesn't expose one).
157
+ Pass `--organization <id>` to be explicit, or when the tenant has no groups /
158
+ groups spanning multiple orgs (the command asks for it in those cases). The
159
+ resolved org **name** is printed so you can eyeball the target before
160
+ confirming — the guard against creating users on the wrong tenant.
161
+ - **Per-row isolation on execute.** An already-existing email is **not created
162
+ again**, but if the row lists permission groups those groups are **assigned to
163
+ the existing user** (reported as `re-scoped`). This makes re-runs idempotent
164
+ *and* self-healing: a row that created a user but then failed to scope them
165
+ (see below) is repaired on the next `--confirm` run. Assignment is **additive**
166
+ — it only adds groups, never removes existing access. A row with no groups on
167
+ an existing email is simply `skipped`. A hard failure is recorded and the batch
168
+ keeps going; any failure makes the command exit non-zero, with a
169
+ created/re-scoped/skipped/failed summary.
170
+ - **Partial-failure remediation (`failed` rows).** A row that reports
171
+ `created (id N) but group assignment failed` means the user exists but is
172
+ **unscoped** — fix the group name if it was rejected, then re-run the same CSV
173
+ with `--confirm`: the create step finds the user already exists and re-scopes
174
+ it. A row reporting `already exists but could not resolve its user id` means
175
+ the operator can't see that user via `/api/it/users` (usually a different org)
176
+ — assign the groups in the UI or check `--organization`.
177
+ - Requires an **org-admin or supporting-manager** login (same gate as the UI
178
+ invite); a support/CSM account with those rights on the tenant works. Note the
179
+ re-scoping behavior means this command can **grant data access to users that
180
+ already existed**, not just ones it creates — the printed org name + `--confirm`
181
+ gate are your guard against doing that on the wrong tenant.
182
+
183
+ ### `users set-email` — change one user's login email
184
+
185
+ `users set-email <user_id> <new_email> [--confirm] [--json]` changes a user's
186
+ **login email** through `PUT /api/it/users/<id>` — the same endpoint the IT
187
+ admin edit dialog uses. The PUT (not a PATCH) is deliberate: it lets the
188
+ server's SSO/CIAM email-sync run, and it preserves the user's
189
+ `excel_online_email` rather than blanking it.
190
+
191
+ - **Dry-run by default.** Without `--confirm` the command GETs the user and
192
+ prints `current_email -> new_email` (or the same as JSON) and makes **zero
193
+ mutating calls**. Re-run with `--confirm` to apply.
194
+ - **Validation.** The `<user_id>` must be positive and `<new_email>` must
195
+ contain `@`, both checked before any network call. A target already taken by
196
+ another user fails with `email already in use: <email>. Pick a different
197
+ address.` (server `EmailInUseError`).
198
+ - On success `--json` emits `{id, previous_email, email}`.
199
+
200
+ ### `users bulk-set-email` — rename many users' emails from a CSV
201
+
202
+ `users bulk-set-email --file <csv> [--confirm] [--json] [--timeout <seconds>]` applies many login-email
203
+ renames in one run, each row reusing the same `PUT /api/it/users/<id>` path as
204
+ single-user `set-email`. Same shape as `bulk-create` / `bulk-permissions`:
205
+ resolve + validate the whole file first, then execute with per-row isolation.
206
+
207
+ **CSV format** (header is exact, in order):
208
+
209
+ ```csv
210
+ source,target
211
+ old.jane@acme.com,jane.doe@acme.com
212
+ old.john@acme.com,john.roe@acme.com
213
+ ```
214
+
215
+ | Column | Required | Notes |
216
+ |--------|----------|-------|
217
+ | `source` | ✅ | The user's **current** email; must contain `@`; matched case-insensitively against tenant users; unique per file. |
218
+ | `target` | ✅ | The **new** email; must contain `@`; unique per file; a row whose `source` == `target` is rejected. |
219
+
220
+ A ready-to-edit template ships at `docs/examples/users-bulk-set-email.csv`.
221
+
222
+ **Safety model:**
223
+ - **Fail-fast before any write.** `planEmailRenames` aborts the whole run
224
+ *before* the first PUT when: a `source` matches no tenant user; the batch
225
+ spans **more than one organization** (all sources must belong to a single org
226
+ — the org name + id are printed so you can eyeball the target); a source user
227
+ has **no organization** (can't verify the same-org rule); or a `target` is
228
+ already in use by an existing user. If that existing user is itself a source
229
+ in the batch, it's a **swap/chain**, which is unsupported (renames apply
230
+ sequentially, not atomically) and also aborts.
231
+ - **Dry-run by default.** Without `--confirm` it prints the resolved plan
232
+ (`source -> target (id N)` per row) and changes nothing.
233
+ - **Per-row isolation on execute.** With `--confirm` each rename runs in order;
234
+ a row failure (e.g. a race where the target got taken) is recorded as `failed`
235
+ and the batch continues. A non-empty failure set makes the command exit
236
+ non-zero after reporting every row's `changed`/`failed` status. `--json`
237
+ emits `{organization, results, attempted, planned}` even if the run aborts
238
+ mid-way.
239
+ - Requires the same **org-admin / supporting-manager** login as the other IT
240
+ user commands.
241
+
242
+ ### `users set-email-notifications` — turn one user's notification email on/off
243
+
244
+ `users set-email-notifications <user_id> --enable|--disable [--confirm] [--json]`
245
+ is the CLI equivalent of the **"Enable notifications"** Yes/No field in the
246
+ admin user-edit form. It writes `disable_emails` through the same
247
+ `PUT /api/it/users/<id>` the dialog uses. Exactly one of `--enable` /
248
+ `--disable` is required.
249
+
250
+ - `--enable` → the user **receives** notification mail (`disable_emails=false`).
251
+ - `--disable` → the user receives none (`disable_emails=true`).
252
+ - **Dry-run by default.** Without `--confirm` it prints
253
+ `notifications off -> on` and makes zero mutating calls; it also says so
254
+ when the user is already in the requested state.
255
+ - **Only `disable_emails` changes.** The write body is deliberately minimal —
256
+ `{disable_emails, email, excel_online_email}` — with `email` echoed back
257
+ **unchanged** from the GET. Sending no `email` would make the server compare
258
+ the stored address against `None` and run its SSO email-sync for a change
259
+ that isn't happening; `excel_online_email` is echoed because the server
260
+ clears it when the key is absent. Role, department, job role, organization
261
+ and block status are all left untouched.
262
+
263
+ **Sync-service and support accounts are refused**, with a message naming why —
264
+ they are provisioned with notifications off and the platform does not send them
265
+ notification mail. The bulk command skips them for the same reason, so the two
266
+ verbs never disagree about the same user. (The server would actually accept the
267
+ write: its sync-service override only fires when `is_sync_service` is in the
268
+ payload, which this minimal body never sends.)
269
+
270
+ **Accounts with a malformed stored email can't be toggled.** Because the write
271
+ echoes the current address back, the server re-runs its email validation over
272
+ it. A user whose stored address isn't a valid one (no TLD, legacy seed data)
273
+ fails with a message naming the address and pointing at
274
+ `users set-email <id> <valid@address>` as the prerequisite. The admin UI has
275
+ the same limitation — its edit payload echoes `email` too — so this is a
276
+ property of the endpoint, not of the CLI.
277
+
278
+ **Blocked users are the one surprise.** The server reports notification state
279
+ as `disable_emails OR is_blocked`, so enabling a blocked user succeeds but
280
+ `users get <id>` keeps showing notifications off until the user is unblocked.
281
+ The command prints an explicit note saying the write landed and no retry is
282
+ needed — don't re-run it.
283
+
284
+ **An unreported current state stays `unknown`.** The server omits `profile`
285
+ entirely for some accounts, and `disable_emails` is optional even when
286
+ `profile` is present. Where the field is missing the preview prints
287
+ `notifications unknown -> on`, never claims "already in the requested
288
+ state", and `--json` reports `current_email_notifications: null` with
289
+ `no_change: null` (and `previous_email_notifications: null` after the write).
290
+ The requested write still goes ahead — you asked for a specific user — but
291
+ nothing in the output asserts a starting state we have no evidence for. The
292
+ bulk command, which was not asked about any one user, skips those users
293
+ instead.
294
+
295
+ To find who is affected on a tenant, `users list` now renders an
296
+ `EMAIL_NOTIF` column, and `users list --email-notifications off
297
+ [--organization <id>]` filters directly to the users who receive nothing.
298
+
299
+ ### `users bulk-set-email-notifications` — same toggle, whole organization
300
+
301
+ `users bulk-set-email-notifications --organization <id> --enable|--disable
302
+ [--only-changed] [--confirm] [--json] [--timeout <seconds>]` applies the toggle to every user in one
303
+ organization. There is **no org-level notification setting** in the data model —
304
+ the flag lives on each user — so this is a per-user write loop, shaped like
305
+ `bulk-set-email`: plan first, dry-run by default, per-user isolation on execute.
306
+
307
+ **Always skipped, without being asked:**
308
+
309
+ | Skipped | Why |
310
+ |---------|-----|
311
+ | sync-service users | Provisioned with notifications off; the platform sends them no notification mail, so the setting has no effect. The single-user command refuses them for the same reason. |
312
+ | support users | Provisioned with notifications off by design. |
313
+ | users whose state the server didn't report | State unknown; writing blind could flip someone you didn't mean to touch. |
314
+ | users already in the requested state | Only with `--only-changed`, and **never** for a blocked user — see below. |
315
+
316
+ **Safety model:**
317
+ - **Fail-fast.** An `--organization` matching no users aborts before any write
318
+ rather than reporting an empty plan — the usual cause is a wrong org id.
319
+ - **Dry-run by default.** Without `--confirm` it prints the full plan
320
+ (`~ email (id N) off -> on`, `[blocked]` where relevant) plus every skipped
321
+ user and its reason, and changes nothing.
322
+ - **Per-user isolation on execute.** A failed user is recorded and the batch
323
+ continues; a non-empty failure set exits non-zero, names the failed ids, and
324
+ points at `--only-changed` to retry just the remainder. `--json` emits
325
+ `{organization, email_notifications, results, skipped, attempted, planned}`
326
+ even if the run aborts mid-way.
327
+ - **`--only-changed` never skips a blocked user.** Their read state is
328
+ `_disable_emails OR is_blocked`, so it always says "off" and reveals nothing
329
+ about the stored column. Skipping on it would pass over a blocked user still
330
+ stored as enabled during `--disable --only-changed`, and they would start
331
+ receiving mail the moment they were unblocked. Blocked users are always
332
+ written.
333
+ - Blocked users in the batch get one summary note, for the same read-back
334
+ reason as the single-user command.
335
+
336
+ ### `users register-identity` — send the deferred invites for a `--no-register-identity` batch
337
+
338
+ `users register-identity --file <csv> [--confirm] [--json] [--timeout <seconds>]` is the second half
339
+ of the `bulk-create --no-register-identity` flow (DR-52009): it registers the deferred
340
+ users in the identity provider and sends the invite email that create skipped.
341
+ **Feed it the SAME CSV** you bulk-created from — it only reads the `email`
342
+ column (all other columns are ignored), resolves each email to its tenant user
343
+ id via `GET /api/it/users`, and calls `PUT /api/it/users/<id>/register_identity`
344
+ per row. That endpoint uses the *same* invite path as create, so the resulting
345
+ email is identical to an immediate invite (**not** a password-reset).
346
+
347
+ **Safety model:**
348
+ - **Dry-run by default.** Without `--confirm` it resolves every email and prints
349
+ the plan — `~ email (id N)` for resolved rows, `! email (NOT FOUND …)` for
350
+ emails not in the tenant — and makes **zero** mutating calls. Re-run with
351
+ `--confirm` to register + invite.
352
+ - **Honest per-row status** (from the server's contract): `registered` (200
353
+ `{"sent":true}` — newly provisioned + emailed), `already_exists` (200
354
+ `{"sent":false}` — the identity was already in the IdP, a safe no-op), or
355
+ `failed` (a 502 `provider_error` from the IdP, an email not found in the
356
+ tenant, or any other error). The run prints a `registered / already_exists /
357
+ failed` summary.
358
+ - **Per-row isolation + idempotent re-runs.** A failed row is recorded and the
359
+ batch keeps going; **only a non-empty `failed` set makes the command exit
360
+ non-zero**. Because an already-registered user reports `already_exists` (not
361
+ `failed`), re-running the same CSV after a partial batch only retries the ones
362
+ that didn't land. `--json` emits `{results}` with per-row `{line, email,
363
+ status, user_id, detail?}`.
364
+ - Requires the same **org-admin / supporting-manager** login as the other IT
365
+ user commands (server enforces `IsAuthenticatedOriginalITAdmin`).
366
+
367
+ > **Note (adjacent monolith bug DR-52121):** the deferred invite emails because
368
+ > a monolith comparison bug forces `send_mail=True`. That's the desired behavior
369
+ > here, but a future DR-52121 fix must keep passing `send_mail=True` or deferred
370
+ > (and inline) invites will silently stop emailing.
371
+
372
+ ### `users permissions` — the Permission Manager (data tables + elements)
373
+
374
+ The CLI surface of the web **Permission Manager** (`/#/it/users/:id/permissions`).
375
+ **Requires the `use_new_table_perms` product flag on the tenant** — verify with
376
+ `dr features orgs list <org_id>`; the new grant/revoke endpoints this uses are
377
+ only active when it's on. All *write* subcommands are **dry-run unless
378
+ `--confirm`** (like `bulk-create`), and support `--json`.
379
+
380
+ ```console
381
+ users permissions get <user_id>
382
+ users permissions set-role <user_id> --role <ROLE> [--confirm]
383
+ users permissions grant-table --email <e> --table <id[,id]> --role <ROLE> [--confirm]
384
+ users permissions revoke-table --email <e> --table <id[,id]> [--confirm]
385
+ users permissions grant-element --type <T> --email <e> --id <id[,id]> --role <ROLE> [--confirm]
386
+ users permissions revoke-element --type <T> --email <e> --id <id[,id]> [--confirm]
387
+ users permissions set-filters <user_id> --file <json> [--confirm]
388
+ users permissions remove-group <user_id> --group <id> [--confirm]
389
+ ```
390
+
391
+ - `get` prints identity, org role, permission-group membership, and the user's
392
+ **per-table roles** (`GET /api/templates?user_to_check_role_id=<id>` — only
393
+ tables with a direct personal role are listed; group-derived access is
394
+ annotated), and the user's **row-level data-access filters**
395
+ (`GET /api/it/users/<id>/permission_filters?include_inherited=true` — direct +
396
+ group-inherited, shown as `Table.Field in/not in [values]`). A **Super Admin**
397
+ shows a full-access note instead of grants. Per-**element** role read-back and
398
+ filter *authoring* (`POST …/permission_filters`) are **not** assembled yet
399
+ (DR-51917 follow-ups).
400
+ - **Common trap — "the user has no table permissions".** Do **not** answer
401
+ "what tables can this user see?" from the raw `GET /api/it/users/<id>` object's
402
+ `permissions` field. That field is Django **global model-level** perm codenames
403
+ (`user.user_permissions`) and is **empty for essentially every VIEWER** — it
404
+ never reflects table access. Real table access lives as **guardian object-level
405
+ roles per table**, visible only via the `user_to_check_role_id` sweep that this
406
+ `get` runs. Always use `users permissions get <id>` (or the sweep directly); an
407
+ empty top-level `permissions: []` is **not** evidence of no access.
408
+ - **`CUSTOM` role in a read/export.** The sweep can report a role of
409
+ `{id: -1, name: "CUSTOM", label: "Custom"}`. This is a **server sentinel**, not
410
+ a catalog role — `BackOffice/serializers.py` returns it whenever the user's
411
+ perms on that object don't map to a named preset role. On a plain VIEWER it
412
+ corresponds to the web UI's **"No access"**; treat/label it as no access (or a
413
+ bespoke perm set), never as a grantable role.
414
+ - `set-role` changes the **org role** via `PUT /api/it/users/<id>/permissions`
415
+ `{role: <NAME>}` — `VIEWER | CONTRIBUTOR | COLLABORATOR | ADMIN | SUPER_ADMIN`
416
+ (the `UserProfileRole` PK is the name). Server-side guards (SUPER_ADMIN
417
+ promotion needs a super-admin operator, `COLLABORATOR` needs `use_new_table_perms`,
418
+ seat limits) surface verbatim.
419
+ - `grant-table` / `revoke-table` drive `POST /api/templates/users/grant_permissions_bulk`
420
+ (`role: null` revokes). Note the path is `/api/templates/…`, not `/api/tables/…`
421
+ — the monolith mounts the Tables app under `/api/templates/` (a `/api/tables/…`
422
+ POST 404s). The `--role` name is resolved case-insensitively against
423
+ `GET /api/permissions/roles/table` — role names are the full enum keys, e.g.
424
+ `OWNER_TABLE_ROLE`, `VIEWER_TABLE_DATA_ROLE`.
425
+ - `grant-element` / `revoke-element` cover **every category in the web Permission
426
+ Manager's Elements pane**. `--type` is one of
427
+ `dashboard | storyboard | filebox | lookup | model | report | datasource`, each
428
+ mapped to its app + role catalog:
429
+
430
+ | `--type` | web category | grant endpoint | role catalog |
431
+ |---|---|---|---|
432
+ | `dashboard` | Dashboards | `POST /api/dashboards/users/grant_permissions_bulk` | `roles/dashboard` |
433
+ | `storyboard` | Storyboards | `POST /api/storytelling/users/grant_permissions_bulk` | `roles/story` |
434
+ | `filebox` | Input fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
435
+ | `lookup` | Lookup fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
436
+ | `model` | Model fileboxes | `POST /api/documents/users/grant_permissions_bulk` | `roles/files_box` |
437
+ | `report` | Reports | `POST /api/filebox_reports/users/grant_permissions_bulk` | `roles/filebox_reports` |
438
+ | `datasource` | Data sources | `POST /api/v3/data_integrations/data-sources/users/grant_permissions_bulk` | `roles/data_source` |
439
+
440
+ `--id` is the entity id (dashboard/story/document/report-space/data-source id),
441
+ repeatable or comma-separated. `filebox`, `lookup`, and `model` share the
442
+ Documents surface — they differ only by which documents the web UI lists; grant
443
+ by id is identical. **`datasource` caveat:** `roles/data_source` is **empty
444
+ unless the tenant has the `new_data_sources_perms_and_roles` flag on**, so
445
+ `--role` resolution fails with an empty valid-role list when it's off.
446
+ - **`report` caveat:** grants at the report-*space* level only; the web UI also
447
+ cascades to the report's nested documents via a second `/api/documents/...` call
448
+ — not yet mirrored here (DR-51917).
449
+ - `set-filters` replaces the user's **row-level data-access filters** via
450
+ `POST /api/it/users/<id>/permission_filters`. **This is a FULL REPLACE** — the
451
+ server deletes every existing own filter and recreates from the file, so pass
452
+ the complete desired set (an empty array clears them all). `--file` is a JSON
453
+ array (or `{"new_filters":[…]}`) of `{field:<FlatTableField id>, values:[…],
454
+ is_excluded?:bool}`. `is_excluded:true` makes it a deny-list. Dry-run prints
455
+ the intended set and the replace warning; `--confirm` applies. (Filter *reads*
456
+ are shown by `get`.)
457
+ - `remove-group` deletes one membership (`DELETE /api/it/users/<id>/permission_groups/<gid>`);
458
+ the additive *add* is done by `bulk-create`'s `permission_groups` column.
459
+ - Every write subcommand echoes a `Target: account <server>, operator <email>`
460
+ line first (text mode only) so a mis-set `--account`/`--server` is visible
461
+ before — and after — a mutation lands. Under `--json` the line is suppressed so
462
+ output stays a single parseable object.
463
+ - With `--json`, every write subcommand emits structured JSON on **both** paths:
464
+ the dry-run object carries `"dry_run": true`; the confirmed object omits it. So
465
+ a script can diff the two without parsing prose.
466
+
467
+ ### `users bulk-permissions` — many Permission-Manager grants from a CSV
468
+
469
+ `users bulk-permissions --file <csv> [--confirm] [--json] [--timeout <seconds>]`
470
+
471
+ The bulk counterpart to `users permissions` — applies the four grant kinds above
472
+ from one CS-authorable CSV instead of one flag-driven call at a time. Same
473
+ `use_new_table_perms` requirement, same **dry-run unless `--confirm`** / `--json`
474
+ model. Header (exact, seven columns): `email,resource,type,ids,role,filter_values,filter_excluded`.
475
+ One row = one grant, discriminated by `resource`:
476
+
477
+ | `resource` | maps to | row shape |
478
+ |---|---|---|
479
+ | `table` | `grant-table` | `ids` = `;`-sep table ids, `role` required |
480
+ | `element` | `grant-element` | `type` = filebox/lookup/model/report/dashboard/storyboard/datasource, `ids` = `;`-sep, `role` required |
481
+ | `role` | `set-role` | `role` = org role; `ids`/`type`/`filter_*` blank |
482
+ | `filter` | `set-filters` | `ids` = one FlatTableField id, `filter_values` = `;`-sep, `filter_excluded` blank/`true`/`false` |
483
+
484
+ - **Resolves everything first and fails loud before any write:** every `email`
485
+ must exist on the tenant, and each `table`/`element` `role` must resolve against
486
+ that surface's role catalog (org roles and filter field ids are validated
487
+ server-side, per row). A typo aborts the whole run before a single grant lands.
488
+ - **`filter` rows are full-replace, aggregated per user:** all `filter` rows for
489
+ one email are collected into a single `POST …/permission_filters`, so the CSV
490
+ must list that user's *complete* desired filter set (re-run idempotency depends
491
+ on it). Grants (`table`/`element`/`role`) are additive.
492
+ - **Per-row isolation on apply:** a failed row is reported, the batch continues,
493
+ and any failure exits non-zero. A corrected re-run is safe for `table`,
494
+ `element`, and `role` rows (additive); a `filter` re-run must still carry each
495
+ user's *complete* set, since the full replace deletes any omitted filter.
496
+ - Requires an org-admin / supporting-manager / contributor operator (same as the
497
+ per-item grants). Ready template: `docs/examples/users-bulk-permissions.csv`.
498
+
499
+ ## Organizations
500
+
501
+ Alias `orgs`.
502
+
503
+ - `organizations list [--mini]`, `organizations get`.
504
+ - `organizations contacts` — per-tenant user directory (the source for
505
+ share-with-user pickers); `role` is `ADMIN|CONTRIBUTOR|VIEWER`.
506
+ - `organizations permission-groups` — **lists** named user bundles (same role
507
+ enum + `members[]`). Read-only (`GET /api/organizations/permission_groups`).
508
+ - `organizations create-group --org <id> --name <n> [--role <VIEWER|CONTRIBUTOR|ADMIN>]
509
+ [--member <email> ...]` — **creates** a permission group via
510
+ `POST /api/it/permission_groups` (the IT app — a different endpoint from the
511
+ list above). `--org` is sent in the body and must be one of your accessible
512
+ tenants (else 403); `--name` is unique per org. `--role` is upper-cased;
513
+ `--member` is repeatable and each must be accessible to you, in the target org,
514
+ and not outrank `--role`. **Dry-run unless `--confirm`**; `--json` on both
515
+ paths. Requires an org-admin / supporting-manager / contributor operator. This
516
+ is the group-creation the web UI's Permission Groups settings does — and the
517
+ way to pre-create groups that `users bulk-create` then references.
518
+ - `organizations update-group <id> --name <n>` — **renames** a group
519
+ (`PUT /api/it/permission_groups/<id>`). The IT update endpoint is **name-only**:
520
+ role is immutable and members are managed separately, so this changes the name
521
+ and nothing else. **Dry-run unless `--confirm`**; `--json` too. The server 403s
522
+ a rename of a default/system group (`is_editable:false`).
523
+ - `organizations delete-group <id>` — **deletes** a group
524
+ (`DELETE /api/it/permission_groups/<id>`), behind the **two-step confirm-token**
525
+ flow like every other `delete`: the first run prints the group's
526
+ name/role/member count and a token; re-run with `--confirm-token <tok>`. Hard
527
+ delete that also removes the group's access to shared assets (members
528
+ themselves remain in Datarails). Prod-gate **Tier A**.
529
+ - `organizations bulk-groups --file <csv> [--organization <id>] [--timeout <seconds>]` — **provisions
530
+ many groups + membership from one CSV** (header `group_name,role,members`,
531
+ exactly those three columns; `members` is `;`-separated emails — a stray comma
532
+ is rejected, not silently truncated, and duplicate emails are deduped
533
+ case-insensitively). Each row is classified against the tenant's existing
534
+ groups: an unknown name is **created** (`create-group`), a known name is
535
+ **augmented** — its listed members are added additively
536
+ (`POST /api/it/users/<id>/permission_groups`, a no-op for existing members).
537
+ `role` applies **only on create** (it's immutable on an existing group); a
538
+ blank `role` lets the server assign its default on create, and on augment
539
+ leaves the existing role untouched. Resolves everything first and **fails loud
540
+ before any write**: a member email not on the tenant, an ambiguous existing
541
+ group name (matches more than one group id), or a `role` cell that disagrees
542
+ with an existing group's role, aborts the whole run. **Dry-run unless
543
+ `--confirm`** — the sole write gate: this is additive + reversible, so unlike
544
+ `delete-group` it is *not* behind the two-step confirm-token flow. `--json` on
545
+ both paths; per-row isolation on apply (a failed row is reported, the batch
546
+ continues, non-zero exit if any failed). `--organization` is only needed to
547
+ place *created* groups, and only when it can't be derived from the tenant's
548
+ existing groups (none exist, or they span multiple orgs); an augment-only run
549
+ never requires it. Requires an org-admin / supporting-manager / contributor
550
+ operator (same as `create-group`). Ready template:
551
+ `docs/examples/organizations-bulk-groups.csv`.
552
+ - `organizations push-targets` — the organizations this account may push a
553
+ dashboard or filebox into. **Run this before `dashboards push` / `filebox
554
+ push`**; those commands reject any destination not on this list.
555
+ - Requires admin or supporting-manager rights; a 403 surfaces verbatim.
556
+ - What you get back depends on the signed-in org: a **template** org
557
+ (`use_as_template=true`) sees its sibling orgs and their descendants; any
558
+ other org sees its own descendants. Both are full subtrees, not just direct
559
+ children. An empty list means there is nowhere to push.
560
+ - The list does **not** carry customer status, so it can include an inactive
561
+ org. `filebox push` to an `Opt-Out`/`Churn`/`On Hold` org is rejected by the
562
+ server with that reason.
563
+ - `organizations add --domain <d> [--name <n>] [--license-type <l>]
564
+ [--category <c>] [--from-template <org_id>] [--use-as-template]
565
+ [--include-input-versions] [--payload-file <p>]` — a 403 (insufficient
566
+ permission) is surfaced verbatim; re-impersonate as needed.
567
+ - `--from-template <org_id>` clones the new tenant from an existing org; the
568
+ source must already have `use_as_template=true` on its row (else 400). There
569
+ is no CLI surface to flip that flag.
570
+ - `--include-input-versions` clones input file versions too (requires
571
+ `--from-template`; the lone flag is rejected client-side).
572
+ - The clone runs **asynchronously** — the POST returns 201 with a sparse org
573
+ row immediately; the new tenant's data lands a few minutes later.
574
+ - `--use-as-template` flags the **new** org as a template (off by default).
575
+
576
+ `support-manager list` (alias `sm`).
577
+
578
+ ## Organization export / import (full-org migration)
579
+
580
+ Move or duplicate a whole organization — same server under a new domain, or
581
+ across servers (e.g. US → EU). Wraps `POST|DELETE /api/organizations/export`
582
+ and `POST|DELETE /api/organizations/import` (the org clone-over-blob
583
+ pipeline). **Requires a datarails.com admin or supporting-manager identity on
584
+ every server involved** — a 403 means the active account isn't one (check
585
+ `dr whoami`).
586
+
587
+ **The completion contract (read this first).** Both POSTs return 200
588
+ immediately and run server-side as background chains. **Completion is
589
+ signaled ONLY by email** to the triggering operator
590
+ (`EXPORT/IMPORT_ORGANIZATION_SUCCESS/WARNING/FAILED`) — there is **no status
591
+ API to poll**, so there is no `--wait`. Do not chain export → import in one
592
+ breath: trigger the export, stop, and only import after the export email
593
+ confirms success. The imported org's row appears immediately, but the tenant
594
+ is **not usable** until its email arrives — a "finished" import may still be
595
+ partial (per-phase failures don't abort the run; the WARNING email lists
596
+ what was skipped).
597
+
598
+ The flow:
599
+
600
+ 1. **Export (on the source server):**
601
+ `organizations export create --domain <src> --new-domain <dest>
602
+ [--keep-user-passwords] [--allow-foreign-server] [--exported-before]`
603
+ - `--new-domain` is the full identity of the copy — it becomes both the
604
+ new org's domain AND its display name, and names the blob folder the
605
+ import reads. Omit only when re-importing under the same domain.
606
+ - **Passwords are hands-off.** A renamed export automatically randomizes
607
+ EVERY user password in the imported org — a fresh cryptographic secret
608
+ per org, sent to the server and recorded nowhere. The copy is simply not
609
+ reachable with knowable credentials; Datarails staff access it via
610
+ impersonation. For a REAL migration (users must keep logging in), pass
611
+ `--keep-user-passwords` to preserve source-org credentials.
612
+ - `--allow-foreign-server` is **required when the source server is not US**
613
+ and whenever the export will be imported on another server.
614
+ - A repeat export is rejected until you either pass `--exported-before`
615
+ (resume) or `export delete` the stale one — the error says which.
616
+ 2. **Wait for the export email.** No email = still running (or the chain
617
+ broke — a hard failure can die without any email; check with the RnD team
618
+ or Groundcover if it's been hours).
619
+ 3. **Import (on the target server):**
620
+ `organizations import run --domain <dest> [--temporary] [--no-skip-models]
621
+ [--imported-before]`
622
+ - `--domain` is always the **domain chosen at export** (the `--new-domain`
623
+ from step 1), NOT the source domain — it names the blob folder.
624
+ - Imports are **permanent by default**. `--temporary` opts into a
625
+ throwaway copy marked for auto-deletion by the server GC.
626
+ - Unknown model types in the blob are **skipped by default** (typical
627
+ version skew between servers; the completion email lists anything
628
+ skipped). `--no-skip-models` makes the import fail instead when the
629
+ blob carries model types this server cannot import.
630
+ 4. **Wait for the import email**, verify the tenant, then — **back on the
631
+ source server** — clean up:
632
+ `organizations export delete --domain <src> [--new-domain <dest>] --server <source>`
633
+ — removes the export record + blob only (org data untouched; two-step
634
+ confirm, Tier B). The source org must still exist on the server you call,
635
+ so clean up the export **before** ever deleting the source org (otherwise
636
+ the record is unreachable via this API and only the ~60-day server GC
637
+ removes it). `--allow-foreign-server` is only needed when deleting a blob
638
+ from a different env than the one that created it — not for the normal
639
+ same-server cleanup.
640
+
641
+ **Desk is NOT part of this clone today.** The full org export/import copies
642
+ Desk config only when the server lists `DESK_AI` in `LIST_OF_MICROSERVICES`
643
+ (pending monolith work); tickets, teams and their members are not. To provision
644
+ Desk in a demo org, run the org clone (or use an existing org) and then
645
+ [`dr desk clone`](desk.md#clone-a-reference-orgs-desk-into-another-org-desk-clone-dr-52763),
646
+ which matches-or-creates the users, teams and Desk data from a reference org
647
+ without touching anything else.
648
+
649
+ **Undo an import:** `organizations import delete-orgs --domain <dest>` —
650
+ **PERMANENTLY DELETES the imported organization** (the full org-delete flow,
651
+ all tenant data destroyed). Two-step confirm, prod word-gate (**Tier A**).
652
+ The server only accepts domains created via the import API; anything else
653
+ must go through the standard permanent-delete flow. The "successfully
654
+ deleted" response acks the **dispatch** — deletion itself runs
655
+ asynchronously, so the org can linger in `organizations list` for a few
656
+ minutes before disappearing. The export blob is not touched — that cleanup
657
+ is `export delete`.
658
+
659
+ - `devices list`, `devices get`, `devices logs`,
660
+ `devices received-logs <machine_id>`, `devices commands <machine_id>`.
661
+ - `devices request-logs <id>…`, `devices download-logs`,
662
+ `devices command-delete <cmd_id>` (two-step confirm-token flow).
663
+
664
+ ## Feature flags (IT)
665
+
666
+ - `features list`, `features groups list`, `features groups get`,
667
+ `features orgs list <org_id>`.
668
+ - `features orgs add <org_id> --flag …`, `features orgs remove <org_id> --flag …`
669
+ (both two-step confirm-token flow — flipping flags changes customer-visible
670
+ behavior).
671
+
672
+ **Know the flag name? Skip the listing entirely.** `--flag` accepts a flag **id
673
+ or name** (repeatable), and the server resolves it — so to enable/disable a flag
674
+ you already know, go straight to `features orgs add <org_id> --flag <name>`. Only
675
+ reach for `features list` when you need to *discover* or confirm a flag's exact
676
+ name.
677
+
678
+ **The two list shapes differ — don't confuse them:**
679
+
680
+ - `features list` (the global catalog) is **paginated** — the `--json` is a DRF
681
+ envelope `{count, next, previous, results}` and returns only **one page**
682
+ (default 50; there are 270+ flags). A name grep on the default page silently
683
+ finds nothing for anything past #50. Pull the whole catalog in one shot with
684
+ `--page-size 500` (max 500), or walk `--page <n>`. This paginates in **both**
685
+ table and `--json` mode; the table footer says "Showing 1-50 of N (more
686
+ available — bump --page)".
687
+ - `features orgs list <org_id>` is a **plain JSON array** of exactly the flags
688
+ **enabled/overridden for that org** — a flag's *presence* in this list is what
689
+ tells you it's on for the org. The `default_on` field on each row is the
690
+ flag's **global default**, NOT the org state (a flag can be enabled for the
691
+ org while `default_on:false`). To confirm an `orgs add` worked, check the flag
692
+ now *appears* here — don't read `default_on`.
693
+
694
+ ## Semantic tables / FX (finance-os)
695
+
696
+ - `semantictables list|get|data|distinct` (alias `semtables`).
697
+ - `fx rates`.
698
+
699
+ ### FX configuration (bulk enablement)
700
+
701
+ - `fx set-posting <template_id> [--field <id|name>]` — mark the table's Monetary
702
+ **posting** amount (`fx_role: posting_amount`). Defaults to the field named
703
+ "Posting Amount" (case/underscore-insensitive). Refuses a second posting field.
704
+ Two-step confirm (`--confirm-token`); Tier B on prod.
705
+ - `fx set-reporting <template_id>` — mark Monetary **reporting** amount(s) with
706
+ currency code + symbol. Three forms:
707
+ - no flags: heuristic, in this precedence — (1) a field already carrying a
708
+ reporting currency keeps it (so a re-run is a no-op, and such a field is listed
709
+ whatever it is named); (2) a currency in the field NAME, read wherever it sits —
710
+ `USD Amount`, `Amount USD`, `Reporting Amount (EUR)`, `Amount in GBP`,
711
+ `Reporting_Amount_USD` all work; (3) a single bare `Amount` / `Reporting Amount`
712
+ takes its code from the table's "Reporting Currency" column when that column
713
+ holds exactly one distinct value (otherwise the error tells you to map
714
+ explicitly). A name that contradicts the stored currency is refused, not guessed.
715
+ - `--field <id|name> --code <CODE> [--symbol <S>]` for one field;
716
+ - repeatable `--map "<field>=<CODE>[:<SYMBOL>]"` for several.
717
+ Symbols default from the FE's ISO catalog; codes validate against the catalog +
718
+ org custom currencies (`--allow-custom` bypasses, and then **registers** the
719
+ unknown code — see below). One reporting field per currency per table is
720
+ enforced. Two-step confirm.
721
+ - **`--all-tables`** on both field commands sweeps every table in the org instead of
722
+ taking a template id — the onboarding shape. A table it cannot judge is reported and
723
+ **skipped, never guessed**: the plan separates tables that need a human decision
724
+ (ambiguous currency, two posting candidates, a currency collision) from tables that
725
+ simply are not FX tables, which are counted rather than listed. One confirm token
726
+ covers the whole sweep. `--field`/`--map`/`--code` name one table's fields, so they
727
+ cannot be combined with it; single-table mode still raises the detailed error with the
728
+ field roster.
729
+ - `fx default-currency [<CODE>] [--none] [--confirm]` — show or set the **org-wide
730
+ default** reporting currency (`GET/PUT /api/v1/currencies/default-currency`), i.e. the
731
+ star in the dashboard dropdown. With no argument it prints the current default plus
732
+ every currency the org can offer (`organization-currencies`) — the answer to "what will
733
+ the selector show". `--none` records the "None" choice, meaning the posting currency.
734
+ A code the org cannot offer is refused (`--allow-unlisted` overrides), because a
735
+ default the selector cannot serve leaves every dashboard on a currency with no data.
736
+ Dry-run by default. **Setting it needs Super Admin server-side**, and the server has
737
+ the last word — the command reports what was stored, not what was sent. This matters
738
+ beyond the UI: an AI agent with no currency of its own uses this default.
739
+ - `fx add-currency <CODE> [--symbol <S>]` — register an org-level custom currency
740
+ (`POST /api/v1/currencies/custom-currencies`), i.e. a code the ISO list doesn't
741
+ carry, such as `BUSD` for "budgeted USD" (compare actuals to budget without FX
742
+ noise). 2-5 letters/digits, upper-cased; re-running with a different `--symbol`
743
+ updates it, mirroring the web app. An ISO code is refused — there is nothing to
744
+ register. Not gated (additive org config, same as `cash currencies set-prefs`).
745
+
746
+ > **Why registration matters.** The web app registers a custom code the moment
747
+ > someone uses it, so the org's custom-currency list and the field's
748
+ > `currency_code` never drift. `set-reporting --allow-custom` therefore registers
749
+ > an unknown code itself, **before** writing the field, and aborts without touching
750
+ > any field if that fails — a field pointing at a code the org doesn't know is the
751
+ > exact state this avoids (the Tables dialog stops offering it). Use
752
+ > `fx add-currency` to pre-register a code before a bulk run, or to fix a symbol.
753
+ - `fx convert-functions [--template <id>] [--force-sum] [--off] [--confirm]` — set
754
+ `is_fx_aware: true` on every Excel/DR.GET function whose value field is Monetary
755
+ **and carries an fx_role**. Non-SUM functions are skipped unless `--force-sum`
756
+ switches them. **Dry-run by default** — always run once without `--confirm` and
757
+ read the plan.
758
+ - `fx convert-widgets (--dashboard <id> | --all) [--force-sum] [--off] [--confirm]` —
759
+ same sweep for dashboard widgets (eligible = exactly one value field, Monetary,
760
+ with an fx_role). The server has the final say per widget; an echo that doesn't
761
+ confirm the flag is reported as a failure, not silently accepted. Dry-run by
762
+ default.
763
+
764
+ **Eligibility is stricter server-side than in the web app.** `FxEligibilityService`
765
+ requires the value field to be Monetary AND to have an `fx_role`; the serializer
766
+ answers a flip on a role-less field with a **400**, not a silent skip. Both
767
+ converters therefore report those as `needs-fx-role` and skip them — run
768
+ `fx set-posting` / `fx set-reporting` first. Chain the four commands in that order:
769
+ tag the posting field, tag the reporting fields, then sweep widgets and functions.
770
+
771
+ **Rollback — `--off`.** Both converters take `--off` to sweep the other way and turn
772
+ conversion off. It acts on the stored flag alone: eligibility and the aggregator are
773
+ irrelevant to a rollback, and the aggregator is never touched (so `--off --force-sum`
774
+ is refused). This deliberately reaches targets the enable direction would call
775
+ ineligible — a widget whose value section has since gained a second field, or a
776
+ function whose field lost its Monetary type, can strand a flag that nothing else can
777
+ clear. Dry-run by default, same as the enable direction.
778
+
779
+ ## Knowledge base
780
+
781
+ `kb search|get|categories|sections|articles-in-section|recent|attachments|article-images`.
782
+
783
+ ## MCP & skills (the binary's own surface)
784
+
785
+ - `mcp register|unregister|list|serve`.
786
+ - `skill list` (file count + total bytes per skill),
787
+ `skill print [name] [--file <relPath>] [--hash]` (defaults to `dr-cli`'s router
788
+ `SKILL.md`; `--file reference/<x>.md` prints a reference file),
789
+ `skill install <target_dir>`, `skill init-project [dir]`.
790
+ - `update` (top-level) — self-upgrade the CLI in place.
791
+
792
+ ## Customer-facing docs (KB)
793
+
794
+ Help Center → the **"User & Permissions"**, **"Billing"**, and **"Security & Legal"** categories. Find current articles with `dr kb search "users permissions billing security"` (or
795
+ browse `dr kb categories` / `dr kb sections`). Cite the article; don't
796
+ paraphrase from memory.