@datarails-ai/dr-cli 1.6.123 → 1.6.124

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.123",
11
+ version: "1.6.124",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -69,7 +69,7 @@ var package_default = {
69
69
  };
70
70
 
71
71
  // src/buildInfo.ts
72
- var BUILD_TIME = "2026-10-12T11:20:38.309Z";
72
+ var BUILD_TIME = "2026-10-12T11:50:53.998Z";
73
73
  function formatBuildTime(iso) {
74
74
  if (iso === "") return "";
75
75
  const d = new Date(iso);
@@ -4474,13 +4474,14 @@ function isBatchTimeoutClamped(callCount) {
4474
4474
  const n = Number.isFinite(callCount) ? Math.max(1, Math.floor(callCount)) : 1;
4475
4475
  return DEFAULT_CALL_TIMEOUT_MS + n * BATCH_PER_TARGET_TIMEOUT_MS > BATCH_MAX_TIMEOUT_MS;
4476
4476
  }
4477
- function extendForBatch(extend, callCount) {
4477
+ var TIMEOUT_FLAG_REMEDY = "re-run it with --timeout <seconds> (a floor, so a larger value only ever widens the budget)";
4478
+ function extendForBatch(extend, callCount, remedy = TIMEOUT_FLAG_REMEDY) {
4478
4479
  extend?.(batchTimeoutMs(callCount));
4479
4480
  if (!isBatchTimeoutClamped(callCount)) return;
4480
4481
  const budgetS = BATCH_MAX_TIMEOUT_MS / 1e3;
4481
4482
  const perCallMs = Math.floor(BATCH_MAX_TIMEOUT_MS / Math.floor(callCount));
4482
4483
  process.stderr.write(
4483
- `warning: ${callCount} calls exceed the ${budgetS}s ceiling on the auto-sized budget \u2014 about ${perCallMs}ms per call. If the run aborts part-way, re-run it with --timeout <seconds> (a floor, so a larger value only ever widens the budget).
4484
+ `warning: ${callCount} calls exceed the ${budgetS}s ceiling on the auto-sized budget \u2014 about ${perCallMs}ms per call. If the run aborts part-way, ${remedy}.
4484
4485
  `
4485
4486
  );
4486
4487
  }
@@ -5419,8 +5420,8 @@ async function openBrowser(url) {
5419
5420
  async function readLine(stdin) {
5420
5421
  return new Promise((resolve3, reject) => {
5421
5422
  let data4 = "";
5422
- const onData = (chunk) => {
5423
- data4 += chunk.toString();
5423
+ const onData = (chunk2) => {
5424
+ data4 += chunk2.toString();
5424
5425
  const i = data4.indexOf("\n");
5425
5426
  if (i >= 0) {
5426
5427
  cleanup();
@@ -5512,8 +5513,8 @@ async function readHiddenLineFromTty(prompt, stdin, out) {
5512
5513
  cleanup();
5513
5514
  reject(err);
5514
5515
  };
5515
- const onData = (chunk) => {
5516
- const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
5516
+ const onData = (chunk2) => {
5517
+ const s = typeof chunk2 === "string" ? chunk2 : chunk2.toString("utf8");
5517
5518
  for (const ch of s) {
5518
5519
  if (ch === "\r" || ch === "\n") {
5519
5520
  cleanup();
@@ -5561,8 +5562,8 @@ async function readFirstLineFromPipe(stdin) {
5561
5562
  cleanup();
5562
5563
  resolve3(line.endsWith("\r") ? line.slice(0, -1) : line);
5563
5564
  };
5564
- const onData = (chunk) => {
5565
- data4 += typeof chunk === "string" ? chunk : chunk.toString("utf8");
5565
+ const onData = (chunk2) => {
5566
+ data4 += typeof chunk2 === "string" ? chunk2 : chunk2.toString("utf8");
5566
5567
  const i = data4.indexOf("\n");
5567
5568
  if (i >= 0) finish(data4.slice(0, i));
5568
5569
  };
@@ -6371,15 +6372,15 @@ async function commandDelete(client, id, options) {
6371
6372
  function strOr(s, fallback) {
6372
6373
  return s === "" ? fallback : s;
6373
6374
  }
6374
- function parsePositiveIntsArgs(args) {
6375
+ function parsePositiveIntsArgs(args, label = "device id") {
6375
6376
  const out = [];
6376
6377
  for (const a of args) {
6377
6378
  const n = Number(a);
6378
6379
  if (!Number.isInteger(n) || String(n) !== a) {
6379
- throw new Error(`invalid device id "${a}": not a number`);
6380
+ throw new Error(`invalid ${label} "${a}": not a number`);
6380
6381
  }
6381
6382
  if (n <= 0) {
6382
- throw new Error(`invalid device id "${a}": must be positive`);
6383
+ throw new Error(`invalid ${label} "${a}": must be positive`);
6383
6384
  }
6384
6385
  out.push(n);
6385
6386
  }
@@ -6392,6 +6393,16 @@ function parsePositiveIntArg(raw, label) {
6392
6393
  }
6393
6394
  return n;
6394
6395
  }
6396
+ function chunk(items, size) {
6397
+ if (!Number.isInteger(size) || size <= 0) {
6398
+ throw new Error("chunk: size must be a positive integer");
6399
+ }
6400
+ const out = [];
6401
+ for (let i = 0; i < items.length; i += size) {
6402
+ out.push(items.slice(i, i + size));
6403
+ }
6404
+ return out;
6405
+ }
6395
6406
 
6396
6407
  // src/commands/_retry.ts
6397
6408
  var AMBIGUOUS_STATUSES = /* @__PURE__ */ new Set([502, 504, 524]);
@@ -19121,6 +19132,22 @@ async function updateAutoCalculateSettings(client, docId, body, reqOptions) {
19121
19132
  async function downloadVersion(client, versionId, reqOptions) {
19122
19133
  return client.do("GET", `/api/documents/versions/${versionId}/file`, void 0, reqOptions);
19123
19134
  }
19135
+ var KIND_EXPLANATION_PATH = "/api/workspace/documents/kind/explanation";
19136
+ var KIND_SUMMARY_PATH = "/api/workspace/documents/kind/summary";
19137
+ var KIND_EXPLAIN_MAX_IDS = 200;
19138
+ var KIND_EXPLAIN_MAX_IDS_WITH_HISTORY = 20;
19139
+ function kindExplanationPath(query) {
19140
+ let path12 = `${KIND_EXPLANATION_PATH}?ids=${query.ids.join(",")}`;
19141
+ if (query.asOf !== void 0) path12 += `&as_of=${encodeURIComponent(query.asOf)}`;
19142
+ if (query.history === true) path12 += "&history=true";
19143
+ return path12;
19144
+ }
19145
+ async function getKindExplanation(client, query, reqOptions) {
19146
+ return client.do("GET", kindExplanationPath(query), void 0, reqOptions);
19147
+ }
19148
+ async function getKindSummary(client, reqOptions) {
19149
+ return client.do("GET", KIND_SUMMARY_PATH, void 0, reqOptions);
19150
+ }
19124
19151
  async function listBucketTemplates(client, bucketId, reqOptions) {
19125
19152
  const out = await client.getJSON(
19126
19153
  `/api/buckets/${bucketId}/templates`,
@@ -35462,6 +35489,678 @@ function registerFileboxAdminCommands(fb, setupCall2, addServerFlag2, addAccount
35462
35489
  addAccountFlag2(vrSetCmd);
35463
35490
  }
35464
35491
 
35492
+ // src/commands/_reconciliationDate.ts
35493
+ function validateDateString(orig, flagName) {
35494
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(orig)) {
35495
+ throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD)`);
35496
+ }
35497
+ const parts = orig.split("-").map(Number);
35498
+ const [year, month1, day] = parts;
35499
+ const probe = new Date(Date.UTC(year, month1 - 1, day));
35500
+ if (probe.getUTCFullYear() !== year || probe.getUTCMonth() + 1 !== month1 || probe.getUTCDate() !== day) {
35501
+ throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD)`);
35502
+ }
35503
+ return { year, month1, day };
35504
+ }
35505
+ function parseDateFlag(orig, flagName) {
35506
+ let normalized = orig;
35507
+ if (/^\d{4}-\d{2}$/.test(orig)) {
35508
+ normalized = `${orig}-01`;
35509
+ }
35510
+ let year;
35511
+ let month1;
35512
+ try {
35513
+ ({ year, month1 } = validateDateString(normalized, flagName));
35514
+ } catch {
35515
+ throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD or YYYY-MM)`);
35516
+ }
35517
+ return Math.floor(Date.UTC(year, month1, 0) / 1e3);
35518
+ }
35519
+ function parseDateFlagEpoch(orig, flagName) {
35520
+ let normalized = orig;
35521
+ if (/^\d{4}-\d{2}$/.test(orig)) {
35522
+ normalized = `${orig}-01`;
35523
+ }
35524
+ let year;
35525
+ let month1;
35526
+ try {
35527
+ ({ year, month1 } = validateDateString(normalized, flagName));
35528
+ } catch {
35529
+ throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD or YYYY-MM)`);
35530
+ }
35531
+ return Math.floor(Date.UTC(year, month1 - 1, 1, 12) / 1e3);
35532
+ }
35533
+ function parseDateFlagDay(orig, flagName) {
35534
+ const { year, month1, day } = validateDateString(orig, flagName);
35535
+ return Math.floor(Date.UTC(year, month1 - 1, day) / 1e3);
35536
+ }
35537
+
35538
+ // src/commands/filebox_kind.ts
35539
+ function parseKindExplainIds(args) {
35540
+ const parts = args.flatMap((arg) => arg.split(","));
35541
+ return [...new Set(parsePositiveIntsArgs(parts, "filebox id"))];
35542
+ }
35543
+ function parseKindAsOf(raw) {
35544
+ if (raw === void 0) return void 0;
35545
+ parseDateFlagDay(raw, "--as-of");
35546
+ return raw;
35547
+ }
35548
+ var SUPPORT_ONLY_CODES = /* @__PURE__ */ new Set([
35549
+ "kind_explanation_support_only",
35550
+ "kind_summary_support_only"
35551
+ ]);
35552
+ function jsonObject(text2) {
35553
+ try {
35554
+ const parsed2 = JSON.parse(text2);
35555
+ if (typeof parsed2 === "object" && parsed2 !== null && !Array.isArray(parsed2)) {
35556
+ return parsed2;
35557
+ }
35558
+ } catch {
35559
+ }
35560
+ return void 0;
35561
+ }
35562
+ function describeKindErrorBody(text2) {
35563
+ const raw = truncate3(text2.trim(), 200);
35564
+ const body = jsonObject(text2);
35565
+ if (body === void 0) return raw;
35566
+ for (const key of ["message", "detail"]) {
35567
+ const value = body[key];
35568
+ if (typeof value === "string" && value !== "") return value;
35569
+ }
35570
+ const parts = Object.entries(body).map(([field, value]) => {
35571
+ const messages = (Array.isArray(value) ? value : [value]).map(
35572
+ (m) => typeof m === "string" ? m : JSON.stringify(m)
35573
+ );
35574
+ return field === "non_field_errors" ? messages.join("; ") : `${field}: ${messages.join("; ")}`;
35575
+ });
35576
+ return parts.length > 0 ? parts.join("; ") : raw;
35577
+ }
35578
+ function kindReadError(status2, text2, ctx) {
35579
+ const server = ctx.client.serverKey;
35580
+ const body = jsonObject(text2);
35581
+ const code = typeof body?.["code"] === "string" ? body["code"] : void 0;
35582
+ if (status2 === 405 && code !== void 0 && SUPPORT_ONLY_CODES.has(code)) {
35583
+ const scope = ctx.command === "kind-explain" ? "only the support seat of the fileboxes' own organization gets an answer" : "it answers the support seat of an organization, for that organization";
35584
+ return new Error(
35585
+ `${ctx.command}: the server refused ${ctx.client.email} (HTTP 405 ${code}: ${describeKindErrorBody(text2)}) \u2014 ${scope}. Log in as that organization's support seat, then run the command as it:
35586
+ dr login-as <support seat email> --server ${server}
35587
+ dr filebox ${ctx.command} \u2026 --server ${server} --account <support seat email>`
35588
+ );
35589
+ }
35590
+ if (status2 === 404) {
35591
+ return new Error(
35592
+ `GET ${ctx.route}: HTTP 404 \u2014 ${server} most likely does not have the filebox-type endpoints yet. They ship with dr-datarails-server PR #8010 (DR-53666); until that change is deployed on ${server}, ${ctx.command} has nothing to ask.`
35593
+ );
35594
+ }
35595
+ if (status2 === 400) {
35596
+ return new Error(
35597
+ `${ctx.command}: the server rejected the request (HTTP 400): ${describeKindErrorBody(text2)}`
35598
+ );
35599
+ }
35600
+ if (status2 === 403) {
35601
+ return new Error(
35602
+ `${ctx.command}: the server refused ${ctx.client.email} (HTTP 403): ${describeKindErrorBody(text2)} \u2014 a blocked seat gets this answer. Check the seat with \`dr whoami --server ${server} --account ${ctx.client.email}\`.`
35603
+ );
35604
+ }
35605
+ return new Error(`GET ${ctx.route}: HTTP ${status2}: ${truncate3(text2.trim(), 200)}`);
35606
+ }
35607
+ async function readKindAnswer(res, ctx) {
35608
+ const text2 = await res.text();
35609
+ if (res.status < 200 || res.status >= 300) throw kindReadError(res.status, text2, ctx);
35610
+ return text2;
35611
+ }
35612
+ function parseKindBody(text2, route) {
35613
+ try {
35614
+ return JSON.parse(text2);
35615
+ } catch (err) {
35616
+ const msg = err instanceof Error ? err.message : String(err);
35617
+ throw new Error(`GET ${route}: decode: ${msg}`);
35618
+ }
35619
+ }
35620
+ function answeredAsOf(body) {
35621
+ if (typeof body !== "object" || body === null) return void 0;
35622
+ const asOf = body["as_of"];
35623
+ return typeof asOf === "string" ? asOf : void 0;
35624
+ }
35625
+ function withNewline(text2) {
35626
+ return text2.endsWith("\n") ? text2 : `${text2}
35627
+ `;
35628
+ }
35629
+ function mergeKindBodies(bodies) {
35630
+ const [first, ...rest] = bodies;
35631
+ if (rest.length === 0) return first;
35632
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
35633
+ if (!isObject(first)) throw new Error("kind-explain: the server answered with no JSON object");
35634
+ const merged = {};
35635
+ for (const [key, value] of Object.entries(first)) {
35636
+ const lists = [value, ...rest.map((body) => isObject(body) ? body[key] : void 0)];
35637
+ merged[key] = lists.every(Array.isArray) ? lists.flatMap((list28) => list28) : value;
35638
+ }
35639
+ return merged;
35640
+ }
35641
+ async function runFileboxKindExplain(cfg) {
35642
+ if (cfg.ids.length === 0) throw new Error("kind-explain: at least one filebox id is required");
35643
+ const ctx = {
35644
+ command: "kind-explain",
35645
+ route: KIND_EXPLANATION_PATH,
35646
+ client: cfg.client
35647
+ };
35648
+ const batches = chunk(
35649
+ cfg.ids,
35650
+ cfg.history ? KIND_EXPLAIN_MAX_IDS_WITH_HISTORY : KIND_EXPLAIN_MAX_IDS
35651
+ );
35652
+ if (batches.length > 1) {
35653
+ extendForBatch(
35654
+ cfg.extendTimeout,
35655
+ batches.length,
35656
+ "split the ids over several runs (kind-explain has no --timeout)"
35657
+ );
35658
+ }
35659
+ const needsParse = batches.length > 1 || !cfg.asJson;
35660
+ const texts = [];
35661
+ const bodies = [];
35662
+ let asOf = cfg.asOf;
35663
+ for (const [index, ids] of batches.entries()) {
35664
+ const res = await getKindExplanation(
35665
+ cfg.client,
35666
+ { ids, asOf, history: cfg.history },
35667
+ { signal: cfg.signal }
35668
+ );
35669
+ let text2;
35670
+ try {
35671
+ text2 = await readKindAnswer(res, ctx);
35672
+ } catch (err) {
35673
+ if (index === 0 || !(err instanceof Error)) throw err;
35674
+ throw new Error(
35675
+ `request ${index + 1} of ${batches.length} failed, nothing was printed: ${err.message}`
35676
+ );
35677
+ }
35678
+ texts.push(text2);
35679
+ if (!needsParse) continue;
35680
+ const body = parseKindBody(text2, KIND_EXPLANATION_PATH);
35681
+ bodies.push(body);
35682
+ asOf ??= answeredAsOf(body);
35683
+ }
35684
+ if (cfg.asJson) {
35685
+ cfg.out.write(
35686
+ batches.length === 1 ? withNewline(texts[0] ?? "") : `${JSON.stringify(mergeKindBodies(bodies))}
35687
+ `
35688
+ );
35689
+ return;
35690
+ }
35691
+ const answer = decodeShape(mergeKindBodies(bodies));
35692
+ renderKindExplanation(cfg.out, answer, { showAsOf: cfg.asOf !== void 0 });
35693
+ }
35694
+ var KIND_LABELS = {
35695
+ manual_input: "Manual input",
35696
+ model: "Model",
35697
+ report: "Report",
35698
+ lookup: "Lookup",
35699
+ integration: "Integration"
35700
+ };
35701
+ var FORMAT_LABELS = { excel: "Excel", ppt: "PPT", pdf: "PDF" };
35702
+ var NO_SOURCE = "source not recorded";
35703
+ var SOURCE_WORDS = {
35704
+ backfill: "migration",
35705
+ creation_flow: "flow",
35706
+ user: "user",
35707
+ support: "support",
35708
+ default: "default",
35709
+ "(none)": NO_SOURCE
35710
+ };
35711
+ var SOURCE_PHRASES = {
35712
+ backfill: "set by the migration",
35713
+ creation_flow: "set by a flow (at creation, or a connection added later)",
35714
+ user: "set by a user",
35715
+ support: "set by support",
35716
+ default: "set by default"
35717
+ };
35718
+ var SETTERS = {
35719
+ backfill: "the migration",
35720
+ creation_flow: "a flow",
35721
+ user: "a user",
35722
+ support: "support",
35723
+ default: "default, at creation"
35724
+ };
35725
+ var UNSURE_PHRASES = {
35726
+ non_latest_version_link_only: "Datarails data is linked only on an older version, not on the latest one",
35727
+ mapper_with_data_source: "a mapper and a data source, but nothing pulled out of Datarails: an intake file or an integration",
35728
+ empty_doc_type: "the file has no extension, so its format says nothing",
35729
+ broken_dimension_tags_json: "the filebox's tags could not be read",
35730
+ archived: "the filebox is archived",
35731
+ format_signal_cannot_identify_a_deck: "neither its connections nor its extension say what it is"
35732
+ };
35733
+ var HISTORY_REASONS = {
35734
+ lookup_attached_to_table: "a table attached it as a lookup",
35735
+ // The old "Filebox type" setting and the report builder both record these.
35736
+ use_type_set_to_lookup: "a LUT/FX label set",
35737
+ use_type_relabelled_out_of_lookup: "a LUT/FX label removed",
35738
+ data_source_connected: "a data source was connected while it was empty",
35739
+ data_source_connected_to_a_report: "a data source was connected to a Report",
35740
+ cloud_drive_connected: "a synced cloud-drive file was connected while it was empty",
35741
+ cloud_drive_connected_to_a_report: "a synced cloud-drive file was connected to a Report",
35742
+ email_upload_turned_on: "email upload turned on",
35743
+ api_upload_turned_on: "API upload turned on",
35744
+ report_format_from_first_file: "its first file set the Report's format"
35745
+ };
35746
+ var SELF_SERVE_KINDS = /* @__PURE__ */ new Set(["manual_input", "model", "report"]);
35747
+ var UPLOAD_CHANNEL_WORDS = { email: "email", api: "API" };
35748
+ var EVIDENCE_LABELS = [
35749
+ ["mappers", "mapper"],
35750
+ ["lookup_tables", "lookup"],
35751
+ ["pulled_out", "pulled"],
35752
+ ["read_into", "read into"],
35753
+ ["data_query", "source"],
35754
+ ["cloud_drive", "drive"],
35755
+ ["budget_builder", "budget"],
35756
+ ["upload_channels", "upload"]
35757
+ ];
35758
+ var SEPARATOR = " | ";
35759
+ var LABEL_WIDTH = 10;
35760
+ var AGREES_COLUMN = 44;
35761
+ function kindLabel(token) {
35762
+ return KIND_LABELS[token] ?? token;
35763
+ }
35764
+ function kindsText(kinds, reportFormat) {
35765
+ if (kinds.length === 0) return "no type";
35766
+ return kinds.map(
35767
+ (kind) => kind === "report" && reportFormat !== null ? `Report (${FORMAT_LABELS[reportFormat] ?? reportFormat})` : kindLabel(kind)
35768
+ ).join(" + ");
35769
+ }
35770
+ function setBy(source) {
35771
+ if (source === null || source === void 0) return NO_SOURCE;
35772
+ return SOURCE_PHRASES[source] ?? `set by ${source}`;
35773
+ }
35774
+ function storedText(stored) {
35775
+ if (stored === null) return "no type yet";
35776
+ const [firstSource] = stored.sources;
35777
+ if (stored.sources.every((source) => source === firstSource)) {
35778
+ return `${kindsText(stored.kinds, stored.report_format)}, ${setBy(firstSource)}`;
35779
+ }
35780
+ return stored.kinds.map((kind, i) => `${kindsText([kind], stored.report_format)}, ${setBy(stored.sources[i])}`).join("; ");
35781
+ }
35782
+ function flowNote(answer) {
35783
+ if (answer.history !== void 0 || !answer.stored?.sources.includes("creation_flow")) return [];
35784
+ return ["--history shows a connection added later; a creation records nothing"];
35785
+ }
35786
+ function slotsText(slots) {
35787
+ if (slots === null || slots.primary === null) return "no type";
35788
+ const kinds = [slots.primary, slots.secondary].filter((k) => k !== null);
35789
+ return kindsText(kinds, slots.report_format);
35790
+ }
35791
+ function momentText(iso) {
35792
+ const ms = Date.parse(iso);
35793
+ return Number.isFinite(ms) ? formatUnixSeconds(Math.floor(ms / 1e3)) : iso;
35794
+ }
35795
+ function splitReasons(text2) {
35796
+ return text2 === null || text2 === void 0 || text2 === "" ? [] : text2.split(SEPARATOR);
35797
+ }
35798
+ function reasonText(text2) {
35799
+ const colon = text2.indexOf(": ");
35800
+ const token = colon > 0 ? text2.slice(0, colon) : "";
35801
+ const head = Object.hasOwn(KIND_LABELS, token) ? `${kindLabel(token)}${text2.slice(colon)}` : text2;
35802
+ return head.replace(
35803
+ /(does not fit beside )([a-z_+]+)$/,
35804
+ (_match, lead2, slots) => `${lead2}${slots.split("+").map(kindLabel).join(" + ")}`
35805
+ );
35806
+ }
35807
+ function lookUp(phrases, code) {
35808
+ return (Object.hasOwn(phrases, code) ? phrases[code] : void 0) ?? code;
35809
+ }
35810
+ function isByHand(source) {
35811
+ return source === "user" || source === "support";
35812
+ }
35813
+ function settersText(stored) {
35814
+ const [firstSource] = stored.sources;
35815
+ if (stored.sources.every((source) => source === firstSource)) {
35816
+ return firstSource === null || firstSource === void 0 ? `, ${NO_SOURCE}` : ` by ${lookUp(SETTERS, firstSource)}`;
35817
+ }
35818
+ const slots = stored.kinds.map((kind, i) => {
35819
+ const source = stored.sources[i];
35820
+ return source === null || source === void 0 ? `${kindLabel(kind)} (${NO_SOURCE})` : `${kindLabel(kind)} by ${lookUp(SETTERS, source)}`;
35821
+ });
35822
+ return `, ${slots.join(" and ")}`;
35823
+ }
35824
+ function uploadText(channels) {
35825
+ return `${channels.map((channel) => lookUp(UPLOAD_CHANNEL_WORDS, channel)).join(" and ")} upload`;
35826
+ }
35827
+ function integrationNotes(integration) {
35828
+ if (integration === null || integration.counts_as_integration || integration.quiet !== true) {
35829
+ return [];
35830
+ }
35831
+ return ["A data source is connected but quiet, so the content decided the type."];
35832
+ }
35833
+ function uploadKeptModelNotes(stored, derived, integration) {
35834
+ const channels = integration?.upload_channels ?? [];
35835
+ if (channels.length === 0 || !stored.kinds.includes("model") || !derived.includes("integration")) {
35836
+ return [];
35837
+ }
35838
+ const upload2 = uploadText(channels);
35839
+ const several = channels.length > 1;
35840
+ return [
35841
+ `${upload2.charAt(0).toUpperCase()}${upload2.slice(1)} ${several ? "are" : "is"} on: the migration puts Integration in place of a Model, but turning ${several ? "an" : "the"} upload on after it was typed keeps the Model, so the stored type stays ${kindsText(stored.kinds, stored.report_format)}.`
35842
+ ];
35843
+ }
35844
+ function kindMoveNeedsSupport(current, next) {
35845
+ const held = new Set(current);
35846
+ const wanted = new Set(next);
35847
+ const changed = [...held, ...wanted].filter((kind) => held.has(kind) !== wanted.has(kind));
35848
+ const integrationStays = held.has("integration") && wanted.has("integration");
35849
+ if (integrationStays && changed.every((kind) => kind === "report")) return false;
35850
+ return [...held, ...wanted].some((kind) => !SELF_SERVE_KINDS.has(kind));
35851
+ }
35852
+ function whoMayChange(stored, derived) {
35853
+ if (kindMoveNeedsSupport(stored, derived)) {
35854
+ return "To change it: the support seat only, in the filebox settings, as Lookup or Integration is held before or after the move.";
35855
+ }
35856
+ return stored.includes("integration") ? "To change it: the owner or the support seat, in the filebox settings: Integration stays, only Report or its format changes." : "To change it: the owner or the support seat, in the filebox settings.";
35857
+ }
35858
+ function meaningLines(answer) {
35859
+ const derived = kindsText(answer.derived.kinds, answer.derived.report_format);
35860
+ const notes = integrationNotes(answer.integration);
35861
+ if (answer.stored === null) {
35862
+ const why = answer.rollout ? "No type stored yet: the migration has not reached this filebox." : "No type stored: the organization is outside the rollout.";
35863
+ return [why, `The migration would make it ${derived}.`, ...notes];
35864
+ }
35865
+ const stored = `${kindsText(answer.stored.kinds, answer.stored.report_format)}${settersText(answer.stored)}`;
35866
+ const byHand = answer.stored.sources.some(isByHand);
35867
+ if (answer.agrees) {
35868
+ return [
35869
+ byHand ? `Set to ${stored}, and its connections say the same.` : `Typed ${stored}, and its connections still say so.`,
35870
+ ...notes
35871
+ ];
35872
+ }
35873
+ if (byHand) {
35874
+ return [
35875
+ `Set on purpose to ${stored}; its connections alone would make it ${derived}.`,
35876
+ "A type set by hand stays: the migration never overwrites it."
35877
+ ];
35878
+ }
35879
+ return [
35880
+ `Typed ${stored}, but its connections now make it ${derived}.`,
35881
+ "The connections changed after it was typed, and a type does not follow them by itself.",
35882
+ ...uploadKeptModelNotes(answer.stored, answer.derived.kinds, answer.integration),
35883
+ ...timeNotes(answer.integration),
35884
+ whoMayChange(answer.stored.kinds, answer.derived.kinds),
35885
+ ...notes
35886
+ ];
35887
+ }
35888
+ function timeNotes(integration) {
35889
+ if (integration === null || integration.quiet === null) return [];
35890
+ if ((integration.upload_channels ?? []).length > 0) return [];
35891
+ return [
35892
+ `Time alone can change the answer too: a connected integration is quiet after ${integration.window_days} days with no file, sync attempt or schedule.`,
35893
+ "To judge it as the migration did: --as-of <backfill day>."
35894
+ ];
35895
+ }
35896
+ function writeField(out, label, values, indent = " ") {
35897
+ if (values.length === 0) return;
35898
+ const head = label.length >= LABEL_WIDTH ? `${label} ` : label.padEnd(LABEL_WIDTH);
35899
+ const pad = " ".repeat(head.length);
35900
+ values.forEach((value, i) => out.write(`${indent}${i === 0 ? head : pad}${value}
35901
+ `));
35902
+ }
35903
+ function labelledLines(rows) {
35904
+ const width = Math.max(LABEL_WIDTH, ...rows.map(([label]) => label.length + 2));
35905
+ return rows.flatMap(
35906
+ ([label, items]) => items.map((item, i) => `${(i === 0 ? label : "").padEnd(width)}${item}`)
35907
+ );
35908
+ }
35909
+ function evidenceLines(evidence) {
35910
+ const rows = EVIDENCE_LABELS.map(
35911
+ ([field, label]) => [label, splitReasons(evidence?.[field])]
35912
+ ).filter(([, items]) => items.length > 0);
35913
+ return rows.length === 0 ? ["none"] : labelledLines(rows);
35914
+ }
35915
+ function integrationLines(integration) {
35916
+ const channels = integration.upload_channels ?? [];
35917
+ const head = [
35918
+ ...integration.quiet === null ? [] : [`${integration.quiet ? "quiet" : "live"} over the last ${integration.window_days} days`],
35919
+ ...channels.length > 0 ? [`${uploadText(channels)} on, no quiet rule`] : [],
35920
+ integration.counts_as_integration ? "counts as Integration" : "does not count as Integration"
35921
+ ].join(" \xB7 ");
35922
+ if (integration.quiet === null) return [head, "no data query or synced cloud drive connected"];
35923
+ return [
35924
+ head,
35925
+ ...labelledLines([
35926
+ [
35927
+ "last file in window",
35928
+ [integration.last_file_in_window ? momentText(integration.last_file_in_window) : "none"]
35929
+ ],
35930
+ [
35931
+ "last sync attempt",
35932
+ [integration.last_sync_attempt ? momentText(integration.last_sync_attempt) : "none"]
35933
+ ],
35934
+ ["schedule", [integration.schedule ?? "none"]],
35935
+ ["auto sync", [integration.auto_sync ? "on" : "off"]]
35936
+ ])
35937
+ ];
35938
+ }
35939
+ function writeHistory(out, history) {
35940
+ if (history.length === 0) {
35941
+ writeField(out, "history", ["none recorded (or the audit server could not be read)"]);
35942
+ return;
35943
+ }
35944
+ out.write(" history\n");
35945
+ const indent = " ";
35946
+ const rows = [["WHEN", "BY", "OPERATOR", "BEFORE \u2192 AFTER", "REASON"]];
35947
+ for (const entry of history) {
35948
+ rows.push([
35949
+ entry.at ? momentText(entry.at) : "-",
35950
+ entry.by ?? "-",
35951
+ entry.operator ?? "-",
35952
+ `${slotsText(entry.before)} \u2192 ${slotsText(entry.after)}`,
35953
+ entry.reason ? lookUp(HISTORY_REASONS, entry.reason) : "retyped in the filebox settings"
35954
+ ]);
35955
+ }
35956
+ writeRows(
35957
+ out,
35958
+ rows.map(([first, ...others]) => [`${indent}${first ?? ""}`, ...others])
35959
+ );
35960
+ }
35961
+ function writeFilebox(out, answer) {
35962
+ const rollout = answer.rollout ? "" : " (outside the rollout)";
35963
+ out.write(
35964
+ `Filebox ${answer.id} "${answer.name}" \xB7 organization ${answer.organization_id}${rollout}
35965
+ `
35966
+ );
35967
+ writeField(out, "meaning", meaningLines(answer));
35968
+ writeField(out, "stored", [storedText(answer.stored), ...flowNote(answer)]);
35969
+ const derived = kindsText(answer.derived.kinds, answer.derived.report_format);
35970
+ const mark = answer.agrees === null ? "" : answer.agrees ? "\u2713 agrees" : "\u2717 disagrees";
35971
+ writeField(out, "rules", [
35972
+ mark === "" ? derived : `${derived.padEnd(Math.max(AGREES_COLUMN, derived.length + 2))}${mark}`
35973
+ ]);
35974
+ writeField(out, "rule", [answer.derived.rule]);
35975
+ if (answer.derived.unknown_reason) {
35976
+ writeField(out, "unsure", [lookUp(UNSURE_PHRASES, answer.derived.unknown_reason)]);
35977
+ }
35978
+ writeField(
35979
+ out,
35980
+ "why",
35981
+ [answer.why.primary, answer.why.secondary].filter((w) => !!w).map(reasonText)
35982
+ );
35983
+ writeField(out, "dropped", splitReasons(answer.why.dropped).map(reasonText));
35984
+ writeField(out, "why not", splitReasons(answer.why.not).map(reasonText));
35985
+ writeField(out, "evidence", evidenceLines(answer.evidence));
35986
+ if (answer.integration) writeField(out, "integration", integrationLines(answer.integration));
35987
+ if (answer.history !== void 0) writeHistory(out, answer.history);
35988
+ }
35989
+ var NOT_FOUND_HINT = "Not found also holds a filebox of another organization: only that organization's support seat gets its answer.\n";
35990
+ function renderKindExplanation(out, answer, opts) {
35991
+ let first = true;
35992
+ if (opts.showAsOf) {
35993
+ out.write(`As of ${answer.as_of}
35994
+ `);
35995
+ first = false;
35996
+ }
35997
+ for (const filebox of answer.fileboxes) {
35998
+ if (!first) out.write("\n");
35999
+ writeFilebox(out, filebox);
36000
+ first = false;
36001
+ }
36002
+ if (answer.not_found.length > 0) {
36003
+ if (!first) out.write("\n");
36004
+ for (const id of answer.not_found) out.write(`Not found: filebox ${id}
36005
+ `);
36006
+ out.write(NOT_FOUND_HINT);
36007
+ }
36008
+ }
36009
+ async function runFileboxKindSummary(cfg) {
36010
+ const ctx = {
36011
+ command: "kind-summary",
36012
+ route: KIND_SUMMARY_PATH,
36013
+ client: cfg.client
36014
+ };
36015
+ const res = await getKindSummary(cfg.client, { signal: cfg.signal });
36016
+ const text2 = await readKindAnswer(res, ctx);
36017
+ if (cfg.asJson) {
36018
+ cfg.out.write(withNewline(text2));
36019
+ return;
36020
+ }
36021
+ renderKindSummary(
36022
+ cfg.out,
36023
+ decodeShape(parseKindBody(text2, KIND_SUMMARY_PATH))
36024
+ );
36025
+ }
36026
+ function countText(n) {
36027
+ return n.toLocaleString("en-US");
36028
+ }
36029
+ function countsLine(entries) {
36030
+ return [...entries].sort(([a, x], [b, y]) => y - x || a.localeCompare(b)).map(([label, n]) => `${label} ${countText(n)}`).join(" \xB7 ");
36031
+ }
36032
+ var UNTYPED_KEY = "(untyped)";
36033
+ function renderKindSummary(out, summary) {
36034
+ const rollout = summary.rollout ? "" : " (outside the rollout)";
36035
+ out.write(`Organization ${summary.organization_id}${rollout}
36036
+ `);
36037
+ const header = ["fileboxes", "typed", "untyped"];
36038
+ const rows = ["live", "archived"].map(
36039
+ (state) => [
36040
+ state,
36041
+ [summary[state].fileboxes, summary[state].typed, summary[state].untyped].map(countText)
36042
+ ]
36043
+ );
36044
+ const widths = header.map(
36045
+ (h, i) => Math.max(h.length, ...rows.map(([, v]) => (v[i] ?? "").length))
36046
+ );
36047
+ const line = (cells) => cells.map((cell, i) => cell.padStart(widths[i] ?? 0)).join(" ");
36048
+ out.write(`${" ".repeat(2 + LABEL_WIDTH)}${line(header)}
36049
+ `);
36050
+ for (const [state, values] of rows) out.write(` ${state.padEnd(LABEL_WIDTH)}${line(values)}
36051
+ `);
36052
+ const sources = Object.entries(summary.sources).map(
36053
+ ([source, n]) => [lookUp(SOURCE_WORDS, source), n]
36054
+ );
36055
+ writeField(out, "set by", [sources.length === 0 ? "none" : countsLine(sources)]);
36056
+ const kinds = Object.entries(summary.kinds);
36057
+ const combinations = (pairs) => countsLine(
36058
+ kinds.filter(([combination]) => combination.includes("+") === pairs).map(([combination, n]) => [kindsText(combination.split("+"), null), n])
36059
+ );
36060
+ const typeLines = [combinations(false), combinations(true)].filter((l) => l !== "");
36061
+ writeField(out, "types", typeLines.length === 0 ? ["none"] : typeLines);
36062
+ if (summary.upload_channel_kinds === void 0) return;
36063
+ const uploads = Object.entries(summary.upload_channel_kinds).map(
36064
+ ([combination, n]) => [
36065
+ combination === UNTYPED_KEY ? "untyped" : kindsText(combination.split("+"), null),
36066
+ n
36067
+ ]
36068
+ );
36069
+ writeField(
36070
+ out,
36071
+ "uploads",
36072
+ uploads.length === 0 ? ["no live filebox has email or API upload on"] : ["live fileboxes with email / API upload on, by stored type", countsLine(uploads)]
36073
+ );
36074
+ }
36075
+ var SUPPORT_SEAT_HELP = [
36076
+ "",
36077
+ "Support seat only. The server answers the support seat of the organization",
36078
+ "asked about and refuses any other seat (HTTP 405), even a support person",
36079
+ "working in a customer user's seat. Log in as that seat, then run as it:",
36080
+ " dr login-as <support seat email> --server <S>"
36081
+ ];
36082
+ function registerFileboxKindCommands(fb) {
36083
+ const explainCmd = fb.command("kind-explain").description(
36084
+ "Why fileboxes have their type: the stored type and who set it, what the migration's rules make of it, whether the two agree, the rule, why, and the evidence (read-only, support seat only)"
36085
+ ).argument("<filebox_ids...>", 'filebox ids (positive integers; "12,34" also accepted)').option(
36086
+ "--history",
36087
+ `Also list the recorded type changes (${KIND_EXPLAIN_MAX_IDS_WITH_HISTORY} ids per request)`,
36088
+ false
36089
+ ).option(
36090
+ "--as-of <date>",
36091
+ "Judge a quiet integration as of this day (YYYY-MM-DD, its end UTC); every other signal is read as it is now"
36092
+ ).option("--json", "Print the server's answer instead of the readable form (see below)", false).addHelpText(
36093
+ "after",
36094
+ [
36095
+ ...SUPPORT_SEAT_HELP,
36096
+ " dr filebox kind-explain 48213 48999 --server <S> --account <support seat email>",
36097
+ "A filebox of another organization, a folder, or an id that does not exist is",
36098
+ 'listed as "Not found". It changes nothing and answers whatever the',
36099
+ "organization's feature flag; each filebox says whether it is in the rollout.",
36100
+ "",
36101
+ `More than ${KIND_EXPLAIN_MAX_IDS} ids (${KIND_EXPLAIN_MAX_IDS_WITH_HISTORY} with --history) are sent as several requests, one`,
36102
+ "after another, the later ones pinned to the first answer's as_of. --json",
36103
+ "prints a single request's body unchanged; for several it prints one body in",
36104
+ "the server's shape: fileboxes and not_found concatenated in request order,",
36105
+ "as_of from the first. Either way --json keeps the HTML entities the server",
36106
+ 'sends (a name reads "P&amp;L"): unlike `filebox list --json`, nothing is decoded.'
36107
+ ].join("\n")
36108
+ ).action(
36109
+ async (idArgs, opts) => {
36110
+ const ids = parseKindExplainIds(idArgs);
36111
+ const asOf = parseKindAsOf(opts.asOf);
36112
+ const setup = await setupCall(opts.server, opts.account);
36113
+ try {
36114
+ await runFileboxKindExplain({
36115
+ client: setup.client,
36116
+ ids,
36117
+ asOf,
36118
+ history: opts.history === true,
36119
+ asJson: opts.json === true,
36120
+ signal: setup.signal,
36121
+ out: process.stdout,
36122
+ extendTimeout: setup.extendTimeout
36123
+ });
36124
+ } finally {
36125
+ setup.cleanup();
36126
+ }
36127
+ }
36128
+ );
36129
+ addServerFlag(explainCmd);
36130
+ addAccountFlag(explainCmd);
36131
+ const summaryCmd = fb.command("kind-summary").description(
36132
+ "How far the seat's organization is typed: live and archived fileboxes, typed and untyped, who set the types, and every stored combination (read-only, support seat only)"
36133
+ ).option("--json", "Print the server's answer unchanged", false).addHelpText(
36134
+ "after",
36135
+ [
36136
+ ...SUPPORT_SEAT_HELP,
36137
+ " dr filebox kind-summary --server <S> --account <support seat email>",
36138
+ 'It answers for the organization of that seat. "set by" and "types" count',
36139
+ 'live and archived fileboxes together; in "set by", "flow" is a flow that',
36140
+ "typed the filebox at creation or when a connection was added later (a table",
36141
+ "attaching it as a lookup, a LUT/FX label, a data source or synced drive",
36142
+ 'connected, email / API upload turned on). "uploads" counts live fileboxes with',
36143
+ "email or API upload on by their stored type: the rules make each an",
36144
+ "Integration, dropping a Model, but turning an upload on keeps a Model, so a",
36145
+ "Model counted there is expected drift (kind-explain says why)."
36146
+ ].join("\n")
36147
+ ).action(async (opts) => {
36148
+ const setup = await setupCall(opts.server, opts.account);
36149
+ try {
36150
+ await runFileboxKindSummary({
36151
+ client: setup.client,
36152
+ asJson: opts.json === true,
36153
+ signal: setup.signal,
36154
+ out: process.stdout
36155
+ });
36156
+ } finally {
36157
+ setup.cleanup();
36158
+ }
36159
+ });
36160
+ addServerFlag(summaryCmd);
36161
+ addAccountFlag(summaryCmd);
36162
+ }
36163
+
35465
36164
  // src/commands/_typedError.ts
