quoxcli 0.2.0 → 0.4.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 (442) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/actions.js +15 -0
  5. package/lib/api/endpoints/admin.js +46 -0
  6. package/lib/api/endpoints/agent-schedules.js +26 -0
  7. package/lib/api/endpoints/agents.js +7 -0
  8. package/lib/api/endpoints/assistants.js +12 -0
  9. package/lib/api/endpoints/away.js +15 -0
  10. package/lib/api/endpoints/blog.js +16 -0
  11. package/lib/api/endpoints/chat.js +14 -0
  12. package/lib/api/endpoints/contactsq.js +6 -0
  13. package/lib/api/endpoints/conversations.js +14 -0
  14. package/lib/api/endpoints/devServerRegistry.js +25 -0
  15. package/lib/api/endpoints/engagement.js +26 -0
  16. package/lib/api/endpoints/evidence.js +20 -0
  17. package/lib/api/endpoints/executions.js +13 -0
  18. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  19. package/lib/api/endpoints/files.js +14 -0
  20. package/lib/api/endpoints/hat.js +44 -0
  21. package/lib/api/endpoints/inbox.js +20 -0
  22. package/lib/api/endpoints/index.js +26 -0
  23. package/lib/api/endpoints/loops.js +26 -0
  24. package/lib/api/endpoints/memory.js +19 -0
  25. package/lib/api/endpoints/mind.js +13 -0
  26. package/lib/api/endpoints/mon.js +179 -0
  27. package/lib/api/endpoints/objectives.js +12 -0
  28. package/lib/api/endpoints/plugins.js +18 -0
  29. package/lib/api/endpoints/presence.js +33 -0
  30. package/lib/api/endpoints/quoxplan.js +7 -0
  31. package/lib/api/endpoints/quoxseo.js +12 -0
  32. package/lib/api/endpoints/router.js +17 -0
  33. package/lib/api/endpoints/safety.js +7 -0
  34. package/lib/api/endpoints/server.js +21 -0
  35. package/lib/api/endpoints/setup.js +6 -0
  36. package/lib/api/endpoints/skills.js +20 -0
  37. package/lib/api/endpoints/stepup.js +20 -0
  38. package/lib/api/endpoints/teams.js +2 -2
  39. package/lib/api/endpoints/triggers.js +26 -0
  40. package/lib/api/endpoints/v1.js +120 -0
  41. package/lib/api/endpoints/ward.js +7 -0
  42. package/lib/api/endpoints/workflows.js +7 -0
  43. package/lib/api/endpoints/x.js +8 -0
  44. package/lib/commands/actions.js +131 -0
  45. package/lib/commands/admin.js +16 -0
  46. package/lib/commands/agent.js +170 -27
  47. package/lib/commands/agentAttach.js +140 -13
  48. package/lib/commands/approvals.js +78 -1
  49. package/lib/commands/aside.js +24 -0
  50. package/lib/commands/ask.js +146 -0
  51. package/lib/commands/assistant.js +59 -4
  52. package/lib/commands/auth.js +8 -2
  53. package/lib/commands/backup.js +8 -4
  54. package/lib/commands/blog.js +110 -0
  55. package/lib/commands/brain.js +105 -14
  56. package/lib/commands/build.js +19 -5
  57. package/lib/commands/chat.js +180 -26
  58. package/lib/commands/chatCompanion.js +27 -0
  59. package/lib/commands/chatState.js +45 -0
  60. package/lib/commands/claude.js +35 -38
  61. package/lib/commands/contacts.js +134 -4
  62. package/lib/commands/contactsSendAt.js +21 -0
  63. package/lib/commands/conversation.js +35 -4
  64. package/lib/commands/dashboard.js +43 -0
  65. package/lib/commands/department.js +28 -7
  66. package/lib/commands/devResident.js +15 -3
  67. package/lib/commands/devSupervisor.js +13 -1
  68. package/lib/commands/discord.js +75 -39
  69. package/lib/commands/done.js +98 -28
  70. package/lib/commands/engagement.js +107 -6
  71. package/lib/commands/evidence.js +154 -0
  72. package/lib/commands/ext.js +23 -0
  73. package/lib/commands/fetchAllowlist.js +71 -0
  74. package/lib/commands/file.js +38 -1
  75. package/lib/commands/fleet.js +336 -4
  76. package/lib/commands/governance.js +12 -3
  77. package/lib/commands/gripe.js +44 -13
  78. package/lib/commands/hat.js +254 -9
  79. package/lib/commands/helpers.js +18 -1
  80. package/lib/commands/hyperion.js +58 -0
  81. package/lib/commands/image.js +68 -0
  82. package/lib/commands/inbox.js +74 -23
  83. package/lib/commands/kuma.js +1050 -0
  84. package/lib/commands/kumaSla.js +174 -0
  85. package/lib/commands/licence.js +52 -155
  86. package/lib/commands/loop.js +291 -0
  87. package/lib/commands/lucid.js +10 -4
  88. package/lib/commands/memory.js +85 -8
  89. package/lib/commands/mind.js +71 -0
  90. package/lib/commands/mon.js +69 -2
  91. package/lib/commands/monitoring.js +1 -1
  92. package/lib/commands/motion.js +2 -2
  93. package/lib/commands/nucleus.js +48 -17
  94. package/lib/commands/objective.js +59 -1
  95. package/lib/commands/offload.js +89 -0
  96. package/lib/commands/onboard.js +2 -1
  97. package/lib/commands/org.js +30 -3
  98. package/lib/commands/peek.js +28 -0
  99. package/lib/commands/plan.js +38 -0
  100. package/lib/commands/plugin.js +110 -0
  101. package/lib/commands/presence.js +89 -0
  102. package/lib/commands/provider.js +217 -0
  103. package/lib/commands/qlarity.js +19 -3
  104. package/lib/commands/quoxchat.js +13 -4
  105. package/lib/commands/quoxseo.js +69 -0
  106. package/lib/commands/reach.js +2 -1
  107. package/lib/commands/research.js +79 -0
  108. package/lib/commands/router.js +77 -0
  109. package/lib/commands/rows.js +367 -0
  110. package/lib/commands/run.js +82 -10
  111. package/lib/commands/scope.js +2 -2
  112. package/lib/commands/server.js +405 -0
  113. package/lib/commands/setup.js +33 -7
  114. package/lib/commands/shortcut.js +41 -0
  115. package/lib/commands/skills.js +68 -9
  116. package/lib/commands/slot.js +260 -7
  117. package/lib/commands/status.js +1 -1
  118. package/lib/commands/team.js +3 -1
  119. package/lib/commands/tips.js +23 -0
  120. package/lib/commands/tool.js +133 -45
  121. package/lib/commands/trace.js +12 -7
  122. package/lib/commands/trigger.js +95 -0
  123. package/lib/commands/update.js +46 -0
  124. package/lib/commands/vault.js +31 -8
  125. package/lib/commands/vm.js +237 -0
  126. package/lib/commands/voice.js +34 -3
  127. package/lib/commands/volt.js +45 -3
  128. package/lib/commands/walkthrough.js +32 -0
  129. package/lib/commands/watch.js +73 -21
  130. package/lib/commands/welcome.js +14 -0
  131. package/lib/commands/workflow.js +189 -32
  132. package/lib/commands/x.js +13 -4
  133. package/lib/commands/youtube.js +172 -0
  134. package/lib/core/actingAs.js +2 -2
  135. package/lib/core/actionItems.js +115 -0
  136. package/lib/core/activeSummary.js +186 -0
  137. package/lib/core/agentAlias.js +2 -0
  138. package/lib/core/agentFamilies.js +7 -0
  139. package/lib/core/aside.js +54 -0
  140. package/lib/core/askDecision.js +60 -0
  141. package/lib/core/auth.js +164 -13
  142. package/lib/core/callerSource.js +1 -1
  143. package/lib/core/cliBuild.js +62 -0
  144. package/lib/core/client.js +152 -35
  145. package/lib/core/commandRegistrars.js +130 -0
  146. package/lib/core/config.js +143 -25
  147. package/lib/core/customHats.js +168 -0
  148. package/lib/core/doneEngineFetch.js +8 -0
  149. package/lib/core/errors.js +329 -20
  150. package/lib/core/findingReconcile.js +90 -0
  151. package/lib/core/global-flags.js +105 -5
  152. package/lib/core/governedSession.js +1 -3
  153. package/lib/core/hyperion.js +102 -0
  154. package/lib/core/inboxResolution.js +65 -0
  155. package/lib/core/lazyCommands.js +4 -1
  156. package/lib/core/liveSessionsReport.js +1 -1
  157. package/lib/core/loopGlyph.js +97 -0
  158. package/lib/core/offload.js +533 -0
  159. package/lib/core/org-override.js +1 -1
  160. package/lib/core/orgResolve.js +70 -0
  161. package/lib/core/output.js +11 -11
  162. package/lib/core/peek.js +126 -0
  163. package/lib/core/profileUsers.js +51 -0
  164. package/lib/core/promptCancel.js +18 -0
  165. package/lib/core/repoPath.js +11 -0
  166. package/lib/core/residentSeed.js +3 -1
  167. package/lib/core/rootOptions.js +39 -0
  168. package/lib/core/router.js +168 -0
  169. package/lib/core/selfUpdate.js +188 -0
  170. package/lib/core/sessionHeartbeat.js +12 -8
  171. package/lib/core/sessionsOverview.js +64 -0
  172. package/lib/core/shortcut.js +83 -0
  173. package/lib/core/slotKeeper.js +248 -3
  174. package/lib/core/slotWake.js +120 -0
  175. package/lib/core/supervisorEngine.js +71 -2
  176. package/lib/core/version.js +1 -1
  177. package/lib/core/vm.js +366 -0
  178. package/lib/core/vmTui.js +86 -0
  179. package/lib/core/walkthrough.js +35 -0
  180. package/lib/dragon/companionView.js +34 -0
  181. package/lib/dragon/frames.js +118 -0
  182. package/lib/dragon/jobs.js +123 -0
  183. package/lib/dragon/renderWorker.js +15 -0
  184. package/lib/dragon/select.js +72 -0
  185. package/lib/evidence/bundleExport.js +47 -0
  186. package/lib/evidence/bundleVerify.js +77 -0
  187. package/lib/hyperion/deck.js +29 -0
  188. package/lib/lucid/LucidCard.jsx +68 -0
  189. package/lib/lucid/LucidIntro.jsx +45 -58
  190. package/lib/lucid/cards.js +86 -0
  191. package/lib/lucid/markdown.js +81 -0
  192. package/lib/lucid/motion.js +56 -0
  193. package/lib/quoxon/EXTENSIONS.md +23 -0
  194. package/lib/quoxon/activity.js +14 -2
  195. package/lib/quoxon/aocl.js +117 -0
  196. package/lib/quoxon/api.js +690 -25
  197. package/lib/quoxon/app.js +3586 -250
  198. package/lib/quoxon/approvalFacts.js +96 -0
  199. package/lib/quoxon/approvalSentence.js +44 -0
  200. package/lib/quoxon/approvalsView.js +384 -0
  201. package/lib/quoxon/asideView.js +40 -0
  202. package/lib/quoxon/ask.js +121 -0
  203. package/lib/quoxon/away.js +52 -0
  204. package/lib/quoxon/bang.js +224 -0
  205. package/lib/quoxon/bulkRisk.js +17 -0
  206. package/lib/quoxon/capabilities.js +13 -6
  207. package/lib/quoxon/cells.js +53 -0
  208. package/lib/quoxon/commandShort.js +124 -0
  209. package/lib/quoxon/commands.js +429 -39
  210. package/lib/quoxon/connect.js +210 -23
  211. package/lib/quoxon/connectCard.js +19 -2
  212. package/lib/quoxon/consent.js +86 -0
  213. package/lib/quoxon/copy.js +4 -3
  214. package/lib/quoxon/countdownLine.js +20 -0
  215. package/lib/quoxon/deck.js +30 -0
  216. package/lib/quoxon/demo.autoplay.js +29 -0
  217. package/lib/quoxon/demo.fixtures.js +1 -1
  218. package/lib/quoxon/demo.js +46 -3
  219. package/lib/quoxon/devObserver.js +20 -0
  220. package/lib/quoxon/dividerAocl.js +192 -0
  221. package/lib/quoxon/dividerRule.js +57 -0
  222. package/lib/quoxon/dragonCompanion.js +255 -0
  223. package/lib/quoxon/dragonModes.js +151 -0
  224. package/lib/quoxon/dragonMoments.js +82 -0
  225. package/lib/quoxon/dragonReads.js +68 -0
  226. package/lib/quoxon/dragonView.js +41 -0
  227. package/lib/quoxon/earlyInput.js +40 -0
  228. package/lib/quoxon/echo.js +15 -0
  229. package/lib/quoxon/egg.js +2 -2
  230. package/lib/quoxon/events.js +266 -26
  231. package/lib/quoxon/extensions.js +276 -0
  232. package/lib/quoxon/feedback.js +50 -0
  233. package/lib/quoxon/gallery.js +21 -7
  234. package/lib/quoxon/handoffOffer.js +78 -0
  235. package/lib/quoxon/hatSwitch.js +37 -0
  236. package/lib/quoxon/headless.js +161 -0
  237. package/lib/quoxon/hostUrls.js +41 -0
  238. package/lib/quoxon/inboxView.js +79 -0
  239. package/lib/quoxon/indicator.js +6 -2
  240. package/lib/quoxon/installCli.js +79 -0
  241. package/lib/quoxon/intent.js +12 -1
  242. package/lib/quoxon/keys.js +51 -8
  243. package/lib/quoxon/launcher.js +103 -0
  244. package/lib/quoxon/loopCard.js +456 -0
  245. package/lib/quoxon/loopCards.js +180 -0
  246. package/lib/quoxon/loopStream.js +87 -0
  247. package/lib/quoxon/main.js +101 -160
  248. package/lib/quoxon/markdown.js +45 -21
  249. package/lib/quoxon/markdownSettle.js +19 -0
  250. package/lib/quoxon/matrixDomains.json +46 -0
  251. package/lib/quoxon/mentions.js +90 -0
  252. package/lib/quoxon/mind.js +103 -0
  253. package/lib/quoxon/mode.js +46 -0
  254. package/lib/quoxon/model.js +132 -26
  255. package/lib/quoxon/modelPrefs.js +345 -0
  256. package/lib/quoxon/motion/clock.js +3 -2
  257. package/lib/quoxon/motion/level.js +91 -0
  258. package/lib/quoxon/mouse.js +222 -0
  259. package/lib/quoxon/noai.js +57 -0
  260. package/lib/quoxon/objectiveOffer.js +82 -0
  261. package/lib/quoxon/objects/away.js +72 -0
  262. package/lib/quoxon/objects/fetch.js +17 -5
  263. package/lib/quoxon/objects/fleet.js +149 -0
  264. package/lib/quoxon/objects/health.js +186 -0
  265. package/lib/quoxon/objects/index.js +46 -7
  266. package/lib/quoxon/objects/mirror.js +137 -0
  267. package/lib/quoxon/objects/model.js +33 -0
  268. package/lib/quoxon/objects/peek.js +17 -0
  269. package/lib/quoxon/objects/proof.js +12 -4
  270. package/lib/quoxon/objects/prove.js +130 -0
  271. package/lib/quoxon/objects/replay.js +125 -0
  272. package/lib/quoxon/objects/sitrep.js +58 -0
  273. package/lib/quoxon/objects/view.js +11 -0
  274. package/lib/quoxon/onboarding/copy.js +32 -4
  275. package/lib/quoxon/onboarding/credential.js +17 -4
  276. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  277. package/lib/quoxon/onboarding/failureCause.js +33 -0
  278. package/lib/quoxon/onboarding/finish.js +15 -0
  279. package/lib/quoxon/onboarding/index.js +34 -7
  280. package/lib/quoxon/onboarding/installRunner.js +64 -5
  281. package/lib/quoxon/onboarding/preflight.js +17 -3
  282. package/lib/quoxon/onboarding/probe.js +65 -0
  283. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  284. package/lib/quoxon/onboarding/view.js +76 -19
  285. package/lib/quoxon/orgLabel.js +10 -0
  286. package/lib/quoxon/orgSwitch.js +59 -0
  287. package/lib/quoxon/ownBroadcast.js +22 -0
  288. package/lib/quoxon/palette.js +43 -0
  289. package/lib/quoxon/planSteps.js +51 -0
  290. package/lib/quoxon/print.js +12 -5
  291. package/lib/quoxon/profileSwitch.js +53 -0
  292. package/lib/quoxon/promptHistory.js +41 -0
  293. package/lib/quoxon/providers.js +48 -0
  294. package/lib/quoxon/qart.js +20 -51
  295. package/lib/quoxon/qdragon.js +410 -0
  296. package/lib/quoxon/qdragonFly.js +62 -0
  297. package/lib/quoxon/qdragonScenes.js +282 -0
  298. package/lib/quoxon/qeyes.js +213 -0
  299. package/lib/quoxon/qmotion.js +75 -37
  300. package/lib/quoxon/render.js +740 -124
  301. package/lib/quoxon/resend.js +72 -0
  302. package/lib/quoxon/roster.js +117 -17
  303. package/lib/quoxon/route.js +6 -5
  304. package/lib/quoxon/rows.js +954 -0
  305. package/lib/quoxon/rowsStore.js +80 -0
  306. package/lib/quoxon/screenContext.js +150 -0
  307. package/lib/quoxon/screenFrames.js +254 -0
  308. package/lib/quoxon/serverChip.js +42 -0
  309. package/lib/quoxon/session.js +182 -2
  310. package/lib/quoxon/sessionDead.js +17 -0
  311. package/lib/quoxon/signin.js +125 -0
  312. package/lib/quoxon/slash/agents.js +112 -0
  313. package/lib/quoxon/slash/argSources.js +65 -0
  314. package/lib/quoxon/slash/args.js +65 -0
  315. package/lib/quoxon/slash/automate.js +89 -0
  316. package/lib/quoxon/slash/broadcast.js +140 -0
  317. package/lib/quoxon/slash/broadcastRun.js +108 -0
  318. package/lib/quoxon/slash/card.js +30 -0
  319. package/lib/quoxon/slash/challenge.js +114 -0
  320. package/lib/quoxon/slash/cliCommand.js +94 -0
  321. package/lib/quoxon/slash/cliLeaves.json +1023 -0
  322. package/lib/quoxon/slash/compact.js +1 -1
  323. package/lib/quoxon/slash/confirm.js +30 -0
  324. package/lib/quoxon/slash/crystallize.js +69 -0
  325. package/lib/quoxon/slash/dashboard.js +136 -0
  326. package/lib/quoxon/slash/draft.js +54 -0
  327. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  328. package/lib/quoxon/slash/hatWalk.js +206 -0
  329. package/lib/quoxon/slash/inverses.js +77 -0
  330. package/lib/quoxon/slash/killer.js +24 -0
  331. package/lib/quoxon/slash/killerApi.js +16 -0
  332. package/lib/quoxon/slash/ledger.js +13 -8
  333. package/lib/quoxon/slash/listView.js +3 -1
  334. package/lib/quoxon/slash/modelCmd.js +261 -0
  335. package/lib/quoxon/slash/palette.js +22 -8
  336. package/lib/quoxon/slash/paletteController.js +117 -7
  337. package/lib/quoxon/slash/panels.js +21 -6
  338. package/lib/quoxon/slash/picker.js +20 -2
  339. package/lib/quoxon/slash/qpeCmd.js +35 -0
  340. package/lib/quoxon/slash/restart.js +95 -13
  341. package/lib/quoxon/slash/resume.js +18 -1
  342. package/lib/quoxon/slash/rowCmd.js +157 -0
  343. package/lib/quoxon/slash/rowWalk.js +175 -0
  344. package/lib/quoxon/slash/settings.js +16 -6
  345. package/lib/quoxon/slash/spawn.js +177 -0
  346. package/lib/quoxon/slash/stepUp.js +274 -0
  347. package/lib/quoxon/slash/stepUpCard.js +60 -0
  348. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  349. package/lib/quoxon/slash/todoCmd.js +122 -0
  350. package/lib/quoxon/slash/undoApi.js +38 -0
  351. package/lib/quoxon/slash/undoCmd.js +49 -0
  352. package/lib/quoxon/slash/undoWish.js +15 -0
  353. package/lib/quoxon/slash/watch.js +164 -0
  354. package/lib/quoxon/slash/wish.js +23 -0
  355. package/lib/quoxon/slash/wishCmd.js +32 -0
  356. package/lib/quoxon/slotChat.js +300 -0
  357. package/lib/quoxon/slotFormat.js +195 -0
  358. package/lib/quoxon/slotRun.js +131 -0
  359. package/lib/quoxon/sound/mapping.js +2 -1
  360. package/lib/quoxon/stray.js +22 -0
  361. package/lib/quoxon/suggestCommand.js +90 -0
  362. package/lib/quoxon/term.js +46 -11
  363. package/lib/quoxon/textBuffer.js +176 -2
  364. package/lib/quoxon/theme.js +58 -25
  365. package/lib/quoxon/tips.js +433 -0
  366. package/lib/quoxon/toolCards.js +30 -0
  367. package/lib/quoxon/toolError.js +41 -0
  368. package/lib/quoxon/transcript.js +42 -4
  369. package/lib/quoxon/web/qface.js +18 -10
  370. package/lib/quoxon/welcome.js +1 -1
  371. package/lib/quoxon/whatsnew.js +136 -0
  372. package/lib/quoxon/whatsnew.jsonl +134 -0
  373. package/lib/quoxon/windowOrg.js +123 -0
  374. package/lib/server/devServers.js +137 -0
  375. package/lib/server/diag.js +129 -0
  376. package/lib/server/doctor.js +71 -0
  377. package/lib/server/install.js +149 -0
  378. package/lib/server/pluginEnable.js +161 -0
  379. package/lib/server/skew.js +56 -0
  380. package/lib/server/snapshot.js +131 -0
  381. package/lib/server/status.js +128 -0
  382. package/lib/server/sys.js +65 -0
  383. package/lib/server/uninstall.js +63 -0
  384. package/lib/server/update.js +234 -0
  385. package/lib/toolOutputShape.js +315 -0
  386. package/lib/tui/views/HyperionDeck.jsx +79 -0
  387. package/lib/tui/views/LucidDeck.jsx +209 -217
  388. package/package.json +5 -1
  389. package/vendor/dragonlab/SOURCE +4 -0
  390. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  391. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  392. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  393. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  394. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  395. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  396. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  397. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  398. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  399. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  400. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  401. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  402. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  403. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  404. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  405. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  406. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  407. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  408. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  409. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  410. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  411. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  412. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  413. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  414. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  415. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  416. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  417. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  418. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  419. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  420. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  421. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  422. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  423. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  424. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  425. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  426. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  427. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  428. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  429. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  430. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  431. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  432. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  433. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  434. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  435. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  436. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  437. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  438. package/vendor/dragonlab/lib/power-effects.js +294 -0
  439. package/vendor/dragonlab/lib/power-registry.js +74 -0
  440. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  441. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
  442. package/lib/core/websiteSession.js +0 -148
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { missingReasonWarning } from '../core/callerSource.js';
6
6
  import chalk from 'chalk';
