quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -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/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Admin commands: config, keys, services, logs, stats, retention
3
+ */
4
+
5
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { resolveEndpoint } from '../api/endpoints.js';
7
+
8
+ export function registerAdminCommands(program, ctx) {
9
+ const { client, auth } = ctx;
10
+
11
+ const admin = program
12
+ .command('admin')
13
+ .description('Platform administration');
14
+
15
+ // Config
16
+ const cfg = admin
17
+ .command('config')
18
+ .description('Manage platform configuration');
19
+
20
+ cfg
21
+ .command('get')
22
+ .description('Get platform config (all keys, or one key)')
23
+ .argument('[key]', 'show only this key')
24
+ .action(async (key) => {
25
+ auth.requireAuth();
26
+ const data = await client.get(resolveEndpoint('admin.config.list').path, { service: resolveEndpoint('admin.config.list').service });
27
+ const config = data.config || data;
28
+ if (key) {
29
+ // quox#533: `get <key>` used to be rejected outright.
30
+ if (!config || typeof config !== 'object' || !(key in config)) {
31
+ throw new Error(`config key "${key}" not found. Run \`quox admin config get\` to list all keys.`);
32
+ }
33
+ formatDetail({ [key]: config[key] }, { format: program.opts().output, title: 'Platform Config' });
34
+ return;
35
+ }
36
+ formatDetail(config, { format: program.opts().output, title: 'Platform Config' });
37
+ });
38
+
39
+ cfg
40
+ .command('set')
41
+ .description('Set a config value')
42
+ .argument('<key>', 'config key')
43
+ .argument('<value>', 'config value')
44
+ .action(async (key, value) => {
45
+ auth.requireAuth();
46
+ await client.patch(resolveEndpoint('admin.config.update', { key }).path, { service: resolveEndpoint('admin.config.update', { key }).service, body: { value } });
47
+ printSuccess(`Config ${key} updated.`);
48
+ });
49
+
50
+ // LICENSE-LAUNCH LL-6b: enforcement posture + break-glass. Parity with the
51
+ // dashboard's Settings → Security → Enforcement panel. In production a
52
+ // security gate switched off in the environment is enforced anyway unless an
53
+ // unexpired break-glass record exists, so an operator needs to see and set
54
+ // those records from the CLI too, not only from the browser.
55
+ const enforcement = admin
56
+ .command('enforcement')
57
+ .description('Security enforcement posture and break-glass windows');
58
+
59
+ enforcement
60
+ .command('list')
61
+ .description('Show every enforcement flag: what the environment asks for and what is actually enforced')
62
+ .action(async () => {
63
+ auth.requireAuth();
64
+ const ep = resolveEndpoint('admin.enforcement.list');
65
+ const data = await client.get(ep.path, { service: ep.service });
66
+ const rows = (data.flags || []).map((f) => ({
67
+ key: f.key,
68
+ env: f.env_enforced ? 'enforce' : 'off',
69
+ effective: f.forced_on ? 'ENFORCED (forced)' : (f.enforced ? 'enforced' : 'off'),
70
+ break_glass: f.break_glass ? `until ${f.break_glass.expires_at}` : '-',
71
+ reason: f.break_glass ? f.break_glass.reason : '',
72
+ }));
73
+ formatOutput({
74
+ columns: ['key', 'env', 'effective', 'break_glass', 'reason'],
75
+ headers: ['Flag', 'Env', 'Effective', 'Break-glass', 'Reason'],
76
+ rows,
77
+ }, { format: program.opts().output });
78
+ });
79
+
80
+ enforcement
81
+ .command('break-glass')
82
+ .description('Open a bounded break-glass window so a gate may stay off in production')
83
+ .argument('<flag>', 'enforcement flag, e.g. COLLECTOR_AUTH_ENFORCE')
84
+ .requiredOption('--reason <reason>', 'why this gate has to be off (recorded against your account)')
85
+ .option('--hours <hours>', 'window length in hours, 72 maximum', '4')
86
+ .action(async (flag, opts) => {
87
+ auth.requireAuth();
88
+ const hours = Number(opts.hours);
89
+ if (!Number.isFinite(hours) || hours <= 0) {
90
+ throw new Error('--hours must be a positive number of hours (72 maximum)');
91
+ }
92
+ const ep = resolveEndpoint('admin.enforcement.break_glass.open');
93
+ const data = await client.post(ep.path, {
94
+ service: ep.service,
95
+ body: {
96
+ flag,
97
+ reason: opts.reason,
98
+ expires_at: new Date(Date.now() + hours * 3600_000).toISOString(),
99
+ },
100
+ });
101
+ printSuccess(`Break-glass open for ${flag} until ${data.break_glass.expires_at}.`);
102
+ });
103
+
104
+ enforcement
105
+ .command('clear')
106
+ .description('Close a break-glass window early')
107
+ .argument('<flag>', 'enforcement flag, e.g. COLLECTOR_AUTH_ENFORCE')
108
+ .action(async (flag) => {
109
+ auth.requireAuth();
110
+ const ep = resolveEndpoint('admin.enforcement.break_glass.clear', { flag });
111
+ const data = await client.delete(ep.path, { service: ep.service });
112
+ printSuccess(data.cleared
113
+ ? `Break-glass closed for ${flag}.`
114
+ : `No break-glass window was open for ${flag}.`);
115
+ });
116
+
117
+ // QUOXPERTICUS P2: governed agent-identity mint (instance-admin only).
118
+ const agentIdentity = admin
119
+ .command('agent-identity')
120
+ .description('Manage governed agent identities (agent-typed user + bound mcp token)');
121
+
122
+ agentIdentity
123
+ .command('mint')
124
+ .description('Mint (or rotate) an agent identity; prints the raw token ONCE')
125
+ .argument('<agentId>', 'agent id, e.g. quoxperticus')
126
+ // NOT requiredOption: --org shadows the root program's global --org
127
+ // <slug> (bin/quox.js). Commander's mandatory-option check runs BEFORE
128
+ // the mergeShadowedGlobals preAction hook, so a requiredOption here
129
+ // always fired "required option '--org' not specified" even when the
130
+ // user supplied it (quox#457 sweep). Validated explicitly below instead.
131
+ .option('--org <orgId>', 'org ULID the agent operates under')
132
+ .option('--rotate', 'revoke the live token and issue a new one')
133
+ .action(async (agentId, opts) => {
134
+ auth.requireAuth();
135
+ if (!opts.org || !String(opts.org).trim()) {
136
+ throw new Error(
137
+ 'Missing --org <orgId>. Usage: quox admin agent-identity mint <agentId> --org <orgId>'
138
+ );
139
+ }
140
+ const ep = resolveEndpoint('admin.agent_identity.mint');
141
+ const data = await client.post(ep.path, {
142
+ service: ep.service,
143
+ body: { agent_id: agentId, org_id: opts.org, rotate: opts.rotate === true },
144
+ });
145
+ printSuccess(`Agent identity ${data.agent_id} ${data.rotated ? 'rotated' : 'minted'} (user ${data.user_id}).`);
146
+ console.log('Raw token (shown ONCE, place it in the supervisor root-only env):');
147
+ console.log(data.token);
148
+ });
149
+
150
+ // Email / SMTP (QCPRO-SMTP): instance email onboarding parity
151
+ const email = admin
152
+ .command('email')
153
+ .description('Instance email delivery (SMTP)');
154
+
155
+ email
156
+ .command('status')
157
+ .description('Show email delivery status (configured, from, last test)')
158
+ .action(async () => {
159
+ auth.requireAuth();
160
+ const ep = resolveEndpoint('admin.email.status');
161
+ const data = await client.get(ep.path, { service: ep.service });
162
+ formatDetail(data, { format: program.opts().output, title: 'Email Delivery' });
163
+ });
164
+
165
+ email
166
+ .command('setup')
167
+ .description('Configure instance SMTP (password encrypted at rest; omit --pass to keep the stored one)')
168
+ .requiredOption('--host <host>', 'SMTP host')
169
+ .option('--port <port>', 'SMTP port', '587')
170
+ .option('--user <user>', 'SMTP username')
171
+ .option('--pass <pass>', 'SMTP password')
172
+ .requiredOption('--from <from>', 'From address, e.g. "QuoxCORE <noreply@acme.com>"')
173
+ .option('--secure', 'Use TLS (direct TLS for 465, STARTTLS otherwise)', true)
174
+ .option('--no-secure', 'Disable TLS')
175
+ .action(async (opts) => {
176
+ auth.requireAuth();
177
+ const ep = resolveEndpoint('admin.email.smtp');
178
+ await client.put(ep.path, {
179
+ service: ep.service,
180
+ body: {
181
+ host: opts.host,
182
+ port: Number(opts.port),
183
+ user: opts.user || '',
184
+ pass: opts.pass || '',
185
+ from: opts.from,
186
+ secure: !!opts.secure,
187
+ },
188
+ });
189
+ printSuccess('SMTP configured. Run "quox admin email test" to verify delivery.');
190
+ });
191
+
192
+ email
193
+ .command('test')
194
+ .description('Send a test email and report the honest outcome')
195
+ .option('--to <address>', 'Recipient (defaults to your account email)')
196
+ .action(async (opts) => {
197
+ auth.requireAuth();
198
+ const ep = resolveEndpoint('admin.email.test');
199
+ const result = await client.post(ep.path, { service: ep.service, body: opts.to ? { to: opts.to } : {} });
200
+ if (result.delivered) {
201
+ printSuccess(`Delivered (message id ${result.message_id || 'unknown'}).`);
202
+ } else {
203
+ console.error(`Not delivered: ${result.reason || 'unknown error'}`);
204
+ process.exitCode = 1;
205
+ }
206
+ });
207
+
208
+ // API Keys
209
+ const keys = admin
210
+ .command('key')
211
+ .description('Manage API keys');
212
+
213
+ keys
214
+ .command('list')
215
+ .description('List API keys')
216
+ .action(async () => {
217
+ auth.requireAuth();
218
+ const data = await client.get(resolveEndpoint('admin.keys.list').path, { service: resolveEndpoint('admin.keys.list').service });
219
+ formatOutput({
220
+ // fleet #392: api_keys has key_preview/last_used_at; prefix, role
221
+ // and expires_at don't exist on this row at all.
222
+ columns: ['id', 'name', 'key_preview', 'last_used_at', 'created_at'],
223
+ headers: ['ID', 'Name', 'Preview', 'Last Used', 'Created'],
224
+ rows: data.keys || data,
225
+ }, { format: program.opts().output });
226
+ });
227
+
228
+ keys
229
+ .command('create')
230
+ .description('Create an API key')
231
+ .requiredOption('--name <name>', 'key name')
232
+ .option('--role <role>', 'key role', 'viewer')
233
+ .option('--expires <date>', 'expiry date (ISO)')
234
+ .action(async (options) => {
235
+ auth.requireAuth();
236
+ const body = { name: options.name, role: options.role };
237
+ if (options.expires) body.expires_at = options.expires;
238
+ const data = await client.post(resolveEndpoint('admin.keys.create').path, { service: resolveEndpoint('admin.keys.create').service, body });
239
+ if (data.key || data.api_key) {
240
+ const key = data.key || data.api_key;
241
+ if (program.opts().output === 'json') {
242
+ console.log(JSON.stringify({ key, id: data.id, name: data.name }));
243
+ } else if (program.opts().output === 'quiet') {
244
+ console.log(key);
245
+ } else {
246
+ console.log(`API Key: ${key}`);
247
+ console.log('(Save this key now — it will not be shown again)');
248
+ }
249
+ } else {
250
+ printSuccess('API key created.');
251
+ }
252
+ });
253
+
254
+ keys
255
+ .command('delete')
256
+ .description('Delete an API key')
257
+ .argument('<keyId>', 'key ID')
258
+ .action(async (keyId) => {
259
+ auth.requireAuth();
260
+ await client.delete(resolveEndpoint('admin.keys.delete', { keyId }).path, { service: resolveEndpoint('admin.keys.delete', { keyId }).service });
261
+ printSuccess('API key deleted.');
262
+ });
263
+
264
+ keys
265
+ .command('test')
266
+ .description('Test an API key')
267
+ .argument('<keyId>', 'key ID')
268
+ .action(async (keyId) => {
269
+ auth.requireAuth();
270
+ const data = await client.post(resolveEndpoint('admin.keys.test', { keyId }).path, { service: resolveEndpoint('admin.keys.test', { keyId }).service });
271
+ printSuccess(data.valid ? 'API key is valid.' : 'API key test failed.');
272
+ });
273
+
274
+ // Services
275
+ const services = admin
276
+ .command('service')
277
+ .description('Manage platform services');
278
+
279
+ services
280
+ .command('list')
281
+ .description('List registered services')
282
+ .action(async () => {
283
+ auth.requireAuth();
284
+ const data = await client.get(resolveEndpoint('admin.services.list').path, { service: resolveEndpoint('admin.services.list').service });
285
+ formatOutput({
286
+ // fleet #392: services has health_endpoint/last_check_at, not
287
+ // url/last_check.
288
+ columns: ['id', 'name', 'status', 'health_endpoint', 'last_check_at'],
289
+ headers: ['ID', 'Name', 'Status', 'Health URL', 'Last Check'],
290
+ rows: data.services || data,
291
+ }, { format: program.opts().output });
292
+ });
293
+
294
+ // Logs
295
+ admin
296
+ .command('logs')
297
+ .description('View admin audit logs')
298
+ .option('--action <action>', 'filter by action')
299
+ .option('--limit <n>', 'max results', '50')
300
+ .action(async (options) => {
301
+ auth.requireAuth();
302
+ const query = { limit: options.limit };
303
+ if (options.action) query.action = options.action;
304
+ const data = await client.get(resolveEndpoint('admin.logs.list').path, { service: resolveEndpoint('admin.logs.list').service, query });
305
+ formatOutput({
306
+ // fleet #392: system_logs has { created_at, service, level, message,
307
+ // user_id } — none of timestamp/user_email/action/detail exist, so
308
+ // all four columns were dashes.
309
+ columns: ['created_at', 'service', 'level', 'message', 'user_id'],
310
+ headers: ['Time', 'Service', 'Level', 'Message', 'User'],
311
+ rows: data.logs || data.entries || data,
312
+ }, { format: program.opts().output });
313
+ });
314
+
315
+ // Stats
316
+ admin
317
+ .command('stats')
318
+ .description('Show platform statistics')
319
+ .action(async () => {
320
+ auth.requireAuth();
321
+ const data = await client.get(resolveEndpoint('admin.stats.list').path, { service: resolveEndpoint('admin.stats.list').service });
322
+ formatDetail(data.stats || data, { format: program.opts().output, title: 'Platform Stats' });
323
+ });
324
+
325
+ // Retention
326
+ const retention = admin
327
+ .command('retention')
328
+ .description('Manage data retention');
329
+
330
+ retention
331
+ .command('config')
332
+ .description('View retention config')
333
+ .action(async () => {
334
+ auth.requireAuth();
335
+ const data = await client.get(resolveEndpoint('admin.retention.config.list').path, { service: resolveEndpoint('admin.retention.config.list').service });
336
+ formatDetail(data.config || data, { format: program.opts().output, title: 'Retention Config' });
337
+ });
338
+
339
+ retention
340
+ .command('cleanup')
341
+ .description('Run retention cleanup')
342
+ .option('--dry-run', 'preview without deleting')
343
+ .action(async (options) => {
344
+ auth.requireAuth();
345
+ const body = {};
346
+ if (options.dryRun) body.dry_run = true;
347
+ const data = await client.post(resolveEndpoint('admin.retention.cleanup').path, { service: resolveEndpoint('admin.retention.cleanup').service, body });
348
+ formatDetail(data, { format: program.opts().output, title: 'Cleanup Result' });
349
+ });
350
+
351
+ retention
352
+ .command('stats')
353
+ .description('View retention statistics')
354
+ .action(async () => {
355
+ auth.requireAuth();
356
+ const data = await client.get(resolveEndpoint('admin.retention.stats.list').path, { service: resolveEndpoint('admin.retention.stats.list').service });
357
+ formatDetail(data.stats || data, { format: program.opts().output, title: 'Retention Stats' });
358
+ });
359
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * AEE (Agent Envelope Exchange) commands
3
+ */
4
+
5
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { parseJSON } from './helpers.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+
9
+ export function registerAeeCommands(program, ctx) {
10
+ const { client, auth } = ctx;
11
+
12
+ const aee = program
13
+ .command('aee')
14
+ .description('Agent Envelope Exchange protocol');
15
+
16
+ aee
17
+ .command('store')
18
+ .description('Store an AEE envelope')
19
+ .requiredOption('--envelope <json>', 'envelope data as JSON')
20
+ .action(async (options) => {
21
+ auth.requireAuth();
22
+ const envelope = parseJSON(options.envelope, '--envelope');
23
+ await client.post(resolveEndpoint('aee.store').path, { service: resolveEndpoint('aee.store').service, body: envelope });
24
+ printSuccess('Envelope stored.');
25
+ });
26
+
27
+ aee
28
+ .command('get')
29
+ .description('Get an envelope by ID')
30
+ .argument('<id>', 'envelope ID')
31
+ .action(async (id) => {
32
+ auth.requireAuth();
33
+ const data = await client.get(resolveEndpoint('aee.get', { id }).path, { service: resolveEndpoint('aee.get', { id }).service });
34
+ formatDetail(data.envelope || data, { format: program.opts().output });
35
+ });
36
+
37
+ aee
38
+ .command('query')
39
+ .description('Query envelopes')
40
+ // fleet #386 + adjacent fix: --from/--to were described as dates, but the
41
+ // route matches them against the envelope's "from"/"to" columns — the AEE
42
+ // sender and recipient (from: 'human.adam', to: 'agent.cipher'). Passing a
43
+ // date matched a sender literally named "2026-01-01", i.e. nothing. The
44
+ // route's real date filters are after/before, which the CLI never sent, so
45
+ // date filtering was unreachable. Descriptions now match the protocol, and
46
+ // --after/--before/--intent expose filters the route already supported.
47
+ .option('--from <sender>', 'envelope sender (e.g. human.adam, agent.cipher)')
48
+ .option('--to <recipient>', 'envelope recipient')
49
+ .option('--after <date>', 'only envelopes at/after this ISO timestamp')
50
+ .option('--before <date>', 'only envelopes before this ISO timestamp')
51
+ .option('--intent <intent>', 'intent substring match (e.g. quox.chat)')
52
+ .option('--corr <id>', 'correlation ID — the envelopes of one conversation (see `aee conversations`)')
53
+ .option('--type <type>', 'envelope type')
54
+ .option('--limit <n>', 'max results', '50')
55
+ .action(async (options) => {
56
+ auth.requireAuth();
57
+ const query = { limit: options.limit };
58
+ if (options.from) query.from = options.from;
59
+ if (options.to) query.to = options.to;
60
+ if (options.after) query.after = options.after;
61
+ if (options.before) query.before = options.before;
62
+ if (options.intent) query.intent = options.intent;
63
+ if (options.corr) query.corr = options.corr;
64
+ if (options.type) query.type = options.type;
65
+ const data = await client.get(resolveEndpoint('aee.query.list').path, { service: resolveEndpoint('aee.query.list').service, query });
66
+ formatOutput({
67
+ columns: ['id', 'type', 'from', 'to', 'intent', 'ts'],
68
+ headers: ['ID', 'Type', 'From', 'To', 'Intent', 'Time'],
69
+ rows: data.envelopes || data.results || data,
70
+ }, { format: program.opts().output });
71
+ });
72
+
73
+ aee
74
+ .command('conversations')
75
+ .description('List AEE conversations')
76
+ .option('--limit <n>', 'max results', '20')
77
+ .action(async (options) => {
78
+ auth.requireAuth();
79
+ const data = await client.get(resolveEndpoint('aee.conversations.list').path, { service: resolveEndpoint('aee.conversations.list').service, query: { limit: options.limit } });
80
+ formatOutput({
81
+ // fleet #385: the endpoint returns { corr, first_ts, last_ts, count }
82
+ // (see the GROUP BY corr query in collector/server.js). This declared
83
+ // corr_id/envelope_count, which don't exist in the payload, so two of
84
+ // three columns rendered as dashes with no error.
85
+ columns: ['corr', 'count', 'last_ts'],
86
+ headers: ['Conversation', 'Envelopes', 'Last Activity'],
87
+ rows: data.conversations || data,
88
+ }, { format: program.opts().output });
89
+ });
90
+
91
+ aee
92
+ .command('stats')
93
+ .description('AEE statistics')
94
+ .action(async () => {
95
+ auth.requireAuth();
96
+ const data = await client.get(resolveEndpoint('aee.stats.list').path, { service: resolveEndpoint('aee.stats.list').service });
97
+ formatDetail(data.stats || data, { format: program.opts().output, title: 'AEE Stats' });
98
+ });
99
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Agent commands: CRUD, activate/deactivate, tool assignments
3
+ */
4
+
5
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { resolveOrgId } from './helpers.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { registerAgentAttachCommands } from './agentAttach.js';
9
+
10
+ export function registerAgentCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const agent = program
14
+ .command('agent')
15
+ .description('Manage agents');
16
+
17
+ agent
18
+ .command('list')
19
+ .description('List agents')
20
+ .option('--status <status>', 'filter by status')
21
+ .option('--level <level>', 'filter by level (0-4)')
22
+ .action(async (options) => {
23
+ auth.requireAuth();
24
+ const orgId = resolveOrgId(auth, program);
25
+ const query = {};
26
+ if (options.status) query.status = options.status;
27
+ if (options.level) query.level = options.level;
28
+ // fleet #392: tool_count is only computed when includeTools=true, which
29
+ // was never sent; and rows carry a `domains` ARRAY, not a `domain`.
30
+ query.includeTools = 'true';
31
+ const data = await client.get(resolveEndpoint('agents.list', { orgId }).path, { service: resolveEndpoint('agents.list', { orgId }).service, query });
32
+ formatOutput({
33
+ columns: ['id', 'agent_id', 'name', 'level', 'status', 'domains', 'tool_count'],
34
+ headers: ['ID', 'Agent ID', 'Name', 'Level', 'Status', 'Domains', 'Tools'],
35
+ rows: (data.agents || data || []).map(a => ({
36
+ ...a,
37
+ domains: Array.isArray(a.domains) ? a.domains.join(', ') : (a.domains ?? '-'),
38
+ })),
39
+ }, { format: program.opts().output });
40
+ });
41
+
42
+ agent
43
+ .command('get')
44
+ .description('Get agent details')
45
+ .argument('<agentId>', 'agent ID')
46
+ .action(async (agentId) => {
47
+ auth.requireAuth();
48
+ const orgId = resolveOrgId(auth, program);
49
+ const data = await client.get(resolveEndpoint('agents.get', { orgId, agentId }).path, { service: resolveEndpoint('agents.get', { orgId, agentId }).service });
50
+ formatDetail(data.agent || data, {
51
+ format: program.opts().output,
52
+ title: data.agent?.name || data.name,
53
+ });
54
+ });
55
+
56
+ agent
57
+ .command('create')
58
+ .description('Create a new agent')
59
+ .requiredOption('--name <name>', 'agent display name')
60
+ .option('--agent-id <id>', 'machine identifier')
61
+ .option('--level <level>', 'agent level (0-4)', '2')
62
+ .option('--domain <domain>', 'agent domain')
63
+ .option('--description <desc>', 'description')
64
+ .option('--parent <parentId>', 'parent agent ID')
65
+ .option('--manifest-id <id>', 'manifest ID')
66
+ .action(async (options) => {
67
+ auth.requireAuth();
68
+ const orgId = resolveOrgId(auth, program);
69
+ const body = {
70
+ name: options.name,
71
+ level: parseInt(options.level, 10),
72
+ };
73
+ if (options.agentId) body.agent_id = options.agentId;
74
+ if (options.domain) body.domain = options.domain;
75
+ if (options.description) body.description = options.description;
76
+ if (options.parent) body.parent_id = options.parent;
77
+ if (options.manifestId) body.manifest_id = options.manifestId;
78
+ await client.post(resolveEndpoint('agents.create', { orgId }).path, { service: resolveEndpoint('agents.create', { orgId }).service, body });
79
+ printSuccess(`Agent "${options.name}" created.`);
80
+ });
81
+
82
+ agent
83
+ .command('update')
84
+ .description('Update an agent')
85
+ .argument('<agentId>', 'agent ID')
86
+ .option('--name <name>', 'new name')
87
+ .option('--description <desc>', 'new description')
88
+ .option('--level <level>', 'new level')
89
+ .option('--status <status>', 'new status')
90
+ .action(async (agentId, options) => {
91
+ auth.requireAuth();
92
+ const orgId = resolveOrgId(auth, program);
93
+ const body = {};
94
+ if (options.name) body.name = options.name;
95
+ if (options.description) body.description = options.description;
96
+ if (options.level) body.level = parseInt(options.level, 10);
97
+ if (options.status) body.status = options.status;
98
+ await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, { service: resolveEndpoint('agents.update', { orgId, agentId }).service, body });
99
+ printSuccess('Agent updated.');
100
+ });
101
+
102
+ agent
103
+ .command('delete')
104
+ .description('Delete an agent')
105
+ .argument('<agentId>', 'agent ID')
106
+ .action(async (agentId) => {
107
+ auth.requireAuth();
108
+ const orgId = resolveOrgId(auth, program);
109
+ await client.delete(resolveEndpoint('agents.delete', { orgId, agentId }).path, { service: resolveEndpoint('agents.delete', { orgId, agentId }).service });
110
+ printSuccess('Agent deleted.');
111
+ });
112
+
113
+ agent
114
+ .command('activate')
115
+ .description('Activate an agent')
116
+ .argument('<agentId>', 'agent ID')
117
+ .action(async (agentId) => {
118
+ auth.requireAuth();
119
+ const orgId = resolveOrgId(auth, program);
120
+ await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, {
121
+ service: resolveEndpoint('agents.update', { orgId, agentId }).service,
122
+ body: { status: 'active' },
123
+ });
124
+ printSuccess('Agent activated.');
125
+ });
126
+
127
+ agent
128
+ .command('deactivate')
129
+ .description('Deactivate an agent')
130
+ .argument('<agentId>', 'agent ID')
131
+ .action(async (agentId) => {
132
+ auth.requireAuth();
133
+ const orgId = resolveOrgId(auth, program);
134
+ await client.patch(resolveEndpoint('agents.update', { orgId, agentId }).path, {
135
+ service: resolveEndpoint('agents.update', { orgId, agentId }).service,
136
+ body: { status: 'inactive' },
137
+ });
138
+ printSuccess('Agent deactivated.');
139
+ });
140
+
141
+ // Agent tool management
142
+ const agentTool = agent
143
+ .command('tool')
144
+ .description('Manage agent tool assignments');
145
+
146
+ agentTool
147
+ .command('list')
148
+ .description('List tools assigned to an agent')
149
+ .argument('<agentId>', 'agent ID')
150
+ .action(async (agentId) => {
151
+ auth.requireAuth();
152
+ const orgId = resolveOrgId(auth, program);
153
+ const data = await client.get(resolveEndpoint('agents.tools.list', { orgId, agentId }).path, { service: resolveEndpoint('agents.tools.list', { orgId, agentId }).service });
154
+ formatOutput({
155
+ // fleet #392: assignment rows carry tool_name/category/enabled.
156
+ columns: ['tool_id', 'tool_name', 'category', 'access_mode', 'enabled'],
157
+ headers: ['Tool ID', 'Name', 'Type', 'Access', 'Status'],
158
+ rows: data.assignments || data.tools || data,
159
+ }, { format: program.opts().output });
160
+ });
161
+
162
+ agentTool
163
+ .command('assign')
164
+ .description('Assign a tool to an agent')
165
+ .argument('<agentId>', 'agent ID')
166
+ .requiredOption('--tool <toolId>', 'tool ID')
167
+ .option('--access-mode <mode>', 'access mode', 'full')
168
+ .action(async (agentId, options) => {
169
+ auth.requireAuth();
170
+ const orgId = resolveOrgId(auth, program);
171
+ await client.post(resolveEndpoint('agents.tools.create', { orgId, agentId }).path, {
172
+ service: resolveEndpoint('agents.tools.create', { orgId, agentId }).service,
173
+ body: { tool_id: options.tool, access_mode: options.accessMode },
174
+ });
175
+ printSuccess(`Tool assigned to agent.`);
176
+ });
177
+
178
+ agentTool
179
+ .command('revoke')
180
+ .description('Revoke a tool from an agent')
181
+ .argument('<agentId>', 'agent ID')
182
+ .requiredOption('--tool <toolId>', 'tool ID')
183
+ .action(async (agentId, options) => {
184
+ auth.requireAuth();
185
+ const orgId = resolveOrgId(auth, program);
186
+ // The DELETE route is keyed by assignment id, not tool id — resolve
187
+ // the assignment whose tool matches the given --tool value first.
188
+ const data = await client.get(resolveEndpoint('agents.tools.list', { orgId, agentId }).path, { service: resolveEndpoint('agents.tools.list', { orgId, agentId }).service });
189
+ const assignments = data.assignments || data.tools || data || [];
190
+ const assignment = assignments.find(a => a.tool_id === options.tool || a.id === options.tool);
191
+ if (!assignment) {
192
+ throw new Error(`Tool "${options.tool}" is not assigned to agent ${agentId}.`);
193
+ }
194
+ await client.delete(resolveEndpoint('agents.tools.delete', { orgId, agentId, id: assignment.id }).path, { service: resolveEndpoint('agents.tools.delete', { orgId, agentId, id: assignment.id }).service });
195
+ printSuccess('Tool revoked from agent.');
196
+ });
197
+
198
+ // QuoxIdentity P1 (docs/QUOXIDENTITY_P1_CONTRACT.md): attach/detach/
199
+ // attach-status/attach-heartbeat, registered onto this same `agent` group.
200
+ registerAgentAttachCommands(agent, ctx, program);
201
+ }