35466
36165
  function isTypedError(err) {
35467
36166
  return !(err instanceof Error) || err.constructor !== Error || "code" in err;
@@ -36148,11 +36847,11 @@ function classifyDataQuerySyncProgress(periods, baseline, versions, dataQueryRaw
36148
36847
  }
36149
36848
  return out;
36150
36849
  }
36151
- async function captureDataQuerySyncBaseline(client, fileboxId, periods, signal) {
36850
+ async function captureDataQuerySyncBaseline(client, fileboxId, periods, signal, preRead) {
36152
36851
  try {
36153
36852
  const [versions, dataQuery] = await Promise.all([
36154
36853
  listVersions(client, fileboxId, {}, { signal }),
36155
- getDataQueryRaw(client, fileboxId, { signal })
36854
+ preRead !== void 0 ? Promise.resolve(preRead.raw) : getDataQueryRaw(client, fileboxId, { signal })
36156
36855
  ]);
36157
36856
  return buildDataQuerySyncBaseline(periods, versions, dataQuery);
36158
36857
  } catch (err) {
@@ -36252,7 +36951,8 @@ function renderSummary2(cfg, progress, outcome, waitedSec) {
36252
36951
  })),
36253
36952
  unmatched_new_versions: progress.unmatchedNewVersions,
36254
36953
  error: progress.error ?? null,
36255
- waited_seconds: waitedSec
36954
+ waited_seconds: waitedSec,
36955
+ warnings: cfg.warnings ?? []
36256
36956
  });
