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,163 @@
1
+ /**
2
+ * Agent display-name alias resolution — DEVSUITE-GRIPE-P7 / quox#567 (CLI
3
+ * half only; server-side alias resolution stays open in #567).
4
+ *
5
+ * `quox agent attach vulcan` failed because the registry id is `forge` —
6
+ * "VULCAN" is only the agent's display name (identity.name in
7
+ * src/config/entities/assistants/forge.json), never sent to any API. This
8
+ * module resolves a non-matching agent id case-insensitively against the
9
+ * core assistants roster BEFORE the attach/detach/attach-status/
10
+ * attach-heartbeat/exec/grant-attach commands call the real API, so the
11
+ * canonical registry id is always what goes over the wire.
12
+ *
13
+ * Two sources, combined:
14
+ * - the collector's no-auth-gate `/api/v1/tools/agents` route, which is
15
+ * the live, authoritative list of agent ids this deployment currently
16
+ * recognises (but carries NO display names — it's a tool-permission
17
+ * map keyed by id);
18
+ * - the bundled assistants roster (src/config/entities/assistants/*.json,
19
+ * shipped inside this same repo checkout) for identity.name /
20
+ * identity.shortName, the only source of display names.
21
+ * A live-roster failure (network down, route not deployed yet) degrades to
22
+ * the bundled roster's own id set rather than blocking alias resolution
23
+ * entirely.
24
+ */
25
+
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { resolveEndpoint } from '../api/endpoints.js';
30
+
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+
33
+ // cli/lib/core/agentAlias.js -> cli/lib -> cli -> repo root -> src/config/...
34
+ const DEFAULT_ASSISTANTS_DIR = path.resolve(__dirname, '../../../src/config/entities/assistants');
35
+
36
+ /**
37
+ * Read the bundled assistants roster. Tolerant of a missing directory (a
38
+ * standalone CLI install without the dashboard source tree alongside it)
39
+ * and of individual malformed files — one bad entry must never break alias
40
+ * resolution for every other agent.
41
+ *
42
+ * @returns {Array<{id: string, name: string|null, shortName: string|null}>}
43
+ */
44
+ export function loadBundledAssistantsRoster(dir = DEFAULT_ASSISTANTS_DIR) {
45
+ let files;
46
+ try {
47
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
48
+ } catch {
49
+ return [];
50
+ }
51
+
52
+ const roster = [];
53
+ for (const file of files) {
54
+ try {
55
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
56
+ if (raw?.id) {
57
+ roster.push({
58
+ id: raw.id,
59
+ name: raw.identity?.name || null,
60
+ shortName: raw.identity?.shortName || null,
61
+ });
62
+ }
63
+ } catch {
64
+ // Skip an unreadable/malformed file rather than failing the whole load.
65
+ }
66
+ }
67
+ return roster;
68
+ }
69
+
70
+ /**
71
+ * Pure resolver, exported for unit tests: no filesystem or network access.
72
+ *
73
+ * @param {string} requestedId - what the user typed
74
+ * @param {Set<string>} validIds - agent ids this deployment currently recognises
75
+ * @param {Array<{id: string, name: string|null, shortName: string|null}>} roster
76
+ * @returns {{id: string, aliasLabel: string|null}} aliasLabel is the display
77
+ * name to print (`alias: <aliasLabel> -> <id>`), or null when the input
78
+ * was already a valid id (no alias happened).
79
+ */
80
+ export function resolveAgentAliasPure(requestedId, validIds, roster) {
81
+ if (!requestedId || typeof requestedId !== 'string') {
82
+ throw new Error('Agent id is required.');
83
+ }
84
+
85
+ // Exact, case-sensitive match against a known id: pass through untouched.
86
+ // This is the common case and must never be affected by alias lookup.
87
+ if (validIds.has(requestedId)) {
88
+ return { id: requestedId, aliasLabel: null };
89
+ }
90
+
91
+ const lower = requestedId.toLowerCase();
92
+ for (const entry of roster) {
93
+ // Never alias to an id this deployment doesn't currently recognise.
94
+ if (!validIds.has(entry.id)) continue;
95
+ const name = entry.name?.toLowerCase();
96
+ const shortName = entry.shortName?.toLowerCase();
97
+ if (name === lower || shortName === lower) {
98
+ return { id: entry.id, aliasLabel: entry.name || entry.shortName || requestedId.toUpperCase() };
99
+ }
100
+ }
101
+
102
+ throw new Error(
103
+ `Unknown agent "${requestedId}": not a registry id, and no matching display name in the assistants roster. `
104
+ + 'Run `quox agent list` to see valid ids.'
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Resolve a possibly-display-name agent id against the live collector
110
+ * roster plus the bundled assistants roster, printing exactly one
111
+ * `alias: <NAME> -> <id>` line on a hit. Callers must use the RETURNED
112
+ * canonical id for every subsequent call — the display name is never sent
113
+ * to the API.
114
+ *
115
+ * Printed unconditionally (never suppressed under -o json/quiet, unlike
116
+ * printInfo): a silent semantic remap of what agent id you are talking to
117
+ * is exactly the kind of thing a scripted/JSON caller most needs to see.
118
+ *
119
+ * @param {object} client - the CLI's HTTP client (ctx.client)
120
+ * @param {string} requestedId
121
+ * @param {{print?: (line: string) => void, rosterDir?: string}} [opts]
122
+ * @returns {Promise<string>} the canonical agent id
123
+ */
124
+ export async function resolveAgentId(client, requestedId, opts = {}) {
125
+ const print = opts.print || ((line) => console.log(line));
126
+ const roster = loadBundledAssistantsRoster(opts.rosterDir);
127
+
128
+ // CORTEX-BENCH: an exact core id resolves to itself whatever the live list
129
+ // says (absent from it, it passes through as a custom id and the server
130
+ // decides), so skip the round trip that cost every exec 60ms-1.4s.
131
+ if (roster.some((r) => r.id === requestedId)) return requestedId;
132
+
133
+ let validIds = null;
134
+ try {
135
+ const ep = resolveEndpoint('v1.tools.agents.list');
136
+ const data = await client.get(ep.path, { service: ep.service, noAuth: true });
137
+ const ids = Object.keys(data?.agentTools || {});
138
+ if (ids.length > 0) validIds = new Set(ids);
139
+ } catch {
140
+ // Live lookup is an optimisation, not a hard requirement -- fall
141
+ // through to the bundled roster's own id set below.
142
+ }
143
+ if (!validIds) validIds = new Set(roster.map((r) => r.id));
144
+
145
+ let resolved;
146
+ try {
147
+ resolved = resolveAgentAliasPure(requestedId, validIds, roster);
148
+ } catch (err) {
149
+ // A non-empty id that matches neither a core-registry id nor a display
150
+ // name may be a user/org-created custom assistant (auth `assistants`
151
+ // table) — the core roster this resolver knows never lists those.
152
+ // QUOXIDENTITY custom-attach (CLI half): the client must not gate an id
153
+ // the server can now resolve. Pass it through and let the server be the
154
+ // authority — the attach-kit endpoint resolves a custom assistant
155
+ // org-scoped, or returns a clean 404 for a genuine typo. (An empty id
156
+ // still throws the original 'Agent id is required.' error.)
157
+ if (!requestedId || typeof requestedId !== 'string') throw err;
158
+ print(`note: "${requestedId}" is not a core registry agent, trying it as a custom/org assistant (server resolves or rejects).`);
159
+ return requestedId;
160
+ }
161
+ if (resolved.aliasLabel) print(`alias: ${resolved.aliasLabel} -> ${resolved.id}`);
162
+ return resolved.id;
163
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * HITL-CONTEXT (owner 2026-10-07): render an approval item's
3
+ * payload.approval_context (built server-side by the collector at request
4
+ * time) for `quox inbox list` / `quox inbox show`. Pure, so it is testable.
5
+ */
6
+
7
+ function clip(text, n) {
8
+ const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
9
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
10
+ }
11
+
12
+ export function getApprovalContext(item) {
13
+ const c = item && item.payload && item.payload.approval_context;
14
+ return c && typeof c === 'object' ? c : null;
15
+ }
16
+
17
+ /** Ordered [label, value] pairs: Source, On behalf of, Reason, Method, Command, Why approval, Risk, Expires. */
18
+ export function approvalContextFields(c) {
19
+ if (!c) return [];
20
+ const rows = [];
21
+ const rb = c.requested_by || {};
22
+ let source = c.source_label || rb.agent_label || 'unknown';
23
+ if (rb.profile) source += ` · profile ${rb.profile}`;
24
+ rows.push(['Source', source]);
25
+ if (c.on_behalf_of) {
26
+ const ob = c.on_behalf_of;
27
+ let v = ob.user_name || ob.user_id || 'unknown user';
28
+ if (ob.request_excerpt) v += `, asked: "${ob.request_excerpt}"`;
29
+ if (ob.conversation_id) v += ` (conversation ${ob.conversation_id})`;
30
+ rows.push(['On behalf of', v]);
31
+ }
32
+ const r = c.reason || {};
33
+ rows.push(['Reason', r.text ? `${r.text}${r.source === 'inferred' ? ' (inferred, not stated by the caller)' : ''}` : 'none given']);
34
+ const m = c.method || {};
35
+ rows.push(['Method', `${m.tool || '?'} via ${m.transport || '?'}${m.target ? ` on ${m.target}` : ''}`]);
36
+ if (m.command) rows.push(['Command', m.command]);
37
+ else if (m.args) rows.push(['Args', m.args]);
38
+ if (c.why_approval && c.why_approval.label) rows.push(['Why approval', c.why_approval.label]);
39
+ if (c.risk) rows.push(['Risk', c.risk]);
40
+ if (c.expires_at) rows.push(['Expires', c.expires_at]);
41
+ return rows;
42
+ }
43
+
44
+ /** Plain-text block for `quox inbox show`. */
45
+ export function formatApprovalContext(c) {
46
+ const rows = approvalContextFields(c);
47
+ if (rows.length === 0) return '';
48
+ const w = Math.max(...rows.map(([k]) => k.length));
49
+ return ['Approval context', ...rows.map(([k, v]) => ` ${k.padEnd(w)} ${v}`)].join('\n');
50
+ }
51
+
52
+ /** Short columns for `quox inbox list`. */
53
+ export function approvalListColumns(item) {
54
+ const c = getApprovalContext(item);
55
+ if (!c) return { source: '', reason: '' };
56
+ const r = c.reason || {};
57
+ return {
58
+ source: clip(c.source_label || '', 40),
59
+ reason: r.text ? clip(`${r.text}${r.source === 'inferred' ? ' (inferred)' : ''}`, 60) : '',
60
+ };
61
+ }
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Auth module for QuoxCLI
3
+ *
4
+ * Handles login, logout, token storage, and auto-refresh.
5
+ * Tokens stored in config profile's auth field.
6
+ */
7
+
8
+ import { QuoxAuthError, QuoxApiError, QuoxTransientAuthError } from './errors.js';
9
+ import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from './residentAuth.js';
10
+
11
+ // A resident agent has no refresh token: its expired-access-token seam is
12
+ // re-running the signed challenge exchange, not demanding `quox login`,
13
+ // since there is no human at the terminal to run it. Kept as a standalone helper
14
+ // so createAuth()'s refresh() stays the single seam client.js already calls
15
+ // on a 401, per quox#453 phase 15.
16
+ async function refreshResident(config, baseUrl, storedAuth) {
17
+ let profile;
18
+ try {
19
+ profile = loadResidentProfile(storedAuth.resident_profile_path);
20
+ } catch (err) {
21
+ throw new QuoxAuthError(`Resident re-authentication failed: ${err.message}`);
22
+ }
23
+
24
+ // The caller passes getServiceUrl('auth'), which can port-swap to
25
+ // loopback :3101 and be unreachable from a remote host, so the resident
26
+ // rules (bootstrap instance_url / explicit profile URL) come FIRST. But
27
+ // edge-only was its own outage (2026-08-23, both residents): on the
28
+ // control box the edge is self-signed https, the systemd unit has no CA
29
+ // trust, and the one URL that does work there is exactly the
30
+ // services-aware one the caller passed (plain-http loopback auth). Try
31
+ // the resident-rules URL, then fall back to the caller's, and only die
32
+ // if both refuse. An unattended runner must exhaust its options before
33
+ // giving up: Option A persistence depends on this.
34
+ const candidates = [...new Set([resolveResidentBaseUrl(config, profile), baseUrl].filter(Boolean))];
35
+
36
+ let tokenData;
37
+ const failures = [];
38
+ const errs = [];
39
+ for (const candidate of candidates) {
40
+ try {
41
+ tokenData = await residentLogin({ baseUrl: candidate, profile });
42
+ break;
43
+ } catch (err) {
44
+ failures.push(`${candidate}: ${err.message}`);
45
+ errs.push(err);
46
+ }
47
+ }
48
+ if (!tokenData) {
49
+ // Every candidate failed for a reason that clears by itself (429, 5xx,
50
+ // connection refused/reset/timeout): surface a typed transient error so a
51
+ // long-lived listener can back off instead of dying.
52
+ const isTransient = (e) => e instanceof QuoxTransientAuthError
53
+ || (!(e instanceof QuoxAuthError) && /fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up|timed? ?out/i.test(`${e?.message} ${e?.cause?.code || ''} ${e?.cause?.message || ''}`));
54
+ if (errs.length > 0 && errs.every(isTransient)) {
55
+ const waits = errs.map((e) => e.retryAfterMs).filter((n) => Number.isFinite(n));
56
+ throw new QuoxTransientAuthError(`Resident re-authentication failed: ${failures.join('; ')}`, waits.length ? Math.max(...waits) : null);
57
+ }
58
+ throw new QuoxAuthError(`Resident re-authentication failed: ${failures.join('; ')}`);
59
+ }
60
+
61
+ const authData = {
62
+ ...storedAuth,
63
+ access_token: tokenData.access_token,
64
+ user_id: tokenData.user?.id ?? storedAuth.user_id,
65
+ email: tokenData.user?.email ?? storedAuth.email,
66
+ org_role: tokenData.user?.role ?? storedAuth.org_role,
67
+ actor_type: tokenData.user?.actor_type ?? storedAuth.actor_type,
68
+ host_id: tokenData.user?.host_id ?? storedAuth.host_id,
69
+ expires_at: Date.now() + (tokenData.expires_in || 900) * 1000,
70
+ };
71
+ config.setAuth(authData, { refresh: true });
72
+ return tokenData;
73
+ }
74
+
75
+ // When a password-backed session dead-ends (revoked or expired) the only
76
+ // recovery is an interactive `quox login`, which an unattended agent cannot
77
+ // run. A resident profile, if one is configured, IS that recovery: durable,
78
+ // passwordless, self-renewing. Build a one-line pointer to it, preferring a
79
+ // resident on the same org as the profile that just died. Returns '' when no
80
+ // resident profile exists (nothing useful to suggest). Never throws — a
81
+ // hint-builder must not become a new failure seam in the auth path.
82
+ function residentProfileHint(config, deadOrgId = null) {
83
+ try {
84
+ const residents = typeof config.listResidentProfiles === 'function'
85
+ ? config.listResidentProfiles()
86
+ : [];
87
+ if (!residents.length) return '';
88
+ const sameOrg = deadOrgId ? residents.find(r => r.orgId === deadOrgId) : null;
89
+ const pick = sameOrg || residents[0];
90
+ return ` For unattended automation use the durable resident profile instead: `
91
+ + `\`quox -p ${pick.name} <command>\` (Ed25519, no password, self-renews — never needs re-login).`;
92
+ } catch {
93
+ return '';
94
+ }
95
+ }
96
+
97
+ export function createAuth(config) {
98
+ return {
99
+ async login(baseUrl, email, password, orgId) {
100
+ const url = `${baseUrl}/api/auth/login`;
101
+ const body = { email, password };
102
+ if (orgId) body.org_id = orgId;
103
+
104
+ const res = await fetch(url, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json' },
107
+ body: JSON.stringify(body),
108
+ });
109
+
110
+ const data = await res.json();
111
+
112
+ if (!res.ok) {
113
+ throw new QuoxApiError(res.status, data.error, data.message || 'Login failed');
114
+ }
115
+
116
+ // Store auth data
117
+ const authData = {
118
+ access_token: data.access_token,
119
+ refresh_token: data.refresh_token,
120
+ user_id: data.user?.id,
121
+ email: data.user?.email,
122
+ display_name: data.user?.display_name,
123
+ org_id: data.org?.id || null,
124
+ org_name: data.org?.name || null,
125
+ org_slug: data.org?.slug || null,
126
+ org_role: data.org?.role || null,
127
+ mfa_required: data.mfa_required,
128
+ expires_at: Date.now() + (data.expires_in || 900) * 1000,
129
+ };
130
+
131
+ config.setAuth(authData);
132
+ return { ...data, authData };
133
+ },
134
+
135
+ async verifyMfa(baseUrl, token, code) {
136
+ const url = `${baseUrl}/api/mfa/verify`;
137
+ const res = await fetch(url, {
138
+ method: 'POST',
139
+ headers: {
140
+ 'Content-Type': 'application/json',
141
+ 'Authorization': `Bearer ${token}`,
142
+ },
143
+ body: JSON.stringify({ code }),
144
+ });
145
+
146
+ const data = await res.json();
147
+ if (!res.ok) {
148
+ throw new QuoxApiError(res.status, data.error, data.message || 'MFA verification failed');
149
+ }
150
+
151
+ // Update stored token if new one issued
152
+ if (data.access_token) {
153
+ const auth = config.getAuth();
154
+ auth.access_token = data.access_token;
155
+ auth.mfa_required = false;
156
+ if (data.expires_in) auth.expires_at = Date.now() + data.expires_in * 1000;
157
+ config.setAuth(auth);
158
+ }
159
+
160
+ return data;
161
+ },
162
+
163
+ async logout(baseUrl) {
164
+ const auth = config.getAuth();
165
+ if (auth?.access_token) {
166
+ try {
167
+ await fetch(`${baseUrl}/api/auth/logout`, {
168
+ method: 'POST',
169
+ headers: { 'Authorization': `Bearer ${auth.access_token}` },
170
+ });
171
+ } catch {
172
+ // Best-effort server-side logout
173
+ }
174
+ }
175
+ config.clearAuth();
176
+ },
177
+
178
+ async refresh(baseUrl) {
179
+ const auth = config.getAuth();
180
+
181
+ // Resident agents authenticate via a signed challenge, not a refresh
182
+ // token; see refreshResident() above.
183
+ if (auth?.kind === 'resident') {
184
+ return refreshResident(config, baseUrl, auth);
185
+ }
186
+
187
+ if (!auth?.refresh_token) {
188
+ throw new QuoxAuthError('No refresh token. Please login again.');
189
+ }
190
+
191
+ // The token goes in the BODY. POST /api/auth/refresh reads
192
+ // `req.cookies.refresh_token || req.body.refresh_token` and never looks
193
+ // at the Authorization header, so sending it only as a Bearer header
194
+ // meant the server saw no token at all: it answered `no_refresh_token`,
195
+ // /health answered OK, and the clearing branch below destroyed a
196
+ // perfectly good session. That is the "logged in, then Session expired
197
+ // twenty minutes later" outage, logged three separate times before
198
+ // anyone read the transport rather than the error text. Verified live
199
+ // 2026-08-24: header -> no_refresh_token, body -> invalid_refresh_token.
200
+ // The header is kept because it costs nothing and other deployments may
201
+ // read it.
202
+ const res = await fetch(`${baseUrl}/api/auth/refresh`, {
203
+ method: 'POST',
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ 'Authorization': `Bearer ${auth.refresh_token}`,
207
+ },
208
+ body: JSON.stringify({ refresh_token: auth.refresh_token }),
209
+ });
210
+
211
+ let data = null;
212
+ try { data = await res.json(); } catch { /* non-JSON error body */ }
213
+ if (!res.ok) {
214
+ // Only a DEFINITIVE rejection (401/403) means the refresh token is
215
+ // dead. A 5xx or gateway hiccup must NOT destroy the stored session:
216
+ // on 2026-08-15 a wiped profile here erased the only copy of a
217
+ // rejected refresh token before anyone could diagnose WHY the auth
218
+ // service rejected it.
219
+ // `no_refresh_token` means the REQUEST carried nothing, not that the
220
+ // stored token is dead. Clearing on it turns a client-side transport
221
+ // fault into a lost session, which is exactly how the header/body bug
222
+ // above cost three re-logins in one night before it was found.
223
+ if ((data && data.error) === 'no_refresh_token') {
224
+ throw new QuoxAuthError(
225
+ 'The auth service received no refresh token, so it could not tell whether yours is valid. '
226
+ + 'Stored session KEPT. This is a client/transport fault, not an expiry — retry, and report it if it persists.'
227
+ );
228
+ }
229
+
230
+ if (res.status === 401 || res.status === 403) {
231
+ // A 401 from refresh means one of TWO very different things, and
232
+ // destroying the session on the wrong one costs the user their login.
233
+ //
234
+ // (a) the refresh token really is dead -> clearing is correct
235
+ // (b) the auth service is not READY yet -> clearing is data loss
236
+ //
237
+ // 2026-08-19: rebuilding the auth container logged out every CLI
238
+ // session, twice in one evening, because a starting service answers
239
+ // 401 before it can answer properly. For an autonomous agent that is
240
+ // a hard stop mid-task; for a customer it is a surprise re-login
241
+ // after any routine restart. The old comment here already refused to
242
+ // clear on a 5xx for the same reason — this is the same hazard one
243
+ // status code over.
244
+ //
245
+ // So ask the service whether it is healthy before believing it about
246
+ // the token. Unreachable or unhealthy means we learned nothing about
247
+ // the token, and a session is far more expensive to lose than a
248
+ // command is to retry.
249
+ let healthy = false;
250
+ try {
251
+ const h = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(3000) });
252
+ healthy = h.ok;
253
+ } catch {
254
+ healthy = false;
255
+ }
256
+ if (!healthy) {
257
+ throw new QuoxAuthError(
258
+ `Auth service is not healthy right now (refresh returned ${res.status} but /health did not answer OK). ` +
259
+ `Stored session KEPT — this is very likely a restart or deploy in progress. Retry in a moment.`
260
+ );
261
+ }
262
+ config.clearAuth({ refresh: true });
263
+ throw new QuoxAuthError(
264
+ `Session expired (refresh rejected: ${(data && data.error) || res.status}). Please login again.`
265
+ + residentProfileHint(config, auth?.org_id),
266
+ );
267
+ }
268
+ throw new QuoxAuthError(`Token refresh failed (${res.status}); stored session kept. Retry, or login again if this persists.`);
269
+ }
270
+
271
+ auth.access_token = data.access_token;
272
+ // The route does not rotate the refresh token today. If it ever starts,
273
+ // silently dropping the new one would make the NEXT refresh fail and
274
+ // clear the session: the same outage with a different first cause.
275
+ if (data.refresh_token) auth.refresh_token = data.refresh_token;
276
+ if (data.expires_in) auth.expires_at = Date.now() + data.expires_in * 1000;
277
+ config.setAuth(auth, { refresh: true });
278
+ return data;
279
+ },
280
+
281
+ getTokens() {
282
+ return config.getAuth();
283
+ },
284
+
285
+ isLoggedIn() {
286
+ // Precedence, matching client.js: a stored USER token is what we
287
+ // actually authenticate with, so it decides this answer. A configured
288
+ // QUOX_SERVICE_KEY still counts as authenticated on its own, but only
289
+ // as the automation/daemon fallback when nobody has logged in.
290
+ //
291
+ // Checking the env var first was a live bug (quoxai/quox#404): dev
292
+ // boxes export QUOX_SERVICE_KEY globally, so the CLI reported
293
+ // "logged in" off the ambient key while sending the wrong credential.
294
+ const auth = config.getAuth();
295
+ // QUOX_FORCE_SERVICE_KEY: a daemon that owns its key deliberately bypasses
296
+ // any stored session (see client.js), so the key alone decides here.
297
+ if (process.env.QUOX_FORCE_SERVICE_KEY && process.env.QUOX_SERVICE_KEY) return true;
298
+ if (auth?.access_token) return true;
299
+ return !!process.env.QUOX_SERVICE_KEY;
300
+ },
301
+
302
+ isExpired() {
303
+ // A forced service key is not a locally-tracked JWT: never "expired".
304
+ if (process.env.QUOX_FORCE_SERVICE_KEY && process.env.QUOX_SERVICE_KEY) return false;
305
+ // A stored user token is the credential in play (see isLoggedIn), so
306
+ // its expiry is the one that matters — an ambient service key must not
307
+ // suppress the expiry signal, or client.js's 401 auto-refresh path
308
+ // (which only fires when isExpired() is true) would never refresh a
309
+ // genuinely expired session.
310
+ const auth = config.getAuth();
311
+ if (auth?.access_token) {
312
+ if (!auth.expires_at) return true;
313
+ return Date.now() > auth.expires_at;
314
+ }
315
+ // Service-key mode: not a JWT with a locally-tracked expiry, so treat
316
+ // it as never expired and never attempt a refresh for it.
317
+ if (process.env.QUOX_SERVICE_KEY) return false;
318
+ if (!auth?.expires_at) return true;
319
+ return Date.now() > auth.expires_at;
320
+ },
321
+
322
+ requireAuth() {
323
+ if (!this.isLoggedIn()) {
324
+ // Name the PROFILE. "Run `quox login`" alone sent the owner in a
325
+ // circle on 2026-08-23: he ran `quox login`, saw it succeed, then
326
+ // `quox -p moo dev enable` still refused, because the login had
327
+ // stored its session in whichever profile was active at the time
328
+ // while -p pointed at a different one. The advice was literally
329
+ // true and still useless, because it omitted the only detail that
330
+ // mattered.
331
+ const name = typeof config.getActiveProfileName === 'function' ? config.getActiveProfileName() : null;
332
+ const activeOrgId = config.getAuth()?.org_id || null;
333
+ if (name) {
334
+ throw new QuoxAuthError(
335
+ `Not authenticated on profile "${name}". Run \`quox -p ${name} login\`. `
336
+ + `Note that \`quox login\` without -p logs into whichever profile is active, which may not be this one.`
337
+ + residentProfileHint(config, activeOrgId)
338
+ );
339
+ }
340
+ throw new QuoxAuthError(`Not authenticated. Run \`quox login\` first.${residentProfileHint(config, activeOrgId)}`);
341
+ }
342
+ },
343
+
344
+ getOrgId(overrideOrg) {
345
+ if (overrideOrg) return overrideOrg;
346
+ const auth = config.getAuth();
347
+ return auth?.org_id || null;
348
+ },
349
+ };
350
+ }