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
@@ -8,6 +8,9 @@ import { resolveEndpoint } from '../api/endpoints.js';
8
8
  import { QuoxApiError, QuoxValidationError } from '../core/errors.js';
9
9
  import { confirmDestructive } from '../core/confirm.js';
10
10
  import { passwordProblem } from '../quoxon/onboarding/finish.js';
11
+ import {
12
+ renderLinkCard, renderLinkedCard, linkedDetails, startWaiting, colorFor, animateFor,
13
+ } from './licenceLinkView.js';
11
14
 
12
15
  // The auth service's rule (services/auth/utils/passwordValidation.js), said up front.
13
16
  export const PASSWORD_POLICY = 'Passwords need at least 12 characters, with an uppercase letter, a lowercase letter, a number and a special character (for example - or !).';
@@ -348,93 +351,142 @@ export function registerSetupCommands(program, ctx) {
348
351
  interval,
349
352
  });
350
353
 
354
+ // QUOXON (owner 2026-11-05): a framed card with the code, the full
355
+ // link and a countdown, then a live waiting line on a TTY. Not a TTY
356
+ // (CI, logs): plain lines, no ANSI, nothing animated.
357
+ const term = ctx.term || process.stdout;
358
+ const format = program.opts().output;
359
+ const pretty = !json && (format === undefined || format === 'table');
360
+ const color = pretty && colorFor(term);
361
+ const verificationUrl = started.verification_url || 'https://quox.ai/link';
362
+ let waiting = null;
351
363
  if (!json) {
352
- printInfo(`Open ${started.verification_url || 'https://quox.ai/link'} and enter this code:`);
353
- console.log('');
354
- console.log(chalk.bold.cyan(` ${started.user_code}`));
355
- console.log('');
356
- printInfo(`The code is good for ${Math.round(expiresIn / 60)} minutes. Waiting for approval...`);
364
+ if (pretty) {
365
+ console.log(renderLinkCard({
366
+ userCode: started.user_code, url: verificationUrl, remainingMs: expiresIn * 1000, color, cols: term.columns || 80,
367
+ }));
368
+ if (term.isTTY) {
369
+ waiting = startWaiting({ term, deadline, totalMs: expiresIn * 1000, color, animate: animateFor(term) });
370
+ }
371
+ } else {
372
+ printInfo(`Open ${verificationUrl} and enter this code:`);
373
+ console.log('');
374
+ console.log(chalk.bold.cyan(` ${started.user_code}`));
375
+ console.log('');
376
+ printInfo(`The code is good for ${Math.round(expiresIn / 60)} minutes. Waiting for approval...`);
377
+ }
357
378
  }
