quoxcli 0.0.0-stage → 0.2.0

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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Monitoring commands: Docker fleet, Wazuh security monitoring, AGH-chain
3
+ * agent honesty leaderboard, Prometheus metrics, and monitoring config.
4
+ *
5
+ * quox mon docker fleet|containers|container|logs|stats|stacks|df|action
6
+ * quox mon wazuh status|agents|alerts|vulns|deploy-history|deploy-agent|deploy-bulk|active-response
7
+ * quox mon agh-chain leaderboard|agent|events
8
+ * quox mon metrics
9
+ * quox mon config get|cache|grafana
10
+ *
11
+ * Backend: services/collector/server.js. See endpoints/mon.js's header for
12
+ * the full auth/org-scoping verification per route family (WP4.3,
13
+ * STREAM_CLI_PARITY_PLAN.md §2.3 #10) — summary:
14
+ * - `/docker/*` — NO auth middleware, no org filter (fleet-wide).
15
+ * - `/api/v1/wazuh/*` — requireServiceOrUserAuth(); not org-filtered by
16
+ * the Wazuh API itself; deploy-agent/deploy-bulk/active-response are
17
+ * HITL-gated (queue an HITL Inbox approval, return 202, do not execute
18
+ * immediately).
19
+ * - `/api/v1/agh-chain/*` — requireServiceOrUserAuth() AND genuinely
20
+ * org-scoped (403s without req.user.org_id).
21
+ * - `/metrics` — NO auth middleware, plain text (Prometheus exposition
22
+ * format), not JSON.
23
+ * - `/api/v1/monitoring/config` — optional auth; `/api/v1/monitoring/
24
+ * cache/:service` and `/api/v1/grafana/config` — requireServiceOrUserAuth().
25
+ * None of the three are org-scoped (single shared config/cache).
26
+ */
27
+
28
+ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
29
+ import { parseJSON } from './helpers.js';
30
+ import { resolveEndpoint } from '../api/endpoints.js';
31
+
32
+ async function confirmOrSkip(program, options, message) {
33
+ if (options.force || program.opts().yes) return true;
34
+ const { default: inquirer } = await import('inquirer');
35
+ const ans = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message, default: false }]);
36
+ return ans.confirm;
37
+ }
38
+
39
+ export function registerMonCommands(program, ctx) {
40
+ const { client, auth } = ctx;
41
+
42
+ const mon = program
43
+ .command('mon')
44
+ .description('Monitoring: Docker fleet, Wazuh security, AGH-chain leaderboard, metrics');
45
+
46
+ // ══════════════════════════════════════════════════════════════════════
47
+ // docker
48
+ // ══════════════════════════════════════════════════════════════════════
49
+
50
+ const docker = mon
51
+ .command('docker')
52
+ .description('Docker fleet: hosts, containers, stacks, disk usage');
53
+
54
+ docker
55
+ .command('fleet')
56
+ .description('Discover Docker hosts across the fleet')
57
+ .option('--refresh', 'force a live scan instead of returning the cache')
58
+ .action(async (options) => {
59
+ auth.requireAuth();
60
+ const ep = resolveEndpoint('mon.docker.fleet.list');
61
+ const data = await client.get(ep.path, { service: ep.service, query: options.refresh ? { refresh: 'true' } : undefined });
62
+ const hosts = data.hosts || [];
63
+ formatOutput({
64
+ // fleet #392: docker fleet rows are id/containers_total/docker_version; ip and status don't exist.
65
+ columns: ['id', 'containers_total', 'containers_running', 'docker_version'],
66
+ headers: ['Host', 'Containers', 'Running', 'Docker'],
67
+ rows: hosts,
68
+ }, { format: program.opts().output, emptyMessage: 'No Docker hosts found.' });
69
+ });
70
+
71
+ docker
72
+ .command('containers')
73
+ .description('List containers across the fleet (or a single host)')
74
+ .option('--host <hostId>', 'filter to a single host')
75
+ .option('--no-all', 'only running containers (default includes stopped)')
76
+ .option('--refresh', 'force a live fetch instead of returning the cache')
77
+ .action(async (options) => {
78
+ auth.requireAuth();
79
+ const ep = resolveEndpoint('mon.docker.containers.list');
80
+ const query = {};
81
+ if (options.host) query.host = options.host;
82
+ if (options.all === false) query.all = 'false';
83
+ if (options.refresh) query.refresh = 'true';
84
+ const data = await client.get(ep.path, { service: ep.service, query });
85
+ formatOutput({
86
+ columns: ['host_id', 'id', 'name', 'image', 'state', 'status'],
87
+ headers: ['Host', 'ID', 'Name', 'Image', 'State', 'Status'],
88
+ rows: data.containers || [],
89
+ }, { format: program.opts().output, emptyMessage: 'No containers found.' });
90
+ });
91
+
92
+ docker
93
+ .command('container')
94
+ .description('Show container detail (stats + recent logs)')
95
+ .argument('<id>', 'container ID')
96
+ .requiredOption('--host <hostId>', 'host the container lives on')
97
+ .action(async (id, options) => {
98
+ auth.requireAuth();
99
+ const ep = resolveEndpoint('mon.docker.containers.get', { id });
100
+ const data = await client.get(ep.path, { service: ep.service, query: { host: options.host } });
101
+ formatDetail(data, {
102
+ format: program.opts().output,
103
+ title: `Container: ${id} (${options.host})`,
104
+ fields: ['host', 'container', 'stats', 'logs'],
105
+ labels: { host: 'Host', container: 'Container', stats: 'Stats', logs: 'Logs' },
106
+ });
107
+ });
108
+
109
+ docker
110
+ .command('logs')
111
+ .description('Show container logs')
112
+ .argument('<id>', 'container ID')
113
+ .requiredOption('--host <hostId>', 'host the container lives on')
114
+ .option('--tail <n>', 'number of lines', '100')
115
+ .action(async (id, options) => {
116
+ auth.requireAuth();
117
+ const ep = resolveEndpoint('mon.docker.containers.logs', { id });
118
+ const data = await client.get(ep.path, { service: ep.service, query: { host: options.host, tail: options.tail } });
119
+ if (program.opts().output === 'json') {
120
+ console.log(JSON.stringify(data, null, 2));
121
+ } else {
122
+ console.log(data.logs || '');
123
+ }
124
+ });
125
+
126
+ docker
127
+ .command('stats')
128
+ .description('Show container resource stats')
129
+ .argument('<id>', 'container ID')
130
+ .requiredOption('--host <hostId>', 'host the container lives on')
131
+ .action(async (id, options) => {
132
+ auth.requireAuth();
133
+ const ep = resolveEndpoint('mon.docker.containers.stats', { id });
134
+ const data = await client.get(ep.path, { service: ep.service, query: { host: options.host } });
135
+ formatDetail(data.stats || data, { format: program.opts().output, title: `Stats: ${id} (${options.host})` });
136
+ });
137
+
138
+ docker
139
+ .command('stacks')
140
+ .description('List Docker Compose stacks')
141
+ .option('--host <hostId>', 'filter to a single host')
142
+ .action(async (options) => {
143
+ auth.requireAuth();
144
+ const ep = resolveEndpoint('mon.docker.stacks.list');
145
+ const data = await client.get(ep.path, { service: ep.service, query: options.host ? { host: options.host } : undefined });
146
+ formatOutput({
147
+ columns: ['host_id', 'name', 'status', 'services'],
148
+ headers: ['Host', 'Stack', 'Status', 'Services'],
149
+ rows: data.stacks || [],
150
+ }, { format: program.opts().output, emptyMessage: 'No stacks found.' });
151
+ });
152
+
153
+ docker
154
+ .command('df')
155
+ .description('Show Docker disk usage on a host')
156
+ .requiredOption('--host <hostId>', 'host to query')
157
+ .action(async (options) => {
158
+ auth.requireAuth();
159
+ const ep = resolveEndpoint('mon.docker.system-df.get');
160
+ const data = await client.get(ep.path, { service: ep.service, query: { host: options.host } });
161
+ formatDetail(data, { format: program.opts().output, title: `Docker disk usage: ${options.host}` });
162
+ });
163
+
164
+ docker
165
+ .command('action')
166
+ .description('Start/stop/restart a container (destructive)')
167
+ .argument('<id>', 'container ID')
168
+ .argument('<action>', 'start|stop|restart')
169
+ .requiredOption('--host <hostId>', 'host the container lives on')
170
+ .option('--force', 'skip confirmation')
171
+ .action(async (id, action, options) => {
172
+ auth.requireAuth();
173
+ if (!['start', 'stop', 'restart'].includes(action)) {
174
+ throw new Error(`Invalid action: ${action}. Must be one of: start, stop, restart.`);
175
+ }
176
+ const confirmed = await confirmOrSkip(program, options, `${action} container ${id} on ${options.host}?`);
177
+ if (!confirmed) {
178
+ printInfo('Cancelled.');
179
+ return;
180
+ }
181
+ const ep = resolveEndpoint('mon.docker.containers.action', { id, action });
182
+ const data = await client.post(ep.path, { service: ep.service, body: { host: options.host } });
183
+ if (data.success === false) {
184
+ printWarning(`Action failed: ${data.error || 'unknown error'}`);
185
+ return;
186
+ }
187
+ printSuccess(`Container ${id}: ${action} succeeded.`);
188
+ });
189
+
190
+ // ══════════════════════════════════════════════════════════════════════
191
+ // wazuh
192
+ // ══════════════════════════════════════════════════════════════════════
193
+
194
+ const wazuh = mon
195
+ .command('wazuh')
196
+ .description('Wazuh security monitoring: manager status, agents, alerts, vulnerabilities');
197
+
198
+ wazuh
199
+ .command('status')
200
+ .description('Show Wazuh manager status')
201
+ .action(async () => {
202
+ auth.requireAuth();
203
+ const ep = resolveEndpoint('mon.wazuh.manager-status.get');
204
+ const data = await client.get(ep.path, { service: ep.service });
205
+ formatDetail(data.data || data, { format: program.opts().output, title: 'Wazuh Manager Status' });
206
+ });
207
+
208
+ wazuh
209
+ .command('agents')
210
+ .description('List Wazuh agents')
211
+ .option('--status <status>', 'filter by agent status')
212
+ .option('--limit <n>', 'max results')
213
+ .option('--offset <n>', 'result offset')
214
+ .option('--q <query>', 'query string passed through to the Wazuh API')
215
+ .action(async (options) => {
216
+ auth.requireAuth();
217
+ const ep = resolveEndpoint('mon.wazuh.agents.list');
218
+ const query = {};
219
+ if (options.status) query.status = options.status;
220
+ if (options.limit) query.limit = options.limit;
221
+ if (options.offset) query.offset = options.offset;
222
+ if (options.q) query.q = options.q;
223
+ const data = await client.get(ep.path, { service: ep.service, query });
224
+ const agents = data.data?.data?.affected_items || data.data?.affected_items || data.data || [];
225
+ formatOutput({
226
+ columns: ['id', 'name', 'ip', 'status', 'version'],
227
+ headers: ['ID', 'Name', 'IP', 'Status', 'Version'],
228
+ rows: Array.isArray(agents) ? agents : [],
229
+ }, { format: program.opts().output, emptyMessage: 'No Wazuh agents found.' });
230
+ });
231
+
232
+ wazuh
233
+ .command('alerts')
234
+ .description('Query recent Wazuh alerts')
235
+ .option('--q <query>', 'search text (rule description / agent name / rule groups)')
236
+ .option('--limit <n>', 'max results (default 50, capped at 500)')
237
+ .action(async (options) => {
238
+ auth.requireAuth();
239
+ const ep = resolveEndpoint('mon.wazuh.alerts.list');
240
+ const query = {};
241
+ if (options.q) query.q = options.q;
242
+ if (options.limit) query.limit = options.limit;
243
+ const data = await client.get(ep.path, { service: ep.service, query });
244
+ formatOutput({
245
+ columns: ['timestamp', 'rule', 'agent', 'level'],
246
+ headers: ['Time', 'Rule', 'Agent', 'Level'],
247
+ rows: (data.data || []).map(a => ({
248
+ timestamp: a.timestamp,
249
+ rule: a.rule?.description || '',
250
+ agent: a.agent?.name || '',
251
+ level: a.rule?.level ?? '',
252
+ })),
253
+ }, { format: program.opts().output, emptyMessage: 'No alerts found.' });
254
+ });
255
+
256
+ wazuh
257
+ .command('vulns')
258
+ .description('Query current vulnerability states')
259
+ .option('--agent-id <id>', 'filter by agent ID')
260
+ .option('--agent-name <name>', 'filter by agent name')
261
+ .option('--severity <severity>', 'filter by severity')
262
+ .option('--limit <n>', 'max results (default 100, capped at 1000)')
263
+ .action(async (options) => {
264
+ auth.requireAuth();
265
+ const ep = resolveEndpoint('mon.wazuh.vulnerabilities.list');
266
+ const query = {};
267
+ if (options.agentId) query.agent_id = options.agentId;
268
+ if (options.agentName) query.agent_name = options.agentName;
269
+ if (options.severity) query.severity = options.severity;
270
+ if (options.limit) query.limit = options.limit;
271
+ const data = await client.get(ep.path, { service: ep.service, query });
272
+ formatOutput({
273
+ columns: ['agent', 'cve', 'severity', 'score'],
274
+ headers: ['Agent', 'CVE', 'Severity', 'Score'],
275
+ rows: (data.data || []).map(v => ({
276
+ agent: v.agent?.name || '',
277
+ cve: v.vulnerability?.id || '',
278
+ severity: v.vulnerability?.severity || '',
279
+ score: v.vulnerability?.score?.base ?? '',
280
+ })),
281
+ }, { format: program.opts().output, emptyMessage: 'No vulnerabilities found.' });
282
+ });
283
+
284
+ wazuh
285
+ .command('deploy-history')
286
+ .description('Show Wazuh agent deploy history')
287
+ .option('--mode <mode>', 'latest|all', 'latest')
288
+ .option('--limit <n>', 'max results (mode=all only)', '200')
289
+ .action(async (options) => {
290
+ auth.requireAuth();
291
+ const ep = resolveEndpoint('mon.wazuh.deploy-history.list');
292
+ const data = await client.get(ep.path, { service: ep.service, query: { mode: options.mode, limit: options.limit } });
293
+ formatOutput({
294
+ // fleet #385 class sweep: the route returns
295
+ // { hostname, ip, ssh_user, success, message, deployed_at }, but this
296
+ // declared hostIp/status/deployedAt — three of four columns rendered
297
+ // as dashes with no error. `success` is a boolean, so it's mapped to
298
+ // a readable status rather than printed raw.
299
+ columns: ['hostname', 'ip', 'status', 'deployed_at'],
300
+ headers: ['Host', 'IP', 'Status', 'Deployed'],
301
+ rows: (data.deploys || []).map(d => ({
302
+ ...d,
303
+ status: d.success ? 'ok' : (d.message ? `failed: ${String(d.message).slice(0, 40)}` : 'failed'),
304
+ })),
305
+ }, { format: program.opts().output, emptyMessage: 'No deploy history found.' });
306
+ });
307
+
308
+ wazuh
309
+ .command('deploy-agent')
310
+ .description('Queue a Wazuh agent deploy for one host (HITL-gated — requires HITL Inbox approval)')
311
+ .requiredOption('--hostname <hostname>', 'target hostname')
312
+ .requiredOption('--host-ip <ip>', 'target host IP')
313
+ .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
314
+ .option('--bastion-host <ip>', 'bastion host for the SSH hop')
315
+ .option('--ssh-user <user>', 'SSH user', 'control')
316
+ .option('--ssh-port <n>', 'SSH port', '22')
317
+ .option('--force', 'skip confirmation')
318
+ .action(async (options) => {
319
+ auth.requireAuth();
320
+ const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${options.hostname} (${options.hostIp})? This will require HITL Inbox approval before it runs.`);
321
+ if (!confirmed) {
322
+ printInfo('Cancelled.');
323
+ return;
324
+ }
325
+ const body = { hostname: options.hostname, hostIp: options.hostIp, sshUser: options.sshUser, sshPort: options.sshPort };
326
+ if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
327
+ if (options.bastionHost) body.bastionHost = options.bastionHost;
328
+ const ep = resolveEndpoint('mon.wazuh.deploy-agent');
329
+ const data = await client.post(ep.path, { service: ep.service, body });
330
+ printSuccess(`Deploy queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
331
+ });
332
+
333
+ wazuh
334
+ .command('deploy-bulk')
335
+ .description('Queue a Wazuh agent deploy for multiple hosts (HITL-gated — requires HITL Inbox approval)')
336
+ .requiredOption('--hosts <json>', 'JSON array of {hostname, ip} objects')
337
+ .option('--wazuh-server <ip>', 'Wazuh manager IP (defaults to server config)')
338
+ .option('--bastion-host <ip>', 'bastion host for the SSH hop')
339
+ .option('--force', 'skip confirmation')
340
+ .action(async (options) => {
341
+ auth.requireAuth();
342
+ const hosts = parseJSON(options.hosts, '--hosts');
343
+ if (!Array.isArray(hosts) || hosts.length === 0) {
344
+ throw new Error('--hosts must be a non-empty JSON array of {hostname, ip} objects.');
345
+ }
346
+ const confirmed = await confirmOrSkip(program, options, `Queue Wazuh agent deploy to ${hosts.length} host(s)? This will require HITL Inbox approval before it runs.`);
347
+ if (!confirmed) {
348
+ printInfo('Cancelled.');
349
+ return;
350
+ }
351
+ const body = { hosts };
352
+ if (options.wazuhServer) body.wazuhServer = options.wazuhServer;
353
+ if (options.bastionHost) body.bastionHost = options.bastionHost;
354
+ const ep = resolveEndpoint('mon.wazuh.deploy-bulk');
355
+ const data = await client.post(ep.path, { service: ep.service, body });
356
+ printSuccess(`Bulk deploy queued for ${data.host_count ?? hosts.length} host(s) — inbox item ${data.inbox_item_id} (awaiting approval).`);
357
+ });
358
+
359
+ wazuh
360
+ .command('active-response')
361
+ .description('Trigger a Wazuh active response (HITL-gated — requires HITL Inbox approval)')
362
+ .requiredOption('--command <command>', 'firewall-block|host-isolate|restart-wazuh-agent')
363
+ .requiredOption('--agents <ids>', 'comma-separated Wazuh agent IDs')
364
+ .requiredOption('--reason <text>', 'justification (min 10 characters)')
365
+ .option('--args <args>', 'comma-separated command arguments')
366
+ .option('--force', 'skip confirmation')
367
+ .action(async (options) => {
368
+ auth.requireAuth();
369
+ const agentList = options.agents.split(',').map(a => a.trim()).filter(Boolean);
370
+ const args = options.args ? options.args.split(',').map(a => a.trim()).filter(Boolean) : [];
371
+ const confirmed = await confirmOrSkip(program, options, `Trigger active response "${options.command}" on ${agentList.length} agent(s)? This will require HITL Inbox approval before it executes.`);
372
+ if (!confirmed) {
373
+ printInfo('Cancelled.');
374
+ return;
375
+ }
376
+ const ep = resolveEndpoint('mon.wazuh.active-response');
377
+ const data = await client.post(ep.path, { service: ep.service, body: { command: options.command, agentList, args, reason: options.reason } });
378
+ printSuccess(`Active response queued — inbox item ${data.inbox_item_id} (awaiting approval).`);
379
+ });
380
+
381
+ // ══════════════════════════════════════════════════════════════════════
382
+ // agh-chain
383
+ // ══════════════════════════════════════════════════════════════════════
384
+
385
+ const aghChain = mon
386
+ .command('agh-chain')
387
+ .description('AGH-chain: per-agent honesty leaderboard (org-scoped)');
388
+
389
+ aghChain
390
+ .command('leaderboard')
391
+ .description('Show the org honesty leaderboard')
392
+ .option('--window <window>', '24h|7d|30d', '24h')
393
+ .action(async (options) => {
394
+ auth.requireAuth();
395
+ const ep = resolveEndpoint('mon.agh-chain.leaderboard.list');
396
+ const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
397
+ formatOutput({
398
+ columns: ['agent_id', 'claims_made', 'claims_verified', 'claims_stripped', 'unfulfilled_promises', 'honesty_rate'],
399
+ headers: ['Agent', 'Claims Made', 'Verified', 'Stripped', 'Unfulfilled', 'Honesty Rate'],
400
+ rows: data.agents || [],
401
+ }, { format: program.opts().output, emptyMessage: 'No leaderboard data found.' });
402
+ });
403
+
404
+ aghChain
405
+ .command('agent')
406
+ .description('Show honesty metrics for a single agent')
407
+ .argument('<agentId>', 'agent ID')
408
+ .option('--window <window>', '24h|7d|30d', '24h')
409
+ .action(async (agentId, options) => {
410
+ auth.requireAuth();
411
+ const ep = resolveEndpoint('mon.agh-chain.leaderboard.get', { agentId });
412
+ const data = await client.get(ep.path, { service: ep.service, query: { window: options.window } });
413
+ formatDetail(data, { format: program.opts().output, title: `Honesty metrics: ${agentId}` });
414
+ });
415
+
416
+ aghChain
417
+ .command('events')
418
+ .description('Show drill-through events for a guard kind')
419
+ .argument('<agentId>', 'agent ID')
420
+ .option('--window <window>', '24h|7d|30d', '24h')
421
+ .option('--guard <kind>', 'guard kind', 'claims_stripped')
422
+ .action(async (agentId, options) => {
423
+ auth.requireAuth();
424
+ const ep = resolveEndpoint('mon.agh-chain.leaderboard.events.list', { agentId });
425
+ const data = await client.get(ep.path, { service: ep.service, query: { window: options.window, guard: options.guard } });
426
+ formatOutput({
427
+ columns: ['event_id', 'run_id', 'ts', 'payload_preview'],
428
+ headers: ['Event', 'Run', 'Time', 'Preview'],
429
+ rows: data.events || [],
430
+ }, { format: program.opts().output, emptyMessage: 'No drill-through events found.' });
431
+ });
432
+
433
+ // ══════════════════════════════════════════════════════════════════════
434
+ // metrics
435
+ // ══════════════════════════════════════════════════════════════════════
436
+
437
+ mon
438
+ .command('metrics')
439
+ .description('Show Prometheus scrape metrics (collector fleet gauges)')
440
+ .option('--raw', 'print the raw Prometheus exposition text')
441
+ .action(async (options) => {
442
+ auth.requireAuth();
443
+ const ep = resolveEndpoint('mon.metrics.get');
444
+ const data = await client.get(ep.path, { service: ep.service });
445
+ const text = data.text || '';
446
+
447
+ if (options.raw || program.opts().output === 'json') {
448
+ if (program.opts().output === 'json') {
449
+ console.log(JSON.stringify({ text }, null, 2));
450
+ } else {
451
+ console.log(text);
452
+ }
453
+ return;
454
+ }
455
+
456
+ const rows = text
457
+ .split('\n')
458
+ .filter(line => line && !line.startsWith('#'))
459
+ .map(line => {
460
+ const idx = line.lastIndexOf(' ');
461
+ if (idx === -1) return null;
462
+ return { metric: line.slice(0, idx), value: line.slice(idx + 1) };
463
+ })
464
+ .filter(Boolean);
465
+
466
+ formatOutput({
467
+ columns: ['metric', 'value'],
468
+ headers: ['Metric', 'Value'],
469
+ rows,
470
+ }, { format: program.opts().output, emptyMessage: 'No metrics returned.' });
471
+ });
472
+
473
+ // ══════════════════════════════════════════════════════════════════════
474
+ // config
475
+ // ══════════════════════════════════════════════════════════════════════
476
+
477
+ const config = mon
478
+ .command('config')
479
+ .description('Monitoring service config (Prometheus/Uptime Kuma/Proxmox/Grafana)');
480
+
481
+ config
482
+ .command('get')
483
+ .description('Show configured monitoring service URLs')
484
+ .action(async () => {
485
+ const ep = resolveEndpoint('mon.config.get');
486
+ const data = await client.get(ep.path, { service: ep.service });
487
+ formatDetail(data.services || data, { format: program.opts().output, title: 'Monitoring Config' });
488
+ });
489
+
490
+ config
491
+ .command('cache')
492
+ .description('Show the last cached scan for a monitoring service')
493
+ .argument('<service>', 'service name, e.g. prometheus|uptime-kuma|proxmox')
494
+ .action(async (service) => {
495
+ auth.requireAuth();
496
+ const ep = resolveEndpoint('mon.config.cache.get', { service });
497
+ const data = await client.get(ep.path, { service: ep.service });
498
+ if (data.cached === false) {
499
+ printInfo(`No cached data for ${service}.`);
500
+ return;
501
+ }
502
+ formatDetail(data, { format: program.opts().output, title: `Monitoring cache: ${service}` });
503
+ });
504
+
505
+ config
506
+ .command('grafana')
507
+ .description('Show the configured Grafana base URL')
508
+ .action(async () => {
509
+ auth.requireAuth();
510
+ const ep = resolveEndpoint('mon.config.grafana.get');
511
+ const data = await client.get(ep.path, { service: ep.service });
512
+ formatDetail(data, { format: program.opts().output, title: 'Grafana Config' });
513
+ });
514
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Monitoring commands: platform health and governance incidents.
3
+ *
4
+ * `quox status` (lib/commands/status.js) and `quox service health`
5
+ * (lib/commands/service.js) already cover aggregate platform health and
6
+ * per-service reachability — this group focuses on the gap: governance
7
+ * incidents, which had no CLI surface at all.
8
+ *
9
+ * Endpoints wired (services/collector/server.js):
10
+ * GET /api/v1/health/aggregate — same endpoint `quox status` uses
11
+ * GET /governance/incidents — list
12
+ * GET /governance/incidents/:id — detail + linked AEE envelopes
13
+ */
14
+
15
+ import { formatOutput, formatDetail } from '../core/output.js';
16
+ import { resolveEndpoint } from '../api/endpoints.js';
17
+
18
+ export function registerMonitoringCommands(program, ctx) {
19
+ const { client, auth } = ctx;
20
+
21
+ const monitoring = program
22
+ .command('monitoring')
23
+ .description('Platform health and governance incidents');
24
+
25
+ // GET /api/v1/health/aggregate — see `quox status` for the human-readable
26
+ // pulse view; this subcommand exists for scripting/discoverability under
27
+ // the monitoring namespace.
28
+ monitoring
29
+ .command('health')
30
+ .description('Show aggregate platform health (services, fleet, sessions)')
31
+ .action(async () => {
32
+ // fleet #602: dropped noAuth: true, which suppressed the Authorization
33
+ // header and 401'd on an authenticated collector (the same bug fixed in
34
+ // `quox status`). Logged-out still works (client sends no header when
35
+ // nothing is stored); logged-in now actually sends the token.
36
+ const data = await client.get(resolveEndpoint('v1.health.aggregate.list').path, {
37
+ service: resolveEndpoint('v1.health.aggregate.list').service,
38
+ timeout: 5000,
39
+ });
40
+ formatDetail(data, { format: program.opts().output, title: 'Platform Health' });
41
+ });
42
+
43
+ const incidents = monitoring
44
+ .command('incidents')
45
+ .description('Governance incidents');
46
+
47
+ incidents
48
+ .command('list')
49
+ .description('List governance incidents')
50
+ .option('--severity <severity>', 'filter by severity')
51
+ .option('--status <status>', 'filter by status')
52
+ .option('--type <type>', 'filter by incident type')
53
+ .option('--agent <id>', 'filter by agent ID')
54
+ .option('--corr <id>', 'filter by correlation ID')
55
+ .option('--since <date>', 'start date (ISO)')
56
+ .option('--until <date>', 'end date (ISO)')
57
+ .option('--limit <n>', 'max results', '100')
58
+ .action(async (options) => {
59
+ auth.requireAuth();
60
+ const query = { limit: options.limit };
61
+ if (options.severity) query.severity = options.severity;
62
+ if (options.status) query.status = options.status;
63
+ if (options.type) query.incident_type = options.type;
64
+ if (options.agent) query.agent_id = options.agent;
65
+ if (options.corr) query.corr = options.corr;
66
+ if (options.since) query.since = options.since;
67
+ if (options.until) query.until = options.until;
68
+
69
+ const data = await client.get(resolveEndpoint('governance.incidents.list').path, { service: resolveEndpoint('governance.incidents.list').service, query });
70
+ formatOutput({
71
+ columns: ['id', 'severity', 'status', 'incident_type', 'agent_id', 'created_at'],
72
+ headers: ['ID', 'Severity', 'Status', 'Type', 'Agent', 'Created'],
73
+ rows: data.data || data.incidents || data,
74
+ }, { format: program.opts().output, emptyMessage: 'No incidents found.' });
75
+ });
76
+
77
+ incidents
78
+ .command('get')
79
+ .description('Show incident details and linked AEE envelopes')
80
+ .argument('<id>', 'incident ID')
81
+ .action(async (id) => {
82
+ auth.requireAuth();
83
+ const data = await client.get(resolveEndpoint('governance.incidents.get', { id }).path, { service: resolveEndpoint('governance.incidents.get', { id }).service });
84
+ formatDetail(data.incident || data, { format: program.opts().output, title: `Incident: ${id}` });
85
+ });
86
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * quox motion: play the Quox motion gallery (every Q animation, captioned). No server needed.
3
+ * CLI-QUOX lab parity: the product version of ~/quoxon-work/intro/lab.
4
+ */
5
+ export function registerMotionCommand(program) {
6
+ program
7
+ .command('motion')
8
+ .description('Play the Quox motion gallery: the Q intro, portal, particle, the Dragon\'s Eye, the status Q, ring and reveals (no server needed)')
9
+ .argument('[scenes...]', 'only these: orbit portal particle eye hello perpetual ring fx')
10
+ .option('--narrow', 'use the 40-column layout (the compact Q)')
11
+ .option('--slow', 'play three times slower')
12
+ .option('--mono', 'no colour (also honoured: NO_COLOR)')
13
+ .action(async (scenes, opts) => {
14
+ const { runGallery } = await import('../quoxon/gallery.js');
15
+ try {
16
+ await runGallery({ only: scenes || [], narrow: Boolean(opts.narrow), slow: Boolean(opts.slow), mono: Boolean(opts.mono) });
17
+ } catch (e) {
18
+ console.error(e.message);
19
+ process.exitCode = 2;
20
+ }
21
+ });
22
+ }