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/quoxon/api.js CHANGED
@@ -3,13 +3,32 @@
3
3
  * driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
4
4
  */
5
5
  import { resolveEndpoint } from '../api/endpoints.js';
6
+ import { rememberEmail } from '../core/auth.js';
7
+ import { isSessionDead } from './sessionDead.js'; // not signin.js: that loads React + Ink (QUOXON-LATENCY)
6
8
  import { buildStreamQuery } from '../commands/chat.js';
7
9
  import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
10
+ import { bundledIdentities } from './objects/fetch.js';
8
11
  import { recordHeldSlot } from '../commands/slot.js';
9
12
  import { askGripe, settleGripe } from '../commands/gripe.js';
13
+ import { sendFeedback } from './feedback.js';
10
14
  import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
11
15
  import { normChoice } from './model.js';
16
+ import { normPrefs as normModelPrefs } from './modelPrefs.js';
17
+ import { createRowsStore } from './rowsStore.js';
12
18
  import { createObjectsApi } from './objects/fetch.js';
19
+ import { createDragonReads } from './dragonReads.js';
20
+ import { createKillerApi } from './slash/killerApi.js';
21
+ import { createUndoOps } from './slash/undoApi.js';
22
+ import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
23
+ import { grantBody } from './consent.js';
24
+ import { loadJourney } from './onboarding/journey.js';
25
+ import { joinServiceUrl } from '../core/config.js';
26
+ import { runVmCommand } from '../core/vmTui.js';
27
+ import { fetchActivity, heldLines } from '../core/hyperion.js';
28
+ import { createPeekFetchers, peekId } from '../core/peek.js';
29
+ import { askAside } from '../core/aside.js';
30
+ import { exportConversationBundle } from '../evidence/bundleExport.js';
31
+ import { addActionItem, listBoard, moveActionItem } from '../core/actionItems.js';
13
32
 
14
33
  const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
15
34
  /** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
@@ -48,12 +67,61 @@ async function* readTurn(open, cur, signal) {
48
67
  const sleepFor = (ms, signal) => new Promise((r) => { if (signal?.aborted) return r(); const t = setTimeout(done, ms); function done() { clearTimeout(t); signal?.removeEventListener?.('abort', done); r(); } signal?.addEventListener?.('abort', done, { once: true }); });
49
68
  const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : null; };
50
69
 
51
- export function createApi({ client, config, auth, reattachBackoff = REATTACH_BACKOFF_MS }) {
70
+ export const SLOTS_UNLICENSED = 'Slots need a licence. This Quox does not have one, so /slot is off. See: quox setup license status';
71
+ // Z2H run-6: every slot write (checkout, /say, /broadcast) names the licence reason the same way.
72
+ /** #1288: a refused vault write, keeping the server's "confirm this private address" hint. */
73
+ const vaultFailure = (e) => ({ ok: false, status: e?.status, message: e?.message || 'failed', ...(e?.body?.confirmable === true ? { confirmable: true, origin: typeof e.body.origin === 'string' ? e.body.origin : null } : {}) });
74
+ const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
75
+
76
+ /** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
77
+ const NOTES_OFF = 'NotesQ is not available for this org. Ask an org admin to enable the NotesQ plugin (dashboard, Plugins), or check `quox notes ls` in a terminal.';
78
+ export function noteParts(text) {
79
+ const t = String(text || '').trim();
80
+ const first = t.split('\n')[0].trim();
81
+ const title = first.length > 60 ? `${first.slice(0, 60).trimEnd()}…` : first;
82
+ return { title, markdown: `# ${title}\n\n${t}\n` };
83
+ }
84
+ async function createNoteFrom(client, text) {
85
+ const { title, markdown } = noteParts(text);
86
+ try {
87
+ const ep = resolveEndpoint('notesq.notes.create');
88
+ const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { scope: 'org', markdown, properties: {} } });
89
+ const n = d?.note || {};
90
+ if (!n.id) return { ok: false, message: 'NotesQ answered without a note id, so nothing is confirmed saved. Check quox notes ls.' };
91
+ return { ok: true, id: n.id, title: n.title || title };
92
+ } catch (e) {
93
+ if (e?.status === 402) return { ok: false, message: 'NotesQ is not licensed for this org. ' + NOTES_OFF };
94
+ if (e?.status === 403 || e?.status === 404 || e?.status === 503) return { ok: false, message: NOTES_OFF };
95
+ return { ok: false, message: `Could not save the note: ${e?.message || 'request failed'}${e?.status ? ` (HTTP ${e.status})` : ''}.` };
96
+ }
97
+ }
98
+ export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
99
+ return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
100
+ }
101
+
102
+ /** A one-line title within the tasks service's 200 character cap. */
103
+ const clipTitle = (g) => { const t = String(g ?? '').replace(/\s+/g, ' ').trim(); return t.length > 200 ? `${t.slice(0, 199)}…` : t; };
104
+
105
+ export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
106
+ let holderSessionFn = null;
52
107
  const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
108
+ const rowsStore = createRowsStore({ client, config, orgId });
109
+ let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
53
110
  const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
111
+ /** Z2H-R3: the server's public app URL (instance_config app_base_url), or null (not admin, old server, unset). Never throws. */
112
+ const serverAppUrl = async () => {
113
+ try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return r?.effective?.appBaseUrl || null; } catch { return null; }
114
+ };
115
+ const journeyUrl = () => { try { return loadJourney()?.dashboardUrl || null; } catch { return null; } };
54
116
  const listApprovals = async () => {
55
- const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
56
- const data = await client.get(ep.path, { service: ep.service, query: { status: 'open', kind: 'approval', limit: 20 } });
117
+ // quox#827: the set comes from the server's one open-approvals function (the same one behind the dashboard badge and LUCID).
118
+ const ep = resolveEndpoint('inbox.approvals.open', { orgId: orgId() });
119
+ try {
120
+ const data = await client.get(ep.path, { service: ep.service });
121
+ if (Array.isArray(data?.approvals)) return data.approvals.map((a) => a.item).filter(Boolean);
122
+ } catch (e) { if (!missing(e)) throw e; } // an older server without the route: fall back to the inbox list below
123
+ const old = resolveEndpoint('inbox.list', { orgId: orgId() });
124
+ const data = await client.get(old.path, { service: old.service, query: { kind: 'approval', include_snoozed: 'true', limit: 200 } }); // status omitted = every ACTIVE status
57
125
  const items = data.items || data.data || data || [];
58
126
  return Array.isArray(items) ? items : [];
59
127
  };
@@ -93,11 +161,99 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
93
161
  err.code = 'reattach_failed';
94
162
  throw err;
95
163
  }
