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

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,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Emit one `dr datamappers create from-file` body per target currency.
4
+ *
5
+ * Each mapper reads one `To <CCY>` tab of the conversion filebox and writes one
6
+ * column per rate type onto the FX table. Coordinates come from the layout
7
+ * sidecar `build-matrix.mjs` wrote, so the two scripts cannot disagree about a
8
+ * cell -- which matters once rate types shift the columns (one type puts the
9
+ * cross-rate in C, two put them in C and E, pairs mode puts the first in B).
10
+ *
11
+ * Two things the body cannot express, and the caller must supply:
12
+ *
13
+ * 1. `--field "<column>:Float"` on the create call, one per mapped column. A
14
+ * `map_to` naming a column the template lacks BOOTSTRAPS that field --
15
+ * that is how these columns come into being, there is no add-field API.
16
+ * But the bootstrapped type is inferred from the sample values and is
17
+ * often Text, and a Text rate column breaks every downstream calculation
18
+ * without complaining. The printed command lines always carry the
19
+ * overrides.
20
+ *
21
+ * 2. `document_version_id` is required by `from-file` and is NOT present in
22
+ * `dr datamappers get` output. Read it from the upload response or
23
+ * `dr filebox versions <filebox_id>`.
24
+ *
25
+ * The currency column maps to whatever `currency_field` the layout sidecar carries,
26
+ * which build-matrix defaults to the UDM `Posting Currency`. The period calc fields
27
+ * below write `Reporting Month`, which UDM requires to be the last day of the month
28
+ * at 00:00:00 UTC -- `[System_Date]` on a month-tagged filebox already is.
29
+ *
30
+ * The body deliberately carries NO `template_id`. In power mode that key names
31
+ * the SOURCE template a mapper was learned on, not the target (the target is
32
+ * the positional table id), and the CLI resolves every `map_to` type from it
33
+ * and THROWS on any name the source lacks -- before --field overrides are
34
+ * merged, so --field cannot rescue it. The rate columns do not exist anywhere
35
+ * yet, by design, so carrying template_id would fail every create. Omitting it
36
+ * opts into server-side inference, and --field pins the type.
37
+ *
38
+ * Usage:
39
+ * node gen-mappers.mjs --layout-file matrix.xlsx.layout.json \
40
+ * --filebox 125890 --version 843990 --template 2760 \
41
+ * [--targets EUR,GBP] [--out-dir ./mappers] [--name-prefix fx_conversion_rates] \
42
+ * [--ordinal-start 1] [--server DEV]
43
+ */
44
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
45
+ import { join } from 'node:path';
46
+
47
+ import { die, parseArgs, splitCodes, str } from './_args.mjs';
48
+
49
+ const args = parseArgs(process.argv.slice(2));
50
+ const layoutFile = str(args, 'layout-file');
51
+ const filebox = Number(args.filebox);
52
+ const version = Number(args.version);
53
+ const template = Number(args.template);
54
+ const outDir = str(args, 'out-dir') ?? './mappers';
55
+ const namePrefix = str(args, 'name-prefix') ?? 'fx_conversion_rates';
56
+ const ordinalStart = args['ordinal-start'] === undefined ? 1 : Number(args['ordinal-start']);
57
+ const serverFlag = str(args, 'server') ?? '<S>';
58
+
59
+ if (layoutFile === undefined) {
60
+ die('--layout-file <matrix.xlsx.layout.json> is required (written by build-matrix.mjs)');
61
+ }
62
+ for (const [flag, value] of [
63
+ ['--filebox', filebox],
64
+ ['--version', version],
65
+ ['--template', template],
66
+ ]) {
67
+ if (!Number.isInteger(value) || value <= 0) {
68
+ die(`${flag} <id> is required and must be a positive integer`);
69
+ }
70
+ }
71
+
72
+ let layout;
73
+ try {
74
+ layout = JSON.parse(await readFile(layoutFile, 'utf8'));
75
+ } catch (err) {
76
+ die(`could not read ${layoutFile}: ${err.message}`);
77
+ }
78
+ if (!Array.isArray(layout?.sheets) || layout.sheets.length === 0) {
79
+ die(`${layoutFile} carries no sheets -- was it written by build-matrix.mjs?`);
80
+ }
81
+
82
+ if (layout.layout === 'lut') {
83
+ die(
84
+ 'this layout was built for wiring B (filebox-sourced lookups), which needs no\n' +
85
+ ' datamappers. Use `dr lut create --payload-file` per tab instead, or rebuild\n' +
86
+ ' with --layout matrix if you meant wiring A.',
87
+ );
88
+ }
89
+
90
+ const wanted = str(args, 'targets');
91
+ const only = wanted === undefined ? undefined : new Set(splitCodes(wanted));
92
+
93
+ const sheets = layout.sheets.filter((s) => only === undefined || only.has(s.target));
94
+ if (sheets.length === 0) die('--targets matched none of the sheets in the layout file');
95
+
96
+ /** One `config.header` entry. Every field but the three varying ones is fixed. */
97
+ const headerEntry = (coordinate, value, mapTo) => ({
98
+ role: 'header',
99
+ coordinate,
100
+ value,
101
+ groups: [],
102
+ map_to: mapTo,
103
+ match_type: 'value',
104
+ });
105
+
106
+ function mapperBody(sheetSpec, ordinal) {
107
+ const { sheet, columns, target, currency_coordinate: currencyCoord } = sheetSpec;
108
+ return {
109
+ name: `${namePrefix}_${template}_${target.toLowerCase()}`,
110
+ sheet_name: sheet,
111
+ document_version_id: version,
112
+ selected_document_ids: [filebox],
113
+ ordinal,
114
+ config: {
115
+ sheets_filter: { values: [sheet], is_excluded: false },
116
+ preview_sheet_name: sheet,
117
+ header: [
118
+ // The sidecar names the currency field; the fallback only defends a
119
+ // hand-edited layout file.
120
+ headerEntry(currencyCoord, 'Currency', layout.currency_field ?? 'Posting Currency'),
121
+ ...columns.map((c) => headerEntry(c.coordinate, c.name, c.name)),
122
+ ],
123
+ footer: [],
124
+ dimension: [],
125
+ attributes: [],
126
+ table_id: [],
127
+ value_field_name: 'value',
128
+ // Both period columns come off the filebox version's own date tag, so
129
+ // rolling the file forward re-stamps the rows without a formula edit.
130
+ calculated_fields: [
131
+ { name: 'Posting Date', formula: ' [System_Date] ', role: 'calculated_fields' },
132
+ { name: 'Reporting Month', formula: ' [System_Date] ', role: 'calculated_fields' },
133
+ ],
134
+ fx_conversions: [],
135
+ ignore_zeros: true,
136
+ improved_etl: null,
137
+ },
138
+ };
139
+ }
140
+
141
+ await mkdir(outDir, { recursive: true });
142
+
143
+ const commands = [];
144
+ for (const [i, spec] of sheets.entries()) {
145
+ const body = mapperBody(spec, ordinalStart + i);
146
+ const file = join(outDir, `${spec.target.toLowerCase()}.json`);
147
+ await writeFile(file, `${JSON.stringify(body, null, 2)}\n`, 'utf8');
148
+ console.log(
149
+ `wrote ${file} (sheet "${spec.sheet}" -> ` +
150
+ `${spec.columns.map((c) => `${c.coordinate} "${c.name}"`).join(', ')})`,
151
+ );
152
+ // The target table is a POSITIONAL argument, not a flag. Every mapped column
153
+ // gets an explicit Float, or the server infers a type from the samples.
154
+ const overrides = spec.columns
155
+ .map((c) => `--field ${JSON.stringify(`${c.name}:Float`)}`)
156
+ .join(' ');
157
+ commands.push(
158
+ `dr datamappers create from-file ${template} --file ${file} ${overrides} ` +
159
+ `--server ${serverFlag} --json`,
160
+ );
161
+ }
162
+
163
+ console.log('\nRun these in order (each is a prod-tier write -- confirm first):\n');
164
+ for (const c of commands) console.log(` ${c}`);
165
+
166
+ const allColumns = [...new Set(sheets.flatMap((s) => s.columns.map((c) => c.name)))];
167
+ console.log(
168
+ `\nThen confirm all ${allColumns.length} column(s) landed as Float, not Text --\n` +
169
+ `on the FX table AND on every table a lookup carries them into:\n` +
170
+ ` dr templates get ${template} --server ${serverFlag} --json\n`,
171
+ );
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Turn a triangulated rate feed into a presentable .xlsx.
4
+ *
5
+ * Reads the shape a triangulating feed produces -- one row per ordered pair per
6
+ * day, cross-rated at the source rather than in a workbook:
7
+ *
8
+ * from_currency, to_currency, rate, timestamp
9
+ *
10
+ * A CSV in that shape is correct but is not what anyone hands a customer. This
11
+ * pivots it into a square matrix for one date and keeps the daily detail on a
12
+ * second tab.
13
+ *
14
+ * Column names are matched case-insensitively and fall back to position, so a
15
+ * feed that calls them `source`/`target` still works.
16
+ *
17
+ * Usage:
18
+ * node rates-to-xlsx.mjs --in rates.csv --out matrix.xlsx [--date 2026-08-31]
19
+ *
20
+ * `--date` picks the matrix tab's date; the default is the latest in the file.
21
+ * No dependencies -- see `_xlsx.mjs`.
22
+ */
23
+ import { readFile, writeFile } from 'node:fs/promises';
24
+
25
+ import { die, parseArgs, str } from './_args.mjs';
26
+ import { buildWorkbook, colName, numberCell, sheetXml, textCell } from './_xlsx.mjs';
27
+
28
+ const args = parseArgs(process.argv.slice(2));
29
+ const inPath = str(args, 'in');
30
+ const outPath = str(args, 'out') ?? 'rates.xlsx';
31
+ const wantDate = str(args, 'date');
32
+
33
+ if (inPath === undefined) {
34
+ die('--in <rates.csv> is required (the file a triangulating data-query produced)');
35
+ }
36
+
37
+ /** Split one CSV line, honouring double-quoted fields. */
38
+ function splitCsvLine(line) {
39
+ const out = [];
40
+ let cur = '';
41
+ let quoted = false;
42
+ for (let i = 0; i < line.length; i += 1) {
43
+ const ch = line[i];
44
+ if (quoted) {
45
+ if (ch === '"' && line[i + 1] === '"') {
46
+ cur += '"';
47
+ i += 1;
48
+ } else if (ch === '"') {
49
+ quoted = false;
50
+ } else {
51
+ cur += ch;
52
+ }
53
+ } else if (ch === '"') {
54
+ quoted = true;
55
+ } else if (ch === ',') {
56
+ out.push(cur);
57
+ cur = '';
58
+ } else {
59
+ cur += ch;
60
+ }
61
+ }
62
+ out.push(cur);
63
+ return out;
64
+ }
65
+
66
+ const raw = (await readFile(inPath, 'utf8')).trim();
67
+ if (raw === '') die(`${inPath} is empty`);
68
+ const lines = raw.split(/\r?\n/);
69
+ const header = splitCsvLine(lines[0]).map((h) => h.trim().toLowerCase());
70
+
71
+ /** Locate a column by any of its known spellings, else by position. */
72
+ const pick = (names, fallbackIndex) => {
73
+ for (const n of names) {
74
+ const i = header.indexOf(n);
75
+ if (i !== -1) return i;
76
+ }
77
+ return fallbackIndex;
78
+ };
79
+
80
+ const iFrom = pick(['from_currency', 'from', 'source', 'source_currency'], 0);
81
+ const iTo = pick(['to_currency', 'to', 'target', 'target_currency'], 1);
82
+ const iRate = pick(['rate', 'mid', 'value'], 2);
83
+ const iDate = pick(['timestamp', 'date', 'reporting_month', 'posting_date'], 3);
84
+
85
+ const records = [];
86
+ for (const line of lines.slice(1)) {
87
+ if (line.trim() === '') continue;
88
+ const f = splitCsvLine(line);
89
+ const rate = Number(f[iRate]);
90
+ if (!Number.isFinite(rate)) continue;
91
+ records.push({
92
+ from: (f[iFrom] ?? '').trim(),
93
+ to: (f[iTo] ?? '').trim(),
94
+ rate,
95
+ // Date only -- a triangulated feed is daily, and the matrix is per day.
96
+ date: (f[iDate] ?? '').trim().slice(0, 10),
97
+ });
98
+ }
99
+
100
+ if (records.length === 0) die(`${inPath} carried no usable rate rows`);
101
+
102
+ const dates = [...new Set(records.map((r) => r.date))].sort();
103
+ const latest = dates.at(-1);
104
+ const matrixDate = wantDate ?? latest;
105
+ if (!dates.includes(matrixDate)) {
106
+ die(`--date ${matrixDate} is not in the file (it holds ${dates[0]} … ${latest})`);
107
+ }
108
+
109
+ const codes = [...new Set(records.map((r) => r.from))].sort();
110
+ const onDate = new Map(
111
+ records.filter((r) => r.date === matrixDate).map((r) => [`${r.from}>${r.to}`, r.rate]),
112
+ );
113
+
114
+ // --- Sheet 1: the square matrix for one date -------------------------------
115
+ const matrix = new Map();
116
+ matrix.set(1, [textCell('A1', `FX Conversion Matrix — ${matrixDate}`)]);
117
+ matrix.set(2, [
118
+ textCell('A2', 'Read as: 1 unit of the ROW currency buys this many of the COLUMN currency'),
119
+ ]);
120
+ matrix.set(4, [
121
+ textCell('A4', 'from \\ to'),
122
+ ...codes.map((c, i) => textCell(`${colName(2 + i)}4`, c)),
123
+ ]);
124
+ codes.forEach((from, i) => {
125
+ const r = 5 + i;
126
+ const cells = [textCell(`A${r}`, from)];
127
+ codes.forEach((to, j) => {
128
+ const v = onDate.get(`${from}>${to}`);
129
+ if (v !== undefined) cells.push(numberCell(`${colName(2 + j)}${r}`, v));
130
+ });
131
+ matrix.set(r, cells);
132
+ });
133
+
134
+ // --- Sheet 2: every pair, every day ----------------------------------------
135
+ const daily = new Map();
136
+ daily.set(1, [
137
+ textCell('A1', 'from_currency'),
138
+ textCell('B1', 'to_currency'),
139
+ textCell('C1', 'rate'),
140
+ textCell('D1', 'date'),
141
+ ]);
142
+ records.forEach((r, i) => {
143
+ const n = i + 2;
144
+ daily.set(n, [
145
+ textCell(`A${n}`, r.from),
146
+ textCell(`B${n}`, r.to),
147
+ numberCell(`C${n}`, r.rate),
148
+ textCell(`D${n}`, r.date),
149
+ ]);
150
+ });
151
+
152
+ // Literal data only -- nothing to recalculate, so no fullCalcOnLoad.
153
+ await writeFile(
154
+ outPath,
155
+ buildWorkbook([
156
+ { name: 'Conversion Matrix', xml: sheetXml(matrix) },
157
+ { name: 'Daily Rates', xml: sheetXml(daily) },
158
+ ]),
159
+ );
160
+
161
+ const totalPairs = codes.length ** 2;
162
+ const missing = totalPairs - onDate.size;
163
+ console.log(
164
+ `wrote ${outPath} — ${codes.length}×${codes.length} matrix for ${matrixDate}, ` +
165
+ `${records.length} daily rows across ${dates.length} date(s)`,
166
+ );
167
+ if (missing > 0) {
168
+ console.log(
169
+ `note: ${missing} of ${totalPairs} pairs are absent on ${matrixDate} — ` +
170
+ `those cells are blank rather than guessed`,
171
+ );
172
+ }
@@ -0,0 +1,325 @@
1
+ ---
2
+ name: fx-onboard
3
+ description: Use when enabling the FX currency selector for a Datarails customer — turning an existing org's Monetary fields, DR.GET functions and dashboard widgets currency-aware so users can switch a dashboard or workbook between currencies. Triggers - "enable FX for <customer>", "set up the currency selector", "make this org currency-aware", "roll out FX conversion to <org>", "turn on multi-currency for this tenant", "FX onboarding", "which tables should be currency aware", "why doesn't the currency dropdown show anything", "the currency selector says N/A", "tag the posting and reporting amounts", "set the org default currency", "roll back the FX conversion". Maps the org, proposes a plan, and only writes after the implementor approves. Run dr-cli:fx-conversion-matrix FIRST - the selector swaps between per-currency reporting-amount columns that must already exist, and that skill is what computes them. Installing or updating the CLI is dr-cli:install; a misbehaving CLI is dr-cli:doctor.
4
+ allowed-tools: Bash, Read, Write
5
+ ---
6
+
7
+ # FX onboarding — enable the currency selector for one org
8
+
9
+ Takes an existing customer from "no FX" to a working currency selector: Monetary
10
+ fields tagged with their FX roles, DR.GET functions and dashboard widgets
11
+ currency-aware, and an org default currency set.
12
+
13
+ This skill teaches the FX flow, not the CLI. The `dr-cli` skill is the command
14
+ authority for anything outside `dr fx`, and its **Auth** section covers login and
15
+ identities. It ships both in the Claude Code plugin and in the npm package, so it is
16
+ loaded wherever this skill is.
17
+
18
+ **Division of authority:** the dry run is the authority on mechanics — what will
19
+ change, in which currency, and on which evidence. This file is the authority on
20
+ *order*, on *when to stop and ask a human*, and on the hazards the commands cannot
21
+ see. Where the two disagree, believe the command output and fix this file.
22
+
23
+ ## Before this skill: do the reporting-amount columns exist?
24
+
25
+ **Run `dr-cli:fx-conversion-matrix` first.** The selector does not convert anything.
26
+ It **swaps columns**: when a viewer picks GBP, `FxReportingFieldResolver` looks for a
27
+ sibling field on the same template with `fx_role=reporting_amount` and
28
+ `currency_code='GBP'`, and queries that column instead. Its own docstring is blunt
29
+ about it — *"No exchange-rate arithmetic happens here. The converted amounts already
30
+ exist as sibling columns on the same template."*
31
+
32
+ So the converted amounts must already be in the table, one column per currency, before
33
+ this skill can tag them. If no sibling column exists for a currency, the resolver
34
+ returns nothing and that currency simply never appears in the selector — visible
35
+ absence, not wrong numbers. The columns are what this skill makes *selectable*; they
36
+ are not what it creates.
37
+
38
+ Getting those columns computed — posting amount times a rate, per currency — is
39
+ `dr-cli:fx-conversion-matrix`. Land the rates there first, compute the reporting
40
+ amounts, then come back here to tag them.
41
+
42
+ > Not to be confused with the older `CurrencyConversionService` (reached through
43
+ > `CoreEngine.db.table_api_djm`), which DOES multiply by a rate, read from one
44
+ > server-wide table named by `fx_rate_table_id` on the `datarails.com` org. That path
45
+ > is separate from the selector and this skill does not use it. Worth knowing because
46
+ > its failure mode is nastier: its SQL defaults an unresolved rate to 1 (DR-52685), so
47
+ > a stale rate table silently returns unconverted numbers.
48
+
49
+ ## What this actually is
50
+
51
+ Not a toggle. On an existing customer the amount columns are `Float`, so this
52
+ **retypes customer columns to `Monetary`** and triggers a rescan. The org sees new
53
+ behaviour in dashboards and Excel afterwards. Treat it as a change to a production
54
+ tenant, because it is one.
55
+
56
+ The hard part is not running the commands. It is answering **"what currency is this
57
+ column in?"** — the question CS raised in the FX focus group, because for most
58
+ customers `amount` carries no clue. This skill's job is to gather the evidence, and
59
+ to **stop and ask a human when the evidence is not there.** Never guess a currency.
60
+
61
+ ## Order is not optional
62
+
63
+ ```
64
+ flags → posting → reporting → default currency → widgets + functions → verify
65
+ ```
66
+
67
+ Currency conversion needs a value field that is `Monetary` **and** carries an
68
+ `fx_role`. The server answers a conversion request on an untagged field with a
69
+ **400, not a skip**, so running the converters first makes every widget report
70
+ `needs-fx-role`. Tag fields first, always.
71
+
72
+ ---
73
+
74
+ ## Step 0 — Preflight
75
+
76
+ Do not reimplement the CLI install. **With the `dr-cli` plugin**, its hook installs the
77
+ pinned version before this skill runs; if `dr` is missing or the wrong version, route
78
+ to `/dr-cli:install`, and if it is installed but misbehaving, to `/dr-cli:doctor`.
79
+ **Without the plugin** (this skill came from the npm package),
80
+ `dr` is already installed, because that install is what delivered this skill. Update
81
+ it with `dr update`, and use the `dr-cli` skill's troubleshooting for a misbehaving CLI.
82
+
83
+ Four things this skill does check, because each one silently wastes the whole session:
84
+
85
+ 1. **`dr fx` exists.** `dr fx --help` must list `set-posting`, `set-reporting`,
86
+ `default-currency`, `convert-functions`, `convert-widgets`. If it does not, the CLI
87
+ predates FX — `/dr-cli:install`, or `dr update` without the plugin.
88
+ 2. **You are on the right tenant, as the right identity.** Run
89
+ `dr whoami --server <key> --account <email>` and **show the operator the org name
90
+ and id before anything else.** Getting this wrong is the single worst outcome here:
91
+ every write lands on someone else's customer. Use `dr login-as <customer-email>`
92
+ for a customer tenant (see `dr-switch-user`).
93
+ 3. **Feature flags.** `fx_enabled` and `fx_dashboard_selector` must be on for the org:
94
+ `dr features orgs list <org_id>` — a flag's *presence* means enabled. Without them
95
+ the customer sees nothing after all this work. Enabling them is
96
+ `dr features orgs add`, and it is a decision for the PM/CS owner, not this skill.
97
+ 4. **The tenant's backend supports FX.** Run `dr fx default-currency`. Be careful how
98
+ you read it: the CLI treats a missing currency endpoint as "no currencies", so
99
+ *"No reporting currencies available"* means **either** a pre-FX server **or** simply
100
+ that no reporting fields are tagged yet — the command cannot tell you which, and says
101
+ so. On a fresh customer that message is expected, not a blocker. To distinguish the
102
+ two, check whether an FX-era build is deployed (ask R&D or check the release) before
103
+ concluding the tenant cannot support FX.
104
+
105
+ ---
106
+
107
+ ## Step 1 — Map the org
108
+
109
+ Read-only. Build the picture before proposing anything.
110
+
111
+ ```bash
112
+ dr templates list # tables, with ids
113
+ dr dashboards list # dashboards
114
+ dr functions list # DR.GET functions, org-wide
115
+ dr fx default-currency # current default + what the selector can offer
116
+ ```
117
+
118
+ Then, for each table that looks financial, `dr templates get <id> --json` and read the
119
+ fields. You are looking for:
120
+
121
+ - **amount-shaped columns** — `Posting Amount`, `Amount`, `Reporting Amount`,
122
+ `<CODE> Amount`, and anything else numeric that holds money.
123
+ - **currency columns** — `Posting Currency`, `Reporting Currency`. These are the
124
+ evidence for step 2.
125
+ - **rate columns** — `Currency EOP rate`, `Currency AVG rate`, `<CODE> rate`. Their
126
+ presence means the customer is already doing FX by hand.
127
+
128
+ ### Stop and look for existing FX machinery
129
+
130
+ **This is the one that can corrupt a customer's numbers.** Before proposing anything,
131
+ check whether the org already converts currency manually:
132
+
133
+ - calculated fields whose formula multiplies an amount by a rate —
134
+ `dr calc-vals list --table <id> --json`, looking for formulas like
135
+ `[Posting Amount] * [Currency EOP rate]`;
136
+ - FX lookup tables — `dr lut list --table <id>` has an `IS_FX` column;
137
+ - amount columns that are already per-currency (`USD Amount`, `EUR Amount`).
138
+
139
+ If any exist, **say so in the plan and do not proceed on those tables without a human
140
+ decision.** Making a field currency-aware when the data is already converted
141
+ double-converts it. Silent wrong numbers are worse than no feature.
142
+
143
+ ### Coverage across tables
144
+
145
+ A dashboard's selector offers a currency only where the data exists. For each
146
+ dashboard, note which tables feed its widgets, and compute the **intersection** of
147
+ reporting currencies across those tables. A currency present in one table but missing
148
+ in another shows the end user a "currency is missing" card on half the dashboard. Call
149
+ that out in the plan — it is a data gap, not a config bug.
150
+
151
+ ---
152
+
153
+ ## Step 2 — Decide the currency semantics per table
154
+
155
+ For each FX table, two questions.
156
+
157
+ **Which column is the posting amount?** Usually the one named `Posting Amount` — the
158
+ original, mixed-currency figure. `dr fx set-posting` finds it by name.
159
+
160
+ **What currency is each reporting column in?** `set-reporting` answers this itself
161
+ where the evidence exists — existing config, then a code in the field name, then a
162
+ single-valued "Reporting Currency" column — and its dry run prints **which** of those
163
+ it used per field in the `SOURCE` column (`existing`, `field-name`,
164
+ `reporting-currency-values`). Read that column rather than re-deriving the rules here;
165
+ `dr fx set-reporting --help` is their definition.
166
+
167
+ Your job is the case the command deliberately refuses:
168
+
169
+ **Nothing says so → ask.** A table the sweep lists with `!` is one it would not guess
170
+ on. Do not infer from the rate columns, the customer's country, or the biggest number.
171
+ Put the question to the CSM or the customer: *"Your `Amount` column — which currency
172
+ are those figures in?"* An honest blocker beats a plausible guess, because a wrong
173
+ currency code mislabels real money and nobody downstream can tell. Resolve it with
174
+ `--map "<field>=<CODE>"` once a human answers.
175
+
176
+ Sample rows when it helps a human decide — `dr templates sample <id> --rows 20` —
177
+ but sampling shows magnitudes, never a currency. Magnitude is a hint for a human, not
178
+ a conclusion for you.
179
+
180
+ ---
181
+
182
+ ## Step 3 — Propose the plan, then stop
183
+
184
+ Write the plan out and **wait for approval.** Use the CLI's own dry runs as the
185
+ evidence — they are exact, and they are free:
186
+
187
+ ```bash
188
+ dr fx set-posting --all-tables
189
+ dr fx set-reporting --all-tables
190
+ dr fx convert-widgets --all
191
+ dr fx convert-functions
192
+ ```
193
+
194
+ Every one of those is read-only until confirmed. The field commands print a plan and a
195
+ `--confirm-token`; the converters print a plan and need `--confirm`.
196
+
197
+ The plan you show the implementor must contain:
198
+
199
+ - **the tenant** — org name and id, and the identity that will write;
200
+ - **per table**: which field becomes posting, which become reporting and in which
201
+ currency, **and where that currency came from** (name / data / the customer told us);
202
+ - **tables needing a human decision**, quoted from the dry run's `!` lines, with the
203
+ question to ask;
204
+ - **existing manual FX machinery** found in step 1, and what it means;
205
+ - **currency coverage gaps** across each dashboard's tables;
206
+ - **counts** of widgets and functions that will convert, plus how many are skipped and
207
+ why (`needs-sum`, `needs-fx-role`);
208
+ - **the org default currency** you propose, and that it is what AI agents fall back to;
209
+ - **what is NOT covered by the CLI** and stays a manual UI step (see below);
210
+ - **the rollback** for each phase.
211
+
212
+ Do not fold the "needs a human" items into the write step. They are the plan's most
213
+ important output.
214
+
215
+ ---
216
+
217
+ ## Step 4 — Execute, in order, on approval
218
+
219
+ Per table or org-wide with `--all-tables`. Each field command is its own two-step
220
+ token: run it, read the plan, re-run with the printed `--confirm-token`.
221
+
222
+ ```bash
223
+ # 1. posting amounts
224
+ dr fx set-posting --all-tables # plan
225
+ dr fx set-posting --all-tables --confirm-token <t> # apply
226
+
227
+ # 2. reporting amounts (add --map "<field>=<CODE>" for tables the sweep could not judge)
228
+ dr fx set-reporting --all-tables
229
+ dr fx set-reporting --all-tables --confirm-token <t>
230
+
231
+ # a table the sweep skipped, resolved by hand:
232
+ dr fx set-reporting <table_id> --map "Amount=GBP"
233
+
234
+ # a custom currency the ISO list lacks (e.g. budgeted USD):
235
+ dr fx add-currency BUSD --symbol 'B$'
236
+
237
+ # 3. the org default (Super Admin)
238
+ dr fx default-currency USD --confirm
239
+
240
+ # 4. the sweeps
241
+ dr fx convert-widgets --all --confirm
242
+ dr fx convert-functions --confirm
243
+ ```
244
+
245
+ Rules while executing:
246
+
247
+ - **Never pass `--force-sum` without saying so in the plan.** It changes a widget's
248
+ aggregator to SUM, which changes what the customer sees.
249
+ - **Never use `--allow-custom` or `--allow-unlisted` to get past a validation error.**
250
+ They exist for a code you know is coming, not to silence a refusal.
251
+ - **Re-runs are safe.** Every command is idempotent — an already-configured field
252
+ reports `unchanged`, an already-aware widget reports `already`. If a run half-fails,
253
+ fix the cause and run it again; do not hand-patch.
254
+ - **A failure is not a reason to skip.** `needs-fx-role` means step 1 or 2 is
255
+ incomplete for that field. Go back rather than forcing.
256
+
257
+ ### Capture a rollback point first
258
+
259
+ For the widget sweep, save the before-state so there is a restore path beyond the flag:
260
+
261
+ ```bash
262
+ dr widgets get <id> -d <dashboard> --json > before-widget-<id>.json
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Step 5 — Verify, and report
268
+
269
+ Do not stop at "the commands succeeded".
270
+
271
+ ```bash
272
+ dr fx set-posting --all-tables # expect: nothing left to set
273
+ dr fx set-reporting --all-tables # expect: nothing left to update
274
+ dr fx convert-widgets --all # expect: nothing left to convert
275
+ dr fx convert-functions # expect: nothing left to convert
276
+ dr fx default-currency # expect: the agreed default, and the currency list
277
+ ```
278
+
279
+ Read the counts the commands print rather than matching exact wording — these summary
280
+ lines belong to the CLI and change with it. What you are checking is that a re-run
281
+ plans no further changes, and that the tables it still lists with `!` are the ones you
282
+ already reported as needing a customer answer.
283
+
284
+ Then confirm it works for a human, not just in the API: open a converted dashboard,
285
+ switch the selector, and check the numbers change and no widget shows the
286
+ "currency is missing" card. A widget whose table lacks the selected currency is a data
287
+ gap to report, not a config error to fix here.
288
+
289
+ Report: what changed (tables, fields, widgets, functions, default), what was skipped
290
+ and why, the open questions for the customer, the manual UI steps still outstanding,
291
+ and the rollback commands.
292
+
293
+ ---
294
+
295
+ ## What the CLI cannot do — say this out loud
296
+
297
+ These stay manual, and omitting them from the report leaves the job half-done:
298
+
299
+ - **Excel report currency.** `convert-functions` sets the DR.GET flag, but a saved
300
+ Excel report carries its own `currency_code`, and the file-level selector is UI only.
301
+ - **Clearing an FX role.** `set-posting`/`set-reporting` set roles; removing one is the
302
+ Tables UI.
303
+ - **Per-user currency preference.** Users pick their own currency per dashboard; only
304
+ the org default is scriptable.
305
+
306
+ ## Rollback
307
+
308
+ - **Widgets / functions:** `dr fx convert-widgets --all --off --confirm` and
309
+ `dr fx convert-functions --off --confirm`. Acts on the stored flag alone, so it also
310
+ clears flags left on entities that no longer qualify.
311
+ - **A widget body:** `dr widgets update <id> -d <dashboard> --file before-widget-<id>.json`.
312
+ - **Org default:** `dr fx default-currency --none --confirm` (back to posting currency)
313
+ or set the previous code.
314
+ - **Field roles:** no CLI path — the Tables UI. Say so rather than implying a clean undo.
315
+
316
+ ## Failure modes worth recognising
317
+
318
+ | Symptom | Cause | Fix |
319
+ |---|---|---|
320
+ | Selector shows `N/A` | Widget has no single Monetary value field | Check the widget's value section; only one value field can be currency-aware |
321
+ | Selector dropdown empty | No `reporting_amount` fields tagged, or flags off | Step 2, then check `fx_enabled` |
322
+ | Widget shows "currency is missing" | That table has no reporting field for the selected currency | Data gap — report it, do not force |
323
+ | Everything reports `needs-fx-role` | Converters ran before the field commands | Do step 1–2 first |
324
+ | `needs-sum` | Aggregator is not SUM; conversion is SUM-only | Decide with the owner, then `--force-sum` deliberately |
325
+ | Numbers doubled after enabling | The org already converted manually | Stop, roll back, resolve the existing FX machinery |