@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,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerManagedInboxesFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,361 @@
1
+ import { readPositiveInt } from "../cli-values.js";
2
+ import { putAvatarBytesToStorage, readJsonFileValue, splitCommaList } from "../command-support.js";
3
+ import { requestOxygen } from "../http-client.js";
4
+ import { isRecord, readOption } from "../util.js";
5
+ import { OxygenError } from "@oxygen/shared/cli-result";
6
+ import { Command, Option } from "commander";
7
+ import { resolve } from "node:path";
8
+ // Builds the `prompts` command tree (list/get/upsert/archive). The
9
+ // deprecated `templates` alias is the identical tree — same subcommands,
10
+ // options, request bodies, and routes — differing only in the parent command
11
+ // name, its description, and the `handleAsyncAction` operation labels, so both
12
+ // surfaces are generated here to stop them drifting (the alias previously
13
+ // duplicated every subcommand). All subcommands hit the shared
14
+ // /api/cli/templates/* routes.
15
+ /**
16
+ * Accept the domain as EITHER a positional argument or --domain.
17
+ *
18
+ * `subscribe` required --domain while get/cancel took a positional, so the same value had
19
+ * two spellings depending on the verb, and `get --domain acme.com` failed outright with "unknown
20
+ * option". Both spellings now work on every verb; nothing that used to work stops working.
21
+ */
22
+ function requireDomainArg(positional, option) {
23
+ const domain = readOption(positional) ?? readOption(option);
24
+ if (!domain) {
25
+ throw new OxygenError("invalid_request", "A domain is required \u2014 pass it as an argument or with --domain. `oxygen managed-inboxes list` shows your managed domains.", { exitCode: 2, details: { next_step: "oxygen managed-inboxes list" } });
26
+ }
27
+ return domain;
28
+ }
29
+ /**
30
+ * Build the `mailboxes[]` an order route receives, from whichever input form the
31
+ * caller used, plus the optional `--sender` stamp.
32
+ *
33
+ * Shared by `managed-inboxes subscribe` and `add-inboxes` because the two used to
34
+ * drift: only one of them accepted the `--count/--prefix` shorthand, and each
35
+ * assembled its own body, so a fix to one silently missed the other. `forms`
36
+ * parameterises which spellings the calling command accepts instead.
37
+ *
38
+ * The `--sender` stamp is the reason this is one function. The inbox vendor has no
39
+ * post-provisioning update, so whatever first/last name reaches it is permanent —
40
+ * which is why a sender-backed item must not also carry a typed name. The server
41
+ * ignores those keys when `sender_profile_id` is present, but a request body that
42
+ * still lists `first_name: "Ada"` reads like Ada is what ships. Strip them here so
43
+ * the body says exactly what the vendor will stamp.
44
+ */
45
+ function buildManagedInboxMailboxes(options, forms) {
46
+ const senderProfileId = readOption(options.sender);
47
+ const mailboxesJson = readOption(options.mailboxes);
48
+ const filePath = readOption(options.file);
49
+ const locals = splitCommaList(options.locals);
50
+ const prefix = readOption(options.prefix);
51
+ const count = readPositiveInt(options.count);
52
+ let mailboxes;
53
+ if (mailboxesJson) {
54
+ mailboxes = JSON.parse(mailboxesJson);
55
+ }
56
+ else if (filePath) {
57
+ const parsed = readJsonFileValue(resolve(filePath), "--file");
58
+ mailboxes = parsed.mailboxes ?? [];
59
+ }
60
+ else if (locals.length > 0) {
61
+ if (!senderProfileId) {
62
+ // Without a sender there is no identity to attach to a bare local part, and
63
+ // the vendor stamps a name on every mailbox permanently. Refusing is the
64
+ // only honest answer: inventing "Talia Rosen" from `talia.rosen` would bake
65
+ // a guess into a mailbox nobody can rename later.
66
+ throw new OxygenError("invalid_request", "--locals needs --sender <sender profile id>: the local parts carry no name, and the inbox vendor stamps a permanent one. Pass --mailboxes/--file with real first_name/last_name instead, or `oxygen senders profiles list` to pick a sender.", { exitCode: 2 });
67
+ }
68
+ mailboxes = locals.map((username) => ({ username }));
69
+ }
70
+ else if (forms.countPrefix && (count !== undefined || prefix)) {
71
+ if (count === undefined || !prefix) {
72
+ throw new Error("--count and --prefix must be passed together (e.g. --count 3 --prefix ada).");
73
+ }
74
+ const base = prefix.toLowerCase();
75
+ if (senderProfileId) {
76
+ // With a sender the shorthand finally has a real identity behind it, so it
77
+ // generates usernames only — the placeholder names below are exactly what
78
+ // this flag exists to stop shipping.
79
+ mailboxes = Array.from({ length: count }, (_entry, index) => ({ username: `${base}${index + 1}` }));
80
+ }
81
+ else {
82
+ // The vendor stamps a first/last name on every mailbox, so there is no
83
+ // name-less order shape to fall back on — the shorthand invents
84
+ // placeholders rather than pretending names are optional. Anyone who
85
+ // wants real human identities on the inboxes passes --sender (best),
86
+ // or --mailboxes/--file.
87
+ const label = `${base.charAt(0).toUpperCase()}${base.slice(1)}`;
88
+ mailboxes = Array.from({ length: count }, (_entry, index) => ({
89
+ username: `${base}${index + 1}`,
90
+ first_name: label,
91
+ last_name: String(index + 1),
92
+ }));
93
+ }
94
+ }
95
+ else {
96
+ const spellings = ["--mailboxes <json>", "--file <path>", "--locals <list> --sender <id>"];
97
+ if (forms.countPrefix)
98
+ spellings.push("--count <n> --prefix <base>");
99
+ throw new Error(`Provide ${spellings.join(", ")}.`);
100
+ }
101
+ if (!senderProfileId || !Array.isArray(mailboxes))
102
+ return mailboxes;
103
+ return mailboxes.map((entry) => {
104
+ if (!isRecord(entry))
105
+ return entry;
106
+ const { first_name: _firstName, last_name: _lastName, profile_picture_url: _profilePictureUrl, ...rest } = entry;
107
+ return { ...rest, sender_profile_id: senderProfileId };
108
+ });
109
+ }
110
+ export function registerManagedInboxesFamily(program, handleAsyncAction) {
111
+ program.addCommand(new Command("managed-inboxes")
112
+ .addCommand(new Command("recover")
113
+ .description("Restore a cancelled managed subscription using its original addresses and domain. Find candidates with managed-inboxes list --status cancelled --json. Without --approved, performs a read-only preview at 0 credits: eligibility, upfront credits, recurring charges and a quote_id. Review the preview, then use --approved --quote <id> --max-credits <n>. Interrupted provider attempts are inspected before any repeat. After provider recovery, verify sending authorization and warmup for the exact recovered mailboxes. If warmup is disabled, preview enrollment and review its scope and costs before approving it.")
114
+ .argument("<domain>", "The cancelled managed sending domain")
115
+ .option("--approved", "Execute the reviewed recovery quote", false)
116
+ .option("--quote <id>", "quote_id from the recovery preview")
117
+ .option("--rebuy", "Include replacement licenses in the recovery preview and approval", false)
118
+ .option("--max-credits <n>", "Upfront recovery credit ceiling; does not cap recurring charges", Number)
119
+ .option("--json", "Print a JSON envelope")
120
+ .action(async (domain, options) => {
121
+ await handleAsyncAction("managed-inboxes recover", options, () => requestOxygen("/api/cli/managed-inboxes/recover", {
122
+ method: "POST", body: { domain, approved: options.approved === true, rebuy: options.rebuy === true,
123
+ ...(options.quote ? { quote_id: options.quote } : {}),
124
+ ...(options.maxCredits !== undefined ? { max_credits: options.maxCredits } : {}) },
125
+ }));
126
+ }))
127
+ .description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, cancel, and recover cancelled original addresses. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes preview first (re-run with --approved --quote); cancel previews first (re-run with --approved --confirm <domain>). To remove several domains with their inboxes, use `domains delete`. The inbox vendor is chosen for you; --vendor pins one.")
128
+ .addCommand(new Command("verify")
129
+ .description("Compare purchased inbox inventory with the vendor and legacy Stripe mailbox seats. Credit-billed orders do not require Stripe seats; their credit captures and renewal amounts are explicitly unverified by this check. Unavailable reads are reported as verification gaps, never agreement or underbilling. Reports mismatches with who loses money (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
130
+ .option("--json", "Print a JSON envelope.")
131
+ .action(async (options) => {
132
+ await handleAsyncAction("managed-inboxes verify", options, () => requestOxygen("/api/cli/managed-inboxes/verify"));
133
+ }))
134
+ .addCommand(new Command("registrant")
135
+ .description("Show the registrant (WHOIS) contact this workspace files domain registrations under, or null if it has never bought one. Your FIRST managed-inboxes subscribe supplies it via --billing; every order after that reuses it automatically, so this is how you check what is on file. Read-only, 0 Oxygen credits.")
136
+ .option("--json", "Print a JSON envelope.")
137
+ .action(async (options) => {
138
+ await handleAsyncAction("managed-inboxes registrant", options, () => requestOxygen("/api/cli/managed-inboxes/registrant"));
139
+ }))
140
+ .addCommand(new Command("subscribe")
141
+ .description("Buy managed monthly inboxes on a new domain or your existing Cloudflare domain. Use --existing-domain for Cloudflare: the read-only preview shows ownership, occupancy, DNS scope and blockers, with zero registration charge. --max-per-domain saves a stricter limit with the approved order, including existing addresses; later additions honor it. Preview does not save the limit or change workspace data. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered, nothing charged); re-run with --approved --quote <id> to place the order. OXYGEN Warm-up defaults on for Google, Microsoft, and Azure when available: preview and approved responses carry `warmup_activation` with the exact `mailboxes` and `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. This order authorizes automatic OXYGEN Warm-up activation after provisioning, so no second `mailboxes warmup enable` approval is needed. The preview shows the price in force: from the 2026-09 repricing date a Google Workspace or Microsoft 365 inbox is one flat mailbox line plus the 100-credit mailbox connection, warm-up included, which `connection_monthly_credits` shows and your balance reserves once the inbox connects (400 credits a month all-in at the committed price), and an Azure tenant is one flat charge for its domain (4,000 a month) plus each inbox's connection; `managed-inboxes list --json` reports the prices in force under `prices`. Domains are priced from the vendor's LIVE domain price, and any price below vendor cost is refused. A domain you bought on its own through Oxygen (`domains buy`) takes its first inboxes here too: the preview says `domain_source: owned`, nothing is registered again and `one_off_credits` is 0 (Google or Microsoft; omit --years). To add mailboxes to a domain that already has inboxes use `managed-inboxes add-inboxes`.")
142
+ .argument("[domain]", "Sending domain to register + host the mailboxes (e.g. send.acme.com), or one you already bought through Oxygen. May also be passed as --domain.")
143
+ .option("--domain <domain>", "Sending domain (alternative to the positional argument).")
144
+ .requiredOption("--provider <provider>", "Mailbox PLATFORM: google, microsoft, or azure. (google/microsoft cap at 5 mailboxes per domain; azure allows 100.)")
145
+ .option("--vendor <vendor>", "Pin the vendor: inboxkit. Omit to let OXYGEN choose. A named vendor with no credential FAILS rather than falling back to another.")
146
+ .option("--sender <id>", "Sender profile id (from `oxygen senders profiles list`) that OWNS every mailbox in this order: its first name, last name, and profile picture are stamped on each one, so you never retype them. A sender profile is one person's sending identity — their LinkedIn/WhatsApp accounts and email inboxes under one name and photo. One sender can own several addresses on the same domain (see --locals). Any first_name/last_name/profile_picture_url in --mailboxes/--file is dropped for the sender's own values.")
147
+ .option("--locals <list>", "Comma-separated local parts to create for --sender, e.g. `--locals talia,talia.rosen,t.rosen` on send.acme.com orders talia@, talia.rosen@, and t.rosen@ — all owned by that one person. Requires --sender, because a bare local part carries no name and the vendor's stamp is permanent.")
148
+ .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}], or [{\"username\",\"sender_profile_id\"}] to take the identity from a sender profile per item (mix freely). profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL — the vendor fetches it server-side at provisioning, so an expiring CDN link (e.g. a media.licdn.com URL with e=<epoch>) ships the mailbox faceless. Use `oxygen managed-inboxes upload-avatar <path>` to host one, or --sender / sender_profile_id, whose photo Oxygen already mirrors.")
149
+ .option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
150
+ .addOption(new Option("--preview", "Explicit read-only quote: connects nothing, buys nothing and charges nothing. Cannot be combined with purchase or DNS cleanup approval.").conflicts(["approved", "dnsCleanupApproved"]))
151
+ .option("--existing-domain", "Use an existing Cloudflare domain from oxygen domains list; never register or transfer it.")
152
+ .option("--max-per-domain <n>", "Save a stricter mailbox limit with the approved order, including existing addresses (e.g. 2). Preview does not save it.")
153
+ .option("--dns-cleanup-records <ids>", "Preview deletion of these comma-separated exact email DNS record IDs on an existing domain; connects and buys nothing.")
154
+ .addOption(new Option("--dns-cleanup-approved", "Delete only the reviewed email records; requires --dns-cleanup-records and --dns-cleanup-hash. Request a fresh purchase quote afterward.").conflicts("approved"))
155
+ .option("--dns-cleanup-hash <hash>", "The plan_hash from the exact DNS cleanup preview, required to delete records.")
156
+ .option("--years <n>", "Years to register a new domain for (1-10). Omit for existing domains.")
157
+ .option("--redirect-url <url>", "Where the domain's web root redirects. Defaults to your workspace's company website; editable later with `oxygen domains forwarding set`.")
158
+ .option("--billing <path>", "Path to a JSON file with the registrant/WHOIS contact (first_name, last_name, phone, country, city, state, address_line_one, postal_code). Required on your FIRST order; later orders reuse the registrant already on file.")
159
+ .option("--no-warmup", "Order WITHOUT automatic OXYGEN Warm-up. Warm-up is included by default and starts after provisioning under this order approval for the exact quoted Google/Microsoft/Azure addresses; opting out means using BYO warm-up or a later standalone warmup approval.")
160
+ .option("--placement", "Add inbox placement to every inbox (optional, billed monthly per inbox from provisioning): inbox-placement (spam) testing, blacklist/reputation monitoring, and authentication checks. Off unless you pass this flag.")
161
+ .option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
162
+ .option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
163
+ .option("--json", "Print a JSON envelope.")
164
+ .action(async (domainArg, options) => {
165
+ await handleAsyncAction("managed-inboxes subscribe", options, () => {
166
+ // The domain may arrive either way. `subscribe` took --domain while get and
167
+ // cancel took a positional, so the same value had two spellings depending on the
168
+ // verb — `get --domain x` simply failed. Both work everywhere now.
169
+ const domain = readOption(domainArg) ?? readOption(options.domain);
170
+ const provider = readOption(options.provider);
171
+ if (!domain)
172
+ throw new Error("--domain is required.");
173
+ if (!provider)
174
+ throw new Error("--provider is required (google, microsoft, or azure).");
175
+ // No --count/--prefix here, deliberately: that shorthand can only
176
+ // reach the vendor by inventing names, and a new domain's first
177
+ // order is exactly where a placeholder becomes permanent.
178
+ const mailboxes = buildManagedInboxMailboxes(options, { countPrefix: false });
179
+ const years = readOption(options.years);
180
+ const vendor = readOption(options.vendor);
181
+ const redirectUrl = readOption(options.redirectUrl);
182
+ const billingPath = readOption(options.billing);
183
+ const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
184
+ const quote = readOption(options.quote);
185
+ const cleanupRecords = readOption(options.dnsCleanupRecords);
186
+ const cleanupHash = readOption(options.dnsCleanupHash);
187
+ if ((cleanupRecords && !options.existingDomain)
188
+ || ((options.dnsCleanupApproved || cleanupHash) && !cleanupRecords)
189
+ || (options.dnsCleanupApproved && !cleanupHash)) {
190
+ throw new OxygenError("invalid_request", "DNS cleanup requires --existing-domain and --dns-cleanup-records; deletion also requires its fresh --dns-cleanup-hash.", { exitCode: 2 });
191
+ }
192
+ return requestOxygen("/api/cli/managed-inboxes/subscribe", {
193
+ method: "POST",
194
+ body: {
195
+ domain,
196
+ provider,
197
+ mailboxes,
198
+ ...(vendor ? { vendor } : {}),
199
+ ...(years ? { years: Number(years) } : {}),
200
+ ...(options.existingDomain ? { existing_domain: true } : {}),
201
+ ...(options.maxPerDomain !== undefined ? { max_mailboxes_per_domain: Number(options.maxPerDomain) } : {}),
202
+ ...(cleanupRecords ? { dns_cleanup: {
203
+ record_ids: cleanupRecords.split(",").map((id) => id.trim()),
204
+ approved: options.dnsCleanupApproved === true,
205
+ ...(cleanupHash ? { plan_hash: cleanupHash } : {}),
206
+ } } : {}),
207
+ ...(redirectUrl ? { redirect_url: redirectUrl } : {}),
208
+ ...(billing ? { billing } : {}),
209
+ // Warm-up is on unless --no-warmup. Placement is opt-in: it is sent only
210
+ // when --placement was passed, so an untouched flag leaves the server's
211
+ // default (off) in force. Older CLIs sent `placement: true` on every
212
+ // order. The selection is hashed into quote_id, so --approved must
213
+ // repeat whatever the preview was run with.
214
+ addons: {
215
+ warmup: options.warmup !== false,
216
+ ...(options.placement === true ? { placement: true } : {}),
217
+ },
218
+ ...(options.approved ? { approved: true } : {}),
219
+ ...(quote ? { quote_id: quote } : {}),
220
+ },
221
+ });
222
+ });
223
+ }))
224
+ .addCommand(new Command("add-inboxes")
225
+ .description("Add mailboxes to a managed domain you ALREADY own — no new domain is registered and there is NO domain registration charge, only the extra inboxes' monthly rate (plus their add-ons). WITHOUT --approved this prints a priced PREVIEW with a quote_id and orders nothing; re-run with --approved --quote <id> to place the order. When the standing OXYGEN Warm-up add-on is enabled and available, preview and approved responses carry `warmup_activation` for only the exact newly added `mailboxes`, with `scope=warmup_only`; it is null when warmup is disabled or unavailable. For InboxKit, Google carries `transport=inboxkit_managed_google_handoff`; Microsoft/Azure carries `transport=inboxkit_sequencer_export`. Both carry `mailbox_credentials_required=false` and `native_send_oauth_separate=true`, so managed warm-up needs no customer-supplied OAuth or mailbox password. Google retrieves the existing managed credential only during activation and never persists or returns it; Microsoft/Azure uses native export. Native sending authorization is separate and may still be required before OXYGEN can send. Verify each exact address with `mailboxes oauth-health --json`; InboxKit's unattended Google domain approval usually lands within minutes and the worker keeps retrying, so a waiting row is not a reason to ask the customer to sign in. The quote authorizes automatic OXYGEN Warm-up activation after provisioning, so do not run a second warmup approval. Capped per domain by the platform the domain was bought on (5 google/microsoft, 100 azure) COUNTING the inboxes already on it. To register a NEW domain use `managed-inboxes subscribe` instead.")
226
+ .argument("[domain]", "A managed domain this workspace already owns (e.g. send.acme.com). May also be passed as --domain.")
227
+ .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
228
+ .option("--sender <id>", "Sender profile id (from `oxygen senders profiles list`) that OWNS every mailbox in this order: its first name, last name, and profile picture are stamped on each one, so you never retype them. A sender profile is one person's sending identity — their LinkedIn/WhatsApp accounts and email inboxes under one name and photo. One sender can own several addresses on the same domain (see --locals). Any first_name/last_name/profile_picture_url in --mailboxes/--file is dropped for the sender's own values.")
229
+ .option("--locals <list>", "Comma-separated local parts to add for --sender, e.g. `--locals talia,talia.rosen,t.rosen` adds talia@, talia.rosen@, and t.rosen@ on this domain — all owned by that one person. Requires --sender, because a bare local part carries no name and the vendor's stamp is permanent.")
230
+ .option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture_url\"}], or [{\"username\",\"sender_profile_id\"}] to take the identity from a sender profile per item (mix freely). The vendor stamps the names on each mailbox permanently, so real names belong here. profile_picture_url is optional and must be a PUBLIC, PERMANENT https URL the vendor can fetch at provisioning; `oxygen managed-inboxes upload-avatar <path>` returns one, and a sender profile's mirrored photo already is one.")
231
+ .option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
232
+ .option("--count <n>", "Shorthand for --mailboxes: how many inboxes to add. Requires --prefix.")
233
+ .option("--prefix <base>", "Shorthand username base for --count: `--count 3 --prefix ada` adds ada1, ada2, ada3. WITH --sender all three belong to that person, named and pictured as them; WITHOUT --sender they get placeholder names (Ada 1, Ada 2, Ada 3) the vendor can never change. Pass --sender, or --mailboxes/--file with real names.")
234
+ .option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
235
+ .option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
236
+ .option("--json", "Print a JSON envelope.")
237
+ .action(async (domainArg, options) => {
238
+ await handleAsyncAction("managed-inboxes add-inboxes", options, () => {
239
+ const domain = requireDomainArg(domainArg, options.domain);
240
+ const mailboxes = buildManagedInboxMailboxes(options, { countPrefix: true });
241
+ const quote = readOption(options.quote);
242
+ // Refuse locally rather than spending a round trip on a PAID path: the
243
+ // route requires the quote that priced this exact expansion, so
244
+ // `--approved` alone can only ever come back as a 400.
245
+ if (options.approved && !quote) {
246
+ // Typed, not a bare Error: a bare throw surfaces as
247
+ // `unexpected_error`, which an agent cannot tell apart from a
248
+ // real failure on a PAID command. Refusing to order is an
249
+ // ordinary, expected outcome and must say so in its code.
250
+ throw new OxygenError("invalid_request", "--approved requires --quote <id> from a fresh preview. Re-run without --approved to get one.", { exitCode: 2 });
251
+ }
252
+ return requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}/mailboxes`, {
253
+ method: "POST",
254
+ body: {
255
+ // Always an explicit mailboxes[]: --count/--prefix is client-side
256
+ // sugar, so the server sees exactly one shape and the preview it
257
+ // prices is the list that gets ordered.
258
+ mailboxes,
259
+ ...(options.approved ? { approved: true } : {}),
260
+ ...(quote ? { quote_id: quote } : {}),
261
+ },
262
+ });
263
+ });
264
+ }))
265
+ .addCommand(new Command("list")
266
+ .description("List the org's managed inbox orders: vendor, platform, live inbox count, lifecycle status, internal billing posture, and each order's FULL monthly cost — the inbox line, each inbox's mailbox connection once inboxes are sold all-in (`orders[].connection`), and the warm-up and inbox-placement add-ons billing per inbox on top of it (`orders[].total_monthly_credits`, `total_monthly_credits` across the workspace). Read-only, 0 Oxygen credits.")
267
+ .option("--status <status>", "Filter by status: pending, active, renewing, past_due, expired, cancelled, or failed.")
268
+ .option("--json", "Print a JSON envelope.")
269
+ .action(async (options) => {
270
+ await handleAsyncAction("managed-inboxes list", options, () => {
271
+ const params = new URLSearchParams();
272
+ const status = readOption(options.status);
273
+ if (status)
274
+ params.set("status", status);
275
+ const suffix = params.toString();
276
+ return requestOxygen(`/api/cli/managed-inboxes${suffix ? `?${suffix}` : ""}`);
277
+ });
278
+ }))
279
+ .addCommand(new Command("get")
280
+ .description("Get one managed inbox subscription by domain (vendor, platform, mailbox count, monthly USD price, vendor order id, lifecycle status, auto-renew, period end, internal billing status). Read-only, 0 Oxygen credits.")
281
+ .argument("[domain]", "The managed inbox domain (e.g. send.acme.com). May also be passed as --domain.")
282
+ .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
283
+ .option("--json", "Print a JSON envelope.")
284
+ .action(async (domainArg, options) => {
285
+ await handleAsyncAction("managed-inboxes get", options, () => {
286
+ const domain = requireDomainArg(domainArg, options.domain);
287
+ return requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}`);
288
+ });
289
+ }))
290
+ .addCommand(new Command("cancel")
291
+ .description("Cancel a managed inbox order by domain: the inboxes AND every recurring add-on on them (warm-up, inbox placement). WITHOUT --approved prints the itemized teardown — every line that stops billing, when the inboxes go dark, and which live sequences send from them. To cancel, re-run with --approved AND --confirm <domain> echoing the domain exactly. Once cancelled, original-address recovery depends on provider eligibility; preview with managed-inboxes recover. The domain stays registered until its term ends. Cancelling is free.")
292
+ .argument("[domain]", "The managed inbox domain to cancel. May also be passed as --domain.")
293
+ .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
294
+ .option("--approved", "Actually cancel (otherwise the teardown preview is returned).")
295
+ .option("--confirm <domain>", "Type the domain again to confirm. Required with --approved.")
296
+ .option("--json", "Print a JSON envelope.")
297
+ .action(async (domainArg, options) => {
298
+ await handleAsyncAction("managed-inboxes cancel", options, () => {
299
+ const domain = requireDomainArg(domainArg, options.domain);
300
+ const confirm = readOption(options.confirm);
301
+ return requestOxygen("/api/cli/managed-inboxes/cancel", {
302
+ method: "POST",
303
+ body: {
304
+ domain,
305
+ ...(options.approved ? { approved: true } : {}),
306
+ // Sent verbatim, never defaulted to `domain`: the whole point of the
307
+ // echo is that a human (or an agent) typed the name a second time, and
308
+ // filling it in here would delete the only friction on the path.
309
+ ...(confirm ? { confirm_domain: confirm } : {}),
310
+ },
311
+ });
312
+ });
313
+ }))
314
+ .addCommand(new Command("exit")
315
+ .description("Transfer ONE managed domain out of OXYGEN: stop every OXYGEN renewal for this domain after the paid period; keep the domain, mailboxes, DNS, warm-up state and data exactly as they are. Nothing is cancelled, disconnected, or released; the handoff is coordinated by a human with the vendor. OXYGEN continues paying the inbox vendor until that handoff or `managed-inboxes cancel`; the customer does not pay that vendor-carry cost. For all external resources in a workspace, preview `workspace infrastructure-exit` instead. WITHOUT --approved prints the domain plan; to record the transfer, re-run with --approved AND --confirm <domain>. --revoke resumes OXYGEN renewals. Free, 0 credits.")
316
+ .argument("[domain]", "The managed domain to stop renewing. May also be passed as --domain.")
317
+ .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
318
+ .option("--contact <email>", "Who the human handoff should be coordinated with (recorded on the request).")
319
+ .option("--note <text>", "Free-text context for the handoff, e.g. where the mailboxes are moving.")
320
+ .option("--revoke", "Undo a recorded exit: renewals resume on the next cycle. Still needs --approved --confirm.")
321
+ .option("--approved", "Actually record the exit (or the revoke); otherwise the plan is printed.")
322
+ .option("--confirm <domain>", "Type the domain again to confirm. Required with --approved.")
323
+ .option("--json", "Print a JSON envelope.")
324
+ .action(async (domainArg, options) => {
325
+ await handleAsyncAction("managed-inboxes exit", options, () => {
326
+ const domain = requireDomainArg(domainArg, options.domain);
327
+ const confirm = readOption(options.confirm);
328
+ const contact = readOption(options.contact);
329
+ const note = readOption(options.note);
330
+ return requestOxygen("/api/cli/managed-inboxes/exit", {
331
+ method: "POST",
332
+ body: {
333
+ domain,
334
+ ...(contact ? { contact } : {}),
335
+ ...(note ? { note } : {}),
336
+ ...(options.revoke ? { revoke: true } : {}),
337
+ ...(options.approved ? { approved: true } : {}),
338
+ // Sent verbatim, never defaulted to `domain` — same rule as cancel: the
339
+ // echo only means something if a human (or an agent) typed it a second time.
340
+ ...(confirm ? { confirm_domain: confirm } : {}),
341
+ },
342
+ });
343
+ });
344
+ }))
345
+ .addCommand(new Command("upload-avatar")
346
+ .description("Host a one-off mailbox profile picture and print the URL to pass as profile_picture_url in --mailboxes JSON. For a reusable sender identity used by FUTURE --sender <id> orders, prefer `oxygen senders profiles set-photo <id> --file <path>` instead. The inbox vendor FETCHES the hosted URL when it provisions the mailbox, often long after the order, so it must be public and permanent. Uploads a PNG, JPEG, or WebP (max 8MB), then normalizes it to a metadata-free 400x400 PNG at a matching .png URL. This only hosts the image: it does not update a sender, place an order, call the inbox provider, or charge credits.")
347
+ .argument("<path>", "Path to a PNG, JPEG, or WebP image.")
348
+ .option("--json", "Print a JSON envelope.")
349
+ .action(async (path, options) => {
350
+ await handleAsyncAction("managed-inboxes upload-avatar", options, async () => {
351
+ const storageKey = await putAvatarBytesToStorage(path);
352
+ // Confirming against the avatars route is what turns the staged
353
+ // bytes into a public URL — the server sniffs the real bytes here,
354
+ // so a .png that is actually a PDF is caught before an order.
355
+ return await requestOxygen("/api/cli/managed-inboxes/avatars", {
356
+ method: "PATCH",
357
+ body: { storage_key: storageKey },
358
+ });
359
+ });
360
+ })));
361
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerMessagesFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,78 @@
1
+ import { requestOxygen } from "../http-client.js";
2
+ import { readOption } from "../util.js";
3
+ import { Command } from "commander";
4
+ export function registerMessagesFamily(program, handleAsyncAction) {
5
+ program.addCommand(new Command("messages")
6
+ .description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus INBOX-WIDE conversation analytics. NOT campaign performance: for per-channel campaign reply rates use `oxygen sequences analytics`. Message-level, unlike inbox (conversation-level triage).")
7
+ .addCommand(new Command("query")
8
+ .description("Query messages across channels newest first, or relevance-ranked when -q is set. --channel all merges email + LinkedIn + WhatsApp (narrow with --channels); a campaign filter (--sequence-id) restricts to email. Filter by direction, account, contact, status, and date range. Scope: the Messages store (Unibox conversations, every native email since v1.927.0 linked to its campaign; earlier sends without a provider thread id are not). The complete send ledger of a sequence is `sequences events --kind sent`; its counts are `sequences stats`.")
9
+ .option("-q, --query <text>", "Full-text search over message bodies (relevance-ranked).")
10
+ .option("--channel <channel>", "Channel: all (merged, default), email, linkedin, or whatsapp.")
11
+ .option("--channels <list>", "channel=all only: comma-separated channels to include (email,linkedin,whatsapp). Empty = all three.")
12
+ .option("--direction <direction>", "Filter by direction: inbound or outbound.")
13
+ .option("--account <id>", "Mailbox id (email) or sender account id (DM) to filter to.")
14
+ .option("--contact <text>", "Filter by counterpart name or handle (address / provider id).")
15
+ .option("--sequence-id <id>", "Email only: campaign (sequence) id (restricts the result to email).")
16
+ .option("--status <keys>", "Comma-separated conversation status keys (e.g. interested,meeting_booked).")
17
+ .option("--tag <tag>", "Only messages whose conversation carries this workspace tag (see `oxygen tags list`).")
18
+ .option("--since <iso>", "Only messages sent at or after this ISO timestamp.")
19
+ .option("--until <iso>", "Only messages sent before this ISO timestamp.")
20
+ .option("--limit <n>", "Maximum messages to return (1-200). Defaults to 50.")
21
+ .option("--cursor <c>", "Pagination cursor from a previous page's next_cursor (recency path only).")
22
+ .option("--json", "Print a JSON envelope.")
23
+ .action(async (options) => {
24
+ await handleAsyncAction("messages query", options, () => {
25
+ const params = new URLSearchParams();
26
+ const query = readOption(options.query);
27
+ if (query)
28
+ params.set("q", query);
29
+ for (const [flag, key] of [
30
+ ["channel", "channel"],
31
+ ["channels", "channels"],
32
+ ["direction", "direction"],
33
+ ["account", "account"],
34
+ ["contact", "contact"],
35
+ ["sequenceId", "sequence_id"],
36
+ ["status", "status"],
37
+ ["tag", "tag"],
38
+ ["since", "since"],
39
+ ["until", "until"],
40
+ ["limit", "limit"],
41
+ ["cursor", "cursor"],
42
+ ]) {
43
+ const value = readOption(options[flag]);
44
+ if (value)
45
+ params.set(key, value);
46
+ }
47
+ const suffix = params.toString();
48
+ return requestOxygen(`/api/cli/messages${suffix ? `?${suffix}` : ""}`);
49
+ });
50
+ }))
51
+ .addCommand(new Command("stats")
52
+ .description("INBOX-WIDE message-ledger analytics over the WHOLE corpus, not your campaigns: outbound/inbound totals, a conversation-level reply rate (has-inbound over has-outbound) by channel, status breakdowns, response-time percentiles, top counterpart domains, and winning openers. It counts every conversation in the workspace, including mail no campaign sent, so its reply rate is NOT your campaign reply rate — for that, and for per-channel campaign performance, use `oxygen sequences analytics`. byCampaign is email-only because direct-message conversations carry no campaign link; an empty list there is inaccessible, not absence. Scope with --sequence-id, --channel, and a date range. Defaults to the last 30 days; pass --all-time for full history.")
53
+ .option("--sequence-id <ids>", "Comma-separated campaign (sequence) ids to scope to.")
54
+ .option("--channel <channel>", "Channel: all (default), email, linkedin, or whatsapp.")
55
+ .option("--since <iso>", "Only messages sent at or after this ISO timestamp. Defaults to 30 days ago.")
56
+ .option("--until <iso>", "Only messages sent before this ISO timestamp.")
57
+ .option("--all-time", "Scan full message history instead of the default trailing 30 days. Ignored if --since or --until is also set.")
58
+ .option("--json", "Print a JSON envelope.")
59
+ .action(async (options) => {
60
+ await handleAsyncAction("messages stats", options, () => {
61
+ const params = new URLSearchParams();
62
+ for (const [flag, key] of [
63
+ ["sequenceId", "sequence_id"],
64
+ ["channel", "channel"],
65
+ ["since", "since"],
66
+ ["until", "until"],
67
+ ]) {
68
+ const value = readOption(options[flag]);
69
+ if (value)
70
+ params.set(key, value);
71
+ }
72
+ if (options.allTime)
73
+ params.set("all_time", "true");
74
+ const suffix = params.toString();
75
+ return requestOxygen(`/api/cli/messages/stats${suffix ? `?${suffix}` : ""}`);
76
+ });
77
+ })));
78
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerNotetakerFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,79 @@
1
+ import { readPositiveInt, resolveLiveDryRunMode } from "../cli-values.js";
2
+ import { parseJsonArray, readPositiveNumber } from "../command-support.js";
3
+ import { requestOxygen } from "../http-client.js";
4
+ import { readOption } from "../util.js";
5
+ import { Command } from "commander";
6
+ function buildNotetakerSetupBody(options) {
7
+ return {
8
+ mode: resolveLiveDryRunMode(options),
9
+ enabled: options.disabled === true ? false : true,
10
+ capture_mode: "invited_bot",
11
+ auto_record_scope: "manual",
12
+ ...(readOption(options.botName) ? { bot_name: readOption(options.botName) } : {}),
13
+ ...(options.botAvatarUrl !== undefined ? { bot_avatar_url: readOption(options.botAvatarUrl) ?? null } : {}),
14
+ ...(options.consentDisclosure !== undefined ? { consent_disclosure: readOption(options.consentDisclosure) ?? null } : {}),
15
+ ...(readPositiveInt(options.defaultMaxMinutes) ? { default_max_minutes: readPositiveInt(options.defaultMaxMinutes) } : {}),
16
+ ...(readPositiveNumber(options.defaultMaxCredits) ? { default_max_credits: readPositiveNumber(options.defaultMaxCredits) } : {}),
17
+ ...(readPositiveInt(options.retentionDays) ? { retention_days: readPositiveInt(options.retentionDays) } : {}),
18
+ };
19
+ }
20
+ function buildNotetakerScheduleBody(meetingUrl, options) {
21
+ return {
22
+ mode: resolveLiveDryRunMode(options),
23
+ meeting_url: meetingUrl,
24
+ approved: options.approved === true,
25
+ ...(readOption(options.title) ? { title: readOption(options.title) } : {}),
26
+ ...(readOption(options.joinAt) ? { join_at: readOption(options.joinAt) } : {}),
27
+ ...(readPositiveInt(options.maxMinutes) ? { max_minutes: readPositiveInt(options.maxMinutes) } : {}),
28
+ ...(readPositiveNumber(options.maxCredits) ? { max_credits: readPositiveNumber(options.maxCredits) } : {}),
29
+ ...(readOption(options.crmLinksJson) ? { crm_links: parseJsonArray(readOption(options.crmLinksJson) ?? "") } : {}),
30
+ };
31
+ }
32
+ export function registerNotetakerFamily(program, handleAsyncAction) {
33
+ program
34
+ .command("notetaker")
35
+ .description("AI meeting notetaker setup, scheduling, and session inspection.")
36
+ .addCommand(new Command("setup")
37
+ .description("Configure the Recall-backed Oxygen Notetaker. Defaults to dry-run.")
38
+ .option("--bot-name <name>", "Meeting bot display name. Defaults to Oxygen Notetaker.")
39
+ .option("--bot-avatar-url <url>", "Meeting bot avatar URL; pass an empty string to clear it.")
40
+ .option("--consent-disclosure <text>", "Recording disclosure; pass an empty string to restore the default disclosure.")
41
+ .option("--disabled", "Save the notetaker as disabled when used with --live.")
42
+ .option("--default-max-minutes <n>", "Default maximum meeting minutes to reserve. Defaults to 60.")
43
+ .option("--default-max-credits <n>", "Default spend cap for live schedules.")
44
+ .option("--retention-days <n>", "Transcript/artifact retention window. Defaults to 30.")
45
+ .option("--dry-run", "Preview setup without writing settings.")
46
+ .option("--live", "Apply notetaker settings.")
47
+ .option("--json", "Print a JSON envelope.")
48
+ .action(async (options) => {
49
+ await handleAsyncAction("notetaker setup", options, () => requestOxygen("/api/cli/notetaker/setup", {
50
+ method: "POST",
51
+ body: buildNotetakerSetupBody(options),
52
+ }));
53
+ }))
54
+ .addCommand(new Command("schedule")
55
+ .description("Schedule an invited meeting bot. Defaults to dry-run.")
56
+ .argument("<meeting_url>", "Zoom, Google Meet, or Teams meeting URL.")
57
+ .option("--title <title>", "Meeting title for the CRM timeline.")
58
+ .option("--join-at <iso>", "ISO date-time for scheduled bot join.")
59
+ .option("--max-minutes <n>", "Maximum meeting minutes to reserve.")
60
+ .option("--max-credits <n>", "Required spend cap for live schedules.")
61
+ .option("--crm-links-json <json>", "JSON array of CRM links: [{\"object\":\"companies\",\"row_id\":\"...\",\"role\":\"account\"}].")
62
+ .option("--approved", "Approve the live paid Recall bot schedule.")
63
+ .option("--dry-run", "Preview schedule and credit estimate without creating a bot.")
64
+ .option("--live", "Create the Recall bot after approval and credit reservation.")
65
+ .option("--json", "Print a JSON envelope.")
66
+ .action(async (meetingUrl, options) => {
67
+ await handleAsyncAction("notetaker schedule", options, () => requestOxygen("/api/cli/notetaker/schedule", {
68
+ method: "POST",
69
+ body: buildNotetakerScheduleBody(meetingUrl, options),
70
+ }));
71
+ }))
72
+ .addCommand(new Command("status")
73
+ .description("Get one notetaker session with artifacts and credit state.")
74
+ .argument("<session_id>", "Notetaker session id.")
75
+ .option("--json", "Print a JSON envelope.")
76
+ .action(async (sessionId, options) => {
77
+ await handleAsyncAction("notetaker status", options, () => requestOxygen(`/api/cli/notetaker/sessions/${encodeURIComponent(sessionId)}`));
78
+ }));
79
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerObservabilityFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;