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,155 @@
1
+ /**
2
+ * Evidence commands (DEVFLEET EV-1): `quox evidence status` shows size, rows, time span, growth, archive and
3
+ * backup for the AEE, WARD and VOLT stores. Read-only; the collector route is instance admin only.
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { formatOutput } from '../core/output.js';
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+ import fs from 'node:fs';
10
+ import { exportConversationBundle, newestConversationId } from '../evidence/bundleExport.js';
11
+ import { verifyEvidenceBundle } from '../evidence/bundleVerify.js';
12
+
13
+ const NA = 'n/a';
14
+
15
+ // verify_state from auth, said once: 'verified', 'never verified', 'verify failed', 'verify n/a'.
16
+ function verifyLabel(v) {
17
+ if (v === null || v === undefined || v === '') return 'verify n/a';
18
+ if (v === 'verified') return 'verified';
19
+ if (v === 'failed') return 'verify failed';
20
+ if (v === 'never_verified') return 'never verified';
21
+ return `verify ${String(v).replace(/_/g, ' ')}`;
22
+ }
23
+
24
+ function bytes(n) {
25
+ if (n === null || n === undefined) return NA;
26
+ if (n < 1024) return `${n} B`;
27
+ const units = ['KB', 'MB', 'GB', 'TB'];
28
+ let v = n; let i = -1;
29
+ do { v /= 1024; i++; } while (v >= 1024 && i < units.length - 1);
30
+ return `${v.toFixed(1)} ${units[i]}`;
31
+ }
32
+
33
+ const val = (v) => (v === null || v === undefined ? NA : String(v));
34
+ const day = (iso) => (iso ? String(iso).slice(0, 10) : NA);
35
+
36
+ function archiveCell(a) {
37
+ if (a === null || a === undefined) return NA;
38
+ if (typeof a === 'string') return a;
39
+ return `${a.anchors} anchors${a.newest_archived_at ? `, newest ${day(a.newest_archived_at)}` : ''}`;
40
+ }
41
+
42
+ function diskCell(d) {
43
+ if (!d) return NA;
44
+ return `${bytes(d.free_bytes)} free of ${bytes(d.total_bytes)} (${d.mount})`;
45
+ }
46
+
47
+ export function registerEvidenceCommands(program, ctx) {
48
+ const { client, auth } = ctx;
49
+
50
+ const evidence = program
51
+ .command('evidence')
52
+ .description('Evidence stores: AEE, WARD, VOLT');
53
+
54
+ evidence
55
+ .command('status')
56
+ .description('Evidence stores (AEE, WARD, VOLT): size, rows, time span, growth, archive and backup, read-only. Instance admin only')
57
+ .addHelpText('after', '\nInstance admin only: an org owner or admin who is not an instance admin gets a 403 (exit 4).')
58
+ .action(async () => {
59
+ auth.requireAuth();
60
+ const ep = resolveEndpoint('evidence.status');
61
+ const data = await client.get(ep.path, { service: ep.service });
62
+ if (!data || !Array.isArray(data.stores)) {
63
+ throw new Error(`Unexpected evidence status response${data && data.error ? `: ${data.error}` : ''}`);
64
+ }
65
+ const format = program.opts().output;
66
+ if (format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
67
+
68
+ const rows = data.stores.map((s) => ({
69
+ store: s.name,
70
+ size: bytes(s.bytes),
71
+ wal: bytes(s.wal_bytes),
72
+ rows: val(s.rows),
73
+ oldest: day(s.oldest),
74
+ newest: day(s.newest),
75
+ last7d: s.per_day && s.per_day.last_7d ? `${s.per_day.last_7d.per_day}/day` : NA,
76
+ archive: archiveCell(s.archive),
77
+ disk: diskCell(s.disk),
78
+ }));
79
+ formatOutput({
80
+ columns: ['store', 'size', 'wal', 'rows', 'oldest', 'newest', 'last7d', 'archive', 'disk'],
81
+ headers: ['Store', 'Size', 'WAL', 'Rows', 'Oldest', 'Newest', 'Last 7d', 'Archive', 'Disk'],
82
+ rows,
83
+ }, { format, full: true });
84
+
85
+ const b = data.backup || {};
86
+ if (b.last) {
87
+ const l = b.last;
88
+ const when = l.ts ? `${String(l.ts).slice(0, 10)} ${String(l.ts).slice(11, 16)} UTC` : NA;
89
+ const parts = [
90
+ b.age_hours === null || b.age_hours === undefined ? 'age n/a' : `${b.age_hours} h ago`,
91
+ l.count === null || l.count === undefined ? 'count n/a' : `${l.count} backups`,
92
+ (b.scope && b.scope.ts) || 'all orgs',
93
+ ];
94
+ const line = `Last backup ${when} (${parts.join(', ')})${b.stale ? ' STALE' : ''}`;
95
+ console.log(b.stale ? chalk.yellow(line) : line);
96
+ // The newest backup can belong to any org; health and verify state are the caller's org only.
97
+ console.log(`Backup health (${(b.scope && b.scope.health) || 'your org'}): ${val(l.status)}, ${verifyLabel(l.verified)}`);
98
+ if (l.last_failed) console.log(` last failure: ${val(l.last_failed.error)} (${l.last_failed.at ? String(l.last_failed.at).slice(0, 16).replace('T', ' ') + ' UTC' : NA})`);
99
+ if (b.verify_error) console.log(` ${b.verify_error}`);
100
+ } else {
101
+ console.log(`Backup: ${NA}`);
102
+ }
103
+
104
+ const errors = [];
105
+ for (const s of data.stores) for (const e of s.errors || []) errors.push(`${s.name}: ${e}`);
106
+ if (b.error) errors.push(`backup: ${b.error}`);
107
+ if (errors.length) {
108
+ console.log(chalk.yellow('Errors'));
109
+ for (const e of errors) console.log(` ${e}`);
110
+ }
111
+ });
112
+ evidence
113
+ .command('bundle')
114
+ .description('Export a signed, offline-verifiable evidence bundle (zip) of every AEE envelope, WARD receipt and VOLT run for one conversation')
115
+ .argument('[conversation]', 'conversation id (the corr); default: your newest chat conversation')
116
+ .option('--out <file.zip>', 'where to write it (default ~/quox-evidence/<conversation>-<timestamp>.zip)')
117
+ .option('--json', 'print the result as JSON')
118
+ .action(async (conversation, options) => {
119
+ auth.requireAuth();
120
+ const corr = conversation || await newestConversationId(client);
121
+ if (!corr) throw new Error('No conversation to export: pass a conversation id (`quox conversation list`).');
122
+ const r = await exportConversationBundle({ client, corr, out: options.out });
123
+ const failed = r.verify.checks.filter((c) => !c.ok);
124
+ if (options.json || program.opts().output === 'json') {
125
+ console.log(JSON.stringify({ path: r.path, conversation: r.corr, bytes: r.bytes, envelopes: r.envelopes, receipts: r.receipts, verified: r.verify.ok, failed_checks: failed }, null, 2));
126
+ } else {
127
+ console.log(`${r.verify.ok ? chalk.green('Evidence bundle written') : chalk.red('Evidence bundle written but FAILED verification')}: ${r.path}`);
128
+ console.log(` conversation ${r.corr} | ${r.envelopes} envelopes | ${r.receipts} WARD receipts | ${r.bytes} bytes`);
129
+ console.log(` verify offline: quox evidence verify ${r.path}`);
130
+ for (const c of failed) console.log(chalk.red(` FAILED ${c.name}: ${c.detail || ''}`));
131
+ }
132
+ if (!r.verify.ok) process.exitCode = 1;
133
+ });
134
+
135
+ evidence
136
+ .command('verify')
137
+ .description('Verify an evidence bundle zip offline: file hashes, envelope list hash, counts and the Ed25519 signature (no network)')
138
+ .argument('<file.zip>', 'a bundle from `quox evidence bundle`')
139
+ .option('--json', 'print the result as JSON')
140
+ .action(async (file, options) => {
141
+ let bytes;
142
+ try { bytes = fs.readFileSync(file); } catch (e) { throw new Error(`Cannot read ${file}: ${e.message}`); }
143
+ const v = await verifyEvidenceBundle(bytes);
144
+ if (options.json || program.opts().output === 'json') {
145
+ console.log(JSON.stringify({ file, verified: v.ok, checks: v.checks, counts: v.manifest?.counts || null, conversation: v.manifest?.corr || null, key_id: v.signature?.key_id || null }, null, 2));
146
+ } else {
147
+ for (const c of v.checks) console.log(` ${c.ok ? chalk.green('PASS') : chalk.red('FAIL')} ${c.name}${c.detail ? ` (${c.detail})` : ''}`);
148
+ const m = v.manifest;
149
+ console.log(v.ok
150
+ ? chalk.green(`VERIFIED: ${m.counts.envelopes} envelopes, ${m.counts.ward_receipts} WARD receipts, conversation ${m.corr}, signed by ${v.signature.key_id}`)
151
+ : chalk.red('NOT VERIFIED: this bundle was altered or is not a Quox evidence bundle.'));
152
+ }
153
+ if (!v.ok) process.exitCode = 1;
154
+ });
155
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * quox ext add|remove|list <pkg>
3
+ *
4
+ * The listing behind quoxon's extension host (cli/lib/quoxon/extensions.js, contract EXTENSIONS.md). It edits
5
+ * ~/.quox/extensions.json only; it does not install npm packages (npm i -g <pkg> does that). Nothing loads unless
6
+ * it is both installed and listed.
7
+ */
8
+ import { readExtList, extAdd, extRemove, extListPath } from '../quoxon/extensions.js';
9
+
10
+ export function registerExtCommands(program) {
11
+ const ext = program.command('ext').description('Optional quoxon extensions: list, add or remove one (npm i -g <pkg> installs it)');
12
+ const out = (r) => { console.log(r.text); if (!r.ok) process.exitCode = 1; };
13
+ ext.command('list').description('Extensions listed in ~/.quox/extensions.json')
14
+ .option('--json', 'print as JSON')
15
+ .action((opts) => {
16
+ const list = readExtList();
17
+ if (opts.json || program.opts().output === 'json') { console.log(JSON.stringify({ file: extListPath(), extensions: list }, null, 2)); return; }
18
+ if (!list.length) { console.log('No extensions listed. quox ext add <pkg>'); return; }
19
+ for (const x of list) console.log(`${x.name} ${x.enabled ? 'on' : 'off'}`);
20
+ });
21
+ ext.command('add').description('List an extension so quoxon loads it once installed').argument('<pkg>').action((pkg) => out(extAdd(pkg)));
22
+ ext.command('remove').description('Stop listing an extension').argument('<pkg>').action((pkg) => out(extRemove(pkg)));
23
+ }
@@ -595,6 +595,31 @@ export function registerFleetCommands(program, ctx) {
595
595
  }, { format: program.opts().output, emptyMessage: data && data.bastion === false ? 'No agents enrolled yet.' : 'No hosts in the Bastion registry.' });
