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
@@ -51,6 +51,18 @@ export function titleOverlap(a, b) {
51
51
  for (const w of x) if (y.has(w)) hit++;
52
52
  return hit / Math.min(x.size, y.size);
53
53
  }
54
+ const NEGATIVE_WORDS = new Set(['disable', 'disabled', 'disabling', 'off', 'deny', 'denied', 'block', 'blocked', 'stop', 'stopped', 'remove', 'removed', 'drop', 'dropped', 'abandon', 'abandoned', 'reject', 'rejected', 'exclude', 'excluded', 'not', 'never', 'no', 'dont', 'cannot', 'cant', 'wont', 'without', 'revoke', 'revoked']);
55
+ /** Odd count of negating words = negative polarity ("not", "stop", "disable", "reject", ...). */
56
+ function negativeParity(t) {
57
+ const toks = String(t || '').toLowerCase().replace(/['\u2019]/g, '').split(/[^a-z0-9]+/).filter(Boolean);
58
+ let n = 0;
59
+ for (const w of toks) if (NEGATIVE_WORDS.has(w)) n++;
60
+ return n % 2;
61
+ }
62
+ /** True when two decision titles read as opposites (enable/disable, on/off, stop X, no longer X...). */
63
+ export function oppositePolarity(a, b) {
64
+ return negativeParity(a) !== negativeParity(b);
65
+ }
54
66
  export const DECIDE_DUPE_THRESHOLD = 0.6;
55
67
  export const DISTILL_CAP = 50;
56
68
 
@@ -136,6 +148,24 @@ export function scoreRetrievalEval(judgements, searches, k = 8) {
136
148
  export function registerBrainCommands(program, ctx) {
137
149
  const { client, auth } = ctx;
138
150
 
151
+ // quox#934: read/history/related go through the NotesQ API, which only knows
152
+ // editable NotesQ notes. A feed note (git_commit, email, ...) that `brain note`
153
+ // resolves came back as a bare 404. On a NotesQ 404, ask the brain itself;
154
+ // rethrow the original error when the brain does not know the note either.
155
+ const brainNoteOn404 = async (noteId, err) => {
156
+ if (!(err instanceof QuoxApiError) || err.status !== 404) throw err;
157
+ let note;
158
+ try {
159
+ const ep = resolveEndpoint('brain.note.get', { id: noteId });
160
+ note = await client.get(ep.path, { service: ep.service });
161
+ } catch {
162
+ throw err;
163
+ }
164
+ if (!note || !note.id) throw err;
165
+ return note;
166
+ };
167
+ const feedReason = (n, what) => `${n.id} is a ${n.kind || 'feed'} note${n.source_type ? ` from the ${n.source_type} source` : ''}, not an editable NotesQ note, so it has no ${what}.`;
168
+
139
169
  const brain = program
140
170
  .command('brain')
141
171
  .description('Query Quox Qortex (vaults, notes, semantic search)')
@@ -164,10 +194,16 @@ export function registerBrainCommands(program, ctx) {
164
194
  console.log(JSON.stringify(data, null, 2));
165
195
  return;
166
196
  }
197
+ // quox#853: a keyword-only fallback fooled both the product and a
198
+ // verifier ("0 hits" read as "does not exist"). Say it in words.
199
+ const degradedNote = data.retrieval === 'fts' && data.degraded
200
+ ? '⚠ Semantic search was unavailable for this query, so this is keyword-only: a missing or zero result does not prove the note is absent. Retry shortly.'
201
+ : '';
167
202
  if (insufficient) {
168
203
  const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
169
204
  console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `0 relevant hits${legLabel}`, ms: data.tookMs ?? '?' }));
170
205
  console.log('⚠ Qortex judged the matches for this query insufficient (no relevant notes). The weak hits are withheld; use -o json to see them with the relevance details.');
206
+ if (degradedNote) console.log(degradedNote);
171
207
  return;
172
208
  }
173
209
  // BRAIN2-AUDIT: sessions must SEE what Qortex recalled, not just a hit
@@ -176,12 +212,14 @@ export function registerBrainCommands(program, ctx) {
176
212
  const results = data.results || [];
177
213
  const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
178
214
  console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${results.length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
215
+ if (degradedNote) console.log(degradedNote);
179
216
  if (results.length === 0) {
180
217
  console.log(glyphLine('brain2', 'recall', { result: 'nothing on record for this query' }));
181
218
  return;
182
219
  }
183
220
  for (const r of results) {
184
- console.log(`${brandName('🌌 QORTEX')} recall │ "${recallSnippet(r)}" · ${recallSource(r)} · ${r.title || 'untitled'} · note ${r.noteId || '?'}`);
221
+ const miss = Array.isArray(r.missingEntities) && r.missingEntities.length ? ` · does not mention ${r.missingEntities.join(', ')}` : '';
222
+ console.log(`${brandName('🌌 QORTEX')} recall │ "${recallSnippet(r)}" · ${recallSource(r)} · ${r.title || 'untitled'} · note ${r.noteId || '?'}${miss}`);
185
223
  }
186
224
  return;
187
225
  }
@@ -191,6 +229,7 @@ export function registerBrainCommands(program, ctx) {
191
229
  if (program.opts().output !== 'json') {
192
230
  const legLabel = data.retrieval ? ` · ${data.retrieval}` : '';
193
231
  console.log(glyphLine('brain2', `search "${query.slice(0, 40)}"`, { result: `${(data.results || []).length} hits${legLabel}`, ms: data.tookMs ?? '?' }));
232
+ if (degradedNote) console.log(degradedNote);
194
233
  }
195
234
  formatOutput({
196
235
  columns: ['title', 'kind', 'snippet', 'score'],
@@ -300,7 +339,7 @@ export function registerBrainCommands(program, ctx) {
300
339
  if (!['private', 'org'].includes(options.scope)) throw new QuoxError('--scope must be private or org.', 2);
301
340
  const prov = decisionProvenance();
302
341
  const title = `Decision: ${what}`.slice(0, 200);
303
- const markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov });
342
+ let markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov });
304
343
  const tags = ['decision', ...String(options.tags || '').split(',').map((t) => t.trim()).filter(Boolean)];
305
344
  const json = program.opts().output === 'json';
306
345
  if (options.dryRun) {
@@ -311,7 +350,15 @@ export function registerBrainCommands(program, ctx) {
311
350
  const sp = resolveEndpoint('brain.search.list');
312
351
  const found = await client.post(sp.path, { service: sp.service, body: { query: what, limit: 5 } });
313
352
  const insufficient = found.insufficient === true || found.relevance?.insufficient === true;
314
- const dupe = insufficient ? null : (found.results || []).find((r) => titleOverlap(what, String(r.title || '').replace(/^Decision:\s*/i, '')) >= DECIDE_DUPE_THRESHOLD);
353
+ const bare = (r) => String(r.title || '').replace(/^Decision:\s*/i, '');
354
+ const matches = insufficient ? [] : (found.results || []).filter((r) => titleOverlap(what, bare(r)) >= DECIDE_DUPE_THRESHOLD);
355
+ // An opposite-polarity match is a reversal of the earlier decision, never a duplicate of it.
356
+ const dupe = matches.find((r) => !oppositePolarity(what, bare(r)));
357
+ const reversed = dupe ? null : matches[0];
358
+ if (reversed) {
359
+ const ref = reversed.noteId || reversed.title;
360
+ if (ref) markdown = decisionMarkdown({ what, why: options.why, rejected: options.rejected, prov: { ...prov, supersedes: ref } });
361
+ }
315
362
  if (dupe && !options.force) {
316
363
  if (json) console.log(JSON.stringify({ saved: false, reason: 'existing_match', match: { noteId: dupe.noteId, title: dupe.title, kind: dupe.kind } }, null, 2));
317
364
  else {
@@ -324,9 +371,10 @@ export function registerBrainCommands(program, ctx) {
324
371
  }
325
372
  const ep = resolveEndpoint('brain.capture.create');
326
373
  const data = await client.post(ep.path, { service: ep.service, body: { title, text: markdown.replace(/^# .*\n\n/, ''), tags, scope: options.scope === 'org' ? 'org' : 'user' } });
327
- if (json) { console.log(JSON.stringify(data, null, 2)); return; }
374
+ if (json) { console.log(JSON.stringify(reversed ? { ...data, reverses: { noteId: reversed.noteId, title: reversed.title } } : data, null, 2)); return; }
328
375
  const verb = data.status === 'created' ? 'saved' : data.status === 'unchanged' ? 'already saved' : 'updated';
329
376
  console.log(glyphLine('brain2', 'decide', { result: `${verb} · note ${String(data.noteId || '?').slice(0, 8)}` }));
377
+ if (reversed) console.log(` reverses ${reversed.title || 'untitled'} (note ${reversed.noteId || '?'})`);
330
378
  });
331
379
 
332
380
  brain
@@ -443,14 +491,14 @@ export function registerBrainCommands(program, ctx) {
443
491
  formatDetail(data, { format });
444
492
  return;
445
493
  }
446
- const verdict = (r) => (r.contradicted ? 'contradicted' : r.supported === true ? 'supported' : r.supported === false ? 'unsupported' : 'not measured');
494
+ const verdict = (r) => (r.fabricated || r.support_method === 'fabricated' ? 'fabricated' : r.support_method === 'unchecked' ? 'unchecked' : r.contradicted ? 'contradicted' : r.supported === true ? 'supported' : r.support_method === 'overlap' ? 'overlap' : r.supported === false ? 'unsupported' : 'not measured');
447
495
  formatOutput({
448
496
  columns: ['session', 'handle', 'note', 'span', 'rev', 'verdict', 'at'],
449
497
  headers: ['Session', 'Handle', 'Note', 'Span', 'Rev', 'Verdict', 'At'],
450
498
  rows: (data.records || []).map((r) => ({
451
499
  session: r.session || '-',
452
500
  handle: r.handle || '-',
453
- note: r.note_id,
501
+ note: r.note_id || '-',
454
502
  span: r.char_start != null && r.char_end != null ? `${r.char_start}-${r.char_end}` : '-',
455
503
  rev: r.note_hash ? String(r.note_hash).slice(0, 8) : '-',
456
504
  verdict: verdict(r),
@@ -509,7 +557,8 @@ export function registerBrainCommands(program, ctx) {
509
557
  { metric: 'answers after brain search', week: cw.answers ?? 0, total: ct.answers ?? 0 },
510
558
  { metric: 'answers citing a note', week: `${cw.grounded ?? 0} (${pct(cw.grounded ?? 0, cw.answers ?? 0)})`, total: `${ct.grounded ?? 0} (${pct(ct.grounded ?? 0, ct.answers ?? 0)})` },
511
559
  ...((cw.supportMeasured ?? 0) + (ct.supportMeasured ?? 0) > 0
512
- ? [{ metric: 'answers with supported citations (lexical)', week: `${cw.supportedAnswers ?? 0} (${pct(cw.supportedAnswers ?? 0, cw.supportMeasured ?? 0)})`, total: `${ct.supportedAnswers ?? 0} (${pct(ct.supportedAnswers ?? 0, ct.supportMeasured ?? 0)})` }]
560
+ ? [{ metric: 'answers with supported citations (entailment)', week: `${cw.supportedAnswers ?? 0} (${pct(cw.supportedAnswers ?? 0, cw.supportMeasured ?? 0)})`, total: `${ct.supportedAnswers ?? 0} (${pct(ct.supportedAnswers ?? 0, ct.supportMeasured ?? 0)})` },
561
+ { metric: 'answers with overlap only (not checked for meaning)', week: `${cw.overlapAnswers ?? 0}`, total: `${ct.overlapAnswers ?? 0}` }]
513
562
  : []),
514
563
  { metric: 'notes cited / retrieved', week: `${cw.citedNotes ?? 0}/${cw.retrievedNotes ?? 0}`, total: `${ct.citedNotes ?? 0}/${ct.retrievedNotes ?? 0}` },
515
564
  ],
@@ -559,7 +608,13 @@ export function registerBrainCommands(program, ctx) {
559
608
  .action(async (noteId, options) => {
560
609
  auth.requireAuth();
561
610
  const ep = resolveEndpoint('notesq.notes.get', { id: noteId });
562
- const data = await client.get(ep.path, { service: ep.service });
611
+ let data;
612
+ try {
613
+ data = await client.get(ep.path, { service: ep.service });
614
+ } catch (e) {
615
+ const bn = await brainNoteOn404(noteId, e);
616
+ data = { note: bn, source: 'brain', editable: false };
617
+ }
563
618
  const n = data.note || data;
564
619
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
565
620
  if (options.raw) { console.log(n.body ?? n.markdown ?? ''); return; }
@@ -602,11 +657,22 @@ export function registerBrainCommands(program, ctx) {
602
657
  .description('Delete a note (recorded as a revision, restorable in NotesQ). Requires --yes.')
603
658
  .argument('<noteId>', 'note id')
604
659
  .option('--yes', 'confirm deletion')
660
+ .option('--expected-hash <hash>', "the note's content_hash you read (see `quox brain read`); a stale value is a conflict (409) and nothing is deleted")
605
661
  .action(async (noteId, options) => {
606
662
  auth.requireAuth();
607
663
  if (!options.yes) throw new QuoxError(`Refusing to delete ${noteId} without --yes.`, 2);
664
+ if (!options.expectedHash) throw new QuoxError(`Refusing to delete ${noteId} without --expected-hash (the content_hash from \`quox brain read ${noteId}\`), so a concurrent edit is never deleted.`, 2);
608
665
  const ep = resolveEndpoint('notesq.notes.delete', { id: noteId });
609
- const data = await client.delete(ep.path, { service: ep.service });
666
+ let data;
667
+ try {
668
+ data = await client.delete(ep.path, { service: ep.service, query: { expected_hash: options.expectedHash } });
669
+ } catch (e) {
670
+ if (e instanceof QuoxApiError && e.status === 409) {
671
+ const h = e.body && (e.body.currentHash || e.body.current_hash);
672
+ throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was deleted. Re-run \`quox brain read ${noteId}\` and retry.`, 1);
673
+ }
674
+ throw e;
675
+ }
610
676
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
611
677
  printSuccess(`Deleted note ${noteId}.`);
612
678
  });
@@ -621,7 +687,14 @@ export function registerBrainCommands(program, ctx) {
621
687
  .action(async (noteId, options) => {
622
688
  auth.requireAuth();
623
689
  const ep = resolveEndpoint('notesq.notes.related', { id: noteId });
624
- const data = await client.get(ep.path, { service: ep.service, query: { limit: Math.min(Math.max(parseInt(options.limit, 10) || 8, 1), 20) } });
690
+ let data;
691
+ try {
692
+ data = await client.get(ep.path, { service: ep.service, query: { limit: Math.min(Math.max(parseInt(options.limit, 10) || 8, 1), 20) } });
693
+ } catch (e) {
694
+ const bn = await brainNoteOn404(noteId, e);
695
+ data = { related: [], editable: false, reason: `${feedReason(bn, 'similarity links')} Use \`quox brain neighbours ${noteId}\` for its graph links.` };
696
+ if (program.opts().output !== 'json') console.log(data.reason);
697
+ }
625
698
  const related = data.related || [];
626
699
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
627
700
  console.log(glyphLine('brain2', `related ${noteId.slice(0, 8)}`, { result: `${related.length} notes` }));
@@ -657,11 +730,13 @@ export function registerBrainCommands(program, ctx) {
657
730
  .description('Token-budgeted pack of the most relevant cited passages on a topic (what the Cortex brain_context tool returns)')
658
731
  .argument('<query>', 'topic or task')
659
732
  .option('--budget <n>', 'approximate token budget (200-4000)', '1200')
733
+ .option('--since <date>', 'only notes dated on or after this ISO date/datetime (e.g. 2026-10-05)')
734
+ .option('--until <date>', 'only notes dated on or before this ISO date/datetime (a date alone means the whole day)')
660
735
  .action(async (query, options) => {
661
736
  auth.requireAuth();
662
737
  const tokenBudget = Math.min(Math.max(parseInt(options.budget, 10) || 1200, 200), 4000);
663
738
  const ep = resolveEndpoint('brain.context.pack');
664
- const data = await client.post(ep.path, { service: ep.service, body: { query, tokenBudget } });
739
+ const data = await client.post(ep.path, { service: ep.service, body: { query, tokenBudget, ...(options.since ? { since: options.since } : {}), ...(options.until ? { until: options.until } : {}) } });
665
740
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
666
741
  const passages = data.passages || [];
667
742
  const insufficient = data.insufficient === true || data.relevance?.insufficient === true || passages.length === 0;
@@ -693,7 +768,7 @@ export function registerBrainCommands(program, ctx) {
693
768
  if (!data || data.success === false) {
694
769
  throw new QuoxError(`brain_check failed: ${(data && (data.error || data.output)) || 'unknown error'}`, 1);
695
770
  }
696
- const mark = { supported: '✔', contradicted: '✖', unknown: '◐' }[data.verdict] || '';
771
+ const mark = { supported: '✔', contradicted: '✖', overlap: '◐', unknown: '◐' }[data.verdict] || '';
697
772
  console.log(glyphLine('brain2', `check "${claim.slice(0, 40)}"`, { result: `${data.verdict} ${mark}`.trim() }));
698
773
  const ev = data.evidence || [];
699
774
  for (const e of ev) {
@@ -749,7 +824,14 @@ export function registerBrainCommands(program, ctx) {
749
824
  return;
750
825
  }
751
826
  const ep = resolveEndpoint('notesq.notes.revisions', { id: noteId });
752
- const data = await client.get(ep.path, { service: ep.service });
827
+ let data;
828
+ try {
829
+ data = await client.get(ep.path, { service: ep.service });
830
+ } catch (e) {
831
+ const bn = await brainNoteOn404(noteId, e);
832
+ data = { revisions: [], editable: false, reason: feedReason(bn, 'revision history') };
833
+ if (program.opts().output !== 'json') { console.log(data.reason); return; }
834
+ }
753
835
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
754
836
  formatOutput({ columns: ['rev', 'action', 'author_kind', 'created_at', 'content_hash'], headers: ['Rev', 'Action', 'Author', 'When', 'Hash'], rows: data.revisions || [] }, { format: program.opts().output });
755
837
  });
@@ -770,7 +852,16 @@ export function registerBrainCommands(program, ctx) {
770
852
  throw new QuoxError(`Conflict: note ${noteId} changed since hash ${options.expectedHash} (current_hash ${current || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
771
853
  }
772
854
  const ep = resolveEndpoint('notesq.notes.restore', { id: noteId });
773
- const data = await client.post(ep.path, { service: ep.service, body: { rev } });
855
+ let data;
856
+ try {
857
+ data = await client.post(ep.path, { service: ep.service, body: { rev, expected_hash: options.expectedHash } });
858
+ } catch (e) {
859
+ if (e instanceof QuoxApiError && e.status === 409) {
860
+ const h = e.body && (e.body.currentHash || e.body.current_hash);
861
+ throw new QuoxError(`Conflict (409): note ${noteId} changed since hash ${options.expectedHash} (current_hash ${h || 'unknown'}). Nothing was restored. Re-run \`quox brain read ${noteId}\` and try again.`, 1);
862
+ }
863
+ throw e;
864
+ }
774
865
  if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
775
866
  printSuccess(data.unchanged ? `Note ${noteId} already matches revision ${rev}.` : `Restored note ${noteId} to revision ${rev} (new rev ${data.revision?.rev ?? '?'}).`);
776
867
  });
@@ -33,10 +33,26 @@ export function registerBuildCommands(program, ctx) {
33
33
  // own parsed opts.
34
34
  const groupOpt = (options, key) => options[key] ?? build.opts()[key];
35
35
 
36
+ // An org with exactly one dev server needs no --host: use it and say so. With several, name them;
37
+ // with none, say so (proof cases code/reach-build-status / -targets exited 1 on a one-server org).
38
+ async function resolveHost(options) {
39
+ const given = groupOpt(options, 'host');
40
+ if (given) return given;
41
+ const listEp = resolveEndpoint('v1.dev.servers.list');
42
+ const list = await client.get(listEp.path, { service: listEp.service });
43
+ const ids = (Array.isArray(list?.servers) ? list.servers : []).map((s) => s.host_id).filter(Boolean);
44
+ if (ids.length === 1) {
45
+ printWarning(`No --host given; using the only dev server: ${ids[0]}`);
46
+ return ids[0];
47
+ }
48
+ if (ids.length === 0) throw new Error('No dev server is registered for this org (register one with `quox dev enable <hostId>`).');
49
+ throw new Error(`--host <hostId> is required: this org has ${ids.length} dev servers (${ids.join(', ')}).`);
50
+ }
51
+
36
52
  async function dispatch(target, options) {
37
53
  auth.requireAuth();
38
54
  options = {
39
- host: groupOpt(options, 'host'),
55
+ host: await resolveHost(options),
40
56
  stream: groupOpt(options, 'stream'),
41
57
  phase: groupOpt(options, 'phase'),
42
58
  msg: groupOpt(options, 'msg'),
@@ -76,8 +92,7 @@ export function registerBuildCommands(program, ctx) {
76
92
  .option('--host <hostId>', 'dev server host_id')
77
93
  .action(async (options) => {
78
94
  auth.requireAuth();
79
- const host = groupOpt(options, 'host');
80
- if (!host) throw new Error('--host <hostId> is required');
95
+ const host = await resolveHost(options);
81
96
  const endpoint = resolveEndpoint('v1.dev.build.status', { hostId: host });
82
97
  const data = await client.get(endpoint.path, { service: endpoint.service });
83
98
  formatDetail(data, { format: program.opts().output, title: `Build status: ${host}` });
@@ -89,8 +104,7 @@ export function registerBuildCommands(program, ctx) {
89
104
  .option('--host <hostId>', 'dev server host_id')
90
105
  .action(async (options) => {
91
106
  auth.requireAuth();
92
- const host = groupOpt(options, 'host');
93
- if (!host) throw new Error('--host <hostId> is required');
107
+ const host = await resolveHost(options);
94
108
  const endpoint = resolveEndpoint('v1.dev.build.targets', { hostId: host });
95
109
  const data = await client.get(endpoint.path, { service: endpoint.service });
96
110
  formatOutput(data.targets || [], {
@@ -5,6 +5,10 @@
5
5
  import chalk from 'chalk';
6
6
  import { formatDetail } from '../core/output.js';
7
7
  import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { existsSync } from 'node:fs';
9
+ import { lastConversation, saveLastConversation } from './chatState.js';
10
+ import { loadChatCompanion } from './chatCompanion.js';
11
+ import { companionPlan, motionPolicy, VENDORED_DRAGONLAB } from '../dragon/select.js';
8
12
 
9
13
  /**
10
14
  * Build the POST body for /chat/stream (quox#782). The prompt and context
@@ -26,6 +30,46 @@ export function buildStreamQuery({ queryId, message, agent, sessionId, context }
26
30
  return body;
27
31
  }
28
32
 
33
+ /**
34
+ * TUI-DIVIDER: `quox chat --trace` prints the turn's protocol events as they arrive, so the
35
+ * per-layer AOCL record (`aocl.layer`: id, name, result, ms) is visible in the CLI before any
36
+ * UI animates it. Returns { text, tone } or null for an event --trace does not show.
37
+ * tone: ok | warn | fail | accent (no grey: every tone is a real colour).
38
+ */
39
+ const RESULT_TONE = { pass: 'ok', gated: 'warn', blocked: 'fail', bypass: 'accent' };
40
+ const fmtTraceMs = (ms) => {
41
+ const n = Number(ms);
42
+ if (!Number.isFinite(n)) return '';
43
+ return n < 10 ? `${Math.round(n * 10) / 10} ms` : `${Math.round(n)} ms`;
44
+ };
45
+ export function formatTraceEvent(ev) {
46
+ if (!ev || typeof ev !== 'object') return null;
47
+ switch (ev.type) {
48
+ case 'turn.started':
49
+ return { text: `▸ turn.started ${[ev.provider, ev.model].filter(Boolean).join(' ')}`, tone: 'accent' };
50
+ case 'aocl.layer': {
51
+ const result = String(ev.result || 'pass');
52
+ const line = `▤ aocl.layer ${String(ev.id || '?').padEnd(4)}${String(ev.name || '').padEnd(10)}${result.padEnd(8)}${fmtTraceMs(ev.ms).padStart(8)} ${ev.phase || ''}${ev.reason ? ` ${ev.reason}` : ''}`;
53
+ return { text: line, tone: RESULT_TONE[result] || 'accent' };
54
+ }
55
+ case 'aocl.summary':
56
+ return { text: `▤ aocl.summary ${ev.accounted ?? '?'}/${ev.total ?? '?'} accounted · ${ev.passed ?? 0} passed · ${ev.bypassed ?? 0} bypassed · ${ev.rejected ?? 0} rejected${ev.ms != null ? ` · ${fmtTraceMs(ev.ms)}` : ''}`, tone: ev.rejected ? 'fail' : 'ok' };
57
+ case 'aocl.verdict':
58
+ return { text: `▤ aocl.verdict ${ev.layer || 'L8'} ${ev.verdict}${ev.reason ? `: ${ev.reason}` : ''}`, tone: ev.verdict === 'blocked' ? 'fail' : 'warn' };
59
+ case 'trace':
60
+ return { text: `· trace ${ev.protocol || ''} ${ev.label || ''}${ev.detail ? ` ${ev.detail}` : ''}`, tone: 'accent' };
61
+ case 'turn.finished':
62
+ return { text: `▸ turn.finished ${ev.status || ''}${ev.usage?.elapsed_ms != null ? ` · ${ev.usage.elapsed_ms} ms` : ''}`, tone: ev.status === 'completed' ? 'ok' : 'fail' };
63
+ default:
64
+ return null;
65
+ }
66
+ }
67
+ const TONE_PAINT = { ok: chalk.green, warn: chalk.yellow, fail: chalk.red, accent: chalk.cyan };
68
+ function printTrace(ev) {
69
+ const f = formatTraceEvent(ev);
70
+ if (f) process.stderr.write((TONE_PAINT[f.tone] || chalk.cyan)(f.text) + '\n');
71
+ }
72
+
29
73
  function generateQueryId() {
30
74
  return `CLI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
31
75
  }
@@ -34,7 +78,7 @@ function generateQueryId() {
34
78
  * Consume a chat SSE stream, printing chunks as they arrive.
35
79
  * Returns { text, sessionId, agentUsed } once the stream completes.
36
80
  */
37
- export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel } = {}) {
81
+ export async function consumeStream(client, { message, agent, sessionId, context }, { onAgentLabel, trace = false, companionPath, companionDisplay } = {}) {
38
82
  const queryId = generateQueryId();
39
83
  const body = buildStreamQuery({ queryId, message, agent, sessionId, context });
40
84
 
@@ -50,7 +94,25 @@ export async function consumeStream(client, { message, agent, sessionId, context
50
94
  onAgentLabel?.(agentUsed);
51
95
  };
52
96
 
53
- for await (const event of client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body })) {
97
+ const companion = await loadChatCompanion(companionPath, { display: companionDisplay });
98
+ const write = text => companion ? companion.write(text) : process.stdout.write(text);
99
+ // Native terminal status follows legacy `done`; observe it when opted in.
100
+ let iter;
101
+ try {
102
+ iter = client.stream(resolveEndpoint('chat.stream').path, { service: resolveEndpoint('chat.stream').service, body, untilClose: Boolean(trace || companion) })[Symbol.asyncIterator]();
103
+ while (true) {
104
+ let step;
105
+ try {
106
+ step = await iter.next();
107
+ } catch (err) {
108
+ // A refusal before the stream opened carries the layers it really evaluated.
109
+ if (trace && Array.isArray(err?.body?.aocl_layers)) { companion?.suspend(); for (const l of err.body.aocl_layers) printTrace({ type: 'aocl.layer', ...l }); }
110
+ throw err;
111
+ }
112
+ if (step.done) break;
113
+ const event = step.value;
114
+ companion?.event(event);
115
+ if (trace) { companion?.suspend(); printTrace(event); }
54
116
  switch (event.type) {
55
117
  case 'connected':
56
118
  break;
@@ -58,9 +120,9 @@ export async function consumeStream(client, { message, agent, sessionId, context
58
120
  case 'thinking': {
59
121
  const stage = event.stage || '';
60
122
  if (stage === 'tool_call' || stage.includes('tool')) {
61
- console.log(chalk.dim(` ⚙ using tool: ${event.message}`));
123
+ write(chalk.dim(` ⚙ using tool: ${event.message}`) + '\n');
62
124
  } else if (process.argv.includes('--verbose') || process.argv.includes('-v')) {
63
- console.log(chalk.dim(` … ${event.message}`));
125
+ write(chalk.dim(` … ${event.message}`) + '\n');
64
126
  }
65
127
  break;
66
128
  }
@@ -68,19 +130,19 @@ export async function consumeStream(client, { message, agent, sessionId, context
68
130
  case 'chunk':
69
131
  printLabel();
70
132
  sawChunk = true;
71
- process.stdout.write(event.content || '');
133
+ write(event.content || '');
72
134
  text += event.content || '';
73
135
  break;
74
136
 
75
137
  case 'done':
76
138
  if (!sawChunk && event.full_response) {
77
139
  printLabel();
78
- process.stdout.write(event.full_response);
140
+ write(event.full_response);
79
141
  text = event.full_response;
80
142
  }
81
143
  if (event.session_id) resultSessionId = event.session_id;
82
144
  if (event.agent_used) agentUsed = event.agent_used;
83
- console.log();
145
+ write('\n');
84
146
  break;
85
147
 
86
148
  case 'error':
@@ -90,28 +152,99 @@ export async function consumeStream(client, { message, agent, sessionId, context
90
152
  break;
91
153
  }
92
154
  }
155
+ } catch (err) {
156
+ companion?.event({ type: 'error' });
157
+ throw err;
158
+ } finally {
159
+ await companion?.close();
160
+ try { await iter?.return?.(); } catch { /* cleanup must not mask a stream error */ }
161
+ }
93
162
 
94
163
  return { text, sessionId: resultSessionId, agentUsed };
95
164
  }
96
165
 
97
- /** One-shot (non-streaming) chat via POST /chat/claude. */
98
- async function sendOneShot(client, { message, agent, sessionId }) {
99
- const body = { prompt: message };
100
- if (agent) body.selected_agent = agent;
101
- if (sessionId) body.session_id = sessionId;
166
+ /** The words for a failed turn: the server's plain message first, the bare error code only as a fallback. */
167
+ export function turnFailureText(result, httpStatus) {
168
+ return result?.message || result?.error || `chat turn failed (HTTP ${httpStatus})`;
169
+ }
170
+
171
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
172
+
173
+ /**
174
+ * Poll a server-side chat turn until it is done. The turn keeps running on the server whatever this
175
+ * client does, so giving up early would hide a result (and any objects it created) from the user.
176
+ */
177
+ async function awaitTurn(client, turnId) {
178
+ const capMs = Number(process.env.QUOX_CHAT_TURN_TIMEOUT_MS) || 20 * 60 * 1000;
179
+ const fixed = Number(process.env.QUOX_CHAT_POLL_MS) || 0;
180
+ const started = Date.now();
181
+ let wait = fixed || 1500;
182
+ let failures = 0;
183
+ const ep = resolveEndpoint('chat.turn.get', { id: turnId });
184
+ while (true) {
185
+ let t;
186
+ try {
187
+ t = await client.get(ep.path, { service: ep.service });
188
+ failures = 0;
189
+ } catch (err) {
190
+ if (err?.status === 404 || /not.found/i.test(err?.message || '')) {
191
+ throw new Error(`The server no longer has chat turn ${turnId} (expired, or the collector restarted). Check \`quox conversation list\` to see whether it finished.`);
192
+ }
193
+ if (++failures >= 5) throw err;
194
+ t = { status: 'running' };
195
+ }
196
+ if (t.status === 'done') {
197
+ if (t.http_status >= 400) throw new Error(turnFailureText(t.result, t.http_status));
198
+ return t.result || {};
199
+ }
200
+ if (t.status === 'error') throw new Error(`chat turn ${turnId} failed on the server: ${t.error || 'unknown error'}`);
201
+ if (Date.now() - started > capMs) {
202
+ throw new Error(`Chat turn ${turnId} is still running after ${Math.round(capMs / 1000)}s. It has not been lost: fetch it with \`quox chat --resume ${turnId}\`.`);
203
+ }
204
+ await sleep(wait);
205
+ if (!fixed) wait = Math.min(wait + 500, 5000);
206
+ }
207
+ }
208
+
209
+ /** One-shot (non-streaming) chat via POST /chat/claude, kept server side as a pollable turn. */
210
+ async function sendOneShot(client, { message, agent, sessionId, resumeTurn }) {
211
+ let data;
212
+ if (resumeTurn) {
213
+ data = await awaitTurn(client, resumeTurn);
214
+ } else {
215
+ const body = { prompt: message };
216
+ if (agent) body.selected_agent = agent;
217
+ if (sessionId) body.session_id = sessionId;
102
218
 
103
- const data = await client.post(resolveEndpoint('chat.claude').path, {
104
- service: resolveEndpoint('chat.claude').service,
105
- body,
106
- timeout: 120000,
107
- });
219
+ // Prefer: respond-async returns 202 {turn_id} at once, so no client timeout can orphan a
220
+ // multi-tool turn. An older collector ignores it and answers synchronously (handled below).
221
+ data = await client.post(resolveEndpoint('chat.claude').path, {
222
+ service: resolveEndpoint('chat.claude').service,
223
+ body,
224
+ headers: { Prefer: 'respond-async' },
225
+ timeout: 30000,
226
+ });
227
+ if (data && data.turn_id && data.status === 'running') data = await awaitTurn(client, data.turn_id);
228
+ }
108
229
 
109
230
  const response = data.result || data.response || data.text || JSON.stringify(data);
110
231
  return { data, response, sessionId: data.session_id || sessionId };
111
232
  }
112
233
 
234
+ /** Explicit --dragon-module wins; otherwise pick the dragon for the attached agent (null path = ordinary chat). */
235
+ export function chooseCompanion({ options, config, env = process.env, isTTY = Boolean(process.stdout.isTTY), exists = existsSync }) {
236
+ const policy = motionPolicy(env);
237
+ if (options.dragonModule) return { companionPath: options.dragonModule, companionDisplay: { still: policy.still, mono: policy.mono } };
238
+ let settings = {};
239
+ try { settings = config?.getActiveProfile?.()?.quoxon || {}; } catch { /* no profile settings */ }
240
+ const e = options.dragon ? { ...env, QUOX_DRAGON_COMPANION: options.dragon } : env;
241
+ const plan = companionPlan({ agent: options.agent, settings, env: e, isTTY, exists, bundled: VENDORED_DRAGONLAB });
242
+ if (!plan.enabled) return { companionPath: undefined, companionDisplay: undefined };
243
+ return { companionPath: plan.file, companionDisplay: { still: plan.still, mono: plan.mono, persist: plan.persist } };
244
+ }
245
+
113
246
  export function registerChatCommands(program, ctx) {
114
- const { client, auth } = ctx;
247
+ const { client, auth, config } = ctx;
115
248
 
116
249
  const chat = program
117
250
  .command('chat')
@@ -120,12 +253,25 @@ export function registerChatCommands(program, ctx) {
120
253
  .option('--agent <name>', 'route to a specific agent (e.g. sentinel, cipher, nova)')
121
254
  .option('--no-stream', 'disable streaming output (one-shot request/response)')
122
255
  .option('--conversation <id>', 'continue a conversation (session id)')
256
+ .option('--continue', 'continue the last conversation for this profile and directory')
257
+ .option('--resume <turn>', 'fetch a chat turn that was still running (its id is printed if a turn outlives the wait)')
258
+ .option('--trace', 'print the turn\'s protocol events (per-layer AOCL, traces, summary) to stderr as they arrive')
259
+ .option('--dragon <mode>', 'dragon companion for dragon-backed agents: on, greeting or off (default from /dragon companion; art is bundled; QUOX_DRAGONLAB overrides it)')
260
+ .option('--dragon-module <path>', 'opt in to a trusted local dragon companion module (streaming, single-message mode)')
123
261
  .action(async (message, options) => {
124
262
  auth.requireAuth();
125
263
  const streaming = options.stream !== false;
264
+ const profileName = (() => { try { return config?.getActiveProfileName?.(); } catch { return undefined; } })();
265
+ const noteConversation = (id) => {
266
+ if (!id) return;
267
+ saveLastConversation(profileName, process.cwd(), id);
268
+ if (program.opts().output !== 'json') process.stderr.write(`conversation: ${id}\n`);
269
+ };
270
+ if (!options.conversation && options.continue) options.conversation = lastConversation(profileName, process.cwd());
271
+ if (options.dragonModule && (!message || !streaming)) throw new Error('--dragon-module requires a single message with streaming enabled.');
126
272
 
127
- // Interactive mode if no message
128
- if (!message) {
273
+ // Interactive mode if no message (a --resume fetch is not interactive)
274
+ if (!message && !options.resume) {
129
275
  const { default: inquirer } = await import('inquirer');
130
276
  let currentAgent = options.agent || 'quox';
131
277
  let sessionId = options.conversation;
@@ -161,8 +307,9 @@ export function registerChatCommands(program, ctx) {
161
307
  sessionId,
162
308
  }, {
163
309
  onAgentLabel: (agentUsed) => process.stdout.write(chalk.green(`${(agentUsed || currentAgent).toUpperCase()}: `)),
310
+ trace: Boolean(options.trace),
164
311
  });
165
- if (result.sessionId) sessionId = result.sessionId;
312
+ if (result.sessionId) { sessionId = result.sessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
166
313
  console.log();
167
314
  } else {
168
315
  const { response, sessionId: newSessionId } = await sendOneShot(client, {
@@ -170,7 +317,7 @@ export function registerChatCommands(program, ctx) {
170
317
  agent: currentAgent,
171
318
  sessionId,
172
319
  });
173
- if (newSessionId) sessionId = newSessionId;
320
+ if (newSessionId) { sessionId = newSessionId; saveLastConversation(profileName, process.cwd(), sessionId); }
174
321
  console.log(chalk.green(`${currentAgent.toUpperCase()}:`), response);
175
322
  console.log();
176
323
  }
@@ -182,15 +329,19 @@ export function registerChatCommands(program, ctx) {
182
329
  }
183
330
 
184
331
  // Single message mode
185
- if (streaming) {
332
+ if (streaming && !options.resume) {
333
+ const companion = chooseCompanion({ options, config });
186
334
  try {
187
- await consumeStream(client, {
335
+ const result = await consumeStream(client, {
188
336
  message,
189
337
  agent: options.agent,
190
338
  sessionId: options.conversation,
191
339
  }, {
192
340
  onAgentLabel: () => {}, // no label prefix in single-shot streaming; just print tokens
341
+ trace: Boolean(options.trace),
342
+ ...companion,
193
343
  });
344
+ noteConversation(result?.sessionId);
194
345
  } catch (err) {
195
346
  console.error(chalk.red(`Error: ${err.message}`));
196
347
  process.exitCode = 1;
@@ -202,15 +353,17 @@ export function registerChatCommands(program, ctx) {
202
353
  const spinner = ora('Thinking...').start();
203
354
 
204
355
  try {
205
- const { data, response } = await sendOneShot(client, {
356
+ const { data, response, sessionId: convId } = await sendOneShot(client, {
206
357
  message,
207
358
  agent: options.agent,
208
359
  sessionId: options.conversation,
360
+ resumeTurn: options.resume,
209
361
  });
210
362
  spinner.stop();
363
+ noteConversation(convId);
211
364
 
212
365
  if (program.opts().output === 'json') {
213
- console.log(JSON.stringify(data, null, 2));
366
+ console.log(JSON.stringify({ ...data, conversation_id: convId || null }, null, 2));
214
367
  } else {
215
368
  console.log(response);
216
369
 
@@ -238,3 +391,4 @@ export function registerChatCommands(program, ctx) {
238
391
  });
239
392
  });
240
393
  }
394
+