164
+ /** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
165
+ async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
166
+ if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
167
+ try {
168
+ const ep = resolveEndpoint('presence.activity');
169
+ const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
170
+ return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
171
+ } catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
172
+ }
96
173
  return {
174
+ ...createKillerApi({ client }),
97
175
  orgId,
176
+ undoOps: createUndoOps({ client, orgId }), // /undo
177
+ /** /note: same call as `quox notes new` (org scope). -> { ok, id, title } | { ok:false, message } */
178
+ createNote: (text) => createNoteFrom(client, text),
98
179
  userId,
180
+ /** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
181
+ peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
182
+ /** /evidence, /ward: the signed bundle of one conversation, written to ~/quox-evidence (same export as `quox evidence bundle`). */
183
+ evidenceExport: (corr) => exportConversationBundle({ client, corr }),
184
+ /** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
185
+ aside: (question, turns = []) => askAside(client, { question, turns }),
186
+ /** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
187
+ async mentionHosts() {
188
+ const ep = resolveEndpoint('v1.fleet.hosts.list');
189
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
190
+ return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
191
+ },
192
+ /** QUOXON objective offer: `quox objective create <goal> --checkpoint` + `start` (same endpoints). -> { id, status } */
193
+ async startObjective(goal, loop = null) {
194
+ const c = resolveEndpoint('objectives.create');
195
+ // QLOOP: a loop offer creates a LOOP (server-derived scope + goal), never a plan under the loop's name.
196
+ const body = loop ? { title: String(goal).slice(0, 200), description: String(goal), execution_mode: 'loop', capability_scope: loop.capability_scope, goal_condition: loop.goal_condition,
197
+ ...(loop.until ? { until: loop.until } : {}), ...(loop.every_seconds ? { every_seconds: loop.every_seconds } : {}), ...(loop.max_iterations ? { max_iterations: loop.max_iterations } : {}), ...(loop.max_duration_seconds ? { max_duration_ms: loop.max_duration_seconds * 1000 } : {}), ...(Array.isArray(loop.done_check) && loop.done_check.length ? { done_check: loop.done_check } : {}) }
198
+ : { title: clipTitle(goal), description: String(goal), checkpoint_after_each_task: true }; // the tasks service caps a title at 200; the whole ask rides in the description
199
+ const made = await client.post(c.path, { service: c.service, body });
200
+ if (!made?.id) throw new Error('the server answered without an objective id');
201
+ const st = resolveEndpoint('objectives.start', { id: made.id });
202
+ const started = await client.post(st.path, { service: st.service, body: {} });
203
+ return { id: made.id, status: started?.status || 'started' };
204
+ },
205
+ /** An objective with its tasks (the same read as `quox objective get`), for the chat's report-back. */
206
+ async objectiveGet(id) {
207
+ const ep = resolveEndpoint('objectives.get', { id: encodeURIComponent(id) });
208
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
209
+ return d?.objective || d;
210
+ },
211
+ /** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
212
+ async mentionTasks() {
213
+ const ep = resolveEndpoint('objectives.list');
214
+ const d = await client.get(ep.path, { service: ep.service, query: { limit: 30 }, retries: 0, timeout: 8000 });
215
+ return (listOf(d, 'objectives', 'data') || []).map(o => ({ id: o.id, title: o.title || '', status: o.status || '' }));
216
+ },
217
+ /** Z2H: the same login path as `quox login` (createAuth().login). Returns only what the card needs; tokens are stored by auth, never surfaced. */
218
+ async signIn(email, password) {
219
+ const r = await auth.login(config.getServiceUrl('auth'), email, password);
220
+ return { mfa_required: Boolean(r.mfa_required), access_token: r.mfa_required ? r.access_token : undefined, user: r.user ? { email: r.user.email } : null };
221
+ },
222
+ verifyMfa: (token, code) => auth.verifyMfa(config.getServiceUrl('auth'), token, code),
223
+ /** The last email this profile signed in with (prefill for /login), or ''. */
224
+ lastEmail() { try { return config.getAuth?.()?.email || config.getActiveProfile?.()?.last_email || ''; } catch { return ''; } },
225
+ /**
226
+ * Startup/ongoing check: is the stored session dead? Only a locally expired token is probed, via the refresh seam.
227
+ * -> { alive: true } | { alive: false, reason: 'idle_timeout'|'max_session'|null }. A transient failure reads as alive (we learned nothing).
228
+ */
229
+ async probeSession() {
230
+ try {
231
+ if (!auth.isExpired?.()) return { alive: true };
232
+ try { rememberEmail(config, config.getAuth?.()?.email); } catch { /* best-effort */ }
233
+ await auth.refresh(config.getServiceUrl('auth'));
234
+ return { alive: true };
235
+ } catch (e) {
236
+ return isSessionDead(e) ? { alive: false, reason: e.reason || null } : { alive: true };
237
+ }
238
+ },
239
+ /**
240
+ * Stale sign-in: true once each time the credentials file changed on disk since the last call (another window
241
+ * or `quox login` wrote it). A cheap stat; the first call only records the baseline.
242
+ */
243
+ credentialsChanged() {
244
+ try {
245
+ const m = config.credentialsMtime?.() || 0;
246
+ const was = credsSeen; credsSeen = m;
247
+ return was !== null && m !== was;
248
+ } catch { return false; }
249
+ },
250
+ /** Re-read this profile's credentials from disk. -> true when a signed-in profile is there to retry with. */
251
+ reloadCredentials() {
252
+ try { config.reloadAuth?.(); return Boolean(config.getAuth?.()?.access_token); } catch { return false; }
253
+ },
99
254
  /** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
100
- objects: createObjectsApi({ client, orgId }),
255
+ objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
256
+ dragonReads: createDragonReads({ client }), // the companion dragons' jobs: bounded read-only reads (dragonReads.js)
101
257
  /** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
102
258
  modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
103
259
  setModelChoice(choice) {
@@ -119,6 +275,46 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
119
275
  await client.patch(ep.path, { service: ep.service, body: { preferences: { soundProfile: value } } });
120
276
  return true;
121
277
  },
278
+ /**
279
+ * QUOXON-FW6: model favourites + default. The server is the source of truth: the user's
280
+ * `quoxonModels` preference (PATCH /api/auth/me merges top-level namespaces, the same store as
281
+ * soundProfile). The active CLI profile keeps a copy (model_prefs) for offline starts.
282
+ * -> { prefs, source: 'server' | 'local', error? }.
283
+ */
284
+ async modelPrefs() {
285
+ const local = () => normModelPrefs(config.getActiveProfile?.()?.model_prefs);
286
+ try {
287
+ const ep = resolveEndpoint('auth.me.list');
288
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
289
+ const raw = (d?.user?.preferences ?? d?.preferences)?.quoxonModels;
290
+ const prefs = normModelPrefs(raw);
291
+ const prof = config.getActiveProfile?.();
292
+ if (prof && raw !== undefined) { prof.model_prefs = prefs; config.save?.(); }
293
+ const useLocal = raw === undefined && local().favourites.length;
294
+ return { prefs: useLocal ? local() : prefs, source: 'server' };
295
+ } catch (e) { return { prefs: local(), source: 'local', error: e?.message || 'failed' }; }
296
+ },
297
+ /** -> { ok, synced, error? }: the local copy is always written; synced=false: refused. */
298
+ async setModelPrefs(value) {
299
+ const prefs = normModelPrefs(value);
300
+ const prof = config.getActiveProfile?.();
301
+ if (prof) { prof.model_prefs = prefs; config.save?.(); }
302
+ try {
303
+ const ep = resolveEndpoint('auth.me.update');
304
+ const body = { preferences: { quoxonModels: prefs } };
305
+ await client.patch(ep.path, { service: ep.service, body, retries: 0 });
306
+ return { ok: true, synced: true };
307
+ } catch (e) { return { ok: Boolean(prof), synced: false, error: e?.message || 'failed' }; }
308
+ },
309
+ localModelPrefs() {
310
+ try { return normModelPrefs(config.getActiveProfile?.()?.model_prefs); }
311
+ catch { return normModelPrefs(null); }
312
+ },
313
+ /**
314
+ * QUOXON-FW17 / ROWS: custom rows. url, quox and plugin rows sync per user (preference namespace `quoxonRows`);
315
+ * cmd rows stay in a local file. One implementation, shared with `quox rows` (rowsStore.js).
316
+ */
317
+ ...rowsStore,
122
318
  /** Local copy on the active CLI profile: offline start + fallback store. */