358
-
359
- const pollEp = resolveEndpoint('setup.licensing.device.poll');
360
- let lastState = 'started';
361
-
362
- while (Date.now() < deadline) {
363
- await sleepFn(interval * 1000);
364
-
365
- let result;
366
- try {
367
- result = await client.post(pollEp.path, {
368
- service: pollEp.service,
369
- body: { device_code: deviceCode, interval },
370
- });
371
- } catch (err) {
372
- // 503 means quox.ai blipped, NOT that the activation died: the code
373
- // is still live upstream and the customer may still be approving it.
374
- // Anything else (409 seat_limit_reached, 403, 400) is terminal.
375
- if (err instanceof QuoxApiError && err.status === 503) {
376
- if (lastState !== 'unreachable') {
377
- emit({ state: 'unreachable', message: err.message });
378
- if (!json) printInfo('The licence server is unreachable right now. Still waiting...');
379
- lastState = 'unreachable';
379
+ try {
380
+ await pollUntilDone();
381
+ } finally {
382
+ if (waiting) waiting.stop();
383
+ }
384
+ return;
385
+
386
+ async function pollUntilDone() {
387
+ const pollEp = resolveEndpoint('setup.licensing.device.poll');
388
+ let lastState = 'started';
389
+
390
+ while (Date.now() < deadline) {
391
+ await sleepFn(interval * 1000);
392
+
393
+ let result;
394
+ try {
395
+ result = await client.post(pollEp.path, {
396
+ service: pollEp.service,
397
+ body: { device_code: deviceCode, interval },
398
+ });
399
+ } catch (err) {
400
+ // 503 means quox.ai blipped, NOT that the activation died: the code
401
+ // is still live upstream and the customer may still be approving it.
402
+ // Anything else (409 seat_limit_reached, 403, 400) is terminal.
403
+ if (err instanceof QuoxApiError && err.status === 503) {
404
+ if (lastState !== 'unreachable') {
405
+ emit({ state: 'unreachable', message: err.message });
406
+ if (waiting) waiting.note('licence server unreachable, still waiting');
407
+ else if (!json) printInfo('The licence server is unreachable right now. Still waiting...');
408
+ lastState = 'unreachable';
409
+ }
410
+ continue;
380
411
  }
381
- continue;
412
+ throw enrichLicenseError(err);
382
413
  }
383
- throw enrichLicenseError(err);
384
- }
385
414
 
386
- const state = result.state || 'pending';
415
+ const state = result.state || 'pending';
387
416
 
388
- if (state === 'slow_down') {
389
- interval = Number(result.interval) > 0 ? Number(result.interval) : interval + 5;
390
- } else if (Number(result.interval) > 0) {
391
- interval = Number(result.interval);
392
- }
417
+ if (state === 'slow_down') {
418
+ interval = Number(result.interval) > 0 ? Number(result.interval) : interval + 5;
419
+ } else if (Number(result.interval) > 0) {
420
+ interval = Number(result.interval);
421
+ }
393
422
 
394
- if (state !== lastState) {
395
- emit({ state, interval, ...(result.error ? { error: result.error } : {}) });
396
- lastState = state;
397
- }
423
+ if (state !== lastState) {
424
+ emit({ state, interval, ...(result.error ? { error: result.error } : {}) });
425
+ lastState = state;
426
+ }
398
427
 
399
- if (state === 'approved') {
400
- const lease = result.lease || {};
401
- emit({ state: 'approved', lease });
402
- if (!json) {
403
- formatDetail({
404
- license_id: lease.license_id || null,
405
- entitlements: lease.entitlements || [],
406
- lease_expires_at: lease.expires_at || null,
407
- }, { format: program.opts().output, title: 'Licence activated' });
408
- printSuccess('This instance is activated. No key was ever pasted into it.');
428
+ if (state === 'approved') {
429
+ const lease = result.lease || {};
430
+ // QUOXON (owner 2026-11-05): read the licence back through the
431
+ // route `quox licence status` uses, so the run NAMES what was
432
+ // linked (plan, plugins, seats). A failed read never hides the
433
+ // activation: the lease is the fallback, and the failure is said.
434
+ let status = null;
435
+ let readBackError = null;
436
+ if (json || pretty) {
437
+ try {
438
+ const readEp = resolveEndpoint('setup.licensing.entitlements.list');
439
+ status = await client.get(readEp.path, { service: readEp.service });
440
+ } catch (err) {
441
+ readBackError = err && err.message ? err.message : String(err);
442
+ }
443
+ }
444
+ const details = linkedDetails(lease, status, result);
445
+ // --json: the pre-existing { state, lease } plus an ADDITIVE `licence`.
446
+ emit({ state: 'approved', lease, licence: details });
447
+ if (waiting) { waiting.stop(); waiting = null; }
448
+ if (pretty) {
449
+ console.log(renderLinkedCard(details, {
450
+ color, cols: term.columns || 80, leaseExpiresAt: lease.expires_at || null,
451
+ profile: program.opts().profile || null, readBackError,
452
+ }));
453
+ } else if (!json) {
454
+ formatDetail({
455
+ license_id: lease.license_id || null,
456
+ entitlements: lease.entitlements || [],
457
+ lease_expires_at: lease.expires_at || null,
458
+ }, { format: program.opts().output, title: 'Licence activated' });
459
+ printSuccess('This instance is activated. No key was ever pasted into it.');
460
+ }
461
+ return;
409
462
  }
410
- return;
411
- }
412
463
 
413
- if (state === 'denied') {
414
- throw new QuoxApiError(403, 'access_denied',
415
- 'The activation was denied on quox.ai. Nothing was activated on this instance.');
416
- }
417
- if (state === 'expired') {
418
- throw new QuoxApiError(410, 'expired_token',
419
- 'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
420
- }
421
- if (state === 'consumed') {
422
- throw new QuoxApiError(409, 'consumed',
423
- 'That code was already used. Run `quox setup license link` again for a fresh one.');
424
- }
425
- if (state === 'activation_failed') {
426
- throw new QuoxApiError(502, result.error || 'activation_failed',
427
- `The approval landed but the licence could not be stored on this instance: ${result.message || result.error || 'unknown error'}. `
428
- + 'Run `quox setup license link` again.');
429
- }
430
- if (state === 'error') {
431
- throw new QuoxApiError(502, result.error || 'device_poll_failed',
432
- result.message || 'Device activation failed.');
464
+ if (state === 'denied') {
465
+ throw new QuoxApiError(403, 'access_denied',
466
+ 'The activation was denied on quox.ai. Nothing was activated on this instance.');
467
+ }
468
+ if (state === 'expired') {
469
+ throw new QuoxApiError(410, 'expired_token',
470
+ 'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
471
+ }
472
+ if (state === 'consumed') {
473
+ throw new QuoxApiError(409, 'consumed',
474
+ 'That code was already used. Run `quox setup license link` again for a fresh one.');
475
+ }
476
+ if (state === 'activation_failed') {
477
+ throw new QuoxApiError(502, result.error || 'activation_failed',
478
+ `The approval landed but the licence could not be stored on this instance: ${result.message || result.error || 'unknown error'}. `
479
+ + 'Run `quox setup license link` again.');
480
+ }
481
+ if (state === 'error') {
482
+ throw new QuoxApiError(502, result.error || 'device_poll_failed',
483
+ result.message || 'Device activation failed.');
484
+ }
433
485
  }
434
- }
435
486
 
436
- throw new QuoxApiError(410, 'expired_token',
437
- 'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
487
+ throw new QuoxApiError(410, 'expired_token',
488
+ 'The code expired before it was approved. Run `quox setup license link` again for a fresh one.');
489
+ }
438
490
  });
439
491
 
440
492
  license
@@ -0,0 +1,41 @@
1
+ // `quox shortcut add|remove`: a short command (default `q`) for quox. Local only; never shadows an existing command.
2
+ import chalk from 'chalk';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { addShortcut, removeShortcut, DEFAULT_NAME } from '../core/shortcut.js';
5
+ import { askText } from '../quoxon/ask.js';
6
+
7
+ async function askYes(line) {
8
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9
+ try { return /^y(es)?$/i.test((await rl.question(`${line} `)).trim()); } finally { rl.close(); }
10
+ }
11
+
12
+ export function registerShortcutCommand(program, ctx = {}) {
13
+ const d = ctx.shortcutDeps || {};
14
+ const out = d.log || ((l) => console.log(l));
15
+ const sc = program.command('shortcut')
16
+ .description('A short command for quox, like q (add / remove); refuses if the name already runs something else, never uses sudo');
17
+ sc.command('add')
18
+ .description('Make a "q" command that runs quox: a symlink in a directory you own that is on PATH. Refuses (and says what it found) if q already resolves to something else on PATH; never shadows an existing command, no sudo, no shell rc edits. Safe to repeat.')
19
+ .option('--name <name>', 'the short command name', DEFAULT_NAME)
20
+ .option('-y, --yes', 'create without asking (needed in a non-interactive shell)')
21
+ .action(async (o) => {
22
+ const name = o.name || DEFAULT_NAME;
23
+ const yes = o.yes || program.opts().yes;
24
+ if (!yes) {
25
+ const tty = d.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
26
+ if (!tty) { out('Not created: this needs a yes. Re-run with quox shortcut add --yes in a non-interactive shell.'); process.exitCode = 1; return; }
27
+ if (!(await (d.ask || askYes)(askText(`Make \`${name}\` run quox?`, { keys: 'y/N' })))) { out('Not created. Nothing was changed.'); return; }
28
+ }
29
+ const r = (d.add || addShortcut)({ name, ...(d.opts || {}) });
30
+ out(r.ok ? chalk.green(`✔ ${r.message}`) : chalk.red(`✖ ${r.message}`));
31
+ if (!r.ok) process.exitCode = 1;
32
+ });
33
+ sc.command('remove')
34
+ .description('Remove the short command, only if it is a link to this quox (anything else named q is left alone)')
35
+ .option('--name <name>', 'the short command name', DEFAULT_NAME)
36
+ .action(async (o) => {
37
+ const r = (d.remove || removeShortcut)({ name: o.name || DEFAULT_NAME, ...(d.opts || {}) });
38
+ out(r.ok ? r.message : chalk.red(`✖ ${r.message}`));
39
+ if (!r.ok) process.exitCode = 1;
40
+ });
41
+ }
@@ -84,6 +84,8 @@ export function registerSkillsCommands(program, ctx) {
84
84
  for (const it of Array.isArray(items) ? items : []) if (it && it.skill_id) active.set(it.skill_id, it.status || status);
85
85
  }
86
86
  rows = rows.map((r) => ({ ...r, review_status: active.get(r.id) || null, runnable: !active.has(r.id) }));
87
+ // Runnable first (stable): a skill that can run leads, so the first row is never one skill_view must refuse.
88
+ rows = [...rows.filter((r) => r.runnable), ...rows.filter((r) => !r.runnable)];
87
89
  } catch (err) {
88
90
  printWarning(`Skill review queue could not be read, so which skills can run is unknown: ${err.message}`);
89
91
  }
@@ -585,11 +587,27 @@ export function registerSkillsCommands(program, ctx) {
585
587
  sealed
586
588
  .command('show')
587
589
  .description('Show a sealed skill\'s manifest (name, description, input contract, content hash). Manifest only.')
588
- .argument('<sealedId>', 'sealed skill ID')
589
- .action(async (sealedId) => {
590
+ .argument('<sealedId>', 'sealed skill ID or name')
591
+ .action(async (idOrName) => {
590
592
  auth.requireAuth();
591
- const ep = resolveEndpoint('skills.sealed.get', { sealedId });
592
- const data = await client.get(ep.path, { service: ep.service });
593
+ let sealedId = idOrName;
594
+ let data;
595
+ try {
596
+ const ep = resolveEndpoint('skills.sealed.get', { sealedId });
597
+ data = await client.get(ep.path, { service: ep.service });
598
+ } catch (err) {
599
+ // Coverage wave (ev_mvg4iwfp_d0b2e3): chat passed the skill's name. A 404 that matches exactly one sealed
600
+ // skill's name resolves to its id; anything else keeps the original error.
601
+ if (err.status !== 404) throw err;
602
+ const lep = resolveEndpoint('skills.sealed.list');
603
+ const list = await client.get(lep.path, { service: lep.service });
604
+ const rows = (list && (list.sealed ?? list)) || [];
605
+ const hits = (Array.isArray(rows) ? rows : []).filter((r) => r && r.name === idOrName);
606
+ if (hits.length !== 1) throw err;
607
+ sealedId = hits[0].sealedId;
608
+ const ep = resolveEndpoint('skills.sealed.get', { sealedId });
609
+ data = await client.get(ep.path, { service: ep.service });
610
+ }
593
611
  printInfo('Manifest only. The sealed content is never returned by the API.');
594
612
  formatDetail(data, { format: program.opts().output, title: `Sealed skill: ${sealedId}` });
595
613
  });
@@ -625,7 +643,8 @@ export function registerSkillsCommands(program, ctx) {
625
643
  try {
626
644
  const ep = resolveEndpoint('skills.sealed.create');
627
645
  const data = await client.post(ep.path, { service: ep.service, body });
628
- printSuccess(`Sealed. sealedId ${data.sealedId}`);
646
+ // the collector answers { sealed: {...}, evidence }; an older/flat body carries sealedId at the top
647
+ printSuccess(`Sealed. sealedId ${(data && data.sealed && data.sealed.sealedId) || (data && data.sealedId)}`);
629
648
  formatDetail(data, { format: program.opts().output, title: 'Sealed skill' });
630
649
  printInfo(SEALED_SECTION_G_SENTENCE);
631
650
  } catch (err) {
@@ -19,6 +19,7 @@ import { dirname, join } from 'node:path';
19
19
 
20
20
  import { formatDetail, formatOutput, printSuccess, printWarning, printInfo } from '../core/output.js';
21
21
  import { events, printEvent } from '../core/quoxglyph.js';
22
+ import { announce, announceOnFail } from '../core/announce.js';
22
23
  import { resolveEndpoint } from '../api/endpoints.js';
23
24
  import { QuoxApiError, QuoxNetworkError } from '../core/errors.js';
24
25
  import { resolveOrgId } from './helpers.js';
@@ -400,6 +401,23 @@ export function registerSlotCommands(program, ctx) {
400
401
  }
401
402
  }
402
403
 
404
+ // SLOT-FROM: the session name this process holds a slot under (held-slots.json lists it, or QUOX_SLOT
405
+ // names a held slot), sent as holder_session so the server's hold record stamps from_slot even when
406
+ // senderSlot() could not bind a live process. The server re-checks identity + session; never trusted.
407
+ function heldSessionFor(from) {
408
+ try {
409
+ const all = readHeldSlots();
410
+ const want = from ? String(from).toLowerCase() : null;
411
+ const bySlot = process.env.QUOX_SLOT ? all[String(process.env.QUOX_SLOT).trim()] : null;
412
+ if (bySlot && !bySlot.member && bySlot.session && (!want || String(bySlot.session).toLowerCase() === want)) return bySlot.session;
413
+ if (!want) return null;
414
+ const hit = Object.values(all).find((e) => e && !e.member && String(e.session || '').toLowerCase() === want);
415
+ return hit ? hit.session : null;
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+
403
421
  // P10: the collector recognises splinter members only via the ATTACH token.
404
422
  // When this Claude session is attached to an agent whose recorded slot is
405
423
  // `name`, send that token (same noAuth + Authorization pattern as
@@ -866,7 +884,7 @@ export function registerSlotCommands(program, ctx) {
866
884
  .option('--session <s>', 'session name to speak as (default: $QUOX_STREAM)')
867
885
  .option('--room', 'post as a joined room member (no hold needed); signed "session (person)" by the server')
868
886
  .option('--run <command>', 'ask the human to run ONE command in their own terminal (the text is why); the quox TUI shows it in full and runs it only on their y. Never run server-side')
869
- .action(async (name, textParts, options) => {
887
+ .action(announceOnFail('quoxslots', (name) => `say into ${name}`, async (name, textParts, options) => {
870
888
  auth.requireAuth();
871
889
  // SLOT-RUN: same rule as the collector (one line, max 2000, no control/bidi characters), checked before sending.
872
890
  if (options.run !== undefined) {
@@ -900,6 +918,8 @@ export function registerSlotCommands(program, ctx) {
900
918
  // A room post already has its address (the room), so it carries none.
901
919
  const fromSlot = options.room ? null : senderSlot(from);
902
920
  if (fromSlot) body.from_slot = fromSlot;
921
+ const heldAs = options.room ? null : heldSessionFor(from);
922
+ if (heldAs) body.holder_session = heldAs;
903
923
  // A freshly created slot gets its Matrix room a few seconds AFTER
904
924
  // checkout returns (provisioning is async), so a say straight after
905
925
  // checkout used to fail with 409 "no Matrix room yet" (slots audit
@@ -952,6 +972,7 @@ export function registerSlotCommands(program, ctx) {
952
972
  throw err;
953
973
  }
954
974
  }
975
+ announce('quoxslots', `said into ${name}`, { result: data?.presence?.state === 'listening' ? 'delivered, listening' : 'posted' });
955
976
  printSuccess(`Said into ${name}.`);
956
977
  if (!options.room && !fromSlot) {
957
978
  printWarning(`${from || 'This session'} holds no slot, so replies cannot reach it. Claim one: quox slot checkout <name> --create --session ${from || '<session>'}`);
@@ -962,7 +983,7 @@ export function registerSlotCommands(program, ctx) {
962
983
  reportHumanPresence(data?.human_presence);
963
984
  reportBridgeDelivery(data?.bridges);
964
985
  if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
965
- });
986
+ }));
966
987
 
967
988
  // QUOXON-BROADCAST: one call fans a message out to every held slot in the org (or --slots).
968
989
  slot
@@ -972,24 +993,38 @@ export function registerSlotCommands(program, ctx) {
972
993
  .option('--slots <list>', 'comma-separated slot names instead of every held slot')
973
994
  .option('--priority <p>', 'normal or urgent', 'normal')
974
995
  .option('--ack', 'ask recipients to acknowledge with /ack <id>')
996
+ .option('--active', 'only sessions listening right now; dormant slots are skipped, not woken')
997
+ .option('--dry-run', 'with --active: print who would be reached and who is skipped dormant, send nothing')
975
998
  .option('--session <s>', 'session name to sign as (default: $QUOX_STREAM)')
976
- .action(async (textParts, options) => {
999
+ .action(announceOnFail('quoxslots', 'broadcast', async (textParts, options) => {
977
1000
  auth.requireAuth();
978
1001
  const parsed = parseBroadcastArgs([
979
- options.slots ? `--slots ${options.slots}` : '', `--priority ${options.priority}`, options.ack ? '--ack' : '', textParts.join(' '),
1002
+ options.slots ? `--slots ${options.slots}` : '', `--priority ${options.priority}`, options.ack ? '--ack' : '', options.active ? '--active' : '', options.dryRun ? '--dry-run' : '', textParts.join(' '),
980
1003
  ].filter(Boolean).join(' '));
981
1004
  if (!parsed.ok) throw new Error(parsed.message);
982
1005
  const from = options.session || process.env.QUOX_STREAM || null;
1006
+ const bcFromSlot = senderSlot(from);
1007
+ const bcHeldAs = heldSessionFor(from);
983
1008
  const ep = resolveEndpoint('v1.slots.broadcast');
984
1009
  const data = await client.post(ep.path, {
985
1010
  service: ep.service, retries: 0,
986
- body: { text: parsed.text, priority: parsed.priority, ack: parsed.ack, ...(parsed.slots ? { slots: parsed.slots } : {}), ...(from ? { from } : {}), org_id: resolveOrgId(auth, program) },
1011
+ body: { text: parsed.text, priority: parsed.priority, ack: parsed.ack, ...(parsed.active ? { active: true } : {}), ...(parsed.dryRun ? { dry_run: true } : {}), ...(parsed.slots ? { slots: parsed.slots } : {}), ...(from ? { from } : {}), ...(bcFromSlot ? { from_slot: bcFromSlot } : {}), ...(bcHeldAs ? { holder_session: bcHeldAs } : {}), org_id: resolveOrgId(auth, program) },
987
1012
  });
988
1013
  if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
1014
+ if (data?.dry_run) { // BROADCAST-ACTIVE --dry-run: nothing was sent
1015
+ const would = data.would_reach || []; const dorm = data.skipped_dormant || [];
1016
+ announce('quoxslots', 'broadcast to active', { result: `${would.length} would reach \u00B7 ${dorm.length} skipped dormant` });
1017
+ if (would.length) printInfo(` would reach: ${would.join(', ')}`);
1018
+ if (dorm.length) printInfo(` skipped dormant: ${dorm.join(', ')}`);
1019
+ return printInfo('Dry run: nothing was sent.');
1020
+ }
1021
+ if (parsed.active && data?.skipped_dormant) {
1022
+ announce('quoxslots', 'broadcast to active', { result: `${data.counts?.delivered || 0} reached \u00B7 ${data.skipped_dormant.length} skipped dormant` });
1023
+ } else announce('quoxslots', 'broadcast sent', { result: data?.id || data?.broadcast?.id });
989
1024
  for (const [line, kind] of broadcastSummary(data)) (kind === 'warn' ? printWarning : printInfo)(line);
990
1025
  reportHumanPresence(data?.human_presence);
991
1026
  return undefined;
992
- });
1027
+ }));
993
1028
 
994
1029
  slot
995
1030
  .command('broadcast-status')
@@ -1052,12 +1087,14 @@ export function registerSlotCommands(program, ctx) {
1052
1087
  }
1053
1088
  function reportBridgeDelivery(bridges) {
1054
1089
  if (!Array.isArray(bridges) || bridges.length === 0) return;
1055
- for (const b of bridges) {
1056
- const where = b.target ? `${b.platform} (${b.target})` : b.platform || 'bridge';
1090
+ // Two bridges that both report "still sending" with no platform/target printed two identical "bridge:" lines;
1091
+ // number them so each line says which bridge it is about.
1092
+ bridges.forEach((b, i) => {
1093
+ const where = b.target ? `${b.platform} (${b.target})` : b.platform || (bridges.length > 1 ? `bridge ${i + 1} of ${bridges.length}` : 'bridge');
1057
1094
  if (b.ok === true) printInfo(` rendered to ${where}`);
1058
1095
  else if (b.ok === null) printInfo(` ${where}: still sending, check \`quox slot read\``);
1059
1096
  else printWarning(` NOT rendered to ${where}: ${b.reason}`);
1060
- }
1097
+ });
1061
1098
  }
1062
1099
 
1063
1100
  async function readMessages(name, options) {
@@ -1134,6 +1171,7 @@ export function registerSlotCommands(program, ctx) {
1134
1171
  let cursor = options.since || null;
1135
1172
  let netFailures = 0;
1136
1173
  let authSince = null; // start of the current run of consecutive 401/403s
1174
+ let downSince = null; // start of the current outage, for the single resume line
1137
1175
 
1138
1176
  for (;;) {
1139
1177
  const query = { org_id: resolveOrgId(auth, program), timeout_ms: 50000 };
@@ -1145,6 +1183,7 @@ export function registerSlotCommands(program, ctx) {
1145
1183
  let data;
1146
1184
  try {
1147
1185
  data = await client.get(ep.path, { service: ep.service, ...attachAuthFor(name), query, timeout: 65000 });
1186
+ if (downSince !== null) { printInfo(`reconnected after ${Math.round((Date.now() - downSince) / 1000)}s`); downSince = null; }
1148
1187
  netFailures = 0;
1149
1188
  authSince = null;
1150
1189
  } catch (err) {
@@ -1182,6 +1221,7 @@ export function registerSlotCommands(program, ctx) {
1182
1221
  authSince = null;
1183
1222
  }
1184
1223
  netFailures += 1;
1224
+ if (downSince === null) downSince = Date.now();
1185
1225
  let delay = Math.min(2000 * 2 ** Math.min(netFailures - 1, 5), 60000);
1186
1226
  delay = Math.round(delay * (0.75 + Math.random() * 0.5));
1187
1227
  if (Number.isFinite(err.retryAfterMs)) delay = Math.min(Math.max(delay, err.retryAfterMs), 300000);