@datarails-ai/dr-cli 1.6.96 → 1.6.97

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.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@datarails-ai/dr-cli",
11
- version: "1.6.96",
11
+ version: "1.6.97",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -67,7 +67,7 @@ var package_default = {
67
67
  };
68
68
 
69
69
  // src/buildInfo.ts
70
- var BUILD_TIME = "2026-10-04T14:30:08.766Z";
70
+ var BUILD_TIME = "2026-10-05T07:44:13.912Z";
71
71
  function formatBuildTime(iso) {
72
72
  if (iso === "") return "";
73
73
  const d = new Date(iso);
@@ -3624,6 +3624,10 @@ var PROD_GATE_TIER_B = /* @__PURE__ */ new Set([
3624
3624
  // tier so a CSM or agent can run a MEC implementation's bulk assignment on
3625
3625
  // prod (DR-54064).
3626
3626
  "recon-member-batch|set",
3627
+ // clear-assignee --file: N `recon-member|clear` writes under one token, same
3628
+ // reasoning — each is undone by set-assignee, already-empty slots are
3629
+ // skipped, and the plan shows the sign-offs it resets (DR-54260).
3630
+ "recon-member-batch|clear",
3627
3631
  "recon-member|clear",
3628
3632
  "recon-member|set-due-date",
3629
3633
  "recon-member|sign",
@@ -42254,31 +42258,51 @@ function validateRow(row, idx) {
42254
42258
 
42255
42259
  // src/commands/_reconciliationAssigneeCsv.ts
42256
42260
  import { promises as fs17 } from "fs";
42257
- var REQUIRED_COLUMNS = ["account_id", "type", "user_id"];
42258
- var OPTIONAL_COLUMNS = ["user_name", "user_email"];
42259
- var KNOWN_COLUMNS = /* @__PURE__ */ new Set([...REQUIRED_COLUMNS, ...OPTIONAL_COLUMNS]);
42261
+ var ASSIGNEE_REQUIRED = ["account_id", "type", "user_id"];
42262
+ var ASSIGNEE_OPTIONAL = ["user_name", "user_email"];
42263
+ var CLEAR_REQUIRED = ["account_id", "type"];
42260
42264
  async function readAssigneeCsv(path11) {
42261
- let raw;
42265
+ return parseAssigneeCsvText(await readFileArg(path11));
42266
+ }
42267
+ async function readClearAssigneeCsv(path11) {
42268
+ return parseClearAssigneeCsvText(await readFileArg(path11));
42269
+ }
42270
+ async function readFileArg(path11) {
42262
42271
  try {
42263
- raw = await fs17.readFile(path11, "utf8");
42272
+ return await fs17.readFile(path11, "utf8");
42264
42273
  } catch (err) {
42265
42274
  const m = err instanceof Error ? err.message : String(err);
42266
42275
  throw new Error(`read --file: ${m}`);
42267
42276
  }
42268
- return parseAssigneeCsvText(raw);
42269
42277
  }
42270
42278
  function parseAssigneeCsvText(text2) {
42279
+ return parseSlotRows(text2, ASSIGNEE_REQUIRED, ASSIGNEE_OPTIONAL, (slot, cell) => {
42280
+ const rawUserId = cell("user_id");
42281
+ const userId = Number(rawUserId);
42282
+ if (!/^\d+$/.test(rawUserId) || !Number.isSafeInteger(userId) || userId <= 0) {
42283
+ throw new Error(
42284
+ `--file: line ${slot.lineNo}: invalid user_id "${rawUserId}" (expected positive integer)`
42285
+ );
42286
+ }
42287
+ return { ...slot, userId, userName: cell("user_name"), userEmail: cell("user_email") };
42288
+ });
42289
+ }
42290
+ function parseClearAssigneeCsvText(text2) {
42291
+ return parseSlotRows(text2, CLEAR_REQUIRED, [], (slot) => slot);
42292
+ }
42293
+ function parseSlotRows(text2, required2, optional, extra) {
42271
42294
  const withoutBom = text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
42272
42295
  const rows = parseCsvRows(withoutBom);
42273
42296
  if (rows.length === 0) {
42274
42297
  throw new Error("--file: CSV is empty");
42275
42298
  }
42299
+ const known = /* @__PURE__ */ new Set([...required2, ...optional]);
42276
42300
  const header = rows[0].map((h) => h.trim());
42277
42301
  const col = /* @__PURE__ */ new Map();
42278
42302
  header.forEach((h, i) => {
42279
- if (!KNOWN_COLUMNS.has(h)) {
42303
+ if (!known.has(h)) {
42280
42304
  throw new Error(
42281
- `--file: unknown CSV column "${h}" \u2014 expected ${[...REQUIRED_COLUMNS, ...OPTIONAL_COLUMNS].join(",")}`
42305
+ `--file: unknown CSV column "${h}" \u2014 expected ${[...required2, ...optional].join(",")}`
42282
42306
  );
42283
42307
  }
42284
42308
  if (col.has(h)) {
@@ -42286,7 +42310,7 @@ function parseAssigneeCsvText(text2) {
42286
42310
  }
42287
42311
  col.set(h, i);
42288
42312
  });
42289
- for (const c of REQUIRED_COLUMNS) {
42313
+ for (const c of required2) {
42290
42314
  if (!col.has(c)) {
42291
42315
  throw new Error(`--file: CSV header is missing required column "${c}"`);
42292
42316
  }
@@ -42295,30 +42319,24 @@ function parseAssigneeCsvText(text2) {
42295
42319
  if (dataRows.length === 0) {
42296
42320
  throw new Error("--file: CSV has no data rows (header only)");
42297
42321
  }
42298
- const cell = (cells, name) => {
42299
- const i = col.get(name);
42300
- return i === void 0 ? "" : (cells[i] ?? "").trim();
42301
- };
42302
42322
  const seenSlots = /* @__PURE__ */ new Map();
42303
42323
  const out = [];
42304
42324
  for (const { cells, lineNo } of dataRows) {
42305
- const accountId = cell(cells, "account_id");
42325
+ const cell = (name) => {
42326
+ const i = col.get(name);
42327
+ return i === void 0 ? "" : (cells[i] ?? "").trim();
42328
+ };
42329
+ const accountId = cell("account_id");
42306
42330
  if (accountId === "") {
42307
42331
  throw new Error(`--file: line ${lineNo}: "account_id" is required`);
42308
42332
  }
42309
- const rawType = cell(cells, "type");
42333
+ const rawType = cell("type");
42310
42334
  if (rawType !== "Preparer" && rawType !== "Reviewer") {
42311
42335
  throw new Error(
42312
42336
  `--file: line ${lineNo}: invalid type "${rawType}" (expected "Preparer" or "Reviewer")`
42313
42337
  );
42314
42338
  }
42315
- const rawUserId = cell(cells, "user_id");
42316
- const userId = Number(rawUserId);
42317
- if (!/^\d+$/.test(rawUserId) || !Number.isSafeInteger(userId) || userId <= 0) {
42318
- throw new Error(
42319
- `--file: line ${lineNo}: invalid user_id "${rawUserId}" (expected positive integer)`
42320
- );
42321
- }
42339
+ const row = extra({ lineNo, accountId, type: rawType }, cell);
42322
42340
  const slot = `${accountId}:${rawType}`;
42323
42341
  const prior = seenSlots.get(slot);
42324
42342
  if (prior !== void 0) {
@@ -42327,14 +42345,7 @@ function parseAssigneeCsvText(text2) {
42327
42345
  );
42328
42346
  }
42329
42347
  seenSlots.set(slot, lineNo);
42330
- out.push({
42331
- lineNo,
42332
- accountId,
42333
- type: rawType,
42334
- userId,
42335
- userName: cell(cells, "user_name"),
42336
- userEmail: cell(cells, "user_email")
42337
- });
42348
+ out.push(row);
42338
42349
  }
42339
42350
  return out;
42340
42351
  }
@@ -44025,52 +44036,186 @@ verify the date matches the accounts' reporting period (try \`dr reconciliation
44025
44036
  account_id: id,
44026
44037
  members: applyAssignments(byId.get(id), changesByAccount.get(id))
44027
44038
  }));
44028
- extendForBatch(cfg.extendTimeout, requests * 2 - 1);
44029
- let applied = 0;
44030
- for (let i = 0; i < items.length; i += chunkSize) {
44031
- const chunk = items.slice(i, i + chunkSize);
44032
- if (i > 0) {
44033
- let drift;
44034
- try {
44035
- drift = await chunkDrift(cfg, byId, chunk);
44036
- } catch (err) {
44037
- const msg = err instanceof Error ? err.message : String(err);
44038
- throw new Error(
44039
- `re-read failed after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent: ${msg}
44040
- Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
44041
- );
44042
- }
44043
- if (drift.length > 0) {
44044
- throw new Error(
44045
- `halted after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent:
44046
- ` + drift.join("\n ") + `
44047
- The tenant changed since the plan. Re-run the same command without --confirm-token: rows that landed plan as unchanged. If accounts are gone, the trial balance was rebuilt and their ids changed \u2014 wait for it to settle and rebuild the file from \`dr reconciliation accounts list\`.`
44039
+ const applied = await writeMemberChunks(cfg, byId, items, chunkSize);
44040
+ if (cfg.asJson) {
44041
+ const changed = accountIds.flatMap((id) => changesByAccount.get(id).map(toJsonRow));
44042
+ writeJSONIndented(cfg.out, { changed, unchanged: unchanged.map(toJsonRow) });
44043
+ return;
44044
+ }
44045
+ cfg.out.write(
44046
+ `Set ${changeCount} role${plural(changeCount)} on ${applied} account${plural(applied)} (${unchanged.length} unchanged).
44047
+ `
44048
+ );
44049
+ }
44050
+ });
44051
+ }
44052
+ function clearBatchActionId(date, rows) {
44053
+ const canonical = rows.map((r) => [r.accountId, r.type]);
44054
+ return createHash8("sha256").update(JSON.stringify(["clear", date, canonical])).digest("hex").slice(0, 12);
44055
+ }
44056
+ async function runReconciliationAccountsClearAssigneeBatch(cfg) {
44057
+ const action = {
44058
+ resource: "recon-member-batch",
44059
+ id: clearBatchActionId(cfg.date, cfg.rows),
44060
+ verb: "clear"
44061
+ };
44062
+ await preflightConfirmToken(action, cfg.token);
44063
+ extendForBatch(cfg.extendTimeout, 1);
44064
+ const accounts = await getAccounts(
44065
+ cfg.client,
44066
+ { date: cfg.date },
44067
+ { signal: cfg.signal }
44068
+ );
44069
+ const byId = new Map(accounts.map((a) => [a.account_id, a]));
44070
+ const problems = [];
44071
+ for (const row of cfg.rows) {
44072
+ const account = byId.get(row.accountId);
44073
+ if (account === void 0) {
44074
+ problems.push(`line ${row.lineNo}: account "${row.accountId}" not found at --date`);
44075
+ } else if (account.members.length > 0 && !account.members.some((m) => m.type === row.type)) {
44076
+ problems.push(
44077
+ `line ${row.lineNo}: account "${row.accountId}" has a partial member set (${account.members.map((m) => m.type).join(", ")}) and is missing ${row.type}`
44078
+ );
44079
+ }
44080
+ }
44081
+ if (problems.length > 0) {
44082
+ const shown = problems.slice(0, 10);
44083
+ const more = problems.length > 10 ? [`\u2026 (+${problems.length - 10} more)`] : [];
44084
+ throw new Error(
44085
+ `--file: ${problems.length} row${plural(problems.length)} cannot be applied; nothing was written:
44086
+ ` + [...shown, ...more].join("\n ") + `
44087
+ verify the date matches the accounts' reporting period (try \`dr reconciliation accounts list --date \u2026\`)`
44088
+ );
44089
+ }
44090
+ const clearsByAccount = /* @__PURE__ */ new Map();
44091
+ const unchanged = [];
44092
+ for (const row of cfg.rows) {
44093
+ const current = byId.get(row.accountId).members.find((m) => m.type === row.type);
44094
+ if (current === void 0 || current.user_id === 0) {
44095
+ unchanged.push(row);
44096
+ continue;
44097
+ }
44098
+ const list28 = clearsByAccount.get(row.accountId) ?? [];
44099
+ list28.push({ row, current });
44100
+ clearsByAccount.set(row.accountId, list28);
44101
+ }
44102
+ const clearCount = cfg.rows.length - unchanged.length;
44103
+ const n = cfg.rows.length;
44104
+ if (clearCount === 0) {
44105
+ if (cfg.asJson) {
44106
+ writeJSONIndented(cfg.out, {
44107
+ changed: [],
44108
+ unchanged: unchanged.map((r) => ({ account_id: r.accountId, type: r.type }))
44109
+ });
44110
+ return;
44111
+ }
44112
+ cfg.out.write(
44113
+ `Nothing to clear: all ${n} row${plural(n)} name a slot that is already Unassigned or does not exist yet.
44114
+ `
44115
+ );
44116
+ return;
44117
+ }
44118
+ const chunkSize = cfg.chunkSize ?? ASSIGNEE_BATCH_CHUNK_SIZE;
44119
+ const accountIds = [...clearsByAccount.keys()];
44120
+ const requests = Math.ceil(accountIds.length / chunkSize);
44121
+ await twoStepConfirm({
44122
+ out: cfg.out,
44123
+ action,
44124
+ token: cfg.token,
44125
+ server: cfg.serverFlag,
44126
+ confirmCmd: cfg.confirmCmd,
44127
+ describe: async () => {
44128
+ let signOffsReset = 0;
44129
+ const preview = [];
44130
+ for (const id of accountIds) {
44131
+ const account = byId.get(id);
44132
+ for (const { row, current } of clearsByAccount.get(id)) {
44133
+ if (current.is_sign_off) signOffsReset += 1;
44134
+ if (preview.length < 5) {
44135
+ preview.push(
44136
+ ` ${account.account_name} (${id}) ${row.type}: ${current.name} (user ${current.user_id}) \u2192 Unassigned`
44048
44137
  );
44049
44138
  }
44050
44139
  }
44051
- try {
44052
- await updateAccountMembers(cfg.client, chunk, { signal: cfg.signal });
44053
- } catch (err) {
44054
- const msg = err instanceof Error ? err.message : String(err);
44055
- throw new Error(
44056
- `PATCH failed after ${applied} of ${items.length} account${plural(items.length)} were written (accounts ${i + 1}\u2013${i + chunk.length} may be partially applied): ${msg}
44057
- Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
44058
- );
44059
- }
44060
- applied += chunk.length;
44061
44140
  }
44141
+ const lines = [
44142
+ `rows: ${n}`,
44143
+ `to clear: ${clearCount} role${plural(clearCount)} on ${accountIds.length} account${plural(accountIds.length)} (${requests} PATCH request${plural(requests)})`,
44144
+ `unchanged: ${unchanged.length} (slot already Unassigned, or the account has no member slots \u2014 skipped)`,
44145
+ `sign-offs reset: ${signOffsReset}`,
44146
+ "changes:",
44147
+ ...preview
44148
+ ];
44149
+ if (clearCount > preview.length) lines.push(` \u2026 (+${clearCount - preview.length} more)`);
44150
+ return lines;
44151
+ },
44152
+ execute: async () => {
44153
+ const items = accountIds.map((id) => {
44154
+ const cleared = new Set(clearsByAccount.get(id).map((c) => c.row.type));
44155
+ return {
44156
+ account_id: id,
44157
+ members: byId.get(id).members.map(
44158
+ (m) => cleared.has(m.type) ? unassignedMember(m.type, m.due_date) : memberSummaryToWire(m)
44159
+ )
44160
+ };
44161
+ });
44162
+ const applied = await writeMemberChunks(cfg, byId, items, chunkSize);
44062
44163
  if (cfg.asJson) {
44063
- const changed = accountIds.flatMap((id) => changesByAccount.get(id).map(toJsonRow));
44064
- writeJSONIndented(cfg.out, { changed, unchanged: unchanged.map(toJsonRow) });
44164
+ writeJSONIndented(cfg.out, {
44165
+ changed: accountIds.flatMap(
44166
+ (id) => clearsByAccount.get(id).map(({ row, current }) => ({
44167
+ account_id: row.accountId,
44168
+ type: row.type,
44169
+ previous_user_id: current.user_id
44170
+ }))
44171
+ ),
44172
+ unchanged: unchanged.map((r) => ({ account_id: r.accountId, type: r.type }))
44173
+ });
44065
44174
  return;
44066
44175
  }
44067
44176
  cfg.out.write(
44068
- `Set ${changeCount} role${plural(changeCount)} on ${applied} account${plural(applied)} (${unchanged.length} unchanged).
44177
+ `Cleared ${clearCount} role${plural(clearCount)} on ${applied} account${plural(applied)} (${unchanged.length} unchanged).
44069
44178
  `
44070
44179
  );
44071
44180
  }
44072
44181
  });
44073
44182
  }
44183
+ async function writeMemberChunks(ctx, planned, items, chunkSize) {
44184
+ const requests = Math.ceil(items.length / chunkSize);
44185
+ extendForBatch(ctx.extendTimeout, requests * 2);
44186
+ let applied = 0;
44187
+ for (let i = 0; i < items.length; i += chunkSize) {
44188
+ const chunk = items.slice(i, i + chunkSize);
44189
+ let drift;
44190
+ try {
44191
+ drift = await chunkDrift(ctx, planned, chunk);
44192
+ } catch (err) {
44193
+ const msg = err instanceof Error ? err.message : String(err);
44194
+ throw new Error(
44195
+ `re-read failed after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent: ${msg}
44196
+ Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
44197
+ );
44198
+ }
44199
+ if (drift.length > 0) {
44200
+ throw new Error(
44201
+ `halted after ${applied} of ${items.length} account${plural(items.length)} were written; nothing more was sent:
44202
+ ` + drift.join("\n ") + `
44203
+ The tenant changed since the plan. Re-run the same command without --confirm-token: rows that landed plan as unchanged. If accounts are gone, the trial balance was rebuilt and their ids changed \u2014 wait for it to settle and rebuild the file from \`dr reconciliation accounts list\`.`
44204
+ );
44205
+ }
44206
+ try {
44207
+ await updateAccountMembers(ctx.client, chunk, { signal: ctx.signal });
44208
+ } catch (err) {
44209
+ const msg = err instanceof Error ? err.message : String(err);
44210
+ throw new Error(
44211
+ `PATCH failed after ${applied} of ${items.length} account${plural(items.length)} were written (accounts ${i + 1}\u2013${i + chunk.length} may be partially applied): ${msg}
44212
+ Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
44213
+ );
44214
+ }
44215
+ applied += chunk.length;
44216
+ }
44217
+ return applied;
44218
+ }
44074
44219
  async function chunkDrift(cfg, planned, chunk) {
44075
44220
  const now = await getAccounts(cfg.client, { date: cfg.date }, { signal: cfg.signal });
44076
44221
  const nowById = new Map(now.map((a) => [a.account_id, a]));
@@ -44579,16 +44724,69 @@ function wireReconciliation(program) {
44579
44724
  );
44580
44725
  addServerFlag(setAssigneeCmd);
44581
44726
  addAccountFlag(setAssigneeCmd);
44582
- const clearAssigneeCmd = accounts.command("clear-assignee").description(
44583
- "Clear the Preparer or Reviewer slot (sets user_id=0 + Unassigned name/email; preserves due_date, resets sign-off)"
44584
- ).argument("<account_uuid>", "account uuid").requiredOption("--date <YYYY-MM-DD>", "Reporting date").requiredOption("--type <s>", "Preparer | Reviewer (case-sensitive)").option("--json", "Output the cleared member record as JSON instead of a confirmation", false).option(
44727
+ const clearAssigneeCmd = accounts.command("clear-assignee").summary("Clear the Preparer or Reviewer slot of one account, or many with --file").description(
44728
+ [
44729
+ "Clear the Preparer or Reviewer slot (sets user_id=0 + Unassigned name/email; preserves due_date, resets sign-off)",
44730
+ "",
44731
+ "Bulk form \u2014 one plan and one token for many slots:",
44732
+ " dr reconciliation accounts clear-assignee --file slots.csv --date 2025-04-30",
44733
+ " # CSV header: account_id,type \u2014 one row per (account, role)"
44734
+ ].join("\n")
44735
+ ).argument("[account_uuid]", "account uuid; omit with --file").requiredOption("--date <YYYY-MM-DD>", "Reporting date").option("--type <s>", "Preparer | Reviewer (case-sensitive); required without --file").option(
44736
+ "--file <path>",
44737
+ "CSV of slots to clear (header account_id,type); one plan + one token for the whole file"
44738
+ ).option(
44739
+ "--json",
44740
+ "Output the cleared member record(s) as JSON instead of a confirmation",
44741
+ false
44742
+ ).option("--timeout <seconds>", `With --file: ${BATCH_TIMEOUT_FLAG_HELP}`).option(
44585
44743
  "--confirm-token <token>",
44586
44744
  "Token from a prior plan run \u2014 required to actually clear",
44587
44745
  ""
44588
44746
  ).action(
44589
44747
  async (accountUuid, opts) => {
44590
44748
  const date = parseDateFlag(opts.date ?? "", "--date");
44591
- const type = parseMemberTypeFlag(opts.type ?? "", "--type");
44749
+ if (opts.file !== void 0) {
44750
+ const conflicting = [
44751
+ accountUuid !== void 0 ? "<account_uuid>" : "",
44752
+ opts.type !== void 0 ? "--type" : ""
44753
+ ].filter((s) => s !== "");
44754
+ if (conflicting.length > 0) {
44755
+ throw new Error(
44756
+ `--file cannot be combined with ${conflicting.join(", ")} (the CSV carries them per row)`
44757
+ );
44758
+ }
44759
+ const rows = await readClearAssigneeCsv(opts.file);
44760
+ await preflightConfirmToken(
44761
+ { resource: "recon-member-batch", id: clearBatchActionId(date, rows), verb: "clear" },
44762
+ opts.confirmToken
44763
+ );
44764
+ const serverKey2 = resolveServer(opts.server);
44765
+ const confirmCmd2 = `dr reconciliation accounts clear-assignee --file ${JSON.stringify(opts.file)} --date ${opts.date}` + (opts.json === true ? " --json" : "") + (opts.timeout !== void 0 ? ` --timeout ${opts.timeout}` : "") + ` --server ${serverKey2}`;
44766
+ const setup2 = await setupCall(opts.server, opts.account, batchSetupOptions(opts.timeout));
44767
+ try {
44768
+ await runReconciliationAccountsClearAssigneeBatch({
44769
+ client: setup2.client,
44770
+ extendTimeout: setup2.extendTimeout,
44771
+ date,
44772
+ rows,
44773
+ token: opts.confirmToken,
44774
+ confirmCmd: confirmCmd2,
44775
+ serverFlag: serverKey2,
44776
+ asJson: opts.json === true,
44777
+ signal: setup2.signal,
44778
+ out: process.stdout
44779
+ });
44780
+ } finally {
44781
+ setup2.cleanup();
44782
+ }
44783
+ return;
44784
+ }
44785
+ if (accountUuid === void 0) {
44786
+ throw new Error("missing <account_uuid> (or pass --file <csv> for many slots)");
44787
+ }
44788
+ if (opts.type === void 0) throw new Error("required option '--type <s>' not specified");
44789
+ const type = parseMemberTypeFlag(opts.type, "--type");
44592
44790
  await preflightConfirmToken(
44593
44791
  {
44594
44792
  resource: "recon-member",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.96",
3
+ "cliVersion": "1.6.97",
4
4
  "_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
5
5
  "commands": {
6
6
  "login": {
@@ -45,8 +45,8 @@ Account assignment / sign-off (all take the bank-account UUID via
45
45
  resets. Every account is checked before anything is written, and the
46
46
  write goes out in PATCHes of 100 accounts. If one fails, the error says
47
47
  how many accounts landed; re-run the same file without a token and the
48
- new plan covers only the rest. Before each PATCH after the first, the
49
- accounts are re-read; if any account in the next chunk is gone (a
48
+ new plan covers only the rest. Before each PATCH, the accounts are
49
+ re-read; if any account in the next chunk is gone (a
50
50
  trial-balance rebuild re-mints account ids) or its member slots changed
51
51
  since the plan, the run halts, says how many accounts landed, and names
52
52
  the drifted accounts. Gone accounts mean the file must be rebuilt from
@@ -55,6 +55,13 @@ Account assignment / sign-off (all take the bank-account UUID via
55
55
  - `reconciliation accounts clear-assignee <account_uuid> --date <YYYY-MM-DD>
56
56
  --type Preparer|Reviewer` — unassigns (preserves due_date, resets sign-off).
57
57
  Two-step confirm.
58
+ - `reconciliation accounts clear-assignee --file <csv> --date <YYYY-MM-DD>` —
59
+ the bulk form. Header `account_id,type`, one row per (account, role); the
60
+ user columns of a set-assignee file are rejected, so a set-assignee
61
+ manifest cannot be passed here by mistake. One plan and one token cover the
62
+ whole file. Slots already Unassigned, and accounts with no member slots,
63
+ are skipped. Same 100-account PATCHes, per-chunk re-check, and resume
64
+ behaviour as `set-assignee --file`.
58
65
  - `reconciliation accounts set-due-date <account_uuid> --date <YYYY-MM-DD>
59
66
  --type Preparer|Reviewer (--due-date <YYYY-MM-DD> | --epoch <n> | --clear)`.
60
67
  Two-step confirm.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.96",
3
+ "version": "1.6.97",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {