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,204 @@
1
+ /**
2
+ * AgentView — Agent management view
3
+ *
4
+ * Left panel: DataTable of agents (name, level, status, tools count)
5
+ * Right panel: DetailPanel with agent detail + tool list
6
+ *
7
+ * Features:
8
+ * - `a` key toggles activate/deactivate on the selected agent
9
+ * - Shows tool assignments in detail panel
10
+ *
11
+ * API (org-scoped):
12
+ * GET /api/orgs/:orgId/agents (service: auth) -> data.agents
13
+ * GET /api/orgs/:orgId/agents/:id (service: auth) -> agent detail
14
+ */
15
+
16
+ import React, { useState, useCallback } from 'react';
17
+ import { Box, Text, useInput } from 'ink';
18
+ import { Layout } from '../components/Layout.jsx';
19
+ import { DataTable } from '../components/DataTable.jsx';
20
+ import { DetailPanel } from '../components/DetailPanel.jsx';
21
+ import { useApi } from '../hooks/useApi.js';
22
+ import { useTui } from '../app.jsx';
23
+ import { matchKey } from '../keys.js';
24
+ import { colors, styles } from '../theme.js';
25
+ import { resolveEndpoint } from '../../api/endpoints.js';
26
+
27
+ // ── Helpers ───────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Safely extract the org ID from TUI context.
31
+ */
32
+ function getOrgId(ctx) {
33
+ return ctx?.auth?.getUser?.()?.org_id
34
+ || ctx?.config?.getActiveProfile?.()?.auth?.org_id
35
+ || null;
36
+ }
37
+
38
+ // ── Column definitions ────────────────────────────────────────────────
39
+ const COLUMNS = [
40
+ { key: 'name', label: 'Name', width: 18 },
41
+ { key: 'level', label: 'Level', width: 8 },
42
+ { key: 'status', label: 'Status', width: 12 },
43
+ { key: 'toolCount', label: 'Tools', width: 8 },
44
+ ];
45
+
46
+ /**
47
+ * Format raw agent data into table rows.
48
+ */
49
+ function formatRows(agents) {
50
+ if (!Array.isArray(agents)) return [];
51
+ return agents.map((a) => ({
52
+ ...a,
53
+ name: a.name || a.display_name || a.id,
54
+ level: a.level != null ? String(a.level) : '--',
55
+ status: `${styles.statusDot(a.status || a.state || 'unknown')} ${a.status || a.state || 'unknown'}`,
56
+ toolCount: Array.isArray(a.tools) ? String(a.tools.length) : (a.tool_count != null ? String(a.tool_count) : '--'),
57
+ _raw: a,
58
+ }));
59
+ }
60
+
61
+ /**
62
+ * Build a detail object for the right panel.
63
+ */
64
+ function buildDetail(raw) {
65
+ if (!raw) return null;
66
+ const detail = {
67
+ 'Name': raw.name || raw.display_name,
68
+ 'ID': raw.id,
69
+ 'Level': raw.level,
70
+ 'Status': raw.status || raw.state,
71
+ 'Description': raw.description,
72
+ 'Category': raw.category,
73
+ 'Created': raw.created_at,
74
+ 'Updated': raw.updated_at,
75
+ };
76
+
77
+ // Add tools as a list
78
+ if (Array.isArray(raw.tools) && raw.tools.length > 0) {
79
+ detail['Tools'] = raw.tools.map((t) => (typeof t === 'string' ? t : t.name || t.id)).join(', ');
80
+ }
81
+
82
+ // Add capabilities
83
+ if (Array.isArray(raw.capabilities) && raw.capabilities.length > 0) {
84
+ detail['Capabilities'] = raw.capabilities.join(', ');
85
+ }
86
+
87
+ return detail;
88
+ }
89
+
90
+ // ── Component ─────────────────────────────────────────────────────────
91
+ export function AgentView() {
92
+ const ctx = useTui();
93
+ const { client, screen, orgVersion } = ctx;
94
+ const isActive = screen === 'agents';
95
+ const orgId = getOrgId(ctx);
96
+
97
+ const [selected, setSelected] = useState(0);
98
+ const [actionMsg, setActionMsg] = useState(null);
99
+
100
+ // ── Data fetching (refetches when the active org changes) ───────────
101
+ const { data, loading, error, refresh } = useApi(
102
+ async () => {
103
+ if (!orgId) return [];
104
+ const res = await client.get(resolveEndpoint('agents.list', { orgId }).path, { service: resolveEndpoint('agents.list', { orgId }).service });
105
+ return res?.agents || [];
106
+ },
107
+ [client, orgId, orgVersion]
108
+ );
109
+
110
+ const rows = formatRows(data);
111
+ const selectedRow = rows[selected];
112
+ const detail = selectedRow ? buildDetail(selectedRow._raw) : null;
113
+
114
+ // ── Toggle activate/deactivate ──────────────────────────────────────
115
+ const toggleAgent = useCallback(async () => {
116
+ if (!selectedRow || !orgId) return;
117
+ const raw = selectedRow._raw;
118
+ const agentId = raw.id;
119
+ const currentStatus = raw.status || raw.state;
120
+ const newStatus = currentStatus === 'active' ? 'inactive' : 'active';
121
+
122
+ try {
123
+ await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, {
124
+ service: resolveEndpoint('agents.update', { orgId, agentId }).service,
125
+ body: { status: newStatus },
126
+ });
127
+ setActionMsg(`Agent ${raw.name || agentId} set to ${newStatus}`);
128
+ refresh();
129
+ } catch (err) {
130
+ setActionMsg(`Failed: ${err.message}`);
131
+ }
132
+
133
+ // Clear message after 3 seconds
134
+ setTimeout(() => setActionMsg(null), 3000);
135
+ }, [selectedRow, orgId, client, refresh]);
136
+
137
+ // ── Keyboard handling ──────────────────────────────────────────────
138
+ useInput((input, key) => {
139
+ if (!isActive) return;
140
+
141
+ // Manual refresh
142
+ if (matchKey('refresh', input, key)) {
143
+ refresh();
144
+ return;
145
+ }
146
+
147
+ // Toggle activate/deactivate
148
+ if (input === 'a') {
149
+ toggleAgent();
150
+ return;
151
+ }
152
+ }, { isActive });
153
+
154
+ // ── Footer hint ────────────────────────────────────────────────────
155
+ const footer = [
156
+ '\u2191/k \u2193/j Navigate',
157
+ 'a Activate/Deactivate',
158
+ 'r Refresh',
159
+ 'Esc Back',
160
+ ].join(' ');
161
+
162
+ // ── Render ─────────────────────────────────────────────────────────
163
+ return (
164
+ <Layout
165
+ title="Agents"
166
+ subtitle={orgId ? `org: ${orgId}` : '(no org)'}
167
+ footer={footer}
168
+ left={
169
+ <Box flexDirection="column">
170
+ {error && (
171
+ <Box marginBottom={1}>
172
+ <Text color={colors.error}>Error: {error.message}</Text>
173
+ </Box>
174
+ )}
175
+ {!orgId && !loading && (
176
+ <Box marginBottom={1}>
177
+ <Text color={colors.warning}>No organization selected. Switch orgs first.</Text>
178
+ </Box>
179
+ )}
180
+ {actionMsg && (
181
+ <Box marginBottom={1}>
182
+ <Text color={colors.info}>{actionMsg}</Text>
183
+ </Box>
184
+ )}
185
+ <DataTable
186
+ columns={COLUMNS}
187
+ rows={rows}
188
+ selected={selected}
189
+ onSelectionChange={setSelected}
190
+ loading={loading}
191
+ emptyMessage={orgId ? 'No agents — create one via `quox agent create`' : 'Select an organization first'}
192
+ active={isActive}
193
+ />
194
+ </Box>
195
+ }
196
+ right={
197
+ <DetailPanel
198
+ title={selectedRow ? (selectedRow._raw.name || selectedRow._raw.id) : undefined}
199
+ data={detail}
200
+ />
201
+ }
202
+ />
203
+ );
204
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * BackupView — Backup management view
3
+ *
4
+ * Left panel: DataTable of backups (id, label, type, size, status, created_at)
5
+ * Right panel: DetailPanel with backup detail
6
+ *
7
+ * Features:
8
+ * - `v` key verifies the selected backup
9
+ * - `r` key refreshes backup list
10
+ * - Action messages auto-clear after 3s
11
+ *
12
+ * API (org-scoped):
13
+ * GET /api/orgs/:orgId/backups (service: auth) -> data.backups
14
+ * POST /api/orgs/:orgId/backups/:id/verify (service: auth) -> verify result
15
+ * POST /api/orgs/:orgId/backups/:id/restore (service: auth) -> restore result
16
+ */
17
+
18
+ import React, { useState, 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 { useApi } from '../hooks/useApi.js';
24
+ import { useTui } from '../app.jsx';
25
+ import { matchKey } from '../keys.js';
26
+ import { colors, styles } from '../theme.js';
27
+ import { resolveEndpoint } from '../../api/endpoints.js';
28
+
29
+ // ── Helpers ───────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Safely extract the org ID from TUI context.
33
+ */
34
+ function getOrgId(ctx) {
35
+ try {
36
+ return ctx?.auth?.getUser?.()?.org_id || ctx?.config?.getActiveProfile?.()?.auth?.org_id || '';
37
+ } catch { return ''; }
38
+ }
39
+
40
+ /**
41
+ * Format bytes into a human-readable file size.
42
+ */
43
+ function formatFileSize(bytes) {
44
+ if (bytes == null || isNaN(bytes)) return '--';
45
+ const num = Number(bytes);
46
+ if (num === 0) return '0 B';
47
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
48
+ const i = Math.floor(Math.log(num) / Math.log(1024));
49
+ const size = (num / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0);
50
+ return `${size} ${units[i]}`;
51
+ }
52
+
53
+ /**
54
+ * Format a date string for display.
55
+ */
56
+ function formatDate(iso) {
57
+ if (!iso) return '--';
58
+ try {
59
+ const d = new Date(iso);
60
+ return d.toISOString().slice(0, 10);
61
+ } catch {
62
+ return String(iso).slice(0, 10);
63
+ }
64
+ }
65
+
66
+ // ── Column definitions ────────────────────────────────────────────────
67
+ const COLUMNS = [
68
+ { key: 'id', label: 'ID', width: 10 },
69
+ { key: 'label', label: 'Label', width: 18 },
70
+ { key: 'type', label: 'Type', width: 10 },
71
+ { key: 'size', label: 'Size', width: 10 },
72
+ { key: 'status', label: 'Status', width: 12 },
73
+ { key: 'created', label: 'Created', width: 12 },
74
+ ];
75
+
76
+ /**
77
+ * Format raw backup data into table rows.
78
+ */
79
+ function formatRows(backups) {
80
+ if (!Array.isArray(backups)) return [];
81
+ return backups.map((b) => ({
82
+ id: b.id ? String(b.id).slice(0, 8) : '--',
83
+ label: b.label || b.name || b.description || '--',
84
+ type: b.type || b.backup_type || '--',
85
+ size: formatFileSize(b.size || b.file_size),
86
+ status: `${styles.statusDot(b.status || 'unknown')} ${b.status || 'unknown'}`,
87
+ created: formatDate(b.created_at),
88
+ _raw: b,
89
+ }));
90
+ }
91
+
92
+ /**
93
+ * Build a detail object for the right panel.
94
+ */
95
+ function buildDetail(raw) {
96
+ if (!raw) return null;
97
+ return {
98
+ 'ID': raw.id,
99
+ 'Label': raw.label || raw.name,
100
+ 'Type': raw.type || raw.backup_type,
101
+ 'Status': raw.status,
102
+ 'Size': formatFileSize(raw.size || raw.file_size),
103
+ 'Description': raw.description || '--',
104
+ 'Created': raw.created_at,
105
+ 'Completed': raw.completed_at || '--',
106
+ 'Verified': raw.verified_at || (raw.verified ? 'Yes' : '--'),
107
+ 'Checksum': raw.checksum || raw.hash || '--',
108
+ 'Path': raw.path || raw.file_path || '--',
109
+ };
110
+ }
111
+
112
+ // ── Component ─────────────────────────────────────────────────────────
113
+ export function BackupView() {
114
+ const ctx = useTui();
115
+ const { client, screen, orgVersion } = ctx;
116
+ const isActive = screen === 'backups';
117
+ const orgId = getOrgId(ctx);
118
+
119
+ const [selected, setSelected] = useState(0);
120
+ const [actionMsg, setActionMsg] = useState(null);
121
+
122
+ // ── Data fetching (refetches when the active org changes) ───────────
123
+ const { data, loading, error, refresh } = useApi(
124
+ async () => {
125
+ if (!orgId) return [];
126
+ const res = await client.get(resolveEndpoint('backups.list', { orgId }).path, { service: resolveEndpoint('backups.list', { orgId }).service });
127
+ return res?.backups || [];
128
+ },
129
+ [client, orgId, orgVersion]
130
+ );
131
+
132
+ const rows = formatRows(data);
133
+ const selectedRow = rows[selected];
134
+ const detail = selectedRow ? buildDetail(selectedRow._raw) : null;
135
+
136
+ // ── Verify backup ──────────────────────────────────────────────────
137
+ const verifyBackup = useCallback(async () => {
138
+ if (!selectedRow || !orgId) return;
139
+ const raw = selectedRow._raw;
140
+ const backupId = raw.id;
141
+
142
+ try {
143
+ setActionMsg('Verifying...');
144
+ const res = await client.post(resolveEndpoint('backups.verify', { orgId, backupId }).path, {
145
+ service: resolveEndpoint('backups.verify', { orgId, backupId }).service,
146
+ body: {},
147
+ });
148
+ const result = res?.status || res?.verified ? 'verified' : 'complete';
149
+ setActionMsg(`Backup "${raw.label || raw.name || backupId}" ${result}`);
150
+ refresh();
151
+ } catch (err) {
152
+ setActionMsg(`Verify failed: ${err.message}`);
153
+ }
154
+
155
+ setTimeout(() => setActionMsg(null), 3000);
156
+ }, [selectedRow, orgId, client, refresh]);
157
+
158
+ // ── Keyboard handling ──────────────────────────────────────────────
159
+ useInput((input, key) => {
160
+ if (!isActive) return;
161
+
162
+ // Manual refresh
163
+ if (matchKey('refresh', input, key)) {
164
+ refresh();
165
+ return;
166
+ }
167
+
168
+ // Verify backup
169
+ if (input === 'v' && selectedRow) {
170
+ verifyBackup();
171
+ return;
172
+ }
173
+ }, { isActive });
174
+
175
+ // ── Footer hint ────────────────────────────────────────────────────
176
+ const footer = [
177
+ '\u2191/k \u2193/j Navigate',
178
+ 'v Verify',
179
+ 'r Refresh',
180
+ 'Esc Back',
181
+ ].join(' ');
182
+
183
+ // ── Render ─────────────────────────────────────────────────────────
184
+ return (
185
+ <Layout
186
+ title="Backups"
187
+ subtitle={orgId ? `${rows.length} backups` : '(no org)'}
188
+ footer={footer}
189
+ left={
190
+ <Box flexDirection="column">
191
+ {error && (
192
+ <Box marginBottom={1}>
193
+ <Text color={colors.error}>Error: {error.message}</Text>
194
+ </Box>
195
+ )}
196
+ {!orgId && !loading && (
197
+ <Box marginBottom={1}>
198
+ <Text color={colors.warning}>No organization selected.</Text>
199
+ </Box>
200
+ )}
201
+ {actionMsg && (
202
+ <Box marginBottom={1}>
203
+ <Text color={colors.info}>{actionMsg}</Text>
204
+ </Box>
205
+ )}
206
+ <DataTable
207
+ columns={COLUMNS}
208
+ rows={rows}
209
+ selected={selected}
210
+ onSelectionChange={setSelected}
211
+ loading={loading}
212
+ emptyMessage={orgId ? 'No backups — create one via `quox backup create`' : 'Select an organization first'}
213
+ active={isActive}
214
+ />
215
+ </Box>
216
+ }
217
+ right={
218
+ <DetailPanel
219
+ title={selectedRow ? (selectedRow._raw.label || selectedRow._raw.name || 'Backup') : undefined}
220
+ data={detail}
221
+ />
222
+ }
223
+ />
224
+ );
225
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * BrainView — Quox Qortex search screen
3
+ *
4
+ * Left panel: status header (note count + milestone) + search box +
5
+ * DataTable of search results
6
+ * Right panel: DetailPanel for the selected result
7
+ *
8
+ * Features:
9
+ * - `/` key focuses the search input
10
+ * - Search posts to the brain semantic/keyword search API
11
+ * - Result detail shows title, kind, vault, snippet, score
12
+ *
13
+ * API:
14
+ * GET /brain/status (service: quoxbrain) -> { noteCount, sources, status, milestone }
15
+ * POST /brain/search (service: quoxbrain, body: { query, limit }) -> { results, tookMs }
16
+ */
17
+
18
+ import React, { useState, useCallback, useEffect } 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 { useApi } from '../hooks/useApi.js';
25
+ import { useTui } from '../app.jsx';
26
+ import { matchKey } from '../keys.js';
27
+ import { colors } from '../theme.js';
28
+ import { resolveEndpoint } from '../../api/endpoints.js';
29
+
30
+ // ── Column definitions ────────────────────────────────────────────────
31
+ const RESULT_COLUMNS = [
32
+ { key: 'title', label: 'Title', width: 26 },
33
+ { key: 'kind', label: 'Kind', width: 12 },
34
+ { key: 'snippet', label: 'Snippet', width: 32 },
35
+ ];
36
+
37
+ // ── Row formatters ────────────────────────────────────────────────────
38
+
39
+ function formatResultRows(results) {
40
+ if (!Array.isArray(results)) return [];
41
+ return results.map((r) => ({
42
+ title: truncate(r.title || r.noteId || 'Untitled', 24),
43
+ kind: r.kind || '--',
44
+ snippet: truncate(r.snippet || '', 30),
45
+ _raw: r,
46
+ }));
47
+ }
48
+
49
+ function buildResultDetail(raw) {
50
+ if (!raw) return null;
51
+ return {
52
+ 'Title': raw.title,
53
+ 'Kind': raw.kind,
54
+ 'Vault': raw.vaultId,
55
+ 'Path': raw.relPath,
56
+ 'Heading': raw.headingPath,
57
+ 'Score': raw.score != null ? raw.score.toFixed(2) : undefined,
58
+ 'Snippet': raw.snippet,
59
+ 'Updated': raw.updatedAt,
60
+ 'Note ID': raw.noteId,
61
+ };
62
+ }
63
+
64
+ // ── Utility ───────────────────────────────────────────────────────────
65
+
66
+ function truncate(str, max) {
67
+ if (!str) return '';
68
+ const s = String(str).replace(/\n/g, ' ');
69
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
70
+ }
71
+
72
+ function formatMilestone(status) {
73
+ if (!status) return '';
74
+ const parts = [];
75
+ if (status.noteCount != null) parts.push(`${status.noteCount} notes`);
76
+ if (status.milestone) parts.push(status.milestone);
77
+ return parts.join(' | ');
78
+ }
79
+
80
+ // ── Component ─────────────────────────────────────────────────────────
81
+ export function BrainView() {
82
+ const ctx = useTui();
83
+ const { client, screen, setInputMode, orgVersion } = ctx;
84
+ const isActive = screen === 'brain';
85
+
86
+ const [selected, setSelected] = useState(0);
87
+ const [search, setSearch] = useState('');
88
+ const [searchActive, setSearchActive] = useState(false);
89
+
90
+ // ── Input-mode registry: claim 'input' while the search box is
91
+ // focused so global shortcuts (e.g. '?') don't steal keystrokes.
92
+ useEffect(() => {
93
+ if (searchActive) setInputMode?.('input');
94
+ else if (isActive) setInputMode?.('nav');
95
+ }, [searchActive, isActive, setInputMode]);
96
+
97
+ // ── Fetch brain status (note count + milestone) ──────────────────────
98
+ const { data: statusData, loading: statusLoading, refresh: refreshStatus } = useApi(
99
+ async () => {
100
+ const res = await client.get(resolveEndpoint('brain.status.list').path, {
101
+ service: resolveEndpoint('brain.status.list').service,
102
+ });
103
+ return res || null;
104
+ },
105
+ [client, orgVersion]
106
+ );
107
+
108
+ // ── Fetch search results (only when a query has been submitted) ─────
109
+ const { data: resultsData, loading: resultsLoading, error: resultsError, refresh: refreshResults } = useApi(
110
+ async () => {
111
+ if (!search) return [];
112
+ const res = await client.post(resolveEndpoint('brain.search.list').path, {
113
+ service: resolveEndpoint('brain.search.list').service,
114
+ body: { query: search, limit: 20 },
115
+ });
116
+ return res?.results || [];
117
+ },
118
+ [client, search, orgVersion]
119
+ );
120
+
121
+ const rows = formatResultRows(resultsData);
122
+ const detail = rows[selected] ? buildResultDetail(rows[selected]._raw) : null;
123
+
124
+ // ── Reset selection when results change ─────────────────────────────
125
+ useEffect(() => {
126
+ setSelected(0);
127
+ }, [resultsData]);
128
+
129
+ // ── Search submit ───────────────────────────────────────────────────
130
+ const handleSearchSubmit = useCallback(() => {
131
+ setSearchActive(false);
132
+ setSelected(0);
133
+ refreshResults();
134
+ }, [refreshResults]);
135
+
136
+ // ── Refresh all ─────────────────────────────────────────────────────
137
+ const refreshAll = useCallback(() => {
138
+ refreshStatus();
139
+ refreshResults();
140
+ }, [refreshStatus, refreshResults]);
141
+
142
+ // ── Keyboard handling ──────────────────────────────────────────────
143
+ useInput((input, key) => {
144
+ if (!isActive) return;
145
+
146
+ // Focus search
147
+ if (matchKey('search', input, key) && !searchActive) {
148
+ setSearchActive(true);
149
+ return;
150
+ }
151
+
152
+ // Escape from search
153
+ if (searchActive && matchKey('back', input, key)) {
154
+ setSearchActive(false);
155
+ return;
156
+ }
157
+
158
+ // Let SearchInput handle keys when focused
159
+ if (searchActive) return;
160
+
161
+ // Manual refresh
162
+ if (matchKey('refresh', input, key)) {
163
+ refreshAll();
164
+ return;
165
+ }
166
+ }, { isActive });
167
+
168
+ // ── Footer hint ────────────────────────────────────────────────────
169
+ const footer = searchActive
170
+ ? 'Type to search Enter Submit Esc Cancel'
171
+ : [
172
+ '↑/k ↓/j Navigate',
173
+ '/ Search',
174
+ 'r Refresh',
175
+ 'Esc Back',
176
+ ].join(' ');
177
+
178
+ const detailTitle = rows[selected] ? (rows[selected]._raw.title || 'Result') : undefined;
179
+
180
+ // ── Render ─────────────────────────────────────────────────────────
181
+ return (
182
+ <Layout
183
+ title="Brain"
184
+ subtitle={statusLoading ? 'loading...' : formatMilestone(statusData)}
185
+ footer={footer}
186
+ left={
187
+ <Box flexDirection="column">
188
+ <Box marginBottom={1}>
189
+ <SearchInput
190
+ placeholder="Search the brain..."
191
+ value={search}
192
+ onChange={setSearch}
193
+ onSubmit={handleSearchSubmit}
194
+ active={searchActive}
195
+ />
196
+ </Box>
197
+
198
+ {resultsError && (
199
+ <Box marginBottom={1}>
200
+ <Text color={colors.error}>Error: {resultsError.message}</Text>
201
+ </Box>
202
+ )}
203
+
204
+ <DataTable
205
+ columns={RESULT_COLUMNS}
206
+ rows={rows}
207
+ selected={selected}
208
+ onSelectionChange={setSelected}
209
+ loading={resultsLoading}
210
+ emptyMessage={search ? 'No results match your search' : 'Type a query and press Enter to search'}
211
+ active={isActive && !searchActive}
212
+ />
213
+ </Box>
214
+ }
215
+ right={
216
+ <DetailPanel
217
+ title={detailTitle}
218
+ data={detail}
219
+ />
220
+ }
221
+ />
222
+ );
223
+ }