quoxcli 0.2.0 → 0.4.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 (442) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/actions.js +15 -0
  5. package/lib/api/endpoints/admin.js +46 -0
  6. package/lib/api/endpoints/agent-schedules.js +26 -0
  7. package/lib/api/endpoints/agents.js +7 -0
  8. package/lib/api/endpoints/assistants.js +12 -0
  9. package/lib/api/endpoints/away.js +15 -0
  10. package/lib/api/endpoints/blog.js +16 -0
  11. package/lib/api/endpoints/chat.js +14 -0
  12. package/lib/api/endpoints/contactsq.js +6 -0
  13. package/lib/api/endpoints/conversations.js +14 -0
  14. package/lib/api/endpoints/devServerRegistry.js +25 -0
  15. package/lib/api/endpoints/engagement.js +26 -0
  16. package/lib/api/endpoints/evidence.js +20 -0
  17. package/lib/api/endpoints/executions.js +13 -0
  18. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  19. package/lib/api/endpoints/files.js +14 -0
  20. package/lib/api/endpoints/hat.js +44 -0
  21. package/lib/api/endpoints/inbox.js +20 -0
  22. package/lib/api/endpoints/index.js +26 -0
  23. package/lib/api/endpoints/loops.js +26 -0
  24. package/lib/api/endpoints/memory.js +19 -0
  25. package/lib/api/endpoints/mind.js +13 -0
  26. package/lib/api/endpoints/mon.js +179 -0
  27. package/lib/api/endpoints/objectives.js +12 -0
  28. package/lib/api/endpoints/plugins.js +18 -0
  29. package/lib/api/endpoints/presence.js +33 -0
  30. package/lib/api/endpoints/quoxplan.js +7 -0
  31. package/lib/api/endpoints/quoxseo.js +12 -0
  32. package/lib/api/endpoints/router.js +17 -0
  33. package/lib/api/endpoints/safety.js +7 -0
  34. package/lib/api/endpoints/server.js +21 -0
  35. package/lib/api/endpoints/setup.js +6 -0
  36. package/lib/api/endpoints/skills.js +20 -0
  37. package/lib/api/endpoints/stepup.js +20 -0
  38. package/lib/api/endpoints/teams.js +2 -2
  39. package/lib/api/endpoints/triggers.js +26 -0
  40. package/lib/api/endpoints/v1.js +120 -0
  41. package/lib/api/endpoints/ward.js +7 -0
  42. package/lib/api/endpoints/workflows.js +7 -0
  43. package/lib/api/endpoints/x.js +8 -0
  44. package/lib/commands/actions.js +131 -0
  45. package/lib/commands/admin.js +16 -0
  46. package/lib/commands/agent.js +170 -27
  47. package/lib/commands/agentAttach.js +140 -13
  48. package/lib/commands/approvals.js +78 -1
  49. package/lib/commands/aside.js +24 -0
  50. package/lib/commands/ask.js +146 -0
  51. package/lib/commands/assistant.js +59 -4
  52. package/lib/commands/auth.js +8 -2
  53. package/lib/commands/backup.js +8 -4
  54. package/lib/commands/blog.js +110 -0
  55. package/lib/commands/brain.js +105 -14
  56. package/lib/commands/build.js +19 -5
  57. package/lib/commands/chat.js +180 -26
  58. package/lib/commands/chatCompanion.js +27 -0
  59. package/lib/commands/chatState.js +45 -0
  60. package/lib/commands/claude.js +35 -38
  61. package/lib/commands/contacts.js +134 -4
  62. package/lib/commands/contactsSendAt.js +21 -0
  63. package/lib/commands/conversation.js +35 -4
  64. package/lib/commands/dashboard.js +43 -0
  65. package/lib/commands/department.js +28 -7
  66. package/lib/commands/devResident.js +15 -3
  67. package/lib/commands/devSupervisor.js +13 -1
  68. package/lib/commands/discord.js +75 -39
  69. package/lib/commands/done.js +98 -28
  70. package/lib/commands/engagement.js +107 -6
  71. package/lib/commands/evidence.js +154 -0
  72. package/lib/commands/ext.js +23 -0
  73. package/lib/commands/fetchAllowlist.js +71 -0
  74. package/lib/commands/file.js +38 -1
  75. package/lib/commands/fleet.js +336 -4
  76. package/lib/commands/governance.js +12 -3
  77. package/lib/commands/gripe.js +44 -13
  78. package/lib/commands/hat.js +254 -9
  79. package/lib/commands/helpers.js +18 -1
  80. package/lib/commands/hyperion.js +58 -0
  81. package/lib/commands/image.js +68 -0
  82. package/lib/commands/inbox.js +74 -23
  83. package/lib/commands/kuma.js +1050 -0
  84. package/lib/commands/kumaSla.js +174 -0
  85. package/lib/commands/licence.js +52 -155
  86. package/lib/commands/loop.js +291 -0
  87. package/lib/commands/lucid.js +10 -4
  88. package/lib/commands/memory.js +85 -8
  89. package/lib/commands/mind.js +71 -0
  90. package/lib/commands/mon.js +69 -2
  91. package/lib/commands/monitoring.js +1 -1
  92. package/lib/commands/motion.js +2 -2
  93. package/lib/commands/nucleus.js +48 -17
  94. package/lib/commands/objective.js +59 -1
  95. package/lib/commands/offload.js +89 -0
  96. package/lib/commands/onboard.js +2 -1
  97. package/lib/commands/org.js +30 -3
  98. package/lib/commands/peek.js +28 -0
  99. package/lib/commands/plan.js +38 -0
  100. package/lib/commands/plugin.js +110 -0
  101. package/lib/commands/presence.js +89 -0
  102. package/lib/commands/provider.js +217 -0
  103. package/lib/commands/qlarity.js +19 -3
  104. package/lib/commands/quoxchat.js +13 -4
  105. package/lib/commands/quoxseo.js +69 -0
  106. package/lib/commands/reach.js +2 -1
  107. package/lib/commands/research.js +79 -0
  108. package/lib/commands/router.js +77 -0
  109. package/lib/commands/rows.js +367 -0
  110. package/lib/commands/run.js +82 -10
  111. package/lib/commands/scope.js +2 -2
  112. package/lib/commands/server.js +405 -0
  113. package/lib/commands/setup.js +33 -7
  114. package/lib/commands/shortcut.js +41 -0
  115. package/lib/commands/skills.js +68 -9
  116. package/lib/commands/slot.js +260 -7
  117. package/lib/commands/status.js +1 -1
  118. package/lib/commands/team.js +3 -1
  119. package/lib/commands/tips.js +23 -0
  120. package/lib/commands/tool.js +133 -45
  121. package/lib/commands/trace.js +12 -7
  122. package/lib/commands/trigger.js +95 -0
  123. package/lib/commands/update.js +46 -0
  124. package/lib/commands/vault.js +31 -8
  125. package/lib/commands/vm.js +237 -0
  126. package/lib/commands/voice.js +34 -3
  127. package/lib/commands/volt.js +45 -3
  128. package/lib/commands/walkthrough.js +32 -0
  129. package/lib/commands/watch.js +73 -21
  130. package/lib/commands/welcome.js +14 -0
  131. package/lib/commands/workflow.js +189 -32
  132. package/lib/commands/x.js +13 -4
  133. package/lib/commands/youtube.js +172 -0
  134. package/lib/core/actingAs.js +2 -2
  135. package/lib/core/actionItems.js +115 -0
  136. package/lib/core/activeSummary.js +186 -0
  137. package/lib/core/agentAlias.js +2 -0
  138. package/lib/core/agentFamilies.js +7 -0
  139. package/lib/core/aside.js +54 -0
  140. package/lib/core/askDecision.js +60 -0
  141. package/lib/core/auth.js +164 -13
  142. package/lib/core/callerSource.js +1 -1
  143. package/lib/core/cliBuild.js +62 -0
  144. package/lib/core/client.js +152 -35
  145. package/lib/core/commandRegistrars.js +130 -0
  146. package/lib/core/config.js +143 -25
  147. package/lib/core/customHats.js +168 -0
  148. package/lib/core/doneEngineFetch.js +8 -0
  149. package/lib/core/errors.js +329 -20
  150. package/lib/core/findingReconcile.js +90 -0
  151. package/lib/core/global-flags.js +105 -5
  152. package/lib/core/governedSession.js +1 -3
  153. package/lib/core/hyperion.js +102 -0
  154. package/lib/core/inboxResolution.js +65 -0
  155. package/lib/core/lazyCommands.js +4 -1
  156. package/lib/core/liveSessionsReport.js +1 -1
  157. package/lib/core/loopGlyph.js +97 -0
  158. package/lib/core/offload.js +533 -0
  159. package/lib/core/org-override.js +1 -1
  160. package/lib/core/orgResolve.js +70 -0
  161. package/lib/core/output.js +11 -11
  162. package/lib/core/peek.js +126 -0
  163. package/lib/core/profileUsers.js +51 -0
  164. package/lib/core/promptCancel.js +18 -0
  165. package/lib/core/repoPath.js +11 -0
  166. package/lib/core/residentSeed.js +3 -1
  167. package/lib/core/rootOptions.js +39 -0
  168. package/lib/core/router.js +168 -0
  169. package/lib/core/selfUpdate.js +188 -0
  170. package/lib/core/sessionHeartbeat.js +12 -8
  171. package/lib/core/sessionsOverview.js +64 -0
  172. package/lib/core/shortcut.js +83 -0
  173. package/lib/core/slotKeeper.js +248 -3
  174. package/lib/core/slotWake.js +120 -0
  175. package/lib/core/supervisorEngine.js +71 -2
  176. package/lib/core/version.js +1 -1
  177. package/lib/core/vm.js +366 -0
  178. package/lib/core/vmTui.js +86 -0
  179. package/lib/core/walkthrough.js +35 -0
  180. package/lib/dragon/companionView.js +34 -0
  181. package/lib/dragon/frames.js +118 -0
  182. package/lib/dragon/jobs.js +123 -0
  183. package/lib/dragon/renderWorker.js +15 -0
  184. package/lib/dragon/select.js +72 -0
  185. package/lib/evidence/bundleExport.js +47 -0
  186. package/lib/evidence/bundleVerify.js +77 -0
  187. package/lib/hyperion/deck.js +29 -0
  188. package/lib/lucid/LucidCard.jsx +68 -0
  189. package/lib/lucid/LucidIntro.jsx +45 -58
  190. package/lib/lucid/cards.js +86 -0
  191. package/lib/lucid/markdown.js +81 -0
  192. package/lib/lucid/motion.js +56 -0
  193. package/lib/quoxon/EXTENSIONS.md +23 -0
  194. package/lib/quoxon/activity.js +14 -2
  195. package/lib/quoxon/aocl.js +117 -0
  196. package/lib/quoxon/api.js +690 -25
  197. package/lib/quoxon/app.js +3586 -250
  198. package/lib/quoxon/approvalFacts.js +96 -0
  199. package/lib/quoxon/approvalSentence.js +44 -0
  200. package/lib/quoxon/approvalsView.js +384 -0
  201. package/lib/quoxon/asideView.js +40 -0
  202. package/lib/quoxon/ask.js +121 -0
  203. package/lib/quoxon/away.js +52 -0
  204. package/lib/quoxon/bang.js +224 -0
  205. package/lib/quoxon/bulkRisk.js +17 -0
  206. package/lib/quoxon/capabilities.js +13 -6
  207. package/lib/quoxon/cells.js +53 -0
  208. package/lib/quoxon/commandShort.js +124 -0
  209. package/lib/quoxon/commands.js +429 -39
  210. package/lib/quoxon/connect.js +210 -23
  211. package/lib/quoxon/connectCard.js +19 -2
  212. package/lib/quoxon/consent.js +86 -0
  213. package/lib/quoxon/copy.js +4 -3
  214. package/lib/quoxon/countdownLine.js +20 -0
  215. package/lib/quoxon/deck.js +30 -0
  216. package/lib/quoxon/demo.autoplay.js +29 -0
  217. package/lib/quoxon/demo.fixtures.js +1 -1
  218. package/lib/quoxon/demo.js +46 -3
  219. package/lib/quoxon/devObserver.js +20 -0
  220. package/lib/quoxon/dividerAocl.js +192 -0
  221. package/lib/quoxon/dividerRule.js +57 -0
  222. package/lib/quoxon/dragonCompanion.js +255 -0
  223. package/lib/quoxon/dragonModes.js +151 -0
  224. package/lib/quoxon/dragonMoments.js +82 -0
  225. package/lib/quoxon/dragonReads.js +68 -0
  226. package/lib/quoxon/dragonView.js +41 -0
  227. package/lib/quoxon/earlyInput.js +40 -0
  228. package/lib/quoxon/echo.js +15 -0
  229. package/lib/quoxon/egg.js +2 -2
  230. package/lib/quoxon/events.js +266 -26
  231. package/lib/quoxon/extensions.js +276 -0
  232. package/lib/quoxon/feedback.js +50 -0
  233. package/lib/quoxon/gallery.js +21 -7
  234. package/lib/quoxon/handoffOffer.js +78 -0
  235. package/lib/quoxon/hatSwitch.js +37 -0
  236. package/lib/quoxon/headless.js +161 -0
  237. package/lib/quoxon/hostUrls.js +41 -0
  238. package/lib/quoxon/inboxView.js +79 -0
  239. package/lib/quoxon/indicator.js +6 -2
  240. package/lib/quoxon/installCli.js +79 -0
  241. package/lib/quoxon/intent.js +12 -1
  242. package/lib/quoxon/keys.js +51 -8
  243. package/lib/quoxon/launcher.js +103 -0
  244. package/lib/quoxon/loopCard.js +456 -0
  245. package/lib/quoxon/loopCards.js +180 -0
  246. package/lib/quoxon/loopStream.js +87 -0
  247. package/lib/quoxon/main.js +101 -160
  248. package/lib/quoxon/markdown.js +45 -21
  249. package/lib/quoxon/markdownSettle.js +19 -0
  250. package/lib/quoxon/matrixDomains.json +46 -0
  251. package/lib/quoxon/mentions.js +90 -0
  252. package/lib/quoxon/mind.js +103 -0
  253. package/lib/quoxon/mode.js +46 -0
  254. package/lib/quoxon/model.js +132 -26
  255. package/lib/quoxon/modelPrefs.js +345 -0
  256. package/lib/quoxon/motion/clock.js +3 -2
  257. package/lib/quoxon/motion/level.js +91 -0
  258. package/lib/quoxon/mouse.js +222 -0
  259. package/lib/quoxon/noai.js +57 -0
  260. package/lib/quoxon/objectiveOffer.js +82 -0
  261. package/lib/quoxon/objects/away.js +72 -0
  262. package/lib/quoxon/objects/fetch.js +17 -5
  263. package/lib/quoxon/objects/fleet.js +149 -0
  264. package/lib/quoxon/objects/health.js +186 -0
  265. package/lib/quoxon/objects/index.js +46 -7
  266. package/lib/quoxon/objects/mirror.js +137 -0
  267. package/lib/quoxon/objects/model.js +33 -0
  268. package/lib/quoxon/objects/peek.js +17 -0
  269. package/lib/quoxon/objects/proof.js +12 -4
  270. package/lib/quoxon/objects/prove.js +130 -0
  271. package/lib/quoxon/objects/replay.js +125 -0
  272. package/lib/quoxon/objects/sitrep.js +58 -0
  273. package/lib/quoxon/objects/view.js +11 -0
  274. package/lib/quoxon/onboarding/copy.js +32 -4
  275. package/lib/quoxon/onboarding/credential.js +17 -4
  276. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  277. package/lib/quoxon/onboarding/failureCause.js +33 -0
  278. package/lib/quoxon/onboarding/finish.js +15 -0
  279. package/lib/quoxon/onboarding/index.js +34 -7
  280. package/lib/quoxon/onboarding/installRunner.js +64 -5
  281. package/lib/quoxon/onboarding/preflight.js +17 -3
  282. package/lib/quoxon/onboarding/probe.js +65 -0
  283. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  284. package/lib/quoxon/onboarding/view.js +76 -19
  285. package/lib/quoxon/orgLabel.js +10 -0
  286. package/lib/quoxon/orgSwitch.js +59 -0
  287. package/lib/quoxon/ownBroadcast.js +22 -0
  288. package/lib/quoxon/palette.js +43 -0
  289. package/lib/quoxon/planSteps.js +51 -0
  290. package/lib/quoxon/print.js +12 -5
  291. package/lib/quoxon/profileSwitch.js +53 -0
  292. package/lib/quoxon/promptHistory.js +41 -0
  293. package/lib/quoxon/providers.js +48 -0
  294. package/lib/quoxon/qart.js +20 -51
  295. package/lib/quoxon/qdragon.js +410 -0
  296. package/lib/quoxon/qdragonFly.js +62 -0
  297. package/lib/quoxon/qdragonScenes.js +282 -0
  298. package/lib/quoxon/qeyes.js +213 -0
  299. package/lib/quoxon/qmotion.js +75 -37
  300. package/lib/quoxon/render.js +740 -124
  301. package/lib/quoxon/resend.js +72 -0
  302. package/lib/quoxon/roster.js +117 -17
  303. package/lib/quoxon/route.js +6 -5
  304. package/lib/quoxon/rows.js +954 -0
  305. package/lib/quoxon/rowsStore.js +80 -0
  306. package/lib/quoxon/screenContext.js +150 -0
  307. package/lib/quoxon/screenFrames.js +254 -0
  308. package/lib/quoxon/serverChip.js +42 -0
  309. package/lib/quoxon/session.js +182 -2
  310. package/lib/quoxon/sessionDead.js +17 -0
  311. package/lib/quoxon/signin.js +125 -0
  312. package/lib/quoxon/slash/agents.js +112 -0
  313. package/lib/quoxon/slash/argSources.js +65 -0
  314. package/lib/quoxon/slash/args.js +65 -0
  315. package/lib/quoxon/slash/automate.js +89 -0
  316. package/lib/quoxon/slash/broadcast.js +140 -0
  317. package/lib/quoxon/slash/broadcastRun.js +108 -0
  318. package/lib/quoxon/slash/card.js +30 -0
  319. package/lib/quoxon/slash/challenge.js +114 -0
  320. package/lib/quoxon/slash/cliCommand.js +94 -0
  321. package/lib/quoxon/slash/cliLeaves.json +1023 -0
  322. package/lib/quoxon/slash/compact.js +1 -1
  323. package/lib/quoxon/slash/confirm.js +30 -0
  324. package/lib/quoxon/slash/crystallize.js +69 -0
  325. package/lib/quoxon/slash/dashboard.js +136 -0
  326. package/lib/quoxon/slash/draft.js +54 -0
  327. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  328. package/lib/quoxon/slash/hatWalk.js +206 -0
  329. package/lib/quoxon/slash/inverses.js +77 -0
  330. package/lib/quoxon/slash/killer.js +24 -0
  331. package/lib/quoxon/slash/killerApi.js +16 -0
  332. package/lib/quoxon/slash/ledger.js +13 -8
  333. package/lib/quoxon/slash/listView.js +3 -1
  334. package/lib/quoxon/slash/modelCmd.js +261 -0
  335. package/lib/quoxon/slash/palette.js +22 -8
  336. package/lib/quoxon/slash/paletteController.js +117 -7
  337. package/lib/quoxon/slash/panels.js +21 -6
  338. package/lib/quoxon/slash/picker.js +20 -2
  339. package/lib/quoxon/slash/qpeCmd.js +35 -0
  340. package/lib/quoxon/slash/restart.js +95 -13
  341. package/lib/quoxon/slash/resume.js +18 -1
  342. package/lib/quoxon/slash/rowCmd.js +157 -0
  343. package/lib/quoxon/slash/rowWalk.js +175 -0
  344. package/lib/quoxon/slash/settings.js +16 -6
  345. package/lib/quoxon/slash/spawn.js +177 -0
  346. package/lib/quoxon/slash/stepUp.js +274 -0
  347. package/lib/quoxon/slash/stepUpCard.js +60 -0
  348. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  349. package/lib/quoxon/slash/todoCmd.js +122 -0
  350. package/lib/quoxon/slash/undoApi.js +38 -0
  351. package/lib/quoxon/slash/undoCmd.js +49 -0
  352. package/lib/quoxon/slash/undoWish.js +15 -0
  353. package/lib/quoxon/slash/watch.js +164 -0
  354. package/lib/quoxon/slash/wish.js +23 -0
  355. package/lib/quoxon/slash/wishCmd.js +32 -0
  356. package/lib/quoxon/slotChat.js +300 -0
  357. package/lib/quoxon/slotFormat.js +195 -0
  358. package/lib/quoxon/slotRun.js +131 -0
  359. package/lib/quoxon/sound/mapping.js +2 -1
  360. package/lib/quoxon/stray.js +22 -0
  361. package/lib/quoxon/suggestCommand.js +90 -0
  362. package/lib/quoxon/term.js +46 -11
  363. package/lib/quoxon/textBuffer.js +176 -2
  364. package/lib/quoxon/theme.js +58 -25
  365. package/lib/quoxon/tips.js +433 -0
  366. package/lib/quoxon/toolCards.js +30 -0
  367. package/lib/quoxon/toolError.js +41 -0
  368. package/lib/quoxon/transcript.js +42 -4
  369. package/lib/quoxon/web/qface.js +18 -10
  370. package/lib/quoxon/welcome.js +1 -1
  371. package/lib/quoxon/whatsnew.js +136 -0
  372. package/lib/quoxon/whatsnew.jsonl +134 -0
  373. package/lib/quoxon/windowOrg.js +123 -0
  374. package/lib/server/devServers.js +137 -0
  375. package/lib/server/diag.js +129 -0
  376. package/lib/server/doctor.js +71 -0
  377. package/lib/server/install.js +149 -0
  378. package/lib/server/pluginEnable.js +161 -0
  379. package/lib/server/skew.js +56 -0
  380. package/lib/server/snapshot.js +131 -0
  381. package/lib/server/status.js +128 -0
  382. package/lib/server/sys.js +65 -0
  383. package/lib/server/uninstall.js +63 -0
  384. package/lib/server/update.js +234 -0
  385. package/lib/toolOutputShape.js +315 -0
  386. package/lib/tui/views/HyperionDeck.jsx +79 -0
  387. package/lib/tui/views/LucidDeck.jsx +209 -217
  388. package/package.json +5 -1
  389. package/vendor/dragonlab/SOURCE +4 -0
  390. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  391. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  392. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  393. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  394. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  395. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  396. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  397. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  398. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  399. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  400. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  401. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  402. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  403. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  404. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  405. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  406. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  407. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  408. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  409. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  410. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  411. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  412. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  413. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  414. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  415. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  416. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  417. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  418. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  419. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  420. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  421. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  422. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  423. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  424. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  425. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  426. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  427. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  428. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  429. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  430. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  431. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  432. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  433. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  434. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  435. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  436. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  437. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  438. package/vendor/dragonlab/lib/power-effects.js +294 -0
  439. package/vendor/dragonlab/lib/power-registry.js +74 -0
  440. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  441. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
  442. package/lib/core/websiteSession.js +0 -148
