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
@@ -10,6 +10,8 @@
10
10
  import chalk from 'chalk';
11
11
  import { emitCliEvent } from '../core/aee.js';
12
12
  import { resolveEndpoint } from '../api/endpoints.js';
13
+ import { probeService } from './watch.js';
14
+ import { joinServiceUrl } from '../core/config.js';
13
15
 
14
16
  const STATUS_ICON = {
15
17
  ok: chalk.green('●'),
@@ -21,13 +23,66 @@ function statusDot(status) {
21
23
  return STATUS_ICON[status] || chalk.dim('○');
22
24
  }
23
25
 
26
+ const OK_STATES = new Set(['ok', 'healthy', 'up']);
27
+
28
+ /**
29
+ * Health percentage computed from the same data the screen shows (services plus
30
+ * fleet agents), so it can never say 100% beside "1 degraded, 1 down".
31
+ * Returns null when there is nothing to compute from.
32
+ */
33
+ export function computeHealth(data) {
34
+ let total = 0;
35
+ let good = 0;
36
+ for (const s of Object.values(data?.services || {})) {
37
+ total += 1;
38
+ if (OK_STATES.has(s?.status)) good += 1;
39
+ }
40
+ const f = data?.fleet;
41
+ if (f && f.total) {
42
+ total += f.total;
43
+ good += Math.min(f.total, f.healthy || 0);
44
+ }
45
+ if (!total) return typeof data?.score === 'number' ? data.score : null;
46
+ return Math.round((100 * good) / total);
47
+ }
48
+
49
+ /**
50
+ * Get status without letting one slow source sink the verb: the aggregate and a
51
+ * set of quick direct service probes run in parallel. Aggregate answers -> full
52
+ * result. Aggregate fails but probes answer -> partial result (exit 0). Nothing
53
+ * answers -> error object (exit 1).
54
+ */
55
+ export async function resolveStatus({ aggregate, probes }) {
56
+ const [agg, prb] = await Promise.allSettled([aggregate(), probes()]);
57
+ const probeMap = prb.status === 'fulfilled' ? prb.value : {};
58
+ if (agg.status === 'fulfilled' && agg.value) {
59
+ return { exitCode: 0, data: { ...agg.value, partial: false, sources: { aggregate: { status: 'ok' } } } };
60
+ }
61
+ const message = agg.reason?.message || 'no answer';
62
+ const sources = {
63
+ aggregate: { status: 'failed', error: message },
64
+ probes: { status: Object.values(probeMap).some((p) => p.status === 'healthy') ? 'ok' : 'failed' },
65
+ };
66
+ const services = {};
67
+ for (const [name, p] of Object.entries(probeMap)) {
68
+ services[name] = { status: p.status === 'healthy' ? 'ok' : p.status === 'down' ? 'down' : 'degraded', latency_ms: p.latency_ms ?? null };
69
+ }
70
+ if (sources.probes.status !== 'ok') {
71
+ return { exitCode: 1, data: { error: 'collector_unavailable', message, sources } };
72
+ }
73
+ return { exitCode: 0, data: { partial: true, degraded: true, score: null, timestamp: new Date().toISOString(), services, sources } };
74
+ }
75
+
24
76
  function renderPulse(data) {
25
77
  const lines = [];
26
78
 
27
79
  // Header
28
- const scoreColor = data.score >= 90 ? chalk.green : data.score >= 70 ? chalk.yellow : chalk.red;
80
+ const score = computeHealth(data);
81
+ const scoreColor = score >= 90 ? chalk.green : score >= 70 ? chalk.yellow : chalk.red;
82
+ const scoreText = score == null ? chalk.yellow('Health: partial') : scoreColor(`Health: ${score}%`);
29
83
  lines.push('');
30
- lines.push(chalk.bold(` QUOX PLATFORM STATUS`) + chalk.dim(` ${scoreColor(`Health: ${data.score}%`)} ${data.timestamp || ''}`));
84
+ lines.push(chalk.bold(` QUOX PLATFORM STATUS`) + chalk.dim(` ${scoreText} ${data.timestamp || ''}`));
85
+ if (data.partial) lines.push(chalk.yellow(` Partial: the full status did not answer in time (${data.sources?.aggregate?.error || 'no answer'}); showing live service checks only.`));
31
86
  lines.push(chalk.dim(' ' + '─'.repeat(60)));
32
87
 
33
88
  // Services
@@ -43,7 +98,7 @@ function renderPulse(data) {
43
98
  // Sessions
44
99
  const s = data.sessions || {};
45
100
  const providers = Object.entries(s.providers || {}).map(([k, v]) => `${v} ${k}`).join(', ');
46
- lines.push(` Sessions ${s.active || 0} active ${providers ? `(${providers})` : ''} ${chalk.dim(`$${(s.cost_today_usd || 0).toFixed(2)} today`)}`);
101
+ lines.push(` Sessions ${s.active || 0} active (usage sessions; "quox active" lists workers) ${providers ? `(${providers})` : ''} ${chalk.dim(`$${(s.cost_today_usd || 0).toFixed(2)} today`)}`);
47
102
 
48
103
  // Workflows
49
104
  const w = data.workflows || {};
@@ -78,60 +133,53 @@ function renderPulse(data) {
78
133
  }
79
134
 
80
135
  export function registerStatusCommand(program, ctx) {
81
- const { client } = ctx;
136
+ const { client, config } = ctx;
82
137
 
83
138
  program
84
139
  .command('status')
85
140
  .description('Show platform health and status overview')
141
+ .addHelpText('after', '\nAdd --json for machine output. If the full status is slow, a partial answer (live service checks, partial: true) is returned with exit 0; only a total failure exits 1 with an error object.')
86
142
  .action(async (opts, cmd) => {
87
143
  const outputFormat = cmd.parent?.opts()?.output || 'table';
88
144
  const isJson = outputFormat === 'json' || cmd.parent?.opts()?.json;
89
145
 
90
- try {
91
- // fleet #602: this used to pass noAuth: true, which suppressed the
92
- // Authorization header the client would otherwise attach. The TUI's
93
- // useHealth/HealthView poll this same endpoint WITH the normal client
94
- // auth, as does a comparable authenticated route (backup.js).
95
- // (monitoring.js's `health` verb had the identical noAuth bug — fixed
96
- // in the same change.) Dropping noAuth here matches that pattern: the
97
- // request still works
98
- // for a logged-out session (client.js sends no Authorization header
99
- // when there's nothing stored), but a logged-in session's token now
100
- // actually reaches the collector instead of being withheld.
101
- const data = await client.get(resolveEndpoint('v1.health.aggregate.list').path, {
102
- service: resolveEndpoint('v1.health.aggregate.list').service,
103
- timeout: 15000, // #930: the aggregate fans out to several services; 5s was too tight under load
104
- });
105
-
106
- if (isJson) {
107
- console.log(JSON.stringify(data, null, 2));
108
- } else {
109
- console.log(renderPulse(data));
110
- }
146
+ const ep = resolveEndpoint('v1.health.aggregate.list');
147
+ const names = ['auth', 'memory', 'collector', 'quoxflow', 'files'];
148
+ const { exitCode, data } = await resolveStatus({
149
+ // fleet #602: the normal client auth applies (no noAuth). #930: the aggregate fans out to
150
+ // several services; it gets 8s here because direct probes cover a slow answer.
151
+ aggregate: () => client.get(ep.path, { service: ep.service, timeout: 8000 }),
152
+ probes: async () => {
153
+ const out = await Promise.all(names.map(async (n) => {
154
+ try {
155
+ return [n, await probeService(joinServiceUrl(config.getServiceUrl(n), '/health').href, fetch, 3000)];
156
+ } catch (err) {
157
+ return [n, { status: 'down', reason: err.message }];
158
+ }
159
+ }));
160
+ return Object.fromEntries(out);
161
+ },
162
+ });
163
+
164
+ if (isJson) {
165
+ console.log(JSON.stringify(data, null, 2));
166
+ } else if (data.error) {
167
+ console.log('');
168
+ console.log(chalk.red(' Platform status unavailable'));
169
+ console.log(chalk.dim(` Collector not reachable: ${data.message}`));
170
+ console.log('');
171
+ } else {
172
+ console.log(renderPulse(data));
173
+ }
111
174
 
175
+ if (!data.error) {
112
176
  emitCliEvent(client, {
113
177
  command: 'status',
114
178
  intent: 'quox.cli.status',
115
- payload: { score: data.score, services: Object.keys(data.services || {}).length },
179
+ payload: { score: computeHealth(data), services: Object.keys(data.services || {}).length, partial: Boolean(data.partial) },
116
180
  });
117
- } catch (err) {
118
- if (isJson) {
119
- console.log(JSON.stringify({ error: 'collector_unavailable', message: err.message }));
120
- } else {
121
- console.log('');
122
- console.log(chalk.red(' Platform status unavailable'));
123
- console.log(chalk.dim(` Collector not reachable: ${err.message}`));
124
- console.log('');
125
- }
126
- // fleet #602: a real request failure (network error, non-2xx from
127
- // the collector) was printing the message above and still exiting
128
- // 0, hiding the failure from scripts/CI that check the exit code.
129
- // client.get only reaches this catch on an actual error (thrown
130
- // QuoxApiError / QuoxNetworkError / QuoxAuthError, see
131
- // lib/core/client.js) — never on a successful 2xx — so it's safe to
132
- // fail the process here without risking a false failure on a
133
- // legitimately-empty-but-200 status payload.
134
- process.exitCode = 1;
135
181
  }
182
+ // Only a total failure exits non-zero; a partial answer is still an answer.
183
+ if (exitCode) process.exitCode = exitCode;
136
184
  });
137
185
  }
@@ -199,6 +199,8 @@ export function registerToolCommands(program, ctx) {
199
199
  // success on failure. A HELD (pending) call awaits a human, so it stays 0.
200
200
  const failed = failedOf(data);
201
201
  if (failed && !(data && data.pending)) process.exitCode = 1;
202
+ // A held call exits 0 (chat relies on it) but must not read as a success: say so on stderr.
203
+ { const heldId = data && data.pending && (data.approvalId || (data._approval && data._approval.id)); if (heldId) console.error(`HELD, not run: approval ${heldId}`); }
202
204
 
203
205
  if (program.opts().output === 'json') {
204
206
  console.log(JSON.stringify(shown, null, 2));
@@ -225,6 +227,34 @@ export function registerToolCommands(program, ctx) {
225
227
  });
226
228
  printSuccess('Tool installed.');
227
229
  });
230
+
231
+ // CLI-COV 2026-11-05: `tool list` shows the org's own tool definitions, never catalog installations, so
232
+ // an install had no CLI read-back and no undo. These read and soft-disable the tool_installations row.
233
+ tool
234
+ .command('installed')
235
+ .description('List catalog tools installed in this organization (what `tool install` added)')
236
+ .action(async () => {
237
+ auth.requireAuth();
238
+ const ep = resolveEndpoint('tools.installed');
239
+ const data = await client.get(ep.path, { service: ep.service });
240
+ formatOutput({
241
+ columns: ['tool_id', 'name', 'category', 'enabled', 'installed_at'],
242
+ headers: ['Tool ID', 'Name', 'Category', 'Enabled', 'Installed'],
243
+ rows: (data && data.installations) || [],
244
+ }, { format: program.opts().output });
245
+ });
246
+
247
+ tool
248
+ .command('uninstall')
249
+ .description('Disable a catalog tool installed with `tool install` (soft: the row is kept, marked disabled)')
250
+ .argument('<toolId>', 'catalog tool_id, as `tool installed` lists it')
251
+ .action(async (toolId) => {
252
+ auth.requireAuth();
253
+ const ep = resolveEndpoint('tools.uninstall-from-scope', { toolId });
254
+ const data = await client.delete(ep.path, { service: ep.service });
255
+ if (data && data.removed) printSuccess(`Tool ${toolId} uninstalled (disabled for this organization).`);
256
+ else console.log(`Tool ${toolId} was not installed in this organization; nothing changed.`);
257
+ });
228
258
  }
229
259
 
230
260
  /**
@@ -4,11 +4,13 @@
4
4
  * quox vault <subcommand> [options]
5
5
  */
6
6
 
7
+ import { withStepUp } from '../core/stepUpPrompt.js';
7
8
  import chalk from 'chalk';
8
9
  import Table from 'cli-table3';
9
10
  import ora from 'ora';
10
11
  import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
11
12
  import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
13
+ import { announce, announceOnFail } from '../core/announce.js';
12
14
  import { resolveOrgId } from './helpers.js';
13
15
  import { resolveEndpoint } from '../api/endpoints.js';
14
16
 
@@ -177,7 +179,7 @@ export function makeVaultGetAction({ client, auth }, program) {
177
179
  }
178
180
 
179
181
  export function makeVaultCreateAction({ client, auth }, program) {
180
- return async (options) => {
182
+ return announceOnFail('vault', (options) => `create credential ${options?.label || ''}`.trim(), async (options) => {
181
183
  auth.requireAuth();
182
184
  const orgId = resolveOrgId(auth, program);
183
185
 
@@ -397,9 +399,10 @@ export function makeVaultCreateAction({ client, auth }, program) {
397
399
  printInfo(`note: not set as default — run "quox vault set-default ${result.id}" to make it the active ${result.integration_type || integrationType} credential`);
398
400
  }
399
401
 
402
+ announce('vault', `credential created ${result.label || ''}`.trim(), { result: `${result.id} · ${result.integration_type || integrationType}` });
400
403
  formatDetail(result, { format: program.opts().output });
401
404
  return result;
402
- };
405
+ });
403
406
  }
404
407
 
405
408
  export function makeVaultUpdateAction({ client, auth }, program) {
@@ -1305,7 +1308,7 @@ export function registerVaultCommands(program, ctx) {
1305
1308
  throw new Error('--duration must be a positive number of minutes.');
1306
1309
  }
1307
1310
  const ep = resolveEndpoint('vault.leases.approve', { orgId, leaseId });
1308
- const result = await client.post(ep.path, { service: ep.service, body: { duration_minutes: duration } });
1311
+ const result = await withStepUp((headers) => client.post(ep.path, { service: ep.service, headers, body: { duration_minutes: duration } }), { client, orgId, io: ctx.stepUpIO, what: 'This lease approval' });
1309
1312
  printEvent(events.updated('quox', 'credential lease', leaseId, `approved for ${duration}m`));
1310
1313
  formatDetail(result, { format: program.opts().output });
1311
1314
  });
@@ -19,7 +19,7 @@ import { resolveOrgId } from './helpers.js';
19
19
  import { formatOutput, printInfo, printWarning } from '../core/output.js';
20
20
  import {
21
21
  VM_ACTIONS, VmPendingError, pendingHint, makeExec, makePoolRest,
22
- listVms, vmInfo, vmDoctor, doctorLines, vmAction, vmSnapshot, vmRehearse, waitRehearsal, poolStatus, poolLease, poolDone, poolConfig, poolHostKeyLines,
22
+ listVms, vmListRows, vmInfo, vmDoctor, doctorLines, vmAction, vmSnapshot, vmRehearse, waitRehearsal, poolStatus, poolLease, poolDone, poolConfig, poolHostKeyLines,
23
23
  readPubkey, textOf, payloadOf,
24
24
  } from '../core/vm.js';
25
25
 
@@ -92,7 +92,7 @@ export function registerVmCommands(program, ctx) {
92
92
  const vms = await listVms(exec, { node: options.node, type: options.type });
93
93
  if (json()) { console.log(JSON.stringify(vms.map((v) => v.raw), null, 2)); return; }
94
94
  formatOutput(
95
- { columns: ['vmid', 'name', 'node', 'type', 'status'], rows: vms.map(({ raw, ...r }) => r) },
95
+ { columns: ['vmid', 'name', 'node', 'type', 'status', 'tags'], rows: vmListRows(vms) },
96
96
  { emptyMessage: 'No VMs found.' },
97
97
  );
98
98
  }));
