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,129 @@
1
+ /**
2
+ * Layout component — header + dual-panel + status bar
3
+ *
4
+ * Provides the two-panel layout (35% left / 65% right) used by most views.
5
+ * Mirrors the QuoxBastion BubbleTea TUI layout pattern.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { Box, Text } from 'ink';
10
+ import { colors } from '../theme.js';
11
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
12
+ import { useTui } from '../app.jsx';
13
+ import { Breadcrumb } from './Breadcrumb.jsx';
14
+ import { SCREEN_TITLES } from '../registry.js';
15
+
16
+ /** Separator width: fills the terminal, with a small margin, floor of 20. */
17
+ function separatorWidth(columns) {
18
+ return Math.max(20, columns - 2);
19
+ }
20
+
21
+ /**
22
+ * Build the breadcrumb trail from nav history + the current screen, e.g.
23
+ * ['Main', 'Fleet']. Renders nothing (via Breadcrumb's own empty-path
24
+ * guard) when the trail is trivial — i.e. we're still on the menu.
25
+ */
26
+ function useBreadcrumbPath() {
27
+ const ctx = useTui();
28
+ const screen = ctx?.screen;
29
+ const history = ctx?.history || [];
30
+
31
+ if (!screen || screen === 'menu') return [];
32
+
33
+ return [...history, screen].map((id) => SCREEN_TITLES[id] || id);
34
+ }
35
+
36
+ export function Layout({ title, subtitle, left, right, footer, children }) {
37
+ const { columns } = useTerminalSize();
38
+ const breadcrumbPath = useBreadcrumbPath();
39
+
40
+ return (
41
+ <Box flexDirection="column" width="100%" height="100%">
42
+ {/* Header */}
43
+ <Box height={1} width="100%">
44
+ <Text color={colors.primary} bold> {title || 'QuoxCLI'}</Text>
45
+ {subtitle && (
46
+ <Text color={colors.dim}> {subtitle}</Text>
47
+ )}
48
+ {breadcrumbPath.length > 0 && (
49
+ <Box marginLeft={2}>
50
+ <Breadcrumb path={breadcrumbPath} />
51
+ </Box>
52
+ )}
53
+ </Box>
54
+
55
+ {/* Separator */}
56
+ <Box height={1} width="100%">
57
+ <Text color={colors.subtle}>{' ' + '\u2500'.repeat(separatorWidth(columns))}</Text>
58
+ </Box>
59
+
60
+ {/* Content area */}
61
+ <Box flexGrow={1} width="100%">
62
+ {children ? (
63
+ children
64
+ ) : (
65
+ <>
66
+ {/* Left panel (35%) */}
67
+ <Box width="35%" flexDirection="column" borderStyle="single" borderColor={colors.primary} paddingX={1}>
68
+ {left}
69
+ </Box>
70
+
71
+ {/* Right panel (65%) */}
72
+ <Box width="65%" flexDirection="column" borderStyle="single" borderColor={colors.subtle} paddingX={1}>
73
+ {right}
74
+ </Box>
75
+ </>
76
+ )}
77
+ </Box>
78
+
79
+ {/* Footer hint bar */}
80
+ {footer && (
81
+ <Box height={1} width="100%">
82
+ <Text color={colors.dim}> {footer}</Text>
83
+ </Box>
84
+ )}
85
+ </Box>
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Single-panel layout for views like Chat that need full width.
91
+ */
92
+ export function FullLayout({ title, subtitle, footer, children }) {
93
+ const { columns } = useTerminalSize();
94
+ const breadcrumbPath = useBreadcrumbPath();
95
+
96
+ return (
97
+ <Box flexDirection="column" width="100%" height="100%">
98
+ {/* Header */}
99
+ <Box height={1} width="100%">
100
+ <Text color={colors.primary} bold> {title || 'QuoxCLI'}</Text>
101
+ {subtitle && (
102
+ <Text color={colors.dim}> {subtitle}</Text>
103
+ )}
104
+ {breadcrumbPath.length > 0 && (
105
+ <Box marginLeft={2}>
106
+ <Breadcrumb path={breadcrumbPath} />
107
+ </Box>
108
+ )}
109
+ </Box>
110
+
111
+ {/* Separator */}
112
+ <Box height={1} width="100%">
113
+ <Text color={colors.subtle}>{' ' + '\u2500'.repeat(separatorWidth(columns))}</Text>
114
+ </Box>
115
+
116
+ {/* Content */}
117
+ <Box flexGrow={1} flexDirection="column" paddingX={1}>
118
+ {children}
119
+ </Box>
120
+
121
+ {/* Footer */}
122
+ {footer && (
123
+ <Box height={1} width="100%">
124
+ <Text color={colors.dim}> {footer}</Text>
125
+ </Box>
126
+ )}
127
+ </Box>
128
+ );
129
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Palette — command palette overlay
3
+ *
4
+ * Opened from app.jsx on Ctrl+K or ':' (nav mode only). Renders in place
5
+ * of the current screen — mirrors HelpOverlay's content-swap pattern, no
6
+ * layout shift. Owns its own keystrokes via its own useInput while open.
7
+ *
8
+ * A text query filters SCREEN_REGISTRY (case-insensitive substring match
9
+ * on title + keywords). Up/Down or Ctrl+P/Ctrl+N move the selection,
10
+ * Enter calls onSelect(id), Esc calls onClose().
11
+ *
12
+ * Props:
13
+ * onSelect(id) - called with the chosen screen id
14
+ * onClose() - called when the user dismisses the palette
15
+ */
16
+
17
+ import React, { useState, useMemo } from 'react';
18
+ import { Box, Text, useInput } from 'ink';
19
+ import { SCREEN_REGISTRY } from '../registry.js';
20
+ import { colors } from '../theme.js';
21
+ import { useTerminalSize } from '../hooks/useTerminalSize.js';
22
+
23
+ const PREFERRED_WIDTH = 56;
24
+ const MIN_WIDTH = 24;
25
+ const MAX_VISIBLE = 8;
26
+
27
+ /** Case-insensitive substring match against title + keywords. */
28
+ function filterEntries(query) {
29
+ const q = query.trim().toLowerCase();
30
+ if (!q) return SCREEN_REGISTRY;
31
+ return SCREEN_REGISTRY.filter((entry) => {
32
+ if (entry.title.toLowerCase().includes(q)) return true;
33
+ return entry.keywords.some((kw) => kw.toLowerCase().includes(q));
34
+ });
35
+ }
36
+
37
+ export function Palette({ onSelect, onClose }) {
38
+ const { columns } = useTerminalSize();
39
+ const [query, setQuery] = useState('');
40
+ const [selected, setSelected] = useState(0);
41
+
42
+ const results = useMemo(() => filterEntries(query), [query]);
43
+
44
+ useInput((input, key) => {
45
+ if (key.escape) {
46
+ onClose?.();
47
+ return;
48
+ }
49
+
50
+ if (key.return) {
51
+ const entry = results[selected];
52
+ if (entry) onSelect?.(entry.id);
53
+ return;
54
+ }
55
+
56
+ if (key.upArrow || (key.ctrl && input === 'p')) {
57
+ setSelected((s) => (results.length === 0 ? 0 : (s > 0 ? s - 1 : results.length - 1)));
58
+ return;
59
+ }
60
+
61
+ if (key.downArrow || (key.ctrl && input === 'n')) {
62
+ setSelected((s) => (results.length === 0 ? 0 : (s < results.length - 1 ? s + 1 : 0)));
63
+ return;
64
+ }
65
+
66
+ if (key.backspace || key.delete) {
67
+ setQuery((q) => q.slice(0, -1));
68
+ setSelected(0);
69
+ return;
70
+ }
71
+
72
+ // Printable characters only — ignore other ctrl/meta/tab combos.
73
+ if (input && !key.ctrl && !key.meta && !key.tab) {
74
+ setQuery((q) => q + input);
75
+ setSelected(0);
76
+ }
77
+ });
78
+
79
+ const width = Math.max(MIN_WIDTH, Math.min(PREFERRED_WIDTH, columns - 4));
80
+
81
+ // Keep the selected row inside the visible window as the list scrolls.
82
+ const start = Math.max(
83
+ 0,
84
+ Math.min(selected - Math.floor(MAX_VISIBLE / 2), Math.max(0, results.length - MAX_VISIBLE))
85
+ );
86
+ const visible = results.slice(start, start + MAX_VISIBLE);
87
+
88
+ return (
89
+ <Box flexDirection="column" alignItems="center" width="100%" paddingTop={1}>
90
+ <Box
91
+ flexDirection="column"
92
+ borderStyle="round"
93
+ borderColor={colors.primary}
94
+ paddingX={2}
95
+ paddingY={1}
96
+ width={width}
97
+ >
98
+ {/* Header */}
99
+ <Box justifyContent="center" marginBottom={1}>
100
+ <Text color={colors.primary} bold>Jump to Screen</Text>
101
+ </Box>
102
+
103
+ {/* Query input */}
104
+ <Box borderStyle="single" borderColor={colors.cyberBlue} paddingX={1} marginBottom={1}>
105
+ <Text color={colors.cyberBlue} bold>{'> '}</Text>
106
+ {query ? (
107
+ <Text color={colors.text}>{query}</Text>
108
+ ) : (
109
+ <Text color={colors.dim}>Type to filter...</Text>
110
+ )}
111
+ <Text color={colors.cyberBlue}>{'█'}</Text>
112
+ </Box>
113
+
114
+ {/* Results */}
115
+ {visible.length === 0 ? (
116
+ <Text color={colors.dim}>No matching screens</Text>
117
+ ) : (
118
+ visible.map((entry, i) => {
119
+ const isSelected = start + i === selected;
120
+ return (
121
+ <Box key={entry.id} height={1}>
122
+ <Text color={isSelected ? colors.primary : colors.subtle}>
123
+ {isSelected ? ' ▶ ' : ' '}
124
+ </Text>
125
+ <Text color={colors.cyberBlue} bold>[{entry.hotkey}] </Text>
126
+ <Text color={isSelected ? colors.text : colors.textMuted} bold={isSelected}>
127
+ {entry.title}
128
+ </Text>
129
+ </Box>
130
+ );
131
+ })
132
+ )}
133
+
134
+ {/* Footer hint */}
135
+ <Box marginTop={1}>
136
+ <Text color={colors.dim}>{'↑/↓'} Navigate Enter Jump Esc Close</Text>
137
+ </Box>
138
+ </Box>
139
+ </Box>
140
+ );
141
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * SearchInput — Text input for search/filter operations
3
+ *
4
+ * Uses @inkjs/ui TextInput when available, with a fallback
5
+ * implementation using ink's useInput for basic text entry.
6
+ *
7
+ * Props:
8
+ * placeholder - placeholder text (default: 'Search...')
9
+ * value - current search value
10
+ * onChange - callback when value changes
11
+ * onSubmit - callback when Enter is pressed
12
+ * active - whether input accepts keyboard input (default: true)
13
+ */
14
+
15
+ import React, { useState, useEffect } from 'react';
16
+ import { Box, Text, useInput } from 'ink';
17
+ import { colors } from '../theme.js';
18
+
19
+ // ── Try to load @inkjs/ui TextInput ────────────────────────────────────
20
+ let InkTextInput = null;
21
+ try {
22
+ const mod = await import('@inkjs/ui');
23
+ InkTextInput = mod.TextInput;
24
+ } catch {
25
+ // Fallback to custom implementation below
26
+ }
27
+
28
+ /**
29
+ * Fallback text input using useInput.
30
+ * Handles printable characters, backspace, and Enter.
31
+ */
32
+ function FallbackTextInput({ placeholder, value, onChange, onSubmit, active }) {
33
+ useInput((input, key) => {
34
+ if (!active) return;
35
+
36
+ // Enter submits
37
+ if (key.return) {
38
+ if (onSubmit) onSubmit(value);
39
+ return;
40
+ }
41
+
42
+ // Backspace / delete
43
+ if (key.backspace || key.delete) {
44
+ if (onChange && value.length > 0) {
45
+ onChange(value.slice(0, -1));
46
+ }
47
+ return;
48
+ }
49
+
50
+ // Escape is handled by parent, not here
51
+
52
+ // Printable characters
53
+ if (input && !key.ctrl && !key.meta && !key.escape && !key.tab) {
54
+ if (onChange) {
55
+ onChange(value + input);
56
+ }
57
+ }
58
+ });
59
+
60
+ const showPlaceholder = !value || value.length === 0;
61
+
62
+ return (
63
+ <Box>
64
+ <Text color={colors.cyberBlue} bold>/ </Text>
65
+ {showPlaceholder ? (
66
+ <Text color={colors.dim}>{placeholder}</Text>
67
+ ) : (
68
+ <Text color={colors.text}>{value}</Text>
69
+ )}
70
+ {active && <Text color={colors.cyberBlue}>{'\u2588'}</Text>}
71
+ </Box>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Wrapper that uses @inkjs/ui TextInput if available.
77
+ */
78
+ function InkUITextInput({ placeholder, value, onChange, onSubmit, active }) {
79
+ // @inkjs/ui TextInput uses defaultValue + onChange, not controlled value.
80
+ // We track the internal value and sync via onChange.
81
+ const [initialized, setInitialized] = useState(false);
82
+
83
+ useEffect(() => {
84
+ setInitialized(true);
85
+ }, []);
86
+
87
+ return (
88
+ <Box>
89
+ <Text color={colors.cyberBlue} bold>/ </Text>
90
+ <InkTextInput
91
+ placeholder={placeholder}
92
+ defaultValue={value || ''}
93
+ isDisabled={!active}
94
+ onChange={(newVal) => {
95
+ if (onChange) onChange(newVal);
96
+ }}
97
+ onSubmit={(val) => {
98
+ if (onSubmit) onSubmit(val);
99
+ }}
100
+ />
101
+ </Box>
102
+ );
103
+ }
104
+
105
+ export function SearchInput({
106
+ placeholder = 'Search...',
107
+ value = '',
108
+ onChange,
109
+ onSubmit,
110
+ active = true,
111
+ }) {
112
+ const InputComponent = InkTextInput ? InkUITextInput : FallbackTextInput;
113
+
114
+ return (
115
+ <Box borderStyle="single" borderColor={active ? colors.cyberBlue : colors.subtle} paddingX={1}>
116
+ <InputComponent
117
+ placeholder={placeholder}
118
+ value={value}
119
+ onChange={onChange}
120
+ onSubmit={onSubmit}
121
+ active={active}
122
+ />
123
+ </Box>
124
+ );
125
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Sparkline — pure presentational block-ramp trend renderer
3
+ *
4
+ * Maps a numeric series onto the unicode block ramp (▁▂▃▄▅▆▇█), scaled
5
+ * between the min/max of the (optionally windowed) series, and renders it
6
+ * as a single <Text> line. Used for btop-style "line went up/down" trends
7
+ * inside dense TUI views (e.g. HealthView).
8
+ *
9
+ * Props:
10
+ * data {number[]} - the series to render (oldest -> newest)
11
+ * width {number} - optional max number of points to show; when the
12
+ * series is longer than width, only the most recent
13
+ * `width` points are rendered (and the ramp is
14
+ * scaled to that visible window)
15
+ * color {string} - optional hex color (defaults to theme cyberBlue)
16
+ */
17
+
18
+ import React from 'react';
19
+ import { Text } from 'ink';
20
+ import { colors } from '../theme.js';
21
+
22
+ // ── Block ramp (low -> high) ────────────────────────────────────────────
23
+ const RAMP = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
24
+ const MID_CHAR = RAMP[Math.floor(RAMP.length / 2)];
25
+
26
+ /**
27
+ * Convert a numeric series into a string of ramp characters.
28
+ * Exported for direct unit testing without rendering ink output.
29
+ */
30
+ export function sparklineChars(data, width) {
31
+ if (!Array.isArray(data) || data.length === 0) return '';
32
+
33
+ const series = width && data.length > width ? data.slice(-width) : data;
34
+
35
+ const min = Math.min(...series);
36
+ const max = Math.max(...series);
37
+ const range = max - min;
38
+
39
+ if (range === 0) {
40
+ return MID_CHAR.repeat(series.length);
41
+ }
42
+
43
+ return series
44
+ .map((v) => {
45
+ const ratio = (v - min) / range;
46
+ const idx = Math.min(RAMP.length - 1, Math.floor(ratio * RAMP.length));
47
+ return RAMP[idx];
48
+ })
49
+ .join('');
50
+ }
51
+
52
+ export function Sparkline({ data, width, color }) {
53
+ const chars = sparklineChars(data, width);
54
+ return <Text color={color || colors.cyberBlue}>{chars}</Text>;
55
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Status bar — bottom of the TUI
3
+ *
4
+ * Shows: user email, active org, profile name, current screen, a live
5
+ * service-health dot (polled every 30s), a refresh/staleness indicator,
6
+ * a clock, and key hints.
7
+ */
8
+
9
+ import React, { useEffect, useState } from 'react';
10
+ import { Box, Text } from 'ink';
11
+ import { colors } from '../theme.js';
12
+ import { useTui } from '../app.jsx';
13
+ import { useHealth } from '../hooks/useHealth.js';
14
+
15
+ // Display labels for each navigable screen id (see MainMenu.jsx MENU_ITEMS).
16
+ const SCREEN_LABELS = {
17
+ menu: 'Menu',
18
+ fleet: 'Fleet',
19
+ agents: 'Agents',
20
+ tools: 'Tools',
21
+ orgs: 'Organizations',
22
+ memory: 'Memory',
23
+ brain: 'Brain',
24
+ workflows: 'Workflows',
25
+ chat: 'Chat',
26
+ admin: 'Admin',
27
+ files: 'Files',
28
+ backups: 'Backups',
29
+ objectives: 'Objectives',
30
+ inbox: 'Inbox',
31
+ vault: 'Vault',
32
+ governance: 'Governance',
33
+ };
34
+
35
+ const HEALTH_DOT_COLORS = {
36
+ green: colors.success,
37
+ amber: colors.warning,
38
+ red: colors.error,
39
+ grey: colors.dim,
40
+ };
41
+
42
+ function formatClock(date) {
43
+ const hh = String(date.getHours()).padStart(2, '0');
44
+ const mm = String(date.getMinutes()).padStart(2, '0');
45
+ return `${hh}:${mm}`;
46
+ }
47
+
48
+ function formatAge(lastFetched, isStale) {
49
+ if (lastFetched == null) return '';
50
+ if (isStale) return 'stale';
51
+ const secs = Math.max(0, Math.floor((Date.now() - lastFetched) / 1000));
52
+ return `${secs}s`;
53
+ }
54
+
55
+ export function StatusBar() {
56
+ const ctx = useTui();
57
+ const config = ctx?.config;
58
+ const client = ctx?.client;
59
+ const screen = ctx?.screen || '';
60
+
61
+ // Try to extract user/org info from auth store
62
+ let userEmail = '';
63
+ let orgName = '';
64
+ let profileName = '';
65
+
66
+ try {
67
+ const profile = config?.getActiveProfile?.();
68
+ userEmail = profile?.auth?.email || profile?.auth?.user_email || '';
69
+ orgName = profile?.auth?.org_name || profile?.auth?.org_slug || '';
70
+ profileName = config?.getActiveProfileName?.() || '';
71
+ } catch {
72
+ // Config might not be loaded
73
+ }
74
+
75
+ const { color: healthColor, loading: healthLoading, lastFetched, isStale } = useHealth(client);
76
+
77
+ // Clock — recomputed on a 30s tick (no 1s timer).
78
+ const [now, setNow] = useState(() => new Date());
79
+ useEffect(() => {
80
+ const id = setInterval(() => setNow(new Date()), 30000);
81
+ return () => clearInterval(id);
82
+ }, []);
83
+
84
+ const screenLabel = SCREEN_LABELS[screen] ?? (screen || '');
85
+ const ageText = formatAge(lastFetched, isStale);
86
+ const dotColor = HEALTH_DOT_COLORS[healthColor] || HEALTH_DOT_COLORS.grey;
87
+
88
+ return (
89
+ <Box height={1} width="100%">
90
+ <Box flexGrow={1}>
91
+ <Text color={colors.primary} bold> QUOX</Text>
92
+ {screenLabel && (
93
+ <>
94
+ <Text color={colors.dim}> | </Text>
95
+ <Text color={colors.accent} bold>{screenLabel}</Text>
96
+ </>
97
+ )}
98
+ <Text color={colors.dim}> | </Text>
99
+ {userEmail ? (
100
+ <>
101
+ <Text color={colors.text}>{userEmail}</Text>
102
+ <Text color={colors.dim}> | </Text>
103
+ </>
104
+ ) : (
105
+ <>
106
+ <Text color={colors.warning}>not logged in</Text>
107
+ <Text color={colors.dim}> | </Text>
108
+ </>
109
+ )}
110
+ {orgName && (
111
+ <>
112
+ <Text color={colors.accent}>{orgName}</Text>
113
+ <Text color={colors.dim}> | </Text>
114
+ </>
115
+ )}
116
+ {profileName && (
117
+ <Text color={colors.dim}>{profileName}</Text>
118
+ )}
119
+ </Box>
120
+ <Box>
121
+ {healthLoading && <Text color={colors.dim}>{'⟳ '}</Text>}
122
+ <Text color={dotColor}>{'●'}</Text>
123
+ {ageText && (
124
+ <Text color={isStale ? colors.warning : colors.dim}> {ageText}</Text>
125
+ )}
126
+ <Text color={colors.dim}> {formatClock(now)} </Text>
127
+ <Text color={colors.dim}>
128
+ {screen === 'menu' ? 'q:quit ?:help' : 'Esc:back ?:help q:quit'}
129
+ </Text>
130
+ </Box>
131
+ </Box>
132
+ );
133
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Toast — Auto-dismissing notification component
3
+ *
4
+ * Shows a message at the top of the view that auto-dismisses after a timeout.
5
+ * Supports success, error, warning, and info variants.
6
+ *
7
+ * Props:
8
+ * message - notification text
9
+ * type - 'success' | 'error' | 'warning' | 'info' (default: 'info')
10
+ * duration - auto-dismiss ms (default: 3000, 0 = no auto-dismiss)
11
+ * onDismiss- called when the toast is dismissed
12
+ * visible - whether the toast is shown (default: true when message is truthy)
13
+ */
14
+
15
+ import React, { useEffect } from 'react';
16
+ import { Box, Text } from 'ink';
17
+ import { colors } from '../theme.js';
18
+
19
+ const TYPE_CONFIG = {
20
+ success: { color: colors.success, icon: '\u2714' }, // ✔
21
+ error: { color: colors.error, icon: '\u2718' }, // ✘
22
+ warning: { color: colors.warning, icon: '\u26A0' }, // ⚠
23
+ info: { color: colors.cyberBlue, icon: '\u2139' }, // ℹ
24
+ };
25
+
26
+ export function Toast({ message, type = 'info', duration = 3000, onDismiss, visible }) {
27
+ const show = visible !== undefined ? visible : !!message;
28
+
29
+ useEffect(() => {
30
+ if (!show || duration <= 0) return;
31
+ const timer = setTimeout(() => {
32
+ onDismiss?.();
33
+ }, duration);
34
+ return () => clearTimeout(timer);
35
+ }, [show, duration, onDismiss]);
36
+
37
+ if (!show || !message) return null;
38
+
39
+ const config = TYPE_CONFIG[type] || TYPE_CONFIG.info;
40
+
41
+ return (
42
+ <Box
43
+ borderStyle="single"
44
+ borderColor={config.color}
45
+ paddingX={1}
46
+ marginBottom={1}
47
+ >
48
+ <Text color={config.color} bold>{config.icon} </Text>
49
+ <Text color={config.color}>{message}</Text>
50
+ </Box>
51
+ );
52
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * useApi — React hook for API calls with loading/error/data state
3
+ *
4
+ * Wraps an async fetch function and manages its lifecycle state.
5
+ * Automatically calls the fetch function on mount and when deps change.
6
+ * Provides a `refresh` function for manual re-fetching.
7
+ *
8
+ * Usage:
9
+ * const { data, loading, error, refresh } = useApi(
10
+ * () => client.fleet.list(),
11
+ * [client]
12
+ * );
13
+ *
14
+ * @param {Function} fetchFn - async function that returns data
15
+ * @param {Array} deps - dependency array (re-fetch when changed)
16
+ * @returns {{ data: any, loading: boolean, error: Error|null, refresh: Function }}
17
+ */
18
+
19
+ import { useState, useEffect, useCallback } from 'react';
20
+
21
+ export function useApi(fetchFn, deps = []) {
22
+ const [data, setData] = useState(null);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState(null);
25
+
26
+ const refresh = useCallback(async () => {
27
+ setLoading(true);
28
+ setError(null);
29
+ try {
30
+ const result = await fetchFn();
31
+ setData(result);
32
+ } catch (err) {
33
+ setError(err);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, deps);
38
+
39
+ useEffect(() => {
40
+ refresh();
41
+ }, [refresh]);
42
+
43
+ return { data, loading, error, refresh };
44
+ }