36257
36957
  return;
36258
36958
  }
@@ -36310,6 +37010,63 @@ function failureMessage(fileboxId, progress, outcome, waitedSec) {
36310
37010
  return `data-query sync on filebox ${fileboxId} did not finish within ${waitedSec}s \u2014 ${pending.length} period(s) unfinished: ${pendingList}. Increase --timeout, or re-check with 'dr filebox versions ${fileboxId}'; do not blindly re-trigger.`;
36311
37011
  }
36312
37012
 
37013
+ // src/commands/_aggregatorBalance.ts
37014
+ var AGGREGATOR_TYPE_NAMES = /* @__PURE__ */ new Set([
37015
+ "plaid",
37016
+ "quiltt",
37017
+ "saltedge",
37018
+ "flinks"
37019
+ ]);
37020
+ function asRecord2(v) {
37021
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
37022
+ }
37023
+ function parsedRecord(raw) {
37024
+ if (typeof raw === "string") {
37025
+ try {
37026
+ return asRecord2(JSON.parse(raw));
37027
+ } catch {
37028
+ return void 0;
37029
+ }
37030
+ }
37031
+ return asRecord2(raw);
37032
+ }
37033
+ async function detectAggregatorBalanceQuery(client, fileboxId, signal) {
37034
+ let dataQueryRaw;
37035
+ try {
37036
+ dataQueryRaw = await getDataQueryRaw(client, fileboxId, { signal });
37037
+ } catch {
37038
+ if (signal.aborted) throw signal.reason ?? new Error("aborted");
37039
+ return void 0;
37040
+ }
37041
+ const dataQuery = asRecord2(dataQueryRaw);
37042
+ const config = parsedRecord(dataQuery?.["config"]);
37043
+ const objectType = config?.["objectType"] ?? parsedRecord(config?.["query"])?.["objectType"];
37044
+ const rawConnection = dataQuery?.["connection"];
37045
+ const connectionId = typeof rawConnection === "number" || typeof rawConnection === "string" ? Number(rawConnection) : NaN;
37046
+ if (typeof objectType !== "string" || objectType.toLowerCase() !== "balances" || !Number.isInteger(connectionId) || connectionId <= 0) {
37047
+ return { dataQueryRaw };
37048
+ }
37049
+ let type;
37050
+ try {
37051
+ const integration = await getById2(client, connectionId, { signal });
37052
+ type = asRecord2(integration.type);
37053
+ } catch {
37054
+ if (signal.aborted) throw signal.reason ?? new Error("aborted");
37055
+ return { dataQueryRaw };
37056
+ }
37057
+ const name = type?.["name"];
37058
+ if (typeof name !== "string" || !AGGREGATOR_TYPE_NAMES.has(name.toLowerCase())) {
37059
+ return { dataQueryRaw };
37060
+ }
37061
+ const display = type?.["display_name"];
37062
+ const aggregator = typeof display === "string" && display.trim() !== "" ? display.trim() : name;
37063
+ return { dataQueryRaw, aggregator };
37064
+ }
37065
+ function aggregatorBalanceWarning(fileboxId, aggregator, periodCount) {
37066
+ const what = periodCount > 1 ? `these ${periodCount} periods will all get today's balance (identical versions under different dates)` : "that period will get today's balance, labelled as if it were that period's";
37067
+ return `filebox ${fileboxId} syncs balances from ${aggregator}, an aggregator: it returns today's balance whatever --date asks for, so ${what}. Backfill only transactions; for balances, sync the current period only and let the nightly schedule build the history. Multi-period balance backfill works only on a direct bank API connection.`;
37068
+ }
37069
+
36313
37070
  // src/commands/filebox.ts