@@ -3,14 +3,17 @@ import { createInterface } from 'node:readline';
3
3
  import { resolveEndpoint } from '../api/endpoints.js';
4
4
  import { resolveOrgId } from './helpers.js';
5
5
  import { runWalkthrough } from '../core/walkthrough.js';
6
+ import { otherOrgsWaiting, crossOrgLines, clientForTarget } from '../core/crossOrgApprovals.js';
6
7
 
7
- export function attachWalkthrough(parent, { client, auth }, program) {
8
+ export function attachWalkthrough(parent, { client: baseClient, auth, clientForOrg }, program) {
8
9
  parent
9
10
  .command('walkthrough')
10
11
  .description('Walk through the questions waiting for you one at a time: what it does, why, my advice, then your choice')
11
12
  .action(async () => {
12
13
  auth.requireAuth();
13
14
  const orgId = resolveOrgId(auth, program);
15
+ const sessionOrg = auth.getOrgId();
16
+ const client = await clientForTarget({ client: baseClient, clientForOrg, sessionOrgId: sessionOrg, orgId });
14
17
  const lp = resolveEndpoint('inbox.list', { orgId });
15
18
  const data = await client.get(lp.path, { service: lp.service, query: { status: 'open', limit: 200 } });
16
19
  const items = (data && (data.items || data)) || [];
@@ -24,9 +27,18 @@ export function attachWalkthrough(parent, { client, auth }, program) {
24
27
  readLine, write: (l) => console.log(l),
25
28
  answer: async (it, key, note) => {
26
29
  const ep = resolveEndpoint('inbox.respond', { orgId, itemId: it.id });
27
- await client.post(ep.path, { service: ep.service, body: { action: 'answer', option: key, ...(note ? { comment: String(note).slice(0, 4000) } : {}) } });
30
+ const comment = note ? { comment: String(note).slice(0, 4000) } : {};
31
+ if (it.__approval) { // a policy-held approval: the same respond path as `quox approvals approve|deny`
32
+ // No PIN prompt here (the walk owns stdin): a server step-up refusal leaves it open, `quox approvals approve <id>` takes the PIN.
33
+ await client.post(ep.path, { service: ep.service, body: { action: key === 'y' ? 'approve' : 'reject', ...comment } });
34
+ return;
35
+ }
36
+ await client.post(ep.path, { service: ep.service, body: { action: 'answer', option: key, ...comment } });
28
37
  },
29
38
  });
30
39
  } finally { rl.close(); }
40
+ // Approvals waiting in the user's OTHER orgs: a count and the command, never the cards.
41
+ const more = crossOrgLines(await otherOrgsWaiting({ client: baseClient, currentOrgId: orgId }), 'walkthrough');
42
+ for (const l of more) console.log(l);
31
43
  });
