quoxcli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Dev transcript capture (opt-in, off by default). One JSONL line per client event so a local
3
+ * review loop can analyse sessions without the owner pasting terminal output.
4
+ *
5
+ * On when QUOX_DEV_TRANSCRIPT=1 or the flag file ~/.config/quox/dev-transcripts exists;
6
+ * QUOX_DEV_TRANSCRIPT=0 forces it off. Fields are ALLOWLISTED per event: objects are never
7
+ * dumped wholesale, and every string passes a token scrubber as a second line of defence.
8
+ */
9
+ import { mkdirSync, openSync, writeSync, closeSync, existsSync, chmodSync } from 'node:fs';
10
+ import { stripMarks } from './palette.js';
11
+ import { textGate } from './events.js';
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+
15
+ export const MAX_FIELD = 4000;
16
+ export const SILENT_GAP_MS = 10000;
17
+ export const MAX_ANSWER = 8192;
18
+
19
+ export function transcriptEnabled({ env = process.env, home = homedir(), exists = existsSync } = {}) {
20
+ if (env.QUOX_DEV_TRANSCRIPT === '0') return false;
21
+ if (env.QUOX_DEV_TRANSCRIPT === '1') return true;
22
+ return exists(join(home, '.config', 'quox', 'dev-transcripts'));
23
+ }
24
+
25
+ const SCRUBS = [
26
+ [/eyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]+)*/g, '[redacted-jwt]'],
27
+ [/\bBearer\s+[^\s"']+/gi, 'Bearer [redacted]'],
28
+ [/\bsk-[A-Za-z0-9_-]{8,}/g, '[redacted-key]'],
29
+ [/\b(?:ghp|gho|ghs|xox[abp]|qsk|AKIA)[A-Za-z0-9_-]{12,}/g, '[redacted-key]'],
30
+ [/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|service[_-]?key|secret|password|passwd|authorization)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi, '$1[redacted]']
31
+ ];
32
+
33
+ export function scrub(s) {
34
+ let out = String(s);
35
+ for (const [re, rep] of SCRUBS) out = out.replace(re, rep);
36
+ return out;
37
+ }
38
+
39
+ /** Strings are scrubbed and truncated; non-strings become a scrubbed JSON string (never a raw object). */
40
+ export function clean(v) {
41
+ if (v == null || typeof v === 'number' || typeof v === 'boolean') return v;
42
+ let s = typeof v === 'string' ? v : (() => { try { return JSON.stringify(v); } catch { return String(v); } })();
43
+ s = stripMarks(scrub(s)); // ⟦⟧ colour markup never reaches a record
44
+ return s.length > MAX_FIELD ? `${s.slice(0, MAX_FIELD)}…[truncated ${s.length - MAX_FIELD} chars]` : s;
45
+ }
46
+
47
+ // SSE event fields worth keeping, by name. Anything not listed is dropped.
48
+ const SSE_FIELDS = ['type', 'agent', 'model', 'provider', 'hat', 'conversation_id', 'session_id', 'stage', 'message',
49
+ 'content', 'call_id', 'tool', 'summary', 'status', 'result_summary', 'duration_ms', 'envelope_id', 'target', 'scope',
50
+ 'risk', 'describable', 'dashboard_url', 'inbox_item_id', 'approval_id', 'decision', 'text', 'protocol', 'label', 'ref',
51
+ 'detail', 'error', 'full_response'];
52
+ const RECEIPT_FIELDS = ['verified', 'ward_seq'];
53
+ // Metering only (numbers, never strings): lets a local eval loop sum the cost of interactive turns.
54
+ const USAGE_FIELDS = ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens'];
55
+ const num = (v) => typeof v === 'number' && Number.isFinite(v);
56
+
57
+ export function pickSse(ev) {
58
+ const o = {};
59
+ if (!ev || typeof ev !== 'object') return o;
60
+ for (const k of SSE_FIELDS) if (ev[k] !== undefined) o[k] = clean(ev[k]);
61
+ if (ev.receipt && typeof ev.receipt === 'object') {
62
+ o.receipt = {};
63
+ for (const k of RECEIPT_FIELDS) if (ev.receipt[k] !== undefined) o.receipt[k] = clean(ev.receipt[k]);
64
+ }
65
+ if (num(ev.cost_usd)) o.cost_usd = ev.cost_usd;
66
+ if (ev.usage && typeof ev.usage === 'object') {
67
+ const u = {};
68
+ for (const k of USAGE_FIELDS) if (num(ev.usage[k])) u[k] = ev.usage[k];
69
+ if (Object.keys(u).length) o.usage = u;
70
+ }
71
+ return o;
72
+ }
73
+
74
+ /** Map one app dispatch action to a transcript record (or null to skip). */
75
+ export function recordFor(a) {
76
+ switch (a?.type) {
77
+ case 'event': return { t: 'sse', ...pickSse(a.ev) };
78
+ case 'user': return { t: 'user', mode: a.echo ? 'command' : a.queued ? 'queue-drain' : 'send', text: clean(a.full ?? a.text) };
79
+ case 'steer.add': return { t: 'user', mode: 'steer', id: clean(a.id), text: clean(a.text) };
80
+ case 'queue.add': return { t: 'user', mode: 'queue', text: clean(a.text) };
81
+ case 'walk.reply': return { t: 'user', mode: 'walk', id: clean(a.id), text: clean(a.text) }; // WALK-FIXES: what was typed at each walkthrough card
82
+ case 'steer.failed': case 'steer.converted': return { t: 'notice', kind: a.type, id: clean(a.id), text: clean(a.reason) };
83
+ case 'note': return { t: 'notice', kind: 'note', tone: clean(a.tone), text: clean(a.text) };
84
+ case 'ask.open': return { t: 'notice', kind: 'note', tone: 'ask', text: clean(a.text) }; // ASK-SETTLE: same record as before
85
+ case 'ask.settle': case 'slotmsg.settle': return { t: 'notice', kind: 'ask.settled', tone: clean(a.tone), text: clean(a.text) };
86
+ case 'restart.countdown': return { t: 'restart.countdown', phase: clean(a.phase), seconds: Number.isFinite(a.seconds) ? a.seconds : null, build: clean(a.build ?? null) }; // started | cancelled | restart-now | timed-out | held: lets a reviewer verify the owner was shown the countdown
87
+ case 'error': return { t: 'error', text: clean(a.text) };
88
+ case 'notify': return { t: 'notify', approvals: a.patch?.approvals ?? null, error: clean(a.patch?.error ?? null) };
89
+ case 'turn.begin': return { t: 'turn.begin', turnId: clean(a.turnId) };
90
+ case 'approval.sent': return { t: 'approval.sent', action: clean(a.action), callId: clean(a.callId), tool: clean(a.tool), target: clean(a.target) };
91
+ case 'approval.result': return { t: 'approval.result', callId: clean(a.callId), outcome: clean(a.outcome), text: clean(a.text ?? null) };
92
+ case 'screen.sent': return { t: 'screen.sent', summary: clean(a.text) }; // SCREEN-CONTEXT: size and sections only, never the block itself
93
+ case 'verbose': return { t: 'verbose', mode: clean(a.mode) };
94
+ case 'ctx': return { t: 'ctx', conversationId: clean(a.ctx?.conversationId ?? null) };
95
+ // /connect: provider and last 4 only, by construction (the key is never in an action).
96
+ case 'connect.started': return { t: 'connect.started', provider: clean(a.provider) };
97
+ case 'connect.saved': return { t: 'connect.saved', provider: clean(a.provider), last4: clean(a.last4) };
98
+ case 'help': return { t: 'action', type: 'help', rows: (Array.isArray(a.model) ? a.model : []).map(m => clean([m.key, m.text].filter(Boolean).join(' '))).slice(0, 80) };
99
+ case 'inbox.sync': return null; // every 30 s, ids only: noise
100
+ default: return { t: 'action', type: clean(a?.type) };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * -> null when disabled. Otherwise { action(a), command(line), resize(cols, rows), end(reason), path }.
106
+ * Never throws into the app: a write failure disables capture for the rest of the session.
107
+ */
108
+ export function createTranscript({ env = process.env, home = homedir(), now = Date.now, exists = existsSync, meta = {} } = {}) {
109
+ if (!transcriptEnabled({ env, home, exists })) return null;
110
+ const dir = env.QUOX_DEV_TRANSCRIPT_DIR || join(home, '.local', 'share', 'quox', 'transcripts');
111
+ let fd = null;
112
+ const start = now();
113
+ const path = join(dir, `${new Date(start).toISOString().replace(/[:.]/g, '-')}-${process.pid}.jsonl`);
114
+ try {
115
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
116
+ try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
117
+ fd = openSync(path, 'a', 0o600);
118
+ chmodSync(path, 0o600);
119
+ } catch { return null; }
120
+ const turn = { id: null, t0: 0, firstText: false, lastSse: 0, text: '', provider: null, model: null, offered: null, tokens: null, answered: false };
121
+ const write = (rec) => {
122
+ if (fd == null || !rec) return;
123
+ try { writeSync(fd, JSON.stringify({ ts: new Date(now()).toISOString(), ...rec }) + '\n'); }
124
+ catch { try { closeSync(fd); } catch { /* ignore */ } fd = null; }
125
+ };
126
+ write({ t: 'start', version: clean(meta.version ?? null), cols: meta.cols ?? null, rows: meta.rows ?? null,
127
+ profile: clean(meta.profile ?? null), org: clean(meta.org ?? null), cwd: clean(meta.cwd ?? null), pid: process.pid });
128
+
129
+ const action = (a) => {
130
+ const t = now();
131
+ const rec = recordFor(a);
132
+ if (a?.type === 'turn.begin') Object.assign(turn, { id: a.turnId, t0: t, firstText: false, lastSse: t, text: '', gate: null, provider: null, model: null, offered: null, tokens: null, answered: false });
133
+ if (a?.type === 'event' && turn.id) {
134
+ if (t - turn.lastSse > SILENT_GAP_MS) write({ t: 'gap', turnId: turn.id, ms: t - turn.lastSse, before: clean(a.ev?.type) });
135
+ turn.lastSse = t;
136
+ }
137
+ write(rec);
138
+ if (a?.type === 'event' && turn.id) {
139
+ const ty = a.ev?.type;
140
+ const ev = a.ev;
141
+ if (ty === 'turn.started') {
142
+ turn.provider = ev.provider ?? turn.provider; turn.model = ev.model ?? turn.model;
143
+ const n = [ev.tools_offered, ev.tool_count, ev.tools_available].find(num);
144
+ if (n !== undefined) turn.offered = n;
145
+ }
146
+ // QUOXON-FW19: the server mirrors every delta as a full-text chunk; the same gate the screen uses decides what is answer text.
147
+ if (ty === 'turn.started' || ty === 'chunk' || ty === 'text.delta') {
148
+ const g = textGate(turn.gate, ev);
149
+ turn.gate = g.st;
150
+ if (g.append && typeof ev.content === 'string' && turn.text.length <= MAX_ANSWER) turn.text += ev.content;
151
+ }
152
+ // a final full text is authoritative: it replaces what accumulated, never adds to it
153
+ if (ty === 'done' && typeof ev.full_response === 'string' && ev.full_response) turn.text = ev.full_response;
154
+ if (ty === 'tool.finished') {
155
+ const failed = ev.status === 'failed' || ev.status === 'denied';
156
+ write({ t: 'tool', turnId: turn.id, name: clean(ev.tool ?? null), ok: ev.status === 'ok', ...(failed || ev.error ? { error: clean(ev.error ?? ev.result_summary ?? ev.status) } : {}), ms: num(ev.duration_ms) ? ev.duration_ms : null });
157
+ }
158
+ if (ty === 'error') write({ t: 'turn.error', turnId: turn.id, message: clean(ev.error ?? ev.message ?? 'stream error'), provider: clean(turn.provider), model: clean(turn.model) });
159
+ if (!turn.firstText && (ty === 'chunk' || ty === 'text.delta') && a.ev?.content) {
160
+ turn.firstText = true;
161
+ write({ t: 'timing', turnId: turn.id, first_text_ms: t - turn.t0 });
162
+ }
163
+ if (ty === 'turn.finished') {
164
+ if (ev.usage && typeof ev.usage === 'object') turn.tokens = { input: num(ev.usage.input_tokens) ? ev.usage.input_tokens : null, output: num(ev.usage.output_tokens) ? ev.usage.output_tokens : null };
165
+ if (turn.text) {
166
+ const raw = scrub(turn.text);
167
+ const text = raw.length > MAX_ANSWER ? `${raw.slice(0, MAX_ANSWER)}…[truncated ${raw.length - MAX_ANSWER} chars]` : raw;
168
+ write({ t: 'answer', turnId: turn.id, provider: clean(turn.provider), model: clean(turn.model), tokens: turn.tokens, duration_ms: t - turn.t0, tools_offered: turn.offered, text });
169
+ }
170
+ write({ t: 'timing', turnId: turn.id, finish_ms: t - turn.t0, status: clean(a.ev?.status ?? null), saw_text: turn.firstText });
171
+ turn.id = null;
172
+ }
173
+ }
174
+ };
175
+ return {
176
+ path,
177
+ action,
178
+ command: (line) => { const [cmd, ...rest] = String(line).trim().split(/\s+/); write({ t: 'command', cmd: clean(cmd), arg: clean(rest.join(' ') || null) }); },
179
+ // Owner observation (/note): recorded verbatim (scrubbed, 2000 chars), never sent anywhere else.
180
+ note: (text) => write({ t: 'note', text: clean(String(text).slice(0, 2000)), ts: new Date(now()).toISOString() }),
181
+ // ! commands (QUOXON-FW8): the command line and exit code only, never the output.
182
+ bang: (cmd, code) => write({ t: 'bang', cmd: clean(String(cmd).slice(0, 2000)), exit: clean(code) }),
183
+ resize: (cols, rows) => write({ t: 'resize', cols: cols ?? null, rows: rows ?? null }),
184
+ // SCREEN-FRAMES: the visible screen as plain text (screenFrames.js caps it at 16 KB), scrubbed, local file only.
185
+ screen: ({ cols = null, rows = null, text = '', colors = {}, truncated } = {}) => write({ t: 'screen', cols, rows, text: scrub(String(text)), colors: colors && typeof colors === 'object' ? colors : {}, ...(truncated ? { truncated } : {}) }),
186
+ end: (reason = 'exit') => { write({ t: 'end', reason: clean(reason), duration_ms: now() - start }); if (fd != null) { try { closeSync(fd); } catch { /* ignore */ } fd = null; } }
187
+ };
188
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The Q face for the web (cli.quox.ai): the SAME pure frames the CLI draws (qart.js, qglyph.js, qhello.js),
3
+ * rendered into <pre> elements as coloured spans. Plays Orbit & Lock, then the Q says hello (the line
4
+ * decodes in from katakana), then idles: the mark breathes, the status Q breathes beside "Ready", and now
5
+ * and then the dragon's googly eyes open and blink. prefers-reduced-motion: the settled frame only.
6
+ *
7
+ * Browser ESM, no dependencies, no build: import it next to ../qart.js, ../qglyph.js, ../qhello.js.
8
+ * import { mountQFace } from './qface.js';
9
+ * mountQFace(document.getElementById('face'), { hello: "Hi, I'm Quox!" });
10
+ */
11
+ import { introPlan, restCellsAt, INTRO_MS } from '../qart.js';
12
+ import { eggCells, EGG_MS } from '../qdragonScenes.js';
13
+ import { qGlyph } from '../qglyph.js';
14
+ import { helloCells, helloGradient, helloMs } from '../qhello.js';
15
+
16
+ const esc = (s) => s.replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
17
+
18
+ /** One row of cells -> HTML spans, merging runs of the same colour. */
19
+ export function rowToHTML(row) {
20
+ let html = '', run = '', key = null;
21
+ const flush = () => { if (!run) return; html += key ? `<span style="${key}">${esc(run)}</span>` : esc(run); run = ''; };
22
+ for (const c of row) {
23
+ const parts = [c.rgb ? `color:rgb(${c.rgb.join(',')})` : null, c.bg ? `background-color:rgb(${c.bg.join(',')})` : null, c.bold ? 'font-weight:700' : null].filter(Boolean);
24
+ const k = parts.length ? parts.join(';') : null; // bg: the dragon's quadrant-block eyes (solid whites, dark pupil)
25
+ if (k !== key) { flush(); key = k; }
26
+ run += c.ch;
27
+ }
28
+ flush();
29
+ return html;
30
+ }
31
+ // Blank cells in Braille art are drawn as U+2800 (the empty Braille pattern), not a space: a browser can
32
+ // give a space and a Braille glyph different advances (font fallback), which shears the Q row by row.
33
+ const BLANK = '\u2800';
34
+ export const cellsToHTML = (rows) => rows.map((r) => rowToHTML(r.map((c) => (c.ch === ' ' ? { ...c, ch: BLANK } : c)))).join('\n');
35
+
36
+ /** Which Q size fits a container of `px` CSS pixels at a monospace advance of `cw` px. */
37
+ export function sizeForWidth(px, cw = 8.4) {
38
+ const cols = Math.floor(px / cw);
39
+ return cols >= 34 ? 'large' : cols >= 22 ? 'mid' : 'small';
40
+ }
41
+
42
+ /**
43
+ * Timeline state at t ms (pure): which picture, the hello progress, and whether the eye is open.
44
+ * eyeAt: list of start times (ms) for idle eye blinks.
45
+ */
46
+ export function faceFrame(t, { size, hello, eyeAt = [], color = true }) {
47
+ const plan = introPlan(size, { color, quant: false });
48
+ const hT = t - INTRO_MS, settledAt = INTRO_MS + helloMs(hello);
49
+ let q;
50
+ const eye = eyeAt.find((s) => t >= s && t < s + EGG_MS + 400);
51
+ if (t < INTRO_MS) q = plan.at(t);
52
+ else if (eye != null) q = eggCells(size, t - eye, { color });
53
+ else q = restCellsAt(size, Math.max(0, t - settledAt), { color, quant: false });
54
+ const line = helloCells(hello, hT, { fg: helloGradient(hello), mono: !color });
55
+ const glyph = qGlyph('resting', Math.max(0, t - settledAt), { mono: !color });
56
+ return { q, line, glyph, showStatus: t >= settledAt };
57
+ }
58
+
59
+ export function mountQFace(root, { hello = "Hi, I'm Quox!", status = 'Ready', seed = 7, reduced = null, eyeEveryMs = [9000, 16000] } = {}) {
60
+ const doc = root.ownerDocument, win = doc.defaultView;
61
+ root.classList.add('qface');
62
+ root.innerHTML = '<pre class="qface-q" aria-hidden="true"></pre><div class="qface-side"><pre class="qface-hello"></pre><pre class="qface-status"></pre></div>';
63
+ root.setAttribute('role', 'img');
64
+ root.setAttribute('aria-label', `The Quox Q says: ${hello}`);
65
+ const [qEl, hEl, sEl] = ['.qface-q', '.qface-hello', '.qface-status'].map((s) => root.querySelector(s));
66
+ const still = reduced ?? Boolean(win.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
67
+ let r = seed; const rand = () => { r = (r * 16807) % 2147483647; return r / 2147483647; };
68
+ const eyeAt = [];
69
+ let next = INTRO_MS + helloMs(hello) + 2500; // the first blink comes soon after hello, then rarely
70
+ for (let i = 0; i < 40; i++) { eyeAt.push(next); next += EGG_MS + eyeEveryMs[0] + rand() * (eyeEveryMs[1] - eyeEveryMs[0]); }
71
+ const size = () => sizeForWidth(Math.min(root.clientWidth || 600, win.innerWidth || 600) * (root.clientWidth > 520 ? 0.55 : 1));
72
+ const draw = (t) => {
73
+ const f = faceFrame(t, { size: size(), hello, eyeAt });
74
+ qEl.innerHTML = cellsToHTML(f.q);
75
+ hEl.innerHTML = rowToHTML(f.line);
76
+ sEl.innerHTML = f.showStatus ? `${rowToHTML(f.glyph.cells)} <span class="qface-word">${esc(status)}</span>` : '&nbsp;';
77
+ };
78
+ if (still) { draw(INTRO_MS + helloMs(hello) + 10); return { stop() {}, pause() {}, resume() {}, seek: draw }; }
79
+ // pause()/resume() keep the animation's own clock: time spent paused (offscreen, hidden tab) is not skipped.
80
+ let raf = 0, t0 = null, stopped = false, paused = false, pausedAt = null, last = 0;
81
+ const tick = (now) => { if (stopped || paused) return; t0 ??= now; last = now - t0; draw(last); raf = win.requestAnimationFrame(tick); };
82
+ raf = win.requestAnimationFrame(tick);
83
+ return {
84
+ stop() { stopped = true; win.cancelAnimationFrame(raf); },
85
+ pause() { if (paused || stopped) return; paused = true; pausedAt = last; win.cancelAnimationFrame(raf); },
86
+ resume() { if (!paused || stopped) return; paused = false; t0 = null; const base = pausedAt; raf = win.requestAnimationFrame((now) => { t0 = now - base; tick(now); }); },
87
+ seek: draw,
88
+ };
89
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * First-run notice for bare `quox`: who we are, legal, docs. Pure model + a tiny "seen" marker kept in the
3
+ * CLI's existing config dir (~/.config/quox, same place as config.json). Links live ONLY here.
4
+ */
5
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join } from 'node:path';
8
+
9
+ export const DOCS_URL = 'https://cli.quox.ai/docs/start/'; // #1100: CLI docs "Start here" (cli.quox.ai/docs/ itself has no index page)
10
+ export const TERMS_URL = 'https://quox.ai/terms';
11
+ export const PRIVACY_URL = 'https://quox.ai/privacy';
12
+ export const LICENCE = 'Quox Community Source Licence 1.0 (source-available)';
13
+ export const WELCOME_VERSION = 1;
14
+
15
+ export function packageVersion() {
16
+ try { return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version || '0.1.0'; } catch { return '0.1.0'; }
17
+ }
18
+
19
+ /** [{ kind: 'title'|'sub'|'rule'|'field'|'para'|'gap'|'hint', ... }] */
20
+ export function welcomeModel(version = packageVersion()) {
21
+ return [
22
+ { kind: 'title', text: 'Welcome to Quox' },
23
+ { kind: 'sub', text: `Quox CLI ${version}` },
24
+ { kind: 'para', text: 'The main front door to Quox, your self-hosted control plane for AI agents.' },
25
+ { kind: 'rule' },
26
+ { kind: 'field', label: 'Docs', value: DOCS_URL, link: true },
27
+ { kind: 'field', label: 'Terms', value: TERMS_URL, link: true },
28
+ { kind: 'field', label: 'Privacy', value: PRIVACY_URL, link: true },
29
+ { kind: 'field', label: 'Licence', value: LICENCE },
30
+ { kind: 'rule' },
31
+ { kind: 'para', text: 'Your prompts go to your own QuoxCORE server and to the AI provider you connect to it.' },
32
+ { kind: 'para', text: 'Risky actions ask you first, and the actions Quox takes leave receipts you can verify.' },
33
+ { kind: 'para', text: 'This CLI sends no telemetry to Quox.' },
34
+ { kind: 'para', text: 'By using Quox CLI you agree to the Terms.' },
35
+ { kind: 'hint', text: 'Enter to continue · /about to see this again' }
36
+ ];
37
+ }
38
+
39
+ export const welcomeStatePath = () => join(homedir(), '.config', 'quox', 'welcome.json');
40
+
41
+ export function welcomeSeen(path = welcomeStatePath()) {
42
+ try { return (JSON.parse(readFileSync(path, 'utf8')).version || 0) >= WELCOME_VERSION; } catch { return false; }
43
+ }
44
+ export function markWelcomeSeen(path = welcomeStatePath()) {
45
+ try { mkdirSync(join(path, '..'), { recursive: true }); writeFileSync(path, JSON.stringify({ version: WELCOME_VERSION, seenAt: new Date().toISOString() }) + '\n'); return true; } catch { return false; }
46
+ }
47
+
48
+ /** First interactive launch only: a TTY, not suppressed, not seen at this notice version. */
49
+ export function shouldShowWelcome({ env = process.env, isTTY = process.stdout.isTTY && process.stdin.isTTY, print = false, path } = {}) {
50
+ if (print || !isTTY || env.QUOX_NO_WELCOME === '1') return false;
51
+ return !welcomeSeen(path);
52
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * "What's new" (QUOXON-FW16, derived in QUOXON-WHATSNEW3). Curated entries live in whatsnew.jsonl: append-only, one
3
+ * JSON object per line, added by the commit that ships the change (see whatsnew.md). ENTRIES is sorted newest first.
4
+ * With no curated source (a dev checkout without the file) entries are derived from git: commits on the CLI tree with
5
+ * a `Whatsnew: <title> | <command>` trailer or a `[whatsnew]` subject tag.
6
+ * Last-seen state is kept on the CLI profile (quoxon.whatsNew = { build, id, date }): the build the user last ran and
7
+ * the newest entry they have been shown.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { execFileSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const byDateDesc = (a, b) => (Date.parse(b.date) || 0) - (Date.parse(a.date) || 0);
14
+
15
+ /** whatsnew.jsonl text -> entries newest first. Blank or unparsable lines are skipped. */
16
+ export function parseEntries(text) {
17
+ const out = [];
18
+ for (const line of String(text || '').split('\n')) {
19
+ const t = line.trim(); if (!t) continue;
20
+ try { const e = JSON.parse(t); if (e && e.id && e.title) out.push(e); } catch { /* skip a broken line */ }
21
+ }
22
+ return out.sort(byDateDesc);
23
+ }
24
+
25
+ // %x1f between fields, %x1e between commits; the trailer value is "title | command" (command optional).
26
+ export const GIT_LOG_FORMAT = '%h%x1f%cI%x1f%s%x1f%(trailers:key=Whatsnew,valueonly,separator=%x20)%x1e';
27
+
28
+ /** git log output (GIT_LOG_FORMAT) -> entries, keeping only commits marked user-facing. */
29
+ export function gitEntries(log) {
30
+ const out = [];
31
+ for (const rec of String(log || '').split('\x1e')) {
32
+ const [sha, date, subject = '', trailer = ''] = rec.replace(/^\s+/, '').split('\x1f');
33
+ if (!sha) continue;
34
+ const tv = trailer.trim();
35
+ let title = '', cmd = '';
36
+ if (tv && tv.toLowerCase() !== 'none') [title, cmd = ''] = tv.split('|').map(x => x.trim());
37
+ else if (/\[whatsnew\]/i.test(subject)) title = subject.replace(/\[[^\]]*\]\s*/g, '').trim();
38
+ if (!title) continue;
39
+ out.push({ id: `git-${sha}`, date: (date || '').trim(), title, text: title, ...(cmd ? (cmd.startsWith('/') ? { try: cmd } : { shell: cmd }) : {}), sha });
40
+ }
41
+ return out.sort(byDateDesc);
42
+ }
43
+
44
+ /** Curated file first; git only when the file is missing or empty. Never throws. */
45
+ export function loadEntries({ readCurated, readGit }) {
46
+ let curated = [];
47
+ try { curated = parseEntries(readCurated()); } catch { /* no curated source */ }
48
+ if (curated.length) return curated;
49
+ try { return gitEntries(readGit()); } catch { return []; }
50
+ }
51
+
52
+ /**
53
+ * Curated text: whatsnew.jsonl in this repo; the public mirror export ships the same entries as a whatsnew.json array
54
+ * (scripts/release/public-mirror/export.js), which is turned back into one JSON object per line here.
55
+ */
56
+ export function readCuratedText(read) {
57
+ try { return read('./whatsnew.jsonl'); } catch { /* not here: try the exported array */ }
58
+ const arr = JSON.parse(read('./whatsnew.json'));
59
+ return Array.isArray(arr) ? arr.map(e => JSON.stringify(e)).join('\n') : '';
60
+ }
61
+
62
+ const CLI_ROOT = fileURLToPath(new URL('../..', import.meta.url));
63
+ export const ENTRIES = loadEntries({
64
+ readCurated: () => readCuratedText(f => readFileSync(new URL(f, import.meta.url), 'utf8')),
65
+ readGit: () => execFileSync('git', ['-C', CLI_ROOT, 'log', '-n', '300', `--format=${GIT_LOG_FORMAT}`, '--', '.'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }),
66
+ });
67
+
68
+ /** Entries newer than the last one shown: by id, else by date (the id is unknown after a source switch). */
69
+ export function unseenEntries(last, entries = ENTRIES) {
70
+ if (!last || typeof last !== 'object' || (!last.id && !last.date)) return entries;
71
+ const at = last.id ? entries.findIndex(e => e.id === last.id) : -1;
72
+ if (at >= 0) return entries.slice(0, at);
73
+ const t = Date.parse(last.date);
74
+ return Number.isFinite(t) ? entries.filter(e => Date.parse(e.date) > t) : entries;
75
+ }
76
+
77
+ /** The profile record after showing: build + newest entry. */
78
+ export const seenRecord = (build, entries = ENTRIES) => ({ build: String(build || ''), id: entries[0]?.id, date: entries[0]?.date });
79
+
80
+ /** The command to try for an entry: a slash command, else a shell command. */
81
+ export const tryText = (e) => e.try || e.shell || '';
82
+
83
+ export const BOX_MAX = 5;
84
+ export const LIST_DEFAULT = 10;
85
+
86
+ export const tryCommand = (e) => String(e.try || '').trim().split(/\s+/)[0];
87
+
88
+ /** Startup decision. -> { show: entries[], more: n, record: {build,id} | null }. Nothing recorded when there are no entries. */
89
+ export function startupWhatsNew({ build, last, entries = ENTRIES }) {
90
+ const newest = entries[0]?.id;
91
+ if (!newest) return { show: [], more: 0, record: null };
92
+ const record = { build: String(build || ''), id: newest };
93
+ if (!last || typeof last !== 'object' || !last.id) return { show: [], more: 0, record }; // first ever run: record silently
94
+ if (last.build === record.build) return { show: [], more: 0, record: last.id === newest ? null : record };
95
+ const at = entries.findIndex(e => e.id === last.id);
96
+ const unseen = at < 0 ? entries : entries.slice(0, at); // unknown last id (e.g. a rollback): treat all as unseen
97
+ return { show: unseen.slice(0, BOX_MAX), more: Math.max(0, unseen.length - BOX_MAX), record };
98
+ }
99
+
100
+ /** /whatsnew [all]. -> { entries, more } */
101
+ export function listWhatsNew(arg = '', entries = ENTRIES) {
102
+ const all = String(arg).trim().toLowerCase() === 'all';
103
+ const shown = all ? entries : entries.slice(0, LIST_DEFAULT);
104
+ return { entries: shown, more: entries.length - shown.length };
105
+ }
106
+
107
+ // ---- New commands (QUOXON-WHATSNEW2) -------------------------------------------------------------------------------
108
+ // Per-profile record of registry command ids the user has already been shown: quoxon.whatsNewSeen = [id...].
109
+ // Startup preference: quoxon.whatsNewStartup = 'on' | 'off' (default on). Both live in the synced profile settings.
110
+ export const startupEnabled = (settings) => String(settings?.whatsNewStartup || 'on').toLowerCase() !== 'off';
111
+
112
+ const activeCommands = (registry) => registry.filter(e => e.kind === 'command' && e.status === 'active');
113
+
114
+ /**
115
+ * Commands the user has not been shown, newest first (registry is oldest first, so reversed). With no record yet
116
+ * (first run, or an install that predates this) only entries flagged `new: true` count, so nobody gets a wall of
117
+ * every command. all=true ignores the record.
118
+ */
119
+ export function unseenCommands({ registry, seen, all = false }) {
120
+ const cmds = activeCommands(registry);
121
+ const pool = all ? cmds : Array.isArray(seen) ? cmds.filter(e => !seen.includes(e.id)) : cmds.filter(e => e.new);
122
+ return [...pool].reverse();
123
+ }
124
+
125
+ /** The ids to store after showing: everything currently active (so later additions show as new). */
126
+ export const seenAfter = (registry) => activeCommands(registry).map(e => e.id);
127
+
128
+ /** One compact startup line, or null. e.g. "6 new commands (/mind, /ask, /page, ...) · /whatsnew for more". */
129
+ export function startupCommandLine(list, max = 3) {
130
+ if (!list.length) return null;
131
+ const names = list.slice(0, max).map(e => '/' + e.id).join(', ') + (list.length > max ? ', ...' : '');
132
+ return `${list.length} new command${list.length === 1 ? '' : 's'} (${names}) \u00b7 /whatsnew for more`;
133
+ }
134
+
135
+ /** /whatsnew on|off. -> 'on' | 'off' | null */
136
+ export const toggleArg = (arg) => { const a = String(arg).trim().toLowerCase(); return a === 'on' || a === 'off' ? a : null; };
@@ -0,0 +1,89 @@
1
+ {"id":"bang-tty","date":"2026-10-16T16:00:00Z","title":"! hands over the terminal when a command needs it","text":"!quox lucid deck, !vim, !top, !less, !ssh host and other interactive commands now get the real terminal; quit them to come back. A ! command that fails for want of a terminal is re-run handed over. Everything else is still captured, so /share works.","since":"0.3.0","shell":"quox lucid deck"}
2
+ {"id":"fw12-demo","date":"2026-10-13T16:00:00Z","title":"Play the demo","text":"See a short guided run of Quox without touching your real setup.","try":"/demo","since":"0.3.0"}
3
+ {"id":"fw12-lan-ollama","date":"2026-10-13T16:30:00Z","title":"Connect a model on your network","text":"Connect an Ollama server on another machine on your network. Quox asks first, and you confirm with /connect yes.","try":"/connect","since":"0.3.0"}
4
+ {"id":"engine-chat-actions","date":"2026-10-13T17:00:00Z","title":"Chat can do things for you","text":"Ask in plain words and chat can create a slot or run a quox command for you. This is rolling out, so it may not be on your server yet.","since":"0.3.0"}
5
+ {"id":"fw13-attach-message","date":"2026-10-13T17:30:00Z","title":"Attach and speak in one go","text":"Attach to an agent by name and send it a message in the same line.","try":"/attach","since":"0.3.0"}
6
+ {"id":"fw13-one-dragon","date":"2026-10-13T18:00:00Z","title":"One dragon everywhere","text":"The dragon is the same everywhere in Quox now. The old eye is retired.","try":"/dragon","since":"0.3.0"}
7
+ {"id":"fw13-aocl-line","date":"2026-10-13T18:30:00Z","title":"One AOCL line per turn","text":"Each turn ends with a single AOCL line. Open the full table whenever you want it.","try":"/aocl","since":"0.3.0"}
8
+ {"id":"fw13-calmer-screen","date":"2026-10-13T19:00:00Z","title":"A calmer screen","text":"Each role gets one colour, history has more room, and protocol lines are hidden until you ask for them.","try":"/verbose on","since":"0.3.0"}
9
+ {"id":"fw14-slots-survive","date":"2026-10-13T19:30:00Z","title":"Your slot survives a restart","text":"Restarting keeps the slot you hold, and renaming the session renames the slot.","try":"/restart","since":"0.3.0"}
10
+ {"id":"fw12-login-in-place","date":"2026-10-13T20:00:00Z","title":"Sign in without leaving","text":"Sign in again right where you are. CLI sign-ins now last 30 days.","try":"/login","since":"0.3.0"}
11
+ {"id":"fw13-tips-search","date":"2026-10-13T20:30:00Z","title":"Search the tips","text":"Tips now come in a frame, and you can search them by topic or by what you want to do.","try":"/tips","since":"0.3.0"}
12
+ {"id":"fw14-shell-share","date":"2026-10-13T21:00:00Z","title":"Run shell commands, share the output","text":"Start a line with ! to run a shell command here, then attach its output to your next message.","try":"/share","since":"0.3.0"}
13
+ {"id":"fw14-tab-suggestions","date":"2026-10-13T21:30:00Z","title":"Tab suggests arguments","text":"After a command and a space, press Tab to see what can come next.","try":"/model","since":"0.3.0"}
14
+ {"id":"fw14-model-chooser","date":"2026-10-13T22:00:00Z","title":"Model chooser with favourites","text":"Pick a model from what is connected, star your favourites and set the one new sessions start on.","try":"/model","since":"0.3.0"}
15
+ {"id":"fw15-approve-many","date":"2026-10-13T22:30:00Z","title":"Approve several at once","text":"Approve more than one request in a single line, or all of them.","try":"/approve all","since":"0.3.0"}
16
+ {"id":"fw15-yn-approvals","date":"2026-10-13T23:00:00Z","title":"Answer approvals with y or n","text":"With an empty composer, y or n answers the approval card you can see. Privileged actions need y twice.","try":"/approvals","since":"0.3.0"}
17
+ {"id":"fw16-whatsnew","date":"2026-10-14T00:00:00Z","title":"What's new","text":"Quox now tells you what changed since you last ran it, once, when you start it.","try":"/whatsnew","since":"0.3.0"}
18
+ {"id":"history-across-runs","date":"2026-10-15T16:00:00Z","title":"Up recalls what you typed","text":"Up steps back through your earlier prompts and commands, most recent first, including ones from earlier runs. /say also takes a session name now: /say SITE-CLI reaches the slot that session holds.","try":"/say SITE-CLI hello","since":"0.3.0"}
19
+ {"id":"say-many-bell","date":"2026-10-15T16:30:00Z","title":"Message several sessions at once","text":"/say a,b,c <text> sends each one a copy that starts with who else it went to. Incoming slot messages now ring the terminal bell; /settings turns it off.","try":"/say cli-site,provisioner hello","since":"0.3.0"}
20
+ {"id":"reconnect-restart","date":"2026-10-15T19:46:21Z","title":"Calm reconnect when the server restarts","text":"If the Quox server restarts, the TUI says it is reconnecting and carries on; nothing is lost.","since":"0.3.0","sha":"716b979f7"}
21
+ {"id":"restart-keeps-agent","date":"2026-10-15T21:17:49Z","title":"/restart keeps your agent and slot","text":"Restart and you come back attached to the same agent, still holding the same slot.","try":"/restart","since":"0.3.0","sha":"0c69c2042"}
22
+ {"id":"poke-wake","date":"2026-10-15T22:33:32Z","title":"/poke and /wake really wake a session","text":"An idle session holding a slot is woken to read its messages, and a draft you are typing is never typed over.","try":"/wake <slot>","since":"0.3.0","sha":"7142e5679"}
23
+ {"id":"health-card","date":"2026-10-15T23:05:16Z","title":"/health shows a Core summary card","text":"Services, computers, agents, approvals and missions, all in one card.","try":"/health","since":"0.3.0","sha":"e6dfaa69e"}
24
+ {"id":"late-answers","date":"2026-10-16T02:19:16Z","title":"Late answers still reach you","text":"When a helper agent answers after the wait, its answer is delivered in the same turn instead of being lost.","since":"0.3.0","sha":"21a609308"}
25
+ {"id":"slots-columns","date":"2026-10-16T02:21:38Z","title":"/slots in tidy columns","text":"Every slot with who holds it and when it was last used, yours first.","try":"/slots","since":"0.3.0","sha":"90f4ccd83"}
26
+ {"id":"sleep-cmd","date":"2026-10-16T02:37:50Z","title":"/sleep","text":"The dragon curls up and sleeps.","try":"/sleep","since":"0.3.0","sha":"da9c1b188"}
27
+ {"id":"mirror-real","date":"2026-10-16T03:06:04Z","title":"/mirror shows the real Mirror","text":"See what CommanderQ sees about your org: identity, health, plugins and memory, with how old it is.","try":"/mirror","since":"0.3.0","sha":"2eb27da6e"}
28
+ {"id":"status-two-rows","date":"2026-10-16T03:08:06Z","title":"A two-row status line with labels","text":"Profile, org, mode, agent, slot and groups each have a label, and the model sits on the line above where you type.","try":"/groups","since":"0.3.0","sha":"6244dbc43"}
29
+ {"id":"update-cmd","date":"2026-10-16T03:11:38Z","title":"/update and quox update","text":"Update Quox to the latest version from inside the TUI or from your shell, then restart.","try":"/update","since":"0.3.0","sha":"30afdffbc"}
30
+ {"id":"run-requests","date":"2026-10-16T03:18:53Z","title":"Run requests in messages","text":"A slot message can ask you to run a command here; you see the whole command first and answer y, N or c.","shell":"quox slot say <slot> --run \"<command>\" \"<why>\"","since":"0.3.0","sha":"fa9b7f9d4"}
31
+ {"id":"triage-q-queue","date":"2026-10-16T04:35:00Z","title":"TRIAGE-Q works your approvals queue","text":"TRIAGE-Q pages through the whole queue, opens items, and snoozes, assigns or dismisses them one at a time.","try":"/attach triage-q","since":"0.3.0","sha":"7374985e2"}
32
+ {"id":"asks-are-cards","date":"2026-10-16T04:37:48Z","title":"Every question is an approval card","text":"Questions and approvals share one card that says what will happen and why; answer y, N, or c to talk it through.","try":"/approvals","since":"0.3.0","sha":"16a82f01d"}
33
+ {"id":"ask-session","date":"2026-10-16T05:11:59Z","title":"/ask a session a question","text":"/ask <slot> <question> sends your question straight to that session, like /say.","try":"/ask <slot> <question>","since":"0.3.0","sha":"2e064d05c"}
34
+ {"id":"org-window-back","date":"2026-10-16T05:38:46Z","title":"/org switch stays in this window, /org back returns","text":"Switching org in one window no longer moves your other windows, and /org back takes you to the org you were in.","try":"/org back","since":"0.3.0","sha":"dab35b8bd"}
35
+ {"id":"memory-core","date":"2026-10-16T05:44:08Z","title":"Memory core, pin and recall","text":"Pin the facts that matter into your org core memory, and chat recalls them with where they came from.","shell":"quox memory core","since":"0.3.0","sha":"be605c485"}
36
+ {"id":"mouse-cmd","date":"2026-10-16T05:59:04Z","title":"/mouse turns on clicks","text":"Click a sender to reply, or y, n or c on a card to answer; hold Shift to copy text.","try":"/mouse on","since":"0.3.0","sha":"dabc66f54"}
37
+ {"id":"org-slugs","date":"2026-10-16T07:05:41Z","title":"--org takes the org short name","text":"quox --org <slug> works with the org short name, not only its ID.","shell":"quox --org <slug> whoami","since":"0.3.0","sha":"9f85dec70"}
38
+ {"id":"mode-plan","date":"2026-10-16T07:21:11Z","title":"/mode plan: plan without changing anything","text":"In plan mode Quox investigates and proposes, the server refuses changes, and you say y to run the plan.","try":"/mode plan","since":"0.3.0","sha":"1351de192"}
39
+ {"id":"partial-says-partial","date":"2026-10-16T07:58:19Z","title":"Partial answers say partial","text":"When a turn could only finish part of the job, the answer says so instead of claiming it is complete.","since":"0.3.0","sha":"56e8a88a1"}
40
+ {"id":"current-focus","date":"2026-10-16T09:34:27Z","title":"Chat knows your current focus","text":"Each chat turn now carries your open objectives and recent asks, so answers stay on what you are working on.","since":"0.3.0","sha":"c62572590"}
41
+ {"id":"whatsnew-derived","date":"2026-10-16T15:15:00Z","title":"What's new shows only what changed","text":"/whatsnew lists what is new since you last looked, then marks it seen; when there is nothing new it says so.","try":"/whatsnew","since":"0.3.0"}
42
+ {"id":"profile-switch","date":"2026-10-16T15:10:00Z","title":"Switch profile without quitting","text":"An agent-login window that hits approvals or a step-up now offers your signed-in login as a y/N card: y switches this window, same conversation, attach, slot and groups. /profile <name> switches directly. A /slot name that does not exist asks to create it.","try":"/profile","since":"0.3.0","sha":"bb00994c8"}
43
+ {"id":"restore-tui-fixes","date":"2026-10-16T18:00:00Z","title":"Tab answers the approval card again","text":"With a draft in the composer, Tab focuses the approval card (your draft is kept, Esc returns to it) instead of sending the draft. /dragon-fly now fits short terminals, the background-agents line shows again, and streamed text is never cut.","try":"/approvals","since":"0.3.0"}
44
+ {"id":"presence-afk-sleep","date":"2026-10-16T16:40:00Z","title":"/afk and /sleep tell your sessions","text":"/afk [note] and /sleep mark you away or sleeping on the server, so sessions saying into hitl or your slot see \"owner away since 15:54 UTC: approvals will queue\" (quox ask and quox slot list too). Nothing is approved while you are away; /back or any message marks you present. quox presence shows who is away; /observe <text> sends a note to the observer.","try":"/afk lunch","since":"0.3.0","sha":"560262bc6"}
45
+ {"id":"answer-pairs-yestoall-pin","date":"2026-10-16T21:00:00Z","title":"Answer many at once: /approve 1 y 2 n","text":"/approve and /answer take pairs: 1 y 2 n, 1 y, 2 n or 1y 2n, mixed yes and no, all checked first, and the PIN is asked once if any yes is high risk (a no never needs it). /yestoall now shows one card for every yes/no question instead of one per item, and a yes on a high-risk ask needs your PIN.","try":"/approve 1 y 2 n","since":"0.3.0"}
46
+ {"id":"hat-command","date":"2026-10-16T19:20:00Z","title":"/hat changes hat without asking the AI","text":"/hat lists your hats with the current one marked. /hat DevOps (the key, the name, or a unique start of it) switches this conversation straight away, recorded on the server; a privileged hat asks y/N first. Works with any model, including small local ones.","try":"/hat","since":"0.3.0"}
47
+ {"id":"approval-wwwh","date":"2026-10-16T21:50:00Z","title":"Approval cards say what, where, who and why","text":"Every approval card, /approvals row and /approvals N detail now shows the exact command (verbatim, cut only with a visible count), the host count and names, the delegation chain and the human it acts for, the reason, your original request and the time, and whether the server judged it read-only. New items also keep your request text when the turn registry lost it.","try":"/approvals","since":"0.3.0"}
48
+ {"id":"slot-reply-path","date":"2026-10-17T00:45:00Z","title":"Replies to your /say now reach you","text":"A /say, @slot or /send from a window that holds a slot is now stamped with that slot by the server, so the other side replies to you. After you say into a slot, the window also shows that slot's holder's lines arriving in it for 30 minutes, and the \"no reply yet\" nudge no longer fires for a reply that showed.","try":"/say lucid sitrep","since":"0.3.0"}
49
+ {"id":"wake-real","date":"2026-10-17T01:15:00Z","title":"/wake really wakes","text":"/poke, /wake and /slap now make an idle session start a turn: the keeper checks the wake line left the prompt box and presses Enter again if Claude Code swallowed it. You are told whether the session was woken or, mid-turn, queued until its turn ends.","try":"/poke <slot>","since":"0.3.0"}
50
+ {"id":"ask-settle","date":"2026-10-17T01:15:00Z","title":"Answered questions go away","text":"A y/N/c question (Wake it now?, Run it?, Switch?, and the rest) now stays a live card only while it is open. Once you answer, it expires or it is no longer open, the card goes and one line stays, such as ✔ Wake it now? yes (00:44).","try":"/poke <slot>","since":"0.3.0"}
51
+ {"id":"active-and-hints","date":"2026-10-17T09:00:00Z","title":"/active: who is doing what","text":"/active (and quox active) is one short summary of every active worker, grouped Working, Waiting on you and Idle, newest first, with what each is doing and since when; /active sessions, slots and agents filter it. A mistyped command now names the 1-3 nearest real commands, by spelling and by what they do, and a single strong match is a y/N card that runs it.","try":"/active","since":"0.3.0"}
52
+ {"id":"restart-no-pileup","date":"2026-10-17T10:00:00Z","title":"/restart no longer piles up old windows","text":"/restart, /update and auto-restart now replace the running client instead of starting a new one inside it, so a terminal keeps exactly one Quox client however many times you restart, and old ones no longer sit in memory.","try":"/restart","since":"0.3.0"}
53
+ {"id":"decision-labels","date":"2026-10-17T10:00:00Z","title":"Questions one at a time, in words","text":"When several questions are waiting, /approvals now walks you through them one at a time: what it is, what I would do in plain words, then just say yes, no or the option. /answer and /deny notes keep their full text, even with an unclosed quote or a line break.","try":"/approvals","since":"0.3.0"}
54
+ {"id":"bulk-yes-never-risky","date":"2026-10-17T09:00:00Z","title":"A bulk yes never covers a risky ask","text":"/yestoall and number ranges now answer only the low-risk questions; high-risk, test-changing and restart asks come one at a time with their full text, and a typed number is refused if the list moved.","try":"/yestoall","since":"0.3.0"}
55
+ {"id":"walkthrough-esc-summary","date":"2026-10-17T10:30:00Z","title":"Walkthrough: Esc exits with a summary","text":"Esc now leaves /walkthrough with a summary. It shows one card at a time, keeps every answer, and answered questions never come back.","try":"/walkthrough","since":"0.3.0"}
56
+ {"id":"walkthrough-yesno-only","date":"2026-10-17T14:30:00Z","title":"Walkthrough: yes/no only answers yes/no questions","text":"On a numbered question, typing yes or no no longer picks anything. The walkthrough shows the choices in words and never picks the asker's favourite for you.","try":"/walkthrough","since":"0.3.0"}
57
+ {"id":"custom-hats","date":"2026-10-17T16:00:00Z","title":"Make your own hats","text":"/hat new walks you through a hat of your own: name it, pick a starting hat and the agents it includes. Under it a turn carries only those agents. /hat edit and /hat delete change or remove one.","try":"/hat new","since":"0.3.0"}
58
+ {"id":"recommended-first","date":"2026-10-17T17:00:00Z","title":"Recommended option shown first","text":"On a decision card, the option the asker recommends is listed first in bold with a star and who recommends it. Keys do not change, and a card with no recommendation shows none.","try":"/walkthrough","since":"0.3.0"}
59
+ {"id":"pin-card-big","date":"2026-10-17T18:30:00Z","title":"A bigger PIN card","text":"The approval PIN card now shows six large slots that light up as you type, a shimmer while it checks, a green tick when authorised and a red flash on a wrong PIN. The full decision title is shown, wrapped.","try":"/authenticate","since":"0.3.0"}
60
+ {"id":"tui-render-fixes","date":"2026-10-17T18:30:00Z","title":"Tables, /help and /slots fit your terminal","text":"Chat tables wrap long cells instead of cutting them with an ellipsis. /help keeps usage and description apart. /slots shows held slots first and folds free ones into +N free (/slots all lists them). Tall cards and the dragon no longer leave garbled frames in a short terminal.","try":"/slots","since":"0.3.0"}
61
+ {"id":"status-rows","date":"2026-10-17T19:00:00Z","title":"Make your own status rows","text":"/row new walks you through a live line above the status bar and shows what it will say before saving: approvals waiting, hosts up, a plugin status or your own script. quox rows does the same from a shell, and a row can now carry a label, a tone colour, a value and a trend.","try":"/row new","since":"0.3.0"}
62
+ {"id":"cli-errors-plain","date":"2026-10-17T19:05:00Z","title":"Plainer error messages","text":"Errors now lead with what happened and why, then one next step, instead of \"Request failed with status 404\". Approving something that was already approved says who approved it and when, and exits 0. Add --verbose to see the HTTP status and server code.","shell":"quox inbox respond <id> --action approve","since":"0.3.0"}
63
+ {"id":"delegation-progress","date":"2026-10-17T20:30:00Z","title":"See what a specialist is doing while you wait","text":"When Quox hands a job to a specialist, the waiting line now names each real step it takes, for example Delegating HYPERION · proxmox_read (vm), instead of sitting silent. Tool footers fold repeats (proxmox_read ×3).","shell":"quox chat \"which VMs use the most memory\"","since":"0.3.0"}
64
+ {"id":"ux-interaction-fixes","date":"2026-10-17T20:00:00Z","title":"Simpler cards and choosers","text":"Approval cards say each thing once and wrap cleanly. /hat, /org and /mode open the same arrow-key chooser as /model. A typo offers a Run /command? card. /sleep no longer swallows a slash command, and an offer you move on from closes itself.","try":"/hat","since":"0.3.0"}
65
+ {"id":"ask-single-key","date":"2026-10-17T21:00:00Z","title":"y, n or c answer a question straight away","text":"y, n or c now answer an open question straight away, and a stray letter no longer sticks to your next command. Pressing Enter right after is ignored, and sending the same queued message twice says it is already queued.","since":"0.3.0"}
66
+ {"id":"tidy-footer","date":"2026-10-17T20:30:00Z","title":"A tidier bottom bar","text":"The status under the input box is now one line that wraps onto a second or third row only when needed, at the same left edge, with no big gaps. It says agent: none when nothing is attached, and the key hints sit on their own row.","try":"/help status","since":"0.3.0"}
67
+ {"id":"handoff-offer","date":"2026-10-17T21:30:00Z","title":"Attached agents offer to hand a question to CommanderQ","text":"When the agent you are attached to has no tools for a question (TRIAGE-Q asked about VM CPU), a card asks: Hand this question to CommanderQ? y asks CommanderQ that one question and you stay attached; n does nothing. quox --print names the same one-turn command.","try":"/attach triage-q","since":"0.3.0"}
68
+ {"id":"divider-aocl","date":"2026-10-17T21:00:00Z","title":"Watch the governance layers light up","text":"After you send, the line under your message shows one mark per AOCL governance layer that lights up as the server reports it, then the model name shimmers while it thinks and turns green when the reply is done, or red with the blocking layer marked.","try":"/aocl","since":"0.3.0"}
69
+ {"id":"approvals-card-first","date":"2026-10-17T22:00:00Z","title":"Approvals open on the card","text":"A plain /approvals now opens straight on the one-at-a-time card. An answer with a note asks you to confirm it first, and a question that has expired says so and moves on.","try":"/approvals","since":"0.3.1"}
70
+ {"id":"welcome-cmd","date":"2026-10-17T21:45:00Z","title":"Replay the welcome screen","text":"/welcome shows the welcome screen again on demand, with the Q, the notice and the dragon greeting on a tall terminal. It changes no setup or config. quox welcome prints the same notice from the shell.","try":"/welcome","since":"0.3.0"}
71
+ {"id":"restart-countdown","date":"2026-10-17T22:30:00Z","title":"Auto-restart counts down and Esc cancels","text":"When /restart auto is on and a newer quox is installed, a line counts down 10 seconds before the restart. Esc stays on this version, Enter restarts now. It waits while a turn runs or you have a draft. QUOX_RESTART_COUNTDOWN sets the seconds (0 restarts at once).","try":"/restart auto","since":"0.3.1"}
72
+ {"id":"active-rows","date":"2026-10-17T22:45:00Z","title":"/active is one clean row per session","text":"Each session shows once, with a status glyph, a readable name, what it needs from you in plain words and its age. Long rows wrap instead of being cut off.","try":"/active","since":"0.3.0"}
73
+ {"id":"no-hint-row","date":"2026-10-17T22:30:00Z","title":"No more Enter send · /help row","text":"The permanent key-hint row under the status line is gone, so the status line ends the screen. While Quox is working you still see how to steer, queue or stop.","try":"/help","since":"0.3.0"}
74
+ {"id":"active-cli-rows","date":"2026-10-17T23:10:00Z","title":"quox active matches the window","text":"quox active now prints the same clean rows as /active, with the unread message count per session. --lines keeps the older layout.","shell":"quox active","since":"0.3.0"}
75
+ {"id":"composer-edit","date":"2026-10-17T22:30:00Z","title":"Edit your draft anywhere, like Claude Code","text":"Move into the middle of your draft and edit there: Ctrl+Left/Right or Alt+B/F jump by word, Ctrl+W or Alt+Backspace delete the word before the cursor, Alt+D the word after, Ctrl+U and Ctrl+K cut to the start or end of the line, and Up/Down move through wrapped lines. Shift with the arrows, Home or End selects text: typing replaces it, Backspace deletes it, and it is copied to your clipboard (OSC 52, works over SSH where the terminal allows it). Your terminal's own mouse selection and copy keep working as usual.","since":"0.3.1"}
76
+ {"id":"peek-id","date":"2026-10-17T22:30:00Z","title":"Look up any id with /peek","text":"/peek <id> works out whether an id is an envelope, inbox item, objective, workflow run, WARD receipt or agent and shows a short card; when nothing matches it says what it tried. Also quox peek <id>.","try":"/peek atlas","since":"0.3.0"}
77
+ {"id":"aside-q","date":"2026-10-17T22:31:00Z","title":"Ask a side question with /aside","text":"/aside <question> asks with no tools and outside the conversation, so it cannot change anything and your next message carries on as if it never happened. The answer shows in its own block marked read only. Also quox aside.","try":"/aside what does a WARD receipt prove?","since":"0.3.0"}
78
+ {"id":"profile-chooser","date":"2026-10-17T22:32:00Z","title":"Switch profile or org from one list","text":"/profile opens a list of your profiles and this login's orgs. Pick one with the arrows and Enter; it changes this window only, never the default your other windows use.","try":"/profile","since":"0.3.0"}
79
+ {"id":"at-mentions","date":"2026-10-17T22:33:00Z","title":"@ completes agents, hosts, tasks and receipts","text":"Type @ in a message to pick an agent, fleet host, task or a receipt from this session; Tab or Enter puts it in. The lists load once per session, so typing never waits on the server.","try":"/help @mention","since":"0.3.0"}
80
+ {"id":"guide-fixes-docker","date":"2026-10-19T06:15:00Z","title":"Getting started can install Docker for you","text":"On Linux, when Docker is missing or your user is not in the docker group, the setup guide offers to fix it (y/N, the exact command shown) and carries on without logging out. A failed install now says why in one line.","shell":"quox","since":"0.3.1","sha":"381c023a3"}
81
+ {"id": "roster-chat-started-runs", "date": "2026-10-19T07:20:00Z", "title": "Workflows you start from chat show as yours", "text": "When you ask Quox to run a workflow, the run is now recorded as started by you, so it appears in your roster and in the away digest instead of being hidden as background work. Scheduled and webhook runs still show as background.", "try": "/roster", "since": "0.3.1", "sha": "d684cdfff"}
82
+ {"id":"stale-signin-recover","date":"2026-10-19T05:30:00Z","title":"An open window picks up a new sign-in","text":"If you sign in again in another window or with quox login, this window now notices and carries on instead of saying it is signed out.","try":"/login","since":"0.3.1"}
83
+ {"id":"presence-auto-clear","date":"2026-10-19T05:30:00Z","title":"Away clears when you are back","text":"If you set /afk and forget /back, your first real action in the terminal (a message, any slash command, or a PIN approval) clears it and says how long you were away. Scripts, --print runs and agents never clear it.","try":"/afk","since":"0.3.1"}
84
+ {"id":"polish5-quiet","date":"2026-10-18T09:00:00Z","title":"Quieter, shorter, truer","text":"Palette descriptions fit one row and /luc finds LUCID first. Waiting approvals collapse to a single row. Your own broadcast no longer echoes back, the org shows by name, and a finished walkthrough leaves no stray line.","try":"/approvals","since":"0.3.0"}
85
+ {"id": "ollama-private-address", "date": "2026-10-19T08:00:00Z", "title": "Connect your own Ollama on your network", "text": "/connect ollama http://<lan-ip>:11434 now asks \"It's on your private network. Connect to it?\" y/N. Say yes (org admins) and Quox checks it answers as Ollama, then allows only that address. No server-wide switch needed.", "try": "/connect ollama", "since": "0.3.1"}
86
+ {"id":"restart-settle","date":"2026-10-19T09:00:00Z","since":"0.3.1","title":"One restart for back-to-back builds","text":"When several new builds land minutes apart, the restart offer now waits until the installed build has stayed unchanged for 2 minutes, so you get one restart instead of two."}
87
+ {"id":"footer-tool-count","date":"2026-10-19T12:00:00Z","since":"0.3.1","title":"Turn footer counts tools properly","text":"The line under an answer now reads 1 tool or 3 tools. Some builds printed [object Object]1 tools after a turn that used a tool."}
88
+ {"id":"cancelled-reads-cancelled","date":"2026-10-19T14:00:00Z","since":"0.3.1","title":"Cancelled reads cancelled","text":"A stopped objective in the activity list now says cancelled (a neutral circle), not failed. Expired and rejected say their own word too, and an empty question no longer draws a stray rule above the composer."}
89
+ {"id":"one-rule-above-composer","date":"2026-10-19T15:00:00Z","since":"0.3.1","title":"One rule above the composer","text":"Under an approval card the composer no longer draws a blank line and a second rule: the card's closing rule is the only one."}