123
319
  localSoundProfile() { try { return config.getActiveProfile?.()?.sound_profile || null; } catch { return null; } },
124
320
  setLocalSoundProfile(p) { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.sound_profile = p; config.save?.(); return true; },
@@ -129,17 +325,21 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
129
325
  * by seq. While reconnecting it yields { type:'reconnecting', attempt } and, once back, { type:'resumed', after }.
130
326
  * A server without v1.1 (no seq on its events) gets the v1 behaviour: no reattach.
131
327
  */
132
- async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, durable = true }, signal) {
328
+ async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, heldSlot = null, durable = true, overrideId = undefined, turnBudgetMs = undefined, mode = undefined, handoffFrom = undefined }, signal) {
133
329
  const body = buildStreamQuery({ queryId: turnId, message, agent: agent || 'quox', sessionId: conversationId, context });
134
330
  body.turn_id = turnId;
135
331
  if (durable) body.durable = true; // the server keeps the turn running when we disconnect (v1.1)
332
+ if (Number(turnBudgetMs) > 0) body.turn_budget_ms = Math.floor(Number(turnBudgetMs)); // quox#1090: a delegation hop may use the turn's budget
136
333
  if (provider) body.provider = provider; // a request: the server policy decides, turn.started says what ran
137
334
  if (provider && model) body.model = model;
138
335
  if (promptProfile === 'compact' || promptProfile === 'full') body.prompt_profile = promptProfile; // D3: server default when absent
139
336
  if (conversationId) body.conversation_id = conversationId;
337
+ if (mode === 'plan') body.mode = 'plan'; // PLAN MODE: the server makes no state-changing tool eligible and refuses writes
338
+ if (heldSlot?.name && heldSlot?.session) body.held_slot = heldSlot; // QUOXON-MESH: slot_say signs as this window's slot
339
+ if (handoffFrom) body.handoff_from = String(handoffFrom).slice(0, 80); // a handed-off turn: this agent stays attached, the server labels the exchange for it
140
340
  const ep = resolveEndpoint('chat.stream');
141
341
  const cur = { lastSeq: 0, finished: false, conversationId: conversationId || null };
142
- const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true }), cur, signal);
342
+ const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true, ...(overrideId ? { headers: { 'x-override-id': overrideId } } : {}) }), cur, signal);
143
343
  if (cur.finished || !drop || !durable || signal?.aborted) return;
144
344
  if (!cur.lastSeq || !cur.conversationId) { if (drop.error) throw drop.error; return; } // v1 server: nothing to reattach to
145
345
  yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
@@ -208,20 +408,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
208
408
  const roster = loadBundledAssistantsRoster();
209
409
  const nameOf = new Map(roster.map(r => [r.id, r.name || r.shortName || r.id]));
210
410
  const out = new Map(); let ok = 0; let lastErr = null;
411
+ let ids = null; const roleOf = (id) => { ids ??= bundledIdentities(); const i = ids.get(id); return i?.tagline || i?.description || null; };
211
412
  try {
212
413
  const ep = resolveEndpoint('v1.tools.agents.list');
213
- const d = await client.get(ep.path, { service: ep.service, noAuth: true, retries: 0 });
214
- for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id });
414
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 }); // authenticated: the collector's default-deny gate 401s a noAuth read
415
+ for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id, role: roleOf(id), status: 'registered' });
215
416
  ok++;
216
- } catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id) }); if (roster.length) ok++; }
417
+ } catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id), role: roleOf(r.id) }); if (roster.length) ok++; }
217
418
  try {
218
419
  const ep = resolveEndpoint('agents.list', { orgId: orgId() });
219
420
  const rows = listOf(await client.get(ep.path, { service: ep.service, query: { includeTools: 'false' }, retries: 0 }), 'agents', 'data', 'items');
220
- for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id });
421
+ for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id, role: r.description || null, status: r.status || null });
221
422
  if (rows) ok++;
222
423
  } catch (e) { lastErr = lastErr || e; }
223
424
  if (!ok) throw lastErr || new Error('no agent list available');
224
- return [...out.values()];
425
+ const list = [...out.values()];
426
+ // The live registry failed and the bundled roster stood in: say so, so a miss is never read as "no such agent".
427
+ if (lastErr) Object.defineProperty(list, 'listError', { value: lastErr });
428
+ return list;
225
429
  },
226
430
  /** -> saved turns of one of YOUR conversations, oldest first [{ id, role, content }]; [] when none or not yours. */
227
431
  async conversationTurns(conversationId) {
@@ -236,6 +440,24 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
236
440
  if (!d || !Array.isArray(d.groups)) throw new Error('unexpected response');
237
441
  return d;
238
442
  },
