@oxygen-agent/cli 1.1014.985 → 1.1014.1087

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.d.ts +2 -0
  3. package/dist/command-families/program.js +241 -0
  4. package/dist/command-families/tables/automation.d.ts +22 -0
  5. package/dist/command-families/tables/automation.js +250 -0
  6. package/dist/command-families/tables/bundle-export.d.ts +7 -0
  7. package/dist/command-families/tables/bundle-export.js +198 -0
  8. package/dist/command-families/tables/bundle-format.d.ts +66 -0
  9. package/dist/command-families/tables/bundle-format.js +274 -0
  10. package/dist/command-families/tables/bundle-import.d.ts +20 -0
  11. package/dist/command-families/tables/bundle-import.js +260 -0
  12. package/dist/command-families/tables/display.d.ts +107 -0
  13. package/dist/command-families/tables/display.js +229 -0
  14. package/dist/command-families/tables/import-batches.d.ts +26 -0
  15. package/dist/command-families/tables/import-batches.js +94 -0
  16. package/dist/command-families/tables/import-columns.d.ts +24 -0
  17. package/dist/command-families/tables/import-columns.js +131 -0
  18. package/dist/command-families/tables/import-upload.d.ts +30 -0
  19. package/dist/command-families/tables/import-upload.js +203 -0
  20. package/dist/command-families/tables/import-values.d.ts +10 -0
  21. package/dist/command-families/tables/import-values.js +60 -0
  22. package/dist/command-families/tables/imports.d.ts +19 -0
  23. package/dist/command-families/tables/imports.js +200 -0
  24. package/dist/command-families/tables/options.d.ts +314 -0
  25. package/dist/command-families/tables/options.js +1 -0
  26. package/dist/command-families/tables/register-automation.d.ts +2 -0
  27. package/dist/command-families/tables/register-automation.js +127 -0
  28. package/dist/command-families/tables/register-external-imports.d.ts +2 -0
  29. package/dist/command-families/tables/register-external-imports.js +293 -0
  30. package/dist/command-families/tables/register-files.d.ts +2 -0
  31. package/dist/command-families/tables/register-files.js +72 -0
  32. package/dist/command-families/tables/register-followers.d.ts +2 -0
  33. package/dist/command-families/tables/register-followers.js +73 -0
  34. package/dist/command-families/tables/register-lifecycle.d.ts +2 -0
  35. package/dist/command-families/tables/register-lifecycle.js +114 -0
  36. package/dist/command-families/tables/register-reads.d.ts +2 -0
  37. package/dist/command-families/tables/register-reads.js +187 -0
  38. package/dist/command-families/tables/register-relations.d.ts +2 -0
  39. package/dist/command-families/tables/register-relations.js +244 -0
  40. package/dist/command-families/tables/register-row-actions.d.ts +2 -0
  41. package/dist/command-families/tables/register-row-actions.js +53 -0
  42. package/dist/command-families/tables/register-row-writes.d.ts +2 -0
  43. package/dist/command-families/tables/register-row-writes.js +140 -0
  44. package/dist/command-families/tables/register-views.d.ts +2 -0
  45. package/dist/command-families/tables/register-views.js +83 -0
  46. package/dist/command-families/tables/register-watchers.d.ts +2 -0
  47. package/dist/command-families/tables/register-watchers.js +65 -0
  48. package/dist/command-families/tables/register-webhooks.d.ts +2 -0
  49. package/dist/command-families/tables/register-webhooks.js +126 -0
  50. package/dist/command-families/tables/registration-context.d.ts +8 -0
  51. package/dist/command-families/tables/registration-context.js +1 -0
  52. package/dist/command-families/tables/registration.d.ts +3 -0
  53. package/dist/command-families/tables/registration.js +30 -0
  54. package/dist/command-families/tables/row-selection.d.ts +5 -0
  55. package/dist/command-families/tables/row-selection.js +82 -0
  56. package/dist/command-families/tables/send.d.ts +6 -0
  57. package/dist/command-families/tables/send.js +111 -0
  58. package/dist/command-manifest.js +1 -0
  59. package/dist/commands/copilot.js +8 -0
  60. package/dist/commands/deliverability.js +4 -2
  61. package/dist/commands/sequences.js +30 -2
  62. package/dist/commands/tables.d.ts +1 -3
  63. package/dist/commands/tables.js +1 -3505
  64. package/dist/credentials.js +1 -1
  65. package/dist/customer-operation-commands.js +62 -3
  66. package/dist/help.js +1 -1
  67. package/dist/index.d.ts +1 -99
  68. package/dist/index.js +5 -337
  69. package/dist/knowledge-mirror.js +1 -1
  70. package/dist/skills.js +4 -2
  71. package/dist/update.js +2 -1
  72. package/dist/workspace-placement-commands.d.ts +3 -0
  73. package/dist/workspace-placement-commands.js +32 -0
  74. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +4 -0
  75. package/node_modules/@oxygen/shared/dist/agent-sessions.js +11 -3
  76. package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +29 -0
  77. package/node_modules/@oxygen/shared/dist/agents-enabled.js +51 -0
  78. package/node_modules/@oxygen/shared/dist/capability-discovery.js +39 -2
  79. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
  80. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  81. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
  82. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
  83. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
  84. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
  85. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
  86. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
  87. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
  88. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  89. package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
  90. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
  91. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
  92. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +17 -15
  93. package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
  94. package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
  95. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +4 -0
  96. package/node_modules/@oxygen/shared/dist/feature-gates.js +5 -0
  97. package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
  98. package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
  99. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
  100. package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
  101. package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
  102. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
  103. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
  104. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
  105. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
  106. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
  107. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
  108. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
  109. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
  110. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
  111. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
  112. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
  113. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
  114. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
  115. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
  116. package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
  117. package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
  118. package/node_modules/@oxygen/shared/dist/index.d.ts +0 -1
  119. package/node_modules/@oxygen/shared/dist/index.js +0 -6
  120. package/node_modules/@oxygen/shared/dist/table-limits.d.ts +1 -0
  121. package/node_modules/@oxygen/shared/dist/table-limits.js +6 -0
  122. package/node_modules/@oxygen/shared/dist/teammates.d.ts +13 -8
  123. package/node_modules/@oxygen/shared/dist/teammates.js +20 -11
  124. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +78 -0
  125. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  126. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  127. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +149 -0
  128. package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +101 -0
  129. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +77 -0
  130. package/node_modules/@oxygen/shared/dist/workspace-placement.js +94 -0
  131. package/package.json +1 -1
