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,234 @@
1
+ /**
2
+ * FleetView — Fleet agent status overview
3
+ *
4
+ * Left panel: DataTable of fleet agents (host, status, cpu, mem, version)
5
+ * Right panel: DetailPanel for the selected agent
6
+ *
7
+ * Features:
8
+ * - Auto-refreshes every 10 seconds via usePolledApi (only while active —
9
+ * never polls in the background when another screen is showing)
10
+ * - `r` key triggers manual refresh
11
+ * - Status dots (green/red/yellow) for agent health
12
+ *
13
+ * API:
14
+ * GET /api/v1/fleet/status (service: collector) -> data.agents or data.hosts
15
+ * GET /api/v1/agents (service: collector) -> data.agents
16
+ */
17
+
18
+ import React, { useState, useEffect, useCallback } from 'react';
19
+ import { Box, Text, useInput } from 'ink';
20
+ import { Layout } from '../components/Layout.jsx';
21
+ import { DataTable } from '../components/DataTable.jsx';
22
+ import { DetailPanel } from '../components/DetailPanel.jsx';
23
+ import { SearchInput } from '../components/SearchInput.jsx';
24
+ import { usePolledApi } from '../hooks/usePolledApi.js';
25
+ import { useTui } from '../app.jsx';
26
+ import { matchKey } from '../keys.js';
27
+ import { colors, styles } from '../theme.js';
28
+ import { resolveEndpoint } from '../../api/endpoints.js';
29
+
30
+ // ── Column definitions ────────────────────────────────────────────────
31
+ const COLUMNS = [
32
+ { key: 'host_id', label: 'Host', width: 16 },
33
+ { key: 'status', label: 'Status', width: 12 },
34
+ { key: 'cpu', label: 'CPU%', width: 8 },
35
+ { key: 'mem', label: 'MEM%', width: 8 },
36
+ { key: 'version', label: 'Version', width: 12 },
37
+ ];
38
+
39
+ // ── Auto-refresh interval (ms) ───────────────────────────────────────
40
+ const REFRESH_INTERVAL = 10_000;
41
+
42
+ /**
43
+ * Format raw fleet data into table rows with status dots.
44
+ */
45
+ function formatRows(agents) {
46
+ if (!Array.isArray(agents)) return [];
47
+ return agents.map((a) => ({
48
+ ...a,
49
+ host_id: a.host_id || a.hostname || a.name || 'unknown',
50
+ status: `${styles.statusDot(a.status || 'unknown')} ${a.status || 'unknown'}`,
51
+ cpu: a.cpu != null ? `${Number(a.cpu).toFixed(1)}%` : '--',
52
+ mem: a.mem != null ? `${Number(a.mem).toFixed(1)}%` : '--',
53
+ version: a.version || '--',
54
+ // Preserve raw for detail panel
55
+ _raw: a,
56
+ }));
57
+ }
58
+
59
+ /**
60
+ * Build a detail object for the right panel from a raw agent record.
61
+ */
62
+ function buildDetail(raw) {
63
+ if (!raw) return null;
64
+ return {
65
+ 'Host ID': raw.host_id || raw.hostname || raw.name,
66
+ 'Status': raw.status,
67
+ 'IP Address': raw.ip || raw.address || raw.ip_address,
68
+ 'CPU': raw.cpu != null ? `${Number(raw.cpu).toFixed(1)}%` : '--',
69
+ 'Memory': raw.mem != null ? `${Number(raw.mem).toFixed(1)}%` : '--',
70
+ 'Disk': raw.disk != null ? `${Number(raw.disk).toFixed(1)}%` : '--',
71
+ 'Version': raw.version,
72
+ 'Uptime': raw.uptime,
73
+ 'OS': raw.os || raw.platform,
74
+ 'Last Seen': raw.last_seen || raw.updated_at,
75
+ 'Tags': raw.tags,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Build the POST v1.tools.execute request body for a fleet exec — the shared
81
+ * contract with `quox fleet exec` (collector reads req.body.tool_name; host_id
82
+ * is optional and omitted for an unknown/placeholder host). Exported for tests.
83
+ */
84
+ export function buildFleetExecBody(toolName, hostId) {
85
+ const body = { tool_name: (toolName || '').trim() };
86
+ if (hostId && hostId !== 'unknown') body.host_id = hostId;
87
+ return body;
88
+ }
89
+
90
+ // ── Component ─────────────────────────────────────────────────────────
91
+ export function FleetView() {
92
+ const { client, screen, orgVersion, showToast, setInputMode } = useTui();
93
+ const [selected, setSelected] = useState(0);
94
+ const [execMode, setExecMode] = useState(false);
95
+ const [execTool, setExecTool] = useState('');
96
+ const [execBusy, setExecBusy] = useState(false);
97
+ const isActive = screen === 'fleet';
98
+
99
+ // ── Data fetching (auto-refreshes every 10s, only while active) ─────
100
+ const { data, loading, error, refresh } = usePolledApi(
101
+ async () => {
102
+ try {
103
+ const res = await client.get(resolveEndpoint('v1.fleet.status.list').path, { service: resolveEndpoint('v1.fleet.status.list').service });
104
+ const agents = res?.agents || res?.hosts || [];
105
+ return agents;
106
+ } catch {
107
+ // Fallback to /agents endpoint
108
+ const res = await client.get(resolveEndpoint('v1.agents.list').path, { service: resolveEndpoint('v1.agents.list').service });
109
+ return res?.agents || [];
110
+ }
111
+ },
112
+ [client, orgVersion],
113
+ { intervalMs: REFRESH_INTERVAL, active: isActive }
114
+ );
115
+
116
+ // ── Formatted rows ─────────────────────────────────────────────────
117
+ const rows = formatRows(data);
118
+ const selectedRow = rows[selected];
119
+ const detail = selectedRow ? buildDetail(selectedRow._raw) : null;
120
+
121
+ // While typing a tool name, mark input mode so global hotkeys (jump,
122
+ // palette, single-letter nav) don't fire on the characters being typed.
123
+ useEffect(() => {
124
+ if (isActive) setInputMode?.(execMode ? 'input' : 'nav');
125
+ }, [execMode, isActive, setInputMode]);
126
+
127
+ // Execute a fleet tool on the selected host — the TUI equivalent of
128
+ // `quox fleet exec --tool <name> --host <id>` (POST v1.tools.execute).
129
+ const runExec = useCallback(async (toolName) => {
130
+ const tool = (toolName || '').trim();
131
+ setExecMode(false);
132
+ setExecTool('');
133
+ if (!tool) return;
134
+ setExecBusy(true);
135
+ try {
136
+ const ep = resolveEndpoint('v1.tools.execute');
137
+ const body = buildFleetExecBody(tool, selectedRow?.host_id);
138
+ const res = await client.post(ep.path, { service: ep.service, body, timeout: 120000 });
139
+ const out = res?.result != null ? String(res.result) : 'ok';
140
+ showToast?.(`${tool}: ${out.replace(/\s+/g, ' ').slice(0, 80)}`, 'success');
141
+ } catch (err) {
142
+ showToast?.(`${tool} failed: ${err.message}`, 'error');
143
+ } finally {
144
+ setExecBusy(false);
145
+ refresh();
146
+ }
147
+ }, [client, selectedRow, showToast, refresh]);
148
+
149
+ // ── Keyboard handling ──────────────────────────────────────────────
150
+ useInput((input, key) => {
151
+ if (!isActive) return;
152
+
153
+ // While the tool-name prompt is open, only handle Esc (cancel);
154
+ // SearchInput owns character/Enter input.
155
+ if (execMode) {
156
+ if (key.escape) { setExecMode(false); setExecTool(''); }
157
+ return;
158
+ }
159
+
160
+ // `x` opens the exec prompt for the selected host
161
+ if (input === 'x' && !execBusy && selectedRow) {
162
+ setExecTool('');
163
+ setExecMode(true);
164
+ return;
165
+ }
166
+
167
+ // Manual refresh
168
+ if (matchKey('refresh', input, key)) {
169
+ refresh();
170
+ return;
171
+ }
172
+ }, { isActive });
173
+
174
+ // ── Footer hint ────────────────────────────────────────────────────
175
+ const footer = execMode
176
+ ? `Tool name \u00b7 Enter Run on ${selectedRow?.host_id || 'fleet'} \u00b7 Esc Cancel`
177
+ : [
178
+ '\u2191/k \u2193/j Navigate',
179
+ 'Enter Select',
180
+ 'x Exec tool',
181
+ 'r Refresh',
182
+ 'n/p Page',
183
+ 'Esc Back',
184
+ ].join(' ');
185
+
186
+ // ── Render ─────────────────────────────────────────────────────────
187
+ return (
188
+ <Layout
189
+ title="Fleet Status"
190
+ subtitle={loading ? '(refreshing...)' : `${rows.length} agents`}
191
+ footer={footer}
192
+ left={
193
+ <Box flexDirection="column">
194
+ {error && (
195
+ <Box marginBottom={1}>
196
+ <Text color={colors.error}>Error: {error.message}</Text>
197
+ </Box>
198
+ )}
199
+ {execMode && (
200
+ <Box marginBottom={1}>
201
+ <SearchInput
202
+ placeholder={`tool to run on ${selectedRow?.host_id || 'fleet'}…`}
203
+ value={execTool}
204
+ onChange={setExecTool}
205
+ onSubmit={runExec}
206
+ active={execMode}
207
+ />
208
+ </Box>
209
+ )}
210
+ {execBusy && (
211
+ <Box marginBottom={1}>
212
+ <Text color={colors.warning}>Executing…</Text>
213
+ </Box>
214
+ )}
215
+ <DataTable
216
+ columns={COLUMNS}
217
+ rows={rows}
218
+ selected={selected}
219
+ onSelectionChange={setSelected}
220
+ loading={loading && rows.length === 0}
221
+ emptyMessage="No fleet agents online — deploy via `quox agent create` or check the collector connection"
222
+ active={isActive && !execMode}
223
+ />
224
+ </Box>
225
+ }
226
+ right={
227
+ <DetailPanel
228
+ title={selectedRow ? selectedRow.host_id : undefined}
229
+ data={detail}
230
+ />
231
+ }
232
+ />
233
+ );
234
+ }
@@ -0,0 +1,453 @@
1
+ /**
2
+ * GovernanceView — WARD chain / VOLT evidence / policies / approvals
3
+ *
4
+ * Tabs: Ward | Evidence | Policies | Approvals
5
+ *
6
+ * Each tab has its own DataTable sourced from governance-related collector
7
+ * endpoints. Tab key cycles through tabs.
8
+ *
9
+ * Features:
10
+ * - `v` key verifies the selected WARD chain's integrity (Ward tab only)
11
+ *
12
+ * API (all service: collector):
13
+ * GET /ward/stats -> summary line (chains, entries, witness)
14
+ * GET /ward/chains -> table (id, entries, last tip)
15
+ * POST /ward/verify { chain_id } -> verify result (INTACT/TAMPERED)
16
+ * GET /quoxflow/evidence?orgId=&limit= -> evidence table
17
+ * GET /api/v1/policies?org_id=&limit= -> policies table
18
+ * GET /api/v1/policies/approvals/pending?org_id= -> pending approvals table
19
+ */
20
+
21
+ import React, { useState, useEffect, useCallback } from 'react';
22
+ import { Box, Text, useInput } from 'ink';
23
+ import { Layout } from '../components/Layout.jsx';
24
+ import { DataTable } from '../components/DataTable.jsx';
25
+ import { DetailPanel } from '../components/DetailPanel.jsx';
26
+ import { usePolledApi } from '../hooks/usePolledApi.js';
27
+ import { useTui } from '../app.jsx';
28
+ import { matchKey } from '../keys.js';
29
+ import { colors } from '../theme.js';
30
+ import { resolveEndpoint } from '../../api/endpoints.js';
31
+
32
+ // ── Helpers ───────────────────────────────────────────────────────────
33
+ function getOrgId(ctx) {
34
+ return ctx?.auth?.getUser?.()?.org_id || ctx?.config?.getActiveProfile?.()?.auth?.org_id || null;
35
+ }
36
+
37
+ // ── Tab definitions ───────────────────────────────────────────────────
38
+ const TABS = ['Ward', 'Evidence', 'Policies', 'Approvals'];
39
+
40
+ // ── Column definitions per tab ────────────────────────────────────────
41
+ const WARD_COLUMNS = [
42
+ { key: 'id', label: 'Chain ID', width: 20 },
43
+ { key: 'entries', label: 'Entries', width: 10 },
44
+ { key: 'lastTip', label: 'Last Tip', width: 24 },
45
+ ];
46
+
47
+ const EVIDENCE_COLUMNS = [
48
+ { key: 'id', label: 'ID', width: 14 },
49
+ { key: 'type', label: 'Type', width: 14 },
50
+ { key: 'correlationId', label: 'Correlation', width: 18 },
51
+ { key: 'workflowId', label: 'Workflow', width: 14 },
52
+ { key: 'created', label: 'Created', width: 16 },
53
+ ];
54
+
55
+ const POLICIES_COLUMNS = [
56
+ { key: 'name', label: 'Name', width: 24 },
57
+ { key: 'status', label: 'Status', width: 12 },
58
+ { key: 'updated', label: 'Updated', width: 16 },
59
+ ];
60
+
61
+ const APPROVALS_COLUMNS = [
62
+ { key: 'id', label: 'ID', width: 10 },
63
+ { key: 'policyId', label: 'Policy', width: 20 },
64
+ { key: 'operation', label: 'Operation', width: 12 },
65
+ { key: 'actor', label: 'Requested By', width: 16 },
66
+ { key: 'created', label: 'Requested', width: 16 },
67
+ ];
68
+
69
+ // ── Unwrap helpers ────────────────────────────────────────────────────
70
+
71
+ function unwrapList(data, keys) {
72
+ if (!data) return [];
73
+ if (Array.isArray(data)) return data;
74
+ for (const k of keys) {
75
+ if (Array.isArray(data[k])) return data[k];
76
+ }
77
+ return [];
78
+ }
79
+
80
+ // ── Row formatters ────────────────────────────────────────────────────
81
+
82
+ function formatWardRows(chains) {
83
+ if (!Array.isArray(chains)) return [];
84
+ return chains.map((c) => ({
85
+ id: c.id || c.chain_id || '--',
86
+ entries: c.entries != null ? String(c.entries) : '--',
87
+ lastTip: c.last_tip || c.lastTip || c.tip || c.latest_hash || '--',
88
+ _raw: c,
89
+ }));
90
+ }
91
+
92
+ function formatEvidenceRows(evidence) {
93
+ if (!Array.isArray(evidence)) return [];
94
+ return evidence.map((e) => ({
95
+ id: e.id ? String(e.id).slice(0, 12) : '--',
96
+ type: e.type || '--',
97
+ correlationId: e.correlationId || e.corr || '--',
98
+ workflowId: e.workflowId || '--',
99
+ created: formatDate(e.created_at || e.createdAt),
100
+ _raw: e,
101
+ }));
102
+ }
103
+
104
+ function formatPoliciesRows(policies) {
105
+ if (!Array.isArray(policies)) return [];
106
+ return policies.map((p) => ({
107
+ name: p.name || p.id || '--',
108
+ status: p.enabled != null ? (p.enabled ? 'enabled' : 'disabled') : (p.status || '--'),
109
+ updated: formatDate(p.updated_at || p.updatedAt),
110
+ _raw: p,
111
+ }));
112
+ }
113
+
114
+ function formatApprovalsRows(approvals) {
115
+ if (!Array.isArray(approvals)) return [];
116
+ return approvals.map((a) => ({
117
+ id: a.id ? String(a.id).slice(0, 8) : '--',
118
+ policyId: a.policy_id || a.policyId || '--',
119
+ operation: a.operation || a.type || '--',
120
+ actor: a.actor_id || a.actor || a.requested_by || '--',
121
+ created: formatDate(a.created_at || a.createdAt),
122
+ _raw: a,
123
+ }));
124
+ }
125
+
126
+ // ── Detail builders ───────────────────────────────────────────────────
127
+
128
+ function buildWardDetail(raw) {
129
+ if (!raw) return null;
130
+ return {
131
+ 'Chain ID': raw.id || raw.chain_id,
132
+ 'Entries': raw.entries,
133
+ 'Last Tip': raw.last_tip || raw.lastTip || raw.tip || raw.latest_hash,
134
+ 'Created': raw.created_at,
135
+ };
136
+ }
137
+
138
+ function buildEvidenceDetail(raw) {
139
+ if (!raw) return null;
140
+ return {
141
+ 'ID': raw.id,
142
+ 'Type': raw.type,
143
+ 'Correlation': raw.correlationId || raw.corr,
144
+ 'Envelope': raw.envelopeId,
145
+ 'Workflow': raw.workflowId,
146
+ 'Execution': raw.executionId,
147
+ 'Created': raw.created_at || raw.createdAt,
148
+ };
149
+ }
150
+
151
+ function buildPolicyDetail(raw) {
152
+ if (!raw) return null;
153
+ return {
154
+ 'Name': raw.name || raw.id,
155
+ 'Category': raw.category,
156
+ 'Effect': raw.effect,
157
+ 'Enabled': raw.enabled,
158
+ 'Status': raw.status,
159
+ 'Updated': raw.updated_at || raw.updatedAt,
160
+ };
161
+ }
162
+
163
+ function buildApprovalDetail(raw) {
164
+ if (!raw) return null;
165
+ return {
166
+ 'ID': raw.id,
167
+ 'Policy': raw.policy_id || raw.policyId,
168
+ 'Operation': raw.operation || raw.type,
169
+ 'Requested By': raw.actor_id || raw.actor || raw.requested_by,
170
+ 'Requested': raw.created_at || raw.createdAt,
171
+ };
172
+ }
173
+
174
+ // ── Utility ───────────────────────────────────────────────────────────
175
+
176
+ function formatDate(iso) {
177
+ if (!iso) return '--';
178
+ try {
179
+ const d = new Date(iso);
180
+ return d.toISOString().slice(0, 16).replace('T', ' ');
181
+ } catch {
182
+ return String(iso).slice(0, 16);
183
+ }
184
+ }
185
+
186
+ // ── Component ─────────────────────────────────────────────────────────
187
+ // ── Per-tab empty-state messages ────────────────────────────────────────
188
+ const EMPTY_MESSAGES = [
189
+ 'No chains yet',
190
+ 'No evidence records',
191
+ 'No policies defined',
192
+ 'No approvals pending ✓',
193
+ ];
194
+
195
+ export function GovernanceView() {
196
+ const ctx = useTui();
197
+ const { client, screen, showToast, orgVersion } = ctx;
198
+ const isActive = screen === 'governance';
199
+ const orgId = getOrgId(ctx);
200
+
201
+ const [activeTab, setActiveTab] = useState(0);
202
+ const [selected, setSelected] = useState(0);
203
+
204
+ // ── Fetch ward stats (summary) ───────────────────────────────────────
205
+ const { data: wardStats } = usePolledApi(
206
+ async () => {
207
+ const res = await client.get(resolveEndpoint('ward.stats.list').path, { service: resolveEndpoint('ward.stats.list').service });
208
+ return res || {};
209
+ },
210
+ [client, orgId, orgVersion],
211
+ { intervalMs: 0, active: isActive }
212
+ );
213
+
214
+ // ── Fetch ward chains ────────────────────────────────────────────────
215
+ const { data: wardChains, loading: wardLoading, error: wardError, refresh: refreshWard } = usePolledApi(
216
+ async () => {
217
+ const res = await client.get(resolveEndpoint('ward.chains.list').path, { service: resolveEndpoint('ward.chains.list').service });
218
+ return unwrapList(res, ['chains', 'items']);
219
+ },
220
+ [client, orgId, orgVersion],
221
+ { intervalMs: 0, active: isActive }
222
+ );
223
+
224
+ // ── Fetch evidence ────────────────────────────────────────────────────
225
+ const { data: evidenceData, loading: evidenceLoading, error: evidenceError, refresh: refreshEvidence } = usePolledApi(
226
+ async () => {
227
+ const res = await client.get(resolveEndpoint('quoxflow.evidence.list').path, {
228
+ service: resolveEndpoint('quoxflow.evidence.list').service,
229
+ query: { orgId, limit: 50 },
230
+ });
231
+ return unwrapList(res, ['evidence', 'data', 'items']);
232
+ },
233
+ [client, orgId, orgVersion],
234
+ { intervalMs: 0, active: isActive }
235
+ );
236
+
237
+ // ── Fetch policies ────────────────────────────────────────────────────
238
+ const { data: policiesData, loading: policiesLoading, error: policiesError, refresh: refreshPolicies } = usePolledApi(
239
+ async () => {
240
+ const res = await client.get(resolveEndpoint('v1.policies.list').path, {
241
+ service: resolveEndpoint('v1.policies.list').service,
242
+ query: { org_id: orgId, limit: 50 },
243
+ });
244
+ return unwrapList(res, ['policies', 'items']);
245
+ },
246
+ [client, orgId, orgVersion],
247
+ { intervalMs: 0, active: isActive }
248
+ );
249
+
250
+ // ── Fetch pending approvals ───────────────────────────────────────────
251
+ const { data: approvalsData, loading: approvalsLoading, error: approvalsError, refresh: refreshApprovals } = usePolledApi(
252
+ async () => {
253
+ const res = await client.get(resolveEndpoint('v1.policies.approvals.pending.list').path, {
254
+ service: resolveEndpoint('v1.policies.approvals.pending.list').service,
255
+ query: { org_id: orgId },
256
+ });
257
+ return unwrapList(res, ['approvals', 'data', 'items']);
258
+ },
259
+ [client, orgId, orgVersion],
260
+ { intervalMs: 0, active: isActive }
261
+ );
262
+
263
+ // ── Active tab data ─────────────────────────────────────────────────
264
+ let columns, rows, detail, loading, error;
265
+
266
+ switch (activeTab) {
267
+ case 0: // Ward
268
+ columns = WARD_COLUMNS;
269
+ rows = formatWardRows(wardChains);
270
+ detail = rows[selected] ? buildWardDetail(rows[selected]._raw) : null;
271
+ loading = wardLoading;
272
+ error = wardError;
273
+ break;
274
+ case 1: // Evidence
275
+ columns = EVIDENCE_COLUMNS;
276
+ rows = formatEvidenceRows(evidenceData);
277
+ detail = rows[selected] ? buildEvidenceDetail(rows[selected]._raw) : null;
278
+ loading = evidenceLoading;
279
+ error = evidenceError;
280
+ break;
281
+ case 2: // Policies
282
+ columns = POLICIES_COLUMNS;
283
+ rows = formatPoliciesRows(policiesData);
284
+ detail = rows[selected] ? buildPolicyDetail(rows[selected]._raw) : null;
285
+ loading = policiesLoading;
286
+ error = policiesError;
287
+ break;
288
+ case 3: // Approvals
289
+ columns = APPROVALS_COLUMNS;
290
+ rows = formatApprovalsRows(approvalsData);
291
+ detail = rows[selected] ? buildApprovalDetail(rows[selected]._raw) : null;
292
+ loading = approvalsLoading;
293
+ error = approvalsError;
294
+ break;
295
+ }
296
+
297
+ // ── Reset selection on tab change ───────────────────────────────────
298
+ useEffect(() => {
299
+ setSelected(0);
300
+ }, [activeTab]);
301
+
302
+ // ── Refresh current tab ─────────────────────────────────────────────
303
+ const refreshCurrent = useCallback(() => {
304
+ const refreshers = [refreshWard, refreshEvidence, refreshPolicies, refreshApprovals];
305
+ refreshers[activeTab]?.();
306
+ }, [activeTab, refreshWard, refreshEvidence, refreshPolicies, refreshApprovals]);
307
+
308
+ // ── Verify selected WARD chain ───────────────────────────────────────
309
+ const verifySelected = useCallback(async () => {
310
+ if (activeTab !== 0) return;
311
+ const row = rows[selected];
312
+ if (!row) return;
313
+ const chainId = row._raw.id || row._raw.chain_id;
314
+ if (!chainId) return;
315
+ try {
316
+ const data = await client.post(resolveEndpoint('ward.verify').path, {
317
+ service: resolveEndpoint('ward.verify').service,
318
+ body: { chain_id: chainId },
319
+ });
320
+ const result = data?.result || data;
321
+ const status = result?.status || 'unknown';
322
+ const ok = status === 'INTACT';
323
+ showToast?.(`Chain ${chainId}: ${status}`, ok ? 'success' : 'error');
324
+ refreshWard();
325
+ } catch (err) {
326
+ showToast?.(err.message, 'error');
327
+ }
328
+ }, [activeTab, rows, selected, client, showToast, refreshWard]);
329
+
330
+ // ── Keyboard handling ──────────────────────────────────────────────
331
+ useInput((input, key) => {
332
+ if (!isActive) return;
333
+
334
+ // Tab to switch sub-tabs
335
+ if (matchKey('tab', input, key)) {
336
+ setActiveTab((t) => (t + 1) % TABS.length);
337
+ return;
338
+ }
339
+
340
+ // Manual refresh
341
+ if (matchKey('refresh', input, key)) {
342
+ refreshCurrent();
343
+ return;
344
+ }
345
+
346
+ // Verify selected WARD chain
347
+ if (input === 'v') {
348
+ verifySelected();
349
+ return;
350
+ }
351
+ }, { isActive });
352
+
353
+ // ── Tab bar rendering ──────────────────────────────────────────────
354
+ const tabBar = (
355
+ <Box marginBottom={1}>
356
+ {TABS.map((tab, i) => {
357
+ const isSel = i === activeTab;
358
+ return (
359
+ <React.Fragment key={tab}>
360
+ {i > 0 && <Text color={colors.subtle}> | </Text>}
361
+ <Text
362
+ color={isSel ? colors.cyberBlue : colors.dim}
363
+ bold={isSel}
364
+ underline={isSel}
365
+ >
366
+ {tab}
367
+ </Text>
368
+ </React.Fragment>
369
+ );
370
+ })}
371
+ <Text color={colors.dim}> (Tab to switch)</Text>
372
+ </Box>
373
+ );
374
+
375
+ // ── Ward summary line ────────────────────────────────────────────────
376
+ const wardSummary = wardStats
377
+ ? `chains: ${wardStats.stats?.chains ?? wardStats.chains ?? '--'} entries: ${wardStats.stats?.entries ?? wardStats.entries ?? '--'}`
378
+ : null;
379
+
380
+ // ── Detail title ───────────────────────────────────────────────────
381
+ let detailTitle;
382
+ if (rows[selected]) {
383
+ switch (activeTab) {
384
+ case 0: detailTitle = rows[selected]._raw.id || rows[selected]._raw.chain_id || 'Chain'; break;
385
+ case 1: detailTitle = rows[selected]._raw.id || 'Evidence'; break;
386
+ case 2: detailTitle = rows[selected]._raw.name || rows[selected]._raw.id || 'Policy'; break;
387
+ case 3: detailTitle = rows[selected]._raw.id || 'Approval'; break;
388
+ }
389
+ }
390
+
391
+ // ── Footer hint ────────────────────────────────────────────────────
392
+ const footer = [
393
+ 'Tab: switch',
394
+ 'v: verify chain',
395
+ 'r: refresh',
396
+ 'Esc: back',
397
+ ].join(' ');
398
+
399
+ // ── Render ─────────────────────────────────────────────────────────
400
+ return (
401
+ <Layout
402
+ title="Governance"
403
+ subtitle={TABS[activeTab]}
404
+ footer={footer}
405
+ left={
406
+ <Box flexDirection="column">
407
+ {/* Tab bar */}
408
+ {tabBar}
409
+
410
+ {activeTab === 0 && wardSummary && (
411
+ <Box marginBottom={1}>
412
+ <Text color={colors.dim}>{wardSummary}</Text>
413
+ </Box>
414
+ )}
415
+
416
+ {error && (
417
+ <Box marginBottom={1}>
418
+ <Text color={colors.error}>Error: {error.message}</Text>
419
+ </Box>
420
+ )}
421
+
422
+ {!orgId && !loading && activeTab !== 0 && (
423
+ <Box marginBottom={1}>
424
+ <Text color={colors.warning}>No organization selected.</Text>
425
+ </Box>
426
+ )}
427
+
428
+ {activeTab === 3 && !loading && !error && rows.length === 0 ? (
429
+ <Box paddingY={1}>
430
+ <Text color={colors.success}>{EMPTY_MESSAGES[3]}</Text>
431
+ </Box>
432
+ ) : (
433
+ <DataTable
434
+ columns={columns}
435
+ rows={rows}
436
+ selected={selected}
437
+ onSelectionChange={setSelected}
438
+ loading={loading}
439
+ emptyMessage={EMPTY_MESSAGES[activeTab]}
440
+ active={isActive}
441
+ />
442
+ )}
443
+ </Box>
444
+ }
445
+ right={
446
+ <DetailPanel
447
+ title={detailTitle}
448
+ data={detail}
449
+ />
450
+ }
451
+ />
452
+ );
453
+ }