32
44
  }
@@ -52,6 +52,14 @@ export async function probeService(url, fetchImpl = fetch, timeoutMs = 8000) {
52
52
  }
53
53
  }
54
54
 
55
+ /** Parse --interval: honour it down to `min`, report when it was raised. */
56
+ export function clampInterval(raw, min, def) {
57
+ const n = parseInt(raw, 10);
58
+ if (!Number.isFinite(n) || n <= 0) return { seconds: def, clamped: false };
59
+ if (n < min) return { seconds: min, clamped: true };
60
+ return { seconds: n, clamped: false };
61
+ }
62
+
55
63
  export function registerWatchCommands(program, { client, auth, config }) {
56
64
  const watch = program
57
65
  .command('watch')
@@ -126,7 +134,8 @@ export function registerWatchCommands(program, { client, auth, config }) {
126
134
  .description('Live service health dashboard')
127
135
  .option('--interval <seconds>', 'Refresh interval', '10')
128
136
  .action(async (opts) => {
129
- const interval = Math.max(5, parseInt(opts.interval, 10) || 10) * 1000;
137
+ const iv = clampInterval(opts.interval, 1, 10);
138
+ const interval = iv.seconds * 1000;
130
139
  const services = [
131
140
  { name: 'auth', port: 3101, path: '/health' },
132
141
  { name: 'collector', port: 9848, path: '/health' },
@@ -157,7 +166,7 @@ export function registerWatchCommands(program, { client, auth, config }) {
157
166
  else if (r.status === 'timeout') console.log(` ${chalk.yellow('●')} ${r.name.padEnd(14)} ${chalk.yellow('slow (no answer in ' + Math.round(r.latency_ms / 1000) + 's)')}`);
158
167
  else console.log(` ${chalk.red('●')} ${r.name.padEnd(14)} ${chalk.red('down')} ${chalk.dim(r.reason || '')}`);
159
168
  }
160
- console.log(chalk.dim('\n refreshing every ' + (interval / 1000) + 's'));
169
+ console.log(chalk.dim('\n refreshing every ' + (interval / 1000) + 's' + (iv.clamped ? ' (minimum is 1s)' : '')));
161
170
  };
162
171
 
163
172
  await tick();