@@ -0,0 +1,293 @@
1
+ import { readPositiveInt } from "../../cli-values.js";
2
+ import { readPositiveNumber } from "../../command-support.js";
3
+ import { addContractFlags, readContractFlags } from "../../contract-flags.js";
4
+ import { requestOxygen } from "../../http-client.js";
5
+ import { readOption } from "../../util.js";
6
+ import { OxygenError } from "@oxygen/shared/cli-result";
7
+ import { PIPEDRIVE_CATALOG_QUERY_CONTRACT, pipedriveCatalogQueryParams } from "@oxygen/shared/contracts/pipedrive-catalog";
8
+ import { Command } from "commander";
9
+ export function registerExternalImports({ tablesCommand, handleAsyncAction }) {
10
+ tablesCommand.addCommand(new Command("sources")
11
+ .description("List every way a table can start — imports, company/people/signal sources, webhooks, functions — with the provider count and per-row credit estimate behind each. Free; nothing is called or written. The same catalog the web New table picker shows.")
12
+ .option("--group <group>", "Only one group: Companies, People, \"Professional Network\" (or linkedin), Imports, CRM, Signals, or Tables.")
13
+ .option("--json", "Print a JSON envelope.")
14
+ .action(async (options) => {
15
+ await handleAsyncAction("tables sources", options, () => {
16
+ const params = new URLSearchParams();
17
+ const group = readOption(options.group);
18
+ if (group)
19
+ params.set("group", group);
20
+ const qs = params.toString() ? `?${params.toString()}` : "";
21
+ return requestOxygen(`/api/cli/tables/sources${qs}`);
22
+ });
23
+ }));
24
+ tablesCommand.addCommand(new Command("records")
25
+ .description("Copy a Records object or saved view into a working Table, once or kept in sync. One-way: Records stay unchanged, the Table is keyed by record_id, cells edited in the Table are kept, and records that leave the source stay as rows with in_records=false.")
26
+ .addCommand(new Command("import")
27
+ .description("Create a Table from a Records object or saved view. --dry-run counts the records and lists the columns without creating anything. --refresh sync also arms a schedule (a Workflow you can pause) that repeats the copy. The copy costs 0 credits and calls no provider; each scheduled sync bills one workflow step (credits_per_scheduled_sync).")
28
+ .requiredOption("--object <slug>", "Records object slug, such as companies, people or deals (`crm objects`).")
29
+ .option("--view <id|name>", "Saved view on that object, by id or exact name (`tables views list <object table>`). Omit for every record.")
30
+ .option("--name <table>", "Name for the new Table. Defaults to the object and view name.")
31
+ .option("--project <project>", "Project id or slug. Defaults to General.")
32
+ .option("--refresh <once|sync>", "once (default) copies now; sync also keeps it in sync (needs --approved).")
33
+ .option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
34
+ .option("--dry-run", "Preview without creating the Table.")
35
+ .option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
36
+ .option("--json", "Print a JSON envelope.")
37
+ .action(async (options) => {
38
+ await handleAsyncAction("tables records import", options, () => requestOxygen("/api/cli/tables/records-import", {
39
+ method: "POST",
40
+ body: {
41
+ object: options.object,
42
+ ...(readOption(options.view) ? { view: readOption(options.view) } : {}),
43
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
44
+ ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
45
+ ...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
46
+ ...(readOption(options.every) ? { every: readOption(options.every) } : {}),
47
+ ...(options.dryRun ? { dry_run: true } : {}),
48
+ ...(options.approved ? { approved: true } : {}),
49
+ },
50
+ }));
51
+ }))
52
+ .addCommand(new Command("sync")
53
+ .description("Copy a Records-imported Table's source again now: new records are added, changed cells updated, edited cells kept, and departed records marked in_records=false (they flip back if they return). Without --refresh the schedule stays as it is; --refresh sync --approved arms or retimes it, --refresh once removes it. preserved_cells counts copied-field cells left as someone edited them in the Table; columns you added are never touched and not counted. Running it now costs 0 credits; each scheduled sync bills one workflow step (credits_per_scheduled_sync in an import dry run).")
54
+ .argument("<table>", "Table id, slug or URL.")
55
+ .option("--refresh <once|sync>", "Omit to keep the schedule; sync (with --approved) arms or retimes it; once removes it.")
56
+ .option("--every <cadence>", "With --refresh sync: hourly, every_6h, daily@<hour> or weekly@<day><hour> (UTC). Default daily@6.")
57
+ .option("--dry-run", "Count what would change without writing.")
58
+ .option("--approved", "Approve the standing per-sync charge that --refresh sync arms.")
59
+ .option("--json", "Print a JSON envelope.")
60
+ .action(async (table, options) => {
61
+ await handleAsyncAction("tables records sync", options, () => requestOxygen("/api/cli/tables/records-sync", {
62
+ method: "POST",
63
+ body: {
64
+ table,
65
+ ...(readOption(options.refresh) ? { refresh: readOption(options.refresh) } : {}),
66
+ ...(readOption(options.every) ? { every: readOption(options.every) } : {}),
67
+ ...(options.dryRun ? { dry_run: true } : {}),
68
+ ...(options.approved ? { approved: true } : {}),
69
+ },
70
+ }));
71
+ }))
72
+ .addCommand(new Command("status")
73
+ .description("Show a Records-imported Table's source object and view, the last sync's counts and its schedule. Read-only, 0 credits.")
74
+ .argument("<table>", "Table id, slug or URL.")
75
+ .option("--json", "Print a JSON envelope.")
76
+ .action(async (table, options) => {
77
+ await handleAsyncAction("tables records status", options, () => requestOxygen(`/api/cli/tables/records-sync?table=${encodeURIComponent(table)}`));
78
+ })));
79
+ tablesCommand.addCommand(new Command("hubspot")
80
+ .description("Import HubSpot contacts or companies into a new table or Records, optionally from a saved list and with do-not-contact suppression.")
81
+ .addCommand(new Command("lists")
82
+ .description("List the connected portal's saved contact/company segments and readable identity properties. Reads definitions only; no memberships, rows, or writes. Free — 0 Oxygen credits, uses your HubSpot API quota.")
83
+ .option("--query <text>", "Optional list-name search.")
84
+ .option("--connection <id>", "HubSpot connection id; defaults to the first connected account.")
85
+ .option("--json", "Print a JSON envelope.")
86
+ .action(async (options) => {
87
+ await handleAsyncAction("tables hubspot lists", options, () => {
88
+ const params = new URLSearchParams();
89
+ const query = readOption(options.query);
90
+ if (query)
91
+ params.set("query", query);
92
+ const connection = readOption(options.connection);
93
+ if (connection)
94
+ params.set("connection_id", connection);
95
+ const suffix = params.toString();
96
+ return requestOxygen(`/api/cli/tables/hubspot-import${suffix ? `?${suffix}` : ""}`);
97
+ });
98
+ }))
99
+ .addCommand(new Command("import")
100
+ .description("Preview or import HubSpot contacts/companies into a new table or Records. Named do-not-contact lists must synchronize before any import. Nothing is enrolled or sent. Live requires the exact dry-run fingerprint and approval.")
101
+ .option("--list <id>", "HubSpot saved-list id. Omit with --object to import all records; discover ids with `oxygen tables hubspot lists`.")
102
+ .option("--object <object>", "HubSpot object: contacts or companies. Required without --list; defaults to contacts for a saved list.")
103
+ .option("--destination <destination>", "Import into table (default) or records.")
104
+ .option("--connection <id>", "HubSpot connection id; defaults to the first connected account.")
105
+ .option("--sample", "Read up to 50 real records during the dry-run preview; uses HubSpot API quota and makes no writes.")
106
+ .option("--dnc-list <id>", "HubSpot contact-list id to suppress into do-not-contact before importing.")
107
+ .option("--company-dnc-list <id>", "Optional HubSpot company-list id to suppress organization-wide.")
108
+ .option("--no-dnc", "Import without suppressing anything. Required if you pass no --dnc-list, so an unsuppressed import is never accidental.")
109
+ .option("--name <table>", "Display name for the new table.")
110
+ .option("--project <project>", "Project id or slug. Defaults to General.")
111
+ .option("--contact-linkedin-property <name>", "Optional contact property holding a public LinkedIn /in/ URL.")
112
+ .option("--contact-company-domain-property <name>", "Optional contact property copied to company_domain.")
113
+ .option("--contact-company-linkedin-property <name>", "Optional contact property copied to company_linkedin_url.")
114
+ .option("--company-domain-property <name>", "Company domain property (default domain).", "domain")
115
+ .option("--company-linkedin-property <name>", "Optional company LinkedIn URL property.")
116
+ .option("--contact-properties <list>", "Comma-separated extra HubSpot contact properties to hydrate (max 50).")
117
+ .option("--max-records-per-list <n>", "Optional record limit; omit to import all selected records.")
118
+ .option("--max-credits <n>", "Hard credits cap per delivery.", "10")
119
+ .option("--once", "Import once instead of refreshing on a schedule.")
120
+ .option("--cron <expression>", "Refresh schedule; default every 15 minutes.")
121
+ .option("--timezone <iana>", "Schedule timezone; default UTC.")
122
+ .option("--reviewed-fingerprint <hash>", "Exact dry-run fingerprint, required for live.")
123
+ .option("--idempotency-key <key>", "Optional first-run idempotency key.")
124
+ .option("--live", "Create the import and queue the first durable run.")
125
+ .option("--approved", "Approve the reviewed reads, DNC writes, and destination upserts.")
126
+ .option("--json", "Print a JSON envelope.")
127
+ .action(async (options) => {
128
+ await handleAsyncAction("tables hubspot import", options, () => {
129
+ const list = readOption(options.list);
130
+ const object = readOption(options.object);
131
+ const destination = readOption(options.destination);
132
+ if (!list && !object)
133
+ throw new OxygenError("invalid_request", "Pass --list or --object contacts|companies to select what to import.", { exitCode: 1 });
134
+ if (object && object !== "contacts" && object !== "companies")
135
+ throw new OxygenError("invalid_request", "--object must be contacts or companies.", { exitCode: 1 });
136
+ if (destination && destination !== "table" && destination !== "records")
137
+ throw new OxygenError("invalid_request", "--destination must be table or records.", { exitCode: 1 });
138
+ const dncList = readOption(options.dncList);
139
+ const companyDncList = readOption(options.companyDncList);
140
+ // Commander maps `--no-dnc` to `dnc: false`; the default is
141
+ // `true`, so an unset flag is indistinguishable from an opt-in.
142
+ // Requiring one of the two means an import that suppresses
143
+ // nothing is always something the operator typed, which is the
144
+ // same guarantee the web lane gets from its preview fingerprint.
145
+ const optedOut = options.dnc === false;
146
+ if (!dncList && !companyDncList && !optedOut) {
147
+ // invalid_request, not a bare Error: handleAsyncAction reports
148
+ // an uncoded throw as `unexpected_error`, which tells a calling
149
+ // agent that OXYGEN broke when the caller simply omitted a flag.
150
+ throw new OxygenError("invalid_request", "Pass --dnc-list (and/or --company-dnc-list) to suppress a HubSpot list, or --no-dnc to import without suppressing anything. Run `oxygen tables hubspot lists` to see the ids your portal exposes.", { exitCode: 1 });
151
+ }
152
+ if (optedOut && (dncList || companyDncList)) {
153
+ throw new OxygenError("invalid_request", "--no-dnc cannot be combined with --dnc-list or --company-dnc-list.", { exitCode: 1 });
154
+ }
155
+ const maxRecords = readPositiveInt(options.maxRecordsPerList);
156
+ const maxCredits = readPositiveNumber(options.maxCredits);
157
+ if (options.maxRecordsPerList !== undefined && (maxRecords === undefined || !Number.isSafeInteger(maxRecords)))
158
+ throw new OxygenError("invalid_request", "--max-records-per-list must be a positive integer.", { exitCode: 1 });
159
+ if (maxCredits === undefined)
160
+ throw new OxygenError("spend_cap_required", "--max-credits must be positive.", { exitCode: 1 });
161
+ const fingerprint = readOption(options.reviewedFingerprint);
162
+ if (options.live && !fingerprint)
163
+ throw new OxygenError("approval_required", "Live import requires --reviewed-fingerprint from the exact dry-run. Run the same command without --live first.", { exitCode: 1 });
164
+ const contactProperties = readOption(options.contactProperties)
165
+ ?.split(",")
166
+ .map((entry) => entry.trim())
167
+ .filter(Boolean) ?? [];
168
+ return requestOxygen("/api/cli/tables/hubspot-import", {
169
+ method: "POST",
170
+ body: {
171
+ lead_list_id: list ?? null,
172
+ ...(object ? { object } : {}),
173
+ ...(destination ? { destination } : {}),
174
+ ...(readOption(options.connection) ? { connection_id: readOption(options.connection) } : {}),
175
+ ...(options.sample ? { sample: true } : {}),
176
+ dnc_list_id: dncList ?? null,
177
+ company_dnc_list_id: companyDncList ?? null,
178
+ table_name: readOption(options.name) ?? null,
179
+ project: readOption(options.project) ?? null,
180
+ contact_linkedin_property: readOption(options.contactLinkedinProperty) ?? null,
181
+ contact_company_domain_property: readOption(options.contactCompanyDomainProperty) ?? null,
182
+ contact_company_linkedin_property: readOption(options.contactCompanyLinkedinProperty) ?? null,
183
+ company_domain_property: readOption(options.companyDomainProperty) ?? "domain",
184
+ company_linkedin_property: readOption(options.companyLinkedinProperty) ?? null,
185
+ contact_properties: contactProperties,
186
+ max_records_per_list: maxRecords ?? null,
187
+ refresh: options.once ? "once" : "scheduled",
188
+ cron: readOption(options.cron) ?? undefined,
189
+ timezone: readOption(options.timezone) ?? undefined,
190
+ max_credits: maxCredits,
191
+ mode: options.live ? "live" : "dry_run",
192
+ ...(options.approved ? { approved: true } : {}),
193
+ ...(fingerprint ? { reviewed_fingerprint: fingerprint } : {}),
194
+ ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
195
+ },
196
+ });
197
+ });
198
+ })));
199
+ tablesCommand.addCommand(new Command("pipedrive")
200
+ .description("Import a Pipedrive saved people filter into a new Oxygen table, optionally suppressing a Pipedrive do-not-contact filter first. Connect Pipedrive with an API token first: `oxygen integrations connect pipedrive --api-key <token>`.")
201
+ .addCommand(addContractFlags(new Command("filters")
202
+ .description("Read connected Pipedrive saved filters and custom-field definitions; no people imported. Four BYOK fees may apply. Use --approved with --max-credits to authorize this read; refusal returns the required cap without provider calls."), PIPEDRIVE_CATALOG_QUERY_CONTRACT, {
203
+ query: { valueName: "<text>" },
204
+ approved: { noNegate: true },
205
+ max_credits: { valueName: "<n>" },
206
+ })
207
+ .option("--json", "Print a JSON envelope.")
208
+ .action(async (options) => {
209
+ await handleAsyncAction("tables pipedrive filters", options, () => {
210
+ const params = pipedriveCatalogQueryParams(readContractFlags(options, PIPEDRIVE_CATALOG_QUERY_CONTRACT, { approved: { noNegate: true } }));
211
+ const suffix = params.toString();
212
+ return requestOxygen(`/api/cli/tables/pipedrive-import${suffix ? `?${suffix}` : ""}`);
213
+ });
214
+ }))
215
+ .addCommand(new Command("import")
216
+ .description("Preview or run a Pipedrive saved people filter into a NEW table. Any do-not-contact filter you name is synchronized first and no row lands unless it fully succeeds. Nothing is ever enrolled or sent. Live requires the exact dry-run fingerprint and approval.")
217
+ .requiredOption("--filter <id>", "Pipedrive people-filter id whose persons become rows. Run `oxygen tables pipedrive filters` to see the ids your account exposes.")
218
+ .option("--dnc-filter <id>", "Pipedrive people-filter id to suppress into do-not-contact before importing (a filter on Marketing status = Unsubscribed is the usual choice).")
219
+ .option("--org-dnc-filter <id>", "Optional Pipedrive organization-filter id to suppress organization-wide.")
220
+ .option("--no-dnc", "Import without suppressing anything. Required if you pass no --dnc-filter, so an unsuppressed import is never accidental.")
221
+ .option("--name <table>", "Display name for the new table.")
222
+ .option("--project <project>", "Project id or slug. Defaults to General.")
223
+ .option("--person-linkedin-field <key>", "Optional person field key holding a public LinkedIn /in/ URL (custom keys are 40-character hex; see `tables pipedrive filters`).")
224
+ .option("--person-title-field <key>", "Optional person field key copied to title. Pipedrive has no native job-title field.")
225
+ .option("--org-domain-field <key>", "Optional organization field key holding the website or domain. Pipedrive has no native domain field; never inferred from email.")
226
+ .option("--org-linkedin-field <key>", "Optional organization field key holding its LinkedIn company page.")
227
+ .option("--person-fields <list>", "Comma-separated extra Pipedrive person field keys to hydrate into pipedrive_fields (max 15).")
228
+ .option("--no-hydrate-organizations", "Skip reading each person's organization; company columns stay empty.")
229
+ .option("--max-records-per-filter <n>", "Complete-snapshot cap (1-5000), enforced while the filter is read.", "5000")
230
+ .option("--max-credits <n>", "Hard credits cap per delivery.", "10")
231
+ .option("--once", "Import once instead of refreshing on a schedule.")
232
+ .option("--cron <expression>", "Refresh schedule; default every 15 minutes.")
233
+ .option("--timezone <iana>", "Schedule timezone; default UTC.")
234
+ .option("--reviewed-fingerprint <hash>", "Exact dry-run fingerprint, required for live.")
235
+ .option("--idempotency-key <key>", "Optional first-run idempotency key.")
236
+ .option("--live", "Create the table and queue the first durable run.")
237
+ .option("--approved", "Approve the reviewed bounded reads, DNC writes, and table upserts.")
238
+ .option("--json", "Print a JSON envelope.")
239
+ .action(async (options) => {
240
+ await handleAsyncAction("tables pipedrive import", options, () => {
241
+ const dncFilter = readOption(options.dncFilter);
242
+ const orgDncFilter = readOption(options.orgDncFilter);
243
+ // Same affirmative contract as `tables hubspot import`: an import
244
+ // that suppresses nothing is always something the operator typed.
245
+ const optedOut = options.dnc === false;
246
+ if (!dncFilter && !orgDncFilter && !optedOut) {
247
+ throw new OxygenError("invalid_request", "Pass --dnc-filter (and/or --org-dnc-filter) to suppress a Pipedrive filter, or --no-dnc to import without suppressing anything. Run `oxygen tables pipedrive filters` to see the ids your account exposes.", { exitCode: 1 });
248
+ }
249
+ if (optedOut && (dncFilter || orgDncFilter)) {
250
+ throw new OxygenError("invalid_request", "--no-dnc cannot be combined with --dnc-filter or --org-dnc-filter.", { exitCode: 1 });
251
+ }
252
+ const maxRecords = readPositiveInt(options.maxRecordsPerFilter);
253
+ const maxCredits = readPositiveNumber(options.maxCredits);
254
+ if (maxRecords === undefined || maxRecords > 5000)
255
+ throw new OxygenError("invalid_request", "--max-records-per-filter must be between 1 and 5000.", { exitCode: 1 });
256
+ if (maxCredits === undefined)
257
+ throw new OxygenError("spend_cap_required", "--max-credits must be positive.", { exitCode: 1 });
258
+ const fingerprint = readOption(options.reviewedFingerprint);
259
+ if (options.live && !fingerprint)
260
+ throw new OxygenError("approval_required", "Live import requires --reviewed-fingerprint from the exact dry-run. Run the same command without --live first.", { exitCode: 1 });
261
+ const personFields = readOption(options.personFields)
262
+ ?.split(",")
263
+ .map((entry) => entry.trim())
264
+ .filter(Boolean) ?? [];
265
+ const hydrateOrganizations = options.hydrateOrganizations !== false;
266
+ return requestOxygen("/api/cli/tables/pipedrive-import", {
267
+ method: "POST",
268
+ body: {
269
+ filter_id: readOption(options.filter),
270
+ dnc_filter_id: dncFilter ?? null,
271
+ org_dnc_filter_id: orgDncFilter ?? null,
272
+ table_name: readOption(options.name) ?? null,
273
+ project: readOption(options.project) ?? null,
274
+ person_linkedin_field: readOption(options.personLinkedinField) ?? null,
275
+ person_title_field: readOption(options.personTitleField) ?? null,
276
+ org_domain_field: readOption(options.orgDomainField) ?? null,
277
+ org_linkedin_field: readOption(options.orgLinkedinField) ?? null,
278
+ person_fields: personFields,
279
+ hydrate_organizations: hydrateOrganizations,
280
+ max_records_per_filter: maxRecords,
281
+ refresh: options.once ? "once" : "scheduled",
282
+ cron: readOption(options.cron) ?? undefined,
283
+ timezone: readOption(options.timezone) ?? undefined,
284
+ max_credits: maxCredits,
285
+ mode: options.live ? "live" : "dry_run",
286
+ ...(options.approved ? { approved: true } : {}),
287
+ ...(fingerprint ? { reviewed_fingerprint: fingerprint } : {}),
288
+ ...(readOption(options.idempotencyKey) ? { idempotency_key: readOption(options.idempotencyKey) } : {}),
289
+ },
290
+ });
291
+ });
292
+ })));
293
+ }
@@ -0,0 +1,2 @@
1
+ import type { TablesRegistrationContext } from "./registration-context.js";
2
+ export declare function registerFiles({ tablesCommand, handleAsyncAction, binaryName }: TablesRegistrationContext): void;
@@ -0,0 +1,72 @@
1
+ import { emitCliFailure } from "../../cli-output.js";
2
+ import { Command } from "commander";
3
+ import { IMPORT_FILE_LIMIT_HELP, LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD, formatJsonBodyLimit } from "./import-values.js";
4
+ import { importRows } from "./imports.js";
5
+ import { exportRows, exportTableBundle } from "./bundle-export.js";
6
+ import { importTableBundle } from "./bundle-import.js";
7
+ export function registerFiles({ tablesCommand, handleAsyncAction, binaryName }) {
8
+ tablesCommand.addCommand(new Command("import")
9
+ .description(`Import JSON, JSONL, CSV, or XLSX rows into a workspace table. The table write is free — 0 Oxygen credits. Files over ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows load durably in the background: the command returns a queued envelope and rows keep landing after it exits, so wait with the printed 'table-ingestions wait <id>' before reading row counts. ${IMPORT_FILE_LIMIT_HELP}`)
10
+ .argument("[table]", "Table id or slug. Omit when using --create.")
11
+ .option("--file <path>", "Input file path. Pass this or --url.")
12
+ .option("--url <https-link>", "Public link to fetch instead of a local file: a CSV, TSV, JSON, JSONL or XLSX file, or a Google Sheet shared with anyone-with-the-link or published to the web. Fetched server-side, up to 10 MB, 0 credits.")
13
+ .option("--preview", "With --url: fetch and show the columns, a row sample and the row count without creating or writing anything.")
14
+ .option("--format <format>", "json, jsonl, csv, or xlsx. Defaults from file extension.")
15
+ .option("--sheet <name>", "Worksheet name for XLSX imports. Defaults to the first sheet.")
16
+ .option("--create <name>", "Create a new table with columns inferred from the file before importing.")
17
+ .option("--project <project>", "Project id or slug for --create. Defaults to General.")
18
+ .option("--upsert-key <key>", "Column key used to upsert instead of inserting.")
19
+ .option("--skip-blank-cells", "Leave a row's existing value where the file's cell is blank instead of clearing it, so a partial update needs one file.")
20
+ .option("--batch-size <n>", `Requested rows per import chunk, not a limit on the file. Defaults to 500; values up to 5000 are accepted, but Oxygen splits writes to at most 500 rows (and smaller for wide rows, about ${formatJsonBodyLimit()} of JSON per request) so they fit request and worker lease budgets.`)
21
+ .option("--background", "Enqueue durable import chunks for the background worker.")
22
+ .option("--sync", `Force foreground import even for files above ${LARGE_IMPORT_BACKGROUND_ROW_THRESHOLD} rows. That threshold picks foreground vs background; it does not cap how many rows a file may hold.`)
23
+ .option("--max-concurrency <n>", "Maximum concurrent import chunks for background mode. Defaults to 5.")
24
+ .option("--json", "Print a JSON envelope.")
25
+ .action(async (table, options) => {
26
+ await handleAsyncAction("tables import", options, () => importRows(table, options));
27
+ }));
28
+ tablesCommand.addCommand(new Command("export")
29
+ .description("Export workspace table rows as JSON, JSONL, CSV, or a human-readable table. Rows only, capped at 1000; to copy a whole table including its columns and every row, use `tables export-bundle`.")
30
+ .argument("<table>", "Table id or slug.")
31
+ .option("--format <format>", "json, jsonl, csv, or table. Defaults to json. Use table for a typed, human-readable rendering with thousands grouping.")
32
+ .option("--output <path>", "Write export content to a file.")
33
+ .option("--limit <n>", "Maximum rows to export. Defaults to 100; hard cap is 1000.")
34
+ .option("--json", "Print a JSON envelope.")
35
+ .action(async (table, options) => {
36
+ if (!options.json && !options.output && options.format?.trim().toLowerCase() === "table") {
37
+ // The human-readable table itself, not a JSON object holding it as one
38
+ // escaped string (blind user evals, 2026-09-30). --json keeps the envelope.
39
+ try {
40
+ const { content } = await exportRows(table, options);
41
+ const text = typeof content === "string" ? content : "";
42
+ process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
43
+ }
44
+ catch (error) {
45
+ emitCliFailure("tables export", error);
46
+ }
47
+ return;
48
+ }
49
+ await handleAsyncAction("tables export", options, () => exportRows(table, options));
50
+ }));
51
+ tablesCommand.addCommand(new Command("export-bundle")
52
+ .description("Export a workspace table as a portable bundle (schema + every row) for cross-environment / cross-org copies. Effect: reads the table and writes only a local file — 0 credits, nothing changes in the workspace. With --output the bundle streams to the file one row per line, so a table of any size exports; without it the whole bundle prints as one JSON document, which only fits a small table.")
53
+ .argument("<table>", "Table id or slug to export.")
54
+ .option("--output <path>", "Write the bundle to this file as line-delimited JSON: a header line with the table and columns, then one {\"row\":{…}} line per row carrying every column value plus the source _row_id, _created_at and _updated_at (import-bundle drops those three), then a {\"totals\":{…}} trailer. Streams, so memory stays flat at any row count. Defaults to stdout as a single JSON document.")
55
+ .option("--page-size <n>", "Rows per cursor-paginated request. Defaults to 500; hard cap is 1000.")
56
+ .option("--json", "Print a JSON envelope (omit row payload — use --output to keep the rows).")
57
+ .action(async (table, options) => {
58
+ await handleAsyncAction("tables export-bundle", options, () => exportTableBundle(table, options));
59
+ }));
60
+ tablesCommand.addCommand(new Command("import-bundle")
61
+ .description("Recreate a workspace table from an export-bundle file in this org. Effect: creates one new table in this workspace and inserts every bundled row — 0 credits, no provider calls, nothing runs. Reads both bundle formats and streams a line-delimited bundle row by row, so a multi-GB file imports without loading into memory. Restores columns (incl. enrichment/tool definitions). Pass --key to make the import idempotent (re-runnable), and --into to resume a failed import into the table it already created.")
62
+ .requiredOption("--file <path>", "Bundle JSON file produced by `tables export-bundle`.")
63
+ .option("--name <name>", "Override the table display name. Defaults to the bundle's table name. Ignored with --into.")
64
+ .option("--project <project>", "Project id or slug for the new table. Defaults to General. Ignored with --into.")
65
+ .option("--into <table>", "Resume into an existing table (id or slug) instead of creating a new one. Requires --key. Use the table id printed by the failed run.")
66
+ .option("--key <column>", "Upsert key column. When set, rows are matched by this key instead of blindly inserted, so re-running the import is idempotent (no duplicates). Required with --into.")
67
+ .option("--batch-size <n>", "Requested rows per write group. Defaults to 500; values up to 5000 are accepted and split into safe writes of at most 500 rows.")
68
+ .option("--json", "Print a JSON envelope.")
69
+ .action(async (options) => {
70
+ await handleAsyncAction("tables import-bundle", options, () => importTableBundle(options, binaryName));
71
+ }));
72
+ }
@@ -0,0 +1,2 @@
1
+ import type { TablesRegistrationContext } from "./registration-context.js";
2
+ export declare function registerFollowers({ tablesCommand, handleAsyncAction }: TablesRegistrationContext): void;
@@ -0,0 +1,73 @@
1
+ import { readPositiveNumber } from "../../command-support.js";
2
+ import { requestOxygen } from "../../http-client.js";
3
+ import { readOption } from "../../util.js";
4
+ import { OxygenError } from "@oxygen/shared/cli-result";
5
+ export function registerFollowers({ tablesCommand, handleAsyncAction }) {
6
+ const followersCommand = tablesCommand.command("followers")
7
+ .description("Order a LinkedIn company page's followers as a table of leads — a competitor's audience. `check` is free and quotes the order; fee-positive own-key previews leave the follower count unknown. `order` places it. The order is fulfilled by ScrapeLi, so the table is created immediately and its rows land later; there is no published delivery time.");
8
+ followersCommand.command("check")
9
+ .description("Free order preview. Managed quotes the follower cost; your own key quotes placement and later check fees, with a total ceiling you choose. No spend or table; `orderable: false` means the order is unavailable.")
10
+ .requiredOption("--company <url>", "LinkedIn company page URL, e.g. https://www.linkedin.com/company/hubspot.")
11
+ .option("--records <n>", "How many followers to price. Minimum 100; defaults to 1000.")
12
+ .option("--package <package>", "Basic (default): profile, job title, location, employer. Advanced (10x the credits): also a verified work email and region.")
13
+ .option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
14
+ .option("--json", "Print a JSON envelope.")
15
+ .action(async (options) => {
16
+ await handleAsyncAction("tables followers check", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
17
+ method: "POST",
18
+ body: {
19
+ action: "preview",
20
+ company_url: readOption(options.company),
21
+ ...(options.records !== undefined ? { records: readPositiveNumber(options.records) } : {}),
22
+ ...(readOption(options.package) ? { package: readOption(options.package) } : {}),
23
+ ...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
24
+ },
25
+ }));
26
+ });
27
+ followersCommand.command("order")
28
+ .description("Place the follower order after checking it. Requires --approved and --max-credits: managed uses the quoted order cost; own-key uses a TOTAL placement/recovery platform-fee ceiling. A ceiling equal to placement cannot recover rows. Budget exhaustion lands no partial rows; refetched pages consume the same ceiling. Creates the Table now; rows arrive later.")
29
+ .requiredOption("--approved", "Approve this purchase. Required, as on every command that buys data.")
30
+ .requiredOption("--company <url>", "LinkedIn company page URL whose followers to order.")
31
+ .option("--max-credits <credits>", "Approved order ceiling. On your key, TOTAL platform fees for placement and later checks; exhaustion stops recovery. At least the quoted placement fee. ScrapeLi charges separately.")
32
+ .requiredOption("--request-id <uuid>", "One stable UUID and identical order payload for retries. A new ID can buy the audience again.")
33
+ .option("--records <n>", "How many followers to buy. Minimum 100; defaults to 1000.")
34
+ .option("--package <package>", "Basic (default) or Advanced (10x the credits; adds a verified work email and region).")
35
+ .option("--name <name>", "Table name. Defaults to \"<Company> Followers\".")
36
+ .option("--project <project>", "Project id or slug; defaults to General.")
37
+ .option("--credential-mode <mode>", "managed (default): Oxygen credits. user_api_key: your saved ScrapeLi key, billed by ScrapeLi, plus Oxygen placement/recovery platform fees.")
38
+ .option("--json", "Print a JSON envelope.")
39
+ .action(async (options) => {
40
+ await handleAsyncAction("tables followers order", options, () => requestOxygen("/api/cli/tables/linkedin-company-followers", {
41
+ method: "POST",
42
+ body: {
43
+ action: "create",
44
+ approved: true,
45
+ company_url: readOption(options.company),
46
+ ...(options.records !== undefined ? { records: readPositiveNumber(options.records) } : {}),
47
+ ...(readOption(options.package) ? { package: readOption(options.package) } : {}),
48
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
49
+ ...(readOption(options.project) ? { project: readOption(options.project) } : {}),
50
+ ...(options.maxCredits !== undefined ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
51
+ ...(readOption(options.requestId) ? { request_id: readOption(options.requestId) } : {}),
52
+ ...(readOption(options.credentialMode) ? { credential_mode: readOption(options.credentialMode) } : {}),
53
+ },
54
+ }));
55
+ });
56
+ followersCommand.command("status")
57
+ .description("Read a fee-budgeted own-key follower order's total fee ceiling, allocated attempts, recorded fees and recovery outcome by its original request ID, including interrupted setup. Optional --table-id verifies the saved Table binding. Recorded fees may settle later. Makes no provider call.")
58
+ .requiredOption("--request-id <uuid>", "Original order request ID.")
59
+ .option("--table-id <id>", "Optional saved Table ID; verifies the order belongs to that Table.")
60
+ .option("--json", "Print a JSON envelope.")
61
+ .action(async (options) => {
62
+ await handleAsyncAction("tables followers status", options, () => {
63
+ const requestId = readOption(options.requestId);
64
+ if (!requestId)
65
+ throw new OxygenError("invalid_input", "--request-id must be the original nonblank order request ID.", { exitCode: 2 });
66
+ const query = new URLSearchParams({ request_id: requestId });
67
+ const tableId = readOption(options.tableId);
68
+ if (tableId)
69
+ query.set("table_id", tableId);
70
+ return requestOxygen(`/api/cli/tables/linkedin-company-followers?${query}`, { method: "GET" });
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,2 @@
1
+ import type { TablesRegistrationContext } from "./registration-context.js";
2
+ export declare function registerLifecycle({ tablesCommand, handleAsyncAction }: TablesRegistrationContext): void;
@@ -0,0 +1,114 @@
1
+ import { readCsvOption, splitCommaList } from "../../command-support.js";
2
+ import { requestOxygen } from "../../http-client.js";
3
+ import { readOption } from "../../util.js";
4
+ import { OxygenError } from "@oxygen/shared/cli-result";
5
+ import { Command } from "commander";
6
+ export function registerLifecycle({ tablesCommand, handleAsyncAction }) {
7
+ tablesCommand.addCommand(new Command("rename")
8
+ .description("Rename a workspace table display name and slug.")
9
+ .argument("<table>", "Table id or slug.")
10
+ .requiredOption("--name <name>", "New table display name.")
11
+ .option("--json", "Print a JSON envelope.")
12
+ .action(async (table, options) => {
13
+ await handleAsyncAction("tables rename", options, () => requestOxygen("/api/cli/tables/rename", {
14
+ method: "POST",
15
+ body: { table, name: options.name },
16
+ }));
17
+ }));
18
+ tablesCommand.addCommand(new Command("tag")
19
+ .description("Replace a table's workspace tags (Tags primitive; \"\" clears). Tags link the table to sequences, wiki pages, and workflows carrying the same tag — browse with `oxygen tags get <tag>`.")
20
+ .argument("<table>", "Table id or slug.")
21
+ .requiredOption("--tags <csv>", "Comma-separated tags; \"\" clears them.")
22
+ .option("--json", "Print a JSON envelope.")
23
+ .action(async (table, options) => {
24
+ await handleAsyncAction("tables tag", options, () => requestOxygen("/api/cli/tables/tags", {
25
+ method: "POST",
26
+ body: { table, tags: readCsvOption(options.tags) },
27
+ }));
28
+ }));
29
+ tablesCommand.addCommand(new Command("archive")
30
+ .description("Archive a workspace table without dropping physical data. Hides it indefinitely with no purge timer; for cleanup with a restorable deadline use `tables delete`.")
31
+ .argument("<table>", "Table id or slug.")
32
+ .option("--json", "Print a JSON envelope.")
33
+ .action(async (table, options) => {
34
+ await handleAsyncAction("tables archive", options, () => requestOxygen(`/api/cli/tables/${encodeURIComponent(table)}`, {
35
+ method: "DELETE",
36
+ }));
37
+ }));
38
+ tablesCommand.addCommand(new Command("delete")
39
+ .description("Delete workspace tables: archives now and schedules the physical purge (default 14 days; restorable via `tables restore` until then). --now purges immediately and permanently — it previews and refuses without --yes. A table with a large history answers `purging` and finishes in the background. Free — 0 Oxygen credits.")
40
+ .argument("<tables...>", "Table ids or slugs.")
41
+ .option("--now", "Purge immediately instead of scheduling. Irreversible; single table only. A large table finishes in the background.")
42
+ .option("--yes", "Approve the immediate purge previewed by --now.")
43
+ .option("--purge-after <days>", "Days until the scheduled purge runs. Defaults to 14; 0 purges on the next background sweep (sweeps visit each workspace every few minutes).")
44
+ .option("--reason <text>", "Recorded with the delete for the audit trail.")
45
+ .option("--json", "Print a JSON envelope.")
46
+ .action(async (tables, options) => {
47
+ await handleAsyncAction("tables delete", options, () => {
48
+ const purgeAfter = readOption(options.purgeAfter) ?? undefined;
49
+ const purgeAfterDays = purgeAfter === undefined ? undefined : Number.parseInt(purgeAfter, 10);
50
+ if (purgeAfterDays !== undefined && (!Number.isFinite(purgeAfterDays) || purgeAfterDays < 0)) {
51
+ throw new OxygenError("invalid_request", "--purge-after must be a non-negative number of days.", {
52
+ exitCode: 2,
53
+ });
54
+ }
55
+ return requestOxygen("/api/cli/tables/delete", {
56
+ method: "POST",
57
+ body: {
58
+ ...(tables.length === 1 ? { table: tables[0] } : { tables }),
59
+ ...(options.now ? { now: true } : {}),
60
+ ...(options.yes ? { approved: true } : {}),
61
+ ...(purgeAfterDays !== undefined ? { purge_after_days: purgeAfterDays } : {}),
62
+ ...(readOption(options.reason) ? { reason: readOption(options.reason) } : {}),
63
+ },
64
+ });
65
+ });
66
+ }));
67
+ tablesCommand.addCommand(new Command("restore")
68
+ .description("Restore an archived or delete-scheduled table: cancels the purge timer and reclaims its slug. A table whose purge has begun (lifecycle.purgeStartedAt) can no longer be restored.")
69
+ .argument("[table]", "Table id or slug (the pre-delete slug works).")
70
+ .option("--tables <refs>", "Restore a comma-separated batch; alternative to the table argument.")
71
+ .option("--json", "Print a JSON envelope.")
72
+ .action(async (table, options) => {
73
+ await handleAsyncAction("tables restore", options, () => {
74
+ const refs = splitCommaList(options.tables);
75
+ if ((!table && refs.length === 0) || (table && refs.length > 0))
76
+ throw new OxygenError("invalid_selection", "Pass one table argument or --tables, not both.", { exitCode: 2 });
77
+ return requestOxygen("/api/cli/tables/restore", {
78
+ method: "POST",
79
+ body: { ...(options.tables !== undefined ? { tables: splitCommaList(options.tables) } : { table }) },
80
+ });
81
+ });
82
+ }));
83
+ tablesCommand.addCommand(new Command("move")
84
+ .description("Move one or more workspace tables into a project (folder) in one call. Rows, columns, runs, and provenance are preserved. Free; reverse it by moving the table back.")
85
+ .argument("<tables...>", "Table ids or slugs; several move together, each reported separately.")
86
+ .requiredOption("--project <project>", "Destination project id or slug (see `oxygen projects list`).")
87
+ .option("--json", "Print a JSON envelope.")
88
+ .action(async (tables, options) => {
89
+ await handleAsyncAction("tables move", options, () => requestOxygen("/api/cli/tables/move", {
90
+ method: "POST",
91
+ body: {
92
+ ...(tables.length === 1 ? { table: tables[0] } : { tables }),
93
+ project: options.project,
94
+ },
95
+ }));
96
+ }));
97
+ tablesCommand.addCommand(new Command("recover-pending")
98
+ .description("Recover existing BetterContact email or phone requests and inspect terminal verification blockers; no new orders.")
99
+ .argument("<table>", "Table id or slug.")
100
+ .option("--max-items <n>", "Max pending cache rows to poll in this run. Defaults to 1000.")
101
+ .option("--json", "Print a JSON envelope.")
102
+ .action(async (table, options) => {
103
+ await handleAsyncAction("tables recover-pending", options, () => {
104
+ const maxItems = options.maxItems ? Number.parseInt(options.maxItems, 10) : undefined;
105
+ return requestOxygen("/api/cli/tables/recover-pending", {
106
+ method: "POST",
107
+ body: {
108
+ table,
109
+ ...(Number.isFinite(maxItems) ? { max_items: maxItems } : {}),
110
+ },
111
+ });
112
+ });
113
+ }));
114
+ }
@@ -0,0 +1,2 @@
1
+ import type { TablesRegistrationContext } from "./registration-context.js";
2
+ export declare function registerReads({ tablesCommand, handleAsyncAction }: TablesRegistrationContext): void;