package/lib/core/auth.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * Tokens stored in config profile's auth field.
6
6
  */
7
7
 
8
+ import { readFileSync, statSync, openSync, closeSync, unlinkSync } from 'node:fs';
8
9
  import { QuoxAuthError, QuoxApiError, QuoxTransientAuthError } from './errors.js';
9
10
  import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from './residentAuth.js';
10
11
 
@@ -94,11 +95,79 @@ function residentProfileHint(config, deadOrgId = null) {
94
95
  }
95
96
  }
96
97
 
98
+ // QUOX_TOKEN_FILE: the W4 builder principal (SDK-TEAM design v2, 2.1.3) runs
99
+ // on ONE agent token that an owner-side helper writes to a file and rewrites
100
+ // near expiry. The builder holds no refresh token, no user session and no
101
+ // service key, so this mode is exclusive and fails CLOSED: the file's token or
102
+ // nothing, never the profile, never an ambient QUOX_SERVICE_KEY. Its "refresh"
103
+ // is re-reading the file; it never calls the auth service and never writes or
104
+ // clears the profile.
105
+ function tokenFilePath() {
106
+ return process.env.QUOX_TOKEN_FILE || null;
107
+ }
108
+
109
+ function readTokenFile(file) {
110
+ let st;
111
+ try {
112
+ st = statSync(file);
113
+ } catch {
114
+ throw new QuoxAuthError(`QUOX_TOKEN_FILE ${file} is not readable. The owner-side builder helper writes it; is the lease live?`);
115
+ }
116
+ if (st.mode & 0o004) {
117
+ throw new QuoxAuthError(`QUOX_TOKEN_FILE ${file} is readable by other users (mode ${(st.mode & 0o777).toString(8)}); refusing to use it. Expected 0640 or tighter.`);
118
+ }
119
+ const token = readFileSync(file, 'utf8').trim();
120
+ if (!token) throw new QuoxAuthError(`QUOX_TOKEN_FILE ${file} is empty.`);
121
+ return { access_token: token, expires_at: jwtExpiryMs(token), token_file: file };
122
+ }
123
+
124
+ // Expiry only, for the client's "looks expired, try the refresh seam" signal.
125
+ // Not verification: the server verifies. Undecodable means no local expiry.
126
+ function jwtExpiryMs(token) {
127
+ try {
128
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
129
+ return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ /** Keep the last login email on the profile (not a secret) so a signed-out session can prefill /login. */
136
+ export function rememberEmail(config, email) {
137
+ try {
138
+ const prof = config.getActiveProfile?.();
139
+ if (prof && email && prof.last_email !== email) { prof.last_email = email; config.save?.(); }
140
+ } catch { /* best-effort */ }
141
+ }
142
+
143
+ // Cross-process refresh lock (wx file next to the credentials). Returns a release function. Never throws:
144
+ // after the wait it proceeds unlocked, because a stuck lock must not sign anyone out.
145
+ async function acquireRefreshLock(file, { staleMs = 20000, waitMs = 20000 } = {}) {
146
+ const deadline = Date.now() + waitMs;
147
+ for (;;) {
148
+ try {
149
+ closeSync(openSync(file, 'wx', 0o600));
150
+ return () => { try { unlinkSync(file); } catch { /* already gone */ } };
151
+ } catch (e) {
152
+ if (e.code !== 'EEXIST') return () => {};
153
+ try {
154
+ if (Date.now() - statSync(file).mtimeMs > staleMs) { try { unlinkSync(file); } catch { /* raced */ } continue; }
155
+ } catch { continue; }
156
+ if (Date.now() > deadline) return () => {};
157
+ await new Promise(r => setTimeout(r, 25));
158
+ }
159
+ }
160
+ }
161
+
97
162
  export function createAuth(config) {
98
163
  return {
99
164
  async login(baseUrl, email, password, orgId) {
165
+ if (tokenFilePath()) {
166
+ throw new QuoxAuthError('QUOX_TOKEN_FILE is set: this principal runs on its issued token only and cannot log in as a user.');
167
+ }
100
168
  const url = `${baseUrl}/api/auth/login`;
101
- const body = { email, password };
169
+ // client: 'cli' marks this a CLI/TUI sign-in so the server gives it the longer CLI session window.
170
+ const body = { email, password, client: 'cli' };
102
171
  if (orgId) body.org_id = orgId;
103
172
 
104
173
  const res = await fetch(url, {
@@ -129,6 +198,7 @@ export function createAuth(config) {
129
198
  };
130
199
 
131
200
  config.setAuth(authData);
201
+ rememberEmail(config, email);
132
202
  return { ...data, authData };
133
203
  },
134
204
 
@@ -175,7 +245,52 @@ export function createAuth(config) {
175
245
  config.clearAuth();
176
246
  },
177
247
 
248
+ /**
249
+ * Refresh the session. Stale-sign-in safe: serialised across processes by a lock file next to the
250
+ * credentials, and it adopts credentials another window or `quox login` wrote since this process
251
+ * loaded. A fresh token on disk is reused with no server call (so two windows never spend one
252
+ * rotating refresh token twice); a rejected refresh re-reads disk and retries ONCE if it changed.
253
+ */
178
254
  async refresh(baseUrl) {
255
+ if (tokenFilePath() || typeof config.reloadAuth !== 'function' || typeof config.refreshLockFile !== 'function') {
256
+ return this._refreshOnce(baseUrl);
257
+ }
258
+ const lock = await acquireRefreshLock(config.refreshLockFile());
259
+ try {
260
+ if (config.reloadAuth() && config.getAuth()?.access_token && !this.isExpired()) {
261
+ return { access_token: config.getAuth().access_token, reused: true };
262
+ }
263
+ const sent = config.getAuth()?.refresh_token;
264
+ try {
265
+ return await this._refreshOnce(baseUrl);
266
+ } catch (e) {
267
+ if (e?.code === 'session_expired') {
268
+ config.reloadAuth();
269
+ const now = config.getAuth();
270
+ if (now?.refresh_token && now.refresh_token !== sent) {
271
+ if (!this.isExpired()) return { access_token: now.access_token, reused: true };
272
+ return await this._refreshOnce(baseUrl);
273
+ }
274
+ }
275
+ throw e;
276
+ }
277
+ } finally {
278
+ lock();
279
+ }
280
+ },
281
+
282
+ async _refreshOnce(baseUrl) {
283
+ const tokenFile = tokenFilePath();
284
+ if (tokenFile) {
285
+ const before = this._tokenFileLast;
286
+ const now = readTokenFile(tokenFile);
287
+ const stillExpired = now.expires_at && Date.now() > now.expires_at;
288
+ if (stillExpired || (before && now.access_token === before)) {
289
+ throw new QuoxAuthError(`QUOX_TOKEN_FILE ${tokenFile} has not been renewed; the owner-side helper re-mints it. Not falling back to any other credential.`);
290
+ }
291
+ this._tokenFileLast = now.access_token;
292
+ return { access_token: now.access_token };
293
+ }
179
294
  const auth = config.getAuth();
180
295
 
181
296
  // Resident agents authenticate via a signed challenge, not a refresh
@@ -199,14 +314,24 @@ export function createAuth(config) {
199
314
  // 2026-08-24: header -> no_refresh_token, body -> invalid_refresh_token.
200
315
  // The header is kept because it costs nothing and other deployments may
201
316
  // 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
- });
317
+ // QUOXON-FW12: a timeout or network error says nothing about the token.
318
+ // It is TRANSIENT (tokens kept, caller backs off and retries), never
319
+ // "signed out". 2026-10-14 02:50: a slow auth service signed the owner out
320
+ // of a session the server still held active.
321
+ let res;
322
+ try {
323
+ res = await fetch(`${baseUrl}/api/auth/refresh`, {
324
+ method: 'POST',
325
+ headers: {
326
+ 'Content-Type': 'application/json',
327
+ 'Authorization': `Bearer ${auth.refresh_token}`,
328
+ },
329
+ body: JSON.stringify({ refresh_token: auth.refresh_token }),
330
+ signal: AbortSignal.timeout(15000),
331
+ });
332
+ } catch (e) {
333
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable (${e?.name === 'TimeoutError' ? 'refresh timed out' : (e?.cause?.code || e?.message || 'network error')}). Stored session KEPT; retrying.`);
334
+ }
210
335
 
211
336
  let data = null;
212
337
  try { data = await res.json(); } catch { /* non-JSON error body */ }
@@ -221,7 +346,7 @@ export function createAuth(config) {
221
346
  // fault into a lost session, which is exactly how the header/body bug
222
347
  // above cost three re-logins in one night before it was found.
223
348
  if ((data && data.error) === 'no_refresh_token') {
224
- throw new QuoxAuthError(
349
+ throw new QuoxTransientAuthError(
225
350
  'The auth service received no refresh token, so it could not tell whether yours is valid. '
226
351
  + 'Stored session KEPT. This is a client/transport fault, not an expiry — retry, and report it if it persists.'
227
352
  );
@@ -254,18 +379,25 @@ export function createAuth(config) {
254
379
  healthy = false;
255
380
  }
256
381
  if (!healthy) {
257
- throw new QuoxAuthError(
382
+ throw new QuoxTransientAuthError(
258
383
  `Auth service is not healthy right now (refresh returned ${res.status} but /health did not answer OK). ` +
259
384
  `Stored session KEPT — this is very likely a restart or deploy in progress. Retry in a moment.`
260
385
  );
261
386
  }
262
387
  config.clearAuth({ refresh: true });
263
- throw new QuoxAuthError(
388
+ const dead = new QuoxAuthError(
264
389
  `Session expired (refresh rejected: ${(data && data.error) || res.status}). Please login again.`
265
390
  + residentProfileHint(config, auth?.org_id),
266
391
  );
392
+ // Machine-readable, so the TUI can say WHY (idle_timeout | max_session, from the server's session policy).
393
+ dead.code = 'session_expired';
394
+ // A revoke carries no `reason`; keep the error so the card can still say why (SIGNOUT-WIPE).
395
+ dead.reason = (data && data.reason) || ((data && data.error) === 'session_revoked' ? 'session_revoked' : null);
396
+ throw dead;
267
397
  }
268
- throw new QuoxAuthError(`Token refresh failed (${res.status}); stored session kept. Retry, or login again if this persists.`);
398
+ // 5xx / 429 / anything not a definitive 401/403: the token was never judged.
399
+ const ra = Number(res.headers?.get?.('retry-after'));
400
+ throw new QuoxTransientAuthError(`Token refresh failed (${res.status}); stored session kept. Retry, or login again if this persists.`, Number.isFinite(ra) && ra > 0 ? ra * 1000 : null);
269
401
  }
270
402
 
271
403
  auth.access_token = data.access_token;
@@ -279,10 +411,20 @@ export function createAuth(config) {
279
411
  },
280
412
 
281
413
  getTokens() {
414
+ const tokenFile = tokenFilePath();
415
+ if (tokenFile) {
416
+ const t = readTokenFile(tokenFile);
417
+ this._tokenFileLast = t.access_token;
418
+ return t;
419
+ }
282
420
  return config.getAuth();
283
421
  },
284
422
 
285
423
  isLoggedIn() {
424
+ const tokenFile = tokenFilePath();
425
+ if (tokenFile) {
426
+ try { readTokenFile(tokenFile); return true; } catch { return false; }
427
+ }
286
428
  // Precedence, matching client.js: a stored USER token is what we
287
429
  // actually authenticate with, so it decides this answer. A configured
288
430
  // QUOX_SERVICE_KEY still counts as authenticated on its own, but only
@@ -300,6 +442,15 @@ export function createAuth(config) {
300
442
  },
301
443
 
302
444
  isExpired() {
445
+ const tokenFile = tokenFilePath();
446
+ if (tokenFile) {
447
+ try {
448
+ const { expires_at } = readTokenFile(tokenFile);
449
+ return expires_at ? Date.now() > expires_at : false;
450
+ } catch {
451
+ return true;
452
+ }
453
+ }
303
454
  // A forced service key is not a locally-tracked JWT: never "expired".
304
455
  if (process.env.QUOX_FORCE_SERVICE_KEY && process.env.QUOX_SERVICE_KEY) return false;
305
456
  // A stored user token is the credential in play (see isLoggedIn), so
@@ -22,7 +22,7 @@ import { join, basename } from 'node:path';
22
22
  export const APPROVAL_GATED_TOOLS = new Set([
23
23
  'loop_halt', 'slot_assign', 'bob_dispatch_build', 'email_send', 'send_notification',
24
24
  'remote_ssh', 'ssh_exec', 'db_query', 'alerts_manage', 'backup_run',
25
- 'keeper_backup_create', 'deploy_template', 'gemini_generate', 'perplexity_research',
25
+ 'keeper_backup_create', 'deploy_template',
26
26
  'system_admin', 'proxmox_vm_action', 'calendar_event_create',
27
27
  ]);
28
28
 
@@ -0,0 +1,62 @@
1
+ /**
2
+ * KEEPER-REEXEC: which quox build is installed, read from files only (cheap enough to check on
3
+ * every keeper control poll, no git or child process). A git checkout (the dev box runs quox from
4
+ * a detached worktree) reports its HEAD sha; an npm install reports `v<version>`. null = unknown,
5
+ * and an unknown build never triggers a re-exec or a takeover.
6
+ */
7
+ import { readFileSync, existsSync, statSync } from 'node:fs';
8
+ import { dirname, join, resolve, isAbsolute } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const SHA_RE = /^[0-9a-f]{40,64}$/;
12
+ const read = (p) => { try { return readFileSync(p, 'utf-8').trim(); } catch { return null; } };
13
+
14
+ function gitDirFor(root) {
15
+ const dotGit = join(root, '.git');
16
+ if (!existsSync(dotGit)) return null;
17
+ try {
18
+ if (statSync(dotGit).isDirectory()) return dotGit;
19
+ } catch { return null; }
20
+ const m = /^gitdir:\s*(.+)$/m.exec(read(dotGit) || '');
21
+ if (!m) return null;
22
+ return isAbsolute(m[1]) ? m[1] : resolve(root, m[1]);
23
+ }
24
+
25
+ function resolveRef(gitDir, ref) {
26
+ const loose = read(join(gitDir, ref));
27
+ if (loose && SHA_RE.test(loose)) return loose;
28
+ const commonRel = read(join(gitDir, 'commondir'));
29
+ const common = commonRel ? (isAbsolute(commonRel) ? commonRel : resolve(gitDir, commonRel)) : null;
30
+ if (common) {
31
+ const c = read(join(common, ref));
32
+ if (c && SHA_RE.test(c)) return c;
33
+ }
34
+ for (const d of [gitDir, common].filter(Boolean)) {
35
+ const packed = read(join(d, 'packed-refs')) || '';
36
+ for (const line of packed.split('\n')) {
37
+ const [sha, name] = line.trim().split(/\s+/);
38
+ if (name === ref && SHA_RE.test(sha)) return sha;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /** cliDir = the CLI package dir (holds package.json); its parent may be the git root. */
45
+ export function readInstalledBuild(cliDir) {
46
+ try {
47
+ for (const root of [cliDir, dirname(cliDir)]) {
48
+ const gd = gitDirFor(root);
49
+ if (!gd) continue;
50
+ const head = read(join(gd, 'HEAD'));
51
+ if (!head) continue;
52
+ if (SHA_RE.test(head)) return head;
53
+ const m = /^ref:\s*(\S+)$/.exec(head);
54
+ if (m) { const sha = resolveRef(gd, m[1]); if (sha) return sha; }
55
+ }
56
+ const pkg = JSON.parse(readFileSync(join(cliDir, 'package.json'), 'utf-8'));
57
+ return pkg && pkg.version ? `v${pkg.version}` : null;
58
+ } catch { return null; }
59
+ }
60
+
61
+ /** The CLI package this module was loaded from (cli/lib/core -> cli). */
62
+ export const CLI_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
@@ -12,6 +12,7 @@
12
12
  import { resolveActing, resolveActingToken } from './actingAs.js';
13
13
  import { detectCallerSource } from './callerSource.js';
14
14
  import { QuoxApiError, QuoxNetworkError, QuoxAuthError, QuoxTransientAuthError } from './errors.js';
15
+ import { joinServiceUrl, isLocalServiceTarget } from './config.js';
15
16
 
16
17
  const RETRY_STATUSES = new Set([429, 502, 503]);
17
18
  const MAX_RETRIES = 3;
@@ -33,8 +34,9 @@ export function describeNetworkError(service, baseUrl, err) {
33
34
  const code = err?.cause?.code || err?.code;
34
35
  let msg = `Could not connect to ${service} service at ${baseUrl}: ${err.message}`;
35
36
  if (code && TLS_TRUST_CODES.has(code)) {
36
- msg += `\n TLS: certificate not trusted (${code}). For a self-signed dev edge, trust its CA:`
37
- + `\n export NODE_EXTRA_CA_CERTS=~/.config/quox/quox-dev-edge.pem`
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>`
38
40
  + `\n Never disable TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0) instead.`;
39
41
  } else if (code === 'ERR_SSL_WRONG_VERSION_NUMBER' || code === 'EPROTO') {
40
42
  // quox#532 defect 1: the profile says https:// but the service answered
@@ -113,6 +115,14 @@ export function createClient(config, auth) {
113
115
  // never tried again for the rest of its life). A cooldown keeps the
114
116
  // don't-hammer-auth property without the death sentence.
115
117
  const REFRESH_RETRY_COOLDOWN_MS = 60 * 1000;
118
+ // quox#1375: a profile with nothing to refresh with (the collector's quox_cli hop writes only the caller's access
119
+ // token) must surface the route's own 401, not "Session expired... No refresh token", which hid the real cause.
120
+ // A token-file principal re-mints its own file and a resident re-auths by signed challenge, so both still refresh.
121
+ const canRefresh = () => {
122
+ if (process.env.QUOX_TOKEN_FILE) return true;
123
+ const a = typeof config.getAuth === 'function' ? config.getAuth() : null;
124
+ return !!(a && (a.refresh_token || a.kind === 'resident'));
125
+ };
116
126
  let refreshFailedAt = 0;
117
127
  const correlationId = generateCorrelationId();
118
128
 
@@ -128,10 +138,13 @@ export function createClient(config, auth) {
128
138
  noAuth = false,
129
139
  noActAs = false,
130
140
  sensitive = false,
141
+ // HYPERION #23: an outside-caller request (trigger webhook-fire) also drops the caller labels
142
+ // (X-Quox-Session / X-Quox-Profile): the receiving route may store every header in an org-readable record.
143
+ anonymous = false,
131
144
  } = options;
132
145
 
133
146
  const baseUrl = config.getServiceUrl(service);
134
- const url = new URL(path, baseUrl);
147
+ const url = joinServiceUrl(baseUrl, path);
135
148
 
136
149
  if (query) {
137
150
  for (const [key, value] of Object.entries(query)) {
@@ -142,7 +155,7 @@ export function createClient(config, auth) {
142
155
  const fetchHeaders = {
143
156
  'X-Quox-Correlation': correlationId,
144
157
  'X-Quox-Client': 'cli',
145
- ...callerContextHeaders(config),
158
+ ...(anonymous ? {} : callerContextHeaders(config)),
146
159
  ...extraHeaders,
147
160
  };
148
161
 
@@ -177,7 +190,7 @@ export function createClient(config, auth) {
177
190
  let usingServiceKey = false;
178
191
  // quox#772: opt-in acting identity (--as / QUOX_AS). Replaces the bearer
179
192
  // with the agent's attach token; throws (no request sent) if not attached.
180
- const acting = !noAuth && !noActAs ? await resolveActing() : { name: null };
193
+ const acting = !noAuth && !noActAs ? await resolveActing({ explicitProfile: Boolean(config._explicitProfile) }) : { name: null };
181
194
  const actingAs = acting.name;
182
195
  if (actingAs) {
183
196
  const heartbeat = {
@@ -195,7 +208,10 @@ export function createClient(config, auth) {
195
208
  usingServiceKey = true;
196
209
  } else if (tokens?.access_token) {
197
210
  fetchHeaders['Authorization'] = `Bearer ${tokens.access_token}`;
198
- } else if (serviceKey) {
211
+ } else if (serviceKey && isLocalServiceTarget(url.href)) {
212
+ // QUOXON-Z2H lane R: the implicit fallback is THIS box's key; never
213
+ // offer it to another host (a remote profile with no login). A caller
214
+ // that owns a remote key says so with QUOX_FORCE_SERVICE_KEY.
199
215
  fetchHeaders['X-Service-Key'] = serviceKey;
200
216
  usingServiceKey = true;
201
217
  }
@@ -218,6 +234,10 @@ export function createClient(config, auth) {
218
234
  }
219
235
 
220
236
  let lastError;
237
+ let staleReloaded = false;
238
+ // REL-424: one refresh per request, on whichever attempt the 401 lands. A token that expires while the
239
+ // client sleeps between 503 retries gets its 401 on attempt 1+, and gating refresh on attempt 0 surfaced it.
240
+ let refreshedThisRequest = false;
221
241
 
222
242
  for (let attempt = 0; attempt <= retries; attempt++) {
223
243
  try {
@@ -255,10 +275,32 @@ export function createClient(config, auth) {
255
275
  // profile over what is really just "wrong token for this route".
256
276
  // In that case, skip the refresh and fall through to the normal
257
277
  // error handling below so the original 401/403 is surfaced as-is.
258
- const looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
278
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
279
+ // The SERVER saying "invalid or expired token" outranks the local clock. expires_at is
280
+ // stamped after the refresh response lands, so for a moment the server rejects a token
281
+ // the client still thinks is good. 2026-10-14 07:32:03: three windows hit that gap
282
+ // together, skipped the refresh and declared the profile signed out. Refresh is safe
283
+ // here: a dead refresh token is still judged by auth.refresh() itself.
284
+ if (res.status === 401 && !looksExpired && !noAuth && !actingAs && !usingServiceKey) {
285
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
286
+ }
287
+
288
+ // Stale sign-in: a sibling window or `quox login` may have written a newer token for this profile.
289
+ // Adopt it and retry this request ONCE before any refresh (which would spend our stale refresh token).
290
+ if (res.status === 401 && !noAuth && !actingAs && !usingServiceKey && !staleReloaded
291
+ && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
292
+ staleReloaded = true;
293
+ const fresh = auth.getTokens?.();
294
+ if (fresh?.access_token) {
295
+ fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
296
+ attempt--; // this retry is free: it does not spend the caller's retry budget
297
+ continue;
298
+ }
299
+ }
259
300
 
260
301
  const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
261
- if (res.status === 401 && !noAuth && !actingAs && attempt === 0 && !refreshCoolingDown && looksExpired) {
302
+ if (res.status === 401 && !noAuth && !actingAs && !refreshedThisRequest && !refreshCoolingDown && looksExpired && canRefresh()) {
303
+ refreshedThisRequest = true;
262
304
  try {
263
305
  if (!refreshing) {
264
306
  const serviceUrl = config.getServiceUrl('auth');
@@ -288,16 +330,25 @@ export function createClient(config, auth) {
288
330
  if (refreshErr && refreshErr.transient) {
289
331
  // Auth is only temporarily unavailable (429 / restart): no
290
332
  // cooldown, and keep the type so a listener can back off.
291
- throw new QuoxTransientAuthError(`Re-authentication temporarily unavailable. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
333
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retrying. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
292
334
  }
293
335
  refreshFailedAt = Date.now();
294
336
  const why = refreshErr?.message ? ` ${refreshErr.message}` : '';
295
- throw new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${why}`);
337
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${why}`);
338
+ if (refreshErr?.code) dead.code = refreshErr.code;
339
+ if (refreshErr?.reason) dead.reason = refreshErr.reason;
340
+ throw dead;
296
341
  }
297
342
  }
298
343
 
299
344
  // Retry transient errors
345
+ // quox#882: the server can say a failure won't clear by itself (a downstream refusing
346
+ // connections): body retryable:false skips the backoff retries.
347
+ let noRetry = false;
300
348
  if (RETRY_STATUSES.has(res.status) && attempt < retries) {
349
+ try { noRetry = (await res.clone().json())?.retryable === false; } catch { /* not JSON */ }
350
+ }
351
+ if (RETRY_STATUSES.has(res.status) && attempt < retries && !noRetry) {
301
352
  const delay = RETRY_DELAYS[attempt] || 9000;
302
353
  if (isDebug) {
303
354
  console.error(` Retrying in ${delay}ms (attempt ${attempt + 1}/${retries})...`);
@@ -315,7 +366,8 @@ export function createClient(config, auth) {
315
366
  try {
316
367
  data = await res.json();
317
368
  } catch {
318
- data = { error: 'unknown', message: res.statusText };
369
+ // CLI-ERRORS: a proxy error page, not a Quox body; named so the renderer can say so
370
+ data = { error: 'non_json_response', message: res.statusText };
319
371
  }
320
372
  // A permission error while we silently fell back to the SERVICE
321
373
  // KEY is almost never a permissions problem: the service key
@@ -327,7 +379,10 @@ export function createClient(config, auth) {
327
379
  // theory about who could grant it. He had held instance-admin
328
380
  // since March. His session had simply expired and the CLI had
329
381
  // quietly downgraded the request to a key with no user attached.
330
- let message = data.message || `Request failed with status ${res.status}`;
382
+ // CLI-ERRORS: a body with only a prose `error` ("Agent not found") leads with that, never
383
+ // "Request failed with status N" (the real reason used to sit on the dim Status line).
384
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
385
+ let message = data.message || (proseError ? data.error : `Request failed with status ${res.status}`);
331
386
  // quox#617: surface the server's machine-readable `reason`. A vault
332
387
  // update returns 403 with `reason: 'mfa_required'` for an MFA-enabled
333
388
  // owner (a step-up gate) vs `reason: 'role_<r>_lacks_capability_<cap>'`
@@ -344,18 +399,38 @@ export function createClient(config, auth) {
344
399
  + `The service key has no user identity, so anything gated on WHO you are will refuse it regardless of your real permissions. `
345
400
  + `Run \`quox login\` (or \`quox -p <profile> login\`) and retry before treating this as a permissions problem.`;
346
401
  }
402
+ // #1012: the paid backends (QuoxPlan, QuoxSEO, QLarity, QuoxSignal) are not part of the public
403
+ // install, so a licensed customer got a bare 502 from the collector proxy. This client cannot
404
+ // tell "not installed" from "installed but down", so it says both, and exits 1 (not 8).
405
+ const pluginDown = (res.status === 502 || res.status === 503) && typeof data.error === 'string'
406
+ && /^(QuoxPlan|QuoxSEO|QLarity|QuoxSignal) service unavailable$/i.exec(data.error);
407
+ if (pluginDown) {
408
+ const err = new QuoxApiError(
409
+ res.status,
410
+ 'PLUGIN_SERVICE_UNAVAILABLE',
411
+ `${pluginDown[1]} isn't reachable on this server. It isn't part of the public install (it ships separately), `
412
+ + 'or it is installed but down: run `quox health`. Nothing was changed.',
413
+ data.detail || data.details,
414
+ data
415
+ );
416
+ err.exitCode = 1;
417
+ throw err;
418
+ }
347
419
  // LL-3d: carry the WHOLE body, not just message/detail. Endpoints
348
420
  // that answer with structured context (the licensing 409's
349
421
  // bound_instance_name / bound[] / max_servers, the 402's plugin)
350
422
  // had it flattened away here, so command handlers could only print
351
423
  // "Request failed with status 409".
352
- throw new QuoxApiError(
424
+ const apiErr = new QuoxApiError(
353
425
  res.status,
354
426
  data.error || 'api_error',
355
427
  message,
356
428
  data.detail || data.details,
357
429
  data
358
430
  );
431
+ if (proseError) apiErr.reasonFromError = true;
432
+ if (usingServiceKey && (res.status === 403 || res.status === 401)) apiErr.serviceKeyFallback = true;
433
+ throw apiErr;
359
434
  }
360
435
 
361
436
  // Handle empty responses (204 No Content)
@@ -431,7 +506,7 @@ export function createClient(config, auth) {
431
506
  } = options;
432
507
 
433
508
  const baseUrl = config.getServiceUrl(service);
434
- const url = new URL(path, baseUrl);
509
+ const url = joinServiceUrl(baseUrl, path);
435
510
 
436
511
  if (query) {
437
512
  for (const [key, value] of Object.entries(query)) {
@@ -449,7 +524,7 @@ export function createClient(config, auth) {
449
524
  // Same precedence as request() above, including the QUOX_FORCE_SERVICE_KEY
450
525
  // override for daemons that own their key and must not borrow the shared
451
526
  // config's stored session.
452
- const streamActingR = await resolveActing();
527
+ const streamActingR = await resolveActing({ explicitProfile: Boolean(config._explicitProfile) });
453
528
  const streamActing = streamActingR.name;
454
529
  const streamTokens = auth.getTokens();
455
530
  let streamServiceKey = null;
@@ -462,7 +537,7 @@ export function createClient(config, auth) {
462
537
  fetchHeaders['X-Service-Key'] = streamServiceKey;
463
538
  } else if (streamTokens?.access_token) {
464
539
  fetchHeaders['Authorization'] = `Bearer ${streamTokens.access_token}`;
465
- } else if (process.env.QUOX_SERVICE_KEY) {
540
+ } else if (process.env.QUOX_SERVICE_KEY && isLocalServiceTarget(url.href)) {
466
541
  streamServiceKey = process.env.QUOX_SERVICE_KEY;
467
542
  fetchHeaders['X-Service-Key'] = streamServiceKey;
468
543
  }
@@ -472,22 +547,61 @@ export function createClient(config, auth) {
472
547
  console.error(`-> ${streamBody !== undefined ? 'POST' : 'GET'}(stream) ${url.toString()}`);
473
548
  }
474
549
 
475
- let res;
476
- try {
477
- res = await fetch(url.toString(), {
478
- method: streamBody !== undefined ? 'POST' : 'GET',
479
- headers: fetchHeaders,
480
- ...(streamBody !== undefined ? { body: JSON.stringify(streamBody) } : {}),
481
- signal: externalSignal,
482
- // Same redirect protection as the non-streaming path above: never let
483
- // a 3xx resend X-Service-Key to a host we did not choose.
484
- ...(streamServiceKey ? { redirect: 'manual' } : {}),
485
- });
486
- } catch (err) {
487
- throw new QuoxNetworkError(
488
- describeNetworkError(service, baseUrl, err),
489
- err
490
- );
550
+ const openStream = async () => {
551
+ try {
552
+ return await fetch(url.toString(), {
553
+ method: streamBody !== undefined ? 'POST' : 'GET',
554
+ headers: fetchHeaders,
555
+ ...(streamBody !== undefined ? { body: JSON.stringify(streamBody) } : {}),
556
+ signal: externalSignal,
557
+ // Same redirect protection as the non-streaming path above: never let
558
+ // a 3xx resend X-Service-Key to a host we did not choose.
559
+ ...(streamServiceKey ? { redirect: 'manual' } : {}),
560
+ });
561
+ } catch (err) {
562
+ throw new QuoxNetworkError(
563
+ describeNetworkError(service, baseUrl, err),
564
+ err
565
+ );
566
+ }
567
+ };
568
+ let res = await openStream();
569
+
570
+ // 2026-10-16: request() refreshed an expired session on a 401, the stream (the chat path) did not,
571
+ // so a chat turn right after the access token expired read "Not signed in" on a signed-in profile.
572
+ // Same rule as request(): one refresh for a user token that looks expired (locally, or the server
573
+ // says invalid_token), shared with any refresh already in flight, then one retry.
574
+ const usesUserToken = !streamActing && !streamServiceKey && !!streamTokens?.access_token;
575
+ if (res.status === 401 && usesUserToken && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
576
+ // Stale sign-in: a sibling window or `quox login` wrote a newer token. Adopt it, retry once.
577
+ const fresh = auth.getTokens?.();
578
+ if (fresh?.access_token) { fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`; res = await openStream(); }
579
+ }
580
+ if (res.status === 401 && usesUserToken) {
581
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
582
+ if (!looksExpired) {
583
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
584
+ }
585
+ const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
586
+ if (looksExpired && !refreshCoolingDown && canRefresh()) {
587
+ try {
588
+ if (!refreshing) {
589
+ refreshing = auth.refresh(config.getServiceUrl('auth')).finally(() => { refreshing = null; });
590
+ }
591
+ await refreshing;
592
+ } catch (refreshErr) {
593
+ if (refreshErr && refreshErr.transient) {
594
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retry shortly. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
595
+ }
596
+ refreshFailedAt = Date.now();
597
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${refreshErr?.message ? ` ${refreshErr.message}` : ''}`);
598
+ dead.status = 401;
599
+ throw dead;
600
+ }
601
+ const fresh = auth.getTokens();
602
+ if (fresh?.access_token) fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
603
+ res = await openStream();
604
+ }
491
605
  }
492
606
 
493
607
  if (!res.ok) {
@@ -495,15 +609,18 @@ export function createClient(config, auth) {
495
609
  try {
496
610
  data = await res.json();
497
611
  } catch {
498
- data = { error: 'unknown', message: res.statusText };
612
+ data = { error: 'non_json_response', message: res.statusText };
499
613
  }
500
- throw new QuoxApiError(
614
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
615
+ const streamErr = new QuoxApiError(
501
616
  res.status,
502
617
  data.error || 'api_error',
503
- data.message || `Stream request failed with status ${res.status}`,
618
+ data.message || (proseError ? data.error : `Stream request failed with status ${res.status}`),
504
619
  data.detail || data.details,
505
620
  data
506
621
  );
622
+ if (proseError) streamErr.reasonFromError = true;
623
+ throw streamErr;
507
624
  }
508
625
 
509
626
  if (!res.body) {