596
596
  });
597
597
 
598
+ fleet
599
+ .command('ping [host]')
600
+ .description('Refresh Bastion host state by pinging every host (or one); org admin, writes a quox.fleet.ping evidence envelope')
601
+ .action(async (host) => {
602
+ auth.requireAuth();
603
+ const ep = resolveEndpoint('v1.fleet.ping');
604
+ const data = await client.post(ep.path, { service: ep.service, body: host ? { host } : {}, timeout: 60000 });
605
+ const results = data.results || [];
606
+ const summary = data.summary || { requested: results.length, reachable: results.filter(r => r.reachable).length, unreachable: results.filter(r => !r.reachable).length };
607
+ if (program.opts().output === 'json') {
608
+ formatDetail({ success: data.success !== false, results, summary }, { format: 'json' });
609
+ return;
610
+ }
611
+ formatOutput({
612
+ columns: ['host_id', 'reachable', 'latency'],
613
+ headers: ['Host', 'Reachable', 'Latency'],
614
+ rows: results.map(r => ({
615
+ host_id: r.host_id,
616
+ reachable: r.reachable ? 'yes' : (r.error ? `no (${r.error})` : 'no'),
617
+ latency: typeof r.latency_ms === 'number' ? `${r.latency_ms}ms` : '-',
618
+ })),
619
+ }, { format: program.opts().output, emptyMessage: 'No hosts answered the ping.' });
620
+ printInfo(`${summary.reachable} of ${summary.requested} reachable, ${summary.unreachable} unreachable`);
621
+ });
622
+
598
623
  fleet
599
624
  .command('gpu')
600
625
  .description('List the durable GPU fleet')
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { formatDetail, printInfo, printWarning } from '../core/output.js';
25
- import { events, line as glyphLine, printEvent } from '../core/quoxglyph.js';
25
+ import { events, gripeFiledLine, line as glyphLine, printEvent } from '../core/quoxglyph.js';
26
26
  import { resolveEndpoint } from '../api/endpoints.js';
27
27
  import { resolveOrgId } from './helpers.js';
28
28
 
@@ -50,20 +50,28 @@ export function registerGripeCommands(program, { client, auth }) {
50
50
  return;
51
51
  }
52
52
  printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
53
- const data = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
54
-
55
- if (program.opts().output === 'json') {
56
- formatDetail(data, { format: 'json' });
57
- return;
58
- }
59
-
60
- const r = await settleGripe({ client, data, complaint, options, orgId });
53
+ // GRIPE-QUEUE: a timeout / network / 5xx saves the gripe locally instead of losing it (/gripes in quox lists it).
54
+ const { gripeQueue, fileOrQueue } = await import('../quoxon/gripeQueue.js');
55
+ let rawData = null;
56
+ const f = await fileOrQueue({
57
+ text: complaint, repo: options.repo || null, queue: gripeQueue(), queueable: !!options.yes,
58
+ run: async (key) => {
59
+ const o = { ...options, idempotencyKey: key, queueRetryable: true };
60
+ rawData = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
61
+ if (program.opts().output === 'json') return { json: true };
62
+ return settleGripe({ client, data: rawData, complaint, options: o, orgId });
63
+ },
64
+ });
65
+ if (!f.ok && f.queued) { printWarning(`${f.saved} (${f.message})`); process.exitCode = 1; return; }
66
+ if (!f.ok) throw new Error(f.message);
67
+ if (f.json) { formatDetail(rawData, { format: 'json' }); return; }
68
+ const r = f;
61
69
  console.log('');