7
+ import tls from 'node:tls';
8
+ import { X509Certificate, createHash } from 'node:crypto';
7
9
  import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
8
10
  import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
9
11
  import { parseJSON } from './helpers.js';
@@ -211,10 +213,166 @@ export function parseSentinel(output) {
211
213
  /** The full install pipe, using install.sh's REAL flags. */
212
214
  export function buildRehomeInstallCommand({ collector, hostId, orgId }) {
213
215
  const base = String(collector).replace(/\/+$/, '');
214
- return `curl -sSL ${base}/install | sudo bash -s --`
216
+ // quox#1223: fail loud, no `curl | bash` (see buildEnrolCommand). TLS verification stays on.
217
+ return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && curl -fsSL ${base}/install -o "$f" && sudo bash "$f"`
215
218
  + ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)}`
216
219
  + ` ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(base)}`
217
- + ` ${INSTALL_SH_FLAGS.org} ${shellSingleQuote(orgId)}`;
220
+ + ` ${INSTALL_SH_FLAGS.org} ${shellSingleQuote(orgId)} )`;
221
+ }
222
+
223
+ // ── fleet enrol (Z2H-AGENTINST) ─────────────────────────────────────────────
224
+
225
+ export const ENROL_OS = ['linux', 'darwin'];
226
+ export const ENROL_ARCH = ['amd64', 'arm64'];
227
+
228
+ /** Exact wording when GET /install answers 503 (no installer shipped). */
229
+ export const ENROL_NO_INSTALLER_MESSAGE =
230
+ 'this install has no agent installer yet (quoxagent binaries not shipped); enrolment is not available';
231
+
232
+ /** Argument validation for `fleet enrol`. Returns a list of errors. */
233
+ export function validateEnrolArgs(hostId, options = {}) {
234
+ const usage = 'Usage: quox fleet enrol <host> [--os linux --arch amd64|arm64] [--run]';
235
+ const errors = [];
236
+ if (!hostId || !String(hostId).trim()) errors.push(`Missing <host>. ${usage}`);
237
+ else if (/[\s'"`$\;|&<>]/.test(String(hostId))) {
238
+ errors.push(`<host> "${hostId}" has characters that are not valid in a host id (spaces, quotes, shell symbols). ${usage}`);
239
+ }
240
+ if (options.os !== undefined && !ENROL_OS.includes(String(options.os))) {
241
+ errors.push(`--os must be one of ${ENROL_OS.join(', ')} (got "${options.os}"). ${usage}`);
242
+ }
243
+ if (options.arch !== undefined && !ENROL_ARCH.includes(String(options.arch))) {
244
+ errors.push(`--arch must be one of ${ENROL_ARCH.join(', ')} (got "${options.arch}"). ${usage}`);
245
+ }
246
+ return errors;
247
+ }
248
+
249
+ /**
250
+ * Pull the collector URL the serving collector templated into install.sh's
251
+ * `COLLECTOR_URL="${COLLECTOR_URL:-<url>}"` default arm. Empty string when the
252
+ * collector has no event URL configured (the script then refuses to run
253
+ * without --collector).
254
+ */
255
+ export function parseInstallerCollectorUrl(script) {
256
+ const m = /^COLLECTOR_URL="\$\{COLLECTOR_URL:-([^}"]*)\}"$/m.exec(String(script || ''));
257
+ return m ? m[1].trim() : '';
258
+ }
259
+
260
+ /**
261
+ * Probe GET <base>/install. Returns { collectorUrl, bytes } or throws an Error
262
+ * whose message is plain English, never a raw status dump. `fetchImpl` is
263
+ * injectable for tests.
264
+ */
265
+ export async function probeEnrolInstaller(base, fetchImpl = fetch) {
266
+ const root = String(base).replace(/\/+$/, '');
267
+ const url = `${root}/install`;
268
+ let resp;
269
+ try {
270
+ resp = await fetchImpl(url, { signal: AbortSignal.timeout(10000) });
271
+ } catch (err) {
272
+ throw new Error(
273
+ `Could not reach ${url} (${err.message}). Check the Quox instance is running and reachable from this machine.`
274
+ );
275
+ }
276
+ if (resp.status === 503) throw new Error(`Enrolment is not available: ${ENROL_NO_INSTALLER_MESSAGE}.`);
277
+ if (!resp.ok) {
278
+ throw new Error(`Enrolment is not available: GET ${url} answered HTTP ${resp.status}, not an installer.`);
279
+ }
280
+ const script = await resp.text();
281
+ if (!script.startsWith(INSTALL_SCRIPT_MAGIC)) {
282
+ throw new Error(
283
+ `Enrolment is not available: ${url} answered but not with the QuoxAgent installer `
284
+ + '(a proxy error page or a login redirect is in the way). Point the profile at the Quox instance itself.'
285
+ );
286
+ }
287
+ return { collectorUrl: parseInstallerCollectorUrl(script), bytes: script.length };
288
+ }
289
+
290
+ /**
291
+ * The exact command to run ON the host. `base` is the URL the installer is
292
+ * fetched from (the collector's own templated LAN URL when it has one, else
293
+ * the instance URL); `collector` is the URL the agent will report to.
294
+ *
295
+ * Fail loud (quox#1223): the script is downloaded to a temp file with
296
+ * `curl -f` and only then run, inside a subshell, so a curl failure (TLS
297
+ * error, HTTP error) stops the command with curl's non-zero exit and nothing
298
+ * is piped into bash. TLS verification stays ON unless the operator passes
299
+ * `insecureTls` (the explicit --insecure-tls opt-in).
300
+ */
301
+ export function buildEnrolCommand({ base, collector, hostId, insecureTls = false, tlsPin = null, binarySha256 = null }) {
302
+ const root = String(base).replace(/\/+$/, '');
303
+ const coll = String(collector || root).replace(/\/+$/, '');
304
+ // REL-423: binary integrity out of band. install.sh fetches the agent binary from the collector URL (plain http
305
+ // on a default install) and fails closed when BINARY_SHA256 does not match; the CLI read this hash over its own
306
+ // verified connection, so a MITM on the binary download cannot swap it.
307
+ const sha = /^[0-9a-f]{64}$/.test(String(binarySha256 || '')) ? `env BINARY_SHA256=${binarySha256} ` : '';
308
+ const flags = ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)} ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(coll)} )`;
309
+ if (tlsPin && !insecureTls) {
310
+ // REL-423: a self-signed edge with TLS verification ON and no -k. The install's own certificate rides in the
311
+ // command as the only trusted CA (--cacert: chain + hostname checked) and its public key is pinned too
312
+ // (--pinnedpubkey: a different key aborts with curl exit 90). Both were read over a verified handshake.
313
+ const { pem, pin } = pemAndPinParts(tlsPin);
314
+ return `( f=$(mktemp) && c=$(mktemp) && trap 'rm -f "$f" "$c"' EXIT && printf -- '${pem}' > "$c"`
315
+ + ` && curl -fsSL --cacert "$c" --pinnedpubkey 'sha256//${pin}' ${root}/install -o "$f" && sudo ${sha}bash "$f"` + flags;
316
+ }
317
+ const curl = insecureTls ? 'curl -fsSLk' : 'curl -fsSL';
318
+ if (sha) return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && ${curl} ${root}/install -o "$f" && sudo ${sha}bash "$f"` + flags;
319
+ return `( f=$(mktemp) && trap 'rm -f "$f"' EXIT && ${curl} ${root}/install -o "$f" && sudo bash "$f"`
320
+ + ` ${INSTALL_SH_FLAGS.hostId} ${shellSingleQuote(hostId)}`
321
+ + ` ${INSTALL_SH_FLAGS.collector} ${shellSingleQuote(coll)} )`;
322
+ }
323
+
324
+ /** Validate a { pem, pin } pair and return the shell-safe parts (base64 only, so nothing can break out of quotes). */
325
+ function pemAndPinParts({ pem, pin }) {
326
+ const body = String(pem || '').replace(/-----(BEGIN|END) CERTIFICATE-----/g, '').replace(/\s+/g, '');
327
+ if (!/^[A-Za-z0-9+/]+=*$/.test(body) || !/^[A-Za-z0-9+/]{43}=$/.test(String(pin || ''))) throw new Error('invalid TLS pin');
328
+ const lines = body.match(/.{1,64}/g).join('\\n');
329
+ return { pem: `-----BEGIN CERTIFICATE-----\\n${lines}\\n-----END CERTIFICATE-----\\n`, pin };
330
+ }
331
+
332
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
333
+
334
+ /**
335
+ * REL-423: the URL another host fetches the installer from. The instance's own TLS edge (it proxies /install and
336
+ * /download/quoxagent-* to the collector), with a loopback profile host (the host CLI's `local` profile is
337
+ * https://localhost:3000) swapped for the LAN host the collector advertises (COLLECTOR_LAN_URL, set by setup.sh).
338
+ * Returns null when a loopback base has no LAN host to swap in.
339
+ */
340
+ export function edgeInstallerBase(base, collectorUrl) {
341
+ const u = new URL(String(base));
342
+ if (LOOPBACK_HOSTNAMES.has(u.hostname)) {
343
+ let lan = '';
344
+ try { lan = collectorUrl ? new URL(collectorUrl).hostname : ''; } catch { lan = ''; }
345
+ if (!lan || LOOPBACK_HOSTNAMES.has(lan)) return null;
346
+ u.hostname = lan;
347
+ }
348
+ return `${u.protocol}//${u.host}`;
349
+ }
350
+
351
+ /**
352
+ * REL-423: read the edge's certificate over a VERIFIED handshake (Node's CAs plus NODE_EXTRA_CA_CERTS, hostname
353
+ * checked) and return { pem, pin } (pin = base64 SHA-256 of the SPKI, curl's --pinnedpubkey form). Throws with a
354
+ * plain reason when the certificate cannot be verified, so no pin is ever taken from an unverified peer.
355
+ */
356
+ export function readEdgeTlsPin(url, { connect = tls.connect, timeoutMs = 10000 } = {}) {
357
+ const u = new URL(String(url));
358
+ const host = u.hostname.replace(/^\[|\]$/g, '');
359
+ const port = Number(u.port || 443);
360
+ const isIp = /^[0-9.]+$/.test(host) || host.includes(':');
361
+ return new Promise((resolve, reject) => {
362
+ const sock = connect({ host, port, servername: isIp ? undefined : host, rejectUnauthorized: true });
363
+ const done = (fn, v) => { try { sock.destroy(); } catch { /* closed */ } fn(v); };
364
+ sock.setTimeout?.(timeoutMs, () => done(reject, new Error(`timed out reading the TLS certificate of ${u.host}`)));
365
+ sock.once('error', (err) => done(reject, new Error(`cannot verify the TLS certificate of ${u.host} (${err.code || err.message})`)));
366
+ sock.once('secureConnect', () => {
367
+ try {
368
+ const raw = sock.getPeerCertificate(true)?.raw;
369
+ if (!raw) throw new Error(`no certificate from ${u.host}`);
370
+ const x = new X509Certificate(raw);
371
+ const spki = x.publicKey.export({ type: 'spki', format: 'der' });
372
+ done(resolve, { pem: x.toString(), pin: createHash('sha256').update(spki).digest('base64') });
373
+ } catch (err) { done(reject, err); }
374
+ });
375
+ });
218
376
  }
