quoxcli 0.0.0-stage → 0.3.1

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 (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -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/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,693 @@
1
+ /**
2
+ * HTTP client for QuoxCLI
3
+ *
4
+ * Wraps native fetch with:
5
+ * - Bearer token injection
6
+ * - Service-aware URL routing (auth:3101, collector:9848, etc.)
7
+ * - Auto-refresh on 401
8
+ * - Retry with exponential backoff for transient errors
9
+ * - Debug logging
10
+ */
11
+
12
+ import { resolveActing, resolveActingToken } from './actingAs.js';
13
+ import { detectCallerSource } from './callerSource.js';
14
+ import { QuoxApiError, QuoxNetworkError, QuoxAuthError, QuoxTransientAuthError } from './errors.js';
15
+ import { joinServiceUrl, isLocalServiceTarget } from './config.js';
16
+
17
+ const RETRY_STATUSES = new Set([429, 502, 503]);
18
+ const MAX_RETRIES = 3;
19
+ const RETRY_DELAYS = [1000, 3000, 9000];
20
+
21
+ // Generate a ULID-like correlation ID (timestamp + random, sortable, unique)
22
+ // quox#558: undici reports every TLS trust failure as a bare "fetch failed",
23
+ // which sent people debugging services that were up. Name the real cause and
24
+ // the sanctioned fix (trust the CA; never disable verification).
25
+ const TLS_TRUST_CODES = new Set([
26
+ 'SELF_SIGNED_CERT_IN_CHAIN',
27
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
28
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
29
+ 'CERT_HAS_EXPIRED',
30
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
31
+ ]);
32
+
33
+ export function describeNetworkError(service, baseUrl, err) {
34
+ const code = err?.cause?.code || err?.code;
35
+ let msg = `Could not connect to ${service} service at ${baseUrl}: ${err.message}`;
36
+ if (code && TLS_TRUST_CODES.has(code)) {
37
+ msg += `\n TLS: certificate not trusted (${code}). A new QuoxCORE uses its own self-signed certificate; trust that file`
38
+ + `\n (on the server, by default ~/quox-install/quox/ssl/quox.crt; copy it here if the server is another machine):`
39
+ + `\n export NODE_EXTRA_CA_CERTS=<path to quox.crt>`
40
+ + `\n Never disable TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0) instead.`;
41
+ } else if (code === 'ERR_SSL_WRONG_VERSION_NUMBER' || code === 'EPROTO') {
42
+ // quox#532 defect 1: the profile says https:// but the service answered
43
+ // PLAINTEXT (classic: url stamped https://localhost:3000 against plain
44
+ // http local services). Say "did you mean http://" instead of a raw
45
+ // fetch failed.
46
+ msg += `\n The server answered plaintext to a TLS handshake: this profile's url says https:// but the service speaks http.`
47
+ + `\n Did you mean ${String(baseUrl).replace(/^https:/, 'http:')} ? Fix it with: quox config set url <http-url>`;
48
+ } else if (code === 'ECONNREFUSED') {
49
+ // quox#548: "connection refused" could not distinguish service-absent
50
+ // from wrong-URL, and on boxes running a project-prefixed parallel stack
51
+ // the service often IS up, on a +10000 offset port. Say so instead of a
52
+ // dead end. Per-profile stamped service URLs also go stale silently
53
+ // (that half of quox#548 stays open) — the override is the escape hatch.
54
+ let offsetHint = '';
55
+ try {
56
+ const u = new URL(baseUrl);
57
+ if (u.port) offsetHint = ` (a project-prefixed stack would serve it at :${Number(u.port) + 10000})`;
58
+ } catch { /* baseUrl unparsable: skip the arithmetic, keep the rest */ }
59
+ msg += `\n Nothing is listening there: the service is absent, or this profile's URL is stale${offsetHint}.`
60
+ + `\n Check \`quox doctor\`, or override for this shell: export QUOX_${String(service).toUpperCase()}_URL=<url>`;
61
+ }
62
+ return msg;
63
+ }
64
+
65
+ function generateCorrelationId() {
66
+ const t = Date.now().toString(36);
67
+ const r = Math.random().toString(36).slice(2, 10);
68
+ return `cli-${t}-${r}`;
69
+ }
70
+
71
+ /**
72
+ * HITL-CONTEXT (owner 2026-10-07): name the calling session and profile so an
73
+ * approval this call triggers says "VULCAN (CLI) · profile moo" instead of a
74
+ * bare agent id. Display-only on the server; never used for authorization.
75
+ * Session: $QUOX_SESSION (the Claude/terminal session name).
76
+ */
77
+ export function callerContextHeaders(config, env = process.env, detectDeps = {}) {
78
+ const headers = {};
79
+ const clean = (v, n) => String(v).replace(/[^\w .:@+\-/]/g, '').trim().slice(0, n);
80
+ // QUOX_SESSION wins; otherwise auto-detect (QUOX_STREAM, attached agent,
81
+ // held slot, parent process) so the card never says only "CLI".
82
+ let session = env.QUOX_SESSION && clean(env.QUOX_SESSION, 48);
83
+ if (!session) {
84
+ try { session = detectCallerSource(env, detectDeps).label; } catch { session = null; }
85
+ }
86
+ if (session) headers['X-Quox-Session'] = clean(session, 48);
87
+ let profile = null;
88
+ try { profile = typeof config?.getActiveProfileName === 'function' ? config.getActiveProfileName() : null; } catch { profile = null; }
89
+ if (profile) {
90
+ const p = clean(profile, 32);
91
+ if (p) headers['X-Quox-Profile'] = p;
92
+ }
93
+ return headers;
94
+ }
95
+
96
+ /**
97
+ * The --debug "Body:" line. A sensitive request prints no body; otherwise top-level secret keys and
98
+ * any nested `credentials` object are masked (a top-level-only denylist printed vault create keys).
99
+ */
100
+ export function debugBody(body, sensitive = false) {
101
+ if (sensitive) return '[redacted: sensitive request]';
102
+ const sanitized = { ...body };
103
+ for (const key of ['password', 'token', 'access_token', 'refresh_token', 'secret', 'api_key', 'mfa_code', 'credentials']) {
104
+ if (sanitized[key]) sanitized[key] = '***';
105
+ }
106
+ return JSON.stringify(sanitized);
107
+ }
108
+
109
+ export function createClient(config, auth) {
110
+ let refreshing = null;
111
+ // After a failed refresh, back off instead of latching off forever. The
112
+ // permanent latch was correct for 2-second CLI invocations but killed
113
+ // long-lived daemons (2026-08-23: both resident supervisors poll-looped
114
+ // to death after ONE transient refresh failure, because this instance
115
+ // never tried again for the rest of its life). A cooldown keeps the
116
+ // don't-hammer-auth property without the death sentence.
117
+ const REFRESH_RETRY_COOLDOWN_MS = 60 * 1000;
118
+ let refreshFailedAt = 0;
119
+ const correlationId = generateCorrelationId();
120
+
121
+ async function request(method, path, options = {}) {
122
+ const {
123
+ service = 'auth',
124
+ body,
125
+ query,
126
+ headers: extraHeaders = {},
127
+ timeout = 30000,
128
+ raw = false,
129
+ retries = MAX_RETRIES,
130
+ noAuth = false,
131
+ noActAs = false,
132
+ sensitive = false,
133
+ // HYPERION #23: an outside-caller request (trigger webhook-fire) also drops the caller labels
134
+ // (X-Quox-Session / X-Quox-Profile): the receiving route may store every header in an org-readable record.
135
+ anonymous = false,
136
+ } = options;
137
+
138
+ const baseUrl = config.getServiceUrl(service);
139
+ const url = joinServiceUrl(baseUrl, path);
140
+
141
+ if (query) {
142
+ for (const [key, value] of Object.entries(query)) {
143
+ if (value != null) url.searchParams.set(key, String(value));
144
+ }
145
+ }
146
+
147
+ const fetchHeaders = {
148
+ 'X-Quox-Correlation': correlationId,
149
+ 'X-Quox-Client': 'cli',
150
+ ...(anonymous ? {} : callerContextHeaders(config)),
151
+ ...extraHeaders,
152
+ };
153
+
154
+ // Inject auth. Precedence: a stored USER token always wins over an
155
+ // ambient QUOX_SERVICE_KEY. The service key is the automation/daemon
156
+ // fallback for when nobody has logged in — a long-running process
157
+ // shelling out to this CLI has no human session and no refresh token,
158
+ // so it can't participate in the login/refresh dance below.
159
+ //
160
+ // The other order was a live bug (quoxai/quox#404): dev boxes export
161
+ // QUOX_SERVICE_KEY globally for quoxpert-mcp, so every command after a
162
+ // successful `quox login` sent X-Service-Key instead of the user's
163
+ // bearer token. The auth service's user endpoints (/api/auth/me and
164
+ // friends) don't accept X-Service-Key, so login printed success (it
165
+ // uses a raw fetch, bypassing this client) and everything after it 401'd.
166
+ //
167
+ // SEC: a custom header like X-Service-Key is NOT stripped by fetch on a
168
+ // cross-origin redirect, unlike Authorization (which the spec drops).
169
+ // That makes the long-lived, higher-privilege service key MORE exposed to
170
+ // a rogue-redirect harvest than an ordinary user's bearer token. Refuse to
171
+ // follow redirects at all when the service key is in play, so the key is
172
+ // never resent to a host we did not choose. Scoped to the service-key path
173
+ // deliberately: normal login flows keep their existing redirect behaviour.
174
+ // QUOX_FORCE_SERVICE_KEY inverts the precedence above for a caller that
175
+ // OWNS its service key and must never borrow the shared config's stored
176
+ // session — a long-running daemon (discord-pro bridge, soak sampler) shares
177
+ // ~/.config/quox/config.json, so whatever profile another process logged in
178
+ // last leaks in as a stored token and wins. When that token drifts to an
179
+ // identity the collector rejects, the daemon goes dark (2026-09-04: the
180
+ // bridge 400'd for hours on a resident JWT). The force flag is
181
+ // opt-in, so the #404 interactive-dev-box case is untouched.
182
+ let usingServiceKey = false;
183
+ // quox#772: opt-in acting identity (--as / QUOX_AS). Replaces the bearer
184
+ // with the agent's attach token; throws (no request sent) if not attached.
185
+ const acting = !noAuth && !noActAs ? await resolveActing({ explicitProfile: Boolean(config._explicitProfile) }) : { name: null };
186
+ const actingAs = acting.name;
187
+ if (actingAs) {
188
+ const heartbeat = {
189
+ post: (p, o) => request('POST', p, { ...o, noActAs: true }),
190
+ get: (p, o) => request('GET', p, { ...o, noActAs: true }),
191
+ };
192
+ const attachToken = await resolveActingToken(actingAs, heartbeat, { implicit: acting.source === 'default' });
193
+ fetchHeaders['Authorization'] = `Bearer ${attachToken}`;
194
+ } else if (!noAuth) {
195
+ const tokens = auth.getTokens();
196
+ const serviceKey = process.env.QUOX_SERVICE_KEY;
197
+ const forceServiceKey = !!process.env.QUOX_FORCE_SERVICE_KEY && !!serviceKey;
198
+ if (forceServiceKey) {
199
+ fetchHeaders['X-Service-Key'] = serviceKey;
200
+ usingServiceKey = true;
201
+ } else if (tokens?.access_token) {
202
+ fetchHeaders['Authorization'] = `Bearer ${tokens.access_token}`;
203
+ } else if (serviceKey && isLocalServiceTarget(url.href)) {
204
+ // QUOXON-Z2H lane R: the implicit fallback is THIS box's key; never
205
+ // offer it to another host (a remote profile with no login). A caller
206
+ // that owns a remote key says so with QUOX_FORCE_SERVICE_KEY.
207
+ fetchHeaders['X-Service-Key'] = serviceKey;
208
+ usingServiceKey = true;
209
+ }
210
+ }
211
+
212
+ if (body && !(body instanceof FormData)) {
213
+ fetchHeaders['Content-Type'] = 'application/json';
214
+ }
215
+
216
+ const controller = new AbortController();
217
+ const timer = setTimeout(() => controller.abort(), timeout);
218
+
219
+ const isDebug = process.argv.includes('--debug');
220
+
221
+ if (isDebug) {
222
+ console.error(`-> ${method} ${url.toString()}`);
223
+ if (body && !(body instanceof FormData)) {
224
+ console.error(` Body: ${debugBody(body, sensitive).slice(0, 200)}`);
225
+ }
226
+ }
227
+
228
+ let lastError;
229
+ let staleReloaded = false;
230
+ // REL-424: one refresh per request, on whichever attempt the 401 lands. A token that expires while the
231
+ // client sleeps between 503 retries gets its 401 on attempt 1+, and gating refresh on attempt 0 surfaced it.
232
+ let refreshedThisRequest = false;
233
+
234
+ for (let attempt = 0; attempt <= retries; attempt++) {
235
+ try {
236
+ const res = await fetch(url.toString(), {
237
+ method,
238
+ headers: fetchHeaders,
239
+ body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined,
240
+ signal: controller.signal,
241
+ ...(usingServiceKey ? { redirect: 'manual' } : {}),
242
+ });
243
+
244
+ // See the redirect note above: with redirect:'manual' a 3xx comes back
245
+ // as a real response instead of being followed. Surface it loudly —
246
+ // a Quox service should never redirect a CLI call, so this is either a
247
+ // misconfiguration or someone trying to harvest the service key.
248
+ if (usingServiceKey && res.status >= 300 && res.status < 400) {
249
+ throw new QuoxApiError(
250
+ `Refused to follow a ${res.status} redirect while authenticating with QUOX_SERVICE_KEY `
251
+ + `(would resend the service key to ${res.headers.get('location') || 'an unknown host'}). `
252
+ + 'Point QUOX_URL at the real collector instead of a redirecting front end.',
253
+ res.status,
254
+ );
255
+ }
256
+
257
+ if (isDebug) {
258
+ console.error(`<- ${res.status} ${res.statusText} (${method} ${path})`);
259
+ }
260
+
261
+ // Auto-refresh on 401 — but only when the locally-tracked token
262
+ // actually looks expired. A 401 can also come from a route that
263
+ // requires entirely different credentials (e.g. a service-key-gated
264
+ // /internal/* route hit with a valid, non-expired user token) — that
265
+ // is not a session-expiry signal, and attempting a refresh anyway
266
+ // risks the refresh call itself failing and wiping the whole stored
267
+ // profile over what is really just "wrong token for this route".
268
+ // In that case, skip the refresh and fall through to the normal
269
+ // error handling below so the original 401/403 is surfaced as-is.
270
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
271
+ // The SERVER saying "invalid or expired token" outranks the local clock. expires_at is
272
+ // stamped after the refresh response lands, so for a moment the server rejects a token
273
+ // the client still thinks is good. 2026-10-14 07:32:03: three windows hit that gap
274
+ // together, skipped the refresh and declared the profile signed out. Refresh is safe
275
+ // here: a dead refresh token is still judged by auth.refresh() itself.
276
+ if (res.status === 401 && !looksExpired && !noAuth && !actingAs && !usingServiceKey) {
277
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
278
+ }
279
+
280
+ // Stale sign-in: a sibling window or `quox login` may have written a newer token for this profile.
281
+ // Adopt it and retry this request ONCE before any refresh (which would spend our stale refresh token).
282
+ if (res.status === 401 && !noAuth && !actingAs && !usingServiceKey && !staleReloaded
283
+ && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
284
+ staleReloaded = true;
285
+ const fresh = auth.getTokens?.();
286
+ if (fresh?.access_token) {
287
+ fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
288
+ attempt--; // this retry is free: it does not spend the caller's retry budget
289
+ continue;
290
+ }
291
+ }
292
+
293
+ const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
294
+ if (res.status === 401 && !noAuth && !actingAs && !refreshedThisRequest && !refreshCoolingDown && looksExpired) {
295
+ refreshedThisRequest = true;
296
+ try {
297
+ if (!refreshing) {
298
+ const serviceUrl = config.getServiceUrl('auth');
299
+ refreshing = auth.refresh(serviceUrl).finally(() => { refreshing = null; });
300
+ }
301
+ await refreshing;
302
+
303
+ // Update auth header and retry
304
+ const newTokens = auth.getTokens();
305
+ if (newTokens?.access_token) {
306
+ fetchHeaders['Authorization'] = `Bearer ${newTokens.access_token}`;
307
+ }
308
+ continue;
309
+ } catch (refreshErr) {
310
+ // Genuine refresh failure — start the retry cooldown and surface
311
+ // a clear re-login prompt. A daemon's next request after the
312
+ // cooldown gets another chance; see REFRESH_RETRY_COOLDOWN_MS.
313
+ //
314
+ // CARRY THE REAL REASON. A bare `catch {}` here replaced every
315
+ // refresh failure with "Session expired", which is a GUESS at the
316
+ // cause: auth.refresh() distinguishes a dead token from an
317
+ // unhealthy service from a malformed request, and all three arrived
318
+ // as the same sentence telling the user to log in again. That cost
319
+ // hours on 2026-08-24, when the real cause was the CLI sending the
320
+ // refresh token in a header the server never reads.
321
+ clearTimeout(timer);
322
+ if (refreshErr && refreshErr.transient) {
323
+ // Auth is only temporarily unavailable (429 / restart): no
324
+ // cooldown, and keep the type so a listener can back off.
325
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retrying. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
326
+ }
327
+ refreshFailedAt = Date.now();
328
+ const why = refreshErr?.message ? ` ${refreshErr.message}` : '';
329
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${why}`);
330
+ if (refreshErr?.code) dead.code = refreshErr.code;
331
+ if (refreshErr?.reason) dead.reason = refreshErr.reason;
332
+ throw dead;
333
+ }
334
+ }
335
+
336
+ // Retry transient errors
337
+ // quox#882: the server can say a failure won't clear by itself (a downstream refusing
338
+ // connections): body retryable:false skips the backoff retries.
339
+ let noRetry = false;
340
+ if (RETRY_STATUSES.has(res.status) && attempt < retries) {
341
+ try { noRetry = (await res.clone().json())?.retryable === false; } catch { /* not JSON */ }
342
+ }
343
+ if (RETRY_STATUSES.has(res.status) && attempt < retries && !noRetry) {
344
+ const delay = RETRY_DELAYS[attempt] || 9000;
345
+ if (isDebug) {
346
+ console.error(` Retrying in ${delay}ms (attempt ${attempt + 1}/${retries})...`);
347
+ }
348
+ await sleep(delay);
349
+ continue;
350
+ }
351
+
352
+ clearTimeout(timer);
353
+
354
+ if (raw) return res;
355
+
356
+ if (!res.ok) {
357
+ let data;
358
+ try {
359
+ data = await res.json();
360
+ } catch {
361
+ // CLI-ERRORS: a proxy error page, not a Quox body; named so the renderer can say so
362
+ data = { error: 'non_json_response', message: res.statusText };
363
+ }
364
+ // A permission error while we silently fell back to the SERVICE
365
+ // KEY is almost never a permissions problem: the service key
366
+ // carries no user identity, so any route gated on who you are
367
+ // refuses it. Say that, because the raw error sends people to fix
368
+ // the wrong thing. Live cost, 2026-08-23: the instance owner hit
369
+ // `403 instance_admin_required` on `quox admin config`, and it was
370
+ // filed as a missing-role bug (quox#470) with a chicken-and-egg
371
+ // theory about who could grant it. He had held instance-admin
372
+ // since March. His session had simply expired and the CLI had
373
+ // quietly downgraded the request to a key with no user attached.
374
+ // CLI-ERRORS: a body with only a prose `error` ("Agent not found") leads with that, never
375
+ // "Request failed with status N" (the real reason used to sit on the dim Status line).
376
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
377
+ let message = data.message || (proseError ? data.error : `Request failed with status ${res.status}`);
378
+ // quox#617: surface the server's machine-readable `reason`. A vault
379
+ // update returns 403 with `reason: 'mfa_required'` for an MFA-enabled
380
+ // owner (a step-up gate) vs `reason: 'role_<r>_lacks_capability_<cap>'`
381
+ // for a genuine missing-permission — two very different fixes that
382
+ // read as one opaque "you do not have permission" once the reason is
383
+ // dropped. The body already carries it (and reaches .body below); we
384
+ // just also fold it into the human message so the WHY is visible
385
+ // without --debug. Skipped when the message already contains it.
386
+ if (data.reason && !String(message).includes(data.reason)) {
387
+ message += ` (reason: ${data.reason})`;
388
+ }
389
+ if (usingServiceKey && (res.status === 403 || res.status === 401)) {
390
+ message += `\nNote: this request used the service key (QUOX_SERVICE_KEY) because no user session was stored for this profile. `
391
+ + `The service key has no user identity, so anything gated on WHO you are will refuse it regardless of your real permissions. `
392
+ + `Run \`quox login\` (or \`quox -p <profile> login\`) and retry before treating this as a permissions problem.`;
393
+ }
394
+ // #1012: the paid backends (QuoxPlan, QuoxSEO, QLarity, QuoxSignal) are not part of the public
395
+ // install, so a licensed customer got a bare 502 from the collector proxy. This client cannot
396
+ // tell "not installed" from "installed but down", so it says both, and exits 1 (not 8).
397
+ const pluginDown = (res.status === 502 || res.status === 503) && typeof data.error === 'string'
398
+ && /^(QuoxPlan|QuoxSEO|QLarity|QuoxSignal) service unavailable$/i.exec(data.error);
399
+ if (pluginDown) {
400
+ const err = new QuoxApiError(
401
+ res.status,
402
+ 'PLUGIN_SERVICE_UNAVAILABLE',
403
+ `${pluginDown[1]} isn't reachable on this server. It isn't part of the public install (it ships separately), `
404
+ + 'or it is installed but down: run `quox health`. Nothing was changed.',
405
+ data.detail || data.details,
406
+ data
407
+ );
408
+ err.exitCode = 1;
409
+ throw err;
410
+ }
411
+ // LL-3d: carry the WHOLE body, not just message/detail. Endpoints
412
+ // that answer with structured context (the licensing 409's
413
+ // bound_instance_name / bound[] / max_servers, the 402's plugin)
414
+ // had it flattened away here, so command handlers could only print
415
+ // "Request failed with status 409".
416
+ const apiErr = new QuoxApiError(
417
+ res.status,
418
+ data.error || 'api_error',
419
+ message,
420
+ data.detail || data.details,
421
+ data
422
+ );
423
+ if (proseError) apiErr.reasonFromError = true;
424
+ if (usingServiceKey && (res.status === 403 || res.status === 401)) apiErr.serviceKeyFallback = true;
425
+ throw apiErr;
426
+ }
427
+
428
+ // Handle empty responses (204 No Content)
429
+ if (res.status === 204) return { success: true };
430
+
431
+ const contentType = res.headers.get('content-type') || '';
432
+ if (contentType.includes('application/json')) {
433
+ return await res.json();
434
+ }
435
+ return { success: true, text: await res.text() };
436
+
437
+ } catch (err) {
438
+ clearTimeout(timer);
439
+
440
+ if (err instanceof QuoxApiError || err instanceof QuoxAuthError) {
441
+ throw err;
442
+ }
443
+
444
+ if (err.name === 'AbortError') {
445
+ throw new QuoxNetworkError(`Request timed out after ${timeout}ms`);
446
+ }
447
+
448
+ lastError = err;
449
+
450
+ if (attempt < retries && isTransientError(err)) {
451
+ const delay = RETRY_DELAYS[attempt] || 9000;
452
+ await sleep(delay);
453
+ continue;
454
+ }
455
+
456
+ throw new QuoxNetworkError(
457
+ describeNetworkError(service, baseUrl, err),
458
+ err
459
+ );
460
+ }
461
+ }
462
+
463
+ throw lastError || new QuoxNetworkError('Request failed after retries');
464
+ }
465
+
466
+ /**
467
+ * Open a Server-Sent Events (SSE) stream and yield parsed events.
468
+ *
469
+ * Mirrors the collector's `POST /chat/stream` contract (quox#782): when
470
+ * `options.body` is given it is sent as a JSON POST body so prompts never
471
+ * appear in the URL; `options.query` remains for non-sensitive params.
472
+ * Response is `text/event-stream`
473
+ * with `data: <json>\n\n` frames (plus `: ping` keep-alive comments).
474
+ *
475
+ * Each yielded value is the parsed JSON payload of one `data:` frame
476
+ * (e.g. `{ type: 'connected', ... }`, `{ type: 'chunk', content: '...' }`,
477
+ * `{ type: 'done', ... }`, `{ type: 'error', error: '...' }`). The
478
+ * generator returns naturally on `done`/`error` events or when the
479
+ * server closes the connection.
480
+ *
481
+ * @param {string} path - e.g. '/chat/stream'
482
+ * @param {object} options
483
+ * @param {string} [options.service='collector']
484
+ * @param {object} [options.query] - query string params (never put prompts here)
485
+ * @param {object} [options.body] - JSON body; switches the request to POST
486
+ * @param {object} [options.headers] - extra headers
487
+ * @param {AbortSignal} [options.signal] - external abort signal (e.g. Ctrl+C)
488
+ * @param {boolean} [options.untilClose] - keep reading past done/error until the server closes (quoxon: turn.finished arrives after done)
489
+ */
490
+ async function* stream(path, options = {}) {
491
+ const {
492
+ service = 'collector',
493
+ query,
494
+ body: streamBody,
495
+ headers: extraHeaders = {},
496
+ signal: externalSignal,
497
+ untilClose = false,
498
+ } = options;
499
+
500
+ const baseUrl = config.getServiceUrl(service);
501
+ const url = joinServiceUrl(baseUrl, path);
502
+
503
+ if (query) {
504
+ for (const [key, value] of Object.entries(query)) {
505
+ if (value != null) url.searchParams.set(key, String(value));
506
+ }
507
+ }
508
+
509
+ const fetchHeaders = {
510
+ Accept: 'text/event-stream',
511
+ 'X-Quox-Correlation': correlationId,
512
+ ...(streamBody !== undefined ? { 'Content-Type': 'application/json' } : {}),
513
+ ...extraHeaders,
514
+ };
515
+
516
+ // Same precedence as request() above, including the QUOX_FORCE_SERVICE_KEY
517
+ // override for daemons that own their key and must not borrow the shared
518
+ // config's stored session.
519
+ const streamActingR = await resolveActing({ explicitProfile: Boolean(config._explicitProfile) });
520
+ const streamActing = streamActingR.name;
521
+ const streamTokens = auth.getTokens();
522
+ let streamServiceKey = null;
523
+ const streamForceKey = !!process.env.QUOX_FORCE_SERVICE_KEY && !!process.env.QUOX_SERVICE_KEY;
524
+ if (streamActing) {
525
+ const heartbeat = { post: (p, o) => request('POST', p, { ...o, noActAs: true }), get: (p, o) => request('GET', p, { ...o, noActAs: true }) };
526
+ fetchHeaders['Authorization'] = `Bearer ${await resolveActingToken(streamActing, heartbeat, { implicit: streamActingR.source === 'default' })}`;
527
+ } else if (streamForceKey) {
528
+ streamServiceKey = process.env.QUOX_SERVICE_KEY;
529
+ fetchHeaders['X-Service-Key'] = streamServiceKey;
530
+ } else if (streamTokens?.access_token) {
531
+ fetchHeaders['Authorization'] = `Bearer ${streamTokens.access_token}`;
532
+ } else if (process.env.QUOX_SERVICE_KEY && isLocalServiceTarget(url.href)) {
533
+ streamServiceKey = process.env.QUOX_SERVICE_KEY;
534
+ fetchHeaders['X-Service-Key'] = streamServiceKey;
535
+ }
536
+
537
+ const isDebug = process.argv.includes('--debug');
538
+ if (isDebug) {
539
+ console.error(`-> ${streamBody !== undefined ? 'POST' : 'GET'}(stream) ${url.toString()}`);
540
+ }
541
+
542
+ const openStream = async () => {
543
+ try {
544
+ return await fetch(url.toString(), {
545
+ method: streamBody !== undefined ? 'POST' : 'GET',
546
+ headers: fetchHeaders,
547
+ ...(streamBody !== undefined ? { body: JSON.stringify(streamBody) } : {}),
548
+ signal: externalSignal,
549
+ // Same redirect protection as the non-streaming path above: never let
550
+ // a 3xx resend X-Service-Key to a host we did not choose.
551
+ ...(streamServiceKey ? { redirect: 'manual' } : {}),
552
+ });
553
+ } catch (err) {
554
+ throw new QuoxNetworkError(
555
+ describeNetworkError(service, baseUrl, err),
556
+ err
557
+ );
558
+ }
559
+ };
560
+ let res = await openStream();
561
+
562
+ // 2026-10-16: request() refreshed an expired session on a 401, the stream (the chat path) did not,
563
+ // so a chat turn right after the access token expired read "Not signed in" on a signed-in profile.
564
+ // Same rule as request(): one refresh for a user token that looks expired (locally, or the server
565
+ // says invalid_token), shared with any refresh already in flight, then one retry.
566
+ const usesUserToken = !streamActing && !streamServiceKey && !!streamTokens?.access_token;
567
+ if (res.status === 401 && usesUserToken && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
568
+ // Stale sign-in: a sibling window or `quox login` wrote a newer token. Adopt it, retry once.
569
+ const fresh = auth.getTokens?.();
570
+ if (fresh?.access_token) { fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`; res = await openStream(); }
571
+ }
572
+ if (res.status === 401 && usesUserToken) {
573
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
574
+ if (!looksExpired) {
575
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
576
+ }
577
+ const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
578
+ if (looksExpired && !refreshCoolingDown) {
579
+ try {
580
+ if (!refreshing) {
581
+ refreshing = auth.refresh(config.getServiceUrl('auth')).finally(() => { refreshing = null; });
582
+ }
583
+ await refreshing;
584
+ } catch (refreshErr) {
585
+ if (refreshErr && refreshErr.transient) {
586
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retry shortly. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
587
+ }
588
+ refreshFailedAt = Date.now();
589
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${refreshErr?.message ? ` ${refreshErr.message}` : ''}`);
590
+ dead.status = 401;
591
+ throw dead;
592
+ }
593
+ const fresh = auth.getTokens();
594
+ if (fresh?.access_token) fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
595
+ res = await openStream();
596
+ }
597
+ }
598
+
599
+ if (!res.ok) {
600
+ let data;
601
+ try {
602
+ data = await res.json();
603
+ } catch {
604
+ data = { error: 'non_json_response', message: res.statusText };
605
+ }
606
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
607
+ const streamErr = new QuoxApiError(
608
+ res.status,
609
+ data.error || 'api_error',
610
+ data.message || (proseError ? data.error : `Stream request failed with status ${res.status}`),
611
+ data.detail || data.details,
612
+ data
613
+ );
614
+ if (proseError) streamErr.reasonFromError = true;
615
+ throw streamErr;
616
+ }
617
+
618
+ if (!res.body) {
619
+ throw new QuoxNetworkError('Stream response had no body.');
620
+ }
621
+
622
+ const reader = res.body.getReader();
623
+ const decoder = new TextDecoder();
624
+ let buffer = '';
625
+
626
+ try {
627
+ while (true) {
628
+ const { done, value } = await reader.read();
629
+ if (done) break;
630
+
631
+ buffer += decoder.decode(value, { stream: true });
632
+ const lines = buffer.split('\n');
633
+ buffer = lines.pop() ?? '';
634
+
635
+ for (const line of lines) {
636
+ if (!line.startsWith('data:')) continue; // skip blank lines and ": ping" comments
637
+ const raw = line.slice(5).trim();
638
+ if (!raw || raw === '[DONE]') continue;
639
+
640
+ let event;
641
+ try {
642
+ event = JSON.parse(raw);
643
+ } catch {
644
+ if (isDebug) console.error(` [stream] skipping malformed frame: ${raw.slice(0, 100)}`);
645
+ continue;
646
+ }
647
+
648
+ yield event;
649
+
650
+ if (!untilClose && (event.type === 'done' || event.type === 'error')) {
651
+ return;
652
+ }
653
+ }
654
+ }
655
+ } finally {
656
+ // cancel first so the HTTP body is torn down, not just unlocked
657
+ try { reader.cancel().catch(() => {}); } catch { /* already closed */ }
658
+ try { reader.releaseLock(); } catch { /* already released */ }
659
+ }
660
+ }
661
+
662
+ return {
663
+ get(path, options) {
664
+ return request('GET', path, options);
665
+ },
666
+ post(path, options) {
667
+ return request('POST', path, options);
668
+ },
669
+ patch(path, options) {
670
+ return request('PATCH', path, options);
671
+ },
672
+ put(path, options) {
673
+ return request('PUT', path, options);
674
+ },
675
+ delete(path, options) {
676
+ return request('DELETE', path, options);
677
+ },
678
+ request,
679
+ stream,
680
+ };
681
+ }
682
+
683
+ function sleep(ms) {
684
+ return new Promise(resolve => setTimeout(resolve, ms));
685
+ }
686
+
687
+ function isTransientError(err) {
688
+ const msg = err.message || '';
689
+ return msg.includes('ECONNRESET') ||
690
+ msg.includes('ECONNREFUSED') ||
691
+ msg.includes('ETIMEDOUT') ||
692
+ msg.includes('fetch failed');
693
+ }