@oxygen-agent/cli 1.1014.135 → 1.1014.618

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 (292) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +27 -19
  3. package/dist/cli-output.d.ts +12 -0
  4. package/dist/cli-output.js +395 -0
  5. package/dist/cli-paths.d.ts +1 -0
  6. package/dist/cli-paths.js +3 -0
  7. package/dist/cli-profile-context.d.ts +3 -0
  8. package/dist/cli-profile-context.js +13 -0
  9. package/dist/command-manifest.d.ts +15 -0
  10. package/dist/command-manifest.js +23 -1
  11. package/dist/command-support.d.ts +438 -0
  12. package/dist/command-support.js +1610 -0
  13. package/dist/commands/action-column.d.ts +3 -0
  14. package/dist/commands/action-column.js +43 -0
  15. package/dist/commands/activation.d.ts +3 -0
  16. package/dist/commands/activation.js +24 -0
  17. package/dist/commands/admin.d.ts +3 -0
  18. package/dist/commands/admin.js +347 -0
  19. package/dist/commands/ads.d.ts +3 -0
  20. package/dist/commands/ads.js +62 -0
  21. package/dist/commands/agent.d.ts +3 -0
  22. package/dist/commands/agent.js +274 -0
  23. package/dist/commands/api-keys.d.ts +3 -0
  24. package/dist/commands/api-keys.js +77 -0
  25. package/dist/commands/approvals.d.ts +3 -0
  26. package/dist/commands/approvals.js +518 -0
  27. package/dist/commands/auth.d.ts +3 -0
  28. package/dist/commands/auth.js +230 -0
  29. package/dist/commands/billing.d.ts +3 -0
  30. package/dist/commands/billing.js +357 -0
  31. package/dist/commands/blueprints.d.ts +3 -0
  32. package/dist/commands/blueprints.js +312 -0
  33. package/dist/commands/budget.d.ts +3 -0
  34. package/dist/commands/budget.js +55 -0
  35. package/dist/commands/callables.d.ts +3 -0
  36. package/dist/commands/callables.js +111 -0
  37. package/dist/commands/capabilities.d.ts +3 -0
  38. package/dist/commands/capabilities.js +44 -0
  39. package/dist/commands/cells.d.ts +3 -0
  40. package/dist/commands/cells.js +94 -0
  41. package/dist/commands/columns.d.ts +3 -0
  42. package/dist/commands/columns.js +1628 -0
  43. package/dist/commands/commands.d.ts +3 -0
  44. package/dist/commands/commands.js +45 -0
  45. package/dist/commands/comments.d.ts +3 -0
  46. package/dist/commands/comments.js +188 -0
  47. package/dist/commands/companies.d.ts +3 -0
  48. package/dist/commands/companies.js +312 -0
  49. package/dist/commands/connections.d.ts +3 -0
  50. package/dist/commands/connections.js +39 -0
  51. package/dist/commands/context.d.ts +3 -0
  52. package/dist/commands/context.js +170 -0
  53. package/dist/commands/copilot.d.ts +3 -0
  54. package/dist/commands/copilot.js +565 -0
  55. package/dist/commands/crm.d.ts +3 -0
  56. package/dist/commands/crm.js +1669 -0
  57. package/dist/commands/custom-integrations.d.ts +3 -0
  58. package/dist/commands/custom-integrations.js +140 -0
  59. package/dist/commands/dashboards.d.ts +3 -0
  60. package/dist/commands/dashboards.js +240 -0
  61. package/dist/commands/db.d.ts +3 -0
  62. package/dist/commands/db.js +190 -0
  63. package/dist/commands/deliverability.d.ts +3 -0
  64. package/dist/commands/deliverability.js +90 -0
  65. package/dist/commands/directory.d.ts +3 -0
  66. package/dist/commands/directory.js +101 -0
  67. package/dist/commands/domains.d.ts +3 -0
  68. package/dist/commands/domains.js +604 -0
  69. package/dist/commands/egress.d.ts +3 -0
  70. package/dist/commands/egress.js +130 -0
  71. package/dist/commands/email.d.ts +3 -0
  72. package/dist/commands/email.js +49 -0
  73. package/dist/commands/engagement.d.ts +3 -0
  74. package/dist/commands/engagement.js +189 -0
  75. package/dist/commands/enrich-column.d.ts +3 -0
  76. package/dist/commands/enrich-column.js +149 -0
  77. package/dist/commands/enrichment.d.ts +3 -0
  78. package/dist/commands/enrichment.js +49 -0
  79. package/dist/commands/feedback.d.ts +3 -0
  80. package/dist/commands/feedback.js +96 -0
  81. package/dist/commands/feeds.d.ts +3 -0
  82. package/dist/commands/feeds.js +486 -0
  83. package/dist/commands/find.d.ts +3 -0
  84. package/dist/commands/find.js +159 -0
  85. package/dist/commands/followers.d.ts +3 -0
  86. package/dist/commands/followers.js +39 -0
  87. package/dist/commands/formulas.d.ts +3 -0
  88. package/dist/commands/formulas.js +43 -0
  89. package/dist/commands/home.d.ts +3 -0
  90. package/dist/commands/home.js +22 -0
  91. package/dist/commands/inbox.d.ts +3 -0
  92. package/dist/commands/inbox.js +779 -0
  93. package/dist/commands/integrations.d.ts +3 -0
  94. package/dist/commands/integrations.js +290 -0
  95. package/dist/commands/knowledge.d.ts +3 -0
  96. package/dist/commands/knowledge.js +874 -0
  97. package/dist/commands/lead-sourcing.d.ts +3 -0
  98. package/dist/commands/lead-sourcing.js +52 -0
  99. package/dist/commands/limits.d.ts +3 -0
  100. package/dist/commands/limits.js +54 -0
  101. package/dist/commands/linkedin.d.ts +3 -0
  102. package/dist/commands/linkedin.js +275 -0
  103. package/dist/commands/login.d.ts +3 -0
  104. package/dist/commands/login.js +347 -0
  105. package/dist/commands/logout.d.ts +3 -0
  106. package/dist/commands/logout.js +12 -0
  107. package/dist/commands/mailboxes.d.ts +3 -0
  108. package/dist/commands/mailboxes.js +1290 -0
  109. package/dist/commands/managed-inboxes.d.ts +3 -0
  110. package/dist/commands/managed-inboxes.js +361 -0
  111. package/dist/commands/messages.d.ts +3 -0
  112. package/dist/commands/messages.js +78 -0
  113. package/dist/commands/notetaker.d.ts +3 -0
  114. package/dist/commands/notetaker.js +79 -0
  115. package/dist/commands/observability.d.ts +3 -0
  116. package/dist/commands/observability.js +125 -0
  117. package/dist/commands/orgs.d.ts +3 -0
  118. package/dist/commands/orgs.js +237 -0
  119. package/dist/commands/people.d.ts +3 -0
  120. package/dist/commands/people.js +353 -0
  121. package/dist/commands/posts.d.ts +3 -0
  122. package/dist/commands/posts.js +139 -0
  123. package/dist/commands/profiles.d.ts +3 -0
  124. package/dist/commands/profiles.js +250 -0
  125. package/dist/commands/projects.d.ts +3 -0
  126. package/dist/commands/projects.js +94 -0
  127. package/dist/commands/prompts.d.ts +3 -0
  128. package/dist/commands/prompts.js +84 -0
  129. package/dist/commands/publishing.d.ts +3 -0
  130. package/dist/commands/publishing.js +1419 -0
  131. package/dist/commands/recipes.d.ts +3 -0
  132. package/dist/commands/recipes.js +120 -0
  133. package/dist/commands/reviews.d.ts +3 -0
  134. package/dist/commands/reviews.js +75 -0
  135. package/dist/commands/rows.d.ts +3 -0
  136. package/dist/commands/rows.js +27 -0
  137. package/dist/commands/runs.d.ts +3 -0
  138. package/dist/commands/runs.js +37 -0
  139. package/dist/commands/search.d.ts +3 -0
  140. package/dist/commands/search.js +182 -0
  141. package/dist/commands/senders.d.ts +3 -0
  142. package/dist/commands/senders.js +393 -0
  143. package/dist/commands/sequences.d.ts +3 -0
  144. package/dist/commands/sequences.js +2322 -0
  145. package/dist/commands/session.d.ts +3 -0
  146. package/dist/commands/session.js +109 -0
  147. package/dist/commands/signals.d.ts +3 -0
  148. package/dist/commands/signals.js +289 -0
  149. package/dist/commands/signup-leads.d.ts +3 -0
  150. package/dist/commands/signup-leads.js +46 -0
  151. package/dist/commands/skills.d.ts +3 -0
  152. package/dist/commands/skills.js +50 -0
  153. package/dist/commands/sourcing.d.ts +3 -0
  154. package/dist/commands/sourcing.js +27 -0
  155. package/dist/commands/status.d.ts +3 -0
  156. package/dist/commands/status.js +51 -0
  157. package/dist/commands/supabase.d.ts +3 -0
  158. package/dist/commands/supabase.js +128 -0
  159. package/dist/commands/support.d.ts +3 -0
  160. package/dist/commands/support.js +651 -0
  161. package/dist/commands/suppressions.d.ts +3 -0
  162. package/dist/commands/suppressions.js +577 -0
  163. package/dist/commands/table-ingestions.d.ts +3 -0
  164. package/dist/commands/table-ingestions.js +145 -0
  165. package/dist/commands/table-runs.d.ts +3 -0
  166. package/dist/commands/table-runs.js +277 -0
  167. package/dist/commands/tables.d.ts +3 -0
  168. package/dist/commands/tables.js +3492 -0
  169. package/dist/commands/tags.d.ts +3 -0
  170. package/dist/commands/tags.js +147 -0
  171. package/dist/commands/tools.d.ts +3 -0
  172. package/dist/commands/tools.js +246 -0
  173. package/dist/commands/update.d.ts +3 -0
  174. package/dist/commands/update.js +106 -0
  175. package/dist/commands/verify.d.ts +3 -0
  176. package/dist/commands/verify.js +27 -0
  177. package/dist/commands/viewers.d.ts +3 -0
  178. package/dist/commands/viewers.js +72 -0
  179. package/dist/commands/voice.d.ts +3 -0
  180. package/dist/commands/voice.js +351 -0
  181. package/dist/commands/whatsapp.d.ts +3 -0
  182. package/dist/commands/whatsapp.js +226 -0
  183. package/dist/commands/whoami.d.ts +3 -0
  184. package/dist/commands/whoami.js +110 -0
  185. package/dist/commands/worker.d.ts +3 -0
  186. package/dist/commands/worker.js +60 -0
  187. package/dist/commands/workflows.d.ts +3 -0
  188. package/dist/commands/workflows.js +1752 -0
  189. package/dist/commands/workspace.d.ts +3 -0
  190. package/dist/commands/workspace.js +94 -0
  191. package/dist/compact-list-flags.d.ts +7 -0
  192. package/dist/compact-list-flags.js +12 -0
  193. package/dist/crm-support-flags.js +14 -5
  194. package/dist/customer-operation-commands.js +8 -6
  195. package/dist/diagnostic-uploader.d.ts +3 -1
  196. package/dist/diagnostic-uploader.js +29 -8
  197. package/dist/help.d.ts +5 -0
  198. package/dist/help.js +22 -0
  199. package/dist/index.d.ts +98 -1
  200. package/dist/index.js +331 -30719
  201. package/dist/local-module-loader.d.ts +4 -0
  202. package/dist/local-module-loader.js +156 -0
  203. package/dist/visual-commands.js +9 -5
  204. package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
  205. package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
  206. package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
  207. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
  208. package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
  209. package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
  210. package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
  211. package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
  212. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
  213. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
  214. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
  215. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
  216. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
  217. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
  218. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
  219. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
  220. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
  221. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
  222. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
  223. package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
  224. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
  225. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
  226. package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
  227. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
  228. package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
  229. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
  230. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
  231. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
  232. package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
  233. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
  234. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
  235. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
  236. package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
  237. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
  238. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
  239. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
  240. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
  241. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
  242. package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
  243. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
  244. package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
  245. package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
  246. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
  247. package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
  248. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
  249. package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
  250. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
  251. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
  252. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
  253. package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
  254. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
  255. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
  256. package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
  257. package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
  258. package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
  259. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
  260. package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
  261. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
  262. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
  263. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  264. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  265. package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
  266. package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
  267. package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
  268. package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
  269. package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
  270. package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
  271. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
  272. package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
  273. package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
  274. package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
  275. package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
  276. package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
  277. package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
  278. package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
  279. package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
  280. package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
  281. package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
  282. package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
  283. package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
  284. package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
  285. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
  286. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  287. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  288. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
  289. package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
  290. package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
  291. package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
  292. package/package.json +1 -1