219
377
 
220
378
  /** Argument validation for `fleet codex-provision`. Returns a list of errors. */
@@ -407,7 +565,20 @@ export function registerFleetCommands(program, ctx) {
407
565
  .action(async () => {
408
566
  auth.requireAuth();
409
567
  const ep = resolveEndpoint('v1.fleet.hosts.list');
410
- const data = await client.get(ep.path, { service: ep.service });
568
+ // quox#1224: on an install with no Bastion the collector answers from the agent registry (200) or, with
569
+ // no agents either, a 503 whose body says so in a sentence. Either way the operator reads that sentence,
570
+ // never a bare status code.
571
+ let data;
572
+ try {
573
+ data = await client.get(ep.path, { service: ep.service });
574
+ } catch (err) {
575
+ if (err && err.status === 503 && err.body && err.body.bastion === false) {
576
+ data = { hosts: [], bastion: false, message: err.body.message || err.body.error };
577
+ } else {
578
+ throw err;
579
+ }
580
+ }
581
+ if (data && data.bastion === false && data.message && program.opts().output !== 'json') printInfo(data.message);
411
582
  const raw = data.hosts || data || [];
412
583
  const rows = (raw || []).map(h => ({
413
584
  id: h.id,
@@ -421,7 +592,32 @@ export function registerFleetCommands(program, ctx) {
421
592
  columns: ['id', 'ip', 'group', 'state', 'health', 'last_seen'],
422
593
  headers: ['Host', 'IP', 'Group', 'State', 'Health', 'Last Seen'],
423
594
  rows,
424
- }, { format: program.opts().output, emptyMessage: 'No hosts in the Bastion registry.' });
595
+ }, { format: program.opts().output, emptyMessage: data && data.bastion === false ? 'No agents enrolled yet.' : 'No hosts in the Bastion registry.' });
596
+ });
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`);
425
621
  });
426
622
 
427
623
  fleet
@@ -514,6 +710,10 @@ export function registerFleetCommands(program, ctx) {
514
710
  // quox#791: a held call prints how to read the output once approved.
515
711
  const heldId = data && data.pending && (data.approvalId || (data._approval && data._approval.id));
516
712
  if (heldId) console.error(`after approval run: quox approvals result ${heldId}`);
713
+ // quox#999: a failed tool call must exit non-zero, or a script/CI step reads success on
714
+ // failure. A HELD (pending) call is not a failure — it awaits a human — so it stays 0.
715
+ const failed = (data && data.success === false) || (data && data.result && data.result.success === false);
716
+ if (failed && !(data && data.pending)) process.exitCode = 1;
517
717
  });
518
718
 
519
719
  fleet
@@ -844,6 +1044,138 @@ export function registerFleetCommands(program, ctx) {
844
1044
  console.log();
845
1045
  });
846
1046
 
1047
+ fleet
1048
+ .command('enrol <host>')
1049
+ .alias('enroll')
1050
+ .description('Print (default; or with --run, execute over the governed path) the exact command that enrols a host into this Quox instance. Needs the instance to ship the quoxagent installer.')
1051
+ .option('--os <os>', `target OS: ${ENROL_OS.join('|')} (default: linux)`)
1052
+ .option('--arch <arch>', `target CPU: ${ENROL_ARCH.join('|')} (default: amd64)`)
1053
+ // NOTE: no --print flag. Printing is the default, and `--print` anywhere in argv is claimed by the
1054
+ // root headless-chat router (lib/quoxon/route.js), which would swallow this whole command.
1055
+ .option('--run', 'run it on <host> through the governed exec path (needs <host> already reachable by the Bastion; may wait on approval)')
1056
+ .option('--insecure-tls', 'opt in to skipping TLS verification when fetching the installer (self-signed certs). Off by default; prints a warning')
1057
+ .option('--exec-org <id>', 'org ID for the governed exec calls (with --run)')
1058
+ .option('--wait <seconds>', 'with --run: how long to wait for the host to show up', '120')
1059
+ .action(async (hostId, options) => {
1060
+ auth.requireAuth();
1061
+ const errors = validateEnrolArgs(hostId, options);
1062
+ if (errors.length) throw new Error(errors.join('\n'));
1063
+
1064
+ const host = String(hostId).trim();
1065
+ const os = options.os || 'linux';
1066
+ const arch = options.arch || 'amd64';
1067
+ const isJson = program.opts().output === 'json';
1068
+ const say = (fn, msg) => { if (!isJson) fn(msg); };
1069
+ const base = String(ctx.config.getBaseUrl()).replace(/\/+$/, '');
1070
+
1071
+ // 1. Is there an installer at all? Never hand out a command that cannot work.
1072
+ const probe = await probeEnrolInstaller(base);
1073
+
1074
+ // 2. A binary must be staged for the platform; its hash (read over this verified connection) is pinned
1075
+ // into the command as BINARY_SHA256 (REL-423), so the host checks the binary it downloads.
1076
+ const binUrl = `${base}/download/quoxagent-${os}-${arch}.sha256`;
1077
+ let bin;
1078
+ try { bin = await fetch(binUrl, { signal: AbortSignal.timeout(10000) }); }
1079
+ catch (err) { throw new Error(`Could not reach ${binUrl} (${err.message}).`); }
1080
+ if (!bin.ok) {
1081
+ throw new Error(
1082
+ `Enrolment is not available for ${os}/${arch}: this install has no quoxagent binary staged for it (HTTP ${bin.status}).`
1083
+ );
1084
+ }
1085
+ const binarySha256 = (/^([0-9a-f]{64})\b/.exec(await bin.text()) || [])[1] || null;
1086
+ if (!binarySha256) throw new Error(`Enrolment is not available for ${os}/${arch}: ${binUrl} did not return a SHA-256.`);
1087
+
1088
+ const collector = probe.collectorUrl || base;
1089
+ const insecureTls = Boolean(options.insecureTls);
1090
+ // REL-423 (supersedes the quox#1223 plain-http collector fetch): an https instance hands out its own TLS edge
1091
+ // (/install is proxied to the collector there), with TLS verification ON for its self-signed certificate:
1092
+ // the certificate and its key pin are read now over a verified handshake and travel in the command.
1093
+ // --insecure-tls (explicit opt-in) and a plain-http instance keep the collector URL, as before.
1094
+ let installerBase = probe.collectorUrl || base;
1095
+ let tlsPin = null;
1096
+ if (/^https:/i.test(base) && !insecureTls) {
1097
+ installerBase = edgeInstallerBase(base, probe.collectorUrl);
1098
+ if (!installerBase) {
1099
+ throw new Error(
1100
+ `Enrolment needs this server's LAN address: the profile points at ${base} and the collector advertises no `
1101
+ + 'LAN URL. Set COLLECTOR_LAN_URL=http://<this-server-LAN-address>:9848 in the install\'s .env '
1102
+ + '(./scripts/setup.sh does this) and restart the collector, or use a profile with the LAN address.'
1103
+ );
1104
+ }
1105
+ try { tlsPin = await readEdgeTlsPin(installerBase); }
1106
+ catch (err) {
1107
+ throw new Error(
1108
+ `Cannot hand out a verified enrol command: ${err.message}. If the certificate does not name `
1109
+ + `${new URL(installerBase).hostname}, reissue it (rm ssl/quox.crt ssl/quox.key && ./scripts/setup.sh); `
1110
+ + 'or re-run with --insecure-tls to accept the risk explicitly.'
1111
+ );
1112
+ }
1113
+ }
1114
+ const command = buildEnrolCommand({ base: installerBase, collector, hostId: host, insecureTls, tlsPin, binarySha256 });
1115
+
1116
+ if (!options.run) {
1117
+ if (isJson) {
1118
+ console.log(JSON.stringify({ host_id: host, os, arch, installer_url: `${installerBase}/install`, collector, insecure_tls: insecureTls, tls_pin: tlsPin ? `sha256//${tlsPin.pin}` : null, binary_sha256: binarySha256, command }, null, 2));
1119
+ return;
1120
+ }
1121
+ if (!probe.collectorUrl) {
1122
+ printWarning(`The installer has no collector URL configured (COLLECTOR_LAN_URL / COLLECTOR_PUBLIC_URL on the collector); the command passes ${collector} explicitly.`);
1123
+ }
1124
+ if (insecureTls) {
1125
+ printWarning('--insecure-tls: the command skips TLS certificate verification. Anyone on the path can swap the installer, which runs as root. Use it only on a network you trust.');
1126
+ } else if (/^http:/i.test(installerBase)) {
1127
+ printWarning(`The installer is fetched over plain http (${installerBase}); the agent binary is still checked against BINARY_SHA256.`);
1128
+ }
1129
+ console.log();
1130
+ console.log(`Run this on ${chalk.cyan(host)} (${os}/${arch}), as a user who can sudo:`);
1131
+ console.log();
1132
+ console.log(` ${chalk.bold(command)}`);
1133
+ console.log();
1134
+ console.log(chalk.dim(' Then `quox fleet agents` and `quox fleet hosts` list it once it heartbeats.'));
1135
+ console.log(chalk.dim(' The host must be able to reach ' + collector + ' (the instance\'s collector).'));
1136
+ console.log();
1137
+ return;
1138
+ }
1139
+
1140
+ // --run: governed ssh_exec to <host>, detached and polled.
1141
+ say(printInfo, `Installing on ${host} from ${base} (detached, polled)...`);
1142
+ const install = await runDetachedStep(host, {
1143
+ unit: 'quoxagent-enrol',
1144
+ command,
1145
+ orgOverride: options.execOrg,
1146
+ });
1147
+ if (install.state === 'timeout') {
1148
+ throw new Error(`The install did not finish in time on ${host}. Check \`systemctl status quoxagent-enrol\` and ${install.log} on the host, then re-run.`);
1149
+ }
1150
+ if (install.state === 'failed') {
1151
+ throw new Error(`QuoxAgent install failed on ${host} (exit ${install.exitCode}):\n${install.text}`);
1152
+ }
1153
+ say(printSuccess, ' install completed.');
1154
+
1155
+ const waitSeconds = Math.max(0, Number.parseInt(options.wait, 10) || 0);
1156
+ const deadline = Date.now() + waitSeconds * 1000;
1157
+ let seen = false;
1158
+ let detail = 'not listed yet';
1159
+ do {
1160
+ try {
1161
+ const ep = resolveEndpoint('v1.agents.list');
1162
+ const data = await client.get(ep.path, { service: ep.service });
1163
+ const agents = data.agents || data || [];
1164
+ seen = (Array.isArray(agents) ? agents : []).some((a) => a.host_id === host);
1165
+ detail = seen ? 'heartbeating' : `agent list has ${Array.isArray(agents) ? agents.length : 0} entries, none matching ${host}`;
1166
+ } catch (err) { detail = err.message; }
1167
+ if (seen || Date.now() >= deadline) break;
1168
+ await sleep(5000);
1169
+ } while (Date.now() < deadline);
1170
+
1171
+ if (isJson) {
1172
+ console.log(JSON.stringify({ host_id: host, command, install: { state: install.state, exit_code: install.exitCode }, enrolled: seen, detail }, null, 2));
1173
+ return;
1174
+ }
1175
+ if (seen) printEvent(glyphLine('bastion', 'host enrolled', { result: `${host} -> ${collector}`, status: 'ok' }));
1176
+ else printWarning(`${host} is not listed yet (${detail}). The install succeeded; check \`quox fleet agents\` shortly.`);
1177
+ });
1178
+
847
1179
  fleet