443
+ /** GET /api/consent: this user's AI data sharing record ({ ai_data_sharing_consent, provider_consent }). */
444
+ async consentStatus() {
445
+ const ep = resolveEndpoint('consent.list');
446
+ return client.get(ep.path, { service: ep.service, retries: 0 });
447
+ },
448
+ /** Grant AI data sharing consent for one provider (the user's own act; keeps the other providers' settings). */
449
+ async grantConsent(provider) {
450
+ const lst = resolveEndpoint('consent.list');
451
+ let cur = null; try { cur = await client.get(lst.path, { service: lst.service, retries: 0 }); } catch { cur = null; }
452
+ const ep = resolveEndpoint('consent.create');
453
+ await client.post(ep.path, { service: ep.service, body: grantBody(cur, provider), retries: 0 });
454
+ return true;
455
+ },
456
+ /** The dashboard address to print (never localhost when a better one is known). */
457
+ async dashboardBase() {
458
+ const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
459
+ return resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
460
+ },
239
461
  /** POST /api/v1/conversations/:id/compact (server-side summary; a model call, so a long timeout and never retried). */
240
462
  async compactConversation(conversationId) {
241
463
  return client.post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, { service: 'collector', body: {}, retries: 0, timeout: 180000 });
@@ -254,6 +476,57 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
254
476
  },
255
477
  /** The `quox doctor` checks, run in this process. */
256
478
  async doctorChecks() { const { runDoctorChecks } = await import('../commands/doctor.js'); return runDoctorChecks({ config }); },
479
+ /** C4: the `quox server status` panel rows for the local install. -> { ok, rows } | { ok:false, error }. */
480
+ async serverStatus() {
481
+ const { createSys } = await import('../server/sys.js');
482
+ const { resolveInstall, backupsRoot } = await import('../server/install.js');
483
+ const { gatherStatus, statusRows, defaultUrls } = await import('../server/status.js');
484
+ const sys = createSys();
485
+ const install = await resolveInstall({ sys });
486
+ if (install.error) return { ok: false, error: install.error };
487
+ const st = await gatherStatus({ install, sys, urls: defaultUrls(config), backups: backupsRoot() });
488
+ return { ok: true, rows: statusRows(st) };
489
+ },
490
+ /** /dashboard: mint a one-time sign-in link for this profile (see slash/dashboard.js). Never throws; kind 'link' | 'plain' | 'error'. */
491
+ async dashboardLink(page) {
492
+ const { runDashboard } = await import('./slash/dashboard.js');
493
+ const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
494
+ const baseUrl = resolveDashboardBase({ serverApp: await serverAppUrl(), profileUrl, journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
495
+ return runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
496
+ },
497
+ /** Z2H-R3: the address to print for the host installer (never localhost when a better one is known). */
498
+ async hostsBaseUrl() {
499
+ let collectorUrl = null; try { collectorUrl = new URL(config.getServiceUrl('collector')).origin; } catch { /* none */ }
500
+ return resolveCollectorBase({ collectorUrl, serverApp: await serverAppUrl(), journeyUrl: journeyUrl(), lanIp: detectLanIp() }).url;
501
+ },
502
+ /** Z2H-3c: can `${base}/install` be served? -> { ok, status } or null when unreachable (never throws, 4 s cap). */
503
+ async hostInstallCheck(base) {
504
+ if (!base || /^<|example/.test(String(base))) return null;
505
+ const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 4000);
506
+ try { const r = await fetch(`${String(base).replace(/\/+$/, '')}/install`, { method: 'GET', signal: ctl.signal }); try { await r.body?.cancel?.(); } catch { /* drained */ } return { ok: r.ok, status: r.status }; }
507
+ catch { return null; } finally { clearTimeout(t); }
508
+ },
509
+ /** TUI-STATUS: the registered server this quox runs on -> { name, role, remote } | null (unregistered or any failure). */
510
+ async serverChip() {
511
+ const { resolveServerChip } = await import('./serverChip.js');
512
+ const { loadRegistry } = await import('../server/devServers.js');
513
+ let registry = { servers: {} };
514
+ try { registry = { servers: (await loadRegistry({ client })).servers }; } catch { /* unreadable registry: no purpose shown */ }
515
+ return resolveServerChip({
516
+ registry,
517
+ url: config.getActiveProfile?.()?.url || config.getBaseUrl?.() || null,
518
+ listHosts: async () => { const ep = resolveEndpoint('v1.fleet.hosts.list'); const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 }); return d?.hosts || d; }
519
+ });
520
+ },
521
+ /** DEVFLEET: the registered dev servers (same rows as `quox server list`) -> rows[]; throws on a Bastion read failure. */
522
+ async devServers() {
523
+ const [{ loadRegistry, buildRows, hereIdentity }] = [await import('../server/devServers.js')];
524
+ const ep = resolveEndpoint('v1.fleet.hosts.list');
525
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 });
526
+ const hosts = d?.hosts || d;
527
+ if (!Array.isArray(hosts)) throw new Error('unexpected response');
528
+ { const reg = await loadRegistry({ client }); return buildRows(hosts, { servers: reg.servers, defaults: reg.defaults }, hereIdentity()); }
529
+ },
257
530
  serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
258
531
  /** /config settings on the active profile. */
259
532
  settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
@@ -287,17 +560,269 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
287
560
  return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
288
561
  }
289
562
  },