@@ -0,0 +1,604 @@
1
+ import { readDetailsNumber } from "../cli-output.js";
2
+ import { readRecordString } from "../cli-values.js";
3
+ import { readCount, readJsonFileValue, readRecord, shellQuote, splitCommaList } from "../command-support.js";
4
+ import { requestOxygen } from "../http-client.js";
5
+ import { resolveCliBinaryName } from "../runtime.js";
6
+ import { isRecord, readOption } from "../util.js";
7
+ import { OxygenError } from "@oxygen/shared/cli-result";
8
+ import { sleep } from "@oxygen/shared/timing";
9
+ import { Command } from "commander";
10
+ import { resolve } from "node:path";
11
+ // Cloudflare domain sync returns partial pages under the request budget; the
12
+ // CLI auto-continues up to this many follow-up POSTs before handing back a
13
+ // still-partial summary.
14
+ const DOMAINS_SYNC_MAX_CONTINUATIONS = 20;
15
+ // Cloudflare's domain-check endpoint accepts at most 20 domains per call.
16
+ const DOMAINS_CHECK_MAX_DOMAINS = 20;
17
+ const DOMAIN_REGISTRATION_WAIT_TIMEOUT_SECONDS = 600;
18
+ const DOMAIN_REGISTRATION_WAIT_INTERVAL_SECONDS = 10;
19
+ // Domain sync is paginated under the server's Cloudflare request budget; the
20
+ // CLI re-POSTs the same body while the server reports a partial sweep with a
21
+ // next page, then prints one aggregated summary.
22
+ async function runDomainsSync(options) {
23
+ const body = options.full ? { full: true } : {};
24
+ let totalSynced = 0;
25
+ let totalPagesFetched = 0;
26
+ let totalRdapResolved = 0;
27
+ let totalDnsChecked = 0;
28
+ let sawRdapResolved = false;
29
+ let sawDnsChecked = false;
30
+ let requestCount = 0;
31
+ for (;;) {
32
+ requestCount += 1;
33
+ const latest = await requestOxygen("/api/cli/domains/sync", {
34
+ method: "POST",
35
+ body,
36
+ });
37
+ // `full` means "restart from page 1" server-side; only the first request may
38
+ // carry it. Continuations must drop it so the server advances the saved
39
+ // cursor instead of re-sweeping the first pages every iteration.
40
+ delete body.full;
41
+ totalSynced += readCount(latest.synced);
42
+ totalPagesFetched += readCount(latest.pages_fetched);
43
+ if (typeof latest.rdap_resolved === "number") {
44
+ sawRdapResolved = true;
45
+ totalRdapResolved += readCount(latest.rdap_resolved);
46
+ }
47
+ if (typeof latest.dns_checked === "number") {
48
+ sawDnsChecked = true;
49
+ totalDnsChecked += readCount(latest.dns_checked);
50
+ }
51
+ const hasNextPage = latest.next_page !== undefined && latest.next_page !== null;
52
+ const shouldContinue = latest.partial === true
53
+ && hasNextPage
54
+ && requestCount <= DOMAINS_SYNC_MAX_CONTINUATIONS;
55
+ if (!shouldContinue) {
56
+ return {
57
+ ...latest,
58
+ synced: totalSynced,
59
+ pages_fetched: totalPagesFetched,
60
+ ...(sawRdapResolved ? { rdap_resolved: totalRdapResolved } : {}),
61
+ ...(sawDnsChecked ? { dns_checked: totalDnsChecked } : {}),
62
+ requests: requestCount,
63
+ };
64
+ }
65
+ // A 429 mid-sync keeps completed pages and reports a pushback window.
66
+ const retryAfterSeconds = readCount(latest.retry_after_seconds);
67
+ if (retryAfterSeconds > 0)
68
+ await sleep(retryAfterSeconds * 1000);
69
+ }
70
+ }
71
+ async function runDomainsBuy(domains, options) {
72
+ const quoteId = readOption(options.quote);
73
+ if (options.approved && !quoteId) {
74
+ throw new Error("--quote <id> is required with --approved. Run the same command without --approved first to get the preview and its quote_id.");
75
+ }
76
+ const billingPath = readOption(options.billing);
77
+ const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
78
+ const redirectUrl = readOption(options.redirectUrl);
79
+ const data = await requestOxygen("/api/cli/domains/buy", {
80
+ method: "POST",
81
+ body: {
82
+ domains,
83
+ ...(options.autoRenew === true ? { auto_renew: true } : {}),
84
+ ...(options.autoRenew === false ? { auto_renew: false } : {}),
85
+ ...(billing ? { billing } : {}),
86
+ ...(redirectUrl ? { redirect_url: redirectUrl } : {}),
87
+ ...(options.privacy === false ? { privacy: false } : {}),
88
+ ...(options.approved ? { approved: true, quote_id: quoteId } : {}),
89
+ ...(options.acceptPremium ? { accept_premium_pricing: true } : {}),
90
+ },
91
+ });
92
+ if (!options.approved) {
93
+ const previewQuoteId = readRecordString(data, "quote_id");
94
+ return previewQuoteId
95
+ ? { ...data, rerun_command: domainsBuyRerunCommand(domains, previewQuoteId, options, data) }
96
+ : data;
97
+ }
98
+ if (!options.wait)
99
+ return data;
100
+ const pendingDomains = Array.isArray(data.results)
101
+ ? data.results
102
+ .filter((entry) => {
103
+ const status = readRecordString(entry, "status");
104
+ return status === "submitting" || status === "in_progress";
105
+ })
106
+ .map((entry) => readRecordString(entry, "domain"))
107
+ .filter((entry) => entry !== null)
108
+ : [];
109
+ if (pendingDomains.length === 0)
110
+ return data;
111
+ const waitResults = [];
112
+ for (const domain of pendingDomains) {
113
+ waitResults.push(await waitForDomainRegistration(domain));
114
+ }
115
+ return { ...data, wait: { waited: pendingDomains.length, results: waitResults } };
116
+ }
117
+ function domainsBuyRerunCommand(domains, quoteId, options, data) {
118
+ // The buy preview returns items and requires_accept_premium_pricing at the
119
+ // TOP level of data (there is no preview wrapper); reading data.preview here
120
+ // drops the premium ack flag and makes a copy-paste of the rerun command 422.
121
+ const items = Array.isArray(data.items) ? data.items : [];
122
+ const needsPremiumAck = options.acceptPremium === true
123
+ || data.requires_accept_premium_pricing === true
124
+ || items.some((item) => readRecordString(item, "tier") === "premium");
125
+ const billingPath = readOption(options.billing)
126
+ ?? (data.registrant_on_file === false ? "registrant.json" : null);
127
+ const flags = [
128
+ "--approved",
129
+ `--quote ${quoteId}`,
130
+ ...(needsPremiumAck ? ["--accept-premium"] : []),
131
+ ...(options.autoRenew === false ? ["--no-auto-renew"] : []),
132
+ ...(billingPath ? [`--billing ${shellQuote(billingPath)}`] : []),
133
+ ...(readOption(options.redirectUrl) ? [`--redirect-url ${readOption(options.redirectUrl)}`] : []),
134
+ ...(options.privacy === false ? ["--no-privacy"] : []),
135
+ ];
136
+ return `${resolveCliBinaryName()} domains buy ${domains.join(" ")} ${flags.join(" ")}`;
137
+ }
138
+ function readDnsDkimOption(value) {
139
+ let records;
140
+ try {
141
+ records = JSON.parse(value);
142
+ }
143
+ catch {
144
+ throw new OxygenError("invalid_request", "--dkim-json must be a JSON array of DKIM records.", { exitCode: 2 });
145
+ }
146
+ if (!Array.isArray(records) || records.some((record) => !isRecord(record) || !["TXT", "CNAME"].includes(String(record.type).toUpperCase()) || !readRecordString(record, "name") || !readRecordString(record, "content"))) {
147
+ throw new OxygenError("invalid_request", "Each --dkim-json record requires type TXT or CNAME, name, and content.", { exitCode: 2 });
148
+ }
149
+ return records;
150
+ }
151
+ function dnsPlanBody(options) {
152
+ const provider = readOption(options.provider);
153
+ if (!provider) {
154
+ throw new Error("--provider <google|microsoft> is required so the correct SPF/MX/DKIM records can be planned.");
155
+ }
156
+ return {
157
+ provider,
158
+ ...(readOption(options.dmarcPolicy) ? { dmarc_policy: readOption(options.dmarcPolicy) } : {}),
159
+ ...(readOption(options.dmarcRua) ? { dmarc_rua: readOption(options.dmarcRua) } : {}),
160
+ ...(readOption(options.trackingHost) ? { tracking_host: readOption(options.trackingHost) } : {}),
161
+ ...(options.autoDkim === false ? { auto_dkim: false } : {}),
162
+ ...(options.dkimJson !== undefined ? { dkim: readDnsDkimOption(options.dkimJson) } : {}),
163
+ };
164
+ }
165
+ // Preview the cold-email DNS plan for a domain (free — no writes). Appends a
166
+ // copy-paste rerun command carrying the plan_hash, mirroring the buy quote flow.
167
+ async function runDomainsDnsPlan(domain, options) {
168
+ const data = await requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/dns/plan`, { method: "POST", body: dnsPlanBody(options) });
169
+ const planHash = readRecordString(data, "plan_hash");
170
+ const provider = readOption(options.provider);
171
+ return planHash && provider
172
+ ? { ...data, rerun_command: [resolveCliBinaryName(), "domains dns apply", shellQuote(domain), "--provider", shellQuote(provider), "--approved --plan", shellQuote(planHash),
173
+ ...(options.dkimJson !== undefined ? ["--dkim-json", shellQuote(options.dkimJson)] : []),
174
+ ...(readOption(options.dmarcPolicy) ? ["--dmarc-policy", shellQuote(options.dmarcPolicy)] : []),
175
+ ...(readOption(options.dmarcRua) ? ["--dmarc-rua", shellQuote(options.dmarcRua)] : []),
176
+ ...(readOption(options.trackingHost) ? ["--tracking-host", shellQuote(options.trackingHost)] : []),
177
+ ...(options.autoDkim === false ? ["--no-auto-dkim"] : []),].join(" ") }
178
+ : data;
179
+ }
180
+ // Apply an approved DNS plan (writes records). Requires --plan <hash> with
181
+ // --approved, mirroring `domains buy --quote`.
182
+ function runDomainsDnsApply(domain, options) {
183
+ if (!options.approved)
184
+ return runDomainsDnsPlan(domain, options);
185
+ const planHash = readOption(options.plan);
186
+ if (options.approved && !planHash) {
187
+ throw new Error("--plan <hash> is required with --approved. Run `domains dns plan` first to get the plan and its hash.");
188
+ }
189
+ return requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/dns/apply`, {
190
+ method: "POST",
191
+ body: {
192
+ ...dnsPlanBody(options),
193
+ ...(options.approved ? { approved: true, plan_hash: planHash } : {}),
194
+ },
195
+ });
196
+ }
197
+ // Staff import of pre-existing managed-account domains. Exactly one of an explicit
198
+ // list or --all; without --yes the server returns a preview and the CLI appends a
199
+ // copy-paste rerun command to execute it.
200
+ async function runDomainsAdopt(domains, options) {
201
+ const hasExplicit = domains.length > 0;
202
+ if (hasExplicit === Boolean(options.all)) {
203
+ throw new Error("Pass exactly one of <domains...> or --all. Use --all to adopt every domain in the managed account, or list specific domains.");
204
+ }
205
+ const organizationId = readOption(options.organizationId);
206
+ const data = await requestOxygen("/api/cli/domains/adopt", {
207
+ method: "POST",
208
+ body: {
209
+ ...(options.all ? { all: true } : {}),
210
+ ...(hasExplicit ? { domains } : {}),
211
+ ...(organizationId ? { organization_id: organizationId } : {}),
212
+ ...(options.yes ? { approved: true } : {}),
213
+ },
214
+ });
215
+ if (options.yes)
216
+ return data;
217
+ return { ...data, rerun_command: domainsAdoptRerunCommand(domains, options) };
218
+ }
219
+ function domainsAdoptRerunCommand(domains, options) {
220
+ const organizationId = readOption(options.organizationId);
221
+ const flags = [
222
+ ...(options.all ? ["--all"] : []),
223
+ ...(organizationId ? [`--organization-id ${organizationId}`] : []),
224
+ "--yes",
225
+ ];
226
+ const args = domains.length > 0 ? ` ${domains.join(" ")}` : "";
227
+ return `${resolveCliBinaryName()} domains adopt${args} ${flags.join(" ")}`;
228
+ }
229
+ // succeeded/failed are terminal; action_required stops the wait too because
230
+ // only the user (in the Cloudflare dashboard) can move it forward.
231
+ function isSettledDomainRegistrationStatus(status) {
232
+ return status === "succeeded"
233
+ || status === "failed"
234
+ || status === "action_required";
235
+ }
236
+ async function waitForDomainRegistration(domain) {
237
+ const startedAt = Date.now();
238
+ const deadline = startedAt + DOMAIN_REGISTRATION_WAIT_TIMEOUT_SECONDS * 1000;
239
+ let polls = 0;
240
+ let lastStatus = null;
241
+ let lastRegistration = null;
242
+ for (;;) {
243
+ let delayMs = DOMAIN_REGISTRATION_WAIT_INTERVAL_SECONDS * 1000;
244
+ try {
245
+ polls += 1;
246
+ const latest = await requestOxygen(`/api/cli/domains/registrations/${encodeURIComponent(domain)}`);
247
+ lastRegistration = readRecord(latest, "registration");
248
+ lastStatus = readRecordString(lastRegistration, "status");
249
+ if (isSettledDomainRegistrationStatus(lastStatus)) {
250
+ if (lastStatus === "action_required") {
251
+ process.stderr.write(`hint: ${domain} needs action in the Cloudflare dashboard before registration can finish.\n`);
252
+ }
253
+ return {
254
+ domain,
255
+ status: lastStatus,
256
+ terminal: true,
257
+ polls,
258
+ elapsedMs: Date.now() - startedAt,
259
+ ...(lastRegistration ? { registration: lastRegistration } : {}),
260
+ ...(readRecordString(latest, "deepLink") ? { deepLink: readRecordString(latest, "deepLink") } : {}),
261
+ };
262
+ }
263
+ }
264
+ catch (error) {
265
+ // Honor the server's rate-limit pushback instead of failing the wait.
266
+ const retryAfterSeconds = error instanceof OxygenError
267
+ ? readDetailsNumber(error.details, "retry_after_seconds")
268
+ : null;
269
+ if (retryAfterSeconds === null)
270
+ throw error;
271
+ delayMs = Math.max(delayMs, retryAfterSeconds * 1000);
272
+ }
273
+ const remainingMs = deadline - Date.now();
274
+ if (remainingMs <= 0) {
275
+ process.stderr.write(`hint: registration for ${domain} is still ${lastStatus ?? "pending"}; the background worker keeps polling — re-run \`${resolveCliBinaryName()} domains registration-status ${domain} --wait\` later.\n`);
276
+ return {
277
+ domain,
278
+ status: lastStatus,
279
+ terminal: false,
280
+ timed_out: true,
281
+ polls,
282
+ elapsedMs: Date.now() - startedAt,
283
+ ...(lastRegistration ? { registration: lastRegistration } : {}),
284
+ };
285
+ }
286
+ await sleep(Math.min(delayMs, remainingMs));
287
+ }
288
+ }
289
+ export function registerDomainsFamily(program, handleAsyncAction) {
290
+ program.addCommand(new Command("domains")
291
+ .description("Cold-email domains: sync zones from your own Cloudflare account (BYOK), inspect age/warmup/DNS health, check availability and pricing, buy domains, and archive or delete domains with their inboxes (OXYGEN-bought domains included). Your own Cloudflare account bills purchases to its payment method; domains registered by OXYGEN cost credits at the vendor price. Preview `domains buy` before approval.")
292
+ .addCommand(new Command("list")
293
+ .description("List the org's cached Cloudflare domains with cold-email metadata (age, mailboxes, warmup, sending volume, DNS health). Each row's `dns` object explains the status (ok, issues, not checked, check failed, partial check, no zone) with a reason and fix path; `managed_domains` lists InboxKit-managed bundle domains (vendor-registered + vendor-DNS'd — manage via `managed-inboxes`), each carrying `internal_billing_status` (ok|past_due) and `last_billed_cycle_key` — a domain the vendor keeps live can still be one OXYGEN could not renew. summary.by_dns_state gives all DNS-state counts in one read. volume.outbound_30d counts outbound email messages in the synced inbox ledger, including mail outside Sequences. Reads the cache only — run `domains sync` to refresh.")
294
+ .option("--status <status>", "Filter by zone status: unknown, initializing, pending, active, moved, or deleted.")
295
+ .option("--registrar <registrar>", "Filter by registrar name.")
296
+ .option("--q <text>", "Filter by domain substring.")
297
+ .option("--dns <state>", "Filter by one derived DNS state: ok, issues, not_checked, check_failed, check_incomplete, or no_zone; summary.by_dns_state gives all state counts.")
298
+ .option("--expiring", "Only non-deleted domains expiring within 60 days.")
299
+ .option("--has-mailboxes <bool>", "true → only domains with sending mailboxes; false → only domains without.")
300
+ .option("--include-archived", "Include archived domains (hidden by default).")
301
+ .option("--tag <tags>", "Comma-separated workspace tags — matches domains carrying ANY of these tags (see `oxygen tags list`).")
302
+ .option("--json", "Print a JSON envelope.")
303
+ .action(async (options) => {
304
+ await handleAsyncAction("domains list", options, () => {
305
+ const params = new URLSearchParams();
306
+ const status = readOption(options.status);
307
+ if (status)
308
+ params.set("status", status);
309
+ const registrar = readOption(options.registrar);
310
+ if (registrar)
311
+ params.set("registrar", registrar);
312
+ const queryText = readOption(options.q);
313
+ if (queryText)
314
+ params.set("q", queryText);
315
+ const dns = readOption(options.dns);
316
+ if (dns)
317
+ params.set("dns", dns);
318
+ if (options.expiring)
319
+ params.set("expiring", "true");
320
+ const hasMailboxes = readOption(options.hasMailboxes);
321
+ if (hasMailboxes)
322
+ params.set("has_mailboxes", hasMailboxes);
323
+ if (options.includeArchived)
324
+ params.set("include_archived", "true");
325
+ const tags = splitCommaList(options.tag);
326
+ if (tags.length > 0)
327
+ params.set("tag", tags.join(","));
328
+ const suffix = params.toString();
329
+ return requestOxygen(`/api/cli/domains${suffix ? `?${suffix}` : ""}`);
330
+ });
331
+ }))
332
+ .addCommand(new Command("archive")
333
+ .description("Hide a domain from the default list — an Oxygen-side declutter flag only. Nothing changes at Cloudflare, DNS and mailbox governance are untouched, syncs never resurrect it, and `domains unarchive` fully reverses it.")
334
+ .argument("<domain>", "Domain name, such as acme.com.")
335
+ .option("--json", "Print a JSON envelope.")
336
+ .action(async (domain, options) => {
337
+ await handleAsyncAction("domains archive", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/archive`, {
338
+ method: "POST",
339
+ body: { archived: true },
340
+ }));
341
+ }))
342
+ .addCommand(new Command("unarchive")
343
+ .description("Restore an archived domain to the default list.")
344
+ .argument("<domain>", "Domain name, such as acme.com.")
345
+ .option("--json", "Print a JSON envelope.")
346
+ .action(async (domain, options) => {
347
+ await handleAsyncAction("domains unarchive", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/archive`, {
348
+ method: "POST",
349
+ body: { archived: false },
350
+ }));
351
+ }))
352
+ .addCommand(new Command("delete")
353
+ .description("Delete domains and every inbox on them: cancels OXYGEN inbox orders; a domain bought through Oxygen on its own (`domains buy`) stops renewing and leaves the workspace, staying registered until expiry; archives your own (DNS and registrar untouched). The preview lists every inbox, each domain's action and the recurring credits that stop; 0 credits, and the current period is not refunded.")
354
+ .argument("<domains...>", "Domain names, space- or comma-separated.")
355
+ .option("--approved", "Execute the fresh preview. Requires --plan-hash and --confirmation.")
356
+ .option("--plan-hash <hash>", "Fresh preview plan_hash.")
357
+ .option("--confirmation <phrase>", "Exact confirmation_phrase returned by the fresh preview.")
358
+ .option("--json", "Print a JSON envelope.")
359
+ .action(async (domainArgs, options) => {
360
+ await handleAsyncAction("domains delete", options, () => {
361
+ const domains = domainArgs.flatMap((value) => splitCommaList(value));
362
+ if (domains.length === 0) {
363
+ throw new OxygenError("invalid_request", "Name at least one domain, such as acme.com. `oxygen domains list` shows your domains.", { exitCode: 2 });
364
+ }
365
+ const planHash = readOption(options.planHash);
366
+ const confirmation = readOption(options.confirmation);
367
+ if (options.approved === true && (!planHash || !confirmation)) {
368
+ throw new OxygenError("invalid_request", "--approved requires --plan-hash and --confirmation from a fresh deletion preview.", { exitCode: 2 });
369
+ }
370
+ return requestOxygen("/api/cli/domains/delete", {
371
+ method: "POST",
372
+ body: {
373
+ domains,
374
+ ...(options.approved === true ? { approved: true } : {}),
375
+ ...(planHash ? { plan_hash: planHash } : {}),
376
+ ...(confirmation ? { confirmation } : {}),
377
+ },
378
+ });
379
+ });
380
+ }))
381
+ .addCommand(new Command("tag")
382
+ .description("Replace a domain's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the domains behind one campaign tag. See `oxygen tags list` for the vocabulary. A later `domains sync` never clobbers them.")
383
+ .argument("<domain>", "Domain name, such as acme.com.")
384
+ .requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
385
+ .option("--json", "Print a JSON envelope.")
386
+ .action(async (domain, options) => {
387
+ await handleAsyncAction("domains tag", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/tags`, {
388
+ method: "POST",
389
+ body: { tags: splitCommaList(options.tags) },
390
+ }));
391
+ }))
392
+ .addCommand(new Command("forwarding")
393
+ .description("Read or change where an Oxygen-managed domain's WEB root forwards visitors (not mail — MX/SPF/DKIM/DMARC are untouched). Only domains Oxygen manages are editable; domains in your own Cloudflare zone are redirected there.")
394
+ .addCommand(new Command("get")
395
+ .description("Show the domain's current web-root forwarding destination. Read-only, 0 credits.")
396
+ .argument("<domain>", "Domain name, such as acme.com.")
397
+ .option("--json", "Print a JSON envelope.")
398
+ .action(async (domain, options) => {
399
+ await handleAsyncAction("domains forwarding get", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/forwarding`));
400
+ }))
401
+ .addCommand(new Command("set")
402
+ .description("Point the domain's web root at a URL. WITHOUT --approved this previews current -> new and writes nothing. Free (0 credits), but it changes what every visitor to the domain sees.")
403
+ .argument("<domain>", "Domain name, such as acme.com.")
404
+ .requiredOption("--url <url>", "Destination http(s) URL. Pass an empty string to REMOVE the forward. Cannot point the domain at itself.")
405
+ .option("--approved", "Apply the change. Without it, a preview is returned.")
406
+ .option("--json", "Print a JSON envelope.")
407
+ .action(async (domain, options) => {
408
+ await handleAsyncAction("domains forwarding set", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/forwarding`, {
409
+ method: "POST",
410
+ body: {
411
+ // readOption() collapses "" to undefined, which would drop the
412
+ // field entirely and read as a malformed request. Clearing is an
413
+ // explicit null, so the empty string is mapped here.
414
+ forwarding_url: readOption(options.url) ?? null,
415
+ ...(options.approved ? { approved: true } : {}),
416
+ },
417
+ }));
418
+ })))
419
+ .addCommand(new Command("sync")
420
+ .description("Refresh Cloudflare-backed domains only: zone pages, registrar metadata, RDAP age backfill, and DNS health. Vendor-managed bundle domains are skipped here and read live with `domains get` / `domains dns`. Partial (paginated) syncs auto-continue until complete.")
421
+ .option("--full", "Restart the sweep from the first zone page instead of resuming the cursor.")
422
+ .option("--json", "Print a JSON envelope.")
423
+ .action(async (options) => {
424
+ await handleAsyncAction("domains sync", options, () => runDomainsSync(options));
425
+ }))
426
+ .addCommand(new Command("get")
427
+ .description("Get one domain's infrastructure/readiness detail: zone state, age with provenance, mailboxes, cached sending volume, and the last DNS health check. This is not Sequence/campaign attribution; use `oxygen sequences analytics` for native sent/replied/bounced results by mailbox/domain. For an Oxygen-managed (vendor-registered) domain it returns the vendor's rail instead — subscription, registration and renewal, nameservers and delegation, catch-all, DMARC, and a live DNS verdict. For a domain hosted OUTSIDE Oxygen that you send from (bought from an inbox provider, DNS elsewhere), rail=external answers from public DNS + the RDAP registry: SPF/DKIM/DMARC/MX verdict, nameservers, MX provider, registrar, domain age, web root, plus your own capacity, sending volume and readiness. Free — no credits, no provider keys.")
428
+ .argument("<domain>", "Domain name, such as acme.com.")
429
+ .option("--json", "Print a JSON envelope.")
430
+ .action(async (domain, options) => {
431
+ await handleAsyncAction("domains get", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}`));
432
+ }))
433
+ .addCommand(new Command("dns")
434
+ .description("Run a live read-only DNS health check (SPF, DKIM, DMARC, MX) against the domain's Cloudflare zone and persist the result. Add --deep for content-level checks (SPF DNS-lookup budget, DMARC enforcement, MX provider) plus an external DNS-over-HTTPS nameserver-authority check. An Oxygen-managed domain reads its vendor-held records and verdict instead (--deep ignored). A domain hosted outside Oxygen that you send from returns rail=external: the records public DNS resolves — the mail set, delegation, policy records and web root — with health.enumerable=false, because public DNS cannot list a zone (a DKIM key is probed at common selectors, so dkim=unknown means unproven, never missing). `dns plan`/`dns apply` edit your own Cloudflare zones only. DNS editing is unavailable for Oxygen-bought domains; keep their nameservers and contact support.")
435
+ .argument("[domain]", "Domain name, such as acme.com.")
436
+ // NOT --provider: commander parses parent options greedily by default,
437
+ // so a --provider here would swallow the `dns plan`/`dns apply`
438
+ // subcommands' required --provider flag.
439
+ .option("--mx-provider <provider>", "With --deep: report whether the live MX matches this mailbox provider (google or microsoft).")
440
+ .option("--deep", "Add content-level DNS analysis (SPF lookup budget, DMARC enforcement, MX provider) and an external DoH nameserver-authority check.")
441
+ .option("--json", "Print a JSON envelope.")
442
+ .action(async (domain, options) => {
443
+ if (!domain) {
444
+ throw new Error("Provide a domain (oxygen domains dns <domain>) or use a subcommand: `dns plan` / `dns apply`.");
445
+ }
446
+ const query = new URLSearchParams();
447
+ if (options.deep)
448
+ query.set("deep", "true");
449
+ if (options.mxProvider)
450
+ query.set("provider", options.mxProvider);
451
+ const suffix = query.size > 0 ? `?${query.toString()}` : "";
452
+ await handleAsyncAction("domains dns", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/dns${suffix}`));
453
+ })
454
+ .addCommand(new Command("plan")
455
+ .description("Preview the cold-email DNS records a domain should hold for its mailbox provider (SPF, per-provider DKIM, DMARC, MX, optional tracking) as a create/update/keep/conflict diff plus a plan_hash. FREE — nothing is written. Own Cloudflare zones only; DNS editing for Oxygen-bought domains is unavailable. Keep their nameservers and contact support.")
456
+ .argument("<domain>", "Domain name, such as acme.com.")
457
+ .requiredOption("--provider <provider>", "Mailbox provider: google or microsoft (determines SPF/MX/DKIM).")
458
+ .option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
459
+ .option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
460
+ .option("--tracking-host <host>", "Open/click tracking CNAME host (only planned when tracking is configured).")
461
+ .option("--dkim-json <json>", "DKIM records as [{type:TXT|CNAME,name,content}]. Use identical records when applying the preview.")
462
+ .option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM selector1/selector2 CNAMEs (default: auto-derive them for microsoft when no DKIM is supplied).")
463
+ .option("--json", "Print a JSON envelope.")
464
+ .action(async (domain, options, command) => {
465
+ await handleAsyncAction("domains dns plan", { ...options, json: command.optsWithGlobals().json === true }, () => runDomainsDnsPlan(domain, options));
466
+ }))
467
+ .addCommand(new Command("apply")
468
+ .description("Write the planned cold-email DNS records into your own Cloudflare zone. Requires a fresh plan_hash from `dns plan`. No Oxygen credits. Without --approved, re-previews the plan. DNS editing for Oxygen-bought domains is unavailable; keep their nameservers and contact support.")
469
+ .argument("<domain>", "Domain name, such as acme.com.")
470
+ .requiredOption("--provider <provider>", "Mailbox provider: google or microsoft (must match the plan).")
471
+ .option("--approved", "Write the records. Without this flag, returns a plan preview only.")
472
+ .option("--plan <hash>", "plan_hash from a fresh `dns plan` (required with --approved).")
473
+ .option("--dmarc-policy <policy>", "DMARC policy: none (default), quarantine, or reject.")
474
+ .option("--dmarc-rua <mailbox>", "DMARC aggregate-report mailbox (rua=mailto:...).")
475
+ .option("--tracking-host <host>", "Open/click tracking CNAME host (only applied when tracking is configured).")
476
+ .option("--dkim-json <json>", "DKIM records as [{type:TXT|CNAME,name,content}]. Use identical records when applying the preview.")
477
+ .option("--no-auto-dkim", "Skip auto-deriving Microsoft DKIM (must match the value used in `dns plan` so the plan_hash matches).")
478
+ .option("--json", "Print a JSON envelope.")
479
+ .action(async (domain, options, command) => {
480
+ await handleAsyncAction("domains dns apply", { ...options, json: command.optsWithGlobals().json === true }, () => runDomainsDnsApply(domain, options));
481
+ })))
482
+ .addCommand(new Command("tracking")
483
+ .description("Open/click tracking domain for native cold email: provision the tracking CNAME on the domain's Cloudflare zone and check its verification status. 0 Oxygen credits.")
484
+ .addCommand(new Command("setup")
485
+ .description("Provision the open/click tracking CNAME (<track-host> CNAME <tracking target>) on the domain's Cloudflare zone. Without --approve, previews the exact record. GATE: needs EMAIL_TRACKING_CNAME_TARGET configured + the tracking hostname attached on Vercel; send-time injection stays off until EMAIL_TRACKING_SECRET is set.")
486
+ .argument("<domain>", "Sending domain, such as acme.com.")
487
+ .option("--host <host>", "Tracking subdomain (default track.<domain>); must be a subdomain of the domain.")
488
+ .option("--approve", "Write the CNAME. Without this flag, returns a preview only.")
489
+ .option("--json", "Print a JSON envelope.")
490
+ .action(async (domain, options) => {
491
+ await handleAsyncAction("domains tracking setup", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/tracking`, {
492
+ method: "POST",
493
+ body: { ...(options.host ? { host: options.host } : {}), ...(options.approve ? { approved: true } : {}) },
494
+ }));
495
+ }))
496
+ .addCommand(new Command("status")
497
+ .description("Show the open/click tracking status for a domain (configured host, verification status, CNAME target, whether the tracking secret is set). Reads the cache — 0 Oxygen credits.")
498
+ .argument("<domain>", "Sending domain, such as acme.com.")
499
+ .option("--json", "Print a JSON envelope.")
500
+ .action(async (domain, options) => {
501
+ await handleAsyncAction("domains tracking status", options, () => requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/tracking`));
502
+ })))
503
+ .addCommand(new Command("add")
504
+ .description("BYOK CLOUDFLARE: onboard a domain registered elsewhere by creating a zone in your connected Cloudflare account. For managed domain + inbox provisioning use `managed-inboxes subscribe`.")
505
+ .argument("<domain>", "Apex domain to add, such as acme.com.")
506
+ .option("--json", "Print a JSON envelope.")
507
+ .action(async (domain, options) => {
508
+ await handleAsyncAction("domains add", options, () => requestOxygen("/api/cli/domains/add", { method: "POST", body: { domain } }));
509
+ }))
510
+ .addCommand(new Command("search")
511
+ .description("Search available domains with their first-year registration price. Free; nothing is purchased. With your own Cloudflare account connected it searches your Registrar (USD, your card); otherwise it lists names Oxygen can register, priced in credits at the registrar's cost (a .com is 1,250 for the first year). Renewal pricing is separate and unknown in search; check an exact name for its estimate.")
512
+ .argument("<query>", "Search text, such as a brand or keyword.")
513
+ .option("--json", "Print a JSON envelope.")
514
+ .action(async (query, options) => {
515
+ await handleAsyncAction("domains search", options, () => {
516
+ const params = new URLSearchParams({ q: query });
517
+ return requestOxygen(`/api/cli/domains/search?${params.toString()}`);
518
+ });
519
+ }))
520
+ .addCommand(new Command("check")
521
+ .description("Check availability and first-year registration price for up to 20 exact domains. Free; nothing is purchased. With your own Cloudflare account connected it checks your Registrar (USD, your card); otherwise the names Oxygen can register, in credits at the registrar's cost (a .com is 1,250 for the first year). The separate renewal estimate may be unknown. A null tier means unknown, not non-premium. A domain you already own through Oxygen comes back with reason `owned`.")
522
+ .argument("<domains...>", "Domain names to check (max 20).")
523
+ .option("--json", "Print a JSON envelope.")
524
+ .action(async (domains, options) => {
525
+ await handleAsyncAction("domains check", options, () => {
526
+ if (domains.length > DOMAINS_CHECK_MAX_DOMAINS) {
527
+ throw new Error(`domains check takes at most ${DOMAINS_CHECK_MAX_DOMAINS} domains per call (got ${domains.length}). Split the list and re-run.`);
528
+ }
529
+ return requestOxygen("/api/cli/domains/check", {
530
+ method: "POST",
531
+ body: { domains },
532
+ });
533
+ });
534
+ }))
535
+ .addCommand(new Command("buy")
536
+ .description("Buy sending domains. NON-REFUNDABLE. Without --approved, returns a free priced preview plus quote_id. A null tier means unknown, not non-premium. Without your own Cloudflare account, Oxygen registers the domain and charges credits at the registrar's cost for one year (a .com is 1,250), then renews it yearly at the renewal cost, charged 30 days before expiry (turn off with `domains renewal <domain> --off`). With your own Cloudflare account connected, it buys through your Registrar on your Cloudflare card for 0 credits. For a domain with inboxes use `managed-inboxes subscribe`.")
537
+ .argument("<domains...>", "Domain names to buy.")
538
+ .option("--approved", "Execute the purchase. Without this flag, returns a preview only.")
539
+ .option("--quote <id>", "Quote id from the preview (required with --approved).")
540
+ .option("--accept-premium", "Acknowledge premium-tier pricing for premium domains.")
541
+ .option("--auto-renew", "Renew the new registrations yearly (the default).")
542
+ .option("--no-auto-renew", "Do not renew: the domain lapses when its year ends.")
543
+ .option("--billing <path>", "JSON file with the registrant (WHOIS) contact: first_name, last_name, phone, country, city, state, address_line_one, postal_code. Needed once, when the workspace has none on file.")
544
+ .option("--redirect-url <url>", "Where the domain's web root forwards visitors. Defaults to your company website.")
545
+ .option("--no-privacy", "Disable WHOIS privacy/redaction on the new registrations.")
546
+ .option("--wait", "After an approved purchase, poll each pending registration to a terminal state (up to 10 minutes per domain).")
547
+ .option("--json", "Print a JSON envelope.")
548
+ .action(async (domains, options) => {
549
+ await handleAsyncAction("domains buy", options, () => runDomainsBuy(domains, options));
550
+ }))
551
+ .addCommand(new Command("renewal")
552
+ .description("Turn the yearly renewal of a domain bought through Oxygen on or off. No charge. On: the registrar's renewal cost is charged in credits 30 days before expiry. Off: the domain lapses when it expires.")
553
+ .argument("<domain>", "A domain from `domains list` (registered_domains).")
554
+ .option("--on", "Renew yearly.")
555
+ .option("--off", "Stop renewing; the domain lapses at expiry.")
556
+ .option("--json", "Print a JSON envelope.")
557
+ .action(async (domain, options) => {
558
+ await handleAsyncAction("domains renewal", options, () => {
559
+ if (Boolean(options.on) === Boolean(options.off)) {
560
+ throw new Error("Pass exactly one of --on or --off.");
561
+ }
562
+ return requestOxygen(`/api/cli/domains/${encodeURIComponent(domain)}/renewal`, {
563
+ method: "POST",
564
+ body: { auto_renew: options.on === true },
565
+ });
566
+ });
567
+ }))
568
+ .addCommand(new Command("adopt")
569
+ .description("STAFF LEGACY RECOVERY: attach domains that already exist in the shared Oxygen-managed Cloudflare account to their workspace. FREE; no purchase. New managed domain + inbox bundles use InboxKit. Without --yes, previews only.")
570
+ .argument("[domains...]", "Specific domains to adopt. Omit and pass --all to adopt every zone in the managed account.")
571
+ .option("--all", "Adopt every zone discovered in the managed Cloudflare account.")
572
+ .option("--organization-id <id>", "STAFF override: adopt into this workspace instead of the calling org.")
573
+ .option("--yes", "Execute the adoption. Without this flag, prints a preview only.")
574
+ .option("--json", "Print a JSON envelope.")
575
+ .action(async (domains, options) => {
576
+ await handleAsyncAction("domains adopt", options, () => runDomainsAdopt(domains, options));
577
+ }))
578
+ .addCommand(new Command("registrations")
579
+ .description("List the org's domain purchase ledger: every registration attempt with status and price snapshot.")
580
+ .option("--status <status>", "Filter by ledger status: submitting, in_progress, succeeded, failed, action_required, or blocked.")
581
+ .option("--json", "Print a JSON envelope.")
582
+ .action(async (options) => {
583
+ await handleAsyncAction("domains registrations", options, () => {
584
+ const params = new URLSearchParams();
585
+ const status = readOption(options.status);
586
+ if (status)
587
+ params.set("status", status);
588
+ const suffix = params.toString();
589
+ return requestOxygen(`/api/cli/domains/registrations${suffix ? `?${suffix}` : ""}`);
590
+ });
591
+ }))
592
+ .addCommand(new Command("registration-status")
593
+ .description("Get one domain registration's ledger state (the server performs a gated live poll when due — safe to call repeatedly). Pass --wait to poll until it settles.")
594
+ .argument("<domain>", "Domain name from a previous `domains buy`.")
595
+ .option("--wait", "Poll every 10s (up to 10 minutes) until the registration reaches a terminal state.")
596
+ .option("--json", "Print a JSON envelope.")
597
+ .action(async (domain, options) => {
598
+ await handleAsyncAction("domains registration-status", options, () => {
599
+ if (options.wait)
600
+ return waitForDomainRegistration(domain);
601
+ return requestOxygen(`/api/cli/domains/registrations/${encodeURIComponent(domain)}`);
602
+ });
603
+ })));
604
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerEgressFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;