848
1180
  .command('watch')
849
1181
  .description('Watch fleet status in real-time')
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { formatOutput, formatDetail, printSuccess, printWarning } from '../core/output.js';
23
- import { resolveOrgId, parseJSON } from './helpers.js';
23
+ import { resolveOrgId, parseJSON, withTimeoutHint } from './helpers.js';
24
24
  import { resolveEndpoint } from '../api/endpoints.js';
25
25
 
26
26
  export function registerGovernanceCommands(program, ctx) {
@@ -396,17 +396,26 @@ export function registerGovernanceCommands(program, ctx) {
396
396
  .description('List entries in a WARD chain')
397
397
  .option('--chain <chainId>', 'chain ID (default: production chain)')
398
398
  .option('--limit <n>', 'max results', '50')
399
+ .option('--before-seq <n>', 'only entries with seq below this (cursor for the next page)')
399
400
  .action(async (options) => {
400
401
  auth.requireAuth();
401
402
  const query = { limit: options.limit };
402
403
  if (options.chain) query.chain = options.chain;
403
- const data = await client.get(resolveEndpoint('ward.entries.list').path, { service: resolveEndpoint('ward.entries.list').service, query });
404
+ if (options.beforeSeq) query.before_seq = options.beforeSeq;
405
+ const data = await withTimeoutHint(
406
+ 'quox governance ward entries',
407
+ 'Try a smaller --limit, or page with --before-seq <n>; pass --chain to pick a smaller chain.',
408
+ () => client.get(resolveEndpoint('ward.entries.list').path, { service: resolveEndpoint('ward.entries.list').service, query }),
409
+ );
404
410
  formatOutput({
405
411
  // fleet #392: WARD entries carry witnessed_at, not created_at.
406
412
  columns: ['seq', 'source_kind', 'chain_id', 'witnessed_at'],
407
413
  headers: ['Seq', 'Source', 'Chain', 'Created'],
408
414
  rows: data.entries || data,
409
415
  }, { format: program.opts().output, emptyMessage: 'No entries found.' });
416
+ if (data && data.has_more && data.next_before_seq != null && program.opts().output !== 'json' && program.opts().output !== 'quiet') {
417
+ console.error(`More entries available: --before-seq ${data.next_before_seq}`);
418
+ }
410
419
  });
411
420
 
412
421
  // POST /ward/verify — non-mutating integrity check, safe to run without --confirm
@@ -562,7 +571,7 @@ export function registerGovernanceCommands(program, ctx) {
562
571
  governance
563
572
  .command('certify')
564
573
  .description('Run VOLT certification against a run (WRITE — creates a certification record)')
565
- .argument('<runId>', 'VOLT run ID')
574
+ .argument('<runId>', 'VOLT run ID (VR_...) or the workflow run (execution) id `quox workflow run` printed')
566
575
  .option('--retention-days <n>', 'retention period in days')
567
576
  .option('--audit-info <json>', 'audit info as JSON')
568
577
  .option('--compliance-info <json>', 'compliance info as JSON')
@@ -33,14 +33,23 @@ export function registerGripeCommands(program, { client, auth }) {
33
33
  .argument('<complaint...>', 'the gripe, e.g. "sidebar tooltip clips on resize"')
34
34
  .option('--repo <owner/name>', 'target repo (GRIPE defaults sensibly when omitted)')
35
35
  .option('--label <label>', 'issue label, e.g. bug, idea, fleet')
36
+ .option('--public', 'confirm filing to a PUBLIC repo (asks y/N; refused if text has internal hostnames/paths)')
36
37
  .option('--yes', 'skip the confirmation step and let GRIPE file it directly')
37
38
  .action(async (complaintParts, options) => {
38
39
  auth.requireAuth();
39
40
  const complaint = complaintParts.join(' ').trim();
40
41
  if (!complaint) throw new Error('say what is wrong: quox gripe "the thing that is broken"');
41
42
 
42
- printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
43
43
  const orgId = resolveOrgId(auth, program);
44
+ if (options.public) {
45
+ // QUOXON-PRIVACY: public repos only via the deterministic path with a
46
+ // human y/N; the GRIPE agent can never confirm a public filing.
47
+ if (!options.repo) throw new Error('--public needs an explicit --repo <name>');
48
+ const r = await fileDeterministically({ client, complaint, options, orgId, confirmPublic: promptConfirmPublic });
49
+ if (!r.filed) process.exitCode = 1;
50
+ return;
51
+ }
52
+ printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
44
53
  const data = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
45
54
 
46
55
  if (program.opts().output === 'json') {
@@ -98,7 +107,7 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
98
107
  // is what stops the outcome from depending on how the agent phrased its
99
108
  // reply. Absent (older collector), classification falls back to prose.
100
109
  const toolsUsed = Array.isArray(data?.tools_used) ? data.tools_used : null;
101
- const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed });
110
+ const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed, complaint });
102
111
  // quox#668: with --yes, a non-filing agent run is followed by the
103
112
  // deterministic file. Printing the agent's prose first ("no tool, cannot
104
113
  // file") right above "issue created" contradicted the real outcome. In
@@ -162,11 +171,26 @@ export async function settleGripe({ client, data, complaint, options = {}, orgId
162
171
  // also cannot file. Still NOT auto-run after auth-unavailable (a 403/429/503
163
172
  // is a platform outage the proxy would hit too; retrying shortly is the fix).
164
173
  // Returns { filed, url?, error? } so the caller can decide the message.
165
- export async function fileDeterministically({ client, complaint, options, orgId, quiet = false }) {
174
+ async function promptConfirmPublic(repo) {
175
+ if (!process.stdin.isTTY) return false;
176
+ const { createInterface } = await import('node:readline/promises');
177
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
178
+ try {
179
+ const a = await rl.question(`${repo} is PUBLIC. This issue will be world-readable. File it? [y/N] `);
180
+ return /^y(es)?$/i.test(a.trim());
181
+ } finally { rl.close(); }
182
+ }
183
+
184
+ export async function fileDeterministically({ client, complaint, options, orgId, quiet = false, confirmPublic = null }) {
166
185
  const notes = [];
167
186
  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); };
168
187
  const repoArg = options.repo || 'quoxai/quox';
169
188
  const [owner, repo] = repoArg.includes('/') ? repoArg.split('/', 2) : ['quoxai', repoArg];
189
+ let confirmed = false;
190
+ if (options.public) {
191
+ confirmed = typeof confirmPublic === 'function' ? !!(await confirmPublic(`${owner}/${repo}`)) : false;
192
+ if (!confirmed) { say({ kind: 'warn', text: 'Not filed: public filing was not confirmed.' }); return { filed: false, notes }; }
193
+ }
170
194
  const firstLine = complaint.split('\n')[0];
171
195
  const title = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
172
196
  const ep = resolveEndpoint('github.issue.create');
@@ -180,6 +204,7 @@ export async function fileDeterministically({ client, complaint, options, orgId,
180
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.`,
181
205
  labels: [options.label || 'fleet/qa'],
182
206
  ...(orgId ? { org_id: orgId } : {}),
207
+ ...(confirmed ? { confirm_public: true } : {}),
183
208
  },
184
209
  });
185
210
  const url = data?.issue?.html_url || data?.issue?.url || null;
@@ -228,7 +253,7 @@ const AUTH_UNAVAILABLE_MESSAGE =
228
253
  * that is not there. The URL check stays first and stays authoritative, since
229
254
  * a real issue link is the only thing that proves filing.
230
255
  */
231
- export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null } = {}) {
256
+ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null, complaint = '' } = {}) {
232
257
  const text = String(response ?? '');
233
258
  const url = (text.match(/https:\/\/github\.com\/\S+\/issues\/\d+/) || [])[0];
234
259
  if (url) return { kind: 'filed', url, certain: true };
@@ -262,13 +287,13 @@ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed
262
287
  // issue; saying "not filed" would send the user to file a duplicate.
263
288
  return { kind: 'filed-unknown', certain: true };
264
289
  }
265
- return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes) };
290
+ return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes, complaint) };
266
291
  }
267
292
 
268
293
  // No structured evidence available (an older collector, or a surface that
269
294
  // does not return tools_used). Fall back to the prose-only classification,
270
295
  // unchanged, rather than degrading every caller to "unknown".
271
- const hint = adviceHint(text, assumedYes);
296
+ const hint = adviceHint(text, assumedYes, complaint);
272
297
  if (hint === 'no-connector') return { kind: 'no-connector' };
273
298
  if (hint === 'auth-unavailable') return { kind: 'auth-unavailable' };
274
299
  if (hint === 'awaiting-confirm') return { kind: 'awaiting-confirm' };
@@ -282,7 +307,18 @@ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed
282
307
  *
283
308
  * @returns {'no-connector'|'awaiting-confirm'|null}
284
309
  */
285
- function adviceHint(text, assumedYes) {
310
+ // The platform-failure shapes adviceHint matches first. Each is a global regex so a match can be blanked.
311
+ const AUTH_SIGNALS = [/membership/gi, /verification .{0,20}unavailable/gi, /auth service did not answer/gi, /\b(403|503|429)\b/g];
312
+
313
+ function adviceHint(text, assumedYes, complaint = '') {
314
+ // 2026-10-17: GRIPE's confirmation reply echoes the complaint, so a gripe ABOUT an HTTP 403 (or the membership
315
+ // page) read as a platform auth failure and "File it?" became "a platform authorization step failed". A signal
316
+ // counts only where the user did not write it: whatever the complaint itself contains is blanked first.
317
+ const userText = String(complaint || '');
318
+ let authText = text;
319
+ for (const re of AUTH_SIGNALS) {
320
+ for (const m of new Set(userText.match(re) || [])) authText = authText.split(m).join(' ');
321
+ }
286
322
  // Platform-side authorization/availability failures beat the no-connector
287
323
  // guess. On 2026-09-21 auth's rate limiter 429'd the collector's membership
288
324
  // verify, the tool call 403'd with "no active membership", GRIPE narrated
@@ -290,12 +326,7 @@ function adviceHint(text, assumedYes) {
290
326
  // "the org has no GitHub connector configured" — a confident falsehood while
291
327
  // the vault held three healthy credentials (quox#565). These shapes come
292
328
  // from the collector's own error strings, so match them FIRST.
293
- if (
294
- /membership/i.test(text) ||
295
- /verification .{0,20}unavailable/i.test(text) ||
296
- /auth service did not answer/i.test(text) ||
297
- /\b(403|503|429)\b/.test(text)
298
- ) {
329
+ if (AUTH_SIGNALS.some((re) => { re.lastIndex = 0; return re.test(authText); })) {
299
330
  return 'auth-unavailable';
300
331
  }
301
332