62
70
  if (r.superseded) printInfo(SUPERSEDED_MESSAGE);
63
71
  else console.log(r.response || '(no response)');
64
72
  console.log('');
65
73
  for (const n of r.notes) {
66
- if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url));
74
+ if (n.kind === 'created') { printEvent(gripeFiledLine(n.url, complaint)); console.log(n.url); }
67
75
  else if (n.kind === 'info') printInfo(n.text);
68
76
  else printWarning(n.text);
69
77
  }
@@ -117,7 +125,7 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
117
125
  const notes = [];
118
126
  let url = null;
119
127
  const det = async () => {
120
- const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true });
128
+ const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true, throwRetryable: !!options.queueRetryable });
121
129
  notes.push(...r.notes); if (r.url) url = r.url;
122
130
  return r;
123
131
  };
@@ -181,7 +189,7 @@ async function promptConfirmPublic(repo) {
181
189
  } finally { rl.close(); }
182
190
  }
183
191
 
184
- export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null }) {
192
+ export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null, throwRetryable = false }) {
185
193
  const notes = [];
186
194
  const say = (n) => { notes.push(n); if (quiet) return; if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url)); else if (n.kind === 'info') printInfo(n.text); else printWarning(n.text); };
187
195
  const repoArg = options.repo || 'quoxai/quox';
@@ -201,9 +209,10 @@ export async function fileDeterministically({ client, complaint, options, orgId,
201
209
  owner,
202
210
  repo,
203
211
  title,
204
- body: `## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
212
+ body: `${options.idempotencyKey ? `<!-- quox-gripe-key: ${options.idempotencyKey} -->\n` : ''}## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
205
213
  labels: [options.label || 'fleet/qa'],
206
214
  ...(orgId ? { org_id: orgId } : {}),
215
+ ...(options.idempotencyKey ? { idempotency_key: options.idempotencyKey } : {}),
207
216
  ...(confirmed ? { confirm_public: true } : {}),
208
217
  },
209
218
  });
