quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,59 @@
1
+ // Does a shell command start detached work? Then the approver's "it ran fine" only covers the launch. Pure: text in, boolean out.
2
+ // Quote-aware: '&' inside quotes, '&&', redirects (2>&1, >&2, &>f), '|&', a backslash-escaped '\&' and '# comments' are not background.
3
+
4
+ export const BACKGROUND_NOTE = 'starts background work: the result you see is only the launch, not the outcome';
5
+
6
+ const WRAPPERS = new Set(['sudo', 'env', 'command', 'exec', 'time', 'nice', 'ionice', 'doas', 'stdbuf', 'timeout']);
7
+ const DETACHERS = new Set(['nohup', 'setsid', 'disown', 'at', 'batch']);
8
+
9
+ /** One command's words (quotes removed) -> true when its command word detaches. */
10
+ function segmentDetaches(words) {
11
+ let i = 0;
12
+ while (i < words.length) {
13
+ const w = words[i];
14
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w)) { i++; continue; } // FOO=1 cmd
15
+ if (WRAPPERS.has(w)) { i++; while (i < words.length && /^-/.test(words[i])) i++; continue; }
16
+ break;
17
+ }
18
+ const cmd = (words[i] || '').replace(/^.*\//, '');
19
+ const rest = words.slice(i + 1);
20
+ if (DETACHERS.has(cmd)) return true;
21
+ if (cmd === 'systemd-run') return !rest.some(a => /^(--wait|--pipe|--pty|--scope|--shell|-P|-t|-S)$/.test(a)); // these wait for the unit; plain systemd-run returns at once
22
+ if (cmd === 'screen') return rest.some(a => /^-[A-Za-z]*d[A-Za-z]*m?[A-Za-z]*$/.test(a) && /^-[A-Za-z]+$/.test(a) && (/d/.test(a)) && (/m/.test(a) || rest.includes('-m'))); // screen -d -m / -dm / -dmS
23
+ if (cmd === 'tmux') return /^(new|new-session)$/.test(rest[0] || '') && rest.slice(1).some(a => /^-[A-Za-z]*d[A-Za-z]*$/.test(a));
24
+ if (cmd === 'docker') {
25
+ const sub = rest.filter(a => !/^-/.test(a));
26
+ if (sub[0] === 'run') return rest.some(a => a === '--detach' || /^-[A-Za-z]*d[A-Za-z]*$/.test(a));
27
+ if (sub[0] === 'compose' && rest.includes('up')) return rest.some(a => a === '--detach' || /^-[A-Za-z]*d[A-Za-z]*$/.test(a));
28
+ return false;
29
+ }
30
+ if (cmd === 'docker-compose') return rest[0] === 'up' && rest.some(a => a === '--detach' || /^-[A-Za-z]*d[A-Za-z]*$/.test(a));
31
+ if (cmd === 'systemctl') return rest.includes('--no-block') && rest.some(a => /^(start|restart|reload|try-restart|reload-or-restart|stop|isolate)$/.test(a));
32
+ return false;
33
+ }
34
+
35
+ export function startsBackgroundWork(text) {
36
+ const s = typeof text === 'string' ? text : '';
37
+ if (!s) return false;
38
+ let words = []; let cur = null; let q = null; let found = false;
39
+ const endWord = () => { if (cur !== null) { words.push(cur); cur = null; } };
40
+ const endSeg = () => { endWord(); if (words.length && segmentDetaches(words)) found = true; words = []; };
41
+ for (let i = 0; i < s.length && !found; i++) {
42
+ const ch = s[i];
43
+ if (q) { if (ch === q) q = null; else if (q === '"' && ch === '\\' && i + 1 < s.length) { cur += s[++i]; } else cur += ch; continue; }
44
+ if (ch === '\\') { cur = (cur ?? '') + (s[++i] ?? ''); continue; }
45
+ if (ch === "'" || ch === '"') { q = ch; cur = cur ?? ''; continue; }
46
+ if (ch === '#' && cur === null) { while (i < s.length && s[i] !== '\n') i++; endSeg(); continue; }
47
+ if (/\s/.test(ch)) { if (ch === '\n') endSeg(); else endWord(); continue; }
48
+ if (ch === '&') {
49
+ if (s[i + 1] === '&') { endSeg(); i++; continue; }
50
+ const prev = s[i - 1];
51
+ if (s[i + 1] === '>' || prev === '>' || prev === '<' || prev === '|') { cur = (cur ?? '') + ch; continue; } // &>f, 2>&1, <&3, |&
52
+ endSeg(); found = true; break; // a lone '&' detaches the command before it
53
+ }
54
+ if (ch === ';' || ch === '|' || ch === '(' || ch === ')' || ch === '{' || ch === '}') { endSeg(); continue; }
55
+ cur = (cur ?? '') + ch;
56
+ }
57
+ if (!found) endSeg();
58
+ return found;
59
+ }
@@ -187,7 +187,7 @@ export function runBang(plan, { cwd = process.cwd(), env = process.env, timeoutM
187
187
  /** The "exit N · 1.2s" footer, plus the plain-words reasons. */
188
188
  export function footer(r, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
189
189
  const secs = `${(r.ms / 1000).toFixed(1)}s`;
190
- if (r.timedOut) return `timed out after ${Math.round(timeoutMs / 1000)}s and was stopped · ${secs} (QUOX_BANG_TIMEOUT_MS changes the limit)`;
190
+ if (r.timedOut) return `timed out after ${Math.round(timeoutMs / 1000)}s · stopped; anything it already started may have completed. Run long commands in a terminal, or set QUOX_BANG_TIMEOUT_MS · ${secs}`;
191
191
  if (r.killed) return `stopped (Ctrl+C) · ${secs}`;
192
192
  if (r.signal) return `killed by ${r.signal} · ${secs}`;
193
193
  return `exit ${r.code} · ${secs}`;
@@ -45,6 +45,11 @@ export function offerFor(capabilityId, res) {
45
45
  }
46
46
  }
47
47
 
48
+ /** -> display names of every requires_setup capability, in server order. */
49
+ export function needsSetupNames(caps) {
50
+ return (Array.isArray(caps) ? caps : []).filter(c => c.availability === 'requires_setup').map(capabilityName);
51
+ }
52
+
48
53
  /** -> [[text, tone?]] for /capabilities. */
49
54
  export function capabilityLines(res) {
50
55
  if (!res?.ok || !Array.isArray(res.capabilities)) {
@@ -59,7 +64,7 @@ export function capabilityLines(res) {
59
64
  if (!installed.length) out.push([' nothing is ready yet']);
60
65
  const setup = of('requires_setup');
61
66
  if (setup.length) {
62
- out.push(['Needs setup:']);
67
+ out.push([`Needs setup (${setup.length}): ${needsSetupNames(caps).join(', ')}`]);
63
68
  for (const c of setup) out.push([`◐ ${capabilityName(c)} ${c.reason || 'needs setup'}${c.setup ? ` · ${c.setup}` : ''}`, 'warn']);
64
69
  }
65
70
  const unk = caps.filter(statusUnreadable);
@@ -70,7 +75,7 @@ export function capabilityLines(res) {
70
75
  const off = of('unavailable').filter(c => !statusUnreadable(c));
71
76
  if (off.length) {
72
77
  out.push(['Not available here:']);
73
- for (const c of off) out.push([`○ ${capabilityName(c)} ${c.reason || 'unavailable'}${c.setup ? ` · ${c.setup}` : ''}`]);
78
+ for (const c of off) out.push([`○ ${capabilityName(c)} ${c.reason === 'No signal to check' ? 'not checked: nothing here reports on it yet' : (c.reason || 'unavailable')}${c.setup ? ` · ${c.setup}` : ''}`]);
74
79
  }
75
80
  const planned = of('planned');
76
81
  if (planned.length) {
@@ -7,6 +7,8 @@
7
7
  export const SHORT_MAX = 54;
8
8
  export const SHORT = {
9
9
  walkthrough: 'a guided pass: what it does, why, my advice',
10
+ inbox: 'your Quox inbox: latest items, read, open',
11
+ mail: 'your triaged email, read only: latest, or N to read',
10
12
  approvals: 'the questions waiting for you, one at a time',
11
13
  peek: 'look up an envelope, item, run or receipt by id',
12
14
  aside: 'a read-only side question, kept out of the chat',
@@ -16,17 +18,22 @@ export const SHORT = {
16
18
  answer: 'answer a question a session asked you',
17
19
  authenticate: 'enter your PIN once, good for 10 minutes',
18
20
  override: 'override one blocked action (PIN, witnessed)',
21
+ 'verify-email': 'mark a user email verified (admin, PIN, audited)',
19
22
  skills: 'list your org\'s governed skills, or show one',
20
23
  skill: 'ask Quox to run a skill (approvals still apply)',
21
24
  rules: 'which rules win: QUOX.md, dev pack, server policy',
22
25
  clear: 'fresh conversation and a clear screen',
26
+ stop: 'stop the current turn, same as Esc',
27
+ devservers: 'your registered dev servers, and how to open one',
23
28
  resume: 'pick a recent conversation and carry on',
24
29
  compact: 'summarise the conversation, keeping decisions',
25
30
  challenge: 'a second opinion on the last answer (one extra turn)',
26
31
  spawn: 'ask HYPERION for a disposable VM (max 24h)',
27
32
  consent: 'AI data sharing: see where you stand, or agree',
33
+ contact: 'add a contact, or list them (as quox contacts)',
28
34
  help: 'this reference, or detail for one command',
29
- note: 'jot a note for the dev observer (also // text)',
35
+ note: 'save a note to NotesQ (/notes lists them)',
36
+ devnote: 'jot a note for the dev observer (also // text)',
30
37
  share: 'attach your last ! command\'s output to your message',
31
38
  copy: 'copy the last answer to the clipboard',
32
39
  dragon: 'the Q comes alive; explain, ask, sleep',
@@ -45,6 +52,8 @@ export const SHORT = {
45
52
  demo: 'play the simulated demo; nothing real runs',
46
53
  feedback: 'tell the Quox team something (you see it first)',
47
54
  gripe: 'file a GitHub issue; /gripe! files at once',
55
+ gripes: 'gripes waiting to be filed, and recent ones',
56
+ outbox: 'messages waiting for QuoxCORE to answer',
48
57
  sessions: 'everything live in your org, and where it runs',
49
58
  active: 'who is doing what, one line per worker',
50
59
  dashboard: 'open the web dashboard already signed in',
@@ -92,13 +101,20 @@ export const SHORT = {
92
101
  org: 'list your orgs, switch to one, or go back',
93
102
  r: 'reply to whoever last messaged you (also /reply)',
94
103
  poke: 'wake a session with an urgent page (also /wake)',
104
+ donotdisturb: 'sleep means sleep: collect quietly, read at morning',
105
+ morningreport: 'what came in while do not disturb was on',
106
+ callmeforapprovals: 'phone me when a high-risk approval sits unanswered',
95
107
  afk: 'tell sessions you are away; approvals still queue',
96
108
  back: 'mark yourself present and show what you missed',
97
109
  observe: 'post a note to the session watching your window',
98
- huh: 'the dragon explains what just happened',
99
- sleep: 'mark yourself sleeping; the dragon curls up',
110
+ huh: 'explain the last output in plain words',
111
+ sleep: 'say you are asleep; sessions stop waiting on you',
100
112
  mode: 'ask first (default) or plan only',
101
113
  slots: 'held slots: who holds them and where',
114
+ todo: 'add an Action Tracker item; /todo alone lists',
115
+ done: 'mark Action Tracker items done: /done td1,td2',
116
+ todos: 'your Action Tracker by column: td1 do1 dn1',
117
+ plugins: 'installed plugins (extensions) and their state',
102
118
  mirror: 'what CommanderQ sees about this org right now',
103
119
  health: 'dashboard summary: services, agents, approvals',
104
120
  status: 'profile, org, server health, model, agent, slot',
@@ -108,5 +124,8 @@ export const SHORT = {
108
124
  server: 'your self-hosted install: version, health, disk',
109
125
  config: 'settings: theme, motion, verbose, model',
110
126
  model: 'choose the provider and model for your next turns',
127
+ qpe: 'how many proof engine rows show',
111
128
  row: 'your own live lines above the status bar',
129
+ deck: 'QuoxDeck MIDI pads (the quoxdeck extension)',
130
+ ext: 'installed extensions: list, turn on or off',
112
131
  };
@@ -34,7 +34,7 @@ const ARGS = {
34
34
  fleet: [{ after: [], src: 'fleetHosts' }],
35
35
  replay: [{ after: [], src: 'conversations' }],
36
36
  org: [
37
- { after: [], choices: [
37
+ { after: [], src: 'orgs', choices: [
38
38
  { value: 'switch', desc: 'move this session to another org' },
39
39
  { value: 'back', desc: 'return to the org you were in before' },
40
40
  { value: 'list', desc: 'your orgs' }] },
@@ -60,9 +60,18 @@ const ARGS = {
60
60
  slot: [{ after: [], src: 'slots', choices: [{ value: 'log', desc: 'recent messages in the held slot' }] }],
61
61
  mode: [{ after: [], choices: [{ value: 'ask', desc: 'ask first (the default)' }, { value: 'plan', desc: 'plan only: investigates and proposes, changes nothing' }] }],
62
62
  say: [{ after: [], src: 'slots' }],
63
- broadcast: [{ after: [], choices: [{ value: '--slots', desc: 'only these slots, comma separated' }, { value: '--priority', desc: 'urgent shows red' }, { value: '--ack', desc: 'ask recipients to /ack' }] }],
63
+ broadcast: [{ after: [], choices: [{ value: '--slots', desc: 'only these slots, comma separated' }, { value: '--priority', desc: 'urgent shows red' }, { value: '--ack', desc: 'ask recipients to /ack' }, { value: '--active', desc: 'only sessions listening now; dormant are skipped' }, { value: '--dry-run', desc: 'with --active: list who would be reached, send nothing' }] }],
64
64
  send: [{ after: [], src: 'slots' }, { after: ['*'], src: 'files', of: 'partial' }],
65
- dragon: [{ after: [], choices: ['explain', 'ask', 'sleep', 'fly', 'hero', 'fire', 'classic'] }],
65
+ dragon: [
66
+ { after: [], choices: ['explain', 'ask', 'sleep', 'fly', 'hero', 'fire', 'classic', { value: 'companion', desc: 'the dragon beside an attached Lucid, Hyperion, Seer, Sentinel, Drago, Cipher, Vulcan or Quoxperticus' }] },
67
+ { after: ['companion'], choices: [{ value: 'on', desc: 'on attach and key moments, then leaves' }, { value: 'greeting', desc: 'only on attach, then leaves' }, { value: 'off', desc: 'never on its own (/dragon shows it)' }, { value: 'status', desc: 'what is set and whether the art loads' }] }
68
+ ],
69
+ ext: [
70
+ { after: [], choices: [{ value: 'list', desc: 'each listed extension and where it was found' }, { value: 'on', desc: 'load one now and at every start' }, { value: 'off', desc: 'unload one' }, { value: 'tab', desc: 'focus an extension panel (off collapses)' }, { value: 'set', desc: 'change one of its settings' }, { value: 'settings', desc: 'show its settings' }] },
71
+ { after: ['on'], src: 'ext.listed' }, { after: ['off'], src: 'ext.loaded' }, { after: ['tab'], src: 'ext.panels' },
72
+ { after: ['set'], src: 'ext.loaded' }, { after: ['settings'], src: 'ext.loaded' }, { after: ['set', '*'], src: 'ext.keys', of: 1 }
73
+ ],
74
+ inbox: [{ after: [], choices: [{ value: 'read', desc: 'read N | all: mark items read' }, { value: 'open', desc: 'open N: take an approval to /approvals' }, { value: 'latest', desc: 'the latest 10' }, { value: 'mute', desc: 'mute <from> [1h|8h|1d]: no arrival lines from that source (approvals never muted)' }, { value: 'unmute', desc: 'unmute <from>' }, { value: 'mutes', desc: 'muted sources and when they expire' }] }, { after: ['mute'], src: 'inbox.sources' }, { after: ['unmute'], src: 'inbox.mutes' }],
66
75
  tips: [{ after: [], choices: topicList().map(t => ({ value: t.topic, desc: `${t.count} tip${t.count === 1 ? "" : "s"}` })) }],
67
76
  whatsnew: [{ after: [], choices: [{ value: 'all', desc: 'every command and entry, regardless of seen' }, { value: 'on', desc: 'show the card at startup when something is new' }, { value: 'off', desc: 'stop the startup card' }] }],
68
77
  'tip-settings': [{ after: [], choices: [
@@ -93,6 +102,19 @@ const ARGS = {
93
102
  ]
94
103
  };
95
104
 
105
+ // Dev-only entries (dev: true) are vendor tooling: they live in lib/vendor/devCommands.js, which package.json
106
+ // "files" keeps out of the published quoxcli (same mechanism as the owner-only licence verbs, quox#1134).
107
+ // Present in the dev tree, absent for every npm user.
108
+ let DEV_ENTRIES = [];
109
+ // TUI-QPE 2026-10-20 (owner: "/qpe is extension right?"): owner tooling that is not dev-observer gated also lives in
110
+ // lib/vendor (VENDOR_ENTRIES), so the public build has no /qpe; customers have no proof engine.
111
+ let VENDOR_ENTRIES = [];
112
+ try {
113
+ const v = await import('../vendor/devCommands.js');
114
+ DEV_ENTRIES = v.DEV_ENTRIES || [];
115
+ VENDOR_ENTRIES = v.VENDOR_ENTRIES || [];
116
+ } catch (e) { if (e?.code !== 'ERR_MODULE_NOT_FOUND') throw e; }
117
+
96
118
  export const REGISTRY = [
97
119
  // Talk (keys)
98
120
  { id: 'type', kind: 'key', group: 'Talk', usage: 'just type', desc: 'ask Quox anything; CommanderQ routes it to the right agent', status: 'active' },
@@ -109,7 +131,10 @@ export const REGISTRY = [
109
131
  { id: 'paste', kind: 'key', group: 'Talk', usage: 'paste', desc: 'long pastes collapse to [Pasted text #N +L lines] and expand on send', status: 'active' },
110
132
  { id: 'ctrlc', kind: 'key', group: 'Talk', usage: 'Ctrl+C', desc: 'clear the draft; during a turn interrupt; twice when idle quits', status: 'active' },
111
133
  // Approvals
112
- { id: 'approvals', kind: 'command', group: 'Approvals', usage: '/approvals [mine|agents|old|<agent>|N]', desc: 'the questions waiting for you, one at a time', server: true, cap: 'approvals', status: 'active' },
134
+ { id: 'approvals', kind: 'command', group: 'Approvals', usage: '/approvals [mine|agents|old|<agent>|N|auto on|off]', desc: 'the questions waiting for you, one at a time', server: true, cap: 'approvals', status: 'active',
135
+ detail: 'New approvals and questions open by themselves as the one-at-a-time card (after your draft, typing pause or turn; never mid-typing, never answered for you), including ones in your other orgs, which are answered there without switching. /approvals auto off keeps them as the ⏸ line; the setting is saved on this profile.' },
136
+ { id: 'inbox', kind: 'command', new: true, group: 'Approvals', usage: '/inbox [N | read N|N-M|all | open N | mute <from> [1h|8h|1d] | unmute <from> | mutes]', desc: 'your Quox inbox: the latest 10 as a table, N shows one in full, read marks read, open takes an approval to /approvals; mute silences a source\'s arrival lines (default 8h, approvals never muted), repeats of the same title collapse to (×N)', needs: ['inboxItems'], cap: 'approvals', status: 'active', args: ARGS.inbox },
137
+ { id: 'mail', kind: 'command', new: true, group: 'Approvals', usage: '/mail [N | focus|fyi|auto|screened]', desc: 'your triaged email (InboxQ), read only: the latest 10, N reads one with its triage reason', needs: ['mailMessages'], status: 'active' },
113
138
  { id: 'walkthrough', kind: 'command', new: true, group: 'Approvals', usage: '/walkthrough', desc: 'walk through what is waiting for you one question at a time: what it does, why, my advice, then your choice (same as /approvals walkthrough)', server: true, cap: 'approvals', status: 'active' },
114
139
  { id: 'yestoall', kind: 'command', new: true, group: 'Approvals', usage: '/yestoall', desc: 'approve everything waiting for you (same as /approve all; privileged ones still ask)', server: true, cap: 'approvals', status: 'active' },
115
140
  { id: 'approve', kind: 'command', group: 'Approvals', usage: '/approve N | 1 y 2 n | agent <name>', desc: 'approve item N from the list, or everything one agent asked', server: true, cap: 'approvals', status: 'active',
@@ -123,16 +148,30 @@ export const REGISTRY = [
123
148
  detail: 'Like /approve N, but asks for your step-up PIN first when there is no live human-present window. The digits are never shown, saved, or sent anywhere except the PIN check.' },
124
149
  { id: 'override', kind: 'command', group: 'Approvals', usage: '/override <block id|last> <reason>', desc: 'override ONE blocked action (PIN, recorded and witnessed)', server: true, cap: 'approvals', status: 'active',
125
150
  detail: 'When a policy block refuses an action (safety level, budget cap, AOCL or WARD deny), a person with a fresh PIN can override that one action. The reason, your name and the block are recorded and witnessed. Today only safety blocks (E-STOP, DEFCON RED dispatch refusals) are honoured by a gate; other block types are recorded but stay blocked, and the answer says so. "last" is the most recent block this window saw.' },
151
+ { id: 'verify-email', kind: 'command', group: 'Approvals', usage: '/verify-email <email> --reason "<why>"', desc: 'mark a user email verified (instance admin; preview, then your PIN; audited)', server: true, cap: 'approvals', status: 'active',
152
+ detail: 'The TUI front of quox admin users verify-email: the same endpoint, and the server stays the only gate (instance admin, a human caller, a one-shot step-up token, a reason). --reason is required and is recorded in the audit log; --user-id <id> refuses unless the address belongs to that user. You see what will happen and answer y/N, then the private PIN card opens. It always asks for the PIN afresh: a live /authenticate window is not reused or spent. The PIN is never an argument, never shown, saved or logged.' },
126
153
  // Skills and rules
127
154
  { id: 'skills', kind: 'command', group: 'Skills', usage: '/skills [name]', desc: 'list your org\'s governed skills, or show one as a card', needs: ['skills'], cap: 'skills', status: 'active',
128
155
  detail: 'Lists your org skills (reusable procedures an agent follows; not Crystals, which are deterministic replays) with edition and one-line description. /skills <name> shows its card: purpose, version and review state, tests (results only when run-tests recorded them), the agents it is for, where it came from, recorded use, and how to run it.' },
129
156
  { id: 'skill', kind: 'command', group: 'Skills', usage: '/skill <name> [args]', desc: 'ask Quox to run a skill (governed: approvals still apply)', server: true, cap: 'skills', status: 'active',
130
157
  detail: 'Sends "Run the skill <name> with: <args>" as a normal turn, so CommanderQ runs it through the usual governed path. Nothing is executed from here, and approval cards work as always.' },
158
+ { id: 'callmeforapprovals', kind: 'command', new: true, group: 'Approvals', usage: '/callmeforapprovals [on|off|status]', desc: 'phone me when a high-risk approval sits unanswered: status, on or off (human org admin only)', needs: ['callMe'], status: 'active', aliases: ['callme'],
159
+ detail: 'The same call as quox approvals call-me. Alone it shows whether the phone-me switch is on, how long an approval waits before the call, quiet hours, the minimum risk tier, and whether the watcher on the server is actually alive (or says it cannot tell). on or off flips only the switch (add --after <minutes> or --quiet 22:00-08:00 to tune it); every other setting is kept. The call only reads you one approval and asks: approve needs a keypad PIN, nothing is ever approved for you. Org admins only; an agent login is refused.' },
160
+ { id: 'plugins', kind: 'command', group: 'Skills', usage: '/plugins [name]', desc: 'plugins (extensions, add-ons) your org has installed: enabled state and version; /plugins <name> shows one', needs: ['pluginList'], status: 'active', aliases: ['plugin', 'extensions', 'extension', 'addons', 'apps'],
161
+ detail: 'Read-only. Lists the plugins installed for the active org (the same call as quox plugin list) with enabled or disabled and the version. /plugins <name> shows one plugin (as quox plugin get). Also answers to /extensions, /extension, /addons and /apps; "/extension list" works too.' },
162
+ { id: 'todo', kind: 'command', group: 'Awareness', usage: '/todo <text>', desc: 'add an Action Tracker item; /todo move do2 done moves one; /todo alone lists', needs: ['todoAdd', 'todoList', 'todoMove'], status: 'active',
163
+ detail: 'Moves: /todo move do5 done, /todo do5 done, /todo mark do2 done, /todo td3 doing (td = to do, do = doing, dn = done; the numbers are the ones /todos last showed, and if the list changed since, it says so and still moves the item you saw). Adds one item to your Action Tracker (status todo) as you, the same as quox actions add "<text>" or adding it in the dashboard, then confirms with the item id. No model call and no approval: you typing it is the authorization. /todo with no text lists your open items, the same as /todos. Mark one done with quox actions done <id>.' },
164
+ { id: 'done', kind: 'command', group: 'Awareness', usage: '/done <ids>', desc: 'mark Action Tracker items done: /done td1,td2', needs: ['todoList', 'todoMove'], status: 'active',
165
+ detail: 'Marks Action Tracker items done by the short ids /todos last showed: /done td1,td2 or /done td1 td2. Each one is confirmed by title. Same as /todo move td1,td2 done. No model call and no approval: you typing it is the authorization.' },
166
+ { id: 'todos', kind: 'command', group: 'Awareness', usage: '/todos', desc: 'your Action Tracker by column, numbered td1 do1 dn1 for /todo move', needs: ['todoList'], status: 'active',
167
+ detail: 'Read-only. Lists your whole Action Tracker (every page) by column: To do td1.., Doing do1.., Done dn1.. (the 10 newest; the rest via quox actions list --status done), newest first. Use the numbers with /todo move do2 done. /todo <text> adds one.' },
131
168
  { id: 'rules', kind: 'command', group: 'Skills', usage: '/rules [files|off|on|N]', desc: 'which rules win: advisory QUOX.md, the dev pack, enforced server policy', status: 'active',
132
169
  detail: '/rules shows the precedence: QUOX.md and the dev pack only guide; armed governance policies on the server are enforced and win. /rules files shows the QUOX.md text. QUOX.md in this directory and parents up to the git root, plus ~/.quox/QUOX.md, are sent with each turn as your rules. World-writable files and symlinks leaving your home or repo are ignored. /rules N shows page N.' },
133
170
  // Conversation
134
171
  { id: 'clear', kind: 'command', group: 'Conversation', usage: '/clear', desc: 'fresh conversation and a clear screen; rules and memory kept, /resume goes back', status: 'active', aliases: ['new', 'reset'],
135
172
  detail: 'Clears the screen and starts a new conversation. Your QUOX.md rules, attached agent, model choice and Quox memory stay as they are. The previous conversation is kept on the server: /resume lists it.' },
173
+ { id: 'stop', kind: 'command', group: 'Conversation', usage: '/stop', desc: 'stop the current turn, same as Esc', status: 'active', aliases: ['cancel'],
174
+ detail: 'Stops the turn that is running, exactly like Esc: the stream is cancelled, the working animation ends and the status line goes back to normal. /cancel does the same. With nothing running it says so.' },
136
175
  { id: 'resume', kind: 'command', group: 'Conversation', usage: '/resume [id|name]', desc: 'pick a recent conversation and carry on where it left off', status: 'active', needs: ['conversations', 'conversationTurns'],
137
176
  detail: 'Alone: a picker of your recent conversations (title, agent, age); Up/Down and Enter resume, Esc cancels. With an id (or the start of one) or a /rename name it resumes directly. The last turns are shown as history and your next message continues that conversation. Also at launch: quox --resume [id|name] or quox --conversation <id>.' },
138
177
  { id: 'compact', kind: 'command', group: 'Conversation', usage: '/compact [show]', desc: 'summarise the conversation on the server, keeping decisions', status: 'active', needs: ['compactConversation'],
@@ -149,15 +188,19 @@ export const REGISTRY = [
149
188
  'suggested too (providers, live model ids, orgs, agents, slots, options): Up and Down' +
150
189
  ' choose, Tab completes, Esc closes, and keep typing to narrow. Suggestions that ' +
151
190
  'cannot be fetched are simply left out.' },
152
- { id: 'note', kind: 'command', group: 'Conversation', usage: '/note <text>', desc: 'jot an observation for the dev observer (also: // text); not sent to Quox', status: 'active', dev: true,
153
- detail: 'Records your text in the dev transcript only: not sent to the model or the server, no turn, not in chat history. Needs dev transcripts on (touch ~/.config/quox/dev-transcripts). Up to 2000 characters.' },
191
+ { id: 'note', kind: 'command', group: 'Conversation', usage: '/note <text>', desc: 'save a NotesQ note (first line is the title); not sent to Quox. /notes lists them', status: 'active',
192
+ detail: 'Creates a NotesQ note through the same backend as quox notes new: the first line (or first 60 characters) becomes the title and your text is the body. It confirms with the title and id. If NotesQ is not licensed or enabled for your org it says so and how to enable it. /notes lists recent notes (quox notes ls).' },
193
+ { id: 'notes', kind: 'command', group: 'Conversation', usage: '/notes', desc: 'list your recent NotesQ notes (same as quox notes ls)', status: 'active',
194
+ detail: 'Runs quox notes list in this window. /note <text> saves a new one.' },
195
+ { id: 'contact', kind: 'command', group: 'Conversation', usage: '/contact new <name> [--phone N] [--email E] [--relationship R]', desc: 'add a contact (same as quox contacts add); /contact list pages them', status: 'active',
196
+ detail: 'Builds a quox contacts add command from what you type: --mobile, --tel and --number become --phone, a multi-word name is quoted for you, and a flag the command does not have is refused with the list it does take. It goes through the same approval rules as the CLI.' },
154
197
  { id: 'bang', kind: 'key', group: 'Talk', usage: '!<command>', desc: 'run a shell command here without leaving (!quox <args> runs this quox as you); output stays on your screen', status: 'active',
155
198
  detail: 'A line starting with ! runs on this machine in the current directory via your shell and prints inline: "$ <command>", the output, then "exit N". It is never sent to Quox, starts no turn and needs no approval, because it is your own command. !quox <args> (or !q <args>) runs THIS quox binary with your current profile, so !quox inbox list works as you; bare quox and tui need the terminal and are refused. Ctrl+C stops it (SIGTERM, then SIGKILL after 2 s) without quitting; one command at a time; stdin is closed, so a command that waits for input gets end-of-file; 120 s limit (QUOX_BANG_TIMEOUT_MS); output is capped at 2000 lines / 256 KB. !! is reserved. Only the command line and exit code go in the dev transcript, never the output.' },
156
199
  { id: 'share', kind: 'command', group: 'Talk', usage: '/share [N]', desc: 'attach the output of your last ! command (or the Nth-last) to your next message', status: 'active',
157
200
  detail: 'Command output is not shared with Quox by default, because it can contain secrets. /share attaches the latest ! result to your next message (up to 16 KB, and it says when it was cut); /share 2 picks the one before. A line "shared with Quox: output of `<command>`" appears when it is sent.' },
158
201
  { id: 'copy', kind: 'command', group: 'Conversation', usage: '/copy [N|code]', desc: 'copy the last answer (raw markdown) to the clipboard; N = Nth-last, code = last code block', status: 'active',
159
202
  detail: 'Uses the OSC 52 terminal escape (works over SSH in most terminals; wrapped for tmux). Needs a terminal that allows OSC 52. Mouse selection is untouched: select and copy with your terminal as usual.' },
160
- { id: 'dragon', kind: 'command', group: 'Conversation', usage: '/dragon [explain|ask [question]|sleep|fly|hero|fire|classic]', desc: 'the Q comes alive; /dragon explain gives the simplest possible explanation of the last output, /dragon ask [question] answers one question just as simply, /dragon sleep is a katakana screensaver (any key wakes)', status: 'active', args: ARGS.dragon,
203
+ { id: 'dragon', kind: 'command', group: 'Conversation', usage: '/dragon [explain|ask [question]|sleep|fly|hero|fire|classic|companion on|greeting|off|status]', desc: 'the Q comes alive; /dragon explain gives the simplest possible explanation of the last output, /dragon ask [question] answers one question just as simply, /dragon sleep is a katakana screensaver (any key wakes)', status: 'active', args: ARGS.dragon,
161
204
  detail: 'Plain /dragon plays the dragon (googly eyes, a talking smile, tail sway and whip, then fire from its mouth); classic and hero are the same show, /dragon fire is the breath alone. explain and ask are ordinary turns on whichever AI you have selected (tools and approvals still apply); the answer stays in the conversation as "Dragon explains" or "Dragon answers". With no AI connected you get the usual connect card. /dragon ask with no question waits for your next line. /dragon sleep hides the screen under katakana with the dragon asleep; any key wakes it and is not typed, an approval wakes it, Ctrl+C quits. It only changes what is drawn, never the conversation. Quiet motion or NO_COLOR shows a still screen.' },
162
205
  { id: 'dragon-fly', kind: 'command', group: 'Conversation', usage: '/dragon-fly', desc: 'the dragon flies across the screen, wings beating (same as /dragon fly)', status: 'active',
163
206
  detail: 'One 9 second pass of the dragon in flight, wings beating; Esc stops it early. It only changes what is drawn, never the conversation. Quiet motion or NO_COLOR shows a single still frame instead.' },
@@ -187,6 +230,10 @@ export const REGISTRY = [
187
230
  detail: '/feedback <text> shows exactly what will be sent (type feedback, your text, and the quox CLI version; no name, email, org or token) to https://api.quox.ai/api/feedback, the Quox team rather than your own Quox server, and asks y/N. y sends it; anything else leaves it unsent. The Quox site takes 5 a hour per address. /gripe is different: it files a GitHub issue through your org.' },
188
231
  { id: 'gripe', kind: 'command', group: 'Conversation', usage: '/gripe <text>', desc: 'file a GitHub issue through the GRIPE agent; shows it first, /gripe! files at once', needs: ['gripe'], status: 'active', aliases: ['gripe!'],
189
232
  detail: 'The same governed path as `quox gripe`: the GRIPE agent files the issue through your org\'s GitHub connector (default repo quoxai/quox). /gripe <text> shows what would be filed and asks; /gripe! (alone, within 2 minutes) files it, or /gripe! <text> files straight away. The issue link is shown when one comes back; with no link it says so and treats it as not filed.' },
233
+ { id: 'gripes', kind: 'command', group: 'Conversation', usage: '/gripes', desc: 'gripes waiting to be filed and recently filed, with issue links', status: 'active',
234
+ detail: 'When Quox does not answer (timeout, network, server error) /gripe! saves the gripe on this machine, in ~/.quox/gripe-queue.json, and files it when Quox answers again: on start, then at most every 2 minutes, giving up after 24 hours with a visible line. Each gripe carries a key so a filing that actually succeeded before the timeout is never filed twice. /gripes lists what is waiting and what was filed, with the issue links.' },
235
+ { id: 'outbox', kind: 'command', group: 'Conversation', usage: '/outbox [drop N|send]', desc: 'messages saved while QuoxCORE did not answer; sent when it does', needs: ['slotSay'], status: 'active',
236
+ detail: 'When a /say or @slot message cannot be sent because QuoxCORE is not answering (connection failed, refused, timeout, server error), it is kept on this machine in ~/.quox/slot-outbox.json (mode 0600, up to 50) and sent in order when QuoxCORE answers again, with a line per message. A refusal (permission, no such slot) is never queued: the error shows and your text is back in the prompt. /outbox lists them, /outbox drop N removes one, /outbox send retries now. A message whose window closed mid-send is marked as possibly delivered and is only retried by /outbox send, so nothing is sent twice by surprise.' },
190
237
  { id: 'sessions', kind: 'command', group: 'Conversation', usage: '/sessions [all|refs]', desc: 'everything live in your org by name and where it runs; refs adds ids, all adds every chat', needs: ['sessionsIndex'], status: 'active',
191
238
  detail: 'Each row leads with the session name and where it runs (host, or "host unknown" when the server does not know), then how long it has been up and when it was last active. Sections: terminal sessions, slots, work (objectives, loops, workflows) and chats. /sessions refs adds ids and attach commands under each row; /sessions all includes every chat from the last 24 h. The same data as `quox sessions`. Names set with /rename are kept on this machine (~/.config/quox/sessions.json).' },
192
239
  { id: 'active', kind: 'command', new: true, group: 'Conversation', usage: '/active [sessions|slots|agents]', desc: 'who is doing what: one line per active worker, grouped working / waiting on you / idle (same as `quox active`)', needs: ['sessionsIndex'], status: 'active',
@@ -203,8 +250,8 @@ export const REGISTRY = [
203
250
  detail: 'Subscribes this window to the slot\'s live stream; the last ~10 messages show first. Holding (/slot) stays separate and exclusive. Post with #<slot> <text> or /say; #<slot> alone makes it the default target (a bare # undoes it). /<slot> join, /<slot> leave and /join #<slot> work too. Your lines are signed with this session\'s name and person, never as a human profile.' },
204
251
  { id: 'leave', kind: 'command', group: 'Conversation', usage: '/leave <slot>', desc: 'leave a slot room you joined (does not release a hold)', needs: ['slotSay'], cap: 'slots', status: 'active', args: ARGS.say },
205
252
  { id: 'rooms', kind: 'command', new: true, group: 'Conversation', usage: '/rooms', desc: 'rooms (groups) this window has joined, with unread counts; also on the status line', needs: ['slotSay'], cap: 'slots', status: 'active', aliases: ['groups'] },
206
- { id: 'broadcast', kind: 'command', new: true, group: 'Conversation', usage: '/broadcast [--slots a,b] [--priority urgent] [--ack] <text>', desc: 'tell every held slot in this org at once, signed as you', needs: ['slotBroadcast'], cap: 'slots', status: 'active', aliases: ['roar'], args: ARGS.broadcast,
207
- detail: 'One server call delivers to every slot held in this org (never another org), or only --slots a,b. --priority urgent marks it red for recipients. --ack asks them to reply /ack <id>; you then see "Broadcast delivery: delivered N / acknowledged M / awaiting K" update. You get a framed card: how many sessions have it (stored in the slot, listening sessions see it at once) and a separate chat-rooms count (sent / sending / failed). /broadcast status <id> redraws it. Typing it is your consent. Not yet: --at (schedule), --fleet, --org, --voice.' },
253
+ { id: 'broadcast', kind: 'command', new: true, group: 'Conversation', usage: '/broadcast [--slots a,b] [--priority urgent] [--ack] [--active [--dry-run]] <text>', desc: 'tell every held slot in this org at once, signed as you', needs: ['slotBroadcast'], cap: 'slots', status: 'active', aliases: ['roar', 'broadcast-active'], args: ARGS.broadcast,
254
+ detail: 'One server call delivers to every slot held in this org (never another org), or only --slots a,b. --priority urgent marks it red for recipients. --ack asks them to reply /ack <id>; you then see "Broadcast delivery: delivered N / acknowledged M / awaiting K" update. You get a framed card: how many sessions have it (stored in the slot, listening sessions see it at once) and a separate chat-rooms count (sent / sending / failed). /broadcast status <id> redraws it. Typing it is your consent. --active (or /broadcast-active) reaches only sessions listening right now (a live slot wait); dormant slots are skipped and not woken, and the card says how many. --dry-run (with --active) lists who would be reached and who is skipped, and sends nothing. Not yet: --at (schedule), --fleet, --org, --voice.' },
208
255
  { id: 'page', kind: 'command', new: true, group: 'Conversation', usage: '/page <slot> <text>', desc: 'urgent message to one slot, acknowledgement requested', needs: ['slotBroadcast'], cap: 'slots', status: 'active', args: ARGS.say,
209
256
  detail: 'Like /broadcast to a single slot with --priority urgent --ack: it shows red there, asks them to /ack, and you see when they have.' },
210
257
  { id: 'ack', kind: 'command', new: true, group: 'Conversation', usage: '/ack <broadcast id>', desc: 'acknowledge a broadcast, as the slot this window holds', needs: ['slotAck'], cap: 'slots', status: 'active',
@@ -234,6 +281,8 @@ export const REGISTRY = [
234
281
  detail: 'off (default) keeps the screen to your conversation, the tool result lines and ONE AOCL footer per turn (layers, tool count, time). on shows the compact AEE / VOLT / AOCL / WARD protocol lines as they happen. full adds the thinking stream. Saved to your profile. The full list of tools offered to the model for the last turn is in /aocl.' },
235
282
  { id: 'proof', kind: 'command', group: 'Verbose', usage: '/proof [envelope]', desc: 'the evidence chain for the last tool call: AEE, AOCL, tool, WARD', server: true, cap: 'receipts', status: 'active',
236
283
  detail: 'Each link is read back from the server and drawn confirmed only when the read says so. WARD is the server-verified inclusion receipt; it can be slow on a large chain and is bounded, so a link may stay open.' },
284
+ { id: 'evidence', kind: 'command', new: true, group: 'Verbose', usage: '/evidence', desc: 'signed evidence bundle for this conversation', server: true, cap: 'receipts', status: 'active', aliases: ['envelope', 'envelopes', 'aee', 'volt', 'ward'],
285
+ detail: 'The same export as `quox evidence bundle`: a zip with envelopes.json/csv, ward-receipts.json, volt.json, a manifest of file hashes and an Ed25519 signature, scoped to your org and this conversation. It prints the path, the envelope count, the receipt count and the command to check it offline (`quox evidence verify <file>`). /ward does the same and lists the WARD receipts; /envelope, /envelopes and /aee show the envelope ids (same as /proof); /volt points at the VOLT run inside the bundle.' },
237
286
  { id: 'prove', kind: 'command', new: true, group: 'Verbose', usage: '/prove', desc: 'independently check that the last turn\'s tool calls really did what they said: receipt chain plus a read-back of the effect', server: true, cap: 'receipts', status: 'active',
238
287
  detail: 'For each tool call of the last turn: the receipt chain is re-read from the server (as in /proof), and where the tool is a write we know how to read back (today: slot_create, memory_save) the effect is read from the system of record. ✔ proven = the chain agrees and the effect is there (or it was a read and the chain agrees). ⚠ unproven = it cannot be shown from here (no read mapping for that write, a read failed, no receipt). ✖ contradicts = the call failed, the server recorded a failure, or the effect is not there. Nothing is assumed: an unmapped write is never called proven. /proof is unchanged and shows one call\'s chain.' },
239
288
  { id: 'replay', kind: 'command', new: true, group: 'Verbose', usage: '/replay [run or conversation id] [page]', desc: 'rebuild an earlier run from the audit trail: decisions, tools with args, outputs and receipts, in order, paged', server: true, cap: 'receipts', status: 'active', args: ARGS.replay,
@@ -244,6 +293,8 @@ export const REGISTRY = [
244
293
  // Awareness
245
294
  { id: 'roster', kind: 'command', group: 'Awareness', usage: '/roster [all]', desc: 'show or hide the live roster under the status line; all includes background work', needs: ['roster'], status: 'active',
246
295
  detail: 'The roster lists what needs you: pending approvals, then work you started (objectives, loops, manual workflows). Scheduled and event-driven workflows are hidden; background failures show once per workflow in the bar below. /roster all shows everything. Refreshed every 15 s from the server. Down on an empty composer focuses it; Enter peeks, s asks to stop (y confirms), Esc returns. A source the server cannot answer shows nothing and is named here.' },
296
+ { id: 'loop', kind: 'command', group: 'Awareness', usage: '/loop [list|watch <id>|peek <id>|focus]', desc: 'follow a loop live in a card that updates in place', server: true, status: 'active',
297
+ detail: 'A loop you start from chat gets a live card: the turn strip (✔ ok, ⚠ waiting or denied, ✖ error, ◐ running), the receipts counter, host and scope, and the last lines of what it is writing. Milestones and errors land as normal chat lines beneath it, and a finished loop prints one line with the proof command. /loop list shows loops, /loop watch <id> follows one that is already running (and gives it the keys), /loop peek <id> prints one snapshot, /loop focus gives the keys back. Keys: Enter expands the output, s asks to halt (y confirms, never immediate), Esc collapses and leaves. Under 60 columns the card is one line. Needs a server with the loop event routes; one without them says so.' },
247
298
  { id: 'workflow', kind: 'command', group: 'Skills', usage: '/workflow <name|id|run>', desc: 'the step flow of a workflow\'s latest run (or one run): state, timings, human gates', server: true, cap: 'workflows', status: 'active',
248
299
  detail: 'Steps come from the workflow definition and each state and duration from that run\'s recorded envelopes. A reached human gate shows amber. Enter on a workflow row in the roster shows the same flow.' },
249
300
  { id: 'crystal', kind: 'command', group: 'Skills', usage: '/crystal <workflow>', desc: 'is this workflow eligible to become a Crystal? checked against the real gate', server: true, cap: 'crystals', status: 'active',
@@ -287,12 +338,15 @@ export const REGISTRY = [
287
338
  { id: 'poke', kind: 'command', new: true, group: 'Conversation', usage: '/poke <slot or session> [text]', desc: 'wake a session: an urgent page asking it to acknowledge (also /wake, /nudge, /ping)', needs: ['slotBroadcast'], cap: 'slots', status: 'active', aliases: ['wake', 'nudge', 'ping', 'slap', 'awaken'] },
288
339
  { id: 'afk', kind: 'command', new: true, group: 'Context', usage: '/afk [note]', desc: 'tell sessions you are away (they stop waiting on approvals, which still queue); /back then shows everything since, grouped', status: 'active',
289
340
  detail: 'Sets your presence on the server (per user and org): sessions saying into hitl, triage-q or this window\'s slot see "owner away since 15:54 UTC (note): approvals will queue"; `quox presence`, `quox slot list`, quox ask and agents see it too. Nothing is approved while you are away. /back or any typed message marks you present.' },
341
+ { id: 'donotdisturb', kind: 'command', new: true, group: 'Context', usage: '/donotdisturb [on|off] [until HH:MM]', desc: 'sleep means sleep: collect arrivals quietly (no notices, cards, bells) until you ask for /morningreport; also /dnd', status: 'active', aliases: ['dnd'],
342
+ detail: 'On sets your presence to sleeping with the note "do not disturb" (sessions see it) and the window only counts what arrives: "🌙 do not disturb · N waiting · /morningreport". Bare /dnd toggles. /afk and /sleep imply it. Saying you are going to sleep (chat or /broadcast) offers it, y/N. A restarted window restores it from the server. /back or /dnd off ends it.' },
343
+ { id: 'morningreport', kind: 'command', new: true, group: 'Context', usage: '/morningreport', desc: 'everything collected while do not disturb was on: incidents first, approvals, inbox (xN), slot messages, sessions that need you; then offers to turn it off', status: 'active' },
290
344
  { id: 'back', kind: 'command', new: true, group: 'Context', usage: '/back', desc: 'mark yourself present and show what happened since /afk (same as /away)', status: 'active' },
291
- { id: 'observe', kind: 'command', new: true, group: 'Context', usage: '/observe <text>', desc: 'a note to the observer: posts your text to the session that watches your window', status: 'active', dev: true },
292
- { id: 'huh', kind: 'command', new: true, group: 'Conversation', usage: '/huh [question]', desc: 'the dragon explains what just happened (same as /dragon explain)', status: 'active' },
293
- { id: 'sleep', kind: 'command', new: true, group: 'Conversation', usage: '/sleep', desc: 'mark yourself sleeping (sessions stop waiting on approvals, which still queue) and the dragon curls up (/dragon sleep)', status: 'active' },
345
+ { id: 'huh', kind: 'command', new: true, group: 'Conversation', usage: '/huh [question]', desc: 'explain the last output in plain words, using one turn on your model (same as /dragon explain)', status: 'active' },
346
+ { id: 'sleep', kind: 'command', new: true, group: 'Conversation', usage: '/sleep', desc: 'tell other sessions you are away sleeping so they stop waiting on your approvals (they still queue), then the dragon curls up (same as /dragon sleep)', status: 'active' },
294
347
  { id: 'mode', kind: 'command', new: true, group: 'Context', usage: '/mode [ask|plan]', desc: 'this window\'s mode: ask first (default) or plan only', status: 'active', args: ARGS.mode,
295
348
  detail: 'Alone it shows the current mode and the options. /mode plan: Quox investigates with read tools and proposes the steps and the exact commands, and runs nothing that changes state; the server enforces it, and the answer ends with "Run this plan now? y/N/c" (y switches back to ask and sends the go-ahead). /mode ask goes back. For this window only: kept across /restart, never saved to your profile. Auto mode is not available yet: it needs standing grants (planned).' },
349
+ { id: 'devservers', kind: 'command', new: true, group: 'Connect', usage: '/devservers [name]', desc: 'your registered dev servers; /devservers <name> shows its go command', status: 'active' },
296
350
  { id: 'slots', kind: 'command', new: true, group: 'Conversation', usage: '/slots [all]', desc: 'held slots: who holds them and where; free ones are summarised, /slots all lists them (same as /slot list)', needs: ['slots'], cap: 'slots', status: 'active' },
297
351
  { id: 'mirror', kind: 'command', new: true, group: 'Context', usage: '/mirror [agents]', desc: 'the Mirror: what CommanderQ sees about this org right now', needs: ['mirror'], status: 'active',
298
352
  detail: 'Read-only, no model call. The org-scoped Mirror the server builds from each service and injects into CommanderQ on every turn: identity, health, implants, capabilities, plugins, memory, codebase, and every value it could not read with the reason. /mirror agents lists the agents. For the QuoxMind implants alone use /mind.' },
@@ -333,6 +387,10 @@ export const REGISTRY = [
333
387
  'prompt profile: compact is a short system prompt for small or local models (the ' +
334
388
  'default on Ollama, LM Studio and vLLM), full is the default elsewhere; --prompt-' +
335
389
  'profile does the same for one --print run.' },
390
+ { id: 'deck', ext: 'quoxdeck', kind: 'command', group: 'Context', usage: '/deck on|off|status', desc: 'QuoxDeck: an optional MIDI pad surface; pads open an approval or preselect an answer, you confirm here', status: 'active',
391
+ detail: 'Provided by the optional quoxdeck extension, not by core: npm i -g quoxdeck, then quox ext add quoxdeck (until then /deck says so). quoxon pushes approvals, health and loops to the deck at most once a second. A pad only opens an approval view or fills the composer with the answer; it never answers for you, and high risk still needs the step-up PIN. The setting is saved, so the deck comes back on at the next start.' },
392
+ { id: 'ext', kind: 'command', group: 'Context', usage: '/ext list|on|off|tab|set|settings <name>', desc: 'optional extensions (npm packages) that add surfaces to this window', status: 'active', args: ARGS.ext,
393
+ detail: 'Extensions load only when installed (npm i -g <pkg>) and listed (quox ext add <pkg>). /ext list shows each one and where it was found, on|off switches one for the next start and now. An extension can open an approval or preselect an answer; it cannot answer for you. /ext tab <name> (or Ctrl+]) focuses an extension panel and Esc returns to the composer; /ext set <name> <key> <value> and /ext settings <name> change and show its settings. Contract: cli/lib/quoxon/EXTENSIONS.md.' },
336
394
  { id: 'row', kind: 'command', group: 'Context', usage: '/row [new|add|list|edit <id>|rm <id>|test <id>|reload|on|off]', desc: 'your own live lines above the status bar: stats from a URL, your Quox, a plugin or a command', status: 'active', args: ARGS.row,
337
395
  detail:
338
396
  '/row new walks you through one row with y/N cards and shows what it will say before saving; /row edit <id> changes one. ' +
@@ -350,12 +408,36 @@ export const REGISTRY = [
350
408
  detail: 'Type @ at the start of a word: a list under the composer suggests agents, fleet hosts, tasks (your newest objectives) and receipts (the envelope ids of this session\'s tool calls), filtered as you type by id or name. Tab or Enter puts it in, Esc closes the list. The lists are fetched once per session on your first @ and then filtered locally, so typing never sends a request; receipts come from this session and need no request at all. /peek <id> shows what an id is.' }
351
409
  ];
352
410
 
411
+ // Splice dev entries back next to their neighbours (after /notes and /back); absent in the public build.
412
+ for (const e of DEV_ENTRIES) {
413
+ const after = e.id === 'devnote' ? 'notes' : 'back';
414
+ const i = REGISTRY.findIndex((x) => x.id === after);
415
+ REGISTRY.splice(i < 0 ? REGISTRY.length : i + 1, 0, e);
416
+ }
417
+ // Vendor (owner tooling) entries go just before /row; absent in the public build.
418
+ for (const e of VENDOR_ENTRIES) {
419
+ const i = REGISTRY.findIndex((x) => x.id === 'row');
420
+ REGISTRY.splice(i < 0 ? REGISTRY.length : i, 0, e);
421
+ }
422
+
353
423
  // The palette row's one-line wording (commandShort.js); /help keeps the long desc.
354
424
  for (const e of REGISTRY) if (e.kind === 'command' && !e.short && SHORT[e.id]) e.short = SHORT[e.id];
355
425
 
426
+ /** Commands of loaded extensions join the registry (help, palette, Tab) while loaded: [{ name, help, ext, subcommands }]. Static ids win. */
427
+ export function setExtEntries(cmds) {
428
+ for (let i = REGISTRY.length - 1; i >= 0; i--) if (REGISTRY[i].dyn) REGISTRY.splice(i, 1);
429
+ for (const c of cmds || []) {
430
+ const id = String(c.name || '').replace(/^\//, '');
431
+ if (!id || REGISTRY.some((e) => e.id === id)) continue;
432
+ const subs = c.subcommands || [];
433
+ REGISTRY.push({ id, dyn: true, ext: c.ext, kind: 'command', group: 'Context', usage: subs.length ? `/${id} ${subs.map((s) => s.name).join('|')}` : `/${id}`, desc: c.help || `from the ${c.ext} extension`, status: 'active',
434
+ ...(subs.length ? { args: [{ after: [], choices: subs.map((s) => ({ value: s.name, desc: s.help || '' })) }] } : {}) });
435
+ }
436
+ }
437
+
356
438
  const norm = (s) => String(s || '').trim().toLowerCase().replace(/^\//, '');
357
439
 
358
- /** Dev-only entries (/observe, /note) are invisible unless a dev observer is configured. */
440
+ /** Dev-only entries (/observe, /devnote) are invisible unless a dev observer is configured. */
359
441
  export const entryVisible = (e) => !e.dev || devObserverOn();
360
442
  /** REGISTRY minus entries hidden on this install. */
361
443
  export const visibleRegistry = () => REGISTRY.filter(entryVisible);
@@ -397,6 +479,14 @@ const STATUS_LINE_HELP = [
397
479
  { kind: 'row', key: '── model ─', text: 'the divider under the composer names the provider and model (/model)' }
398
480
  ];
399
481
 
482
+ /** QUOXON-DELIGHT: the short "Try these" block at the end of /help (the full verified set is the collector's tryThese.json). */
483
+ export const TRY_THESE = [
484
+ { id: 'challenge', usage: '/challenge', hook: 'a different AI provider argues with the answer you just got' },
485
+ { id: 'spawn', usage: '/spawn 2c 4G 20G 1h for X', hook: 'a disposable VM in seconds, gone when the timer ends' },
486
+ { id: 'crystallize', usage: '/crystallize', hook: 'turn what you just did into a reusable skill' },
487
+ { id: 'roar', usage: '/roar <text>', hook: 'shout to every session you hold at once' }
488
+ ];
489
+
400
490
  /** registry: the entries to list (default: this install's visible registry; tests pass a fixture). */
401
491
  export function helpModel(topic, caps = {}, registry = visibleRegistry()) {
402
492
  if (topic) {
@@ -425,6 +515,11 @@ export function helpModel(topic, caps = {}, registry = visibleRegistry()) {
425
515
  out.push({ kind: 'gap' }, { kind: 'head', text: 'Coming', dim: true });
426
516
  for (const e of planned) out.push({ kind: 'dim', key: e.usage, text: e.desc });
427
517
  }
518
+ const tries = TRY_THESE.filter(t => registry.some(e => e.status === 'active' && (e.id === t.id || (e.aliases || []).includes(t.id))));
519
+ if (tries.length) {
520
+ out.push({ kind: 'gap' }, { kind: 'subhead', text: 'Try these' });
521
+ for (const t of tries) out.push({ kind: 'row', key: t.usage, text: t.hook });
522
+ }
428
523
  out.push({ kind: 'gap' }, { kind: 'dim', text: '/help <command> for detail, e.g. /help esc' });
429
524
  return out;
430
525
  }
@@ -435,7 +530,7 @@ export function suggestCommands(input, max = 3) {
435
530
  if (!q) return [];
436
531
  const names = [...new Set(commandNames())];
437
532
  const lev = (a, b) => { const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]); for (let j = 1; j <= b.length; j++) dp[0][j] = j; for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); return dp[a.length][b.length]; };
438
- return names.map(n => { const k = n.slice(1); const rel = q.startsWith(k) || k.startsWith(q) || (k.length >= 2 && q.includes(k)) || (q.length >= 2 && k.includes(q)); const head = q.length >= 3 && k.length > q.length ? lev(q, k.slice(0, q.length)) + 0.5 : Infinity; return { n, d: rel ? 0 : Math.min(lev(q, k), head), len: Math.abs(k.length - q.length) }; })
533
+ return names.map(n => { const k = n.slice(1); const sep = k.length >= 2 && q.length > k.length && q.startsWith(k) && /[-_ :]/.test(q[k.length]); const rel = sep || k.startsWith(q) || (k.length >= 2 && q.includes(k) && q.length - k.length <= 2) || (q.length >= 2 && k.includes(q)); const head = q.length >= 3 && k.length > q.length ? lev(q, k.slice(0, q.length)) + 0.5 : Infinity; return { n, d: sep ? -1 : rel ? 0 : Math.min(lev(q, k), head), len: Math.abs(k.length - q.length) }; })
439
534
  .filter(x => x.d <= Math.max(2, Math.floor(q.length / 3)))
440
535
  .sort((a, b) => a.d - b.d || a.len - b.len || a.n.localeCompare(b.n)).slice(0, max).map(x => x.n);
441
536
  }
@@ -467,7 +562,7 @@ export function nearestCommands(input, max = 3) {
467
562
  }
468
563
 
469
564
  // Commands whose last argument is free text: a pasted line that the terminal wrapped is still one command.
470
- const FREE_TEXT_CMDS = new Set(['ask', 'aside', 'afk', 'observe', 'roar', 'awaken', 'poke', 'wake', 'nudge', 'ping', 'slap', 'feedback', 'huh', 'r', 'reply', 'say', 'page', 'broadcast', 'gripe', 'gripe!', 'note', 'dragon', 'deny', 'wish', 'challenge']);
565
+ const FREE_TEXT_CMDS = new Set(['ask', 'aside', 'afk', 'observe', 'roar', 'awaken', 'poke', 'wake', 'nudge', 'ping', 'slap', 'feedback', 'huh', 'r', 'reply', 'say', 'page', 'broadcast', 'gripe', 'gripe!', 'note', 'devnote', 'dragon', 'deny', 'wish', 'challenge']);
471
566
  /** "/say a long\n text" -> { text: '/say a long text', lines: 2 }; null when it is not a wrapped free-text command. */
472
567
  export function joinWrappedSlash(text) {
473
568
  const t = String(text || '');
@@ -125,7 +125,7 @@ export const pickRow = (rows, type) => {
125
125
  * `caps` is the server's capabilities answer, the source of truth for whether each provider is usable
126
126
  * (providers.js, shared with /model and /doctor). Anything neither can answer is "? unknown".
127
127
  */
128
- export function connectionLines(res, caps = null) {
128
+ export function connectionLines(res, caps = null, { includeOthers = true } = {}) {
129
129
  const STATUS_PROVIDERS = [PROVIDERS.openai, PROVIDERS.anthropic, PROVIDERS.ollama, PROVIDERS.openrouter, PROVIDERS.huggingface, PROVIDERS.groq, PROVIDERS.together, PROVIDERS.lmstudio, PROVIDERS.vllm, PROVIDERS.custom];
130
130
  const out = [];
131
131
  const ok = res && res.ok && Array.isArray(res.rows);
@@ -136,7 +136,7 @@ export function connectionLines(res, caps = null) {
136
136
  const st = providerState({ cap: p.id, vault: p.type }, caps, res);
137
137
  const def = st.row;
138
138
  if (st.state === 'unknown') out.push([`? ${p.name} unknown (vault not readable)`, 'warn']);
139
- else if (st.state === 'connected' && def) out.push([`● ${p.name} connected · ${def.label || def.id} · ${testTag(def)}`, def.test_status === 'failed' ? 'warn' : 'ok']);
139
+ else if (st.state === 'connected' && def) out.push([`● ${p.name} connected · ${def.label || def.id} · ${testTag(def)}${def.test_status === 'failed' ? ` · re-test: quox provider test ${p.id}` : ''}`, def.test_status === 'failed' ? 'warn' : 'ok']);
140
140
  else if (st.state === 'connected') out.push([`● ${p.name} ready on this server (an instance-wide key, not in your org vault)`, 'ok']);
141
141
  else if (st.state === 'stored') out.push([`◐ ${p.name} stored but not the default · quox vault set-default ${st.row.id}`, 'warn']);
142
142
  else if (st.state === 'unavailable') out.push([`○ ${p.name} not available here${st.reason ? `: ${st.reason}` : ''}`]);
@@ -155,7 +155,9 @@ export function connectionLines(res, caps = null) {
155
155
  if (!cur || (r.is_default && !cur.is_default)) others.set(r.integration_type, r);
156
156
  }
157
157
  const list = [...others.values()];
158
- if (list.length) {
158
+ if (!includeOthers) {
159
+ if (list.length) out.push([` ${list.length} other integration${list.length === 1 ? '' : 's'} in your vault: quox provider list --all`]);
160
+ } else if (list.length) {
159
161
  out.push(['Integrations in your vault:']);
160
162
  for (const r of list.slice(0, 12)) out.push([`● ${r.integration_type} ${r.label || r.id} · ${testTag(r)}`, r.test_status === 'failed' ? 'warn' : undefined]);
161
163
  if (list.length > 12) out.push([` +${list.length - 12} more · quox vault list`]);
@@ -1,6 +1,15 @@
1
1
  /** The restart countdown line, fitted to the width: full text when it fits, shorter forms below, never wrapped. */
2
- export function countdownLine(secs, cols) {
2
+ export function countdownLine(secs, cols, { waiting = false } = {}) {
3
3
  const room = Math.max(20, (cols || 80) - 4); // paddingLeft 2 plus a margin
4
+ if (waiting) { // the countdown is over but the Quox server is down (a land is redeploying it): hold the restart until it answers
5
+ const w = [
6
+ '↻ New version ready, waiting for Quox server to come back before restarting · Esc to stay · Enter to restart now',
7
+ '↻ Waiting for Quox server to come back before restarting · Esc stay · Enter now',
8
+ '↻ Waiting for Quox server before restarting · Esc stay',
9
+ '↻ Waiting for Quox server · Esc stay',
10
+ ];
11
+ return w.find(f => f.length <= room) || w[w.length - 1];
12
+ }
4
13
  const forms = [
5
14
  `↻ New version ready. Restarting in ${secs}s · Esc to stay on this version · Enter to restart now`,
6
15
  `↻ New version · restarting in ${secs}s · Esc stay · Enter now`,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * QuoxDeck compatibility shim. The deck is no longer part of core: it is an installable extension ("quoxdeck") run by the
3
+ * generic host in extensions.js (see EXTENSIONS.md). This file only keeps the old deck-shaped surface (on/off/status, the
4
+ * QUOXDECK_MODULE override, the "not installed" text) as a thin wrapper over that host. New code should use extensions.js.
5
+ */
6
+ import { createExtensionHost, buildSnapshot, pickDraft, notInstalled, THROTTLE_MS } from './extensions.js';
7
+
8
+ export const DECK_PACKAGE = 'quoxdeck';
9
+ export const DECK_NOT_INSTALLED = notInstalled(DECK_PACKAGE);
10
+ export const DECK_THROTTLE_MS = THROTTLE_MS;
11
+ export const buildDeckState = buildSnapshot;
12
+ export const deckPickDraft = pickDraft;
13
+
14
+ export function createDeckHost({ env = process.env, ...rest } = {}) {
15
+ const legacy = env?.QUOXDECK_MODULE; // the old override still works, mapped to QUOX_EXT_QUOXDECK_MODULE
16
+ const h = createExtensionHost({ ...rest, env: legacy ? { ...env, QUOX_EXT_QUOXDECK_MODULE: legacy } : env });
17
+ return {
18
+ armed: h.armed,
19
+ emit: (msg) => h.emit(msg),
20
+ push: h.push,
21
+ async on() {
22
+ if (h.armed()) return { ok: true, text: 'QuoxDeck is already on.' };
23
+ const r = await h.load(DECK_PACKAGE);
24
+ if (r.ok) return { ok: true, text: 'QuoxDeck is on. Pads can open an approval or preselect an answer; you confirm in this window.' };
25
+ return { ok: false, text: r.notFound ? `QuoxDeck ${DECK_NOT_INSTALLED}` : `QuoxDeck ${r.text.replace(/^quoxdeck /, '')}`.replace('could not load: the package has no create().', 'could not load: the package has no createDeck.') };
26
+ },
27
+ off() { const was = h.armed(); h.unloadAll(); return was ? { ok: true, text: 'QuoxDeck is off.' } : { ok: true, text: 'QuoxDeck was not on.' }; },
28
+ status: () => (h.armed() ? 'QuoxDeck: on (armed).' : 'QuoxDeck: off. /deck on arms it.')
29
+ };
30
+ }