@oxygen-agent/cli 1.226.15 → 1.233.8
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/README.md +1 -1
- package/dist/index.js +299 -8
- package/dist/local-custom-http-column.js +378 -141
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +62 -0
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +91 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-url.js +6 -1
- package/node_modules/@oxygen/shared/dist/select-options.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +11 -0
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +15 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -428,9 +428,66 @@ function buildCrmSyncImportBody(provider, options) {
|
|
|
428
428
|
...(options.approved ? { approved: true } : {}),
|
|
429
429
|
};
|
|
430
430
|
}
|
|
431
|
-
function
|
|
431
|
+
function buildCrmSyncConfigureBody(options) {
|
|
432
|
+
return {
|
|
433
|
+
provider: options.provider,
|
|
434
|
+
object: options.object,
|
|
435
|
+
mode: resolveCrmSetupMode(options),
|
|
436
|
+
...(readOption(options.direction) ? { direction: readOption(options.direction) } : {}),
|
|
437
|
+
...(readOption(options.cron) ? { cron: readOption(options.cron) } : {}),
|
|
438
|
+
...(readOption(options.timezone) ? { timezone: readOption(options.timezone) } : {}),
|
|
439
|
+
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
440
|
+
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
441
|
+
...(options.approved ? { approved: true } : {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function buildCrmSyncRunBody(options) {
|
|
445
|
+
return {
|
|
446
|
+
provider: options.provider,
|
|
447
|
+
object: options.object,
|
|
448
|
+
mode: resolveCrmSetupMode(options),
|
|
449
|
+
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
450
|
+
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
451
|
+
...(options.approved ? { approved: true } : {}),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function buildCrmSyncEnableBody(options) {
|
|
455
|
+
return {
|
|
456
|
+
provider: options.provider,
|
|
457
|
+
object: options.object,
|
|
458
|
+
...(readOption(options.direction) ? { direction: readOption(options.direction) } : {}),
|
|
459
|
+
...(readOption(options.cron) ? { cron: readOption(options.cron) } : {}),
|
|
460
|
+
...(readOption(options.timezone) ? { timezone: readOption(options.timezone) } : {}),
|
|
461
|
+
...(readPositiveInt(options.maxRows) !== undefined ? { max_rows_per_cycle: readPositiveInt(options.maxRows) } : {}),
|
|
462
|
+
...(readPositiveNumber(options.maxCredits) !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
|
|
463
|
+
...(options.approved ? { approved: true } : {}),
|
|
464
|
+
// Commander maps --no-run-now to runNow:false (default true); only send the
|
|
465
|
+
// opt-out so the route's default (run an immediate first cycle) holds.
|
|
466
|
+
...(options.runNow === false ? { run_now: false } : {}),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function buildCrmSyncDisableBody(options) {
|
|
470
|
+
return { provider: options.provider, object: options.object };
|
|
471
|
+
}
|
|
472
|
+
function buildCrmSyncConfigStatusPath(options) {
|
|
473
|
+
const runId = readOption(options.runId);
|
|
474
|
+
const provider = readOption(options.provider);
|
|
475
|
+
const object = readOption(options.object);
|
|
476
|
+
const hasPair = provider !== null && object !== null;
|
|
477
|
+
if (runId !== null && (provider !== null || object !== null)) {
|
|
478
|
+
throw new OxygenError("conflicting_flags", "Pass either --run-id or --provider/--object, not both.", { exitCode: 1 });
|
|
479
|
+
}
|
|
480
|
+
if (runId === null && !hasPair) {
|
|
481
|
+
throw new OxygenError("missing_target", "Pass --run-id for a single run, or --provider and --object for a standing sync config.", { exitCode: 1 });
|
|
482
|
+
}
|
|
432
483
|
const params = new URLSearchParams();
|
|
433
|
-
|
|
484
|
+
if (runId !== null) {
|
|
485
|
+
params.set("run_id", runId);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
params.set("provider", provider);
|
|
489
|
+
params.set("object", object);
|
|
490
|
+
}
|
|
434
491
|
return `/api/cli/crm/sync/status?${params.toString()}`;
|
|
435
492
|
}
|
|
436
493
|
function buildCrmSyncLinksPath(object, rowId) {
|
|
@@ -1625,7 +1682,7 @@ export function createProgram() {
|
|
|
1625
1682
|
await handleAsyncAction("crm automation audit", options, () => requestOxygen(buildCrmAutomationAuditPath(options)));
|
|
1626
1683
|
})))
|
|
1627
1684
|
.addCommand(new Command("sync")
|
|
1628
|
-
.description("
|
|
1685
|
+
.description("Bidirectionally sync a connected CRM (HubSpot/Attio) with Oxygen CRM objects: import records, configure a standing two-way sync, run a reconciliation cycle, and inspect runs and provider links.")
|
|
1629
1686
|
.addCommand(new Command("import")
|
|
1630
1687
|
.description("Import provider records into an Oxygen CRM object as a durable workflow run. Defaults to dry-run.")
|
|
1631
1688
|
.argument("<provider>", "Connected CRM to import from: hubspot or attio.")
|
|
@@ -1642,13 +1699,84 @@ export function createProgram() {
|
|
|
1642
1699
|
method: "POST",
|
|
1643
1700
|
body: buildCrmSyncImportBody(provider, options),
|
|
1644
1701
|
}));
|
|
1702
|
+
}))
|
|
1703
|
+
.addCommand(new Command("configure")
|
|
1704
|
+
.description("Configure a standing bidirectional sync for one CRM object pair. Defaults to dry-run, which returns a real preview of the next cycle.")
|
|
1705
|
+
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
1706
|
+
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
1707
|
+
.option("--direction <direction>", "Sync direction: inbound, outbound, or bidirectional. Defaults to bidirectional.")
|
|
1708
|
+
.option("--cron <cron>", "Cron expression for the scheduled reconciliation cadence, e.g. '*/15 * * * *'.")
|
|
1709
|
+
.option("--timezone <tz>", "IANA timezone the cron schedule runs in, e.g. America/New_York. Defaults to UTC.")
|
|
1710
|
+
.option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
|
|
1711
|
+
.option("--max-credits <n>", "Managed-credit + external-quota cap per cycle.")
|
|
1712
|
+
.option("--approved", "Confirm a live configuration after inspecting a dry run.")
|
|
1713
|
+
.option("--dry-run", "Preview the configuration and next cycle without writing. Default.")
|
|
1714
|
+
.option("--live", "Write the configuration. Requires --approved and --max-credits.")
|
|
1715
|
+
.option("--json", "Print a JSON envelope.")
|
|
1716
|
+
.action(async (options) => {
|
|
1717
|
+
await handleAsyncAction("crm sync configure", options, () => requestOxygen("/api/cli/crm/sync/configure", {
|
|
1718
|
+
method: "POST",
|
|
1719
|
+
body: buildCrmSyncConfigureBody(options),
|
|
1720
|
+
}));
|
|
1721
|
+
}))
|
|
1722
|
+
.addCommand(new Command("run")
|
|
1723
|
+
.description("Run one bidirectional reconciliation cycle for a configured CRM object pair as a durable workflow run. Defaults to dry-run.")
|
|
1724
|
+
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
1725
|
+
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
1726
|
+
.option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
|
|
1727
|
+
.option("--max-credits <n>", "Managed-credit + external-quota cap for this cycle.")
|
|
1728
|
+
.option("--approved", "Confirm a live cycle after inspecting a dry run.")
|
|
1729
|
+
.option("--dry-run", "Preview the cycle without pulling or pushing. Default.")
|
|
1730
|
+
.option("--live", "Run the live reconciliation cycle. Requires --approved and --max-credits.")
|
|
1731
|
+
.option("--json", "Print a JSON envelope.")
|
|
1732
|
+
.action(async (options) => {
|
|
1733
|
+
await handleAsyncAction("crm sync run", options, () => requestOxygen("/api/cli/crm/sync/run", {
|
|
1734
|
+
method: "POST",
|
|
1735
|
+
body: buildCrmSyncRunBody(options),
|
|
1736
|
+
}));
|
|
1737
|
+
}))
|
|
1738
|
+
.addCommand(new Command("enable")
|
|
1739
|
+
.description("Enable continuous (scheduled) bidirectional sync for one CRM object pair: arms the cron trigger + the standing write permission, and runs an immediate first cycle. Always live — requires --approved, --max-credits, and --max-rows.")
|
|
1740
|
+
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
1741
|
+
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
1742
|
+
.option("--direction <direction>", "Sync direction: inbound, outbound, or bidirectional. Defaults to bidirectional.")
|
|
1743
|
+
.option("--cron <cron>", "Cron expression for the cadence, e.g. '*/15 * * * *'. Defaults to every 15 minutes.")
|
|
1744
|
+
.option("--timezone <tz>", "IANA timezone the cron runs in. Defaults to UTC.")
|
|
1745
|
+
.requiredOption("--max-rows <n>", "Per-cycle row cap — the volume brake on provider writes (required).")
|
|
1746
|
+
.requiredOption("--max-credits <n>", "Managed-credit + external-quota cap per cycle (required).")
|
|
1747
|
+
.option("--approved", "Confirm enabling scheduled provider writes after inspecting a dry run.")
|
|
1748
|
+
.option("--no-run-now", "Do not run an immediate first cycle after enabling.")
|
|
1749
|
+
.option("--json", "Print a JSON envelope.")
|
|
1750
|
+
.action(async (options) => {
|
|
1751
|
+
await handleAsyncAction("crm sync enable", options, () => requestOxygen("/api/cli/crm/sync/enable", {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
body: buildCrmSyncEnableBody(options),
|
|
1754
|
+
}));
|
|
1755
|
+
}))
|
|
1756
|
+
.addCommand(new Command("disable")
|
|
1757
|
+
.description("Disable the scheduled sync for one CRM object pair: stops the cron trigger and revokes the standing write permission. The standing config (direction, cadence, mappings, watermarks) is preserved for re-enable.")
|
|
1758
|
+
.requiredOption("--provider <provider>", "Connected CRM: hubspot or attio.")
|
|
1759
|
+
.requiredOption("--object <object>", "Provider object: hubspot contacts|companies, or attio people|companies.")
|
|
1760
|
+
.option("--json", "Print a JSON envelope.")
|
|
1761
|
+
.action(async (options) => {
|
|
1762
|
+
await handleAsyncAction("crm sync disable", options, () => requestOxygen("/api/cli/crm/sync/disable", {
|
|
1763
|
+
method: "POST",
|
|
1764
|
+
body: buildCrmSyncDisableBody(options),
|
|
1765
|
+
}));
|
|
1645
1766
|
}))
|
|
1646
1767
|
.addCommand(new Command("status")
|
|
1647
|
-
.description("Show
|
|
1648
|
-
.argument("
|
|
1768
|
+
.description("Show a CRM sync status: pass --run-id for one workflow run, or --provider/--object for the standing sync config (cadence, watermarks, last-cycle counts).")
|
|
1769
|
+
.argument("[run_id]", "Workflow run id. Equivalent to --run-id.")
|
|
1770
|
+
.option("--run-id <id>", "Workflow run id to read a single run.")
|
|
1771
|
+
.option("--provider <provider>", "Connected CRM (hubspot|attio) for a standing-config read. Use with --object.")
|
|
1772
|
+
.option("--object <object>", "Provider object for a standing-config read. Use with --provider.")
|
|
1649
1773
|
.option("--json", "Print a JSON envelope.")
|
|
1650
1774
|
.action(async (runId, options) => {
|
|
1651
|
-
|
|
1775
|
+
const resolved = {
|
|
1776
|
+
...options,
|
|
1777
|
+
...(runId !== undefined && readOption(options.runId) === null ? { runId } : {}),
|
|
1778
|
+
};
|
|
1779
|
+
await handleAsyncAction("crm sync status", resolved, () => requestOxygen(buildCrmSyncConfigStatusPath(resolved)));
|
|
1652
1780
|
}))
|
|
1653
1781
|
.addCommand(new Command("links")
|
|
1654
1782
|
.description("Show provider record links (HubSpot/Attio ids) mapped to one Oxygen CRM record.")
|
|
@@ -1908,6 +2036,15 @@ export function createProgram() {
|
|
|
1908
2036
|
...(options.includeArchived ? { include_archived: true } : {}),
|
|
1909
2037
|
},
|
|
1910
2038
|
}));
|
|
2039
|
+
}))
|
|
2040
|
+
.addCommand(new Command("tidy-suggest")
|
|
2041
|
+
.description("Read-only tidy analysis for a workspace table: duplicate columns, formatting issues, hide candidates, and value-sanity findings.")
|
|
2042
|
+
.argument("<table>", "Table id or slug.")
|
|
2043
|
+
.option("--limit <n>", "Maximum rows to sample for the analysis. Defaults to and is capped at 5000.")
|
|
2044
|
+
.option("--categories <csv>", "Comma-separated subset of duplicates,formatting,hide,valueSanity. Defaults to all four.")
|
|
2045
|
+
.option("--json", "Print a JSON envelope.")
|
|
2046
|
+
.action(async (table, options) => {
|
|
2047
|
+
await handleTablesTidySuggestAction(table, options);
|
|
1911
2048
|
}))
|
|
1912
2049
|
.addCommand(new Command("activity")
|
|
1913
2050
|
.description("Show background runs active on a workspace table right now, with a worker-queue health rollup.")
|
|
@@ -2816,6 +2953,29 @@ export function createProgram() {
|
|
|
2816
2953
|
method: "POST",
|
|
2817
2954
|
body: { table, column },
|
|
2818
2955
|
}));
|
|
2956
|
+
}))
|
|
2957
|
+
.addCommand(new Command("reorder")
|
|
2958
|
+
.description("Move a workspace column to a new position, by absolute index or relative to another column.")
|
|
2959
|
+
.argument("<table>", "Table id or slug.")
|
|
2960
|
+
.argument("<column>", "Column id or key to move.")
|
|
2961
|
+
.option("--position <n>", "Target zero-based index for the column.")
|
|
2962
|
+
.option("--before <column>", "Place the column immediately before this column id or key.")
|
|
2963
|
+
.option("--after <column>", "Place the column immediately after this column id or key.")
|
|
2964
|
+
.option("--dry-run", "Return the would-be position without persisting the reorder.")
|
|
2965
|
+
.option("--json", "Print a JSON envelope.")
|
|
2966
|
+
.action(async (table, column, options) => {
|
|
2967
|
+
await handleAsyncAction("columns reorder", options, async () => {
|
|
2968
|
+
const position = await resolveColumnReorderPosition(table, options);
|
|
2969
|
+
return requestOxygen("/api/cli/tables/columns/reorder", {
|
|
2970
|
+
method: "POST",
|
|
2971
|
+
body: {
|
|
2972
|
+
table,
|
|
2973
|
+
column,
|
|
2974
|
+
position,
|
|
2975
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
2976
|
+
},
|
|
2977
|
+
});
|
|
2978
|
+
});
|
|
2819
2979
|
}));
|
|
2820
2980
|
program
|
|
2821
2981
|
.command("action-column")
|
|
@@ -4430,6 +4590,7 @@ export function createProgram() {
|
|
|
4430
4590
|
.addCommand(new Command("connect")
|
|
4431
4591
|
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Open the URL in a browser to complete authentication.")
|
|
4432
4592
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
4593
|
+
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
4433
4594
|
.option("--json", "Print a JSON envelope.")
|
|
4434
4595
|
.action(async (options) => {
|
|
4435
4596
|
await handleAsyncAction("senders connect", options, () => {
|
|
@@ -4438,6 +4599,7 @@ export function createProgram() {
|
|
|
4438
4599
|
method: "POST",
|
|
4439
4600
|
body: {
|
|
4440
4601
|
...(reconnect ? { reconnect_connection_id: reconnect } : {}),
|
|
4602
|
+
...(options.salesNav ? { sales_nav: true } : {}),
|
|
4441
4603
|
},
|
|
4442
4604
|
});
|
|
4443
4605
|
});
|
|
@@ -4497,8 +4659,9 @@ export function createProgram() {
|
|
|
4497
4659
|
.option("--relations-reads-per-day <n>", "Daily cap on relations/connections list reads (scrape protection).")
|
|
4498
4660
|
.option("--messages-reads-per-day <n>", "Daily cap on chat and message-history reads.")
|
|
4499
4661
|
.option("--searches-per-day <n>", "Daily cap on LinkedIn search executions.")
|
|
4662
|
+
.option("--sales-nav-search-results-per-day <n>", "Daily cap on Sales Navigator search result rows fetched.")
|
|
4500
4663
|
.option("--api-reads-per-day <n>", "Daily cap on all other LinkedIn API reads.")
|
|
4501
|
-
.option("--total-reads-per-day <n>", "Daily cap across all read
|
|
4664
|
+
.option("--total-reads-per-day <n>", "Daily cap across all read units.")
|
|
4502
4665
|
.option("--min-spacing-seconds <n>", "Minimum seconds between actions.")
|
|
4503
4666
|
.option("--spacing-jitter-seconds <n>", "Random jitter seconds added to action spacing.")
|
|
4504
4667
|
.option("--timezone <tz>", "IANA timezone the daily action counters reset in, e.g. America/New_York.")
|
|
@@ -4866,13 +5029,14 @@ export function createProgram() {
|
|
|
4866
5029
|
.description("Run analysis on a conversation now: status + sentiment + a drafted reply (Oxygen's default model). The worker also does this automatically on inbound conversations.")
|
|
4867
5030
|
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
4868
5031
|
.option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
|
|
5032
|
+
.option("--force", "Re-analyze even if the conversation was already classified (an explicit paid re-classification; spends one managed credit). Without it, an already-analyzed email is a free no-op. DMs always re-analyze.")
|
|
4869
5033
|
.option("--json", "Print a JSON envelope.")
|
|
4870
5034
|
.action(async (conversation, options) => {
|
|
4871
5035
|
await handleAsyncAction("inbox analyze", options, () => {
|
|
4872
5036
|
const channel = readOption(options.channel) ?? "email";
|
|
4873
5037
|
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/analyze`, {
|
|
4874
5038
|
method: "POST",
|
|
4875
|
-
body: { channel },
|
|
5039
|
+
body: { channel, ...(options.force ? { force: true } : {}) },
|
|
4876
5040
|
});
|
|
4877
5041
|
});
|
|
4878
5042
|
}))
|
|
@@ -9629,6 +9793,132 @@ function formatSequenceVariants(data) {
|
|
|
9629
9793
|
lines.push("");
|
|
9630
9794
|
return lines.join("\n");
|
|
9631
9795
|
}
|
|
9796
|
+
// `columns reorder` accepts either an absolute --position or a relative
|
|
9797
|
+
// --before/--after naming a sibling column. The relative form is resolved
|
|
9798
|
+
// client-side into an absolute index against the table's current column order
|
|
9799
|
+
// (fetched via /api/cli/tables/describe — the same describe the other column
|
|
9800
|
+
// commands read) so the server only ever receives a single numeric position.
|
|
9801
|
+
// Exactly one of the three flags must be supplied.
|
|
9802
|
+
async function resolveColumnReorderPosition(table, options) {
|
|
9803
|
+
const before = readOption(options.before);
|
|
9804
|
+
const after = readOption(options.after);
|
|
9805
|
+
const explicit = readNonNegativeInt(options.position);
|
|
9806
|
+
const provided = [explicit !== undefined, Boolean(before), Boolean(after)].filter(Boolean).length;
|
|
9807
|
+
if (provided === 0) {
|
|
9808
|
+
throw new OxygenError("invalid_request", "Provide --position <n>, --before <column>, or --after <column>.", { exitCode: 1 });
|
|
9809
|
+
}
|
|
9810
|
+
if (provided > 1) {
|
|
9811
|
+
throw new OxygenError("invalid_request", "Pass only one of --position, --before, or --after.", { exitCode: 1 });
|
|
9812
|
+
}
|
|
9813
|
+
if (explicit !== undefined)
|
|
9814
|
+
return explicit;
|
|
9815
|
+
const reference = (before ?? after);
|
|
9816
|
+
const describe = await requestOxygen("/api/cli/tables/describe", { method: "POST", body: { table } });
|
|
9817
|
+
const columns = describe.columns ?? [];
|
|
9818
|
+
const index = columns.findIndex((candidate) => candidate.id === reference || candidate.key === reference);
|
|
9819
|
+
if (index < 0) {
|
|
9820
|
+
throw new OxygenError("invalid_request", `Column "${reference}" was not found on table "${table}".`, { details: { table, column: reference }, exitCode: 1 });
|
|
9821
|
+
}
|
|
9822
|
+
// --before lands the column at the reference's index; --after lands it one slot later.
|
|
9823
|
+
return before ? index : index + 1;
|
|
9824
|
+
}
|
|
9825
|
+
// `tables tidy-suggest` is a read-only analysis (no credits, no writes): it
|
|
9826
|
+
// surfaces duplicate columns, formatting inconsistencies, hide candidates, and
|
|
9827
|
+
// value-sanity findings so the caller can pick which reversible cleanup to run
|
|
9828
|
+
// next. --json returns the raw report envelope; the default rendering compacts
|
|
9829
|
+
// the four sections for a terminal reader and echoes the deep-link.
|
|
9830
|
+
async function handleTablesTidySuggestAction(table, options) {
|
|
9831
|
+
try {
|
|
9832
|
+
const limit = readPositiveInt(options.limit);
|
|
9833
|
+
const categories = readCsvOption(options.categories);
|
|
9834
|
+
const data = await requestOxygen("/api/cli/tables/tidy/suggest", {
|
|
9835
|
+
method: "POST",
|
|
9836
|
+
body: {
|
|
9837
|
+
table,
|
|
9838
|
+
...(limit ? { limit } : {}),
|
|
9839
|
+
...(categories.length ? { categories } : {}),
|
|
9840
|
+
},
|
|
9841
|
+
});
|
|
9842
|
+
if (options.json) {
|
|
9843
|
+
writeJson(success("tables tidy-suggest", data));
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9846
|
+
process.stdout.write(formatTidySuggestions(data));
|
|
9847
|
+
}
|
|
9848
|
+
catch (error) {
|
|
9849
|
+
const failure = toFailure("tables tidy-suggest", error);
|
|
9850
|
+
writeJson(failure);
|
|
9851
|
+
writeMaxCreditsHint(error);
|
|
9852
|
+
process.exitCode = error instanceof OxygenError ? error.exitCode : 1;
|
|
9853
|
+
}
|
|
9854
|
+
}
|
|
9855
|
+
function formatTidyCount(value) {
|
|
9856
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : "0";
|
|
9857
|
+
}
|
|
9858
|
+
function formatTidyPercent(value) {
|
|
9859
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
9860
|
+
return "—";
|
|
9861
|
+
return `${(value * 100).toFixed(0)}%`;
|
|
9862
|
+
}
|
|
9863
|
+
function formatTidySuggestions(data) {
|
|
9864
|
+
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
9865
|
+
const duplicates = Array.isArray(data.duplicateColumns) ? data.duplicateColumns : [];
|
|
9866
|
+
const formatting = Array.isArray(data.formattingIssues) ? data.formattingIssues : [];
|
|
9867
|
+
const hide = Array.isArray(data.hideSuggestions) ? data.hideSuggestions : [];
|
|
9868
|
+
const valueSanity = Array.isArray(data.valueSanity) ? data.valueSanity : [];
|
|
9869
|
+
const lines = [
|
|
9870
|
+
"",
|
|
9871
|
+
`${styles.bold("Table tidy suggestions")} ${styles.dim(`(sampled ${formatTidyCount(data.sampledRows)} of ${formatTidyCount(data.rowCount)} rows)`)}`,
|
|
9872
|
+
];
|
|
9873
|
+
lines.push("", styles.bold(`Duplicate columns (${duplicates.length})`));
|
|
9874
|
+
if (duplicates.length === 0) {
|
|
9875
|
+
lines.push(` ${styles.dim("None")}`);
|
|
9876
|
+
}
|
|
9877
|
+
else {
|
|
9878
|
+
for (const dup of duplicates) {
|
|
9879
|
+
const loser = dup.loserKey ?? dup.loserColumnId ?? "?";
|
|
9880
|
+
const survivor = dup.survivorKey ?? dup.survivorColumnId ?? "?";
|
|
9881
|
+
lines.push(` ${loser} ${styles.dim("→ duplicate of")} ${survivor} ${styles.dim(`(${formatTidyPercent(dup.similarity)} match, ${dup.recommendation ?? "review"})`)}`);
|
|
9882
|
+
}
|
|
9883
|
+
}
|
|
9884
|
+
lines.push("", styles.bold(`Formatting issues (${formatting.length})`));
|
|
9885
|
+
if (formatting.length === 0) {
|
|
9886
|
+
lines.push(` ${styles.dim("None")}`);
|
|
9887
|
+
}
|
|
9888
|
+
else {
|
|
9889
|
+
for (const issue of formatting) {
|
|
9890
|
+
const sampleCount = Array.isArray(issue.sampleRowIds) ? issue.sampleRowIds.length : 0;
|
|
9891
|
+
const column = issue.columnKey ?? issue.columnId ?? "?";
|
|
9892
|
+
lines.push(` ${column} ${styles.dim(`— ${issue.issue ?? "issue"}`)}${issue.detail ? `: ${issue.detail}` : ""} ${styles.dim(`(${sampleCount} sample row(s))`)}`);
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
lines.push("", styles.bold(`Hide suggestions (${hide.length})`));
|
|
9896
|
+
if (hide.length === 0) {
|
|
9897
|
+
lines.push(` ${styles.dim("None")}`);
|
|
9898
|
+
}
|
|
9899
|
+
else {
|
|
9900
|
+
for (const suggestion of hide) {
|
|
9901
|
+
const column = suggestion.columnKey ?? suggestion.columnId ?? "?";
|
|
9902
|
+
lines.push(` ${column} ${styles.dim(`— ${suggestion.reason ?? "hide"} (${formatTidyPercent(suggestion.fillRate)} filled)`)}`);
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
lines.push("", styles.bold(`Value-sanity findings (${valueSanity.length})`));
|
|
9906
|
+
if (valueSanity.length === 0) {
|
|
9907
|
+
lines.push(` ${styles.dim("None")}`);
|
|
9908
|
+
}
|
|
9909
|
+
else {
|
|
9910
|
+
for (const finding of valueSanity) {
|
|
9911
|
+
const column = finding.columnKey ?? finding.columnId ?? "?";
|
|
9912
|
+
lines.push(` ${column} ${styles.dim(`row ${finding.rowId ?? "?"} — ${finding.reason ?? "check value"}`)}`);
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
const link = data.deepLink ?? data.web_url ?? data.url;
|
|
9916
|
+
if (link) {
|
|
9917
|
+
lines.push("", styles.dim(`View table: ${link}`));
|
|
9918
|
+
}
|
|
9919
|
+
lines.push("");
|
|
9920
|
+
return lines.join("\n");
|
|
9921
|
+
}
|
|
9632
9922
|
function formatWhoami(identity, context) {
|
|
9633
9923
|
const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
|
|
9634
9924
|
const email = identity.user.email ?? identity.user.id;
|
|
@@ -10083,6 +10373,7 @@ options) {
|
|
|
10083
10373
|
setLimit("relations_reads_per_day", options.relationsReadsPerDay);
|
|
10084
10374
|
setLimit("messages_reads_per_day", options.messagesReadsPerDay);
|
|
10085
10375
|
setLimit("searches_per_day", options.searchesPerDay);
|
|
10376
|
+
setLimit("sales_nav_search_results_per_day", options.salesNavSearchResultsPerDay);
|
|
10086
10377
|
setLimit("api_reads_per_day", options.apiReadsPerDay);
|
|
10087
10378
|
setLimit("total_reads_per_day", options.totalReadsPerDay);
|
|
10088
10379
|
setLimit("min_action_spacing_seconds", options.minSpacingSeconds);
|