@oxygen-agent/cli 1.1014.136 → 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,130 @@
1
+ import { readNonNegativeInt } from "../cli-values.js";
2
+ import { requestOxygen } from "../http-client.js";
3
+ import { readOption } from "../util.js";
4
+ import { Command } from "commander";
5
+ export function registerEgressFamily(program, handleAsyncAction) {
6
+ program.addCommand(new Command("egress")
7
+ .description("Native email egress: status, shared/dedicated IP inventory, the dedicated-IP add-on, policy assignments, and a fail-closed retired rotation shim.")
8
+ .addCommand(new Command("status")
9
+ .description("Show this workspace's configured native email routing, active IP, transport readiness, health counts, dedicated add-on, and one bounded page of the mailbox policy ledger. Configuration and readiness do not prove observed traffic. Native Gmail/Microsoft sends, reply reads and token refreshes are configured for the workspace email proxy; Zapmail-connected (Zapbox) mailboxes send through Zapmail. Read-only, 0 credits.")
10
+ .option("--assignment-offset <number>", "Continue the policy ledger from open_assignments_next_offset.")
11
+ .option("--json", "Print a JSON envelope.")
12
+ .action(async (options) => {
13
+ const assignmentOffset = readNonNegativeInt(options.assignmentOffset);
14
+ const query = assignmentOffset === undefined
15
+ ? ""
16
+ : `?assignment_offset=${encodeURIComponent(String(assignmentOffset))}`;
17
+ await handleAsyncAction("egress status", options, () => requestOxygen(`/api/cli/egress${query}`));
18
+ }))
19
+ .addCommand(new Command("ips")
20
+ .description("List the configured shared email-proxy IP and this workspace's dedicated IP history. A configured address does not prove observed traffic; inspect readiness and verification fields with `egress status`. Read-only, 0 credits.")
21
+ .option("--json", "Print a JSON envelope.")
22
+ .action(async (options) => {
23
+ await handleAsyncAction("egress ips", options, () => requestOxygen("/api/cli/egress?view=ips"));
24
+ }))
25
+ .addCommand(new Command("dedicated")
26
+ .description("Dedicated native egress add-on (2,500 credits per 30-day period): one static IP only this workspace uses, which IT can allowlist; workspace tenant isolation and account safety, not a deliverability or inbox-placement lever. Preview the quote, order with `request --approved`, cancel with `cancel --approve`, or check status.")
27
+ .addCommand(new Command("request")
28
+ .description("Preview the dedicated native egress add-on (2,500 credits per 30-day period, $25 face value). With --approved, ORDER it: OXYGEN provisions one isolated email proxy plus one static egress IP for the workspace, captures the first period only after the proxy is ready, and debits the next 30 days later — not on the 1st. Every current inbox is assigned to that IP, every future inbox joins it automatically, and after the guarded handoff every native send, reply read and token refresh for the workspace's mailboxes uses that one IP, so IT can allowlist it in Microsoft Conditional Access or Google context-aware access. Recipients of Gmail API and Microsoft Graph sends still see Google/Microsoft relay IPs; a Microsoft mailbox on the SMTP transport may show the sending IP, shared or dedicated, in its headers. This is workspace tenant isolation and account safety, not a deliverability or inbox-placement lever. Without --approved nothing is ordered or charged. While native provisioning is unavailable, ordering fails closed and the preview says so.")
29
+ // --approved (not --approve): the credit-spending approval flag,
30
+ // which is also what derives spends_credits in the self-index.
31
+ .option("--approved", "Execute the order (a real recurring credit charge). Omit for a no-side-effect preview.")
32
+ // Deliberately NOT named like an approval flag: the self-index
33
+ // derives spends_credits from --approved/--max-credits/--budget-credits
34
+ // appearing in a command's flag strings, so an approval-shaped name
35
+ // here would mislabel every command that carries it.
36
+ .option("--move-existing", "Deprecated compatibility flag. Existing inboxes now move to the dedicated IP automatically, so old scripts may keep passing this safely.")
37
+ .option("--country <code>", "ISO 3166-1 alpha-2 country the dedicated IP should sit in (e.g. US, DE). Match it to where your mailboxes' owners plausibly sign in from — the IP's job is making the sign-in look ordinary, and an account that suddenly authenticates from another country is what gets it challenged. Omitted uses the default region. An unserviceable or out-of-stock country fails before anything is ordered or charged.")
38
+ .option("--idempotency-key <key>", "Stable key for safely retrying the same order after a timeout. Reuse the key shown in a timeout error; use a new key only for an intentionally new purchase.")
39
+ .option("--json", "Print a JSON envelope.")
40
+ .action(async (options) => {
41
+ const idempotencyKey = readOption(options.idempotencyKey);
42
+ await handleAsyncAction("egress dedicated request", options, () => requestOxygen("/api/cli/egress/dedicated", {
43
+ method: "POST",
44
+ ...(idempotencyKey ? { idempotencyKey } : {}),
45
+ body: {
46
+ ...(options.approved ? { approve: true } : {}),
47
+ ...(options.moveExisting ? { move_existing: true } : {}),
48
+ ...(readOption(options.country) ? { country: readOption(options.country) } : {}),
49
+ },
50
+ }));
51
+ }))
52
+ .addCommand(new Command("cancel")
53
+ .description("Cancel the dedicated sending-IP add-on. Approval-gated: without --approve it previews. Cancellation takes effect at the END of the 30-day period already paid for: the dedicated IP remains active until then, and only at period end does it retire, the workspace returns to the shared email proxy, and credit billing stops. No refund, no early cutoff.")
54
+ .option("--approve", "Execute the cancellation. Omit for a no-side-effect preview.")
55
+ .option("--json", "Print a JSON envelope.")
56
+ .action(async (options) => {
57
+ await handleAsyncAction("egress dedicated cancel", options, () => requestOxygen("/api/cli/egress/dedicated", {
58
+ method: "POST",
59
+ body: { cancel: true, ...(options.approve ? { approve: true } : {}) },
60
+ }));
61
+ }))
62
+ .addCommand(new Command("status")
63
+ .description("Show this org's dedicated sending-IP add-on state (status, quantity, activation, price). Read-only, 0 credits.")
64
+ .option("--json", "Print a JSON envelope.")
65
+ .action(async (options) => {
66
+ await handleAsyncAction("egress dedicated status", options, () => requestOxygen("/api/cli/egress/dedicated"));
67
+ })))
68
+ .addCommand(new Command("rotate")
69
+ .description("RETIRED compatibility command. Native egress is workspace-scoped, so OXYGEN cannot rotate one mailbox without violating the workspace's shared-or-one-dedicated routing contract. The API always fails closed with no provider call or change.")
70
+ .requiredOption("--mailbox <id>", "Mailbox id to rotate.")
71
+ .option("--vendor <vendor>", "Retired compatibility input; never used for a provider call.")
72
+ .option("--approve", "Retired compatibility flag; no rotation is executed.")
73
+ .option("--json", "Print a JSON envelope.")
74
+ .action(async (options) => {
75
+ await handleAsyncAction("egress rotate", options, () => requestOxygen("/api/cli/egress/rotate", {
76
+ method: "POST",
77
+ body: {
78
+ mailbox_id: readOption(options.mailbox),
79
+ ...(readOption(options.vendor) ? { vendor: readOption(options.vendor) } : {}),
80
+ ...(options.approve ? { approve: true } : {}),
81
+ },
82
+ }));
83
+ }))
84
+ .addCommand(new Command("register")
85
+ .description("STAFF RECOVERY: register legacy/manual logical policy inventory for this environment. This does not configure native send transport; dedicated email proxies are created only by `egress dedicated request`. Secrets never pass here — --credential-ref names the Doppler family EGRESS_CRED_<REF>_USERNAME/_PASSWORD, set separately in Doppler.")
86
+ .requiredOption("--host <host>", "Proxy hostname or IP — bare, no scheme/port/credentials.")
87
+ .requiredOption("--port <port>", "Proxy port (1-65535).")
88
+ .requiredOption("--credential-ref <ref>", "Doppler credential-family name (A-Z, 0-9, _).")
89
+ .requiredOption("--vendor <vendor>", "Vendor slug for inventory provenance.")
90
+ .option("--protocol <protocol>", "Proxy protocol: http (default), https, or socks5.")
91
+ .option("--tier <tier>", "shared (default) or dedicated (requires --dedicated-org).")
92
+ .option("--dedicated-org <orgId>", "Organization id a dedicated-tier IP is reserved for.")
93
+ .option("--session-label <label>", "Optional vendor session/order label for provenance.")
94
+ .option("--json", "Print a JSON envelope.")
95
+ .action(async (options) => {
96
+ await handleAsyncAction("egress register", options, () => requestOxygen("/api/cli/egress/register", {
97
+ method: "POST",
98
+ body: {
99
+ host: readOption(options.host),
100
+ port: Number(readOption(options.port)),
101
+ credential_ref: readOption(options.credentialRef),
102
+ vendor: readOption(options.vendor),
103
+ ...(readOption(options.protocol) ? { protocol: readOption(options.protocol) } : {}),
104
+ ...(readOption(options.tier) ? { tier: readOption(options.tier) } : {}),
105
+ ...(readOption(options.dedicatedOrg) ? { dedicated_organization_id: readOption(options.dedicatedOrg) } : {}),
106
+ ...(readOption(options.sessionLabel) ? { session_label: readOption(options.sessionLabel) } : {}),
107
+ },
108
+ }));
109
+ }))
110
+ .addCommand(new Command("assign")
111
+ .description("Converge mailbox policy assignments: managed/shared by default, or the workspace's dedicated IP for every current and future inbox once that add-on is active. Existing pins stay stable within their tier; --ip cannot override an active dedicated tier. Admin/owner role; no spend, 0 credits.")
112
+ .option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to assign.")
113
+ .option("--all", "Converge every sending mailbox to the workspace's current shared or dedicated tier.")
114
+ .option("--ip <egressIpId>", "Pin the targeted mailboxes to this egress IP id (must be active and org-visible).")
115
+ .option("--json", "Print a JSON envelope.")
116
+ .action(async (options) => {
117
+ await handleAsyncAction("egress assign", options, () => {
118
+ const list = readOption(options.mailboxes);
119
+ const mailboxes = list ? list.split(",").map((entry) => entry.trim()).filter(Boolean) : [];
120
+ return requestOxygen("/api/cli/egress/assign", {
121
+ method: "POST",
122
+ body: {
123
+ ...(mailboxes.length > 0 ? { mailboxes } : {}),
124
+ ...(options.all ? { all: true } : {}),
125
+ ...(readOption(options.ip) ? { egress_ip_id: readOption(options.ip) } : {}),
126
+ },
127
+ });
128
+ });
129
+ })));
130
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerEmailFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,49 @@
1
+ import { readCsvOption } from "../command-support.js";
2
+ import { requestOxygen } from "../http-client.js";
3
+ import { readOption } from "../util.js";
4
+ import { Command } from "commander";
5
+ import { readFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ export function registerEmailFamily(program, handleAsyncAction) {
8
+ program.addCommand(new Command("email")
9
+ .description("Ad-hoc email from the org's sending fleet: one-off sends with no sequence, enrollment, or contact sync. Zapbox-connected mailboxes send through Zapmail's API (no Google/Microsoft consent); BYOK — 0 Oxygen credits.")
10
+ .addCommand(new Command("send")
11
+ .description("Send ONE email right now. Without --approved, returns a preview naming the exact mailbox + transport. --from pins a mailbox; otherwise the pool's LRU rotation picks one.")
12
+ .requiredOption("--to <email>", "Recipient email address (exactly one).")
13
+ .requiredOption("--subject <text>", "Subject line.")
14
+ .option("--body <text>", "Plain-text body (sent verbatim, no templating).")
15
+ .option("--body-file <path>", "Read the body from a file instead of --body.")
16
+ .option("--html <html>", "Optional HTML body — sends multipart/alternative with --body as the plain-text part.")
17
+ .option("--html-file <path>", "Read the HTML body from a file instead of --html.")
18
+ .option("--display-name <name>", "From display name for this send (overrides the mailbox's stored display name).")
19
+ .option("--cc <list>", "Comma-separated Cc addresses.")
20
+ .option("--bcc <list>", "Comma-separated Bcc addresses.")
21
+ .option("--from <mailbox>", "Sending mailbox (address or id). Defaults to the pool rotation.")
22
+ .option("--approved", "Approve and send. Without this flag, returns a preview only.")
23
+ .option("--json", "Print a JSON envelope.")
24
+ .action(async (options) => {
25
+ await handleAsyncAction("email send", options, async () => {
26
+ const bodyText = readOption(options.body) ?? (options.bodyFile ? readFileSync(resolve(options.bodyFile), "utf8") : undefined);
27
+ if (!bodyText || !bodyText.trim()) {
28
+ throw new Error("Provide --body or --body-file.");
29
+ }
30
+ const htmlBody = readOption(options.html) ?? (options.htmlFile ? readFileSync(resolve(options.htmlFile), "utf8") : undefined);
31
+ const cc = readCsvOption(options.cc);
32
+ const bcc = readCsvOption(options.bcc);
33
+ return requestOxygen("/api/cli/email/send", {
34
+ method: "POST",
35
+ body: {
36
+ to: readOption(options.to),
37
+ subject: readOption(options.subject),
38
+ body: bodyText,
39
+ ...(htmlBody?.trim() ? { html: htmlBody } : {}),
40
+ ...(readOption(options.displayName) ? { display_name: readOption(options.displayName) } : {}),
41
+ ...(cc.length > 0 ? { cc } : {}),
42
+ ...(bcc.length > 0 ? { bcc } : {}),
43
+ ...(readOption(options.from) ? { from: readOption(options.from) } : {}),
44
+ ...(options.approved ? { approved: true } : {}),
45
+ },
46
+ });
47
+ });
48
+ })));
49
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerEngagementFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,189 @@
1
+ import { parseJsonArray, readFileIfPresent, readPositiveInteger, readPositiveNumber } from "../command-support.js";
2
+ import { requestOxygen } from "../http-client.js";
3
+ import { isRecord, readOption } from "../util.js";
4
+ import { Command } from "commander";
5
+ // A post's engagers list is the one payload in this tree that is routinely
6
+ // thousands of lines long, and the field that decides whether you may act on it
7
+ // — `truncated` — is one boolean inside it. Printed raw, a partial audience
8
+ // looks exactly like a whole one until you scroll past every person in it. So
9
+ // lead with the receipt on stderr (the same stdout/stderr split the dry-run and
10
+ // credits notices use, leaving stdout a clean envelope) and say plainly what to
11
+ // do about a short read. `--json` callers are untouched: they read the fields.
12
+ function writeEngagersReceipt(data) {
13
+ if (!data || typeof data !== "object" || Array.isArray(data))
14
+ return;
15
+ const record = data;
16
+ const pages = isRecord(record.pages_read) ? record.pages_read : {};
17
+ const line = (text) => process.stderr.write(`${text}\n`);
18
+ line(`Post ${String(record.post ?? "?")}`);
19
+ line(` ${String(record.total_engagers ?? 0)} engagers`
20
+ + ` (${String(record.reactors_count ?? 0)} reactions, ${String(record.commenters_count ?? 0)} comments)`
21
+ + ` from ${String(pages.reactions ?? 0)}+${String(pages.comments ?? 0)} pages`);
22
+ if (record.truncated === true) {
23
+ const reason = typeof record.partial_reason === "string" ? record.partial_reason : "unknown";
24
+ line(` PARTIAL (${reason}) — this post has more engagers than were read.`);
25
+ line(reason === "max_pages"
26
+ ? " Raise --max-pages (max 20), or run `oxygen engagement harvest` to walk the whole post into a table."
27
+ : " Retry, or run `oxygen engagement harvest` to walk the whole post into a table.");
28
+ }
29
+ if (typeof record.web_url === "string")
30
+ line(` ${record.web_url}`);
31
+ }
32
+ export function registerEngagementFamily(program, handleAsyncAction) {
33
+ program.addCommand(new Command("engagement")
34
+ .description("For a LinkedIn audience campaign, find the matching play with `oxygen recipes list \"post engagers\" --view summary --json`: own, competitor, creator or keyword post engagers, company followers, or own followers. The Recipe and shared guide prepare qualified recipients and a disabled ongoing router. For one known post, read its engagers or harvest into a durable Table using its public URL or composite social_id from `oxygen posts get`; harvesting runs as a paced background drip. Existing intent captures are managed with `oxygen feeds list|pause|resume|run`; existing profile watchers use `oxygen tables watcher get`. To have teammates react to or comment on the workspace's own posts, use `oxygen publishing amplification --help`.")
35
+ .addCommand(new Command("engagers")
36
+ .description("Read one post's reactors and commenters right now as a de-duplicated people list ready to enroll. Pages both sources up to --max-pages (default 5, max 20) x 100 per page; `truncated: true` in the envelope means a cap stopped a source that still had more — raise --max-pages, or use `oxygen engagement harvest` for a post too big to read in one request. Nothing is sent and no credits are spent, but every page is one LinkedIn read against the sender account. --post is the composite social_id from `oxygen posts get` (NOT the activity URN).")
37
+ .requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
38
+ .option("--account <ref>", "Sender account that reads (sender id, connection id, or Unipile account id). Omit for the org default.")
39
+ .option("--limit <n>", "Engagers per PAGE (default 100, which is also the provider maximum). Per source you get --limit x --max-pages.")
40
+ .option("--max-pages <n>", "Pages to walk per source, 1-20 (default 5, so 500 reactors + 500 commenters).")
41
+ .option("--no-reactions", "Skip reactors.")
42
+ .option("--no-comments", "Skip commenters.")
43
+ .option("--json", "Print a JSON envelope.")
44
+ .action(async (options) => {
45
+ await handleAsyncAction("engagement engagers", options, async () => {
46
+ const post = readOption(options.post);
47
+ if (!post)
48
+ throw new Error("--post is required (the composite social_id from `oxygen posts get`).");
49
+ const account = readOption(options.account);
50
+ const limit = readPositiveInteger(options.limit);
51
+ const maxPages = readPositiveInteger(options.maxPages);
52
+ const params = new URLSearchParams({ post });
53
+ if (account)
54
+ params.set("account", account);
55
+ if (limit !== undefined)
56
+ params.set("limit", String(limit));
57
+ // Forwarded as-is: the route owns the 1-20 range so an
58
+ // out-of-range value fails loudly instead of silently reading
59
+ // fewer pages than the caller asked for.
60
+ if (maxPages !== undefined)
61
+ params.set("max_pages", String(maxPages));
62
+ if (options.reactions === false)
63
+ params.set("include_reactions", "false");
64
+ if (options.comments === false)
65
+ params.set("include_comments", "false");
66
+ const data = await requestOxygen(`/api/cli/linkedin/engagement?${params.toString()}`);
67
+ // Ahead of the payload, and only for a human: a machine caller
68
+ // reads truncated/partial_reason off the envelope itself.
69
+ if (!options.json)
70
+ writeEngagersReceipt(data);
71
+ return data;
72
+ });
73
+ }))
74
+ .addCommand(new Command("harvest")
75
+ .description("Start (or re-arm) a harvest of a post's engagers into a workspace table you can enroll into a sequence. Engagers drip into the table over many ticks; poll `engagement status` to watch it fill. No messages are sent. Cookieless harvests spend Oxygen credits per scraper page and require --max-credits.")
76
+ .requiredOption("--post <social_id_or_url>", "Composite post social_id from `oxygen posts get` (NOT the activity URN), or the public LinkedIn post URL for cookieless.")
77
+ .option("--source <source>", "cookieless|unipile. Defaults to cookieless when a public post URL is supplied.")
78
+ .option("--post-url <url>", "Public LinkedIn post URL for cookieless harvesting when --post is a social id.")
79
+ .option("--max-credits <credits>", "Required for cookieless harvesting; caps managed scraper spend for this armed run.")
80
+ .option("--recurring", "Keep re-polling the newest pages for new engagers (default: one walk).")
81
+ .option("--account <ref>", "Sender account that reads (sender id, connection id, or Unipile account id). Omit for the org default.")
82
+ .option("--no-reactions", "Skip reactors.")
83
+ .option("--no-comments", "Skip commenters.")
84
+ .option("--json", "Print a JSON envelope.")
85
+ .action(async (options) => {
86
+ await handleAsyncAction("engagement harvest", options, () => {
87
+ const post = readOption(options.post);
88
+ const account = readOption(options.account);
89
+ const source = readOption(options.source);
90
+ const postUrl = readOption(options.postUrl);
91
+ const maxCredits = readPositiveNumber(options.maxCredits);
92
+ return requestOxygen("/api/cli/linkedin/engagement/harvest", {
93
+ method: "POST",
94
+ body: {
95
+ ...(post ? { post } : {}),
96
+ ...(source ? { source } : {}),
97
+ ...(postUrl ? { post_url: postUrl } : {}),
98
+ ...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
99
+ ...(options.recurring ? { recurring: true } : {}),
100
+ ...(account ? { account } : {}),
101
+ // commander sets reactions/comments=false for --no-* flags.
102
+ ...(options.reactions === false ? { include_reactions: false } : {}),
103
+ ...(options.comments === false ? { include_comments: false } : {}),
104
+ },
105
+ });
106
+ });
107
+ }))
108
+ .addCommand(new Command("status")
109
+ .description("Show an engagement harvest's progress (drip status, engagers ingested, last error) plus the workspace table it fills and a preview of harvested engagers.")
110
+ .option("--account <ref>", "Scope to one sender account (sender id, connection id, or Unipile account id).")
111
+ .option("--post <social_id>", "Scope to one harvested post.")
112
+ .option("--json", "Print a JSON envelope.")
113
+ .action(async (options) => {
114
+ await handleAsyncAction("engagement status", options, () => {
115
+ const account = readOption(options.account);
116
+ const post = readOption(options.post);
117
+ const params = new URLSearchParams();
118
+ if (account)
119
+ params.set("account", account);
120
+ if (post)
121
+ params.set("post", post);
122
+ const suffix = params.toString();
123
+ return requestOxygen(`/api/cli/linkedin/engagement/harvest${suffix ? `?${suffix}` : ""}`);
124
+ });
125
+ }))
126
+ .addCommand(new Command("react")
127
+ .description("React to a LinkedIn post (or to a comment with --comment-id). A real external LinkedIn write that everyone can see and that counts against the sender account's daily action quota, gated behind approval: prints a preview by default and only reacts when you pass --approved. --post is the composite post social_id from `oxygen posts get` (NOT the activity URN).")
128
+ .requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
129
+ .option("--reaction-type <type>", "Reaction kind: like, celebrate, support, love, insightful, or funny (default like).")
130
+ .option("--comment-id <id>", "React to this comment id instead of the post itself.")
131
+ .option("--as-organization <org>", "LinkedIn organization id to react as a company page.")
132
+ .option("--account <ref>", "Sender account that reacts (sender id, connection id, or Unipile account id). Omit for the org default.")
133
+ .option("--approved", "Actually add the reaction. Omit to preview only.")
134
+ .option("--json", "Print a JSON envelope.")
135
+ .action(async (options) => {
136
+ await handleAsyncAction("engagement react", options, () => {
137
+ const post = readOption(options.post);
138
+ const reactionType = readOption(options.reactionType);
139
+ const commentId = readOption(options.commentId);
140
+ const asOrganization = readOption(options.asOrganization);
141
+ const account = readOption(options.account);
142
+ return requestOxygen("/api/cli/linkedin/engagement/react", {
143
+ method: "POST",
144
+ body: {
145
+ ...(post ? { post } : {}),
146
+ ...(reactionType ? { reaction_type: reactionType } : {}),
147
+ ...(commentId ? { comment_id: commentId } : {}),
148
+ ...(asOrganization ? { as_organization: asOrganization } : {}),
149
+ ...(account ? { account } : {}),
150
+ ...(options.approved ? { approved: true } : {}),
151
+ },
152
+ });
153
+ });
154
+ }))
155
+ .addCommand(new Command("comment")
156
+ .description("Comment on a LinkedIn post (or reply to a comment with --comment-id). A real external LinkedIn write that everyone can see and that counts against the sender account's daily action quota, gated behind approval: prints a preview by default and only posts when you pass --approved. --post is the composite post social_id from `oxygen posts get` (NOT the activity URN).")
157
+ .requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (NOT the activity URN).")
158
+ .requiredOption("--text <text>", "Comment text to post.")
159
+ .option("--mentions-json <json-or-file>", "Provider comment mention objects.")
160
+ .option("--comment-id <id>", "Reply under this comment id instead of commenting on the post.")
161
+ .option("--as-organization <org>", "LinkedIn organization id to comment as a company page.")
162
+ .option("--external-link <url>", "URL to attach as a link preview on the comment.")
163
+ .option("--account <ref>", "Sender account that comments (sender id, connection id, or Unipile account id). Omit for the org default.")
164
+ .option("--approved", "Actually post the comment. Omit to preview only.")
165
+ .option("--json", "Print a JSON envelope.")
166
+ .action(async (options) => {
167
+ await handleAsyncAction("engagement comment", options, () => {
168
+ const post = readOption(options.post);
169
+ const text = readOption(options.text);
170
+ const commentId = readOption(options.commentId);
171
+ const asOrganization = readOption(options.asOrganization);
172
+ const externalLink = readOption(options.externalLink);
173
+ const account = readOption(options.account);
174
+ return requestOxygen("/api/cli/linkedin/engagement/comment", {
175
+ method: "POST",
176
+ body: {
177
+ ...(post ? { post } : {}),
178
+ ...(options.mentionsJson !== undefined ? { mentions: parseJsonArray(readFileIfPresent(options.mentionsJson)) } : {}),
179
+ ...(text ? { text } : {}),
180
+ ...(commentId ? { comment_id: commentId } : {}),
181
+ ...(asOrganization ? { as_organization: asOrganization } : {}),
182
+ ...(externalLink ? { external_link: externalLink } : {}),
183
+ ...(account ? { account } : {}),
184
+ ...(options.approved ? { approved: true } : {}),
185
+ },
186
+ });
187
+ });
188
+ })));
189
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerEnrichColumnFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,149 @@
1
+ import { readJsonObjectOption, readPositiveInt } from "../cli-values.js";
2
+ import { readCsvOption, readFilterSelectionOption, readPositiveNumber } from "../command-support.js";
3
+ import { requestOxygen } from "../http-client.js";
4
+ import { readOption } from "../util.js";
5
+ import { OxygenError } from "@oxygen/shared/cli-result";
6
+ import { Command } from "commander";
7
+ function buildEnrichColumnBody(// skipcq: JS-R1005 -- CLI body builder maps enrichment aliases, selection, provider order, and safety caps.
8
+ table, options) {
9
+ const limit = readPositiveInt(options.limit);
10
+ const filterSelection = readFilterSelectionOption(options.filterJson);
11
+ const explicitSelection = readJsonObjectOption(options.selectionJson);
12
+ const selectedModes = [
13
+ Boolean(options.all),
14
+ limit !== undefined,
15
+ Boolean(filterSelection),
16
+ Boolean(explicitSelection),
17
+ ].filter(Boolean).length;
18
+ if (selectedModes > 1) {
19
+ throw new OxygenError("invalid_selection", "Pass only one of --all, --limit, --filter-json, or --selection-json.", {
20
+ exitCode: 1,
21
+ });
22
+ }
23
+ return {
24
+ table,
25
+ ...(readOption(options.sourceColumn) ? { source_column: readOption(options.sourceColumn) } : {}),
26
+ ...(readOption(options.fullNameColumn) ? { full_name_column: readOption(options.fullNameColumn) } : {}),
27
+ ...(readOption(options.firstNameColumn) ? { first_name_column: readOption(options.firstNameColumn) } : {}),
28
+ ...(readOption(options.lastNameColumn) ? { last_name_column: readOption(options.lastNameColumn) } : {}),
29
+ ...(readOption(options.linkedinUrlColumn) ? { linkedin_url_column: readOption(options.linkedinUrlColumn) } : {}),
30
+ ...(readOption(options.emailColumn) ? { email_column: readOption(options.emailColumn) } : {}),
31
+ ...(readOption(options.companyDomainColumn) ? { company_domain_column: readOption(options.companyDomainColumn) } : {}),
32
+ ...(readOption(options.companyNameColumn) ? { company_name_column: readOption(options.companyNameColumn) } : {}),
33
+ ...(readOption(options.companyLinkedinUrlColumn)
34
+ ? { company_linkedin_url_column: readOption(options.companyLinkedinUrlColumn) }
35
+ : {}),
36
+ ...(readOption(options.capability) ? { capability: readOption(options.capability) } : {}),
37
+ ...(readOption(options.targetColumn) ? { target_column: readOption(options.targetColumn) } : {}),
38
+ ...(readOption(options.onExistingManualColumn)
39
+ ? { on_existing_manual_column: readOption(options.onExistingManualColumn) }
40
+ : {}),
41
+ ...(readOption(options.providerOrder) ? { provider_order: readCsvOption(options.providerOrder) } : {}),
42
+ ...(readOption(options.emailWaterfallProfile)
43
+ ? { email_waterfall_profile: readOption(options.emailWaterfallProfile) }
44
+ : {}),
45
+ ...(readOption(options.emailPatternValidation)
46
+ ? { email_pattern_validation: readOption(options.emailPatternValidation) }
47
+ : {}),
48
+ ...(readOption(options.phoneWaterfallProfile)
49
+ ? { phone_waterfall_profile: readOption(options.phoneWaterfallProfile) }
50
+ : {}),
51
+ ...(options.verifyPhone ? { verify_phone: true } : {}),
52
+ ...(options.allowPremiumLanes ? { allow_premium_lanes: true } : {}),
53
+ ...(options.includePersonalEmail ? { include_personal_email: true } : {}),
54
+ ...(readOption(options.phoneVerificationCredentialMode)
55
+ ? { phone_verification_credential_mode: readOption(options.phoneVerificationCredentialMode) }
56
+ : {}),
57
+ ...(options.all ? { selection: { mode: "all" } } : {}),
58
+ ...(filterSelection ? { selection: filterSelection } : {}),
59
+ ...(explicitSelection ? { selection: explicitSelection } : {}),
60
+ ...(limit !== undefined ? { limit } : {}),
61
+ ...(options.onlyMissing ? { only_missing: true } : {}),
62
+ };
63
+ }
64
+ export function registerEnrichColumnFamily(program, handleAsyncAction) {
65
+ program
66
+ .command("enrich-column")
67
+ .description("High-level table enrichment helpers. To see every enrichment a table's columns support, with per-row prices, before choosing one, run `tools search --for-table <table>` (0 credits).")
68
+ .addCommand(new Command("preview")
69
+ .description("Preflight an enrichment column without provider calls or credit usage. A lane on your own key (BYOK) is quoted at OXYGEN's platform fee per successful call, including a successful miss; vendor charges follow your provider's terms (provider_cost_breakdown[].byok_platform_fee_credits_per_call).")
70
+ .argument("<table>", "Table id or slug.")
71
+ .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
72
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
73
+ .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
74
+ .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
75
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by BlitzAPI and enough for Prospeo, Hunter, ContactOut, BetterContact, People Data Labs, or Crustdata on your own key.")
76
+ .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
77
+ .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
78
+ .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
79
+ .option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
80
+ .option("--capability <capability>", "What to run: mobile_phone, work_email, linkedin_url (find a missing value), or verify_email (grade an email the row already has). For a personal email add --include-personal-email to work_email (personal_email alone still works). Defaults to mobile_phone.")
81
+ .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
82
+ .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
83
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
84
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults route by cost and coverage for whichever inputs are mapped.")
85
+ .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
86
+ .option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
87
+ .option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
88
+ .option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr), until the 2026-09 repricing date, when a LinkedIn URL starts billing only on a hit; it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
89
+ .option("--include-personal-email", "work_email only: also find the personal (non-work) email from the LinkedIn URL column, billed only on a hit; the preview adds its price and the address lands in the cell's personal_email.")
90
+ .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
91
+ .option("--limit <n>", "Rows to estimate. Defaults to 10.")
92
+ .option("--all", "Estimate all rows.")
93
+ .option("--filter-json <json>", "Filter object or array, e.g. '{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}'.")
94
+ .option("--selection-json <json>", "Full row selection object, e.g. '{\"mode\":\"filter\",\"filters\":[{\"column\":\"mobile_phone_e164\",\"op\":\"is_null\"}],\"limit\":100}'.")
95
+ .option("--only-missing", "Estimate rows missing the capability's normalized output when no explicit selection is passed.")
96
+ .option("--json", "Print a JSON envelope.")
97
+ .action(async (table, options) => {
98
+ await handleAsyncAction("enrich-column preview", options, () => requestOxygen("/api/cli/enrich-column/preview", {
99
+ method: "POST",
100
+ body: buildEnrichColumnBody(table, options),
101
+ }));
102
+ }))
103
+ .addCommand(new Command("run")
104
+ .description("Create or reuse an enrichment column and queue a background run.")
105
+ .argument("<table>", "Table id or slug.")
106
+ .option("--source-column <column>", "Source column key or id. For mobile_phone this is the LinkedIn URL column.")
107
+ .option("--full-name-column <column>", "Column key or id containing the person's full name for work_email. Pair with company-domain/name for Prospeo, LeadMagic, Hunter, Dropleads, ContactOut, BetterContact, or People Data Labs.")
108
+ .option("--first-name-column <column>", "Column key or id containing the person's first name for work_email.")
109
+ .option("--last-name-column <column>", "Column key or id containing the person's last name for work_email.")
110
+ .option("--linkedin-url-column <column>", "Column key or id containing the person's LinkedIn URL. Required by BlitzAPI and enough for Prospeo, Hunter, ContactOut, BetterContact, People Data Labs, or Crustdata on your own key.")
111
+ .option("--email-column <column>", "Column key or id containing a known work email for providers that accept email as an identity fallback.")
112
+ .option("--company-domain-column <column>", "Column key or id containing the company domain for work_email. Pair with full-name/first+last for name+company providers.")
113
+ .option("--company-name-column <column>", "Column key or id containing the company name for work_email when no domain is available.")
114
+ .option("--company-linkedin-url-column <column>", "Column key or id containing the company's LinkedIn URL for company identity fallback.")
115
+ .requiredOption("--max-credits <credits>", "Required credit ceiling for the queued run.")
116
+ .option("--capability <capability>", "What to run: mobile_phone, work_email, linkedin_url (find a missing value), or verify_email (grade an email the row already has). For a personal email add --include-personal-email to work_email (personal_email alone still works). Defaults to mobile_phone.")
117
+ .option("--target-column <column>", "Target enrichment column key. Defaults to the capability payload column.")
118
+ .option("--on-existing-manual-column <mode>", "How to handle an existing manual target: error, write_if_empty, or create_enrichment_column.")
119
+ .option("--provider-order <providers>", "Comma-separated provider order. Overrides the default cost-aware waterfall profile.")
120
+ .option("--email-waterfall-profile <profile>", "Work-email waterfall profile: auto, name_domain, linkedin_url, or first_last_domain. Defaults route by cost and coverage for whichever inputs are mapped.")
121
+ .option("--email-pattern-validation <mode>", "Work-email pattern pre-step: leadmagic_valid_only or disabled.")
122
+ .option("--phone-waterfall-profile <profile>", "Phone waterfall profile: auto (input-aware), linkedin_url, email, or name_domain. Auto picks the cheapest cost-ordered provider set for each row's inputs (mobile match rate ~30-60%).")
123
+ .option("--verify-phone", "Validate found phone numbers with ClearoutPhone (adds phone_line_type + phone_carrier; filter phone_line_type=mobile for mobile-only).")
124
+ .option("--allow-premium-lanes", "Opt in to managed lanes billing over 50 credits per call. Only linkedin_url has one (LeadMagic email_to_profile, 122.5cr), until the 2026-09 repricing date, when a LinkedIn URL starts billing only on a hit; it is off by default so an unattended run can't bill-shock. No effect on mobile_phone, work_email or verify_email.")
125
+ .option("--include-personal-email", "work_email only: also find the personal (non-work) email from the LinkedIn URL column, billed only on a hit; the preview adds its price and the address lands in the cell's personal_email.")
126
+ .option("--phone-verification-credential-mode <mode>", "ClearoutPhone credential mode for phone verification: managed or user_api_key.")
127
+ .option("--limit <n>", "Rows to queue.")
128
+ .option("--all", "Queue all rows.")
129
+ .option("--filter-json <json>", "Filter object or array for server-side row selection.")
130
+ .option("--selection-json <json>", "Full row selection object for server-side row selection.")
131
+ .option("--only-missing", "Queue rows missing the capability's normalized output when no explicit selection is passed.")
132
+ .option("--force", "Re-run rows with an existing target enrichment value.")
133
+ .option("--max-concurrency <n>", "Maximum concurrent row items for this enrichment run (1-1000; a ceiling shared with worker and provider capacity). Defaults to 20.")
134
+ .option("--json", "Print a JSON envelope.")
135
+ .option("--approved", "Approve THIS run after inspecting `enrich-column preview` (free). Paid enrichment runs fail with approval_required (exit 7) without it.")
136
+ .action(async (table, options) => {
137
+ const maxConcurrency = readPositiveInt(options.maxConcurrency);
138
+ await handleAsyncAction("enrich-column run", options, () => requestOxygen("/api/cli/enrich-column/run", {
139
+ method: "POST",
140
+ body: {
141
+ ...buildEnrichColumnBody(table, options),
142
+ max_credits: readPositiveNumber(options.maxCredits),
143
+ ...(options.approved ? { approved: true } : {}),
144
+ ...(maxConcurrency ? { max_concurrency: maxConcurrency } : {}),
145
+ ...(options.force ? { force: true } : {}),
146
+ },
147
+ }));
148
+ }));
149
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerEnrichmentFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
@@ -0,0 +1,49 @@
1
+ import { requestOxygen } from "../http-client.js";
2
+ import { readOption } from "../util.js";
3
+ import { Command } from "commander";
4
+ export function registerEnrichmentFamily(program, handleAsyncAction) {
5
+ program
6
+ .command("enrichment")
7
+ .description("The enrichment catalogue: name the DATA (work email, phone, headcount, funding, tech stack, decision makers), see its status, expected vs up-to credits per row, coverage and the exact add command — never pick a provider by hand.")
8
+ .addCommand(new Command("catalog")
9
+ .description("Name the DATA, not the provider: list every data type you can ask a table to fill (person profile, work email, phone, headcount, funding, tech stack, hiring, competitors, news, decision makers, signals ...) as `intents`, each with its status (default | selectable | no_default | coming_soon | rejected), the inputs it needs, the fields it fills, region coverage bands (vendor claims until measured), the provider order behind it as metadata, a per-row credit estimate (expected on measured-or-assumed hit rates, plus the up-to ceiling) and the exact add command on CLI, MCP and web. `entries` keeps the column presets, capabilities and recipes. Read this before `columns add --preset` or `--capability`; 0 credits. (Extract column templates have their own `columns catalog`.)")
10
+ .option("--intent <id>", "One intent id, e.g. find_work_email, enrich_company:funding, people_search:account_contacts.")
11
+ .option("--group <group>", "Filter intents to one group: person, contact, company, decision_maker, or signal.")
12
+ .option("--region <region>", "Show the coverage band for one region and hide rows the vendors exclude there: US, UK, DACH, EU_REST, or APAC.")
13
+ .option("--status <status>", "Filter intents to one status: default, selectable, no_default, coming_soon, or rejected.")
14
+ .option("--category <category>", "Filter the column entries to one category: Person, Contact, Company, or Ads.")
15
+ .option("--json", "Print a JSON envelope.")
16
+ .action(async (options) => {
17
+ await handleAsyncAction("enrichment catalog", options, async () => {
18
+ const params = new URLSearchParams();
19
+ for (const key of ["intent", "group", "region", "status"]) {
20
+ const value = readOption(options[key]);
21
+ if (value)
22
+ params.set(key, value);
23
+ }
24
+ const query = params.toString();
25
+ const envelope = await requestOxygen(`/api/cli/enrichment/catalog${query ? `?${query}` : ""}`, { method: "GET" });
26
+ const wanted = readOption(options.category)?.toLowerCase();
27
+ if (!wanted || !Array.isArray(envelope.entries))
28
+ return envelope;
29
+ const entries = envelope.entries.filter((entry) => String(entry.category ?? "").toLowerCase() === wanted);
30
+ return { ...envelope, entries, category: wanted };
31
+ });
32
+ }))
33
+ .addCommand(new Command("apply-default-cascade")
34
+ .description("Patch an existing enrichment column to the server-side default provider cascade for its intent.")
35
+ .argument("<table>", "Table id or slug.")
36
+ .argument("<column>", "Enrichment column id or key.")
37
+ .option("--dry-run", "Return the would-be merged definition without writing.")
38
+ .option("--json", "Print a JSON envelope.")
39
+ .action(async (table, column, options) => {
40
+ await handleAsyncAction("enrichment apply-default-cascade", options, () => requestOxygen("/api/cli/enrichment/apply-default-cascade", {
41
+ method: "POST",
42
+ body: {
43
+ table,
44
+ column,
45
+ ...(options.dryRun ? { dry_run: true } : {}),
46
+ },
47
+ }));
48
+ }));
49
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerFeedbackFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;