@oxygen-agent/cli 1.1014.136 → 1.1014.618

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +27 -19
  3. package/dist/cli-output.d.ts +12 -0
  4. package/dist/cli-output.js +395 -0
  5. package/dist/cli-paths.d.ts +1 -0
  6. package/dist/cli-paths.js +3 -0
  7. package/dist/cli-profile-context.d.ts +3 -0
  8. package/dist/cli-profile-context.js +13 -0
  9. package/dist/command-manifest.d.ts +15 -0
  10. package/dist/command-manifest.js +23 -1
  11. package/dist/command-support.d.ts +438 -0
  12. package/dist/command-support.js +1610 -0
  13. package/dist/commands/action-column.d.ts +3 -0
  14. package/dist/commands/action-column.js +43 -0
  15. package/dist/commands/activation.d.ts +3 -0
  16. package/dist/commands/activation.js +24 -0
  17. package/dist/commands/admin.d.ts +3 -0
  18. package/dist/commands/admin.js +347 -0
  19. package/dist/commands/ads.d.ts +3 -0
  20. package/dist/commands/ads.js +62 -0
  21. package/dist/commands/agent.d.ts +3 -0
  22. package/dist/commands/agent.js +274 -0
  23. package/dist/commands/api-keys.d.ts +3 -0
  24. package/dist/commands/api-keys.js +77 -0
  25. package/dist/commands/approvals.d.ts +3 -0
  26. package/dist/commands/approvals.js +518 -0
  27. package/dist/commands/auth.d.ts +3 -0
  28. package/dist/commands/auth.js +230 -0
  29. package/dist/commands/billing.d.ts +3 -0
  30. package/dist/commands/billing.js +357 -0
  31. package/dist/commands/blueprints.d.ts +3 -0
  32. package/dist/commands/blueprints.js +312 -0
  33. package/dist/commands/budget.d.ts +3 -0
  34. package/dist/commands/budget.js +55 -0
  35. package/dist/commands/callables.d.ts +3 -0
  36. package/dist/commands/callables.js +111 -0
  37. package/dist/commands/capabilities.d.ts +3 -0
  38. package/dist/commands/capabilities.js +44 -0
  39. package/dist/commands/cells.d.ts +3 -0
  40. package/dist/commands/cells.js +94 -0
  41. package/dist/commands/columns.d.ts +3 -0
  42. package/dist/commands/columns.js +1628 -0
  43. package/dist/commands/commands.d.ts +3 -0
  44. package/dist/commands/commands.js +45 -0
  45. package/dist/commands/comments.d.ts +3 -0
  46. package/dist/commands/comments.js +188 -0
  47. package/dist/commands/companies.d.ts +3 -0
  48. package/dist/commands/companies.js +312 -0
  49. package/dist/commands/connections.d.ts +3 -0
  50. package/dist/commands/connections.js +39 -0
  51. package/dist/commands/context.d.ts +3 -0
  52. package/dist/commands/context.js +170 -0
  53. package/dist/commands/copilot.d.ts +3 -0
  54. package/dist/commands/copilot.js +565 -0
  55. package/dist/commands/crm.d.ts +3 -0
  56. package/dist/commands/crm.js +1669 -0
  57. package/dist/commands/custom-integrations.d.ts +3 -0
  58. package/dist/commands/custom-integrations.js +140 -0
  59. package/dist/commands/dashboards.d.ts +3 -0
  60. package/dist/commands/dashboards.js +240 -0
  61. package/dist/commands/db.d.ts +3 -0
  62. package/dist/commands/db.js +190 -0
  63. package/dist/commands/deliverability.d.ts +3 -0
  64. package/dist/commands/deliverability.js +90 -0
  65. package/dist/commands/directory.d.ts +3 -0
  66. package/dist/commands/directory.js +101 -0
  67. package/dist/commands/domains.d.ts +3 -0
  68. package/dist/commands/domains.js +604 -0
  69. package/dist/commands/egress.d.ts +3 -0
  70. package/dist/commands/egress.js +130 -0
  71. package/dist/commands/email.d.ts +3 -0
  72. package/dist/commands/email.js +49 -0
  73. package/dist/commands/engagement.d.ts +3 -0
  74. package/dist/commands/engagement.js +189 -0
  75. package/dist/commands/enrich-column.d.ts +3 -0
  76. package/dist/commands/enrich-column.js +149 -0
  77. package/dist/commands/enrichment.d.ts +3 -0
  78. package/dist/commands/enrichment.js +49 -0
  79. package/dist/commands/feedback.d.ts +3 -0
  80. package/dist/commands/feedback.js +96 -0
  81. package/dist/commands/feeds.d.ts +3 -0
  82. package/dist/commands/feeds.js +486 -0
  83. package/dist/commands/find.d.ts +3 -0
  84. package/dist/commands/find.js +159 -0
  85. package/dist/commands/followers.d.ts +3 -0
  86. package/dist/commands/followers.js +39 -0
  87. package/dist/commands/formulas.d.ts +3 -0
  88. package/dist/commands/formulas.js +43 -0
  89. package/dist/commands/home.d.ts +3 -0
  90. package/dist/commands/home.js +22 -0
  91. package/dist/commands/inbox.d.ts +3 -0
  92. package/dist/commands/inbox.js +779 -0
  93. package/dist/commands/integrations.d.ts +3 -0
  94. package/dist/commands/integrations.js +290 -0
  95. package/dist/commands/knowledge.d.ts +3 -0
  96. package/dist/commands/knowledge.js +874 -0
  97. package/dist/commands/lead-sourcing.d.ts +3 -0
  98. package/dist/commands/lead-sourcing.js +52 -0
  99. package/dist/commands/limits.d.ts +3 -0
  100. package/dist/commands/limits.js +54 -0
  101. package/dist/commands/linkedin.d.ts +3 -0
  102. package/dist/commands/linkedin.js +275 -0
  103. package/dist/commands/login.d.ts +3 -0
  104. package/dist/commands/login.js +347 -0
  105. package/dist/commands/logout.d.ts +3 -0
  106. package/dist/commands/logout.js +12 -0
  107. package/dist/commands/mailboxes.d.ts +3 -0
  108. package/dist/commands/mailboxes.js +1290 -0
  109. package/dist/commands/managed-inboxes.d.ts +3 -0
  110. package/dist/commands/managed-inboxes.js +361 -0
  111. package/dist/commands/messages.d.ts +3 -0
  112. package/dist/commands/messages.js +78 -0
  113. package/dist/commands/notetaker.d.ts +3 -0
  114. package/dist/commands/notetaker.js +79 -0
  115. package/dist/commands/observability.d.ts +3 -0
  116. package/dist/commands/observability.js +125 -0
  117. package/dist/commands/orgs.d.ts +3 -0
  118. package/dist/commands/orgs.js +237 -0
  119. package/dist/commands/people.d.ts +3 -0
  120. package/dist/commands/people.js +353 -0
  121. package/dist/commands/posts.d.ts +3 -0
  122. package/dist/commands/posts.js +139 -0
  123. package/dist/commands/profiles.d.ts +3 -0
  124. package/dist/commands/profiles.js +250 -0
  125. package/dist/commands/projects.d.ts +3 -0
  126. package/dist/commands/projects.js +94 -0
  127. package/dist/commands/prompts.d.ts +3 -0
  128. package/dist/commands/prompts.js +84 -0
  129. package/dist/commands/publishing.d.ts +3 -0
  130. package/dist/commands/publishing.js +1419 -0
  131. package/dist/commands/recipes.d.ts +3 -0
  132. package/dist/commands/recipes.js +120 -0
  133. package/dist/commands/reviews.d.ts +3 -0
  134. package/dist/commands/reviews.js +75 -0
  135. package/dist/commands/rows.d.ts +3 -0
  136. package/dist/commands/rows.js +27 -0
  137. package/dist/commands/runs.d.ts +3 -0
  138. package/dist/commands/runs.js +37 -0
  139. package/dist/commands/search.d.ts +3 -0
  140. package/dist/commands/search.js +182 -0
  141. package/dist/commands/senders.d.ts +3 -0
  142. package/dist/commands/senders.js +393 -0
  143. package/dist/commands/sequences.d.ts +3 -0
  144. package/dist/commands/sequences.js +2322 -0
  145. package/dist/commands/session.d.ts +3 -0
  146. package/dist/commands/session.js +109 -0
  147. package/dist/commands/signals.d.ts +3 -0
  148. package/dist/commands/signals.js +289 -0
  149. package/dist/commands/signup-leads.d.ts +3 -0
  150. package/dist/commands/signup-leads.js +46 -0
  151. package/dist/commands/skills.d.ts +3 -0
  152. package/dist/commands/skills.js +50 -0
  153. package/dist/commands/sourcing.d.ts +3 -0
  154. package/dist/commands/sourcing.js +27 -0
  155. package/dist/commands/status.d.ts +3 -0
  156. package/dist/commands/status.js +51 -0
  157. package/dist/commands/supabase.d.ts +3 -0
  158. package/dist/commands/supabase.js +128 -0
  159. package/dist/commands/support.d.ts +3 -0
  160. package/dist/commands/support.js +651 -0
  161. package/dist/commands/suppressions.d.ts +3 -0
  162. package/dist/commands/suppressions.js +577 -0
  163. package/dist/commands/table-ingestions.d.ts +3 -0
  164. package/dist/commands/table-ingestions.js +145 -0
  165. package/dist/commands/table-runs.d.ts +3 -0
  166. package/dist/commands/table-runs.js +277 -0
  167. package/dist/commands/tables.d.ts +3 -0
  168. package/dist/commands/tables.js +3492 -0
  169. package/dist/commands/tags.d.ts +3 -0
  170. package/dist/commands/tags.js +147 -0
  171. package/dist/commands/tools.d.ts +3 -0
  172. package/dist/commands/tools.js +246 -0
  173. package/dist/commands/update.d.ts +3 -0
  174. package/dist/commands/update.js +106 -0
  175. package/dist/commands/verify.d.ts +3 -0
  176. package/dist/commands/verify.js +27 -0
  177. package/dist/commands/viewers.d.ts +3 -0
  178. package/dist/commands/viewers.js +72 -0
  179. package/dist/commands/voice.d.ts +3 -0
  180. package/dist/commands/voice.js +351 -0
  181. package/dist/commands/whatsapp.d.ts +3 -0
  182. package/dist/commands/whatsapp.js +226 -0
  183. package/dist/commands/whoami.d.ts +3 -0
  184. package/dist/commands/whoami.js +110 -0
  185. package/dist/commands/worker.d.ts +3 -0
  186. package/dist/commands/worker.js +60 -0
  187. package/dist/commands/workflows.d.ts +3 -0
  188. package/dist/commands/workflows.js +1752 -0
  189. package/dist/commands/workspace.d.ts +3 -0
  190. package/dist/commands/workspace.js +94 -0
  191. package/dist/compact-list-flags.d.ts +7 -0
  192. package/dist/compact-list-flags.js +12 -0
  193. package/dist/crm-support-flags.js +14 -5
  194. package/dist/customer-operation-commands.js +8 -6
  195. package/dist/diagnostic-uploader.d.ts +3 -1
  196. package/dist/diagnostic-uploader.js +29 -8
  197. package/dist/help.d.ts +5 -0
  198. package/dist/help.js +22 -0
  199. package/dist/index.d.ts +98 -1
  200. package/dist/index.js +331 -30719
  201. package/dist/local-module-loader.d.ts +4 -0
  202. package/dist/local-module-loader.js +156 -0
  203. package/dist/visual-commands.js +9 -5
  204. package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
  205. package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
  206. package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
  207. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
  208. package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
  209. package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
  210. package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
  211. package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
  212. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
  213. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
  214. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
  215. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
  216. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
  217. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
  218. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
  219. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
  220. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
  221. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
  222. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
  223. package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
  224. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
  225. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
  226. package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
  227. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
  228. package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
  229. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
  230. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
  231. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
  232. package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
  233. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
  234. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
  235. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
  236. package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
  237. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
  238. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
  239. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
  240. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
  241. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
  242. package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
  243. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
  244. package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
  245. package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
  246. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
  247. package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
  248. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
  249. package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
  250. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
  251. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
  252. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
  253. package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
  254. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
  255. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
  256. package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
  257. package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
  258. package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
  259. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
  260. package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
  261. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
  262. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
  263. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  264. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  265. package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
  266. package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
  267. package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
  268. package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
  269. package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
  270. package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
  271. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
  272. package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
  273. package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
  274. package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
  275. package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
  276. package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
  277. package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
  278. package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
  279. package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
  280. package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
  281. package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
  282. package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
  283. package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
  284. package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
  285. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
  286. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  287. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  288. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
  289. package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
  290. package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
  291. package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
  292. package/package.json +1 -1