290
- async checkoutSlot(name, session) {
563
+ /**
564
+ * /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
565
+ * Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
566
+ */
567
+ async feedback(payload, { version = null } = {}) { return sendFeedback(payload, { version }); },
568
+ /** Installed, enabled plugin ids for the org (GET plugins.list, as `quox plugin list`). Throws on failure so it never reads as "none". */
569
+ async installedPlugins() {
570
+ const id = orgId();
571
+ if (!id) throw new Error('no org');
572
+ const ep = resolveEndpoint('plugins.list', { orgId: id });
573
+ const data = await client.get(ep.path, { service: ep.service, retries: 0 });
574
+ const rows = listOf(data, 'plugins', 'data', 'items');
575
+ if (!rows) throw new Error('unexpected response');
576
+ return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
577
+ },
578
+ /** /todo: add an Action Tracker item as the logged-in user (the same core as `quox actions add`). Throws on failure. -> the item. */
579
+ async todoAdd(text) { return addActionItem(client, { title: text }); },
580
+ /** /todos: the whole board { backlog, doing, done }, every page. Throws on failure so it never reads as "none". */
581
+ async todoList() { return listBoard(client); },
582
+ /** /todo move: move one item (real id) to todo, doing or done. -> the item. */
583
+ async todoMove(id, status) { return moveActionItem(client, id, status); },
584
+ /** -> [{ plugin_id, version, is_enabled, ... }] every plugin installed for the org (GET plugins.list, as `quox plugin list`). Throws on failure. */
585
+ async pluginList() {
586
+ const id = orgId();
587
+ if (!id) throw new Error('no org');
588
+ const ep = resolveEndpoint('plugins.list', { orgId: id });
589
+ const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'plugins', 'data', 'items');
590
+ if (!rows) throw new Error('unexpected response');
591
+ return rows;
592
+ },
593
+ /** One installed plugin's record (GET plugins.get, as `quox plugin get`), or null when unknown (404). */
594
+ async pluginGet(pluginId) {
595
+ const id = orgId();
596
+ if (!id) throw new Error('no org');
597
+ const ep = resolveEndpoint('plugins.get', { orgId: id, pluginId });
598
+ try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.plugin || d; }
599
+ catch (e) { if (e?.status === 404) return null; throw e; }
600
+ },
601
+ /** Fleet hosts already connected (tips skip the QuoxAgent tip when > 0). GET agents list; throws on failure so a failure never reads as "no hosts". */
602
+ async hostCount() {
603
+ const ep = resolveEndpoint('v1.agents.list');
604
+ const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'agents', 'data', 'items');
605
+ if (!rows) throw new Error('unexpected response');
606
+ return rows.length;
607
+ },
608
+ /** -> [{ name, host_name, assigned_to, ... }] ; GET /api/v1/slots (throws on failure, never reads as empty). */
609
+ async slots() {
610
+ const ep = resolveEndpoint('v1.slots.list');
611
+ const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'slots');
612
+ if (!rows) throw new Error('unexpected response');
613
+ return rows;
614
+ },
615
+ /**
616
+ * Post a message into a slot room (same call as `quox slot say`). QUOXON-MESH: `from` signs it as this window
617
+ * ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
618
+ * -> { ok:true, message } | { ok:false, message, status }.
619
+ */
620
+ /** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
621
+ bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
622
+ async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null, anyOrg = true } = {}) {
623
+ const send = async (cl, oid) => {
624
+ const ep = resolveEndpoint('v1.slots.say', { name });
625
+ // SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
626
+ const d = await cl.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: oid, ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
627
+ return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
628
+ };
629
+ const fail = (e) => {
630
+ if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
631
+ return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
632
+ };
633
+ let first;
634
+ try { return await send(client, orgId()); } catch (e) { first = fail(e); }
635
+ // HITL-INBOX-BAR: a window in another org cannot see a slot that lives in the profile's home org. Same user, same
636
+ // sessions (not a cross-tenant leak): try the home org, then each of the user's orgs, each on a token for THAT org.
637
+ if (!anyOrg || first.status !== 404 || !clientForOrg) return first;
638
+ const tried = new Set([orgId()]);
639
+ const order = [];
640
+ try { const home = config.getAuth?.()?.org_id; if (home) order.push(home); } catch { /* no home org known */ }
641
+ try {
642
+ const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
643
+ for (const o of (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []))) if (o?.id) order.push(o.id);
644
+ } catch { /* the home org alone */ }
645
+ for (const oid of order) {
646
+ if (tried.has(oid)) continue; tried.add(oid);
647
+ try {
648
+ const cl = await clientForOrg(oid);
649
+ const r = await send(cl, oid);
650
+ return { ...r, viaOrgId: oid };
651
+ } catch (e) { const f = fail(e); if (f.status !== 404) return { ...f, viaOrgId: oid }; } // the slot is there but refused: that is the real answer
652
+ }
653
+ return first;
654
+ },
655
+ /** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
656
+ async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, from = null } = {}) {
657
+ const ep = resolveEndpoint('v1.slots.broadcast');
658
+ try {
659
+ const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
660
+ return { ok: true, data };
661
+ } catch (e) {
662
+ if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
663
+ return { ok: false, message: e?.message || 'failed', status: e?.status || null };
664
+ }
665
+ },
666
+ async slotBroadcastStatus(id) {
667
+ const ep = resolveEndpoint('v1.slots.broadcast.get', { id });
668
+ try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
669
+ },
670
+ // POKE-WAKE: slot control (server allows a human org owner/admin only; agents are refused).
671
+ async slotControl(slot, command) {
672
+ const ep = resolveEndpoint('v1.slots.control', { name: slot });
673
+ try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { command, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
674
+ },
675
+ async slotControlGet(slot, id) {
676
+ const ep = resolveEndpoint('v1.slots.control.get', { name: slot, id });
677
+ try { return { ok: true, data: await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
678
+ },
679
+ async slotAck(id, slot) {
680
+ const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
681
+ try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { slot, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
682
+ },
683
+ /** QLOOP-LIVE: the org's loops, newest first (GET /api/v1/loops) -> { loops:[{ objective_id, title, state, created_at }] }. */
684
+ async loopList() {
685
+ return client.get('/api/v1/loops', { service: 'collector', retries: 0, timeout: 8000, query: { limit: 20 } });
686
+ },
687
+ /** QLOOP-LIVE: one loop's snapshot (GET /api/v1/loops/:id). Throws on failure (the card decides what a failure means). */
688
+ async loopSnapshot(id) {
689
+ return client.get(`/api/v1/loops/${encodeURIComponent(id)}`, { service: 'collector', retries: 0, timeout: 8000 });
690
+ },
691
+ /** QLOOP-LIVE: one loop's events (SSE; ?since resumes). Yields { event_seq, type, ... }; ends after loop.finished or a drop. */
692
+ async *loopEvents(id, { since = null } = {}, signal) {
693
+ yield* client.stream(`/api/v1/loops/${encodeURIComponent(id)}/events`, { service: 'collector', query: { since }, signal, untilClose: true });
694
+ },
695
+ /** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
696
+ async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
697
+ const ep = resolveEndpoint('v1.slots.stream', { name });
698
+ yield* client.stream(ep.path, { service: ep.service, query: { org_id: orgId(), since, exclude_from: excludeFrom, ...(session ? { session } : {}) }, signal, untilClose: true });
699
+ },
700
+ /** QUOXON-ROOMS: join / leave a slot as a room (no hold). -> { ok, already? | removed? } | { ok:false, message, status }. */
701
+ async slotJoin(name, session, person = null) {
702
+ const ep = resolveEndpoint('v1.slots.join', { name });
703
+ try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, ...(person ? { person } : {}), org_id: orgId() } }); return { ok: true, already: d?.already === true }; }
704
+ catch (e) { return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null }; }
705
+ },
706
+ async slotLeave(name, session) {
707
+ const ep = resolveEndpoint('v1.slots.leave', { name });
708
+ try { const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { session, org_id: orgId() } }); return { ok: true, removed: d?.removed === true }; }
709
+ catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
710
+ },
711
+ /** The rooms this session has joined (server truth, survives a restart). -> [{ slot_name, joined_at }]; throws on failure. */
712
+ async slotRooms(session) {
713
+ const ep = resolveEndpoint('v1.slots.rooms');
714
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, query: { session, org_id: orgId() } });
715
+ return Array.isArray(d?.rooms) ? d.rooms : [];
716
+ },
717
+ /** Long-poll fallback for a server without the stream route (same call as `quox slot wait`). */
718
+ async slotWait(name, { since = null, excludeFrom = null } = {}) {
719
+ const ep = resolveEndpoint('v1.slots.wait', { name });
720
+ return client.get(ep.path, { service: ep.service, retries: 0, timeout: 40000, query: { org_id: orgId(), since, exclude_from: excludeFrom, timeout_ms: 25000 } });
721
+ },
722
+ /** Last `limit` messages of a slot, oldest first (same call as `quox slot read`). Throws on failure. */
723
+ async slotMessages(name, limit = 20) {
724
+ const ep = resolveEndpoint('v1.slots.messages', { name });
725
+ const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0, query: { org_id: orgId(), limit } }), 'messages');
726
+ if (!rows) throw new Error('unexpected response');
727
+ return rows;
728
+ },
729
+ /** The signed-in human's display name for slot lines (display_name, else the email's local part). */
730
+ displayName() {
731
+ try { const a = config.getAuth?.() || {}; return a.display_name || (a.email ? String(a.email).split('@')[0] : null); } catch { return null; }
732
+ },
733
+ /** `take` = the same signed-in user's other/renamed/restarted session takes a LIVE hold (stale holds of yours are reclaimed regardless). */
734
+ async checkoutSlot(name, session, { take = false } = {}) {
291
735
  const ep = resolveEndpoint('v1.slots.checkout', { name });
292
736
  try {
293
- const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true } });
737
+ const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true, ...(take ? { take: true } : {}) } });
294
738
  recordHeldSlot(name, session);