36314
37071
  async function fileToBlob(filePath) {
36315
37072
  const buf = await fsPromises.readFile(filePath);
@@ -36623,7 +37380,7 @@ ${JSON.stringify(result)}
36623
37380
  `
36624
37381
  );
36625
37382
  }
36626
- function asRecord2(v) {
37383
+ function asRecord3(v) {
36627
37384
  return v !== null && typeof v === "object" ? v : void 0;
36628
37385
  }
36629
37386
  function numOf(r, k) {
@@ -36633,9 +37390,9 @@ function strOf(r, k) {
36633
37390
  return r !== void 0 && typeof r[k] === "string" ? r[k] : void 0;
36634
37391
  }
36635
37392
  function extractUploadedVersionMeta(result) {
36636
- const r = asRecord2(result);
36637
- const dv = asRecord2(r?.["document_version"]);
36638
- const docObj = asRecord2(r?.["document"]);
37393
+ const r = asRecord3(result);
37394
+ const dv = asRecord3(r?.["document_version"]);
37395
+ const docObj = asRecord3(r?.["document"]);
36639
37396
  return {
36640
37397
  id: numOf(r, "id") ?? numOf(dv, "id") ?? numOf(docObj, "latest_version_id") ?? 0,
36641
37398
  version: numOf(r, "version") ?? numOf(dv, "version") ?? 0,
@@ -36794,7 +37551,7 @@ async function runFileboxUploadVersion(cfg) {
36794
37551
  ...cycleTagVerdictJSON(confirmation)
36795
37552
  });
36796
37553
  } else if (cfg.label !== void 0) {
36797
- cfg.out.write(JSON.stringify({ ...asRecord2(result), ...labelJSON }) + "\n");
37554
+ cfg.out.write(JSON.stringify({ ...asRecord3(result), ...labelJSON }) + "\n");
36798
37555
  } else {
36799
37556
  cfg.out.write(JSON.stringify(result) + "\n");
36800
37557
  }
@@ -38600,6 +39357,7 @@ async function runFileboxDataQuerySync(cfg) {
38600
39357
  const dateTargets = [];
38601
39358
  const periodLabels = [];
38602
39359
  const waitPeriods = [];
39360
+ const pastOrFuturePeriods = /* @__PURE__ */ new Set();
38603
39361
  if (parsedDates.length > 0) {
38604
39362
  const details = await getDetails(cfg.client, cfg.id, {
38605
39363
  signal: cfg.signal
@@ -38620,6 +39378,11 @@ async function runFileboxDataQuerySync(cfg) {
38620
39378
  );
38621
39379
  }
38622
39380
  }
39381
+ const now = cfg.today ?? /* @__PURE__ */ new Date();
39382
+ const todays = [
39383
+ { year: now.getFullYear(), month0: now.getMonth(), day: now.getDate() },
39384
+ { year: now.getUTCFullYear(), month0: now.getUTCMonth(), day: now.getUTCDate() }
39385
+ ].map((t) => dateTags.map((tag) => resolveDateForSubType(tagSubType(tag), t).value));
38623
39386
  for (const parsed2 of parsedDates) {
38624
39387
  const resolved = dateTags.map((tag) => {
38625
39388
  const { value, label: label2 } = resolveDateForSubType(tagSubType(tag), parsed2);
@@ -38628,6 +39391,8 @@ async function runFileboxDataQuerySync(cfg) {
38628
39391
  dateTargets.push(
38629
39392
  JSON.stringify(resolved.map(({ id, name, value }) => ({ id, name, value })))
38630
39393
  );
39394
+ const isToday = todays.some((t) => resolved.every((r, i) => r.value === t[i]));
39395
+ if (!isToday) pastOrFuturePeriods.add(resolved.map((r) => r.value).join("|"));
38631
39396
  const label = resolved.map((r) => r.label).join(" + ");
38632
39397
  periodLabels.push(label);
38633
39398
  waitPeriods.push({
@@ -38642,7 +39407,31 @@ async function runFileboxDataQuerySync(cfg) {
38642
39407
  if (waitPeriods.length === 0) {
38643
39408
  waitPeriods.push({ label: "latest sync", tags: [] });
38644
39409
  }
38645
- const baseline = cfg.wait === void 0 ? void 0 : await captureDataQuerySyncBaseline(cfg.client, cfg.id, waitPeriods, cfg.signal);
39410
+ const warnings = [];
39411
+ let preReadDataQuery;
39412
+ if (pastOrFuturePeriods.size > 0) {
39413
+ const detected = await detectAggregatorBalanceQuery(cfg.client, cfg.id, cfg.signal);
39414
+ if (detected !== void 0) {
39415
+ preReadDataQuery = { raw: detected.dataQueryRaw };
39416
+ if (detected.aggregator !== void 0) {
39417
+ warnings.push(
39418
+ aggregatorBalanceWarning(cfg.id, detected.aggregator, pastOrFuturePeriods.size)
39419
+ );
39420
+ }
39421
+ }
39422
+ }
39423
+ const errOut = cfg.err ?? cfg.wait?.err ?? process.stderr;
39424
+ for (const w of warnings) {
39425
+ errOut.write(`warning: ${w}
39426
+ `);
39427
+ }
39428
+ const baseline = cfg.wait === void 0 ? void 0 : await captureDataQuerySyncBaseline(
39429
+ cfg.client,
39430
+ cfg.id,
39431
+ waitPeriods,
39432
+ cfg.signal,
39433
+ preReadDataQuery
39434
+ );
38646
39435
  const multipleTags = [...rawTags, ...dateTargets];
38647
39436
  const body = {};
38648
39437
  if (multipleTags.length > 0) {
@@ -38685,6 +39474,7 @@ async function runFileboxDataQuerySync(cfg) {
38685
39474
  out: cfg.out,
38686
39475
  errOut: cfg.wait.err ?? process.stderr,
38687
39476
  signal: cfg.signal,
39477
+ warnings,
38688
39478
  pollIntervalMs: cfg.wait.pollIntervalMs,
38689
39479
  maxPollIntervalMs: cfg.wait.maxPollIntervalMs,
38690
39480
  sleep: cfg.wait.sleep,
@@ -38792,9 +39582,9 @@ async function runFileboxDownload(cfg) {
38792
39582
  const fh = await fsPromises.open(target, "w");
38793
39583
  let total = 0;
38794
39584
  try {
38795
- for (const chunk of head) {
38796
- await fh.write(chunk);
38797
- total += chunk.length;
39585
+ for (const chunk2 of head) {
39586
+ await fh.write(chunk2);
39587
+ total += chunk2.length;
38798
39588
  }
38799
39589
  if (!streamDone) {
38800
39590
  while (true) {
@@ -39978,7 +40768,7 @@ function registerFileboxCommand(root) {
39978
40768
  addServerFlag(dqClearCmd);
39979
40769
  addAccountFlag(dqClearCmd);
39980
40770
  const dqSyncCmd = dataQuery.command("sync").description(
39981
- `Manually trigger a sync of the filebox's data-query (async on server). --date YYYY-MM-DD (repeatable) scopes the sync to that date, truncated to whatever granularity the filebox's own date tag uses \u2014 e.g. --date 2026-07-01 syncs "July 2026" on a monthly filebox, "Q3 2026" on a quarterly one, or the exact day/ISO week on a daily/weekly one. Repeat to backfill several periods in one call.`
40771
+ `Manually trigger a sync of the filebox's data-query (async on server). --date YYYY-MM-DD (repeatable) scopes the sync to that date, truncated to whatever granularity the filebox's own date tag uses \u2014 e.g. --date 2026-07-01 syncs "July 2026" on a monthly filebox, "Q3 2026" on a quarterly one, or the exact day/ISO week on a daily/weekly one. Repeat to backfill several periods in one call. Balances from an aggregator (Plaid, Quiltt, Salt Edge, Flinks) are always today's balance whatever --date says, so backfilling them writes identical versions under different dates (the CLI warns): backfill only transactions, sync balances for the current period and let the nightly schedule build the history. Multi-period balance backfill works only on a direct bank API connection.`
39982
40772
  ).argument("<filebox_id>", "filebox id (positive integer)").option(
39983
40773
  "--date <YYYY-MM-DD>",
39984
40774
  "sync whatever date-tag period this date falls in, e.g. 2026-07-01. Repeatable.",
@@ -40025,6 +40815,7 @@ function registerFileboxCommand(root) {
40025
40815
  addServerFlag(dqSyncCmd);
40026
40816
  addAccountFlag(dqSyncCmd);
40027
40817
  registerFileboxAdminCommands(fb, setupCall, addServerFlag, addAccountFlag);
40818
+ registerFileboxKindCommands(fb);
40028
40819
  registerFileboxRescanVersionCommand(fb);
40029
40820
  }
40030
40821
 
@@ -45680,52 +46471,6 @@ async function getAuditExportStatus(client, params, options) {
45680
46471
  return out ?? { job_id: params.jobId, status: "PENDING" };
45681
46472
  }
45682
46473
 
45683
- // src/commands/_reconciliationDate.ts
45684
- function validateDateString(orig, flagName) {
45685
- if (!/^\d{4}-\d{2}-\d{2}$/.test(orig)) {
45686
- throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD)`);
45687
- }
45688
- const parts = orig.split("-").map(Number);
45689
- const [year, month1, day] = parts;
45690
- const probe = new Date(Date.UTC(year, month1 - 1, day));
45691
- if (probe.getUTCFullYear() !== year || probe.getUTCMonth() + 1 !== month1 || probe.getUTCDate() !== day) {
45692
- throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD)`);
45693
- }
45694
- return { year, month1, day };
45695
- }
45696
- function parseDateFlag(orig, flagName) {
45697
- let normalized = orig;
45698
- if (/^\d{4}-\d{2}$/.test(orig)) {
45699
- normalized = `${orig}-01`;
45700
- }
45701
- let year;
45702
- let month1;
45703
- try {
45704
- ({ year, month1 } = validateDateString(normalized, flagName));
45705
- } catch {
45706
- throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD or YYYY-MM)`);
45707
- }
45708
- return Math.floor(Date.UTC(year, month1, 0) / 1e3);
45709
- }
45710
- function parseDateFlagEpoch(orig, flagName) {
45711
- let normalized = orig;
45712
- if (/^\d{4}-\d{2}$/.test(orig)) {
45713
- normalized = `${orig}-01`;
45714
- }
45715
- let year;
45716
- let month1;
45717
- try {
45718
- ({ year, month1 } = validateDateString(normalized, flagName));
45719
- } catch {
45720
- throw new Error(`invalid ${flagName} "${orig}" (expected YYYY-MM-DD or YYYY-MM)`);
45721
- }
45722
- return Math.floor(Date.UTC(year, month1 - 1, 1, 12) / 1e3);
45723
- }
45724
- function parseDateFlagDay(orig, flagName) {
45725
- const { year, month1, day } = validateDateString(orig, flagName);
45726
- return Math.floor(Date.UTC(year, month1 - 1, day) / 1e3);
45727
- }
45728
-
45729
46474
  // src/commands/_reconciliationSettingsFile.ts
45730
46475
  var ALLOWED_FREQUENCIES = /* @__PURE__ */ new Set(["Monthly", "Quarterly", "Yearly", "Exclude"]);
45731
46476
  var REQUIRED_FIELDS = [
@@ -47122,17 +47867,17 @@ async function runReconciliationAccountsUpdateSettings(cfg) {
47122
47867
  execute: async () => {
47123
47868
  extendForBatch(cfg.extendTimeout, chunks.length);
47124
47869
  let applied = 0;
47125
- for (const chunk of chunks) {
47870
+ for (const chunk2 of chunks) {
47126
47871
  try {
47127
- await updateAccountSettings(cfg.client, chunk, { signal: cfg.signal });
47872
+ await updateAccountSettings(cfg.client, chunk2, { signal: cfg.signal });
47128
47873
  } catch (err) {
47129
47874
  const msg = err instanceof Error ? err.message : String(err);
47130
47875
  throw new Error(
47131
- `PATCH failed after ${applied} of ${changed.length} rows were written (rows ${applied + 1}\u2013${applied + chunk.length} may be partially applied): ${msg}
47876
+ `PATCH failed after ${applied} of ${changed.length} rows were written (rows ${applied + 1}\u2013${applied + chunk2.length} may be partially applied): ${msg}
47132
47877
  Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
47133
47878
  );
47134
47879
  }
47135
- applied += chunk.length;
47880
+ applied += chunk2.length;
47136
47881
  }
47137
47882
  cfg.out.write(
47138
47883
  `Updated ${applied} account setting${plural(applied)} (${unchanged} unchanged).
@@ -47725,10 +48470,10 @@ async function writeMemberChunks(ctx, planned, items, chunkSize) {
47725
48470
  extendForBatch(ctx.extendTimeout, requests * 2);
47726
48471
  let applied = 0;
47727
48472
  for (let i = 0; i < items.length; i += chunkSize) {
47728
- const chunk = items.slice(i, i + chunkSize);
48473
+ const chunk2 = items.slice(i, i + chunkSize);
47729
48474
  let drift;
47730
48475
  try {
47731
- drift = await chunkDrift(ctx, planned, chunk);
48476
+ drift = await chunkDrift(ctx, planned, chunk2);
47732
48477
  } catch (err) {
47733
48478
  const msg = err instanceof Error ? err.message : String(err);
47734
48479
  throw new Error(
@@ -47744,23 +48489,23 @@ The tenant changed since the plan. Re-run the same command without --confirm-tok
47744
48489
  );
47745
48490
  }
47746
48491
  try {
47747
- await updateAccountMembers(ctx.client, chunk, { signal: ctx.signal });
48492
+ await updateAccountMembers(ctx.client, chunk2, { signal: ctx.signal });
47748
48493
  } catch (err) {
47749
48494
  const msg = err instanceof Error ? err.message : String(err);
47750
48495
  throw new Error(
47751
- `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}
48496
+ `PATCH failed after ${applied} of ${items.length} account${plural(items.length)} were written (accounts ${i + 1}\u2013${i + chunk2.length} may be partially applied): ${msg}
47752
48497
  Re-run the same command without --confirm-token: rows that landed plan as unchanged, so the new plan covers only the rest.`
47753
48498
  );
47754
48499
  }
47755
- applied += chunk.length;
48500
+ applied += chunk2.length;
47756
48501
  }
47757
48502
  return applied;
47758
48503
  }
47759
- async function chunkDrift(cfg, planned, chunk) {
48504
+ async function chunkDrift(cfg, planned, chunk2) {
47760
48505
  const now = await getAccounts(cfg.client, { date: cfg.date }, { signal: cfg.signal });
47761
48506
  const nowById = new Map(now.map((a) => [a.account_id, a]));
47762
48507
  const drift = [];
47763
- for (const { account_id: id } of chunk) {
48508
+ for (const { account_id: id } of chunk2) {
47764
48509
  const before = planned.get(id);
47765
48510
  const after = nowById.get(id);
47766
48511
  const label = `${before.account_name} (${id})`;
@@ -58742,8 +59487,8 @@ function captureVersion() {
58742
59487
  return new Promise((resolve3) => {
58743
59488
  const child2 = spawn2("dr", ["--version"], { shell: true });
58744
59489
  let out = "";
58745
- child2.stdout.on("data", (chunk) => {
58746
- out += chunk.toString();
59490
+ child2.stdout.on("data", (chunk2) => {
59491
+ out += chunk2.toString();
58747
59492
  });
58748
59493
  child2.on("close", (code) => {
58749
59494
  if (code !== 0) {
@@ -62975,7 +63720,7 @@ async function pollExtractionStatus(client, contractFileIds, options = {}) {
62975
63720
  if (outstanding.length === 0) break;
62976
63721
  if (now() > deadline || inner.signal.aborted) break;
62977
63722
  for (let i = 0; i < outstanding.length; i += MAX_BULK_STATUS_IDS) {
62978
- const chunk = outstanding.slice(i, i + MAX_BULK_STATUS_IDS);
63723
+ const chunk2 = outstanding.slice(i, i + MAX_BULK_STATUS_IDS);
62979
63724
  const tickAc = new AbortController();
62980
63725
  const onInnerAbort = () => tickAc.abort(inner.signal.reason);
62981
63726
  if (inner.signal.aborted) {
@@ -62984,13 +63729,13 @@ async function pollExtractionStatus(client, contractFileIds, options = {}) {
62984
63729
  inner.signal.addEventListener("abort", onInnerAbort, { once: true });
62985
63730
  }
62986
63731
  try {
62987
- const resp = await getBulkProcessStatus(client, chunk, {
63732
+ const resp = await getBulkProcessStatus(client, chunk2, {
62988
63733
  signal: tickAc.signal
62989
63734
  });
62990
- for (let j = 0; j < chunk.length; j++) {
63735
+ for (let j = 0; j < chunk2.length; j++) {
62991
63736
  const entry = resp.results[j];
62992
63737
  if (entry !== void 0 && isTerminalAiStatus(entry.ai_processing_status)) {
62993
- resolved.set(chunk[j], entry);
63738
+ resolved.set(chunk2[j], entry);
62994
63739
  }
62995
63740
  }
62996
63741
  } catch (err) {
@@ -64751,10 +65496,10 @@ async function extractSync(cfg, parts) {
64751
65496
  const chunked = chunkContracts([...parts], MAX_FILES_PER_BATCH);
64752
65497
  let abortedMidRun = false;
64753
65498
  for (let ci = 0; ci < chunked.length; ci++) {
64754
- const chunk = chunked[ci];
65499
+ const chunk2 = chunked[ci];
64755
65500
  try {
64756
65501
  const results = await retryAsync(
64757
- () => uploadFilesWithAi(cfg.client, chunk, {
65502
+ () => uploadFilesWithAi(cfg.client, chunk2, {
64758
65503
  isAdditional: cfg.isAdditional,
64759
65504
  signal: cfg.signal
64760
65505
  }),
@@ -64772,7 +65517,7 @@ async function extractSync(cfg, parts) {
64772
65517
  }
64773
65518
  );
64774
65519
  allResults.push(...results);
64775
- for (const part of chunk) {
65520
+ for (const part of chunk2) {
64776
65521
  sourceFiles.push(part.sourcePath);
64777
65522
  outcomes.push({ path: part.sourcePath, ok: true, detail: "extracted" });
64778
65523
  }
@@ -64780,7 +65525,7 @@ async function extractSync(cfg, parts) {
64780
65525
  const msg = err instanceof Error ? err.message : String(err);
64781
65526
  const kind = classifyFailure(err);
64782
65527
  const detail = kind === "ambiguous" ? `${msg} \u2014 upstream may still be processing; check before re-sending` : msg;
64783
- for (const part of chunk) {
65528
+ for (const part of chunk2) {
64784
65529
  outcomes.push({ path: part.sourcePath, ok: false, detail });
64785
65530
  }
64786
65531
  if (kind === "abort") {
@@ -67787,8 +68532,8 @@ Pipe it (printf '%s\\n' "$APP_PASSWORD" | dr desk smtp set \u2026) or use --pass
67787
68532
  }
67788
68533
  const chunks = [];
67789
68534
  let total = 0;
67790
- for await (const chunk of process.stdin) {
67791
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
68535
+ for await (const chunk2 of process.stdin) {
68536
+ const buf = Buffer.isBuffer(chunk2) ? chunk2 : Buffer.from(String(chunk2));
67792
68537
  total += buf.length;
67793
68538
  if (total > maxBytes) {
67794
68539
  throw new Error(