quoxcli 0.0.0-stage → 0.3.1

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 (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,1447 @@
1
+ /**
2
+ * Vault commands: credential management, health, export, migration
3
+ *
4
+ * quox vault <subcommand> [options]
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import Table from 'cli-table3';
9
+ import ora from 'ora';
10
+ import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
11
+ import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
12
+ import { resolveOrgId } from './helpers.js';
13
+ import { resolveEndpoint } from '../api/endpoints.js';
14
+
15
+ /**
16
+ * Read a credential's declared repository scope (quoxai/quox#481).
17
+ * Tolerant of absent or malformed metadata: a credential that cannot state its
18
+ * scope simply has none, rather than breaking a listing.
19
+ */
20
+ export function readRepoScope(credential) {
21
+ const raw = credential?.metadata_json ?? credential?.metadata;
22
+ if (!raw) return [];
23
+ let meta;
24
+ try {
25
+ meta = typeof raw === 'string' ? JSON.parse(raw) : raw;
26
+ } catch (_err) {
27
+ return [];
28
+ }
29
+ return Array.isArray(meta?.repos) ? meta.repos.filter((r) => typeof r === 'string' && r.trim()) : [];
30
+ }
31
+
32
+ /**
33
+ * Validate one repository pattern (quoxai/quox#481).
34
+ *
35
+ * Validation matters more here than it looks: a typo'd pattern matches
36
+ * nothing, so resolution degrades silently to the org default and files the
37
+ * issue with the wrong token. That is the exact bug repo scoping exists to
38
+ * fix, only now with a config file that looks correct. Refuse at the point
39
+ * the human can still see what they typed.
40
+ */
41
+ export function validateRepoPattern(pattern) {
42
+ if (pattern === '*' || pattern === '*/*') return pattern;
43
+ // owner/name, or owner/* — the wildcard is only ever the whole name segment.
44
+ if (/^[A-Za-z0-9._-]+\/([A-Za-z0-9._-]+|\*)$/.test(pattern)) return pattern;
45
+ throw new Error(
46
+ `Invalid repository pattern "${pattern}". Use owner/name, owner/* for every repo under an owner, `
47
+ + 'or * for everything.'
48
+ );
49
+ }
50
+
51
+ /**
52
+ * fleet #391 — resolve credential field values for a headless
53
+ * `vault create`, from a JSON object supplied via --credentials-file or
54
+ * --credentials-stdin.
55
+ *
56
+ * Secret hygiene is the whole point of this shape:
57
+ * - no `--value` flag, so nothing lands in argv, the process list, or
58
+ * shell history;
59
+ * - a file can be 0600 and shredded afterwards;
60
+ * - stdin never touches disk;
61
+ * - an "env:VAR_NAME" value is dereferenced here at runtime, so the
62
+ * secret itself need not be written down anywhere.
63
+ *
64
+ * VAULT-INTEG P6: also the pre-save `vault test-new` path, and any future
65
+ * headless credential-field entry point, share this exact routine so there
66
+ * is one place that decides how a secret gets off disk/stdin/env and never
67
+ * onto argv.
68
+ *
69
+ * @returns {Promise<Record<string,string>>} field -> value ({} if none supplied)
70
+ */
71
+ export async function loadSuppliedCredentials(options) {
72
+ let raw = null;
73
+
74
+ if (options.credentialsStdin) {
75
+ raw = await new Promise((resolve, reject) => {
76
+ let buf = '';
77
+ process.stdin.setEncoding('utf8');
78
+ process.stdin.on('data', c => { buf += c; });
79
+ process.stdin.on('end', () => resolve(buf));
80
+ process.stdin.on('error', reject);
81
+ });
82
+ } else if (options.credentialsFile) {
83
+ const { readFile } = await import('node:fs/promises');
84
+ raw = await readFile(options.credentialsFile, 'utf8');
85
+ } else {
86
+ return {};
87
+ }
88
+
89
+ const source = options.credentialsStdin ? 'stdin' : options.credentialsFile;
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(raw);
93
+ } catch (err) {
94
+ throw new Error(`Could not parse credentials JSON from ${source}: ${err.message}`);
95
+ }
96
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
97
+ throw new Error(`Credentials from ${source} must be a JSON object of {field: value}.`);
98
+ }
99
+
100
+ const out = {};
101
+ for (const [k, v] of Object.entries(parsed)) {
102
+ if (typeof v !== 'string') { out[k] = v; continue; }
103
+ if (v.startsWith('env:')) {
104
+ const varName = v.slice(4);
105
+ const fromEnv = process.env[varName];
106
+ if (fromEnv === undefined) {
107
+ throw new Error(`Credential field "${k}" references env:${varName}, which is not set.`);
108
+ }
109
+ out[k] = fromEnv;
110
+ } else {
111
+ out[k] = v;
112
+ }
113
+ }
114
+ return out;
115
+ }
116
+
117
+ // ── Shared action factories ───────────────────────────────────────────────
118
+ //
119
+ // VAULT-INTEG P6: `quox integration ...` is a deprecated alias of `quox
120
+ // vault ...` (see lib/commands/integration.js). Rather than two copies of
121
+ // the same request/response handling, the six overlapping verbs (list, get,
122
+ // create, update, delete, test) live here as factories that both
123
+ // registerVaultCommands() below AND registerIntegrationCommands() wire up.
124
+ // One implementation, two command trees.
125
+
126
+ export function makeVaultListAction({ client, auth }, program) {
127
+ return async (options) => {
128
+ auth.requireAuth();
129
+ const orgId = resolveOrgId(auth, program);
130
+ const query = {};
131
+ if (options.type) query.type = options.type;
132
+
133
+ const data = await client.get(resolveEndpoint('vault.list', { orgId }).path, { service: resolveEndpoint('vault.list', { orgId }).service, query });
134
+ let rows = data.credentials || data;
135
+
136
+ // Client-side status filter (API only supports type filter)
137
+ if (options.status) {
138
+ rows = rows.filter(r => r.test_status === options.status);
139
+ }
140
+
141
+ // #481: repository scope decides WHICH credential a tool call gets, so
142
+ // leaving it out of the listing hides the thing most likely to explain a
143
+ // "wrong token" surprise. Shown only when at least one credential
144
+ // declares a scope, so orgs that never use it keep the narrower table.
145
+ const withRepos = rows.map((r) => ({ ...r, repos: readRepoScope(r).join(', ') || '' }));
146
+ const anyScoped = withRepos.some((r) => r.repos);
147
+
148
+ formatOutput({
149
+ columns: anyScoped
150
+ ? ['id', 'integration_type', 'label', 'repos', 'test_status', 'is_default', 'use_count', 'last_used_at']
151
+ : ['id', 'integration_type', 'label', 'test_status', 'is_default', 'use_count', 'last_used_at'],
152
+ headers: anyScoped
153
+ ? ['ID', 'Type', 'Label', 'Repos', 'Status', 'Default', 'Uses', 'Last Used']
154
+ : ['ID', 'Type', 'Label', 'Status', 'Default', 'Uses', 'Last Used'],
155
+ rows: withRepos,
156
+ }, { format: program.opts().output, emptyMessage: 'No credentials found.' });
157
+ };
158
+ }
159
+
160
+ export function makeVaultGetAction({ client, auth }, program) {
161
+ return async (id) => {
162
+ auth.requireAuth();
163
+ const orgId = resolveOrgId(auth, program);
164
+ const data = await client.get(resolveEndpoint('vault.get', { orgId, id }).path, { service: resolveEndpoint('vault.get', { orgId, id }).service });
165
+ const cred = data.integration || data;
166
+ formatDetail(cred, {
167
+ format: program.opts().output,
168
+ title: `Credential: ${cred.label || id}`,
169
+ fields: ['id', 'integration_type', 'label', 'test_status', 'is_default', 'use_count', 'last_used_at', 'created_at', 'endpoint_url', 'description'],
170
+ labels: {
171
+ id: 'ID', integration_type: 'Type', label: 'Label', test_status: 'Status',
172
+ is_default: 'Default', use_count: 'Uses', last_used_at: 'Last Used',
173
+ created_at: 'Created', endpoint_url: 'Endpoint', description: 'Description',
174
+ },
175
+ });
176
+ };
177
+ }
178
+
179
+ export function makeVaultCreateAction({ client, auth }, program) {
180
+ return async (options) => {
181
+ auth.requireAuth();
182
+ const orgId = resolveOrgId(auth, program);
183
+
184
+ // Fetch available types
185
+ const typesData = await client.get(resolveEndpoint('vault.types.list', { orgId }).path, { service: resolveEndpoint('vault.types.list', { orgId }).service });
186
+ const types = typesData.types || typesData;
187
+
188
+ let integrationType = options.type;
189
+ let label = options.label;
190
+
191
+ // fleet #391: load any headless field values BEFORE prompting, so a
192
+ // fully-specified invocation never blocks on a TTY.
193
+ const supplied = await loadSuppliedCredentials(options);
194
+ const noInput = options.input === false || options.credentialsStdin;
195
+
196
+ if ((!integrationType || !label) && noInput) {
197
+ throw new Error(
198
+ `Missing required option${!integrationType ? ' --type' : ''}${!label ? ' --label' : ''} `
199
+ + '(running non-interactively, so it cannot be prompted for).'
200
+ );
201
+ }
202
+
203
+ // Interactive prompts if needed
204
+ if (!integrationType || !label) {
205
+ const { default: inquirer } = await import('inquirer');
206
+
207
+ if (!integrationType) {
208
+ const choices = types.map(t => ({ name: `${t.name} — ${t.description}`, value: t.id }));
209
+ const ans = await inquirer.prompt([{
210
+ type: 'list',
211
+ name: 'type',
212
+ message: 'Integration type:',
213
+ choices,
214
+ }]);
215
+ integrationType = ans.type;
216
+ }
217
+
218
+ if (!label) {
219
+ const ans = await inquirer.prompt([{
220
+ type: 'input',
221
+ name: 'label',
222
+ message: 'Label:',
223
+ validate: v => v.trim().length >= 2 || 'Label must be at least 2 characters',
224
+ }]);
225
+ label = ans.label;
226
+ }
227
+ }
228
+
229
+ // Get type definition for fields
230
+ const typeDef = types.find(t => t.id === integrationType);
231
+ if (!typeDef) throw new Error(`Unknown integration type: ${integrationType}`);
232
+
233
+ // Collect credential field values. fleet #391: anything already
234
+ // supplied via --credentials-file/--credentials-stdin is used as-is;
235
+ // only genuinely missing fields are prompted for, and under --no-input
236
+ // a missing required field is a clear error rather than a hang.
237
+ const credentials = {};
238
+ const missing = [];
239
+ // quox#564: `inquirer` used to be a `let ... = null` populated only
240
+ // inside this loop's missing-field branch. Any code path below that
241
+ // used `inquirer.prompt(...)` without that branch having run first
242
+ // (headless --no-input skipping this branch entirely, or an
243
+ // interactive run where --credentials-file already supplied every
244
+ // field so the branch never fired) crashed with "Cannot read
245
+ // properties of null (reading 'prompt')" instead of a clear error.
246
+ // A lazy per-use loader means inquirer is never accessed before it
247
+ // is loaded, on any path.
248
+ let inquirer = null;
249
+ async function getInquirer() {
250
+ if (!inquirer) ({ default: inquirer } = await import('inquirer'));
251
+ return inquirer;
252
+ }
253
+ for (const field of typeDef.fields) {
254
+ if (Object.prototype.hasOwnProperty.call(supplied, field.key)) {
255
+ credentials[field.key] = supplied[field.key];
256
+ continue;
257
+ }
258
+ if (!(field.required || !field.default)) continue;
259
+
260
+ if (noInput) {
261
+ if (field.required) missing.push(field.key);
262
+ else if (field.default !== undefined) credentials[field.key] = field.default;
263
+ continue;
264
+ }
265
+
266
+ const inq = await getInquirer();
267
+ const ans = await inq.prompt([{
268
+ type: field.type === 'password' ? 'password' : 'input',
269
+ name: field.key,
270
+ message: `${field.label}:`,
271
+ default: field.default,
272
+ validate: v => field.required ? (v.trim().length > 0 || `${field.label} is required`) : true,
273
+ }]);
274
+ credentials[field.key] = ans[field.key];
275
+ }
276
+
277
+ // quox#577: `supplied` can carry keys the type definition never declared
278
+ // (e.g. an "smtp" type's fields miss imap_host/imap_port on an email
279
+ // credential that needs both). The loop above only ever copies declared
280
+ // fields across, so anything extra was silently dropped even though
281
+ // create still reported success. Union the leftovers in rather than
282
+ // discard them, and say so — a silent drop here is the exact bug.
283
+ const extraKeys = Object.keys(supplied).filter(
284
+ (k) => !Object.prototype.hasOwnProperty.call(credentials, k)
285
+ );
286
+ if (extraKeys.length > 0) {
287
+ for (const k of extraKeys) credentials[k] = supplied[k];
288
+ printInfo(
289
+ `note: including ${extraKeys.length} field(s) not declared by type "${integrationType}": `
290
+ + extraKeys.join(', ')
291
+ );
292
+ }
293
+
294
+ if (missing.length > 0) {
295
+ throw new Error(
296
+ `Missing required field(s) for type "${integrationType}": ${missing.join(', ')}. `
297
+ + 'Provide them via --credentials-file <path> or --credentials-stdin as a JSON object, '
298
+ + `e.g. {"${missing[0]}": "..."} (or "env:MY_VAR" to read from the environment).`
299
+ );
300
+ }
301
+
302
+ // Ask about default
303
+ let isDefault = options.setDefault || false;
304
+ // fleet #391: `--yes` (and any non-interactive invocation) must not
305
+ // stop here either — the field prompts above used to run first, so
306
+ // this gate was never reached in a headless run anyway.
307
+ if (!options.setDefault && !program.opts().yes && !noInput) {
308
+ const inq = await getInquirer();
309
+ const ans = await inq.prompt([{
310
+ type: 'confirm',
311
+ name: 'isDefault',
312
+ message: 'Set as default for this type?',
313
+ default: false,
314
+ }]);
315
+ isDefault = ans.isDefault;
316
+ }
317
+
318
+ // Optional non-secret metadata tags (e.g. {"purpose":"bootstrap"}).
319
+ let metadata;
320
+ if (options.metadata) {
321
+ try {
322
+ metadata = JSON.parse(options.metadata);
323
+ } catch (err) {
324
+ throw new Error(`--metadata must be a JSON object: ${err.message}`);
325
+ }
326
+ if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) {
327
+ throw new Error('--metadata must be a JSON object, e.g. \'{"purpose":"bootstrap"}\'');
328
+ }
329
+ }
330
+
331
+ // #481: --repos folds into the same metadata object, so the two flags
332
+ // compose rather than one silently winning.
333
+ if (options.repos) {
334
+ const patterns = options.repos.split(',').map((p) => p.trim()).filter(Boolean);
335
+ if (patterns.length === 0) throw new Error('--repos needs at least one pattern.');
336
+ for (const p of patterns) validateRepoPattern(p);
337
+ metadata = { ...(metadata || {}), repos: patterns };
338
+ }
339
+
340
+ // Create
341
+ const spinner = ora('Creating credential...').start();
342
+ const result = await client.post(resolveEndpoint('vault.create', { orgId }).path, {
343
+ service: resolveEndpoint('vault.create', { orgId }).service,
344
+ body: {
345
+ integration_type: integrationType,
346
+ label,
347
+ credentials,
348
+ is_default: isDefault,
349
+ ...(metadata ? { metadata } : {}),
350
+ ...(options.kind ? { credential_kind: options.kind } : {}),
351
+ },
352
+ });
353
+ spinner.succeed('Credential created.');
354
+
355
+ // Optional test. quox#564: this confirm was missing the `!noInput`
356
+ // guard its sibling above (the "set as default?" confirm) already had,
357
+ // so a headless `--no-input` run (or `--yes`-less) fell through to
358
+ // `inquirer.prompt(...)` on whatever `inquirer` happened to be at that
359
+ // point -- null whenever no field prompt had run first. Headless has no
360
+ // TTY to prompt on, so it never runs the test unless asked explicitly
361
+ // via `--yes`.
362
+ if (options.test !== false) {
363
+ let runTest = program.opts().yes || false;
364
+ if (!runTest && !noInput) {
365
+ const inq = await getInquirer();
366
+ const ans = await inq.prompt([{
367
+ type: 'confirm',
368
+ name: 'test',
369
+ message: 'Test connection now?',
370
+ default: true,
371
+ }]);
372
+ runTest = ans.test;
373
+ }
374
+
375
+ if (runTest) {
376
+ const testSpinner = ora('Testing connection...').start();
377
+ try {
378
+ const testResult = await client.post(resolveEndpoint('vault.test', { orgId, id: result.id }).path, { service: resolveEndpoint('vault.test', { orgId, id: result.id }).service });
379
+ if (testResult.success) {
380
+ testSpinner.succeed('Connection test passed.');
381
+ } else {
382
+ testSpinner.fail(`Connection test failed: ${testResult.error || testResult.message || 'unknown error'}`);
383
+ }
384
+ } catch (err) {
385
+ testSpinner.fail(`Connection test failed: ${err.message}`);
386
+ }
387
+ }
388
+ }
389
+
390
+ // fleet #397: creating the FIRST credential of a type gave no signal
391
+ // that it silently isn't used until it's also made default (a caller
392
+ // resolving by type only ever sees the default). Print-only, never
393
+ // auto-set-default — that would swap which credential live traffic
394
+ // uses as a side effect of a create. printInfo already no-ops for
395
+ // -o json / -q, so this can't pollute headless output.
396
+ if (!result.is_default) {
397
+ printInfo(`note: not set as default — run "quox vault set-default ${result.id}" to make it the active ${result.integration_type || integrationType} credential`);
398
+ }
399
+
400
+ formatDetail(result, { format: program.opts().output });
401
+ return result;
402
+ };
403
+ }
404
+
405
+ export function makeVaultUpdateAction({ client, auth }, program) {
406
+ return async (id, options) => {
407
+ auth.requireAuth();
408
+ const orgId = resolveOrgId(auth, program);
409
+ const body = {};
410
+ if (options.label) body.label = options.label;
411
+ if (options.description) body.description = options.description;
412
+ if (options.kind) body.credential_kind = options.kind;
413
+
414
+ // quox#580: `vault update` could only ever touch --label/--description —
415
+ // there was no way to change a credential's field VALUES (e.g. rotate an
416
+ // email credential's imap_host) short of `vault rotate`'s interactive
417
+ // prompt flow, and no way to send credential_kind at all. Reuses the
418
+ // same headless load path as `vault create` (loadSuppliedCredentials),
419
+ // so the same secret-hygiene rules apply: no --value flag, file/stdin/
420
+ // env:VAR only.
421
+ const supplied = await loadSuppliedCredentials(options);
422
+ if (Object.keys(supplied).length > 0) body.credentials = supplied;
423
+
424
+ if (Object.keys(body).length === 0) {
425
+ throw new Error(
426
+ 'Specify at least one field to update (--label, --description, --kind, '
427
+ + '--credentials-file, or --credentials-stdin).'
428
+ );
429
+ }
430
+
431
+ await client.put(resolveEndpoint('vault.update', { orgId, id }).path, { service: resolveEndpoint('vault.update', { orgId, id }).service, body });
432
+ printEvent(events.updated('quox', 'vault credential', id));
433
+ };
434
+ }
435
+
436
+ export function makeVaultDeleteAction({ client, auth }, program) {
437
+ return async (id, options) => {
438
+ auth.requireAuth();
439
+ const orgId = resolveOrgId(auth, program);
440
+
441
+ if (!options.force && !program.opts().yes) {
442
+ const { default: inquirer } = await import('inquirer');
443
+ const ans = await inquirer.prompt([{
444
+ type: 'confirm',
445
+ name: 'confirm',
446
+ message: `Delete credential ${id}? This cannot be undone.`,
447
+ default: false,
448
+ }]);
449
+ if (!ans.confirm) {
450
+ printInfo('Cancelled.');
451
+ return;
452
+ }
453
+ }
454
+
455
+ await client.delete(resolveEndpoint('vault.delete', { orgId, id }).path, { service: resolveEndpoint('vault.delete', { orgId, id }).service });
456
+ printEvent(glyphLine('quox', 'vault credential deleted', { result: id, status: 'ok' }));
457
+ };
458
+ }
459
+
460
+ export function makeVaultTestAction({ client, auth }, program) {
461
+ return async (id) => {
462
+ auth.requireAuth();
463
+ const orgId = resolveOrgId(auth, program);
464
+
465
+ const spinner = ora('Testing connection...').start();
466
+ const start = Date.now();
467
+
468
+ try {
469
+ const result = await client.post(resolveEndpoint('vault.test', { orgId, id }).path, { service: resolveEndpoint('vault.test', { orgId, id }).service });
470
+ const elapsed = Date.now() - start;
471
+
472
+ if (result.success) {
473
+ spinner.succeed(`Connection test passed (${elapsed}ms)`);
474
+ } else {
475
+ spinner.fail(`Connection test failed: ${result.error || result.message || 'unknown error'}`);
476
+ }
477
+ } catch (err) {
478
+ const elapsed = Date.now() - start;
479
+ spinner.fail(`Connection test failed (${elapsed}ms): ${err.message}`);
480
+ }
481
+ };
482
+ }
483
+
484
+ export function registerVaultCommands(program, ctx) {
485
+ const { client, auth } = ctx;
486
+
487
+ const vault = program
488
+ .command('vault')
489
+ .description('Manage QuoxVault credentials');
490
+
491
+ // ── vault list ──────────────────────────────────────────────────────────────
492
+
493
+ vault
494
+ .command('list')
495
+ .description('List credentials (masked)')
496
+ .option('--type <type>', 'filter by integration type')
497
+ .option('--status <status>', 'filter by test status (success|failed|untested)')
498
+ .action(makeVaultListAction(ctx, program));
499
+
500
+ // ── vault get ───────────────────────────────────────────────────────────────
501
+
502
+ vault
503
+ .command('get')
504
+ .description('Show credential details')
505
+ .argument('<id>', 'credential ID')
506
+ .action(makeVaultGetAction(ctx, program));
507
+
508
+ // ── vault create ────────────────────────────────────────────────────────────
509
+
510
+ vault
511
+ .command('create')
512
+ .description('Create a credential (interactive, or headless via --credentials-file/--credentials-stdin)')
513
+ .option('--type <type>', 'integration type (skip prompt)')
514
+ .option('--label <label>', 'display label (skip prompt)')
515
+ .option('--set-default', 'set as default for this type')
516
+ .option('--no-test', 'skip connection test after creation')
517
+ // fleet #391: there was no non-interactive path for the field VALUES —
518
+ // including the secret — so a headless operator (CI, an agent, this
519
+ // very box) could not add so much as an LLM key. Deliberately NOT
520
+ // adding `--value <secret>`: that leaks the secret into argv, the
521
+ // process list and shell history. File and stdin both keep it out of
522
+ // all three. `env:VAR` is resolved at runtime so a value can also come
523
+ // from the environment without ever being written down.
524
+ .option('--credentials-file <path>', 'JSON object of {field: value} for the credential fields (use 0600; supports "env:VAR_NAME" values)')
525
+ .option('--credentials-stdin', 'read that same JSON object from stdin (never touches argv or disk)')
526
+ // BASTION-CRED: free-form metadata tags on the credential, e.g. a bootstrap
527
+ // SSH credential is tagged {"purpose":"bootstrap"} so the collector's
528
+ // auto-revoke path (DELETE /internal/integrations/:id) will accept it.
529
+ // Metadata is NOT secret and never contains field values.
530
+ .option('--metadata <json>', 'JSON object of non-secret metadata tags (e.g. \'{"purpose":"bootstrap"}\')')
531
+ // #481: sugar for the one metadata key that changes which credential gets
532
+ // used. Spelling it out as a flag is the difference between a scope people
533
+ // set and one they never discover inside a --metadata JSON blob.
534
+ .option('--repos <patterns>', 'repositories this credential covers: comma-separated owner/name, owner/*, or *')
535
+ .option('--kind <kind>', 'credential_kind to record on the credential, if the type distinguishes one')
536
+ .option('--no-input', 'never prompt; fail with a clear error if a required field is missing')
537
+ .action(makeVaultCreateAction(ctx, program));
538
+
539
+ // ── vault update ────────────────────────────────────────────────────────────
540
+
541
+ vault
542
+ .command('update')
543
+ .description('Update a credential (interactive label/description fields, or headless credential field values via --credentials-file/--credentials-stdin)')
544
+ .argument('<id>', 'credential ID')
545
+ .option('--label <label>', 'new label')
546
+ .option('--description <desc>', 'new description')
547
+ .option('--kind <kind>', 'credential_kind to record on the credential, if the type distinguishes one')
548
+ // quox#580: same secret-hygiene shape as `vault create` — no --value
549
+ // flag, so a field value never lands in argv, the process list, or
550
+ // shell history.
551
+ .option('--credentials-file <path>', 'JSON object of {field: value} for the credential fields to change (use 0600; supports "env:VAR_NAME" values)')
552
+ .option('--credentials-stdin', 'read that same JSON object from stdin (never touches argv or disk)')
553
+ .action(makeVaultUpdateAction(ctx, program));
554
+
555
+ // ── vault repos ─────────────────────────────────────────────────────────────
556
+
557
+ vault
558
+ .command('repos')
559
+ .description('Show or set which repositories a credential covers (GitHub and similar)')
560
+ .argument('<id>', 'credential ID')
561
+ .option('--set <patterns>', 'comma-separated patterns: owner/name, owner/*, or * for everything')
562
+ .option('--clear', 'remove the repository scope, so only the org default reaches this credential')
563
+ .action(async (id, options) => {
564
+ auth.requireAuth();
565
+ const orgId = resolveOrgId(auth, program);
566
+
567
+ if (options.set && options.clear) {
568
+ throw new Error('Pass --set or --clear, not both.');
569
+ }
570
+
571
+ const getEp = resolveEndpoint('vault.get', { orgId, id });
572
+ const data = await client.get(getEp.path, { service: getEp.service });
573
+ const cred = data.integration || data.credential || data;
574
+
575
+ // Metadata is MERGED, never replaced. A credential can already carry
576
+ // {"purpose":"bootstrap"}, which the collector's auto-revoke path reads,
577
+ // and clobbering it would silently disarm that.
578
+ let metadata = {};
579
+ const raw = cred.metadata_json ?? cred.metadata;
580
+ if (raw) {
581
+ try { metadata = typeof raw === 'string' ? JSON.parse(raw) : { ...raw }; } catch (_err) { metadata = {}; }
582
+ }
583
+ if (metadata === null || typeof metadata !== 'object' || Array.isArray(metadata)) metadata = {};
584
+
585
+ if (!options.set && !options.clear) {
586
+ const current = Array.isArray(metadata.repos) ? metadata.repos : [];
587
+ if (current.length === 0) {
588
+ // Say what the absence MEANS. "No scope" reads as "fine"; what it
589
+ // actually means is that only the org default ever reaches this
590
+ // credential, which is the confusion behind quoxai/quox#481.
591
+ printInfo(`${cred.label || id} declares no repositories, so it is only reachable as the org default.`);
592
+ } else {
593
+ console.log(`${cred.label || id} covers:`);
594
+ for (const p of current) console.log(` ${p}`);
595
+ }
596
+ return;
597
+ }
598
+
599
+ if (options.clear) {
600
+ delete metadata.repos;
601
+ } else {
602
+ const patterns = options.set.split(',').map((p) => p.trim()).filter(Boolean);
603
+ if (patterns.length === 0) throw new Error('--set needs at least one pattern.');
604
+ for (const p of patterns) validateRepoPattern(p);
605
+ metadata.repos = patterns;
606
+ }
607
+
608
+ const ep = resolveEndpoint('vault.update', { orgId, id });
609
+ await client.put(ep.path, { service: ep.service, body: { metadata } });
610
+
611
+ if (options.clear) printEvent(events.updated('quox', 'vault credential repos', id, 'cleared'));
612
+ else printEvent(events.updated('quox', 'vault credential repos', id, metadata.repos.join(', ')));
613
+ });
614
+
615
+ // ── vault delete ────────────────────────────────────────────────────────────
616
+
617
+ vault
618
+ .command('delete')
619
+ .description('Delete a credential')
620
+ .argument('<id>', 'credential ID')
621
+ .option('--force', 'skip confirmation')
622
+ .action(makeVaultDeleteAction(ctx, program));
623
+
624
+ // ── vault set-default ───────────────────────────────────────────────────────
625
+
626
+ vault
627
+ .command('set-default')
628
+ .description('Set a credential as the default for its type')
629
+ .argument('<id>', 'credential ID')
630
+ .action(async (id) => {
631
+ auth.requireAuth();
632
+ const orgId = resolveOrgId(auth, program);
633
+ const result = await client.post(resolveEndpoint('vault.default', { orgId, id }).path, { service: resolveEndpoint('vault.default', { orgId, id }).service });
634
+ printEvent(glyphLine('quox', 'vault credential default set', { result: `${id} · ${result.integration_type || 'its type'}`, status: 'ok' }));
635
+ });
636
+
637
+ // ── vault test ──────────────────────────────────────────────────────────────
638
+
639
+ vault
640
+ .command('test')
641
+ .description('Test a credential connection')
642
+ .argument('<id>', 'credential ID')
643
+ .action(makeVaultTestAction(ctx, program));
644
+
645
+ // ── vault test-new ──────────────────────────────────────────────────────────
646
+ //
647
+ // VAULT-INTEG P6: pre-save test — verify a set of credential field values
648
+ // BEFORE creating anything, mirroring the auth service's
649
+ // POST /integrations/test-credentials. Nothing is persisted either way.
650
+ // Same secret-hygiene shape as `vault create`: no --value flag, field
651
+ // values only ever arrive via --credentials-file/--credentials-stdin.
652
+
653
+ vault
654
+ .command('test-new')
655
+ .description('Test credential field values before saving (nothing is persisted)')
656
+ .requiredOption('--type <type>', 'integration type to test')
657
+ .option('--endpoint-url <url>', 'endpoint URL to test against (custom/self-hosted types)')
658
+ .option('--credentials-file <path>', 'JSON object of {field: value} for the credential fields (use 0600; supports "env:VAR_NAME" values)')
659
+ .option('--credentials-stdin', 'read that same JSON object from stdin (never touches argv or disk)')
660
+ .action(async (options) => {
661
+ auth.requireAuth();
662
+ const orgId = resolveOrgId(auth, program);
663
+
664
+ const credentials = await loadSuppliedCredentials(options);
665
+ if (Object.keys(credentials).length === 0) {
666
+ throw new Error(
667
+ 'Provide the credential field values to test via --credentials-file <path> or '
668
+ + '--credentials-stdin (a JSON object of {field: value}).'
669
+ );
670
+ }
671
+
672
+ const spinner = ora('Testing credentials (nothing will be saved)...').start();
673
+ try {
674
+ const ep = resolveEndpoint('vault.test-credentials', { orgId });
675
+ const result = await client.post(ep.path, {
676
+ service: ep.service,
677
+ body: {
678
+ integration_type: options.type,
679
+ credentials,
680
+ ...(options.endpointUrl ? { endpoint_url: options.endpointUrl } : {}),
681
+ },
682
+ });
683
+ if (result.success) {
684
+ spinner.succeed('Connection test passed. Nothing was saved.');
685
+ } else {
686
+ spinner.fail(`Connection test failed: ${result.error || result.message || 'unknown error'}`);
687
+ }
688
+ } catch (err) {
689
+ spinner.fail(`Connection test failed: ${err.message}`);
690
+ }
691
+ });
692
+
693
+ // ── vault test-all ──────────────────────────────────────────────────────────
694
+
695
+ vault
696
+ .command('test-all')
697
+ .description('Test all credentials sequentially')
698
+ .action(async () => {
699
+ auth.requireAuth();
700
+ const orgId = resolveOrgId(auth, program);
701
+
702
+ const data = await client.get(resolveEndpoint('vault.list', { orgId }).path, { service: resolveEndpoint('vault.list', { orgId }).service });
703
+ const creds = data.credentials || data;
704
+
705
+ if (!creds.length) {
706
+ printInfo('No credentials to test.');
707
+ return;
708
+ }
709
+
710
+ const results = [];
711
+ for (const cred of creds) {
712
+ const spinner = ora(`Testing ${cred.label} (${cred.integration_type})...`).start();
713
+ const start = Date.now();
714
+ try {
715
+ const result = await client.post(resolveEndpoint('vault.test', { orgId, id: cred.id }).path, { service: resolveEndpoint('vault.test', { orgId, id: cred.id }).service });
716
+ const elapsed = Date.now() - start;
717
+ if (result.success) {
718
+ spinner.succeed(`${cred.label}: passed (${elapsed}ms)`);
719
+ results.push({ id: cred.id, label: cred.label, type: cred.integration_type, result: 'pass', time: `${elapsed}ms` });
720
+ } else {
721
+ spinner.fail(`${cred.label}: failed — ${result.error || result.message || 'unknown'}`);
722
+ results.push({ id: cred.id, label: cred.label, type: cred.integration_type, result: 'fail', time: `${elapsed}ms` });
723
+ }
724
+ } catch (err) {
725
+ const elapsed = Date.now() - start;
726
+ spinner.fail(`${cred.label}: error — ${err.message}`);
727
+ results.push({ id: cred.id, label: cred.label, type: cred.integration_type, result: 'error', time: `${elapsed}ms` });
728
+ }
729
+ }
730
+
731
+ const passed = results.filter(r => r.result === 'pass').length;
732
+ const failed = results.length - passed;
733
+ console.log('');
734
+ printInfo(`Results: ${passed} passed, ${failed} failed out of ${results.length} credentials`);
735
+
736
+ if (program.opts().output === 'json') {
737
+ console.log(JSON.stringify(results, null, 2));
738
+ }
739
+ });
740
+
741
+ // ── vault stats ─────────────────────────────────────────────────────────────
742
+
743
+ vault
744
+ .command('stats')
745
+ .description('Show vault health dashboard')
746
+ .action(async () => {
747
+ auth.requireAuth();
748
+ const orgId = resolveOrgId(auth, program);
749
+ const stats = await client.get(resolveEndpoint('vault.vault-stats.list', { orgId }).path, { service: resolveEndpoint('vault.vault-stats.list', { orgId }).service });
750
+
751
+ const format = program.opts().output;
752
+ if (format === 'json') {
753
+ console.log(JSON.stringify(stats, null, 2));
754
+ return;
755
+ }
756
+
757
+ // Render health dashboard
758
+ const table = new Table({
759
+ chars: {
760
+ 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗',
761
+ 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝',
762
+ 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼',
763
+ 'right': '║', 'right-mid': '╢', 'middle': '│',
764
+ },
765
+ style: { head: [], border: [] },
766
+ });
767
+
768
+ table.push(
769
+ [{ colSpan: 2, content: chalk.bold('QuoxVault Health'), hAlign: 'center' }],
770
+ ['Credentials', String(stats.total || 0)],
771
+ [chalk.green('● Healthy'), String(stats.healthy || 0)],
772
+ [chalk.red('● Failed'), String(stats.failed || 0)],
773
+ [chalk.dim('○ Untested'), String(stats.untested || 0)],
774
+ [chalk.yellow('◐ Stale'), String(stats.stale || 0)],
775
+ ['Total Uses', String(stats.totalUseCount || 0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')],
776
+ ['Encryption', stats.encryption || 'AES-256-GCM'],
777
+ );
778
+
779
+ console.log(table.toString());
780
+
781
+ // Type breakdown if available
782
+ if (stats.typeBreakdown && stats.typeBreakdown.length > 0) {
783
+ console.log('');
784
+ formatOutput({
785
+ columns: ['integration_type', 'count', 'healthy', 'failed'],
786
+ headers: ['Type', 'Count', 'Healthy', 'Failed'],
787
+ rows: stats.typeBreakdown,
788
+ }, { format });
789
+ }
790
+ });
791
+
792
+ // ── vault scan ──────────────────────────────────────────────────────────────
793
+
794
+ vault
795
+ .command('scan')
796
+ .description('Run a health scan and show issues')
797
+ .action(async () => {
798
+ auth.requireAuth();
799
+ const orgId = resolveOrgId(auth, program);
800
+
801
+ const spinner = ora('Running health scan...').start();
802
+ const result = await client.post(resolveEndpoint('vault.health-scan', { orgId }).path, { service: resolveEndpoint('vault.health-scan', { orgId }).service });
803
+ spinner.stop();
804
+
805
+ const format = program.opts().output;
806
+ if (format === 'json') {
807
+ console.log(JSON.stringify(result, null, 2));
808
+ return;
809
+ }
810
+
811
+ if (!result.issues || result.issues.length === 0) {
812
+ printSuccess('No issues found. All credentials are healthy.');
813
+ return;
814
+ }
815
+
816
+ console.log(chalk.bold(`Found ${result.issueCount || result.issues.length} issue(s):\n`));
817
+
818
+ formatOutput({
819
+ columns: ['severity', 'type', 'label', 'issue', 'message'],
820
+ headers: ['Severity', 'Type', 'Label', 'Issue', 'Message'],
821
+ rows: result.issues,
822
+ }, { format });
823
+ });
824
+
825
+ // ── vault activity ──────────────────────────────────────────────────────────
826
+
827
+ vault
828
+ .command('activity')
829
+ .description('Show vault audit trail')
830
+ .option('--limit <n>', 'number of events', '20')
831
+ .action(async (options) => {
832
+ auth.requireAuth();
833
+ const orgId = resolveOrgId(auth, program);
834
+ const limit = parseInt(options.limit) || 20;
835
+
836
+ const data = await client.get(resolveEndpoint('vault.activity.list', { orgId }).path, {
837
+ service: resolveEndpoint('vault.activity.list', { orgId }).service,
838
+ query: { limit },
839
+ });
840
+ const events = data.events || data;
841
+
842
+ formatOutput({
843
+ columns: ['created_at', 'action', 'actor_type', 'status', 'target_id'],
844
+ headers: ['Time', 'Action', 'Actor', 'Status', 'Target'],
845
+ rows: events,
846
+ }, { format: program.opts().output, emptyMessage: 'No activity found.' });
847
+ });
848
+
849
+ // ── vault types ─────────────────────────────────────────────────────────────
850
+
851
+ vault
852
+ .command('types')
853
+ .description('List available integration types')
854
+ .action(async () => {
855
+ auth.requireAuth();
856
+ const orgId = resolveOrgId(auth, program);
857
+ const data = await client.get(resolveEndpoint('vault.types.list', { orgId }).path, { service: resolveEndpoint('vault.types.list', { orgId }).service });
858
+ const types = data.types || data;
859
+
860
+ formatOutput({
861
+ columns: ['id', 'name', 'description'],
862
+ headers: ['ID', 'Name', 'Description'],
863
+ rows: types,
864
+ }, { format: program.opts().output });
865
+ });
866
+
867
+ // ── vault rotate ────────────────────────────────────────────────────────────
868
+
869
+ vault
870
+ .command('rotate')
871
+ .description('Rotate a credential with new values')
872
+ .argument('<id>', 'credential ID')
873
+ .option('--no-test', 'skip connection test after rotation')
874
+ .option('--expires <date>', 'set expiration date (ISO 8601)')
875
+ .action(async (id, options) => {
876
+ auth.requireAuth();
877
+ const orgId = resolveOrgId(auth, program);
878
+
879
+ // Get current credential info
880
+ const data = await client.get(resolveEndpoint('vault.get', { orgId, id }).path, { service: resolveEndpoint('vault.get', { orgId, id }).service });
881
+ const cred = data.integration || data;
882
+
883
+ console.log(chalk.bold(`Rotating: ${cred.label} (${cred.integration_type})`));
884
+ console.log(chalk.dim(`Current key: ${cred.key_preview || '****'}`));
885
+
886
+ // Get type definition for fields
887
+ const typesData = await client.get(resolveEndpoint('vault.types.list', { orgId }).path, { service: resolveEndpoint('vault.types.list', { orgId }).service });
888
+ const types = typesData.types || typesData;
889
+ const typeDef = types.find(t => t.id === cred.integration_type);
890
+
891
+ if (!typeDef) throw new Error(`Unknown integration type: ${cred.integration_type}`);
892
+
893
+ // Prompt for new credential fields
894
+ const { default: inquirer } = await import('inquirer');
895
+ const credentials = {};
896
+ for (const field of typeDef.fields) {
897
+ if (field.required || !field.default) {
898
+ const ans = await inquirer.prompt([{
899
+ type: field.type === 'password' ? 'password' : 'input',
900
+ name: field.key,
901
+ message: `New ${field.label}:`,
902
+ validate: v => field.required ? (v.trim().length > 0 || `${field.label} is required`) : true,
903
+ }]);
904
+ credentials[field.key] = ans[field.key];
905
+ }
906
+ }
907
+
908
+ const body = { credentials };
909
+ if (options.expires) body.expires_at = options.expires;
910
+
911
+ const testQuery = options.test !== false ? '?test=true' : '';
912
+ const spinner = ora('Rotating credential...').start();
913
+
914
+ const result = await client.post(resolveEndpoint('vault.rotate', { orgId, id, testQuery }).path, { service: resolveEndpoint('vault.rotate', { orgId, id, testQuery }).service, body });
915
+
916
+ spinner.succeed('Credential rotated.');
917
+ console.log(chalk.dim(`New key: ${result.key_preview || '****'}`));
918
+
919
+ if (result.test_status === 'success') {
920
+ printSuccess('Connection test passed.');
921
+ } else if (result.test_status === 'failed') {
922
+ printWarning('Connection test failed — credential was still rotated.');
923
+ }
924
+
925
+ formatDetail(result, { format: program.opts().output });
926
+ });
927
+
928
+ // ── vault expiring ─────────────────────────────────────────────────────────
929
+
930
+ vault
931
+ .command('expiring')
932
+ .description('List credentials expiring soon')
933
+ .option('--days <n>', 'expiration window in days', '7')
934
+ .action(async (options) => {
935
+ auth.requireAuth();
936
+ const orgId = resolveOrgId(auth, program);
937
+ const days = parseInt(options.days) || 7;
938
+
939
+ const data = await client.get(resolveEndpoint('vault.expiring.list', { orgId }).path, {
940
+ service: resolveEndpoint('vault.expiring.list', { orgId }).service,
941
+ query: { days },
942
+ });
943
+ const credentials = data.credentials || data;
944
+
945
+ if (credentials.length === 0) {
946
+ // quox#920: --json always prints valid JSON, an honest empty included.
947
+ if (program.opts().output === 'json') { console.log('[]'); return; }
948
+ printInfo(`No credentials expiring within ${days} days.`);
949
+ return;
950
+ }
951
+
952
+ console.log(chalk.bold(`Credentials expiring within ${days} days:\n`));
953
+
954
+ formatOutput({
955
+ columns: ['id', 'integration_type', 'label', 'expires_at', 'rotated_at', 'test_status'],
956
+ headers: ['ID', 'Type', 'Label', 'Expires At', 'Last Rotated', 'Status'],
957
+ rows: credentials,
958
+ }, { format: program.opts().output });
959
+ });
960
+
961
+ // ── vault folder ────────────────────────────────────────────────────────────
962
+ //
963
+ // VAULT-INTEG P6: credential folders (organize vault entries into a tree,
964
+ // max nesting depth 3 — see services/auth/db/integrations.js createFolder).
965
+
966
+ const folder = vault
967
+ .command('folder')
968
+ .description('Organize credentials into folders');
969
+
970
+ folder
971
+ .command('list')
972
+ .description('List credential folders')
973
+ .action(async () => {
974
+ auth.requireAuth();
975
+ const orgId = resolveOrgId(auth, program);
976
+ const ep = resolveEndpoint('vault.folders.list', { orgId });
977
+ const data = await client.get(ep.path, { service: ep.service });
978
+ const folders = data.folders || data;
979
+
980
+ formatOutput({
981
+ columns: ['id', 'name', 'parent_id', 'color', 'icon', 'credential_count'],
982
+ headers: ['ID', 'Name', 'Parent', 'Color', 'Icon', 'Credentials'],
983
+ rows: folders,
984
+ }, { format: program.opts().output, emptyMessage: 'No credential folders found.' });
985
+ });
986
+
987
+ folder
988
+ .command('create')
989
+ .description('Create a credential folder')
990
+ .requiredOption('--name <name>', 'folder name')
991
+ .option('--parent <folderId>', 'parent folder ID (max nesting depth 3)')
992
+ .option('--color <color>', 'folder color tag')
993
+ .option('--icon <icon>', 'folder icon tag')
994
+ .action(async (options) => {
995
+ auth.requireAuth();
996
+ const orgId = resolveOrgId(auth, program);
997
+ const ep = resolveEndpoint('vault.folders.create', { orgId });
998
+ const result = await client.post(ep.path, {
999
+ service: ep.service,
1000
+ body: { name: options.name, parent_id: options.parent || null, color: options.color, icon: options.icon },
1001
+ });
1002
+ printEvent(events.created('quox', 'credential folder', result.id));
1003
+ formatDetail(result, { format: program.opts().output });
1004
+ });
1005
+
1006
+ folder
1007
+ .command('rename')
1008
+ .description('Rename a credential folder')
1009
+ .argument('<folderId>', 'folder ID')
1010
+ .requiredOption('--name <name>', 'new folder name')
1011
+ .action(async (folderId, options) => {
1012
+ auth.requireAuth();
1013
+ const orgId = resolveOrgId(auth, program);
1014
+ const ep = resolveEndpoint('vault.folders.update', { orgId, folderId });
1015
+ const result = await client.put(ep.path, { service: ep.service, body: { name: options.name } });
1016
+ printEvent(events.updated('quox', 'credential folder', folderId, `renamed to "${options.name}"`));
1017
+ formatDetail(result, { format: program.opts().output });
1018
+ });
1019
+
1020
+ folder
1021
+ .command('delete')
1022
+ .description('Delete a credential folder (its credentials move to no-folder)')
1023
+ .argument('<folderId>', 'folder ID')
1024
+ .option('--force', 'skip confirmation')
1025
+ .action(async (folderId, options) => {
1026
+ auth.requireAuth();
1027
+ const orgId = resolveOrgId(auth, program);
1028
+
1029
+ if (!options.force && !program.opts().yes) {
1030
+ const { default: inquirer } = await import('inquirer');
1031
+ const ans = await inquirer.prompt([{
1032
+ type: 'confirm',
1033
+ name: 'confirm',
1034
+ message: `Delete folder ${folderId}? Its credentials move to no-folder.`,
1035
+ default: false,
1036
+ }]);
1037
+ if (!ans.confirm) {
1038
+ printInfo('Cancelled.');
1039
+ return;
1040
+ }
1041
+ }
1042
+
1043
+ const ep = resolveEndpoint('vault.folders.delete', { orgId, folderId });
1044
+ await client.delete(ep.path, { service: ep.service });
1045
+ printEvent(glyphLine('quox', 'credential folder deleted', { result: folderId, status: 'ok' }));
1046
+ });
1047
+
1048
+ // ── vault assign / unassign / assignments ──────────────────────────────────
1049
+ //
1050
+ // VAULT-INTEG P6: agent credential assignments (services/auth/routes/
1051
+ // agent-credentials.js) — which credential an agent uses for a given tool
1052
+ // type, ranked by priority. Not under the /vault/ namespace alias
1053
+ // server-side; these routes already live at their own path.
1054
+
1055
+ const isJsonOutput = () => program.opts().output === 'json' || program.opts().json === true;
1056
+
1057
+ /**
1058
+ * BLAST-RADIUS-P5: grant-time reach delta. Every vault assign/unassign
1059
+ * names an agent, so once the grant/revoke has landed, re-fetch that
1060
+ * agent's reach — the exact GET .../reach call `quox reach agent` uses —
1061
+ * and surface the one line that just changed.
1062
+ *
1063
+ * A failed reach fetch must never fail the assign/unassign: it downgrades
1064
+ * to one plain warning line (never rendered as data — an error body is
1065
+ * not a reach result) and the command still exits 0. In --json mode no
1066
+ * glyph line is printed either way; on success the caller folds the
1067
+ * returned object into the JSON payload as `reach_delta`, on failure it's
1068
+ * simply absent.
1069
+ */
1070
+ async function reachGrantDelta({ agentId, credentialRef, direction, unavailableReason }) {
1071
+ const json = isJsonOutput();
1072
+
1073
+ if (unavailableReason) {
1074
+ if (!json) {
1075
+ process.stdout.write(`\u{1F4A5} REACH │ grant delta unavailable (${unavailableReason})\n`);
1076
+ }
1077
+ return null;
1078
+ }
1079
+
1080
+ let result;
1081
+ try {
1082
+ const orgId = resolveOrgId(auth, program);
1083
+ const ep = resolveEndpoint('reach.agent.get', { orgId, agentId });
1084
+ result = await client.get(ep.path, { service: ep.service });
1085
+ } catch (err) {
1086
+ if (!json) {
1087
+ process.stdout.write(
1088
+ `\u{1F4A5} REACH │ grant delta unavailable (${err?.message || 'reach fetch failed'})\n`
1089
+ );
1090
+ }
1091
+ return null;
1092
+ }
1093
+
1094
+ const sources = result.sources || {};
1095
+ const badge = result.badge || { level: 'R0' };
1096
+ const credentials = sources.credentials?.count ?? 0;
1097
+ const matched = (result.may?.credentials || []).find((c) => c && c.id === credentialRef);
1098
+ const credRef = matched?.name || credentialRef;
1099
+ const verb = direction === 'assign' ? 'now in reach' : 'no longer in reach';
1100
+
1101
+ if (!json) {
1102
+ process.stdout.write(
1103
+ `\u{1F4A5} REACH │ grant delta ${agentId} │ credentials ${credentials} · ${badge.level} · ${credRef} ${verb}\n`
1104
+ );
1105
+ }
1106
+
1107
+ return { agent_id: agentId, credentials, level: badge.level, credential: credRef, direction };
1108
+ }
1109
+
1110
+ vault
1111
+ .command('assign')
1112
+ .description('Assign a credential to an agent for a tool type')
1113
+ .requiredOption('--agent <agentId>', 'agent ID')
1114
+ .requiredOption('--credential <credentialId>', 'credential ID')
1115
+ .requiredOption('--tool-type <toolType>', 'tool type this assignment covers')
1116
+ .option('--priority <n>', 'priority when multiple assignments match (higher wins)', '0')
1117
+ .action(async (options) => {
1118
+ auth.requireAuth();
1119
+ const orgId = resolveOrgId(auth, program);
1120
+ const priority = parseInt(options.priority, 10);
1121
+ const ep = resolveEndpoint('vault.agent-assignments.create', { orgId });
1122
+ const result = await client.post(ep.path, {
1123
+ service: ep.service,
1124
+ body: {
1125
+ agent_id: options.agent,
1126
+ credential_id: options.credential,
1127
+ tool_type: options.toolType,
1128
+ priority: Number.isFinite(priority) ? priority : 0,
1129
+ },
1130
+ });
1131
+ printEvent(events.assigned('quox', 'vault credential', `${options.agent} · ${options.toolType}`));
1132
+
1133
+ // BLAST-RADIUS-P5: assign always names an agent (--agent is required),
1134
+ // so the delta fires unconditionally here.
1135
+ const reachDelta = await reachGrantDelta({
1136
+ agentId: options.agent,
1137
+ credentialRef: options.credential,
1138
+ direction: 'assign',
1139
+ });
1140
+
1141
+ const detail = isJsonOutput() && reachDelta ? { ...result, reach_delta: reachDelta } : result;
1142
+ formatDetail(detail, { format: program.opts().output });
1143
+ });
1144
+
1145
+ vault
1146
+ .command('unassign')
1147
+ .description('Remove an agent credential assignment')
1148
+ .argument('<assignmentId>', 'assignment ID')
1149
+ .action(async (assignmentId) => {
1150
+ auth.requireAuth();
1151
+ const orgId = resolveOrgId(auth, program);
1152
+
1153
+ // BLAST-RADIUS-P5: unassign only takes the assignment ID, but the
1154
+ // reach delta needs to know WHICH agent lost the credential. Look the
1155
+ // assignment up (best effort, before it's deleted) via the same list
1156
+ // endpoint `quox vault assignments` already uses — no new endpoint.
1157
+ let targetAgentId = null;
1158
+ let credentialRef = assignmentId;
1159
+ let lookupFailure = null;
1160
+ try {
1161
+ const listEp = resolveEndpoint('vault.agent-assignments.list', { orgId });
1162
+ const data = await client.get(listEp.path, { service: listEp.service });
1163
+ const assignments = data.assignments || data;
1164
+ const match = Array.isArray(assignments) ? assignments.find((a) => a && a.id === assignmentId) : null;
1165
+ if (match) {
1166
+ targetAgentId = match.agent_id;
1167
+ credentialRef = match.label || match.credential_id || assignmentId;
1168
+ } else {
1169
+ lookupFailure = `could not resolve the agent for assignment ${assignmentId}`;
1170
+ }
1171
+ } catch (err) {
1172
+ lookupFailure = err?.message || `could not resolve the agent for assignment ${assignmentId}`;
1173
+ }
1174
+
1175
+ const ep = resolveEndpoint('vault.agent-assignments.delete', { orgId, id: assignmentId });
1176
+ await client.delete(ep.path, { service: ep.service });
1177
+ printEvent(glyphLine('quox', 'agent credential assignment removed', { result: assignmentId, status: 'ok' }));
1178
+
1179
+ const reachDelta = await reachGrantDelta({
1180
+ agentId: targetAgentId,
1181
+ credentialRef,
1182
+ direction: 'unassign',
1183
+ unavailableReason: targetAgentId ? null : lookupFailure,
1184
+ });
1185
+
1186
+ if (isJsonOutput() && reachDelta) {
1187
+ formatDetail({ deleted: true, id: assignmentId, reach_delta: reachDelta }, { format: program.opts().output });
1188
+ }
1189
+ });
1190
+
1191
+ vault
1192
+ .command('assignments')
1193
+ .description('List agent credential assignments')
1194
+ .option('--agent <agentId>', 'filter by agent ID')
1195
+ .option('--tool-type <toolType>', 'filter by tool type')
1196
+ .action(async (options) => {
1197
+ auth.requireAuth();
1198
+ const orgId = resolveOrgId(auth, program);
1199
+ const ep = resolveEndpoint('vault.agent-assignments.list', { orgId });
1200
+ const query = {};
1201
+ if (options.agent) query.agent_id = options.agent;
1202
+ if (options.toolType) query.tool_type = options.toolType;
1203
+ const data = await client.get(ep.path, { service: ep.service, query });
1204
+ const assignments = data.assignments || data;
1205
+
1206
+ formatOutput({
1207
+ columns: ['id', 'agent_id', 'label', 'integration_type', 'tool_type', 'priority'],
1208
+ headers: ['ID', 'Agent', 'Credential', 'Type', 'Tool Type', 'Priority'],
1209
+ rows: assignments,
1210
+ }, { format: program.opts().output, emptyMessage: 'No agent credential assignments found.' });
1211
+ });
1212
+
1213
+ // ── vault lease ─────────────────────────────────────────────────────────────
1214
+ //
1215
+ // VAULT-INTEG P6: just-in-time credential leases (services/auth/routes/
1216
+ // credential-leases.js) — an agent requests time-boxed access to a
1217
+ // lease-required credential; a human approves/denies via HITL inbox or
1218
+ // directly here.
1219
+
1220
+ const lease = vault
1221
+ .command('lease')
1222
+ .description('Just-in-time credential leases for agent access');
1223
+
1224
+ lease
1225
+ .command('request')
1226
+ .description('Request a lease on a credential (creates a pending HITL approval)')
1227
+ .requiredOption('--credential <credentialId>', 'credential ID')
1228
+ .requiredOption('--agent <agentId>', 'requesting agent ID')
1229
+ .option('--task <taskId>', 'associated task ID')
1230
+ .option('--justification <text>', 'why this lease is needed (plaintext, max 500 chars)')
1231
+ .option('--context <json>', 'JSON object of extra context (max 10KB)')
1232
+ .action(async (options) => {
1233
+ auth.requireAuth();
1234
+ const orgId = resolveOrgId(auth, program);
1235
+
1236
+ let context;
1237
+ if (options.context) {
1238
+ try {
1239
+ context = JSON.parse(options.context);
1240
+ } catch (err) {
1241
+ throw new Error(`--context must be a JSON object: ${err.message}`);
1242
+ }
1243
+ }
1244
+
1245
+ const ep = resolveEndpoint('vault.leases.request', { orgId });
1246
+ const result = await client.post(ep.path, {
1247
+ service: ep.service,
1248
+ body: {
1249
+ credential_id: options.credential,
1250
+ agent_id: options.agent,
1251
+ task_id: options.task,
1252
+ justification: options.justification,
1253
+ context,
1254
+ },
1255
+ });
1256
+ printEvent(events.created('quox', 'credential lease request', result.id));
1257
+ formatDetail(result, { format: program.opts().output });
1258
+ });
1259
+
1260
+ lease
1261
+ .command('list')
1262
+ .description('List credential leases')
1263
+ .option('--status <status>', 'filter by status (pending|active|denied|revoked|expired)')
1264
+ .option('--agent <agentId>', 'filter by agent ID')
1265
+ .option('--credential <credentialId>', 'filter by credential ID')
1266
+ .option('--limit <n>', 'max results', '50')
1267
+ .action(async (options) => {
1268
+ auth.requireAuth();
1269
+ const orgId = resolveOrgId(auth, program);
1270
+ const ep = resolveEndpoint('vault.leases.list', { orgId });
1271
+ const data = await client.get(ep.path, {
1272
+ service: ep.service,
1273
+ query: {
1274
+ status: options.status,
1275
+ agent_id: options.agent,
1276
+ credential_id: options.credential,
1277
+ limit: options.limit,
1278
+ },
1279
+ });
1280
+ const leases = data.leases || data;
1281
+
1282
+ // VAULT-P5.M parity: surface the org's actual enforcement mode rather
1283
+ // than implying the backend gates every lease-required credential.
1284
+ if (data.enforcement_mode && program.opts().output !== 'json') {
1285
+ printInfo(`lease enforcement mode: ${data.enforcement_mode}`);
1286
+ }
1287
+
1288
+ formatOutput({
1289
+ columns: ['id', 'credential_label', 'requested_by_agent_id', 'status', 'expires_at'],
1290
+ headers: ['ID', 'Credential', 'Agent', 'Status', 'Expires'],
1291
+ rows: leases,
1292
+ }, { format: program.opts().output, emptyMessage: 'No leases found.' });
1293
+ });
1294
+
1295
+ lease
1296
+ .command('approve')
1297
+ .description('Approve a pending lease with a duration')
1298
+ .argument('<leaseId>', 'lease ID')
1299
+ .requiredOption('--duration <minutes>', 'lease duration in minutes')
1300
+ .action(async (leaseId, options) => {
1301
+ auth.requireAuth();
1302
+ const orgId = resolveOrgId(auth, program);
1303
+ const duration = parseInt(options.duration, 10);
1304
+ if (!Number.isFinite(duration) || duration < 1) {
1305
+ throw new Error('--duration must be a positive number of minutes.');
1306
+ }
1307
+ const ep = resolveEndpoint('vault.leases.approve', { orgId, leaseId });
1308
+ const result = await client.post(ep.path, { service: ep.service, body: { duration_minutes: duration } });
1309
+ printEvent(events.updated('quox', 'credential lease', leaseId, `approved for ${duration}m`));
1310
+ formatDetail(result, { format: program.opts().output });
1311
+ });
1312
+
1313
+ lease
1314
+ .command('deny')
1315
+ .description('Deny a pending lease')
1316
+ .argument('<leaseId>', 'lease ID')
1317
+ .action(async (leaseId) => {
1318
+ auth.requireAuth();
1319
+ const orgId = resolveOrgId(auth, program);
1320
+ const ep = resolveEndpoint('vault.leases.deny', { orgId, leaseId });
1321
+ const result = await client.post(ep.path, { service: ep.service });
1322
+ printEvent(events.updated('quox', 'credential lease', leaseId, 'denied'));
1323
+ formatDetail(result, { format: program.opts().output });
1324
+ });
1325
+
1326
+ lease
1327
+ .command('revoke')
1328
+ .description('Revoke an active lease immediately')
1329
+ .argument('<leaseId>', 'lease ID')
1330
+ .action(async (leaseId) => {
1331
+ auth.requireAuth();
1332
+ const orgId = resolveOrgId(auth, program);
1333
+ const ep = resolveEndpoint('vault.leases.revoke', { orgId, leaseId });
1334
+ const result = await client.post(ep.path, { service: ep.service });
1335
+ if (result?.revoked === false) {
1336
+ printWarning(`Lease ${leaseId} was not revoked (already inactive?).`);
1337
+ } else {
1338
+ printEvent(glyphLine('quox', 'credential lease revoked', { result: leaseId, status: 'ok' }));
1339
+ }
1340
+ });
1341
+
1342
+ // ── vault cross-org ─────────────────────────────────────────────────────────
1343
+ //
1344
+ // VAULT-INTEG P6: GET /me/cross-org-credentials — find credentials of a
1345
+ // type across every org the caller belongs to. Metadata only, no secrets.
1346
+
1347
+ vault
1348
+ .command('cross-org')
1349
+ .description('Find credentials of a type across every org you belong to (no secrets returned)')
1350
+ .requiredOption('--type <type>', 'integration type to look up')
1351
+ .action(async (options) => {
1352
+ auth.requireAuth();
1353
+ const ep = resolveEndpoint('vault.cross-org.list', {});
1354
+ const data = await client.get(ep.path, { service: ep.service, query: { type: options.type } });
1355
+ const availableIn = data.available_in || [];
1356
+
1357
+ formatOutput({
1358
+ columns: ['org_id', 'org_name', 'credential_id', 'label', 'is_default', 'test_status'],
1359
+ headers: ['Org ID', 'Org Name', 'Credential ID', 'Label', 'Default', 'Status'],
1360
+ rows: availableIn,
1361
+ }, { format: program.opts().output, emptyMessage: `No ${options.type} credentials found in any of your orgs.` });
1362
+ });
1363
+
1364
+ // ── vault health ────────────────────────────────────────────────────────────
1365
+ //
1366
+ // VAULT-INTEG P6: GET /internal/vault-health. This is a SERVICE-KEY-gated
1367
+ // route (checks the raw `x-service-key` header against INTERNAL_SERVICE_KEY,
1368
+ // not a user Bearer JWT) — unlike the removed `vault export` decrypt route,
1369
+ // the CLI client already has a legitimate service-key path (QUOX_SERVICE_KEY
1370
+ // env var, see lib/core/client.js), so this is wired rather than removed.
1371
+ // A normal logged-in user session sends a Bearer token instead and this
1372
+ // route will 401 it — that is not a bug, it is the route's design; run
1373
+ // with QUOX_SERVICE_KEY set to the instance's INTERNAL_SERVICE_KEY (or
1374
+ // QUOX_FORCE_SERVICE_KEY=1 if a user session is also stored) to use it.
1375
+
1376
+ vault
1377
+ .command('health')
1378
+ .description('Vault health summary: total, healthy, failed, stale, expiring credentials')
1379
+ .option('--org-id <orgId>', 'with QUOX_SERVICE_KEY only: org ID to scope the summary to (server default: "default")')
1380
+ .action(async (options) => {
1381
+ // #914: the internal route accepts only the service key, so an owner's `vault health` was a 401
1382
+ // "invalid service key". Automation (QUOX_SERVICE_KEY) keeps that route; a signed-in person reads
1383
+ // the same numbers from their org's vault stats.
1384
+ if (process.env.QUOX_SERVICE_KEY) {
1385
+ const ep = resolveEndpoint('vault.health', {});
1386
+ const data = await client.get(ep.path, {
1387
+ service: ep.service,
1388
+ query: options.orgId ? { org_id: options.orgId } : undefined,
1389
+ });
1390
+ formatDetail(data, { format: program.opts().output, title: 'QuoxVault Health' });
1391
+ return;
1392
+ }
1393
+ auth.requireAuth();
1394
+ const orgId = resolveOrgId(auth, program);
1395
+ const ep = resolveEndpoint('vault.vault-stats.list', { orgId });
1396
+ const st = await client.get(ep.path, { service: ep.service });
1397
+ const summary = {
1398
+ total: st.total ?? 0,
1399
+ healthy: st.healthy ?? 0,
1400
+ failed: st.failed ?? 0,
1401
+ untested: st.untested ?? 0,
1402
+ stale: st.stale ?? 0,
1403
+ expiring_soon: st.expiring ?? 0,
1404
+ rotation_overdue: st.rotationOverdue ?? 0,
1405
+ source: 'org vault stats',
1406
+ };
1407
+ formatDetail(summary, { format: program.opts().output, title: 'QuoxVault Health' });
1408
+ });
1409
+
1410
+ // ── vault export (removed — WP0.2) ──────────────────────────────────────────
1411
+ //
1412
+ // REMOVED (WP0.2): this command decrypted credentials via
1413
+ // POST /internal/vault/:id/decrypt, which aliases to
1414
+ // /internal/integrations/:id/decrypt — a SERVICE-KEY-gated route
1415
+ // (authenticateApiKey(), expects `x-api-key: msk_...`). The CLI only
1416
+ // ever sends a user Bearer JWT, so this call would 401 in production.
1417
+ // There is no legitimate service-key mode wired into the CLI client,
1418
+ // so the command is removed rather than silently broken.
1419
+
1420
+ // ── vault migrate ───────────────────────────────────────────────────────────
1421
+
1422
+ vault
1423
+ .command('migrate')
1424
+ .description('Migrate legacy instance_config keys to QuoxVault')
1425
+ .option('--dry-run', 'show what would be migrated without making changes')
1426
+ .action(async (options) => {
1427
+ auth.requireAuth();
1428
+
1429
+ const spinner = ora('Running key migration...').start();
1430
+
1431
+ try {
1432
+ // Shell out to the migration script
1433
+ const { execSync } = await import('child_process');
1434
+ const scriptPath = new URL('../../../services/auth/scripts/migrate-keys-to-vault.js', import.meta.url).pathname;
1435
+ const args = options.dryRun ? '--dry-run' : '';
1436
+ const output = execSync(`node ${scriptPath} ${args}`, {
1437
+ encoding: 'utf-8',
1438
+ timeout: 30000,
1439
+ });
1440
+ spinner.stop();
1441
+ console.log(output);
1442
+ } catch (err) {
1443
+ spinner.fail('Migration failed.');
1444
+ throw new Error(err.stderr || err.message);
1445
+ }
1446
+ });
1447
+ }