295
- return { ok: true, slot: data?.slot || data };
739
+ return { ok: true, slot: data?.slot || data, tookOver: data?.took_over || null };
296
740
  } catch (e) {
297
- if (e?.status === 409) return { ok: false, message: e.detail?.assigned_to ? `slot ${name} is assigned to ${e.detail.assigned_to}` : `slot ${name} is held by ${e.detail?.holder_session || 'another session'}` };
298
- return { ok: false, message: e?.message || 'failed' };
741
+ if (e?.status === 409) {
742
+ const d = e.detail || {};
743
+ if (d.assigned_to) return { ok: false, message: `slot ${name} is assigned to ${d.assigned_to}` };
744
+ return { ok: false, message: `slot ${name} is held by ${d.holder_session || 'another session'}`, held: { holder_session: d.holder_session || null, same_user: d.same_user === true, state: d.state || null, since: d.checked_out_at || null, last_seen: d.last_heartbeat_at || null } };
745
+ }
746
+ // Z2H-R5 C: a free install answers 403 "QuoxSlots is not licensed": say that, not a bare 403.
747
+ if (slotsUnlicensed(e)) {
748
+ return { ok: false, unlicensed: true, transient: false, status: 403, message: SLOTS_UNLICENSED };
749
+ }
750
+ // No answer, or the server failing (timeout, network, 5xx, e.g. a collector mid-redeploy): worth retrying.
751
+ const st = e?.status || null;
752
+ return { ok: false, message: e?.message || 'failed', status: st, transient: !st || st >= 500 || st === 429 || st === 408 };
299
753
  }
300
754
  },
