@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,518 @@
1
+ import { readPositiveInt } from "../cli-values.js";
2
+ import { COLLAB_BETA_NOTE, COLLAB_SUBJECT_PATH_HELP, COLLAB_SUBJECT_REF_HELP, collabSnippet, collabStyles, collabText, collabTrailer, collectRepeatable, formatCollabAge, formatCollabWriteResult, handleCollabAction, readCollabPath, renderTextTable, requireCollabSubject } from "../command-support.js";
3
+ import { requestOxygen } from "../http-client.js";
4
+ import { isRecord, readOption } from "../util.js";
5
+ import { OxygenError } from "@oxygen/shared/cli-result";
6
+ import { COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_LABELS, GATE_KIND_SUBJECTS } from "@oxygen/shared/collab";
7
+ import { Command } from "commander";
8
+ /** Every gate, what it means, and which kinds it can be required on. */
9
+ const COLLAB_GATE_GLOSS = COLLAB_GATE_KINDS
10
+ .map((gate) => `${gate} — ${COLLAB_GATE_PROSE[gate]}; set it on ${GATE_KIND_SUBJECTS[gate]
11
+ .map((kind) => COLLAB_SUBJECT_LABELS[kind].many)
12
+ .join(" or ")}`)
13
+ .join(". ");
14
+ const COLLAB_GATE_FLAG_HELP = `Which gate: ${COLLAB_GATE_KINDS.join(" | ")}. ${COLLAB_GATE_GLOSS}.`;
15
+ function requireCollabGate(value) {
16
+ const raw = readOption(value);
17
+ const gate = COLLAB_GATE_KINDS.find((candidate) => candidate === raw);
18
+ if (!gate) {
19
+ throw new OxygenError("invalid_request", `--gate must be one of: ${COLLAB_GATE_KINDS.join(", ")}. ${COLLAB_GATE_GLOSS}.`, {
20
+ details: { gate: raw, valid_gate_kinds: [...COLLAB_GATE_KINDS] },
21
+ exitCode: 2,
22
+ });
23
+ }
24
+ return gate;
25
+ }
26
+ function requireCollabSubjectKind(value) {
27
+ const raw = readOption(value);
28
+ const kind = COLLAB_SUBJECT_KINDS.find((candidate) => candidate === raw);
29
+ if (!kind) {
30
+ throw new OxygenError("invalid_subject_ref", `--default must be one of: ${COLLAB_SUBJECT_KINDS.join(", ")}.`, {
31
+ details: { subject_kind: raw, valid_kinds: [...COLLAB_SUBJECT_KINDS] },
32
+ exitCode: 2,
33
+ });
34
+ }
35
+ return kind;
36
+ }
37
+ /**
38
+ * At least one assignee. Commander cannot enforce this (the repeatable
39
+ * collector's `[]` default satisfies its mandatory check), and a request with
40
+ * nobody on it is the exact failure the API refuses to create: it looks filed,
41
+ * blocks the gate, and is discovered the next morning.
42
+ */
43
+ function requireCollabAssignees(values) {
44
+ const assignees = values.map((value) => value.trim()).filter(Boolean);
45
+ if (assignees.length === 0) {
46
+ throw new OxygenError("invalid_request", "--assignee <email> is required: name at least one workspace member who can decide.", { exitCode: 2 });
47
+ }
48
+ return assignees;
49
+ }
50
+ /**
51
+ * Exactly one of --approve / --reject / --changes-requested. Neither zero nor
52
+ * two is guessable: `changes_requested` is a real decision that does NOT open
53
+ * the gate, so silently defaulting either way would either stall a campaign or
54
+ * launch one the client never agreed to.
55
+ */
56
+ function requireCollabDecision(options) {
57
+ const selected = [];
58
+ if (options.approve)
59
+ selected.push("approve");
60
+ if (options.reject)
61
+ selected.push("reject");
62
+ if (options.changesRequested)
63
+ selected.push("changes_requested");
64
+ if (selected.length !== 1) {
65
+ throw new OxygenError("invalid_request", selected.length === 0
66
+ ? "Pass exactly one decision: --approve, --reject, or --changes-requested (asks for edits; does NOT open the gate)."
67
+ : `Pass only one decision flag — got ${selected.length}: --approve, --reject, and --changes-requested are mutually exclusive.`, { details: { decisions_passed: selected }, exitCode: 2 });
68
+ }
69
+ return selected[0];
70
+ }
71
+ /** Exactly one scope (--on or --default) and exactly one requirement flag. */
72
+ function collabGateSetBody(options) {
73
+ const on = readOption(options.on);
74
+ const asDefault = readOption(options.default);
75
+ if (Boolean(on) === Boolean(asDefault)) {
76
+ throw new OxygenError("invalid_request", "Pass exactly one scope: --on <kind>:<id> for one object, or --default <kind> for every object of that kind.", { details: { on, default: asDefault }, exitCode: 2 });
77
+ }
78
+ if (Boolean(options.required) === Boolean(options.notRequired)) {
79
+ throw new OxygenError("invalid_request", "Pass exactly one of --required or --not-required.", { exitCode: 2 });
80
+ }
81
+ const gateKind = requireCollabGate(options.gate);
82
+ const subject = on ? requireCollabSubject(on) : null;
83
+ const subjectKind = subject ? subject.kind : requireCollabSubjectKind(asDefault ?? undefined);
84
+ if (!GATE_KIND_SUBJECTS[gateKind].includes(subjectKind)) {
85
+ throw new OxygenError("invalid_request", `The ${gateKind} gate applies to ${GATE_KIND_SUBJECTS[gateKind].map((kind) => COLLAB_SUBJECT_LABELS[kind].many).join(" or ")}, not ${COLLAB_SUBJECT_LABELS[subjectKind].many}.`, { details: { gate_kind: gateKind, subject_kind: subjectKind, applies_to: [...GATE_KIND_SUBJECTS[gateKind]] }, exitCode: 2 });
86
+ }
87
+ return {
88
+ subject_kind: subjectKind,
89
+ // null is meaningful here: it is what makes the policy the workspace
90
+ // default for the kind rather than a row about one object.
91
+ subject_id: subject ? subject.id : null,
92
+ gate_kind: gateKind,
93
+ required: Boolean(options.required),
94
+ };
95
+ }
96
+ function collabRequestRow(request) {
97
+ return [
98
+ collabText(request.id),
99
+ collabText(request.subject),
100
+ collabText(request.gate_kind),
101
+ collabText(request.status),
102
+ collabText(request.requested_by_email, "—"),
103
+ formatCollabAge(request.created_at),
104
+ ];
105
+ }
106
+ const COLLAB_REQUEST_HEADERS = ["ID", "SUBJECT", "GATE", "STATUS", "REQUESTED BY", "AGE"];
107
+ function formatCollabRequestList(data) {
108
+ const styles = collabStyles();
109
+ const requests = Array.isArray(data.requests) ? data.requests.filter(isRecord) : [];
110
+ const lines = ["", styles.bold(`Approval requests (${requests.length})`)];
111
+ if (requests.length === 0) {
112
+ lines.push(` ${styles.dim(collabText(data.hint, "No approval requests match those filters."))}`);
113
+ }
114
+ else {
115
+ lines.push(...renderTextTable(COLLAB_REQUEST_HEADERS, requests.map(collabRequestRow)));
116
+ }
117
+ lines.push(...collabTrailer(data));
118
+ lines.push("");
119
+ return lines.join("\n");
120
+ }
121
+ /**
122
+ * The daily surface. Two sections and nothing else: what needs a decision from
123
+ * this reader, and where they were named. An item the reader cannot act on has
124
+ * no business being here, so there is no "everything else" section.
125
+ */
126
+ function formatCollabInbox(data) {
127
+ const styles = collabStyles();
128
+ const pending = Array.isArray(data.pending_approvals) ? data.pending_approvals.filter(isRecord) : [];
129
+ const mentions = Array.isArray(data.mentions) ? data.mentions.filter(isRecord) : [];
130
+ const lines = ["", styles.bold(`Waiting on you (${pending.length})`)];
131
+ if (pending.length === 0) {
132
+ lines.push(` ${styles.dim("No approvals are assigned to you.")}`);
133
+ }
134
+ else {
135
+ lines.push(...renderTextTable(COLLAB_REQUEST_HEADERS, pending.map(collabRequestRow)));
136
+ }
137
+ lines.push("", styles.bold(`Threads that name you (${mentions.length})`));
138
+ if (mentions.length === 0) {
139
+ lines.push(` ${styles.dim("Nobody has mentioned you in an open thread.")}`);
140
+ }
141
+ else {
142
+ lines.push(...renderTextTable(["THREAD", "SUBJECT", "LAST COMMENT", "AGE"], mentions.map((thread) => {
143
+ const comments = Array.isArray(thread.comments) ? thread.comments.filter(isRecord) : [];
144
+ const last = comments[comments.length - 1];
145
+ return [
146
+ collabText(thread.id),
147
+ collabText(thread.subject),
148
+ collabSnippet(last?.body, 44) || collabText(thread.title, "—"),
149
+ formatCollabAge(thread.updated_at ?? thread.created_at),
150
+ ];
151
+ })));
152
+ }
153
+ lines.push(...collabTrailer(data));
154
+ lines.push("");
155
+ return lines.join("\n");
156
+ }
157
+ /**
158
+ * `approvals show`. The reader here usually arrived from a link in an email and
159
+ * knows nothing but the id, so this answers the four questions a table cell
160
+ * cannot: what is being asked, on what, who owes the answer, and whether the
161
+ * request can still be decided. An expired request says so in the headline —
162
+ * `approvals decide` on it is refused by the API.
163
+ */
164
+ function formatCollabRequestDetail(data) {
165
+ const styles = collabStyles();
166
+ const request = isRecord(data.request) ? data.request : data;
167
+ const status = collabText(request.status);
168
+ const lapsed = request.lapsed === true;
169
+ const badge = lapsed
170
+ ? styles.yellow("[EXPIRED]")
171
+ : request.opens_gate === true
172
+ ? styles.green("[APPROVED]")
173
+ : status === "pending"
174
+ ? styles.dim("[PENDING]")
175
+ : styles.yellow(`[${status.toUpperCase()}]`);
176
+ const assignees = Array.isArray(request.assignee_emails)
177
+ ? request.assignee_emails.filter((value) => typeof value === "string")
178
+ : [];
179
+ const lines = [
180
+ "",
181
+ ` ${badge} ${collabText(request.title, "Approval request")}`,
182
+ ` ${styles.dim("Request")} ${collabText(request.id)}`,
183
+ ` ${styles.dim("Subject")} ${collabText(request.subject)}`,
184
+ ` ${styles.dim("Gate")} ${collabText(request.gate_kind)} — ${collabText(request.gate_meaning, "")}`,
185
+ ` ${styles.dim("Asked by")} ${collabText(request.requested_by_email, "someone")}`,
186
+ ` ${styles.dim("Decides")} ${assignees.length > 0 ? assignees.join(", ") : "—"}`,
187
+ ];
188
+ const body = collabSnippet(request.body, 72);
189
+ if (body)
190
+ lines.push(` ${styles.dim("Asking")} ${body}`);
191
+ if (typeof request.expires_at === "string") {
192
+ lines.push(` ${styles.dim("Expires")} ${request.expires_at}${lapsed ? styles.yellow(" — already passed") : ""}`);
193
+ }
194
+ const note = collabText(request.decision_note, "");
195
+ if (note)
196
+ lines.push(` ${styles.dim("Note")} ${note}`);
197
+ lines.push(...collabTrailer(data));
198
+ lines.push("");
199
+ return lines.join("\n");
200
+ }
201
+ /**
202
+ * `approvals client list`. The column that matters is ACCESS: a blank role cell
203
+ * means FULL access, not a restricted one, and reading it the other way is how
204
+ * an agency convinces itself a client login is limited when it is not.
205
+ */
206
+ function formatCollabMembers(data) {
207
+ const styles = collabStyles();
208
+ const members = Array.isArray(data.members) ? data.members.filter(isRecord) : [];
209
+ const lines = ["", styles.bold(`Workspace members (${members.length})`)];
210
+ if (members.length === 0) {
211
+ lines.push(` ${styles.dim("No members resolved for this workspace.")}`);
212
+ }
213
+ else {
214
+ lines.push(...renderTextTable(["EMAIL", "WORKSPACE ROLE", "OXYGEN ROLE", "ACCESS"], members.map((member) => [
215
+ collabText(member.email, collabText(member.user_id)),
216
+ collabText(member.workspace_role),
217
+ member.oxygen_role === "client" ? "client" : "—",
218
+ collabText(member.access),
219
+ ])));
220
+ // Why a grant on a given row would be refused. The payload has carried this
221
+ // since the web page needed it; printing it is what stops the terminal from
222
+ // being the surface that only finds out by failing — on an all-admin
223
+ // workspace EVERY grant is refused, and the table alone never says so.
224
+ const blocked = members.filter((member) => member.can_restrict === false);
225
+ if (blocked.length > 0) {
226
+ lines.push("", ` ${styles.dim("Cannot be restricted to the client role:")}`);
227
+ for (const member of blocked) {
228
+ lines.push(` ${styles.yellow("!")} ${collabText(member.restrict_blocked_reason)}`);
229
+ }
230
+ }
231
+ }
232
+ // Invited but not joined. They hold nothing yet, so they are listed apart from
233
+ // the members rather than mixed in with a blank role that reads as access.
234
+ const invitations = Array.isArray(data.pending_invitations)
235
+ ? data.pending_invitations.filter(isRecord)
236
+ : [];
237
+ if (invitations.length > 0) {
238
+ lines.push("", styles.bold(`Invited, not joined yet (${invitations.length})`));
239
+ lines.push(...renderTextTable(["EMAIL", "INVITED AS", "ON JOIN", "INVITED"], invitations.map((invitation) => [
240
+ collabText(invitation.email),
241
+ collabText(invitation.workspace_role),
242
+ invitation.oxygen_role_on_join === "client" ? "client" : "full access",
243
+ formatCollabAge(invitation.invited_at),
244
+ ])));
245
+ }
246
+ lines.push(...collabTrailer(data));
247
+ lines.push("");
248
+ return lines.join("\n");
249
+ }
250
+ function formatCollabRequestWrite(data) {
251
+ const styles = collabStyles();
252
+ // The request is nested; the resolved assignee emails are not.
253
+ const request = isRecord(data.request) ? data.request : data;
254
+ const assignees = Array.isArray(data.assignee_emails)
255
+ ? data.assignee_emails.filter((value) => typeof value === "string")
256
+ : [];
257
+ const lines = [
258
+ "",
259
+ ` ${styles.green("[OK]")} Asked ${assignees.length > 0 ? assignees.join(", ") : "the assignees"} to decide.`,
260
+ ` ${styles.dim("Request")} ${collabText(request.id)}`,
261
+ ` ${styles.dim("Subject")} ${collabText(request.subject)}`,
262
+ ` ${styles.dim("Gate")} ${collabText(request.gate_kind)} — ${collabText(request.gate_meaning, "")}`,
263
+ ];
264
+ if (typeof request.expires_at === "string") {
265
+ lines.push(` ${styles.dim("Expires")} ${request.expires_at} ${styles.dim("(an expired request does not open the gate)")}`);
266
+ }
267
+ lines.push(...collabTrailer(data));
268
+ lines.push("");
269
+ return lines.join("\n");
270
+ }
271
+ /**
272
+ * A decision's headline is `opens_gate`, never the status alone: "the client
273
+ * replied" and "the client said yes" are the two states an agency must never
274
+ * confuse, and `changes_requested` is the one that reads like the second and
275
+ * behaves like the first.
276
+ */
277
+ function formatCollabDecision(data) {
278
+ const styles = collabStyles();
279
+ const request = isRecord(data.request) ? data.request : data;
280
+ const status = collabText(request.status);
281
+ const opensGate = request.opens_gate === true || data.gate_opened === true;
282
+ const lines = [
283
+ "",
284
+ ` ${opensGate ? styles.green("[APPROVED]") : styles.yellow(`[${status.toUpperCase()}]`)} ${collabText(request.subject)} — request ${collabText(request.id)}`,
285
+ ` ${styles.dim("Gate")} ${opensGate
286
+ ? `open — the ${collabText(request.gate_kind)} gate no longer blocks this.`
287
+ : `still shut — a ${collabText(request.gate_kind)} gate only opens on --approve.`}`,
288
+ ];
289
+ const note = collabText(request.decision_note, "");
290
+ if (note)
291
+ lines.push(` ${styles.dim("Note")} ${note}`);
292
+ if (data.already === true)
293
+ lines.push(` ${styles.dim("Replay of the same decision; nothing changed.")}`);
294
+ lines.push(...collabTrailer(data));
295
+ lines.push("");
296
+ return lines.join("\n");
297
+ }
298
+ function formatCollabGateSet(data) {
299
+ const styles = collabStyles();
300
+ const policy = isRecord(data.policy) ? data.policy : {};
301
+ const required = policy.required === true;
302
+ const scope = collabText(data.scope, "subject") === "workspace_default"
303
+ ? `every ${collabText(policy.subject_kind)}`
304
+ : `${collabText(policy.subject_kind)}:${collabText(policy.subject_id)}`;
305
+ const lines = [
306
+ "",
307
+ ` ${required ? styles.green("[GATE ON]") : styles.dim("[GATE OFF]")} ${collabText(policy.gate_kind)} on ${scope}`,
308
+ ` ${styles.dim("Means")} ${collabText(policy.gate_meaning, "")}`,
309
+ ];
310
+ lines.push(...collabTrailer(data));
311
+ lines.push("");
312
+ return lines.join("\n");
313
+ }
314
+ function formatCollabGateList(data) {
315
+ const styles = collabStyles();
316
+ const policies = Array.isArray(data.policies) ? data.policies.filter(isRecord) : [];
317
+ const effective = Array.isArray(data.effective) ? data.effective.filter(isRecord) : [];
318
+ const lines = ["", styles.bold(`Gate policies (${policies.length})`)];
319
+ if (policies.length === 0) {
320
+ lines.push(` ${styles.dim("No gate is required anywhere in this workspace.")}`);
321
+ }
322
+ else {
323
+ lines.push(...renderTextTable(["GATE", "SCOPE", "APPLIES TO", "REQUIRED"], policies.map((policy) => [
324
+ collabText(policy.gate_kind),
325
+ collabText(policy.scope),
326
+ collabText(policy.subject_id, `every ${collabText(policy.subject_kind)}`),
327
+ policy.required === true ? "yes" : "no",
328
+ ])));
329
+ }
330
+ if (effective.length > 0) {
331
+ lines.push("", styles.bold(`Effective for ${collabText(data.subject)}`));
332
+ lines.push(...renderTextTable(["GATE", "REQUIRED", "SOURCE", "BLOCKED NOW", "PENDING REQUEST"], effective.map((entry) => [
333
+ collabText(entry.gate_kind),
334
+ entry.required === true ? "yes" : "no",
335
+ collabText(entry.source),
336
+ entry.blocked === true ? "yes" : "no",
337
+ collabText(entry.pending_request_id, "—"),
338
+ ])));
339
+ }
340
+ lines.push(...collabTrailer(data));
341
+ lines.push("");
342
+ return lines.join("\n");
343
+ }
344
+ export function registerApprovalsFamily(program, handleAsyncAction) {
345
+ program
346
+ .command("approvals")
347
+ .description(`Second-party approvals: ask a named person to decide, and optionally make that decision a hard gate on the live action. Gates: ${COLLAB_GATE_GLOSS}. Asking never blocks anything by itself — a gate is separate, admin-set workspace policy (\`approvals gate set\`), and the owning primitive honors it (a gated sequence still previews and dry-runs; only the live start is refused). Only \`--approve\` opens a gate: \`--changes-requested\` is a real answer that asks for edits and leaves the gate shut. \`approvals client grant <email>\` hands a client a restricted login that can read the work, comment, and decide what is assigned to them — nothing else. ${COLLAB_BETA_NOTE}`)
348
+ .addCommand(new Command("request")
349
+ .description("Ask a named second party to decide. Assignees are workspace member EMAILS, and never your own: assigning yourself is refused, because a request you can answer yourself is not an approval. An address that is not a member is refused by name rather than silently dropped, because a request nobody can decide is only discovered the next morning.")
350
+ .requiredOption("--on <kind:id>", COLLAB_SUBJECT_REF_HELP)
351
+ .requiredOption("--gate <gate>", COLLAB_GATE_FLAG_HELP)
352
+ // requiredOption keeps `required: true` in the machine-readable manifest,
353
+ // but commander's mandatory check is satisfied by the collector's []
354
+ // default — so the action asserts non-empty itself (requireCollabAssignees).
355
+ .requiredOption("--assignee <email>", "Who decides — someone other than you. Repeatable: any one of them can answer. Required.", collectRepeatable, [])
356
+ .option("--path <path>", COLLAB_SUBJECT_PATH_HELP)
357
+ .option("--title <text>", "Short headline for the ask.")
358
+ .option("--body <text>", "What you want them to check.")
359
+ .option("--expires-hours <n>", "Deadline for the ANSWER: an undecided request lapses to expired after N hours, and an expired request never opens the gate. It does not put a clock on a yes — an approval given before the deadline keeps the gate open until the approved content changes.")
360
+ .option("--json", "Print a JSON envelope.")
361
+ .action(async (options) => {
362
+ await handleCollabAction("approvals request", options, () => {
363
+ const subject = requireCollabSubject(options.on);
364
+ const gateKind = requireCollabGate(options.gate);
365
+ const assignees = requireCollabAssignees(options.assignee);
366
+ const path = readCollabPath(options.path);
367
+ const title = readOption(options.title);
368
+ const body = readOption(options.body);
369
+ const expiresInHours = readPositiveInt(options.expiresHours);
370
+ return requestOxygen("/api/cli/collab/requests", {
371
+ method: "POST",
372
+ body: {
373
+ subject_kind: subject.kind,
374
+ subject_id: subject.id,
375
+ ...(path ? { subject_path: path } : {}),
376
+ gate_kind: gateKind,
377
+ ...(title ? { title } : {}),
378
+ ...(body ? { body } : {}),
379
+ assignees,
380
+ ...(expiresInHours !== undefined ? { expires_in_hours: expiresInHours } : {}),
381
+ },
382
+ });
383
+ }, formatCollabRequestWrite);
384
+ }))
385
+ .addCommand(new Command("list")
386
+ .description("Approval requests in this workspace, newest first. Use --mine for the ones assigned to you, or `approvals inbox` for the daily view.")
387
+ .option("--status <status>", `Filter by status: ${["pending", "approved", "changes_requested", "rejected", "cancelled", "expired"].join(", ")}.`)
388
+ .option("--mine", "Only requests assigned to you.")
389
+ .option("--on <kind:id>", `Only requests on one subject. ${COLLAB_SUBJECT_REF_HELP}`)
390
+ .option("--limit <n>", "Maximum requests to return.")
391
+ .option("--json", "Print a JSON envelope.")
392
+ .action(async (options) => {
393
+ await handleCollabAction("approvals list", options, () => {
394
+ const params = new URLSearchParams();
395
+ const status = readOption(options.status);
396
+ if (status)
397
+ params.set("status", status);
398
+ if (options.mine)
399
+ params.set("assigned_to_me", "true");
400
+ const on = readOption(options.on);
401
+ if (on) {
402
+ const subject = requireCollabSubject(on);
403
+ params.set("subject", `${subject.kind}:${subject.id}`);
404
+ }
405
+ const limit = readPositiveInt(options.limit);
406
+ if (limit !== undefined)
407
+ params.set("limit", String(limit));
408
+ const query = params.toString();
409
+ return requestOxygen(`/api/cli/collab/requests${query ? `?${query}` : ""}`);
410
+ }, formatCollabRequestList);
411
+ }))
412
+ .addCommand(new Command("inbox")
413
+ .description("What is waiting on YOU: pending approvals assigned to you plus open threads that name you, each with the command that answers it. This is the surface a client logs in to.")
414
+ .option("--limit <n>", "Maximum items per section.")
415
+ .option("--json", "Print a JSON envelope.")
416
+ .action(async (options) => {
417
+ await handleCollabAction("approvals inbox", options, () => {
418
+ const limit = readPositiveInt(options.limit);
419
+ return requestOxygen(`/api/cli/collab/inbox${limit !== undefined ? `?limit=${limit}` : ""}`);
420
+ }, formatCollabInbox);
421
+ }))
422
+ .addCommand(new Command("show")
423
+ .description("One approval request by its own id: what is being asked, on what, by whom, who must decide, and whether it can still be decided at all. Run this on the id in a 'you have been asked to approve' email or a `/collab/approvals/<id>` link — `list` cannot find an id you have no subject for. An expired request is reported as such, with the command that clears it.")
424
+ .argument("<requestId>", "Request id (from a notification email, a deep-link, or `oxygen approvals inbox`).")
425
+ .option("--json", "Print a JSON envelope.")
426
+ .action(async (requestId, options) => {
427
+ await handleCollabAction("approvals show", options, () => requestOxygen(`/api/cli/collab/requests/${encodeURIComponent(requestId)}`), formatCollabRequestDetail);
428
+ }))
429
+ .addCommand(new Command("decide")
430
+ .description("Answer a pending request. Exactly one of --approve / --reject / --changes-requested. Assignees decide; a workspace admin may decide for the workspace; the requester may NOT decide their own request (that is the whole point) — withdraw it with `approvals cancel` instead. ONLY --approve opens a gate.")
431
+ .argument("<requestId>", "Request id (from `oxygen approvals inbox`).")
432
+ .option("--approve", "Approve it. This is the only decision that opens a gate.")
433
+ .option("--reject", "Refuse it. The gate stays shut.")
434
+ .option("--changes-requested", "Ask for edits. A real answer that does NOT open the gate — the requester revises and asks again.")
435
+ .option("--note <text>", "Why. Stored on the request and quoted back to whoever is blocked by it.")
436
+ .option("--json", "Print a JSON envelope.")
437
+ .action(async (requestId, options) => {
438
+ await handleCollabAction("approvals decide", options, () => {
439
+ const decision = requireCollabDecision(options);
440
+ const note = readOption(options.note);
441
+ return requestOxygen(`/api/cli/collab/requests/${encodeURIComponent(requestId)}/decide`, {
442
+ method: "POST",
443
+ body: { decision, ...(note ? { note } : {}) },
444
+ });
445
+ }, formatCollabDecision);
446
+ }))
447
+ .addCommand(new Command("cancel")
448
+ .description("Withdraw a pending request you filed (or, as an admin, clear a stale one). Replaying a cancel is a success flagged `already`; cancelling something already approved or rejected is refused, because a silent no-op would leave you believing a live action is still gated when it is not.")
449
+ .argument("<requestId>", "Request id.")
450
+ .option("--note <text>", "Why you are withdrawing it.")
451
+ .option("--json", "Print a JSON envelope.")
452
+ .action(async (requestId, options) => {
453
+ await handleCollabAction("approvals cancel", options, () => {
454
+ const note = readOption(options.note);
455
+ return requestOxygen(`/api/cli/collab/requests/${encodeURIComponent(requestId)}/cancel`, {
456
+ method: "POST",
457
+ body: note ? { note } : {},
458
+ });
459
+ }, formatCollabWriteResult);
460
+ }))
461
+ .addCommand(new Command("gate")
462
+ .description("The opt-in hard gate. Off everywhere until an admin turns it on, so shipping this changed nobody's workflow.")
463
+ .addCommand(new Command("set")
464
+ .description(`Require (or stop requiring) an approval before the live action. ADMIN ONLY — a member who can switch the gate off at will is not a gate. Scope it to one object with --on, or to every object of a kind with --default. ${COLLAB_GATE_GLOSS}.`)
465
+ .option("--on <kind:id>", `Gate one object. ${COLLAB_SUBJECT_REF_HELP}`)
466
+ .option("--default <kind>", `Gate every object of a kind (the workspace default). One of: ${COLLAB_SUBJECT_KINDS.join(", ")}.`)
467
+ .requiredOption("--gate <gate>", COLLAB_GATE_FLAG_HELP)
468
+ .option("--required", "Turn the gate ON.")
469
+ .option("--not-required", "Turn the gate OFF.")
470
+ .option("--json", "Print a JSON envelope.")
471
+ .action(async (options) => {
472
+ await handleCollabAction("approvals gate set", options, () => requestOxygen("/api/cli/collab/gates", {
473
+ method: "POST",
474
+ body: collabGateSetBody(options),
475
+ }), formatCollabGateSet);
476
+ }))
477
+ .addCommand(new Command("list")
478
+ .description("Which gates this workspace requires. Pass --on to also get the EFFECTIVE answer for one object — its own policy, then the workspace default for its kind, then 'not required' — plus whether it is blocked right now.")
479
+ .option("--on <kind:id>", `Also resolve the effective gates for one object. ${COLLAB_SUBJECT_REF_HELP}`)
480
+ .option("--json", "Print a JSON envelope.")
481
+ .action(async (options) => {
482
+ await handleCollabAction("approvals gate list", options, () => {
483
+ const on = readOption(options.on);
484
+ if (!on)
485
+ return requestOxygen("/api/cli/collab/gates");
486
+ const subject = requireCollabSubject(on);
487
+ return requestOxygen(`/api/cli/collab/gates?subject=${encodeURIComponent(`${subject.kind}:${subject.id}`)}`);
488
+ }, formatCollabGateList);
489
+ })))
490
+ .addCommand(new Command("client")
491
+ .description("The restricted login an agency hands its client: read the work, comment on it, decide the approvals assigned to them, nothing else. ADMIN ONLY. Invite them to the workspace first — this grants the overlay on an existing membership.")
492
+ .addCommand(new Command("grant")
493
+ .description("Restrict a member to the client role. An address that is INVITED but has not joined works too: the role is recorded against the invitation and applies the moment they accept, so you do not have to watch for the acceptance and come back.")
494
+ .argument("<email>", "Workspace member's email, or a pending invitation's email.")
495
+ .option("--json", "Print a JSON envelope.")
496
+ .action(async (email, options) => {
497
+ await handleCollabAction("approvals client", options, () => requestOxygen("/api/cli/collab/members/role", {
498
+ method: "POST",
499
+ body: { email, role: "client" },
500
+ }), formatCollabWriteResult);
501
+ }))
502
+ .addCommand(new Command("revoke")
503
+ .description("Clear the client role and restore full workspace access. Works on a pending invitation too, clearing what that address would land as.")
504
+ .argument("<email>", "Workspace member's email, or a pending invitation's email.")
505
+ .option("--json", "Print a JSON envelope.")
506
+ .action(async (email, options) => {
507
+ await handleCollabAction("approvals client", options, () => requestOxygen("/api/cli/collab/members/role", {
508
+ method: "POST",
509
+ body: { email, role: null },
510
+ }), formatCollabWriteResult);
511
+ }))
512
+ .addCommand(new Command("list")
513
+ .description("Every member of this workspace and what they may do — the workspace role plus the Oxygen client overlay — followed by anyone invited who has not joined yet, and what they will land as. Also names, per member, why a client grant on them would be refused. Same read the /collab/members page renders.")
514
+ .option("--json", "Print a JSON envelope.")
515
+ .action(async (options) => {
516
+ await handleCollabAction("approvals client list", options, () => requestOxygen("/api/cli/collab/members"), formatCollabMembers);
517
+ })));
518
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerAuthFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;