@@ -0,0 +1,565 @@
1
+ import { emitCliFailure, emitSuccess } from "../cli-output.js";
2
+ import { readJsonObjectOption, readNonNegativeInt, readPositiveInt, readRecordString } from "../cli-values.js";
3
+ import { collectRepeatable, readPositiveNumber } from "../command-support.js";
4
+ import { addContractFlags, readContractFlags } from "../contract-flags.js";
5
+ import { requestOxygen } from "../http-client.js";
6
+ import { waitForCliRun } from "../run-wait.js";
7
+ import { resolveCliBinaryName } from "../runtime.js";
8
+ import { isRecord, readOption } from "../util.js";
9
+ import { OxygenError } from "@oxygen/shared/cli-result";
10
+ import { COPILOT_SESSIONS_LIST_CONTRACT } from "@oxygen/shared/contracts/copilot-sessions";
11
+ import { formatCopilotPlanDuration, formatCopilotPlanSeconds, normalizeCopilotPlanStepStatus } from "@oxygen/shared/copilot-plan";
12
+ import { COPILOT_ASK_USER_TOOL_NAME, normalizeCopilotQuestions } from "@oxygen/shared/copilot-questions";
13
+ import { Command, Option } from "commander";
14
+ import { readFileSync } from "node:fs";
15
+ import { basename, resolve } from "node:path";
16
+ // `copilot send` streams a single turn: a shorter default deadline than a batch
17
+ // workflow tail, polled every couple of seconds so assistant text feels live.
18
+ const COPILOT_SEND_DEFAULT_TIMEOUT_SECONDS = 300;
19
+ const COPILOT_SEND_DEFAULT_INTERVAL_SECONDS = 2;
20
+ // "Show me" is retired (2026-10-14): the web app shows what the copilot creates
21
+ // or changes in the chat, and nothing moves anyone's screen. `copilot follow`
22
+ // and --no-follow stay so older scripts keep running; they change nothing.
23
+ const FOLLOW_RETIRED_NOTICE = "The web app now shows changes in the chat.";
24
+ /**
25
+ * Attach one local file to a copilot session: presign, PUT the bytes straight to
26
+ * object storage, then have the server verify and describe what landed.
27
+ *
28
+ * The middle hop bypasses the API on purpose — the same reason `tables import`
29
+ * does it. A real lead list does not fit in a request body, and the worker reads
30
+ * the object out of the bucket anyway.
31
+ */
32
+ async function uploadCopilotAttachment(sessionId, filePath) {
33
+ const resolved = resolve(filePath);
34
+ let fileBuffer;
35
+ try {
36
+ fileBuffer = readFileSync(resolved);
37
+ }
38
+ catch {
39
+ throw new OxygenError("file_not_found", `Could not read ${filePath}.`, { exitCode: 1 });
40
+ }
41
+ const fileName = basename(resolved);
42
+ const ticket = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/attachments`, {
43
+ method: "POST",
44
+ body: {
45
+ file_name: fileName,
46
+ content_length: fileBuffer.byteLength,
47
+ },
48
+ });
49
+ const upload = isRecord(ticket.upload) ? ticket.upload : {};
50
+ const attachment = isRecord(ticket.attachment) ? ticket.attachment : {};
51
+ const uploadUrl = readRecordString(upload, "url");
52
+ const attachmentId = readRecordString(attachment, "id");
53
+ if (!uploadUrl || !attachmentId) {
54
+ throw new OxygenError("attachment_upload_failed", "The server did not return an upload URL for that attachment.", { exitCode: 1 });
55
+ }
56
+ const controller = new AbortController();
57
+ const timer = setTimeout(() => controller.abort(), 300_000);
58
+ // Content-Type is bound into the storage signature. Older tickets omit the
59
+ // SDK's binary default; newer tickets supply every required upload header.
60
+ const uploadHeaders = { "content-type": "application/octet-stream" };
61
+ if (isRecord(upload.headers)) {
62
+ for (const [name, value] of Object.entries(upload.headers)) {
63
+ if (typeof value === "string")
64
+ uploadHeaders[name] = value;
65
+ }
66
+ }
67
+ let putResponse;
68
+ try {
69
+ putResponse = await fetch(uploadUrl, {
70
+ method: "PUT",
71
+ headers: uploadHeaders,
72
+ body: new Uint8Array(fileBuffer),
73
+ signal: controller.signal,
74
+ });
75
+ }
76
+ finally {
77
+ clearTimeout(timer);
78
+ }
79
+ if (!putResponse.ok) {
80
+ throw new OxygenError("attachment_upload_failed", `Uploading ${fileName} to object storage failed (HTTP ${putResponse.status}).`, { details: { status: putResponse.status }, exitCode: 1 });
81
+ }
82
+ const completed = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/attachments/${encodeURIComponent(attachmentId)}/complete`, { method: "POST", timeoutMs: 120_000 });
83
+ return {
84
+ id: attachmentId,
85
+ attachment: isRecord(completed.attachment) ? completed.attachment : { id: attachmentId },
86
+ };
87
+ }
88
+ function isTerminalCopilotTurnStatus(status) {
89
+ // Terminal here means "stop streaming this turn": completed/failed/cancelled
90
+ // are final, and awaiting_input parks the turn on a human decision (an
91
+ // approval) that the caller surfaces rather than polling through.
92
+ return status === "completed" || status === "failed"
93
+ || status === "cancelled" || status === "awaiting_input";
94
+ }
95
+ function readCopilotTurnStatus(data, turnId) {
96
+ const turns = Array.isArray(data.turns) ? data.turns : [];
97
+ for (const turn of turns) {
98
+ if (isRecord(turn) && turn.id === turnId) {
99
+ return typeof turn.status === "string" ? turn.status : null;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ function readCopilotTurnCreditsCaptured(data, turnId) {
105
+ const turns = Array.isArray(data.turns) ? data.turns : [];
106
+ for (const turn of turns) {
107
+ if (isRecord(turn) && turn.id === turnId && typeof turn.credits_captured === "number") {
108
+ return turn.credits_captured;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ // The session-detail payload carries no top-level `credits` block, so the shared
114
+ // writeCreditsReceipt (which handleAsyncAction runs on the returned data) is a
115
+ // no-op for `copilot send`. Mirror the sent turn's own captured spend as one
116
+ // stderr line so a terminal still shows what the turn cost.
117
+ function writeCopilotTurnCreditsSummary(data, turnId) {
118
+ const captured = readCopilotTurnCreditsCaptured(data, turnId);
119
+ if (captured === null)
120
+ return;
121
+ process.stderr.write(`credits used this turn: ${captured.toLocaleString("en-US")}\n`);
122
+ }
123
+ // Print one appended copilot event to stderr as it streams in. assistant_delta
124
+ // text is written verbatim — no prefix, no trailing newline — so the model's
125
+ // output flows as one continuous block; every other kind is a one-line human
126
+ // notice. billing_settled is intentionally silent (its spend lands in the
127
+ // end-of-turn credits summary); tool_call_* surface as brief progress so a turn
128
+ // that pauses on a slow capability does not look frozen.
129
+ /**
130
+ * The plan as a person reads it.
131
+ *
132
+ * `copilot get` has no human renderer at all -- both its branches call writeJson,
133
+ * so --json only decides whether the payload is wrapped in an envelope. This leaf
134
+ * exists because "how far along is it" is a question a JSON dump answers badly,
135
+ * and because the same projection has to be legible on all three surfaces.
136
+ */
137
+ function writeCopilotPlan(plan, webUrl) {
138
+ if (!isRecord(plan)) {
139
+ process.stdout.write("No plan yet. The copilot publishes one for multi-step work; a short answer finishes without planning.\n");
140
+ return;
141
+ }
142
+ const progress = isRecord(plan.progress) ? plan.progress : {};
143
+ const eta = isRecord(plan.eta) ? plan.eta : {};
144
+ const steps = Array.isArray(plan.steps) ? plan.steps : [];
145
+ const lines = [];
146
+ if (typeof plan.summary === "string" && plan.summary)
147
+ lines.push(plan.summary, "");
148
+ lines.push(`${progress.done ?? 0}/${progress.total ?? 0} steps done${formatPlanEta(eta, plan.turnActive !== false)}`, "");
149
+ for (const step of steps) {
150
+ if (!isRecord(step))
151
+ continue;
152
+ const status = String(step.status ?? "pending");
153
+ const mark = status === "done" ? "[x]" : status === "in_progress" ? "[>]" : status === "blocked" ? "[!]" : "[ ]";
154
+ const timing = status === "done"
155
+ ? formatPlanDuration(step.elapsedMs)
156
+ : typeof step.etaSeconds === "number" ? `~${formatCopilotPlanSeconds(step.etaSeconds)} left` : "";
157
+ lines.push(`${mark} ${String(step.title ?? "")}${timing ? ` (${timing})` : ""}`);
158
+ for (const sub of Array.isArray(step.substeps) ? step.substeps : []) {
159
+ if (!isRecord(sub))
160
+ continue;
161
+ const subStatus = String(sub.status ?? "pending");
162
+ const subMark = subStatus === "done" ? "-" : subStatus === "blocked" ? "!" : ">";
163
+ const subTiming = formatPlanDuration(sub.durationMs);
164
+ lines.push(` ${subMark} ${String(sub.title ?? "")}${subTiming ? ` (${subTiming})` : ""}`);
165
+ }
166
+ }
167
+ if (typeof webUrl === "string" && webUrl)
168
+ lines.push("", webUrl);
169
+ process.stdout.write(`${lines.join("\n")}\n`);
170
+ }
171
+ /** Every estimate names its basis, so nobody reads a guess as a measurement. */
172
+ function formatPlanEta(eta, turnActive) {
173
+ // Nothing is running, so nothing REMAINS. "estimating" on a session that stopped
174
+ // weeks ago describes work in flight that does not exist; the step marks still
175
+ // say where the copilot got to, which is the honest half of the answer.
176
+ if (!turnActive)
177
+ return "";
178
+ const remaining = typeof eta.remainingSeconds === "number" ? eta.remainingSeconds : null;
179
+ if (remaining === null)
180
+ return " · time remaining: estimating";
181
+ const measured = typeof eta.measuredSteps === "number" ? eta.measuredSteps : 0;
182
+ const basis = eta.basis === "calibrated" || eta.basis === "measured"
183
+ ? `measured from ${measured} completed step${measured === 1 ? "" : "s"}`
184
+ : eta.basis === "model" ? "estimated, nothing measured yet" : "no basis";
185
+ const bound = eta.deadlineBound === true ? ", capped by the turn deadline" : "";
186
+ return ` · ~${formatCopilotPlanSeconds(remaining)} left (${basis}${bound})`;
187
+ }
188
+ /** Narrows the untyped ledger value, then defers to the one shared formatter. */
189
+ function formatPlanDuration(ms) {
190
+ return (typeof ms === "number" ? formatCopilotPlanDuration(ms) : null) ?? "";
191
+ }
192
+ function printCopilotEvent(event, sessionId) {
193
+ if (!isRecord(event))
194
+ return;
195
+ const kind = typeof event.kind === "string" ? event.kind : "";
196
+ const payload = isRecord(event.payload) ? event.payload : {};
197
+ switch (kind) {
198
+ case "assistant_delta": {
199
+ const text = typeof payload.text === "string" ? payload.text : "";
200
+ if (text)
201
+ process.stderr.write(text);
202
+ return;
203
+ }
204
+ case "tool_call_started": {
205
+ // A question prints whole once it is asked; "[tool] ask_user…" says nothing.
206
+ if (payload.tool === COPILOT_ASK_USER_TOOL_NAME)
207
+ return;
208
+ const capability = typeof payload.capability === "string" ? payload.capability : "tool";
209
+ process.stderr.write(`\n[tool] ${capability}…\n`);
210
+ return;
211
+ }
212
+ case "tool_call_finished": {
213
+ if (payload.tool === COPILOT_ASK_USER_TOOL_NAME) {
214
+ printCopilotQuestion(payload, sessionId);
215
+ return;
216
+ }
217
+ const capability = typeof payload.capability === "string" ? payload.capability : "tool";
218
+ const summary = typeof payload.summary === "string" ? payload.summary : "";
219
+ process.stderr.write(`[tool] ${capability} ${payload.ok === true ? "ok" : "failed"}${summary ? ` — ${summary}` : ""}\n`);
220
+ return;
221
+ }
222
+ case "approval_requested": {
223
+ const capability = typeof payload.capability === "string" ? payload.capability : "action";
224
+ const preview = typeof payload.preview === "string" ? payload.preview : "";
225
+ const estimated = typeof payload.estimated_credits === "number" ? payload.estimated_credits : null;
226
+ const approvalId = typeof payload.approval_id === "string" ? payload.approval_id : "";
227
+ process.stderr.write(`\n[approval required] ${capability}${preview ? ` — ${preview}` : ""}`
228
+ + `${estimated !== null ? ` (~${estimated.toLocaleString("en-US")} credits)` : ""}. `
229
+ + `Approve with: ${resolveCliBinaryName()} copilot approve ${sessionId} ${approvalId}\n`);
230
+ return;
231
+ }
232
+ case "plan_updated": {
233
+ // The plan is the one event that says how far along the turn is, and it was
234
+ // the only kind this printer dropped -- a `copilot send` streamed every tool
235
+ // call it made and never once said what it was working towards.
236
+ const steps = Array.isArray(payload.steps) ? payload.steps : [];
237
+ const done = steps.filter((step) => isRecord(step) && normalizeCopilotPlanStepStatus(step.status) === "done").length;
238
+ const current = steps.find((step) => isRecord(step) && normalizeCopilotPlanStepStatus(step.status) === "in_progress");
239
+ const title = isRecord(current)
240
+ ? String(current.title ?? current.description ?? "").trim()
241
+ : "";
242
+ process.stderr.write(`\n[plan] ${done}/${steps.length} done${title ? ` — now: ${title}` : ""}\n`);
243
+ return;
244
+ }
245
+ case "fallback_engaged": {
246
+ const from = typeof payload.from === "string" ? payload.from : "?";
247
+ const to = typeof payload.to === "string" ? payload.to : "?";
248
+ const reason = typeof payload.reason === "string" ? payload.reason : "";
249
+ process.stderr.write(`[model fallback] ${from} → ${to}${reason ? ` (${reason})` : ""}\n`);
250
+ return;
251
+ }
252
+ case "error": {
253
+ const code = typeof payload.code === "string" ? payload.code : "error";
254
+ const message = typeof payload.message === "string" ? payload.message : "";
255
+ process.stderr.write(`[error] ${code}${message ? `: ${message}` : ""}\n`);
256
+ return;
257
+ }
258
+ default:
259
+ return;
260
+ }
261
+ }
262
+ /**
263
+ * The Copilot ended its turn on a question (`ask_user`). The web answers it on a
264
+ * card; here the options print numbered and the answer is the next message, so
265
+ * nothing about it needs a command of its own.
266
+ */
267
+ function printCopilotQuestion(payload, sessionId) {
268
+ const result = isRecord(payload.result) ? payload.result : {};
269
+ if (result.ok === false)
270
+ return;
271
+ const asked = normalizeCopilotQuestions(result.questions);
272
+ if (!asked.ok)
273
+ return;
274
+ const lines = [];
275
+ for (const question of asked.questions) {
276
+ lines.push(`\n[question] ${question.question}${question.multiSelect ? " (choose any)" : ""}`);
277
+ question.options.forEach((option, index) => {
278
+ lines.push(` ${index + 1}. ${option.label}${option.recommended ? " (recommended)" : ""}`
279
+ + `${option.description ? ` — ${option.description}` : ""}`);
280
+ });
281
+ }
282
+ lines.push(`Answer with: ${resolveCliBinaryName()} copilot send ${sessionId} <your answer>\n`);
283
+ process.stderr.write(lines.join("\n"));
284
+ }
285
+ // `copilot send` is the CLI's one streaming wait surface. It reuses the shared
286
+ // waitForCliRun poll loop (timeout/interval parsing, deadline, bounded sleep,
287
+ // typed timeout throw) but injects a fetch that GETs the session detail after a
288
+ // moving `after_seq` cursor, prints each newly appended event to stderr, and
289
+ // advances the cursor to latest_seq. The SENT TURN's status — not the session's
290
+ // — is the terminal signal, so fetchRun returns a record whose `status` mirrors
291
+ // that turn while shapeTerminal returns the last full GET payload for scripts.
292
+ function waitForCopilotTurn(options) {
293
+ let cursor = options.initialSeq;
294
+ let latestData = {};
295
+ return waitForCliRun({
296
+ runId: options.turnId,
297
+ requestedTimeoutSeconds: options.requestedTimeoutSeconds,
298
+ requestedIntervalSeconds: options.requestedIntervalSeconds,
299
+ defaultTimeoutSeconds: COPILOT_SEND_DEFAULT_TIMEOUT_SECONDS,
300
+ defaultIntervalSeconds: COPILOT_SEND_DEFAULT_INTERVAL_SECONDS,
301
+ fetchRun: async () => {
302
+ const query = new URLSearchParams({ after_seq: String(cursor) });
303
+ latestData = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(options.sessionId)}?${query.toString()}`);
304
+ const events = Array.isArray(latestData.events) ? latestData.events : [];
305
+ for (const event of events)
306
+ printCopilotEvent(event, options.sessionId);
307
+ const latestSeq = latestData.latest_seq;
308
+ if (typeof latestSeq === "number" && Number.isFinite(latestSeq))
309
+ cursor = latestSeq;
310
+ // waitForCliRun reads terminality off `status`; mirror the sent turn's
311
+ // status there while leaving latestData itself untouched for shapeTerminal.
312
+ return { ...latestData, status: readCopilotTurnStatus(latestData, options.turnId) };
313
+ },
314
+ isTerminal: isTerminalCopilotTurnStatus,
315
+ shapeTerminal: () => latestData,
316
+ timeoutCode: "copilot_send_timeout",
317
+ // Only the CLI stops waiting. The turn keeps running on the server and keeps
318
+ // billing until it finishes or is interrupted, so the timeout says so and names
319
+ // the commands that read and stop it.
320
+ timeoutMessage: "Stopped waiting for the copilot turn. It is still running and keeps spending credits until it finishes or you interrupt it.",
321
+ timeoutDetailIdKey: "turn_id",
322
+ timeoutExtraDetails: () => ({
323
+ session_id: options.sessionId,
324
+ turn_still_running: true,
325
+ next_step: `oxygen copilot get ${options.sessionId}`,
326
+ interrupt_command: `oxygen copilot interrupt ${options.sessionId} ${options.turnId}`,
327
+ }),
328
+ });
329
+ }
330
+ export function registerCopilotFamily(program, handleAsyncAction) {
331
+ program
332
+ .command("copilot")
333
+ .description("Workspace Copilot: an attended workspace agent you drive turn by turn from the terminal. Start a session, send it messages and watch the reply stream, approve the actions it proposes, then cancel when done. Managed inference uses workspace credits; paid or external actions keep their own approval gates. Web: /copilot.")
334
+ .addCommand(new Command("dev-eval")
335
+ .description("DEV staff owner only: immutable USD30 campaign and supplier ledger; classify dispatches guarded inference.")
336
+ .argument("<operation>", "create, register, readiness, ledger, effects, classify, accept-pilot or end")
337
+ .option("--campaign-id <uuid>")
338
+ .option("--session-id <uuid>")
339
+ .option("--models <ids>", "Comma-separated exact priced model IDs for create")
340
+ .option("--question-set <name>", "Pinned thesis-icp-1 or title-fit-1")
341
+ .option("--state-json <json>", "Classifier state from the native readout")
342
+ .option("--json", "Print a JSON envelope.")
343
+ .action(async (operation, options) => {
344
+ await handleAsyncAction("copilot dev-eval", options, () => {
345
+ if (operation === "readiness")
346
+ return requestOxygen("/api/cli/copilot/dev-eval?readiness=1");
347
+ if (operation === "effects")
348
+ return requestOxygen(`/api/cli/copilot/dev-eval?campaign_id=${encodeURIComponent(options.campaignId ?? "")}&session_id=${encodeURIComponent(options.sessionId ?? "")}`);
349
+ if (operation === "ledger")
350
+ return requestOxygen(`/api/cli/copilot/dev-eval?campaign_id=${encodeURIComponent(options.campaignId ?? "")}`);
351
+ if (!["create", "register", "end", "classify", "accept-pilot"].includes(operation))
352
+ throw new OxygenError("invalid_input", "Unknown campaign operation.", { exitCode: 1 });
353
+ return requestOxygen("/api/cli/copilot/dev-eval", { method: "POST", body: {
354
+ operation, campaign_id: options.campaignId, session_id: options.sessionId,
355
+ ...(options.questionSet ? { question_set: options.questionSet, state: JSON.parse(options.stateJson ?? "null") } : {}),
356
+ ...(options.models ? { models: options.models.split(",").map(value => value.trim()) } : {}),
357
+ } });
358
+ });
359
+ }), { hidden: true })
360
+ .addCommand(new Command("start")
361
+ .description("Start an attended Workspace Copilot session on Oxygen Balanced by default. Starting is free; the next send starts inference using workspace credits. No inference budget is required. Legacy budget flags remain accepted but do not authorize or cap spend. Paid or external actions require separate approval.")
362
+ .option("--budget-credits <number>", "Legacy compatibility only: accepted and clamped, but does not authorize or cap attended Copilot inference.")
363
+ .option("--tier <auto|low|medium|high>", "Model tier: auto=Oxygen Auto, low=Oxygen Fast, medium=Oxygen Balanced (default), high=Oxygen Max.")
364
+ .option("--model <id>", "Pin a specific priced model id. Unknown ids fall back to the selected tier.")
365
+ .option("--per-turn-ceiling <number>", "Legacy compatibility only: accepted and clamped, but does not cap an attended turn.")
366
+ .option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
367
+ .option("--title <text>", "Optional human title for the session.")
368
+ .option("--auto-approve", "Start with auto-approve ON for paid, workspace-internal actions (cards are still created and decided automatically within each action's credit cap). External sends, sequence starts, publishes, DNS, and external CRM pushes always stay human-gated.")
369
+ .addOption(new Option("--no-follow", "Retired: the web app now shows changes in the chat. Still accepted so older scripts keep running.").hideHelp())
370
+ .option("--json", "Print a JSON envelope.")
371
+ .action(async (options) => {
372
+ await handleAsyncAction("copilot start", options, () => {
373
+ const budgetCredits = readPositiveNumber(options.budgetCredits);
374
+ if (options.budgetCredits !== undefined && budgetCredits === undefined) {
375
+ throw new OxygenError("invalid_budget", "When supplied for legacy compatibility, --budget-credits must be a positive number.", { exitCode: 1 });
376
+ }
377
+ const perTurnCeiling = readPositiveNumber(options.perTurnCeiling);
378
+ if (options.perTurnCeiling !== undefined && perTurnCeiling === undefined) {
379
+ throw new OxygenError("invalid_budget", "When supplied for legacy compatibility, --per-turn-ceiling must be a positive number.", { exitCode: 1 });
380
+ }
381
+ const tier = readOption(options.tier);
382
+ const model = readOption(options.model);
383
+ const journey = readOption(options.journey);
384
+ const title = readOption(options.title);
385
+ const body = {};
386
+ if (budgetCredits !== undefined)
387
+ body.budget_credits = budgetCredits;
388
+ if (tier)
389
+ body.tier = tier;
390
+ if (model)
391
+ body.model = model;
392
+ if (perTurnCeiling !== undefined)
393
+ body.per_turn_credit_ceiling = perTurnCeiling;
394
+ if (journey)
395
+ body.journey = journey;
396
+ if (title)
397
+ body.title = title;
398
+ if (options.autoApprove)
399
+ body.auto_approve = true;
400
+ if (options.follow === false)
401
+ process.stderr.write(`${FOLLOW_RETIRED_NOTICE}\n`);
402
+ return requestOxygen("/api/cli/copilot/sessions", { method: "POST", body });
403
+ });
404
+ }))
405
+ .addCommand(addContractFlags(new Command("list")
406
+ .description("List Workspace Copilot conversations by session status, newest first; this read starts no inference.")
407
+ .option("--json", "Print a JSON envelope."), COPILOT_SESSIONS_LIST_CONTRACT)
408
+ .action(async (options) => {
409
+ await handleAsyncAction("copilot list", options, () => {
410
+ const selectors = readContractFlags(options, COPILOT_SESSIONS_LIST_CONTRACT);
411
+ const query = new URLSearchParams(Object.entries(selectors).map(([key, value]) => [key, String(value)]));
412
+ return requestOxygen(`/api/cli/copilot/sessions${query.size ? `?${query}` : ""}`);
413
+ });
414
+ }))
415
+ .addCommand(new Command("get")
416
+ .description("Show one Workspace Copilot session: its state, turns, and event log. Use --after-seq to page through the event stream, --before to read earlier turns of a long chat.")
417
+ .argument("<sessionId>", "Copilot session id.")
418
+ .option("--after-seq <n>", "Only return events after this sequence number.")
419
+ .option("--events <n>", "Maximum events to return.")
420
+ .option("--wait-seconds <n>", "Native bounded event long-poll, requires --after-seq (maximum 50 seconds).")
421
+ .option("--before <turn>", "Only return turns before this turn index (window.first_turn_index loads earlier ones).")
422
+ .option("--turns <n>", "Maximum turns to return (a teammate chat returns its newest 40 by default).")
423
+ .option("--json", "Print a JSON envelope.")
424
+ .action(async (sessionId, options) => {
425
+ await handleAsyncAction("copilot get", options, () => {
426
+ const query = new URLSearchParams();
427
+ const afterSeq = readNonNegativeInt(options.afterSeq);
428
+ const eventLimit = readPositiveInt(options.events);
429
+ const turnsBefore = readNonNegativeInt(options.before);
430
+ const turnLimit = readPositiveInt(options.turns);
431
+ if (afterSeq !== undefined)
432
+ query.set("after_seq", String(afterSeq));
433
+ if (options.waitSeconds !== undefined)
434
+ query.set("wait_seconds", String(options.waitSeconds));
435
+ if (eventLimit !== undefined)
436
+ query.set("event_limit", String(eventLimit));
437
+ if (turnsBefore !== undefined)
438
+ query.set("turns_before", String(turnsBefore));
439
+ if (turnLimit !== undefined)
440
+ query.set("turn_limit", String(turnLimit));
441
+ const suffix = query.toString() ? `?${query.toString()}` : "";
442
+ return requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}${suffix}`);
443
+ });
444
+ }))
445
+ .addCommand(new Command("plan")
446
+ .description("Show how far along a Workspace Copilot session is: its plan's major steps, the substeps under each, what is done, and how much longer the rest is expected to take.")
447
+ .argument("<sessionId>", "Copilot session id.")
448
+ .option("--json", "Print a JSON envelope.")
449
+ .action(async (sessionId, options) => {
450
+ const result = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}?event_limit=1`).catch((error) => {
451
+ emitCliFailure("copilot plan", error);
452
+ return null;
453
+ });
454
+ if (!result)
455
+ return;
456
+ const plan = isRecord(result) ? result.plan : null;
457
+ if (options.json) {
458
+ emitSuccess("copilot plan", { plan, web_url: isRecord(result) ? result.web_url : null }, options);
459
+ return;
460
+ }
461
+ writeCopilotPlan(plan, isRecord(result) ? result.web_url : null);
462
+ }))
463
+ .addCommand(new Command("follow")
464
+ .description("Retired compatibility command: the web app now shows what the copilot creates or changes in the chat, and nothing moves your screen, so this changes nothing.")
465
+ .argument("<sessionId>", "Copilot session id.")
466
+ .option("--off", "Retired compatibility flag; changes nothing.")
467
+ .option("--json", "Print a JSON envelope.")
468
+ .action(async (sessionId, options) => {
469
+ await handleAsyncAction("copilot follow", options, async () => {
470
+ // The server accepts the retired field and answers with its notice.
471
+ const result = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}`, {
472
+ method: "PATCH",
473
+ body: { surface_policy: options.off ? "off" : "follow" },
474
+ });
475
+ process.stderr.write(`${FOLLOW_RETIRED_NOTICE}\n`);
476
+ return result;
477
+ });
478
+ }))
479
+ .addCommand(new Command("send")
480
+ .description("Send a message to a Workspace Copilot session and stream the reply to stderr. Waits for the turn to finish (or pause for an approval) unless --no-wait. Managed inference uses workspace credits.")
481
+ .argument("<sessionId>", "Copilot session id.")
482
+ .argument("<message...>", "Message to send (all words joined with spaces).")
483
+ .option("--no-wait", "Post the turn and return immediately without streaming the reply.")
484
+ .option("--context <json>", "Optional JSON object of surface context to attach to the turn.")
485
+ .option("--attach <path>", "Attach a file to this message (repeatable). Spreadsheets become tables the copilot can import and enrich; text documents ground the conversation. Uploaded straight to object storage, so large files are fine.", collectRepeatable, [])
486
+ .option("--timeout-seconds <n>", "Maximum time to wait for the turn to finish. Defaults to 300.")
487
+ .option("--interval-seconds <n>", "Polling interval while streaming. Defaults to 2.")
488
+ .option("--json", "Print a JSON envelope.")
489
+ .action(async (sessionId, messageParts, options) => {
490
+ await handleAsyncAction("copilot send", options, async () => {
491
+ const message = messageParts.join(" ");
492
+ const context = readJsonObjectOption(options.context);
493
+ const attachmentIds = [];
494
+ for (const path of options.attach ?? []) {
495
+ const attached = await uploadCopilotAttachment(sessionId, path);
496
+ attachmentIds.push(attached.id);
497
+ }
498
+ const posted = await requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/turns`, {
499
+ method: "POST",
500
+ body: {
501
+ message,
502
+ ...(context ? { context } : {}),
503
+ ...(attachmentIds.length > 0 ? { attachment_ids: attachmentIds } : {}),
504
+ },
505
+ });
506
+ // Commander maps --no-wait to `wait: false` (default true).
507
+ if (options.wait === false)
508
+ return posted;
509
+ const turn = isRecord(posted.turn) ? posted.turn : {};
510
+ const turnId = typeof turn.id === "string" ? turn.id : "";
511
+ const latestSeq = posted.latest_seq;
512
+ const initialSeq = typeof latestSeq === "number" && Number.isFinite(latestSeq) ? latestSeq : 0;
513
+ const finalData = await waitForCopilotTurn({
514
+ sessionId,
515
+ turnId,
516
+ initialSeq,
517
+ requestedTimeoutSeconds: options.timeoutSeconds,
518
+ requestedIntervalSeconds: options.intervalSeconds,
519
+ });
520
+ writeCopilotTurnCreditsSummary(finalData, turnId);
521
+ return finalData;
522
+ });
523
+ }))
524
+ .addCommand(new Command("attach")
525
+ .description("Attach a file to a Workspace Copilot session without sending a message yet. The file uploads straight to object storage and is described back to you (a spreadsheet's columns and row count); pass the returned id to `copilot send --attach` or just send next — unsent attachments ride the next message.")
526
+ .argument("<sessionId>", "Copilot session id.")
527
+ .argument("<path>", "Path to the file to attach.")
528
+ .option("--json", "Print a JSON envelope.")
529
+ .action(async (sessionId, path, options) => {
530
+ await handleAsyncAction("copilot attach", options, () => uploadCopilotAttachment(sessionId, path).then((result) => ({
531
+ attachment: result.attachment,
532
+ })));
533
+ }))
534
+ .addCommand(new Command("attachments")
535
+ .description("List the files attached to a Workspace Copilot session, sent and unsent.")
536
+ .argument("<sessionId>", "Copilot session id.")
537
+ .option("--json", "Print a JSON envelope.")
538
+ .action(async (sessionId, options) => {
539
+ await handleAsyncAction("copilot attachments", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/attachments`));
540
+ }))
541
+ .addCommand(new Command("approve")
542
+ .description("Approve (or, with --reject, reject) an action a Workspace Copilot session is waiting on; approving re-queues the paused turn.")
543
+ .argument("<sessionId>", "Copilot session id.")
544
+ .argument("<approvalId>", "Approval id from the approval_requested event.")
545
+ .option("--reject", "Reject the action instead of approving it.")
546
+ .option("--json", "Print a JSON envelope.")
547
+ .action(async (sessionId, approvalId, options) => {
548
+ await handleAsyncAction("copilot approve", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.reject ? "reject" : "approve" } }));
549
+ }))
550
+ .addCommand(new Command("interrupt")
551
+ .description("Stop the turn a Workspace Copilot session is currently working on, leaving the session open for your next message.")
552
+ .argument("<sessionId>", "Copilot session id.")
553
+ .argument("<turnId>", "Turn id to stop (see `oxygen copilot get <sessionId>`).")
554
+ .option("--json", "Print a JSON envelope.")
555
+ .action(async (sessionId, turnId, options) => {
556
+ await handleAsyncAction("copilot interrupt", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/turns/${encodeURIComponent(turnId)}/interrupt`, { method: "POST" }));
557
+ }))
558
+ .addCommand(new Command("cancel")
559
+ .description("End a Workspace Copilot session for good, stopping any in-flight turn. The session cannot take new messages afterwards — to stop only the current turn, use `copilot interrupt`.")
560
+ .argument("<sessionId>", "Copilot session id.")
561
+ .option("--json", "Print a JSON envelope.")
562
+ .action(async (sessionId, options) => {
563
+ await handleAsyncAction("copilot cancel", options, () => requestOxygen(`/api/cli/copilot/sessions/${encodeURIComponent(sessionId)}/cancel`, { method: "POST" }));
564
+ }));
565
+ }
@@ -0,0 +1,3 @@
1
+ import { ExtractedCommandActionHandler } from "../command-support.js";
2
+ import { Command } from "commander";
3
+ export declare function registerCrmFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;