@@ -216,6 +225,8 @@ export async function fileDeterministically({ client, complaint, options, orgId,
216
225
  say({ kind: 'warn', text: 'Deterministic file returned no issue URL; treat as NOT filed until you can see the issue.' });
217
226
  return { filed: false, notes };
218
227
  } catch (err) {
228
+ // GRIPE-QUEUE: a timeout / network / 5xx is "Quox did not answer", not a refusal; the caller queues it.
229
+ if (throwRetryable && (err?.status == null || Number(err.status) >= 500)) throw err;
219
230
  say({ kind: 'warn', text: `Not filed: the GRIPE run did not file and the deterministic fallback failed too (${err.message}).` });
220
231
  return { filed: false, error: err.message, notes };
221
232
  }
@@ -4,6 +4,7 @@
4
4
  * quox hat list
5
5
  * quox hat show <id|key> [--rev <n>]
6
6
  * quox hat reset <id|key> --yes (audited reset-to-template, built-ins)
7
+ * quox hat upgrade <id|key> [--dry-run|--yes] (3-way merge of an org-edited built-in onto the current template)
7
8
  * quox hat create <key> --name "<Name>" [--from <hat>] [--agents a,b] [--tools x,y] [--mode m] [--offer o]
8
9
  * quox hat edit <hat> [--name N] [--add-agents|--remove-agents|--agents] [--add-tools|--remove-tools|--tools] [--mode m]
9
10
  * quox hat delete <hat> --yes (custom hats only)
@@ -42,6 +43,12 @@ export function templateLabel(hat) {
42
43
  const latestAuthor = Array.isArray(hat.revisions) && hat.revisions[0] ? hat.revisions[0].created_by : null;
43
44
  const customised = typeof latestAuthor === 'string' && !latestAuthor.startsWith('template:');
44
45
  if (latest && cur && cur < latest) return `v${cur} (template v${latest} available)`;
46
+ // Merge-upgrade: the last applied 3-way merge (org edits carried onto the new template).
47
+ const u = hat.last_upgrade;
48
+ if (u && u.to_template_version === cur) {
49
+ const k = Number(u.org_changes_kept) || 0;
50
+ return `v${cur}, upgraded from v${u.from_template_version}, ${k} org change${k === 1 ? '' : 's'} kept`;
51
+ }
45
52
  return customised ? `v${cur || '?'} (edited)` : `v${cur || '?'}`;
46
53
  }
47
54
 
@@ -89,6 +96,30 @@ export async function findUnknownAgents(client, orgId, ids) {
89
96
 
90
97
  function isBuiltin(h) { return !!h?.template_key; }
91
98
 
99
+ const fmtItem = (e) => (e.item !== undefined ? `${e.path}: ${e.item}` : e.path);
100
+
101
+ /** Human preview of a merge upgrade (exported for reuse by other surfaces). */
102
+ export function renderUpgradePreview(key, p) {
103
+ const out = [];
104
+ out.push(chalk.bold(`Hat ${key}: template v${p.from_template_version} -> v${p.to_template_version}`) + chalk.dim(` (from rev ${p.from_revision})`));
105
+ out.push(`Tools added: ${(p.tools_added || []).join(', ') || 'none'}`);
106
+ out.push(`Tools removed: ${(p.tools_removed || []).join(', ') || 'none'}`);
107
+ const changed = (p.template_changed || []).map(c => c.path);
108
+ if (changed.length) out.push(`Template changed: ${changed.join(', ')}`);
109
+ out.push(chalk.bold(`Org changes kept (${(p.org_changes || []).length}):`));
110
+ for (const c of p.org_changes || []) out.push(` ${c.change.padEnd(7)} ${fmtItem(c)}`);
111
+ if (!(p.org_changes || []).length) out.push(' none');
112
+ out.push(chalk.bold(`Conflicts, org choice kept (${(p.conflicts || []).length}):`));
113
+ for (const c of p.conflicts || []) out.push(` ${c.path}${c.reason ? ` (${c.reason})` : ''}`);
114
+ if (!(p.conflicts || []).length) out.push(' none');
115
+ out.push(chalk.bold(`Held, would widen authorization (${(p.held || []).length}):`));
116
+ for (const c of p.held || []) out.push(` ${c.path} ${(c.fields || []).join(', ')}`);
117
+ if (!(p.held || []).length) out.push(' none');
118
+ out.push(`Valid: ${p.valid ? 'yes' : `NO: ${(p.errors || []).join('; ')}`}`);
119
+ out.push(`Digest: ${p.from_digest || '-'} -> ${p.digest}`);
120
+ return out;
121
+ }
122
+
92
123
  export function registerHatCommands(program, ctx) {
93
124
  const { client, auth } = ctx;
94
125
 
@@ -234,6 +265,51 @@ export function registerHatCommands(program, ctx) {
234
265
  if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
235
266
  console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
236
267
  });
268
+ // ── hat upgrade ─────────────────────────────────────────────────────────────
269
+ hat
270
+ .command('upgrade <hat>')
271
+ .description('Carry an org-edited built-in hat onto the current template (3-way merge: org changes kept, conflicts keep the org choice, widening authz changes held); previews unless --yes')
272
+ .option('--dry-run', 'preview the merge only (writes nothing); the default without --yes')
273
+ .option('--yes', 'apply the previewed merge as a new system-authored revision (audited)')
274
+ .action(async (ref, options) => {
275
+ auth.requireAuth();
276
+ if (options.dryRun && options.yes) throw new Error('Pass either --dry-run (preview) or --yes (apply), not both.');
277
+ const orgId = resolveOrgId(auth, program);
278
+ const list = await fetchList(client, orgId);
279
+ const found = findHat(list?.hats, ref);
280
+ if (!found) {
281
+ const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
282
+ throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
283
+ }
284
+ const pep = resolveEndpoint('hat.upgrade.preview', { orgId, hatId: found.id });
285
+ let data;
286
+ try {
287
+ data = await client.get(pep.path, { service: pep.service });
288
+ } catch (err) {
289
+ const code = err?.response?.data?.error || err?.data?.error || err?.body?.error;
290
+ if (code === 'up_to_date') { console.log(` Ⓠ QUOX │ hat ${found.key} upgrade │ already on template v${found.template_version} ✔`); return; }
291
+ throw err;
292
+ }
293
+ const p = data?.preview;
294
+ if (!p) throw new Error(`Upgrade preview for '${ref}' returned nothing`);
295
+ const json = program.opts().output === 'json';
296
+
297
+ if (!options.yes) {
298
+ if (json) { console.log(JSON.stringify({ preview: p, applied: false }, null, 2)); return; }
299
+ console.log(renderUpgradePreview(found.key, p).join('\n'));
300
+ console.log('');
301
+ console.log(` dry run: nothing written. Apply with: quox hat upgrade ${found.key} --yes`);
302
+ return;
303
+ }
304
+ if (!p.valid) throw new Error(`The merged hat does not validate: ${(p.errors || []).join('; ')}`);
305
+ const aep = resolveEndpoint('hat.upgrade', { orgId, hatId: found.id });
306
+ const res = await client.post(aep.path, { service: aep.service, body: { expected_revision: p.from_revision } });
307
+ const h = res?.hat;
308
+ if (!h) throw new Error(`Upgrade of '${ref}' returned no hat`);
309
+ if (json) { console.log(JSON.stringify({ hat: h, preview: res.preview || p, applied: true }, null, 2)); return; }
310
+ console.log(` Ⓠ QUOX │ hat ${h.key} upgraded │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
311
+ });
312
+
237
313
  // ── hat create (custom) ───────────────────────────────────────────────────────
238
314
  hat
239
315
  .command('create <key>')
@@ -0,0 +1,68 @@
1
+ /**
2
+ * quox image generate "<prompt>" [--out file] [--provider gemini|openai] [--aspect-ratio 16:9] [--count n]
3
+ *
4
+ * CLI verb first for image generation. It runs the SAME governed tool the chat uses (gemini_image_generate via
5
+ * POST /api/v1/tools/execute), so the provider fallback, the receipt/witnessing and the plain all-fail message are
6
+ * the collector's, not re-implemented here. Providers tried: Gemini, then OpenAI, using only what the org already
7
+ * connected (`quox provider list`). Nothing here adds a provider or a key.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { printSuccess } from '../core/output.js';
12
+ import { executeGovernedTool } from './tool.js';
13
+
14
+ const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' };
15
+
16
+ export function registerImageCommands(program, ctx) {
17
+ const { client, auth } = ctx;
18
+ const image = program.command('image').description('Generate images with the providers your org has connected');
19
+
20
+ image
21
+ .command('generate')
22
+ .description('Generate an image from a prompt (tries Gemini, then OpenAI; says which made it)')
23
+ .argument('<prompt>', 'what to draw')
24
+ .option('--out <file>', 'where to save the image (default ./quox-image-<time>.png)')
25
+ .option('--provider <name>', 'use only this provider: gemini or openai (no fall-through)')
26
+ .option('--aspect-ratio <ratio>', 'e.g. 16:9 for a hero, 1:1 for social, 9:16 for stories')
27
+ .option('--count <n>', 'how many images (1-4)', '1')
28
+ .action(async (prompt, options) => {
29
+ const input = { prompt };
30
+ if (options.provider) input.provider = options.provider;
31
+ if (options.aspectRatio) input.aspect_ratio = options.aspectRatio;
32
+ const n = Number(options.count);
33
+ if (n > 1) input.number_of_images = Math.min(n, 4);
34
+
35
+ const data = await executeGovernedTool({ client, auth, program }, 'gemini_image_generate', input, { reason: 'quox image generate' });
36
+ const r = (data && data.result && typeof data.result === 'object') ? data.result : (data || {});
37
+ const failed = (data && data.success === false) || r.success === false;
38
+
39
+ if (program.opts().output === 'json') {
40
+ // Never dump base64 into a terminal or a pipe: report where the files went.
41
+ const out = failed ? r : { ...r, images: undefined };
42
+ if (!failed) out.files = saveImages(r.images, options.out);
43
+ console.log(JSON.stringify({ ...out, ...(failed && !r.error && data.error ? { error: data.error } : {}) }, null, 2));
44
+ if (failed) process.exitCode = 1;
45
+ return;
46
+ }
47
+ if (failed) {
48
+ console.error(r.error || data.error || 'Image generation failed.');
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ const files = saveImages(r.images, options.out);
53
+ printSuccess(`Made by ${r.provider_used || 'gemini'}${r.model_used ? ` (${r.model_used})` : ''}: ${files.join(', ')}`);
54
+ if (r.note) console.log(r.note);
55
+ });
56
+ }
57
+
58
+ function saveImages(images, out) {
59
+ const list = Array.isArray(images) ? images : [];
60
+ const stamp = Date.now();
61
+ return list.map((img, i) => {
62
+ const ext = EXT[img.mime_type] || 'png';
63
+ let file = out || `quox-image-${stamp}.${ext}`;
64
+ if (list.length > 1) { const p = path.parse(file); file = path.join(p.dir, `${p.name}-${i + 1}${p.ext || `.${ext}`}`); }
65
+ fs.writeFileSync(file, Buffer.from(img.base64, 'base64'));
66
+ return file;
67
+ });
68
+ }
@@ -9,6 +9,8 @@ import { attachWalkthrough } from './walkthrough.js';
9
9
  import { getApprovalContext, formatApprovalContext, approvalListColumns } from '../core/approvalContext.js';
10
10
  import { QuoxUserError } from '../core/errors.js';
11
11
  import { resolutionFromItem, alreadyResolvedError } from '../core/inboxResolution.js';
12
+ import { withStepUp } from '../core/stepUpPrompt.js';
13
+ import { announce, announceFailure } from '../core/announce.js';
12
14
  import chalk from 'chalk';
13
15
 
14
16
  export function registerInboxCommands(program, ctx) {
@@ -55,7 +57,7 @@ export function registerInboxCommands(program, ctx) {
55
57
  inbox
56
58
  .command('list')
57
59
  .description('List inbox items')
58
- .option('--status <status>', 'filter: one status, a comma list (resolved,cancelled,expired), or all (includes answered approvals). Default: active only')
60
+ .option('--status <status>', 'filter: one status, a comma list (resolved,cancelled,expired,withdrawn), or all (includes answered approvals). Default: active only')
59
61
  .option('--decision <decision>', 'with resolved/all: approved or rejected')
60
62
  .option('--page <n>', 'page number (newest first)')
61
63
  .option('--priority <priority>', 'filter: low, normal, high, urgent')
@@ -78,7 +80,15 @@ export function registerInboxCommands(program, ctx) {
78
80
  columns: ['id', 'kind', 'priority', 'title', 'source', 'reason', 'status', 'created_at'],
79
81
  headers: ['ID', 'Kind', 'Priority', 'Title', 'Source', 'Reason', 'Status', 'Created'],
80
82
  rows: items,
81
- }, { format: program.opts().output });
83
+ }, {
84
+ format: program.opts().output,
85
+ emptyMessage: 'Nothing waiting for you. Try `--status all` for answered items, or `--awaiting-human` for approvals waiting on a human org-wide.',
86
+ });
87
+ const limit = Number(options.limit);
88
+ if (program.opts().output !== 'json' && items.length > 0 && Number.isFinite(limit) && items.length >= limit) {
89
+ const total = typeof data.total === 'number' ? data.total : null;
90
+ console.log(`showing ${items.length}${total && total > items.length ? ` of ${total}` : ''} · --limit to see more`);
91
+ }
82
92
  });
83
93
 
84
94
  inbox
@@ -116,9 +126,13 @@ export function registerInboxCommands(program, ctx) {
116
126
  auth.requireAuth();
117
127
  const orgId = resolveOrgId(auth, program);
118
128
  const format = program.opts().output;
129
+ // DS-010 / #1335: a step-up sink answers 403 step_up_required; ask the person for their PIN (hidden, TTY only) and retry once.
119
130
  const post = (itemId) => {
120
131
  const ep = resolveEndpoint('inbox.respond', { orgId, itemId });
121
- return client.post(ep.path, { service: ep.service, body: { action: options.action, comment: options.comment } });
132
+ return withStepUp(
133
+ (headers) => client.post(ep.path, { service: ep.service, headers, body: { action: options.action, comment: options.comment } }),
134
+ { client, orgId, io: ctx.stepUpIO, what: 'This approval' },
135
+ );
122
136
  };
123
137
  // Resolve an approvalId to its card (server-side on a current server, else the client scan). When
124
138
  // this profile cannot see the card, still ask the server: it knows WHY (an agent login, another
@@ -161,6 +175,26 @@ export function registerInboxCommands(program, ctx) {
161
175
  } catch { return null; }
162
176
  }
163
177
 
178
+ inbox
179
+ .command('unread')
180
+ .description('Mark inbox items unread again (undo a mark-as-read), for you only')
181
+ .argument('<itemId...>', 'one or more inbox item IDs')
182
+ .action(async (ids) => {
183
+ auth.requireAuth();
184
+ const orgId = resolveOrgId(auth, program);
185
+ let done = 0;
186
+ for (const id of ids) {
187
+ try {
188
+ await client.patch(`${resolveEndpoint('inbox.get', { orgId, itemId: encodeURIComponent(id) }).path}/read`, { service: resolveEndpoint('inbox.get', { orgId, itemId: id }).service, body: { is_read: false } });
189
+ done++;
190
+ } catch (err) {
191
+ announceFailure('inbox', `unread ${id}`, err);
192
+ throw err.status === 404 ? notFound(id, err) : err;
193
+ }
194
+ }
195
+ announce('inbox', `marked ${done} unread`, { result: ids.length === 1 ? ids[0] : `${done} items` });
196
+ });
197
+
164
198
  inbox
165
199
  .command('stats')
166
200
  .description('Show inbox statistics')