755
+ /** Keep a TUI-held slot alive (the hold lapses to stale after 15m without one). -> { ok } */
756
+ async slotHeartbeat(name, session) {
757
+ const ep = resolveEndpoint('v1.slots.heartbeat', { name });
758
+ try { await client.post(ep.path, { service: ep.service, body: { holder_session: session, org_id: orgId() } }); return { ok: true }; }
759
+ catch (e) { return { ok: false, message: e?.message || 'failed' }; }
760
+ },
761
+ /** quox#1183: the org's hats -> [{ id, key, name, definition }]. Same endpoint as `quox hat list`. */
762
+ async hats() {
763
+ const ep = resolveEndpoint('hat.list', { orgId: orgId() });
764
+ const d = await client.get(ep.path, { service: ep.service });
765
+ return Array.isArray(d?.hats) ? d.hats.filter(h => h.status !== 'archived') : [];
766
+ },
767
+ /** CUSTOM-HATS: create an org hat of your own -> { hat, warnings }. Throws the server's refusal unchanged. */
768
+ async createHat({ key, name, definition, visibility = 'org' }) {
769
+ const ep = resolveEndpoint('hat.create', { orgId: orgId() });
770
+ return client.post(ep.path, { service: ep.service, retries: 0, body: { key, name, definition, visibility } });
771
+ },
772
+ /** CUSTOM-HATS: a new revision of a hat -> { hat, warnings }. */
773
+ async updateHat(hatId, { definition, name, change_reason }) {
774
+ const ep = resolveEndpoint('hat.update', { orgId: orgId(), hatId });
775
+ return client.put(ep.path, { service: ep.service, retries: 0, body: { definition, ...(name ? { name } : {}), change_reason } });
776
+ },
777
+ /** CUSTOM-HATS: delete a custom hat -> { deleted: { key, revision, bindings_cleared, user_prefs_cleared } }. */
778
+ async deleteHat(hatId) {
779
+ const ep = resolveEndpoint('hat.delete', { orgId: orgId(), hatId });
780
+ return client.delete(ep.path, { service: ep.service, retries: 0 });
781
+ },
782
+ /** quox#1183: the hat this conversation is on (or would start on) -> { key, name } | null. */
783
+ async activeHat(conversationId) {
784
+ const ep = resolveEndpoint('hat.resolve', { orgId: orgId() });
785
+ const d = await client.get(ep.path, { service: ep.service, ...(conversationId ? { query: { conversation_id: conversationId } } : {}) });
786
+ return d?.hat ? { key: d.hat.key, name: d.hat.name, source: d.source || null } : null;
787
+ },
788
+ /** quox#1183: the person's own hat switch for this conversation (audited on the server). Throws the refusal unchanged. */
789
+ async bindHat(conversationId, hatId) {
790
+ const ep = resolveEndpoint('hat.bind', { orgId: orgId(), conversationId });
791
+ const d = await client.put(ep.path, { service: ep.service, retries: 0, body: { hat_id: hatId } });
792
+ return d?.binding || d;
793
+ },
794
+ /** QUOXON-FW3: the orgs the signed-in user belongs to -> [{ id, name, slug, role }]. Same endpoint as `quox org list`. */
795
+ async orgs() {
796
+ const ep = resolveEndpoint('orgs.list');
797
+ const d = await client.get(ep.path, { service: ep.service });
798
+ const rows = Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || []);
799
+ return Array.isArray(rows) ? rows : [];
800
+ },
801
+ /**
802
+ * QUOXON-FW3, #1034: re-scope THIS WINDOW to another org. The org-scoped token is held in this window's memory
803
+ * (windowOrg); the shared profile, its file and every other window keep their org. `quox org switch` in a shell is
804
+ * the explicit way to change a profile's default. -> { id, name }. Throws the server's refusal unchanged.
805
+ */
806
+ async switchOrg(targetId) {
807
+ if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
808
+ return windowOrg.switchTo(targetId);
809
+ },
810
+ /**
811
+ * "also make default" from the /org picker: the same profile-wide change as `quox org switch <org>` in a shell
812
+ * (moves the shared session and rewrites the profile's stored org + token). This window's own org is untouched.
813
+ */
814
+ async makeOrgDefault(targetId) {
815
+ const ep = resolveEndpoint('auth.switch-org');
816
+ const data = await client.post(ep.path, { service: ep.service, body: { org_id: targetId } });
817
+ if (!data?.org?.id) { const e = new Error('The server did not confirm the switch.'); e.code = 'switch_failed'; throw e; }
818
+ const a = config.getAuth() || {};
819
+ a.org_id = data.org.id; a.org_name = data.org.name; a.org_slug = data.org.slug; a.org_role = data.role;
820
+ if (data.access_token) a.access_token = data.access_token;
821
+ config.setAuth(a);
822
+ return { id: data.org.id, name: data.org.name };
823
+ },
824
+ /** The window's own org id when it differs from the profile's (carried across /restart), else null. */
825
+ windowOrgId: () => windowOrg?.current()?.id || null,
301
826
  /** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
302
827
  async skills() {
303
828
  const ep = resolveEndpoint('quoxflow.skills.list');
@@ -361,6 +886,29 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
361
886
  }
362
887
  return { ok: false, unavailable: true };
363
888
  },
889
+ /** /sitrep: the LUCID situation (cursor not moved) and the approvals inbox, each part on its own. */
890
+ async sitrep() {
891
+ const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status}` : /timed out/i.test(e?.message || '') ? 'timed out' : (e?.message || 'failed') }; } };
892
+ const [situation, approvals] = await Promise.all([
893
+ part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0, timeout: 12000 }); }),
894
+ part(() => listApprovals())
895
+ ]);
896
+ return { situation, approvals };
897
+ },
898
+ /** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
899
+ async setPresence(state, { note, slots } = {}) {
900
+ const ep = resolveEndpoint('presence.set');
901
+ try {
902
+ const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, body: { state, org_id: orgId(), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
903
+ return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
904
+ } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
905
+ },
906
+ noteActivity,
907
+ /** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
908
+ async away(since) {
909
+ const ep = resolveEndpoint('away.digest');
910
+ return client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
911
+ },
364
912
  /** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
365
913
  async observers() {
366
914
  const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, unavailable: missing(e), error: e?.message || 'failed' }; } };
@@ -377,8 +925,27 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
377
925
  ]);
378
926
  return { situation: sit, lessons, learning: [reflect, learn, insight] };
379
927
  },
928
+ /** /mind [id]: QuoxMind implants from the collector. -> { implants, health } | { implant }. Throws on non-2xx. */
929
+ async mind(id) {
930
+ if (id) { const ep = resolveEndpoint('mind.implant', { id }); return { implant: await client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 }) }; }
931
+ const [l, h] = await Promise.all([resolveEndpoint('mind.implants'), resolveEndpoint('mind.health')].map((ep) => client.get(ep.path, { service: ep.service, retries: 0, timeout: 15000 })));
932
+ return { implants: l.implants || [], health: h };
933
+ },
934
+ /** /mirror: the Mirror as CommanderQ sees it (compact System Status block + age). Throws on non-2xx. */
935
+ async mirror() {
936
+ const ep = resolveEndpoint('mind.mirror');
937
+ return client.get(ep.path, { service: ep.service, retries: 0, timeout: 10000 });
938
+ },
939
+ /** #904 part 3: providers refusing turns right now (weekly limit, no credits). -> [{ provider, state, line }] | null on error. */
940
+ async providerHealth() {
941
+ try {
942
+ const ep = resolveEndpoint('chat.status.list');
943
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
944
+ return Array.isArray(d?.provider_health) ? d.provider_health : [];
945
+ } catch { return null; }
946
+ },
380
947
  async health(timeoutMs = 12000) {
381
- const url = new URL('/health', config.getServiceUrl('collector'));
948
+ const url = joinServiceUrl(config.getServiceUrl('collector'), '/health');
382
949
  const ctrl = new AbortController();
383
950
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
384
951
  try { const r = await fetch(url, { signal: ctrl.signal }); return { ok: r.ok, status: r.status, url: url.origin }; }
@@ -399,6 +966,31 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
399
966
  }
400
967
  },
401
968
  /** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
969
+ /** LAZY-AI-CLI: which vendor CLIs (claude-code, codex) this server has. Never throws. */
970
+ async providerCliStatus() {
971
+ const ep = resolveEndpoint('v1.providers.cli.status');
972
+ try {
973
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
974
+ return Array.isArray(d?.tools) ? { ok: true, tools: d.tools } : { ok: false, error: 'unexpected response' };
975
+ } catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
976
+ },
977
+ /** The install plan: source, exact package + version, download size. Instance admin only (403 otherwise). Never throws. */
978
+ async providerCliPlan(tool) {
979
+ const ep = resolveEndpoint('v1.providers.cli.plan', { tool });
980
+ try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.ok ? d : { ok: false, error: d?.message || 'unexpected response' }; }
981
+ catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.body?.message || e?.message || 'failed' }; }
982
+ },
983
+ /** Start an install job (the server audits it). Never throws. */
984
+ async providerCliInstall(tool, { approvalId = null } = {}) {
985
+ const ep = resolveEndpoint('v1.providers.cli.install', { tool });
986
+ try { return await client.post(ep.path, { service: ep.service, body: approvalId ? { approval_id: approvalId } : {}, retries: 0 }); }
987
+ catch (e) { return { ok: false, status: e?.status, error: e?.body?.message || e?.message || 'failed' }; }
988
+ },
989
+ async providerCliJob(id) {
990
+ const ep = resolveEndpoint('v1.providers.cli.job', { id });
991
+ try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
992
+ catch (e) { return { state: 'unknown', error: e?.message || 'failed', status: e?.status }; }
993
+ },
402
994
  async connections() {
403
995
  try {
404
996
  const ep = resolveEndpoint('vault.list', { orgId: orgId() });
@@ -419,20 +1011,32 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
419
1011
  } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
420
1012
  },
421
1013
  /** The governed vault write (same route as `quox vault create`): audited, AEE envelope carries type + label only. */
422
- async saveProviderKey({ type, label, credentials, isDefault }) {
1014
+ async saveProviderKey({ type, label, credentials, isDefault, confirmPrivateOrigin }) {
423
1015
  const ep = resolveEndpoint('vault.create', { orgId: orgId() });
424
1016
  try {
425
- const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
1017
+ const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
426
1018
  return { ok: Boolean(r?.id), id: r?.id || null, message: r?.id ? null : 'unexpected response' };
427
- } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
1019
+ } catch (e) { return vaultFailure(e); }
428
1020
  },
429
1021
  /** B3: refresh an existing vault credential (same governed route as `quox vault update`). */
430
- async updateProviderCredential({ id, credentials, isDefault }) {
1022
+ async updateProviderCredential({ id, credentials, isDefault, confirmPrivateOrigin }) {
431
1023
  const ep = resolveEndpoint('vault.update', { orgId: orgId(), id });
432
1024
  try {
433
- const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
1025
+ const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault), ...(confirmPrivateOrigin ? { confirm_private_origin: confirmPrivateOrigin } : {}) }, retries: 0, sensitive: true });
434
1026
  return { ok: Boolean(r && (r.id || r.success || r.credential)), id, message: null };
435
- } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
1027
+ } catch (e) { return vaultFailure(e); }
1028
+ },
1029
+ /** Z2H: read the instance LAN-target gate -> { ok, enabled } | { ok:false, status, message }. */
1030
+ async lanTargets() {
1031
+ const ep = resolveEndpoint('admin.instance.lan_targets.get');
1032
+ try { const r = await client.get(ep.path, { service: ep.service, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
1033
+ catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
1034
+ },
1035
+ /** Z2H: set the instance LAN-target gate (instance admin only; audited server side). */
1036
+ async setLanTargets(on) {
1037
+ const ep = resolveEndpoint('admin.instance.lan_targets.set');
1038
+ try { const r = await client.put(ep.path, { service: ep.service, body: { enabled: Boolean(on) }, retries: 0 }); return { ok: true, enabled: Boolean(r?.enabled) }; }
1039
+ catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
436
1040
  },
437
1041
  /** B3: server-side probe of an Ollama URL (GET <url>/api/tags). Nothing stored. */
438
1042
  async ollamaProbe(baseUrl) {
@@ -446,6 +1050,12 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
446
1050
  try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
447
1051
  catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
448
1052
  },
1053
+ /** QUOXON-MODEL: live model list for openai | anthropic -> { ok, models:[{id,family,created}], default_model } | { ok:false, error, message }. */
1054
+ async providerModels(provider, { refresh = false } = {}) {
1055
+ const ep = resolveEndpoint('chat.providers.models', { provider });
1056
+ try { return await client.get(ep.path, { service: ep.service, query: refresh ? { refresh: 1 } : undefined, retries: 0 }); }
1057
+ catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
1058
+ },
449
1059
  /** B5: server-side probe of a self-hosted / custom OpenAI-compatible base URL (lmstudio|vllm|custom). Nothing stored. */
450
1060
  async compatProbe(preset, baseUrl) {
451
1061
  const ep = resolveEndpoint('chat.providers.compat.probe', { preset });
@@ -458,16 +1068,71 @@ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BAC
458
1068
  try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
459
1069
  catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
460
1070
  },
1071
+ /** /vm <verb> ...: the HYPERION Proxmox tools through POST /api/v1/tools/execute. Returns { lines, tone }. */
1072
+ async vm(argv) { return runVmCommand({ client, orgId: orgId(), argv }); },
1073
+ /** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
1074
+ async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
461
1075
  approvals: listApprovals,
1076
+ /** HITL-INBOX-BAR: the owner's latest inbox items (any kind, active), newest first. Throws on failure, never reads as empty. */
1077
+ async inboxItems({ limit = 30 } = {}) {
1078
+ const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
1079
+ const data = await client.get(ep.path, { service: ep.service, query: { limit } });
1080
+ const items = data?.items || data?.data || data || [];
1081
+ return Array.isArray(items) ? items : [];
1082
+ },
1083
+ /** One inbox item in full (the server marks it read for this user as a side effect). */
1084
+ async inboxItem(itemId) {
1085
+ const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
1086
+ const data = await client.get(ep.path, { service: ep.service });
1087
+ return data?.item || data;
1088
+ },
1089
+ /** Mark one inbox item read (PATCH .../inbox/:id/read). */
1090
+ async inboxMarkRead(itemId, isRead = true) {
1091
+ const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
1092
+ return client.patch(`${ep.path}/read`, { service: ep.service, body: { is_read: Boolean(isRead) } });
1093
+ },
1094
+ /** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
1095
+ async approvalStats() {
1096
+ const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
1097
+ const d = await client.get(ep.path, { service: ep.service });
1098
+ return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
1099
+ },
462
1100
  /** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
463
1101
  async approvalResult(approvalId) {
464
1102
  const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
465
1103
  try { return await client.get(ep.path, { service: ep.service, query: { org_id: orgId() || undefined } }); }
466
1104
  catch (e) { if (e?.status === 404) return null; throw e; }
467
1105
  },
468
- async respond(itemId, action) {
1106
+ /** quox ask: answer a decision with one of its option keys (and an optional note). */
1107
+ async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
1108
+ const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
1109
+ const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action: 'answer', option, ...(bulk ? { bulk: true } : {}), ...(note ? { comment: String(note).slice(0, 4000) } : {}) } });
1110
+ if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
1111
+ return r;
1112
+ },
1113
+ async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
469
1114
  const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
470
- return client.post(ep.path, { service: ep.service, body: { action, comment: 'via quoxon' } });
1115
+ const tag = via ? `via quoxon ${via}` : 'via quoxon';
1116
+ const why = reason ? String(reason).trim().slice(0, 4000) : '';
1117
+ const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action, ...(bulk ? { bulk: true } : {}), comment: why ? `${why} (${tag})` : tag, ...(why ? { reason: why } : {}) } });
1118
+ if (stepUp && Object.keys(stepUp).length) noteActivity('approval', { quiet: true }); // PRESENCE
1119
+ return r;
1120
+ },
1121
+ /** QUOXON-STEPUP: PIN -> short-lived step-up token. The PIN goes straight into the request body; never logged, never stored. */
1122
+ async stepUpVerify(pin, { orgId: org = null, scopes = ['approve'] } = {}) {
1123
+ const ep = resolveEndpoint('stepup.verify');
1124
+ const r = await client.post(ep.path, { service: ep.service, body: { pin, org_id: org || orgId() || undefined, scopes } });
1125
+ noteActivity('pin-step-up', { quiet: true }); // PRESENCE: a PIN step-up is a live human
1126
+ return r;
1127
+ },
1128
+ async stepUpStatus() {
1129
+ const ep = resolveEndpoint('stepup.status');
1130
+ return client.get(ep.path, { service: ep.service });
1131
+ },
1132
+ /** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
1133
+ async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
1134
+ const ep = resolveEndpoint('safety.override');
1135
+ return client.post(ep.path, { service: ep.service, headers: stepUp, body: { block_id: blockId, block_type: blockType, reason, agent_id: agentId || undefined, action: action || undefined } });
471
1136
  }
472
1137
  };
473
1138
  }