@oxygen-agent/cli 1.1014.136 → 1.1014.619

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 (293) 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 +1672 -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 +3505 -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 +20 -16
  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.d.ts +8 -0
  248. package/node_modules/@oxygen/shared/dist/file-import.js +21 -2
  249. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
  250. package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
  251. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
  252. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
  253. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
  254. package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
  255. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
  256. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
  257. package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
  258. package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
  259. package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
  260. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
  261. package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
  262. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
  263. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
  264. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  265. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  266. package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
  267. package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
  268. package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
  269. package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
  270. package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
  271. package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
  272. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
  273. package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
  274. package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
  275. package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
  276. package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
  277. package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
  278. package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
  279. package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
  280. package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
  281. package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
  282. package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
  283. package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
  284. package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
  285. package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
  286. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
  287. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  288. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  289. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
  290. package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
  291. package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
  292. package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
  293. package/package.json +1 -1
@@ -0,0 +1,651 @@
1
+ import { emitCliFailure, handleAsyncAction, writeJson } from "../cli-output.js";
2
+ import { ansi, readCsvOption, readSelectedTranscriptExcerpt } from "../command-support.js";
3
+ import { requestOxygen } from "../http-client.js";
4
+ import { captureCurrentTranscript, TranscriptCaptureError } from "../transcript.js";
5
+ import { isRecord, readOption } from "../util.js";
6
+ import { OxygenError, success } from "@oxygen/shared/cli-result";
7
+ import { Command } from "commander";
8
+ import { stdout as output } from "node:process";
9
+ function buildSupportTicketBody(options) {
10
+ const message = readOption(options.body);
11
+ if (!message) {
12
+ throw new OxygenError("invalid_input", 'Provide a support message with --body "...".', { exitCode: 1 });
13
+ }
14
+ const body = { subject: readOption(options.subject), body: message };
15
+ const sessionId = readOption(options.sessionId);
16
+ const file = readOption(options.transcriptFile);
17
+ if (options.transcript !== false && options.transcriptText !== undefined) {
18
+ if (options.includeTranscript || sessionId || file) {
19
+ throw new OxygenError("invalid_input", "Choose --transcript-text or one local transcript option.", { exitCode: 1 });
20
+ }
21
+ body.transcript_text = readSelectedTranscriptExcerpt(options.transcriptText);
22
+ }
23
+ if (options.transcript !== false && (options.includeTranscript || sessionId || file)) {
24
+ try {
25
+ const captured = captureCurrentTranscript({ sessionId, file });
26
+ if (!captured) {
27
+ throw new OxygenError("transcript_not_found", "No local agent transcript was found. Choose --session-id or --transcript-file.", { exitCode: 1 });
28
+ }
29
+ const { path: _path, ...payload } = captured;
30
+ body.transcript = payload;
31
+ }
32
+ catch (error) {
33
+ if (error instanceof TranscriptCaptureError) {
34
+ throw new OxygenError("transcript_not_found", error.message, { exitCode: 1 });
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ if (readOption(options.severity))
40
+ body.severity = readOption(options.severity);
41
+ if (readOption(options.category))
42
+ body.category = readOption(options.category);
43
+ if (readOption(options.source))
44
+ body.source = readOption(options.source);
45
+ const context = {};
46
+ if (readOption(options.cliVersion))
47
+ context.cli_version = readOption(options.cliVersion);
48
+ if (readOption(options.operation))
49
+ context.operation = readOption(options.operation);
50
+ if (readOption(options.errorCode))
51
+ context.error_code = readOption(options.errorCode);
52
+ if (readOption(options.slackPermalink))
53
+ context.slack_permalink = readOption(options.slackPermalink);
54
+ if (readOption(options.slackChannelId))
55
+ context.slack_channel_id = readOption(options.slackChannelId);
56
+ if (readOption(options.customerWorkspace))
57
+ context.customer_workspace = readOption(options.customerWorkspace);
58
+ const runIds = readCsvOption(options.runIds);
59
+ const tableIds = readCsvOption(options.tableIds);
60
+ const deepLinks = readCsvOption(options.deepLinks);
61
+ if (runIds.length)
62
+ context.run_ids = runIds;
63
+ if (tableIds.length)
64
+ context.table_ids = tableIds;
65
+ if (deepLinks.length)
66
+ context.deep_links = deepLinks;
67
+ if (Object.keys(context).length)
68
+ body.context = context;
69
+ return body;
70
+ }
71
+ function withSupportListQuery(path, options) {
72
+ const params = new URLSearchParams();
73
+ const status = readOption(options.status);
74
+ const limit = readOption(options.limit);
75
+ if (status)
76
+ params.set("status", status);
77
+ if (limit)
78
+ params.set("limit", limit);
79
+ const query = params.toString();
80
+ return query ? `${path}?${query}` : path;
81
+ }
82
+ function withSupportEventsQuery(path, options) {
83
+ const params = new URLSearchParams();
84
+ const after = readOption(options.after);
85
+ const limit = readOption(options.limit);
86
+ if (after)
87
+ params.set("after", after);
88
+ if (limit)
89
+ params.set("limit", limit);
90
+ const query = params.toString();
91
+ return query ? `${path}?${query}` : path;
92
+ }
93
+ // Workflow text flags (--verify-notes/--plan/--draft) bypass readOption: an
94
+ // explicitly-passed empty string clears the field server-side, while an absent
95
+ // flag leaves it untouched, so "" must survive to the request body.
96
+ function buildSupportAdminWorkflowBody(options) {
97
+ const body = {};
98
+ const verifyStatus = readOption(options.verifyStatus);
99
+ if (verifyStatus)
100
+ body.verify_status = verifyStatus;
101
+ if (options.verifyNotes !== undefined)
102
+ body.verify_notes = options.verifyNotes;
103
+ const planStatus = readOption(options.planStatus);
104
+ if (planStatus)
105
+ body.plan_status = planStatus;
106
+ if (options.plan !== undefined)
107
+ body.plan = options.plan;
108
+ const draftStatus = readOption(options.draftStatus);
109
+ if (draftStatus)
110
+ body.draft_status = draftStatus;
111
+ if (options.draft !== undefined)
112
+ body.draft_reply = options.draft;
113
+ if (!Object.keys(body).length) {
114
+ throw new OxygenError("invalid_request", "Pass at least one of --verify-status, --verify-notes, --plan-status, --plan, --draft-status, --draft.", { exitCode: 1 });
115
+ }
116
+ return body;
117
+ }
118
+ function buildSupportAdminUpdateBody(options) {
119
+ if (options.assign !== undefined && options.unassign) {
120
+ throw new OxygenError("invalid_request", "Pass either --assign <email> or --unassign, not both.", {
121
+ exitCode: 1,
122
+ });
123
+ }
124
+ const body = {};
125
+ const status = readOption(options.status);
126
+ if (status)
127
+ body.status = status;
128
+ const assign = readOption(options.assign);
129
+ if (assign)
130
+ body.assignee_email = assign;
131
+ if (options.unassign)
132
+ body.assignee_email = null;
133
+ const note = readOption(options.note);
134
+ if (note)
135
+ body.note = note;
136
+ if (!Object.keys(body).length) {
137
+ throw new OxygenError("invalid_request", "Pass at least one of --status, --assign, --unassign, --note.", { exitCode: 1 });
138
+ }
139
+ return body;
140
+ }
141
+ function supportAdminOpenResult(data) {
142
+ if (!isRecord(data) || !isRecord(data.ticket)) {
143
+ throw new OxygenError("invalid_response", "The support admin response did not include a Plain Thread.", { exitCode: 1 });
144
+ }
145
+ const plainUrl = data.ticket.plain_url;
146
+ if (typeof plainUrl !== "string" || !plainUrl.startsWith("https://app.plain.com/")) {
147
+ throw new OxygenError("invalid_response", "The support admin response did not include a valid Plain Inbox URL.", { exitCode: 1 });
148
+ }
149
+ return {
150
+ thread_id: data.ticket.id ?? null,
151
+ thread_ref: data.ticket.ref ?? null,
152
+ plain_url: plainUrl,
153
+ };
154
+ }
155
+ async function handleSupportAdminUpdateRequest(action, ticketId, options, fields) {
156
+ await handleAsyncAction(`support admin ${action}`, options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, {
157
+ method: "POST",
158
+ body: { action, ...fields },
159
+ }));
160
+ }
161
+ /**
162
+ * Plain snoozes by duration, but humans and agents think in days and hours.
163
+ * Requiring at least one keeps `snooze` from silently meaning "five minutes".
164
+ */
165
+ function readSupportSnoozeSeconds(options) {
166
+ const days = readSupportSnoozeUnit(options.days, "--days");
167
+ const hours = readSupportSnoozeUnit(options.hours, "--hours");
168
+ const seconds = days * 24 * 60 * 60 + hours * 60 * 60;
169
+ if (seconds <= 0) {
170
+ throw new OxygenError("invalid_request", "Pass --days and/or --hours to say how long the Thread should stay parked.", { exitCode: 1 });
171
+ }
172
+ return seconds;
173
+ }
174
+ function readSupportSnoozeUnit(raw, flag) {
175
+ if (raw === undefined)
176
+ return 0;
177
+ const value = Number(raw.trim());
178
+ if (!Number.isInteger(value) || value < 0) {
179
+ throw new OxygenError("invalid_request", `${flag} must be a whole, non-negative number.`, { exitCode: 1 });
180
+ }
181
+ return value;
182
+ }
183
+ async function handleSupportAdminWorkflowAction(ticketId, options) {
184
+ try {
185
+ const body = buildSupportAdminWorkflowBody(options);
186
+ const data = await requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/workflow`, { method: "POST", body });
187
+ if (options.json) {
188
+ writeJson(success("support admin workflow", data));
189
+ return;
190
+ }
191
+ process.stdout.write(formatSupportAdminWorkflowTicket(data));
192
+ }
193
+ catch (error) {
194
+ emitCliFailure("support admin workflow", error);
195
+ }
196
+ }
197
+ async function handleSupportAdminUpdateAction(ticketId, options) {
198
+ try {
199
+ const body = buildSupportAdminUpdateBody(options);
200
+ const data = await requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, { method: "POST", body });
201
+ if (options.json) {
202
+ writeJson(success("support admin update", data));
203
+ return;
204
+ }
205
+ process.stdout.write(formatSupportAdminUpdateTicket(data));
206
+ }
207
+ catch (error) {
208
+ emitCliFailure("support admin update", error);
209
+ }
210
+ }
211
+ function formatSupportAdminWorkflowStep(styles, label, status, artifactLabel, artifact) {
212
+ const artifactNote = artifact?.trim() ? `${artifactLabel} set` : `no ${artifactLabel}`;
213
+ return ` ${label.padEnd(6)} ${status ?? "pending"} ${styles.dim(`(${artifactNote})`)}`;
214
+ }
215
+ function formatSupportAdminWorkflowTicket(data) {
216
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
217
+ const ticket = data.ticket ?? {};
218
+ const workflow = ticket.workflow ?? {};
219
+ const lines = [
220
+ `${styles.bold("Workflow updated")}${ticket.subject ? styles.dim(` — ${ticket.subject}`) : ""}`,
221
+ formatSupportAdminWorkflowStep(styles, "verify", workflow.verify_status, "notes", workflow.verify_notes),
222
+ formatSupportAdminWorkflowStep(styles, "plan", workflow.plan_status, "plan", workflow.plan),
223
+ formatSupportAdminWorkflowStep(styles, "draft", workflow.draft_status, "draft reply", workflow.draft_reply),
224
+ ];
225
+ if (ticket.admin_web_url)
226
+ lines.push(`View ticket: ${ticket.admin_web_url}`);
227
+ return `${lines.join("\n")}\n`;
228
+ }
229
+ function formatSupportAdminUpdateTicket(data) {
230
+ const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
231
+ const ticket = data.ticket ?? {};
232
+ const lines = [
233
+ `${styles.bold("Ticket updated")}${ticket.subject ? styles.dim(` — ${ticket.subject}`) : ""}`,
234
+ ` Status: ${ticket.status ?? "?"}`,
235
+ ` Assignee: ${ticket.assignee_email ?? "(unassigned)"}`,
236
+ ];
237
+ if (ticket.admin_web_url)
238
+ lines.push(`View ticket: ${ticket.admin_web_url}`);
239
+ return `${lines.join("\n")}\n`;
240
+ }
241
+ export function registerSupportFamily(program, handleAsyncAction) {
242
+ program
243
+ .command("support")
244
+ .description("Open and track Plain support conversations for the active OXYGEN organization. In the app, the support chat is the widget in the bottom-right corner of every page. Retry is the only recovery control; it returns after each failed connection attempt and never creates a Thread. `support chat` starts or continues a zero-credit conversation without making you write a ticket title. Guide: https://oxygen-agent.com/docs/surfaces/support.")
245
+ .addCommand(new Command("chat")
246
+ .description("Start a Plain Chat conversation from one message, or pass an existing Thread ID to continue it. New conversations derive Plain's internal title from the message; you never have to file each message as a separate titled ticket.")
247
+ .argument("[threadId]", "Existing Plain Thread ID (th_...) or Thread ref (T-<n>) to continue. Omit to start a new conversation.")
248
+ .requiredOption("-m, --message <message>", "Your message to OXYGEN support.")
249
+ .option("--severity <severity>", "New conversation only: low | normal | high. Defaults to normal.")
250
+ .option("--category <category>", "New conversation only: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other. Defaults to question.")
251
+ .option("--transcript-text <text>", "New conversation only: explicitly share a selected transcript excerpt of 1–200,000 bytes after credential redaction.")
252
+ .option("--no-transcript", "Omit any selected transcript excerpt.")
253
+ .option("--json", "Print a JSON envelope.")
254
+ .addHelpText("after", `
255
+ Examples:
256
+ oxygen support chat --message "The dedicated IP is not being assigned" --category bug
257
+ oxygen support chat th_... --message "Here is the additional evidence"
258
+ `)
259
+ .action(async (threadId, options) => {
260
+ const message = readOption(options.message);
261
+ if (threadId) {
262
+ await handleAsyncAction("support chat", options, () => {
263
+ if (options.transcript !== false && options.transcriptText !== undefined) {
264
+ throw new OxygenError("invalid_transcript", "Transcript excerpts can only be attached when starting a new conversation. Omit the Thread ID or use support file.", { exitCode: 1 });
265
+ }
266
+ return requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(threadId)}/messages`, {
267
+ method: "POST",
268
+ body: { body: message },
269
+ });
270
+ });
271
+ return;
272
+ }
273
+ const severity = readOption(options.severity);
274
+ await handleAsyncAction("support chat", options, () => {
275
+ const body = {
276
+ message,
277
+ category: readOption(options.category) ?? "question",
278
+ ...(severity ? { severity } : {}),
279
+ };
280
+ if (options.transcript !== false && options.transcriptText !== undefined) {
281
+ body.transcript_text = readSelectedTranscriptExcerpt(options.transcriptText);
282
+ }
283
+ return requestOxygen("/api/cli/support/tickets", {
284
+ method: "POST",
285
+ body,
286
+ });
287
+ });
288
+ }))
289
+ .addCommand(new Command("file")
290
+ .description("Create a structured Plain support conversation with a subject and required message. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when operation, error, run, or table metadata helps triage. Share a selected excerpt with --transcript-text, or a local transcript with --transcript-file, --session-id, or --include-transcript. Check `support list --status open` first because a new filing can create a second Thread.")
291
+ .requiredOption("--subject <subject>", "One-line summary of the problem.")
292
+ .requiredOption("--body <body>", "Required message: what you were doing, what happened, and what you tried.")
293
+ .option("--include-transcript", "Attach the most recent local agent transcript after redaction.")
294
+ .option("--session-id <id>", "Attach this local agent session transcript.")
295
+ .option("--transcript-file <path>", "Attach the last 2 MB of a selected text/JSONL file after best-effort credential redaction; review it first.")
296
+ .option("--transcript-text <text>", "Explicitly share a selected transcript excerpt of 1–200,000 bytes after credential redaction, instead of a local transcript.")
297
+ .option("--no-transcript", "Omit any transcript, including explicitly selected content.")
298
+ .option("--severity <severity>", "low | normal | high. Defaults to normal.")
299
+ .option("--category <category>", "Request type: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other.")
300
+ .option("--source <source>", "Where the ticket originated. Only slack is supported today.")
301
+ .option("--operation <operation>", "The operation/tool that failed, e.g. oxygen_columns_run.")
302
+ .option("--error-code <code>", "The error envelope code you received.")
303
+ .option("--run-ids <ids>", "Comma-separated run ids to attach.")
304
+ .option("--table-ids <ids>", "Comma-separated table ids to attach.")
305
+ .option("--deep-links <urls>", "Comma-separated https://oxygen-agent.com/... links.")
306
+ .option("--slack-permalink <url>", "Slack message permalink (https://*.slack.com/...) to attach as context.")
307
+ .option("--slack-channel-id <id>", "Slack channel id (e.g. C0123ABC456) the report came from.")
308
+ .option("--customer-workspace <label>", "Customer workspace (org name/slug) a forwarded report concerns.")
309
+ .option("--cli-version <version>", "CLI version associated with the reported problem.")
310
+ .option("--json", "Print a JSON envelope.")
311
+ .action(async (options) => {
312
+ await handleAsyncAction("support ticket create", options, () => requestOxygen("/api/cli/support/tickets", {
313
+ method: "POST",
314
+ body: buildSupportTicketBody(options),
315
+ }));
316
+ }))
317
+ .addCommand(new Command("list")
318
+ .description("List Plain support Threads for the active organization.")
319
+ .option("--status <status>", "Filter by status (open, triaging, waiting_on_user, resolved, closed).")
320
+ .option("--limit <n>", "Max tickets to return.")
321
+ .option("--json", "Print a JSON envelope.")
322
+ .action(async (options) => {
323
+ await handleAsyncAction("support tickets list", options, () => requestOxygen(withSupportListQuery("/api/cli/support/tickets", options)));
324
+ }))
325
+ .addCommand(new Command("get")
326
+ .description("Show one Plain support Thread and its customer-visible timeline.")
327
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>) — both shown by `oxygen support list` as `id` and `ref` — or a migrated legacy ticket UUID.")
328
+ .option("--json", "Print a JSON envelope.")
329
+ .action(async (ticketId, options) => {
330
+ await handleAsyncAction("support ticket get", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}`));
331
+ }))
332
+ .addCommand(new Command("reply")
333
+ .description("Reply to a Plain Chat thread; migrated or native-channel history continues in one linked Chat thread.")
334
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>) — both shown by `oxygen support list` as `id` and `ref` — or a migrated legacy ticket UUID.")
335
+ .requiredOption("--body <body>", "Your reply.")
336
+ .option("--json", "Print a JSON envelope.")
337
+ .action(async (ticketId, options) => {
338
+ await handleAsyncAction("support ticket reply", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
339
+ method: "POST",
340
+ body: { body: readOption(options.body) },
341
+ }));
342
+ }))
343
+ .addCommand(new Command("ack")
344
+ .description("Legacy command; Plain owns delivery and read state.")
345
+ .argument("<ticketId>", "Ticket UUID.")
346
+ .option("--json", "Print a JSON envelope.")
347
+ .action(async (ticketId, options) => {
348
+ await handleAsyncAction("support ticket ack", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(ticketId)}/ack`, {
349
+ method: "POST",
350
+ body: {},
351
+ }));
352
+ }), { hidden: true })
353
+ .addCommand(new Command("admin")
354
+ .description("Operate the live Plain support queue as OXYGEN staff. Public replies are previewed first and sent as the shared Oxygen Support identity.")
355
+ .addCommand(new Command("list")
356
+ .description("List live Plain Threads across customer Tenants (staff only).")
357
+ .option("--status <status>", "Filter by status.")
358
+ .option("--limit <n>", "Max tickets to return.")
359
+ .option("--json", "Print a JSON envelope.")
360
+ .action(async (options) => {
361
+ await handleAsyncAction("support admin list", options, () => requestOxygen(withSupportListQuery("/api/cli/admin/support/tickets", options)));
362
+ }))
363
+ .addCommand(new Command("get")
364
+ .description("Show one live Plain Thread, its routing metadata, and customer-visible messages (staff only). Add --include-notes to also read back the Thread's team-only notes and staged reply drafts as `internal_notes` (kind note, reply_draft, suggested_reply_provenance or undelivered_customer_report); read-only, never shown to the customer.")
365
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
366
+ .option("--include-notes", "Also return team-only internal notes and staged drafts.")
367
+ .option("--json", "Print a JSON envelope.")
368
+ .action(async (ticketId, options) => {
369
+ await handleAsyncAction("support admin get", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}${options.includeNotes ? "?include_notes=1" : ""}`));
370
+ }))
371
+ .addCommand(new Command("open")
372
+ .description("Return the exact Plain Inbox URL for one live Thread (staff only).")
373
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
374
+ .option("--json", "Print a JSON envelope.")
375
+ .action(async (ticketId, options) => {
376
+ await handleAsyncAction("support admin open", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}`)
377
+ .then((data) => supportAdminOpenResult(data)));
378
+ }))
379
+ .addCommand(new Command("claim")
380
+ .description("Claim a live Plain Thread and move it to In progress without overwriting another assignee (staff only).")
381
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
382
+ .option("--agent", "Claim as the authenticated Oxygen Support machine user instead of the signed-in human.")
383
+ .option("--json", "Print a JSON envelope.")
384
+ .action(async (ticketId, options) => {
385
+ await handleAsyncAction("support admin claim", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, {
386
+ method: "POST",
387
+ body: { action: "claim", as_machine_user: options.agent === true },
388
+ }));
389
+ }))
390
+ .addCommand(new Command("start")
391
+ .description("Set a live Plain Thread to Plain TODO with the In progress detail when OXYGEN work remains after a reply, preserving its assignee (staff only).")
392
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
393
+ .requiredOption("--agent", "Required safety declaration: reconcile this Thread's status as the Oxygen Support machine user.")
394
+ .requiredOption("--confirm-message <messageId>", "Apply only while this is still the exact latest customer-visible conversation-head message ID. From support admin get --json, use the id of the data.messages[] entry with the greatest created_at.")
395
+ .option("--json", "Print a JSON envelope.")
396
+ .addHelpText("after", `
397
+ Safety: 0 credits, but this is a real Plain queue write. It preserves the current assignee and changes only the lifecycle to plain_status TODO with plain_status_detail ThreadStatusDetailInProgress (projected as ticket.status triaging).
398
+
399
+ Safe sequence:
400
+ oxygen support admin get <thread-id> --json
401
+ # Select the data.messages[] entry with the greatest created_at and copy its id.
402
+ oxygen support admin start <thread-id> --agent --confirm-message <latest-message-id> --json
403
+ oxygen support admin get <thread-id> --json
404
+
405
+ The final readback must show ticket.status triaging, plain_status TODO, the In progress detail, the same conversation head, and an unchanged assignee.
406
+ `)
407
+ .action(async (ticketId, options) => {
408
+ await handleAsyncAction("support admin start", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/update`, {
409
+ method: "POST",
410
+ body: {
411
+ action: "start",
412
+ as_agent: true,
413
+ confirm_message_id: readOption(options.confirmMessage),
414
+ },
415
+ }));
416
+ }))
417
+ .addCommand(new Command("priority")
418
+ .description("Set the native Plain priority for a live Thread (staff only).")
419
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
420
+ .requiredOption("--priority <priority>", "urgent | high | normal | low")
421
+ .option("--json", "Print a JSON envelope.")
422
+ .action(async (ticketId, options) => {
423
+ await handleSupportAdminUpdateRequest("priority", ticketId, options, {
424
+ priority: readOption(options.priority),
425
+ });
426
+ }))
427
+ .addCommand(new Command("snooze")
428
+ .description("Park a live Thread until a named time, so work blocked on a release, a provider, or a scheduled retry stops reading as unanswered (staff only).")
429
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
430
+ .option("--days <n>", "Snooze for this many days.")
431
+ .option("--hours <n>", "Snooze for this many hours. Combined with --days when both are given.")
432
+ .requiredOption("--confirm-message <messageId>", "Confirm the exact latest customer-visible message ID. A Thread whose customer just wrote must be answered, not snoozed past.")
433
+ .option("--json", "Print a JSON envelope.")
434
+ .action(async (ticketId, options) => {
435
+ await handleSupportAdminUpdateRequest("snooze", ticketId, options, {
436
+ duration_seconds: readSupportSnoozeSeconds(options),
437
+ confirm_message_id: readOption(options.confirmMessage),
438
+ });
439
+ }))
440
+ .addCommand(new Command("todo")
441
+ .description("Return a snoozed Thread to the queue when its blocker clears (staff only). Never reopens a Done Thread: Plain does that on real customer activity.")
442
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
443
+ .option("--json", "Print a JSON envelope.")
444
+ .action(async (ticketId, options) => {
445
+ await handleSupportAdminUpdateRequest("todo", ticketId, options, {});
446
+ }))
447
+ .addCommand(new Command("assign")
448
+ .description("Hand a live Thread to a named human, for escalation past an agent's authority or judgment boundary (staff only).")
449
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
450
+ .requiredOption("--user <email>", "The staff email to assign the Thread to.")
451
+ .option("--allow-takeover", "Required to move a Thread that already has an owner. Without it, an owned Thread is refused rather than silently reassigned.")
452
+ .option("--json", "Print a JSON envelope.")
453
+ .action(async (ticketId, options) => {
454
+ await handleSupportAdminUpdateRequest("assign", ticketId, options, {
455
+ assignee_email: readOption(options.user),
456
+ allow_takeover: options.allowTakeover === true,
457
+ });
458
+ }))
459
+ .addCommand(new Command("field-set")
460
+ .description("Record OXYGEN workflow state on a live Thread as structured, filterable Plain data — which version and commit carry a fix, and the PR that shipped it (staff only).")
461
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
462
+ .requiredOption("--field <key>", "oxygen_fix_version | oxygen_fix_sha | oxygen_fix_pr. Customer-intake fields are read-only evidence and cannot be written here.")
463
+ .requiredOption("--value <value>", "The value to record.")
464
+ .option("--json", "Print a JSON envelope.")
465
+ .action(async (ticketId, options) => {
466
+ await handleSupportAdminUpdateRequest("field_set", ticketId, options, {
467
+ field_key: readOption(options.field),
468
+ field_value: readOption(options.value),
469
+ });
470
+ }))
471
+ .addCommand(new Command("label-add")
472
+ .description("Add an active Plain label by external ID (staff only).")
473
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
474
+ .requiredOption("--label <externalId>", "Plain label external ID, for example oxygen_request_bug.")
475
+ .option("--json", "Print a JSON envelope.")
476
+ .action(async (ticketId, options) => {
477
+ await handleSupportAdminUpdateRequest("label_add", ticketId, options, {
478
+ label_external_id: readOption(options.label),
479
+ });
480
+ }))
481
+ .addCommand(new Command("label-remove")
482
+ .description("Remove a Plain label by external ID (staff only).")
483
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
484
+ .requiredOption("--label <externalId>", "Plain label external ID.")
485
+ .option("--json", "Print a JSON envelope.")
486
+ .action(async (ticketId, options) => {
487
+ await handleSupportAdminUpdateRequest("label_remove", ticketId, options, {
488
+ label_external_id: readOption(options.label),
489
+ });
490
+ }))
491
+ .addCommand(new Command("note")
492
+ .description("Add a team-only internal note to a live Plain Thread (staff only).")
493
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
494
+ .requiredOption("--body <body>", "Internal note body.")
495
+ .option("--json", "Print a JSON envelope.")
496
+ .action(async (ticketId, options) => {
497
+ await handleSupportAdminUpdateRequest("note", ticketId, options, {
498
+ body: readOption(options.body),
499
+ });
500
+ }))
501
+ .addCommand(new Command("draft")
502
+ .description("Stage a reply as a team-only Plain note; this never sends to the customer (staff and agents).")
503
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
504
+ .requiredOption("--body <body>", "Proposed customer reply.")
505
+ .option("--json", "Print a JSON envelope.")
506
+ .action(async (ticketId, options) => {
507
+ await handleSupportAdminUpdateRequest("draft", ticketId, options, {
508
+ body: readOption(options.body),
509
+ });
510
+ }))
511
+ .addCommand(new Command("reply")
512
+ .description("Preview or send a guarded public reply on the Thread's native Plain channel as Oxygen Support (staff only). CLI delivery is agent-only; named humans use Plain Inbox or Plain MCP.")
513
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
514
+ .requiredOption("--body <body>", "Customer-visible reply body.")
515
+ .requiredOption("--agent", "Required safety declaration: reply as the authenticated Oxygen Support machine user after it owns the Thread.")
516
+ .option("--confirm-ref <ref>", "Send only when this exactly matches the previewed Plain ref (for example T-10). Omit to preview without sending.")
517
+ .option("--confirm-message <messageId>", "Agent send only: confirm the latest customer-visible conversation-head message ID returned by preview.")
518
+ .option("--confirm-token <token>", "Agent send only: use the exact server-authenticated preview token bound to the Thread, latest message, reply body, and actor mode; never calculate it locally.")
519
+ .option("--json", "Print a JSON envelope.")
520
+ .action(async (ticketId, options) => {
521
+ await handleAsyncAction("support admin reply", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/messages`, {
522
+ method: "POST",
523
+ body: {
524
+ body: readOption(options.body),
525
+ as_agent: true,
526
+ ...(readOption(options.confirmRef)
527
+ ? { confirm_ref: readOption(options.confirmRef) }
528
+ : {}),
529
+ ...(readOption(options.confirmMessage)
530
+ ? { confirm_message_id: readOption(options.confirmMessage) }
531
+ : {}),
532
+ ...(readOption(options.confirmToken)
533
+ ? { confirm_token: readOption(options.confirmToken) }
534
+ : {}),
535
+ },
536
+ }));
537
+ }))
538
+ .addCommand(new Command("done")
539
+ .description("Preview or mark a Plain Thread Done after the exact verified agent reply; new customer activity reopens it to Todo (staff only). CLI completion is agent-only.")
540
+ .argument("<ticketId>", "Plain Thread ID (th_...) or Thread ref (T-<n>), as shown by `oxygen support admin list`.")
541
+ .requiredOption("--agent", "Required safety declaration: mark Done as the authenticated Oxygen Support machine user while it still owns the Thread.")
542
+ .requiredOption("--reply-token <token>", "Use the exact agent_done_token returned by the preceding live `support admin reply --agent`; previews and errors never mint or reveal one.")
543
+ .option("--confirm-ref <ref>", "Complete only when this exactly matches the previewed Plain ref.")
544
+ .option("--confirm-message <messageId>", "Complete only when this exactly matches the latest verified staff reply ID from preview.")
545
+ .option("--json", "Print a JSON envelope.")
546
+ .action(async (ticketId, options) => {
547
+ await handleAsyncAction("support admin done", options, () => requestOxygen(`/api/cli/admin/support/tickets/${encodeURIComponent(ticketId)}/resolve`, {
548
+ method: "POST",
549
+ body: {
550
+ as_agent: true,
551
+ ...(readOption(options.replyToken)
552
+ ? { agent_reply_token: readOption(options.replyToken) }
553
+ : {}),
554
+ ...(readOption(options.confirmRef)
555
+ ? { confirm_ref: readOption(options.confirmRef) }
556
+ : {}),
557
+ ...(readOption(options.confirmMessage)
558
+ ? { confirm_message_id: readOption(options.confirmMessage) }
559
+ : {}),
560
+ },
561
+ }));
562
+ }))
563
+ .addCommand(new Command("file")
564
+ .description("Retired legacy write; always returns the Plain-cutover error (staff only).")
565
+ .requiredOption("--organization <organization>", "Target organization id or slug the ticket belongs to.")
566
+ .requiredOption("--subject <subject>", "One-line summary of the customer's request.")
567
+ .requiredOption("--body <body>", "The customer's request in their words, plus any staff notes.")
568
+ .option("--severity <severity>", "low | normal | high. Defaults to normal.")
569
+ .option("--category <category>", "Optional category label, e.g. bug, feature, question.")
570
+ .option("--source <source>", "Where the ticket originated. Only slack is supported today.")
571
+ .option("--operation <operation>", "The operation/tool involved, if known.")
572
+ .option("--error-code <code>", "The error envelope code, if known.")
573
+ .option("--run-ids <ids>", "Comma-separated run ids to attach.")
574
+ .option("--table-ids <ids>", "Comma-separated table ids to attach.")
575
+ .option("--deep-links <urls>", "Comma-separated https://oxygen-agent.com/... links.")
576
+ .option("--slack-permalink <url>", "Slack message permalink (https://*.slack.com/...) to attach as context.")
577
+ .option("--slack-channel-id <id>", "Slack channel id (e.g. C0123ABC456) the report came from.")
578
+ .option("--customer-workspace <label>", "Customer workspace label when --org is not the customer's own org.")
579
+ .option("--json", "Print a JSON envelope.")
580
+ .action(async (options) => {
581
+ await handleAsyncAction("support admin file", options, () => requestOxygen("/api/cli/admin/support/tickets", {
582
+ method: "POST",
583
+ body: {
584
+ ...buildSupportTicketBody(options),
585
+ organization: readOption(options.organization),
586
+ },
587
+ }));
588
+ }), { hidden: true })
589
+ .addCommand(new Command("setup")
590
+ .description("Reconcile the shared Plain workspace against the taxonomy OXYGEN declares in code: request-type labels and intake fields, plus the workflow-state labels and release fields the support loop writes. Previews by default and never sends anything to a customer (staff only).")
591
+ .option("--apply", "Apply the previewed plan. Requires --confirm-plan with the exact plan_hash you just read.")
592
+ .option("--confirm-plan <hash>", "The plan_hash from the immediately preceding preview. A workspace that drifted in between fails closed.")
593
+ .option("--json", "Print a JSON envelope.")
594
+ .action(async (options) => {
595
+ if (options.apply !== true) {
596
+ await handleAsyncAction("support admin setup", options, () => requestOxygen("/api/cli/admin/support/setup"));
597
+ return;
598
+ }
599
+ await handleAsyncAction("support admin setup", options, () => requestOxygen("/api/cli/admin/support/setup", {
600
+ method: "POST",
601
+ body: { confirm_plan_hash: readOption(options.confirmPlan) },
602
+ }));
603
+ }))
604
+ .addCommand(new Command("events")
605
+ .description("Read-only poll of legacy OXYGEN archive events for cutover compatibility; never use it as the live queue (staff only).")
606
+ .option("--after <cursor>", "If has_more=true, pass next_cursor to continue that scan. If false, persist the non-null watermark_cursor for later polls. A watermark poll intentionally replays the settled 24h window, not only unseen events; dedupe every page by (ticket_ref, version).")
607
+ .option("--limit <n>", "Events to return (1-100, defaults to 50).")
608
+ .option("--json", "Print a JSON envelope.")
609
+ .addHelpText("after", `
610
+ Event schema (each data.events item has exactly these fields):
611
+ event_kind: ticket_snapshot
612
+ ticket_ref: UUID; version: decimal string; event_time: UTC timestamp
613
+ status: open | triaging | waiting_on_user | resolved | closed | null
614
+ severity: low | normal | high | null
615
+ category: account | auth | billing | bug | data | deliverability |
616
+ feature_request | feedback | integration | other | performance |
617
+ provider | security | sequencer | tables | workflow | null
618
+ source: slack | null; route: slack | cli | mcp | web | null
619
+
620
+ The cursor is opaque. has_more discriminates its phase: next_cursor continues a
621
+ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retain
622
+ (ticket_ref, version) as the idempotency key across every poll.
623
+ `)
624
+ .action(async (options) => {
625
+ await handleAsyncAction("support admin events", options, () => requestOxygen(withSupportEventsQuery("/api/cli/admin/support/events", options)));
626
+ }), { hidden: true })
627
+ .addCommand(new Command("workflow")
628
+ .description("Retired legacy write; Plain notes and downstream engineering work own triage (staff only).")
629
+ .argument("<ticketId>", "Ticket UUID.")
630
+ .option("--verify-status <status>", "Verify step: pending | in_progress | done | blocked | skipped.")
631
+ .option("--verify-notes <text>", "Verification notes. Pass an empty string to clear.")
632
+ .option("--plan-status <status>", "Plan step: pending | in_progress | done | blocked | skipped.")
633
+ .option("--plan <text>", "Remediation plan. Pass an empty string to clear.")
634
+ .option("--draft-status <status>", "Draft step: pending | in_progress | done | blocked | skipped.")
635
+ .option("--draft <text>", "Draft reply to the opener. Pass an empty string to clear.")
636
+ .option("--json", "Print a JSON envelope.")
637
+ .action(async (ticketId, options) => {
638
+ await handleSupportAdminWorkflowAction(ticketId, options);
639
+ }), { hidden: true })
640
+ .addCommand(new Command("update")
641
+ .description("Retired legacy write; Plain owns status, assignee, and notes (staff only).")
642
+ .argument("<ticketId>", "Ticket UUID.")
643
+ .option("--status <status>", "open | triaging | waiting_on_user | closed. Use the resolve command to resolve.")
644
+ .option("--assign <email>", "Assign the ticket to a staff email.")
645
+ .option("--unassign", "Clear the ticket assignment.")
646
+ .option("--note <text>", "Append a staff message to the ticket thread.")
647
+ .option("--json", "Print a JSON envelope.")
648
+ .action(async (ticketId, options) => {
649
+ await handleSupportAdminUpdateAction(ticketId, options);
650
+ }), { hidden: true }));
651
+ }