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,230 @@
1
+ /**
2
+ * ObjectivesView — Objectives (autonomous multi-step goals) management view
3
+ *
4
+ * Left panel: DataTable of objectives (title, status, progress)
5
+ * Right panel: DetailPanel with objective detail
6
+ *
7
+ * Features:
8
+ * - `a` key approves a decomposed objective's plan (starts execution)
9
+ * - `s` key starts an objective (decomposes into tasks)
10
+ * - Auto-refreshes every 10 seconds via usePolledApi (only while active —
11
+ * never polls in the background when another screen is showing)
12
+ * - After `a`/`s`, an immediate refresh() is fired (decomposition/start
13
+ * happens async server-side — see services/tasks/lib/objectiveDispatch.js
14
+ * — so the list often hasn't caught up yet) and the acted-on row shows a
15
+ * transient "starting…"/"decomposing…" hint until a subsequent fetch
16
+ * (immediate refresh or the next 10s poll) reflects the new status.
17
+ *
18
+ * API (NOT org-scoped in the path — org/user scoping comes from the JWT):
19
+ * GET /api/objectives (service: tasks) -> data.objectives
20
+ * GET /api/objectives/:id (service: tasks) -> objective detail
21
+ * POST /api/objectives/:id/approve-plan (service: tasks)
22
+ * POST /api/objectives/:id/start (service: tasks)
23
+ */
24
+
25
+ import React, { useState, useCallback, useEffect } from 'react';
26
+ import { Box, Text, useInput } from 'ink';
27
+ import { Layout } from '../components/Layout.jsx';
28
+ import { DataTable } from '../components/DataTable.jsx';
29
+ import { DetailPanel } from '../components/DetailPanel.jsx';
30
+ import { usePolledApi } from '../hooks/usePolledApi.js';
31
+ import { useTui } from '../app.jsx';
32
+ import { matchKey } from '../keys.js';
33
+ import { colors, styles } from '../theme.js';
34
+ import { resolveEndpoint } from '../../api/endpoints.js';
35
+
36
+ // ── Helpers ───────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Safely extract the org ID from TUI context.
40
+ */
41
+ function getOrgId(ctx) {
42
+ return ctx?.auth?.getUser?.()?.org_id
43
+ || ctx?.config?.getActiveProfile?.()?.auth?.org_id
44
+ || null;
45
+ }
46
+
47
+ // ── Column definitions ────────────────────────────────────────────────
48
+ const COLUMNS = [
49
+ { key: 'title', label: 'Title', width: 24 },
50
+ { key: 'status', label: 'Status', width: 14 },
51
+ { key: 'progress', label: 'Progress', width: 10 },
52
+ ];
53
+
54
+ /**
55
+ * Format raw objective data into table rows.
56
+ * `pendingAction` (if present) is `{ id, hint }` for an in-flight
57
+ * start/approve action whose effect hasn't shown up in `objectives` yet.
58
+ */
59
+ function formatRows(objectives, pendingAction) {
60
+ if (!Array.isArray(objectives)) return [];
61
+ return objectives.map((o) => {
62
+ const isPending = pendingAction && pendingAction.id === o.id;
63
+ const statusText = `${styles.statusDot(o.status || 'unknown')} ${o.status || 'unknown'}`;
64
+ return {
65
+ ...o,
66
+ title: o.title || o.name || o.id,
67
+ status: isPending ? `${statusText} (${pendingAction.hint})` : statusText,
68
+ progress: (o.tasks_done != null || o.tasks_total != null)
69
+ ? `${o.tasks_done || 0}/${o.tasks_total || 0}`
70
+ : '--',
71
+ _raw: o,
72
+ };
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Build a detail object for the right panel.
78
+ * `pendingAction` (if present and matching this objective) appends the
79
+ * transient "starting…"/"decomposing…" hint to the full-width Status line
80
+ * (the DataTable row's Status column is too narrow to show it in full).
81
+ */
82
+ function buildDetail(raw, pendingAction) {
83
+ if (!raw) return null;
84
+ const isPending = pendingAction && pendingAction.id === raw.id;
85
+ return {
86
+ 'Title': raw.title || raw.name,
87
+ 'ID': raw.id,
88
+ 'Status': isPending ? `${raw.status} (${pendingAction.hint})` : raw.status,
89
+ 'Description': raw.description,
90
+ 'Team': raw.team_id,
91
+ 'Progress': (raw.tasks_done != null || raw.tasks_total != null)
92
+ ? `${raw.tasks_done || 0}/${raw.tasks_total || 0}`
93
+ : '--',
94
+ 'Created': raw.created_at,
95
+ 'Updated': raw.updated_at,
96
+ };
97
+ }
98
+
99
+ // ── Component ─────────────────────────────────────────────────────────
100
+ export function ObjectivesView() {
101
+ const ctx = useTui();
102
+ const { client, screen, showToast, orgVersion } = ctx;
103
+ const isActive = screen === 'objectives';
104
+ const orgId = getOrgId(ctx);
105
+
106
+ const [selected, setSelected] = useState(0);
107
+ const [filter, setFilter] = useState(undefined);
108
+ // In-flight start/approve action: { id, previousStatus, hint }. Cleared
109
+ // once a fetch shows the objective's status has actually moved on, or
110
+ // after a safety timeout if it hasn't.
111
+ const [pendingAction, setPendingAction] = useState(null);
112
+
113
+ // ── Data fetching (auto-refreshes every 10s, only while active) ─────
114
+ const { data, loading, error, refresh } = usePolledApi(
115
+ async () => {
116
+ const res = await client.get(resolveEndpoint('objectives.list').path, { service: resolveEndpoint('objectives.list').service, query: { status: filter } });
117
+ return res?.objectives || res?.items || (Array.isArray(res) ? res : []);
118
+ },
119
+ [client, orgVersion, filter],
120
+ { intervalMs: 10000, active: isActive }
121
+ );
122
+
123
+ const rows = formatRows(data, pendingAction);
124
+ const selectedRow = rows[selected];
125
+ const detail = selectedRow ? buildDetail(selectedRow._raw, pendingAction) : null;
126
+
127
+ // ── Clear the pending hint once the objective's status has moved on ──
128
+ useEffect(() => {
129
+ if (!pendingAction || !Array.isArray(data)) return;
130
+ const current = data.find((o) => o.id === pendingAction.id);
131
+ if (current && current.status !== pendingAction.previousStatus) {
132
+ setPendingAction(null);
133
+ }
134
+ }, [data, pendingAction]);
135
+
136
+ // Safety timeout: never leave the hint stuck if status genuinely hasn't
137
+ // changed by the time it fires (e.g. decomposition takes longer than this).
138
+ useEffect(() => {
139
+ if (!pendingAction) return;
140
+ const t = setTimeout(() => setPendingAction(null), 15000);
141
+ return () => clearTimeout(t);
142
+ }, [pendingAction]);
143
+
144
+ // ── Approve plan ──────────────────────────────────────────────────
145
+ const approvePlan = useCallback(async () => {
146
+ if (!selectedRow) return;
147
+ const id = selectedRow._raw.id;
148
+ try {
149
+ await client.post(resolveEndpoint('objectives.approve-plan', { id }).path, { service: resolveEndpoint('objectives.approve-plan', { id }).service });
150
+ showToast('Objective started', 'success');
151
+ setPendingAction({ id, previousStatus: selectedRow._raw.status, hint: 'starting…' });
152
+ refresh();
153
+ } catch (err) {
154
+ showToast(err.message, 'error');
155
+ }
156
+ }, [selectedRow, client, refresh, showToast]);
157
+
158
+ // ── Start objective ───────────────────────────────────────────────
159
+ const startObjective = useCallback(async () => {
160
+ if (!selectedRow) return;
161
+ const id = selectedRow._raw.id;
162
+ try {
163
+ await client.post(resolveEndpoint('objectives.start', { id }).path, { service: resolveEndpoint('objectives.start', { id }).service, body: {} });
164
+ showToast('Objective started', 'success');
165
+ setPendingAction({ id, previousStatus: selectedRow._raw.status, hint: 'decomposing…' });
166
+ refresh();
167
+ } catch (err) {
168
+ showToast(err.message, 'error');
169
+ }
170
+ }, [selectedRow, client, refresh, showToast]);
171
+
172
+ // ── Keyboard handling ──────────────────────────────────────────────
173
+ useInput((input, key) => {
174
+ if (!isActive) return;
175
+
176
+ // Manual refresh
177
+ if (matchKey('refresh', input, key)) {
178
+ refresh();
179
+ return;
180
+ }
181
+
182
+ // Approve plan
183
+ if (input === 'a') {
184
+ approvePlan();
185
+ return;
186
+ }
187
+
188
+ // Start objective
189
+ if (input === 's') {
190
+ startObjective();
191
+ return;
192
+ }
193
+ }, { isActive });
194
+
195
+ // ── Footer hint ────────────────────────────────────────────────────
196
+ const footer = 'a: approve s: start r: refresh Esc: back';
197
+
198
+ // ── Render ─────────────────────────────────────────────────────────
199
+ return (
200
+ <Layout
201
+ title="Objectives"
202
+ subtitle={orgId ? `org: ${orgId}` : '(no org)'}
203
+ footer={footer}
204
+ left={
205
+ <Box flexDirection="column">
206
+ {error && (
207
+ <Box marginBottom={1}>
208
+ <Text color={colors.error}>Error: {error.message}</Text>
209
+ </Box>
210
+ )}
211
+ <DataTable
212
+ columns={COLUMNS}
213
+ rows={rows}
214
+ selected={selected}
215
+ onSelectionChange={setSelected}
216
+ loading={loading}
217
+ emptyMessage="No objectives yet — press s to start one, or create via `quox objective create`"
218
+ active={isActive}
219
+ />
220
+ </Box>
221
+ }
222
+ right={
223
+ <DetailPanel
224
+ title={selectedRow ? (selectedRow._raw.title || selectedRow._raw.id) : undefined}
225
+ data={detail}
226
+ />
227
+ }
228
+ />
229
+ );
230
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * OrgView — Organization management view
3
+ *
4
+ * Left panel: DataTable of orgs (name, slug, members count)
5
+ * Right panel: DetailPanel with org detail (owner, plan, created)
6
+ *
7
+ * Features:
8
+ * - `s` key switches active org (updates config)
9
+ * - Shows current org with a marker in the table
10
+ *
11
+ * API:
12
+ * GET /api/orgs (service: auth) -> data.orgs or data.organizations
13
+ */
14
+
15
+ import React, { useState, useCallback } from 'react';
16
+ import { Box, Text, useInput } from 'ink';
17
+ import { Layout } from '../components/Layout.jsx';
18
+ import { DataTable } from '../components/DataTable.jsx';
19
+ import { DetailPanel } from '../components/DetailPanel.jsx';
20
+ import { useApi } from '../hooks/useApi.js';
21
+ import { useTui } from '../app.jsx';
22
+ import { matchKey } from '../keys.js';
23
+ import { colors } from '../theme.js';
24
+ import { resolveEndpoint } from '../../api/endpoints.js';
25
+
26
+ // ── Helpers ───────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Safely extract the current org ID from TUI context.
30
+ */
31
+ function getOrgId(ctx) {
32
+ return ctx?.auth?.getUser?.()?.org_id
33
+ || ctx?.config?.getActiveProfile?.()?.auth?.org_id
34
+ || null;
35
+ }
36
+
37
+ // ── Column definitions ────────────────────────────────────────────────
38
+ const COLUMNS = [
39
+ { key: 'marker', label: ' ', width: 3 },
40
+ { key: 'name', label: 'Name', width: 22 },
41
+ { key: 'slug', label: 'Slug', width: 16 },
42
+ { key: 'members', label: 'Members', width: 10 },
43
+ ];
44
+
45
+ /**
46
+ * Format raw org data into table rows.
47
+ * Marks the currently active org with a star.
48
+ */
49
+ function formatRows(orgs, currentOrgId) {
50
+ if (!Array.isArray(orgs)) return [];
51
+ return orgs.map((o) => {
52
+ const id = o.id || o.org_id;
53
+ const isCurrent = id === currentOrgId;
54
+ return {
55
+ marker: isCurrent ? '\u2605' : ' ',
56
+ name: o.name || o.display_name || id,
57
+ slug: o.slug || '--',
58
+ members: o.member_count != null
59
+ ? String(o.member_count)
60
+ : (Array.isArray(o.members) ? String(o.members.length) : '--'),
61
+ _raw: o,
62
+ _isCurrent: isCurrent,
63
+ };
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Build a detail object for the right panel.
69
+ */
70
+ function buildDetail(raw, isCurrent) {
71
+ if (!raw) return null;
72
+ return {
73
+ 'Name': raw.name || raw.display_name,
74
+ 'ID': raw.id || raw.org_id,
75
+ 'Slug': raw.slug,
76
+ 'Owner': raw.owner || raw.owner_email || raw.owner_id,
77
+ 'Plan': raw.plan || raw.subscription || '--',
78
+ 'Members': raw.member_count != null
79
+ ? raw.member_count
80
+ : (Array.isArray(raw.members) ? raw.members.length : '--'),
81
+ 'Active Org': isCurrent,
82
+ 'Created': raw.created_at,
83
+ 'Updated': raw.updated_at,
84
+ 'Description': raw.description,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Merge an `auth.switch-org` response into the stored auth blob — the shared
90
+ * contract with `quox org switch`. The response carries the new org's info and
91
+ * (critically) an org-scoped access_token; persisting only a local org_id
92
+ * without this token leaves the OLD org's token in place. Exported for tests.
93
+ */
94
+ export function mergeSwitchedOrgAuth(prevAuth, data, fallbackOrgId) {
95
+ const authData = { ...(prevAuth || {}) };
96
+ if (data?.org) {
97
+ authData.org_id = data.org.id;
98
+ authData.org_name = data.org.name;
99
+ authData.org_slug = data.org.slug;
100
+ authData.org_role = data.role;
101
+ } else {
102
+ authData.org_id = fallbackOrgId;
103
+ }
104
+ if (data?.access_token) authData.access_token = data.access_token;
105
+ return authData;
106
+ }
107
+
108
+ // ── Component ─────────────────────────────────────────────────────────
109
+ export function OrgView() {
110
+ const ctx = useTui();
111
+ const { client, config, screen, onOrgChange } = ctx;
112
+ const isActive = screen === 'orgs';
113
+ const currentOrgId = getOrgId(ctx);
114
+
115
+ const [selected, setSelected] = useState(0);
116
+ const [actionMsg, setActionMsg] = useState(null);
117
+
118
+ // ── Data fetching ───────────────────────────────────────────────────
119
+ const { data, loading, error, refresh } = useApi(
120
+ async () => {
121
+ const res = await client.get(resolveEndpoint('orgs.list').path, { service: resolveEndpoint('orgs.list').service });
122
+ return res?.orgs || res?.organizations || [];
123
+ },
124
+ [client]
125
+ );
126
+
127
+ const rows = formatRows(data, currentOrgId);
128
+ const selectedRow = rows[selected];
129
+ const detail = selectedRow
130
+ ? buildDetail(selectedRow._raw, selectedRow._isCurrent)
131
+ : null;
132
+
133
+ // ── Switch active org ──────────────────────────────────────────────
134
+ const switchOrg = useCallback(async () => {
135
+ if (!selectedRow) return;
136
+ const raw = selectedRow._raw;
137
+ const newOrgId = raw.id || raw.org_id;
138
+
139
+ if (newOrgId === currentOrgId) {
140
+ setActionMsg('Already the active org');
141
+ setTimeout(() => setActionMsg(null), 2000);
142
+ return;
143
+ }
144
+
145
+ setActionMsg('Switching…');
146
+ try {
147
+ // Mirror `quox org switch`: POST switch-org to obtain the org-scoped
148
+ // access_token + canonical org info, then persist BOTH. The old code
149
+ // only poked a local org_id, leaving the previous org's token in place
150
+ // so every subsequent refetch hit the new org with the wrong token.
151
+ const ep = resolveEndpoint('auth.switch-org');
152
+ const data = await client.post(ep.path, { service: ep.service, body: { org_id: newOrgId } });
153
+ const nextAuth = mergeSwitchedOrgAuth(config?.getAuth?.(), data, newOrgId);
154
+ config?.setAuth?.(nextAuth);
155
+
156
+ setActionMsg(`Switched to ${data?.org?.name || raw.name || newOrgId}`);
157
+ refresh();
158
+ // Notify org-scoped views so they refetch against the new org.
159
+ onOrgChange?.();
160
+ } catch (err) {
161
+ setActionMsg(`Failed to switch: ${err.message}`);
162
+ }
163
+
164
+ setTimeout(() => setActionMsg(null), 3000);
165
+ }, [selectedRow, currentOrgId, client, config, refresh, onOrgChange]);
166
+
167
+ // ── Keyboard handling ──────────────────────────────────────────────
168
+ useInput((input, key) => {
169
+ if (!isActive) return;
170
+
171
+ // Switch org
172
+ if (input === 's') {
173
+ switchOrg();
174
+ return;
175
+ }
176
+
177
+ // Manual refresh
178
+ if (matchKey('refresh', input, key)) {
179
+ refresh();
180
+ return;
181
+ }
182
+ }, { isActive });
183
+
184
+ // ── Footer hint ────────────────────────────────────────────────────
185
+ const footer = [
186
+ '\u2191/k \u2193/j Navigate',
187
+ 's Switch org',
188
+ 'r Refresh',
189
+ 'Esc Back',
190
+ ].join(' ');
191
+
192
+ // ── Render ─────────────────────────────────────────────────────────
193
+ return (
194
+ <Layout
195
+ title="Organizations"
196
+ subtitle={currentOrgId ? `active: ${currentOrgId}` : '(none active)'}
197
+ footer={footer}
198
+ left={
199
+ <Box flexDirection="column">
200
+ {error && (
201
+ <Box marginBottom={1}>
202
+ <Text color={colors.error}>Error: {error.message}</Text>
203
+ </Box>
204
+ )}
205
+ {actionMsg && (
206
+ <Box marginBottom={1}>
207
+ <Text color={colors.info}>{actionMsg}</Text>
208
+ </Box>
209
+ )}
210
+ <DataTable
211
+ columns={COLUMNS}
212
+ rows={rows}
213
+ selected={selected}
214
+ onSelectionChange={setSelected}
215
+ loading={loading}
216
+ emptyMessage="No organizations — create one via `quox org create`"
217
+ active={isActive}
218
+ />
219
+ </Box>
220
+ }
221
+ right={
222
+ <DetailPanel
223
+ title={selectedRow ? (selectedRow._raw.name || 'Organization') : undefined}
224
+ data={detail}
225
+ />
226
+ }
227
+ />
228
+ );
229
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * ToolView — Tool catalog browser
3
+ *
4
+ * Left panel: DataTable of tools (name, type, category)
5
+ * Right panel: DetailPanel with tool description and parameters
6
+ *
7
+ * API (org-scoped):
8
+ * GET /api/orgs/:orgId/tools (service: auth) -> data.tools
9
+ * Tool detail comes from the list data itself (no separate endpoint needed).
10
+ */
11
+
12
+ import React, { useState, useEffect } from 'react';
13
+ import { Box, Text, useInput } from 'ink';
14
+ import { Layout } from '../components/Layout.jsx';
15
+ import { DataTable } from '../components/DataTable.jsx';
16
+ import { DetailPanel } from '../components/DetailPanel.jsx';
17
+ import { SearchInput } from '../components/SearchInput.jsx';
18
+ import { useApi } from '../hooks/useApi.js';
19
+ import { useTui } from '../app.jsx';
20
+ import { matchKey } from '../keys.js';
21
+ import { colors } from '../theme.js';
22
+ import { resolveEndpoint } from '../../api/endpoints.js';
23
+
24
+ // ── Helpers ───────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Safely extract the org ID from TUI context.
28
+ */
29
+ function getOrgId(ctx) {
30
+ return ctx?.auth?.getUser?.()?.org_id
31
+ || ctx?.config?.getActiveProfile?.()?.auth?.org_id
32
+ || null;
33
+ }
34
+
35
+ // ── Column definitions ────────────────────────────────────────────────
36
+ const COLUMNS = [
37
+ { key: 'name', label: 'Name', width: 22 },
38
+ { key: 'type', label: 'Type', width: 14 },
39
+ { key: 'category', label: 'Category', width: 14 },
40
+ ];
41
+
42
+ /**
43
+ * Format raw tool data into table rows.
44
+ */
45
+ function formatRows(tools) {
46
+ if (!Array.isArray(tools)) return [];
47
+ return tools.map((t) => ({
48
+ name: t.name || t.display_name || t.id,
49
+ type: t.type || '--',
50
+ category: t.category || '--',
51
+ _raw: t,
52
+ }));
53
+ }
54
+
55
+ /**
56
+ * Build a detail object for the right panel.
57
+ */
58
+ function buildDetail(raw) {
59
+ if (!raw) return null;
60
+ const detail = {
61
+ 'Name': raw.name || raw.display_name,
62
+ 'ID': raw.id,
63
+ 'Type': raw.type,
64
+ 'Category': raw.category,
65
+ 'Description': raw.description,
66
+ 'Version': raw.version,
67
+ 'Enabled': raw.enabled != null ? raw.enabled : raw.active,
68
+ 'Created': raw.created_at,
69
+ };
70
+
71
+ // Parameters
72
+ if (raw.parameters || raw.input_schema) {
73
+ const params = raw.parameters || raw.input_schema;
74
+ if (typeof params === 'object') {
75
+ detail['Parameters'] = params;
76
+ }
77
+ }
78
+
79
+ // Tags
80
+ if (Array.isArray(raw.tags) && raw.tags.length > 0) {
81
+ detail['Tags'] = raw.tags;
82
+ }
83
+
84
+ return detail;
85
+ }
86
+
87
+ /**
88
+ * Filter rows by search term (matches name, type, category).
89
+ */
90
+ function filterRows(rows, search) {
91
+ if (!search) return rows;
92
+ const term = search.toLowerCase();
93
+ return rows.filter((r) =>
94
+ (r.name || '').toLowerCase().includes(term) ||
95
+ (r.type || '').toLowerCase().includes(term) ||
96
+ (r.category || '').toLowerCase().includes(term)
97
+ );
98
+ }
99
+
100
+ // ── Component ─────────────────────────────────────────────────────────
101
+ export function ToolView() {
102
+ const ctx = useTui();
103
+ const { client, screen, setInputMode, orgVersion } = ctx;
104
+ const isActive = screen === 'tools';
105
+ const orgId = getOrgId(ctx);
106
+
107
+ const [selected, setSelected] = useState(0);
108
+ const [search, setSearch] = useState('');
109
+ const [searchActive, setSearchActive] = useState(false);
110
+
111
+ // ── Input-mode registry: claim 'input' while the search box is
112
+ // focused so global shortcuts (e.g. '?') don't steal keystrokes.
113
+ useEffect(() => {
114
+ if (searchActive) setInputMode?.('input');
115
+ else if (isActive) setInputMode?.('nav');
116
+ }, [searchActive, isActive, setInputMode]);
117
+
118
+ // ── Data fetching (refetches when the active org changes) ───────────
119
+ const { data, loading, error, refresh } = useApi(
120
+ async () => {
121
+ if (!orgId) return [];
122
+ const res = await client.get(resolveEndpoint('tools.list', { orgId }).path, { service: resolveEndpoint('tools.list', { orgId }).service });
123
+ return res?.tools || [];
124
+ },
125
+ [client, orgId, orgVersion]
126
+ );
127
+
128
+ const allRows = formatRows(data);
129
+ const rows = filterRows(allRows, search);
130
+ const selectedRow = rows[selected];
131
+ const detail = selectedRow ? buildDetail(selectedRow._raw) : null;
132
+
133
+ // ── Keyboard handling ──────────────────────────────────────────────
134
+ useInput((input, key) => {
135
+ if (!isActive) return;
136
+
137
+ // Toggle search focus
138
+ if (matchKey('search', input, key) && !searchActive) {
139
+ setSearchActive(true);
140
+ return;
141
+ }
142
+
143
+ // Escape from search
144
+ if (searchActive && matchKey('back', input, key)) {
145
+ setSearchActive(false);
146
+ return;
147
+ }
148
+
149
+ if (searchActive) return; // Let SearchInput handle keys
150
+
151
+ // Manual refresh
152
+ if (matchKey('refresh', input, key)) {
153
+ refresh();
154
+ return;
155
+ }
156
+ }, { isActive });
157
+
158
+ // ── Reset selection when search changes ────────────────────────────
159
+ const handleSearchChange = (val) => {
160
+ setSearch(val);
161
+ setSelected(0);
162
+ };
163
+
164
+ // ── Footer hint ────────────────────────────────────────────────────
165
+ const footer = searchActive
166
+ ? 'Type to filter Esc Cancel search'
167
+ : [
168
+ '\u2191/k \u2193/j Navigate',
169
+ '/ Search',
170
+ 'r Refresh',
171
+ 'Esc Back',
172
+ ].join(' ');
173
+
174
+ // ── Render ─────────────────────────────────────────────────────────
175
+ return (
176
+ <Layout
177
+ title="Tools"
178
+ subtitle={orgId ? `${rows.length} tools` : '(no org)'}
179
+ footer={footer}
180
+ left={
181
+ <Box flexDirection="column">
182
+ {/* Search bar */}
183
+ <Box marginBottom={1}>
184
+ <SearchInput
185
+ placeholder="Filter tools..."
186
+ value={search}
187
+ onChange={handleSearchChange}
188
+ active={searchActive}
189
+ />
190
+ </Box>
191
+
192
+ {error && (
193
+ <Box marginBottom={1}>
194
+ <Text color={colors.error}>Error: {error.message}</Text>
195
+ </Box>
196
+ )}
197
+ {!orgId && !loading && (
198
+ <Box marginBottom={1}>
199
+ <Text color={colors.warning}>No organization selected.</Text>
200
+ </Box>
201
+ )}
202
+ <DataTable
203
+ columns={COLUMNS}
204
+ rows={rows}
205
+ selected={selected}
206
+ onSelectionChange={setSelected}
207
+ loading={loading}
208
+ emptyMessage={search ? `No tools match "${search}" — press Esc to clear the filter` : 'No tools installed — add one via `quox tool create` or `quox tool install`'}
209
+ active={isActive && !searchActive}
210
+ />
211
+ </Box>
212
+ }
213
+ right={
214
+ <DetailPanel
215
+ title={selectedRow ? (selectedRow._raw.name || selectedRow._raw.id) : undefined}
216
+ data={detail}
217
+ />
218
+ }
219
+ />
220
+ );
221
+ }