quoxcli 0.2.0 → 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 (327) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/admin.js +26 -0
  5. package/lib/api/endpoints/agent-schedules.js +26 -0
  6. package/lib/api/endpoints/agents.js +7 -0
  7. package/lib/api/endpoints/assistants.js +12 -0
  8. package/lib/api/endpoints/away.js +15 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/contactsq.js +2 -0
  11. package/lib/api/endpoints/conversations.js +14 -0
  12. package/lib/api/endpoints/executions.js +13 -0
  13. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  14. package/lib/api/endpoints/files.js +14 -0
  15. package/lib/api/endpoints/hat.js +30 -0
  16. package/lib/api/endpoints/inbox.js +20 -0
  17. package/lib/api/endpoints/index.js +14 -0
  18. package/lib/api/endpoints/memory.js +19 -0
  19. package/lib/api/endpoints/mind.js +13 -0
  20. package/lib/api/endpoints/mon.js +6 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/plugins.js +18 -0
  23. package/lib/api/endpoints/presence.js +33 -0
  24. package/lib/api/endpoints/quoxplan.js +7 -0
  25. package/lib/api/endpoints/quoxseo.js +6 -0
  26. package/lib/api/endpoints/router.js +17 -0
  27. package/lib/api/endpoints/safety.js +7 -0
  28. package/lib/api/endpoints/skills.js +20 -0
  29. package/lib/api/endpoints/stepup.js +20 -0
  30. package/lib/api/endpoints/teams.js +2 -2
  31. package/lib/api/endpoints/triggers.js +26 -0
  32. package/lib/api/endpoints/v1.js +86 -0
  33. package/lib/api/endpoints/ward.js +7 -0
  34. package/lib/api/endpoints/workflows.js +7 -0
  35. package/lib/api/endpoints/x.js +8 -0
  36. package/lib/commands/actions.js +61 -0
  37. package/lib/commands/admin.js +16 -0
  38. package/lib/commands/agent.js +163 -27
  39. package/lib/commands/agentAttach.js +94 -6
  40. package/lib/commands/approvals.js +73 -1
  41. package/lib/commands/aside.js +24 -0
  42. package/lib/commands/ask.js +144 -0
  43. package/lib/commands/assistant.js +59 -4
  44. package/lib/commands/auth.js +8 -2
  45. package/lib/commands/backup.js +8 -4
  46. package/lib/commands/brain.js +105 -14
  47. package/lib/commands/build.js +19 -5
  48. package/lib/commands/chat.js +57 -2
  49. package/lib/commands/claude.js +34 -8
  50. package/lib/commands/contacts.js +49 -0
  51. package/lib/commands/conversation.js +35 -4
  52. package/lib/commands/dashboard.js +43 -0
  53. package/lib/commands/department.js +28 -7
  54. package/lib/commands/devResident.js +15 -3
  55. package/lib/commands/devSupervisor.js +13 -1
  56. package/lib/commands/discord.js +75 -39
  57. package/lib/commands/done.js +98 -28
  58. package/lib/commands/engagement.js +17 -6
  59. package/lib/commands/fetchAllowlist.js +71 -0
  60. package/lib/commands/file.js +38 -1
  61. package/lib/commands/fleet.js +311 -4
  62. package/lib/commands/governance.js +12 -3
  63. package/lib/commands/gripe.js +44 -13
  64. package/lib/commands/hat.js +178 -9
  65. package/lib/commands/helpers.js +18 -1
  66. package/lib/commands/hyperion.js +58 -0
  67. package/lib/commands/inbox.js +74 -23
  68. package/lib/commands/licence.js +14 -155
  69. package/lib/commands/lucid.js +10 -4
  70. package/lib/commands/memory.js +85 -8
  71. package/lib/commands/mind.js +71 -0
  72. package/lib/commands/mon.js +69 -2
  73. package/lib/commands/monitoring.js +1 -1
  74. package/lib/commands/motion.js +2 -2
  75. package/lib/commands/nucleus.js +48 -17
  76. package/lib/commands/objective.js +11 -0
  77. package/lib/commands/offload.js +89 -0
  78. package/lib/commands/onboard.js +2 -1
  79. package/lib/commands/org.js +11 -1
  80. package/lib/commands/peek.js +28 -0
  81. package/lib/commands/plan.js +38 -0
  82. package/lib/commands/plugin.js +110 -0
  83. package/lib/commands/presence.js +89 -0
  84. package/lib/commands/provider.js +159 -0
  85. package/lib/commands/qlarity.js +19 -3
  86. package/lib/commands/quoxchat.js +13 -4
  87. package/lib/commands/quoxseo.js +38 -0
  88. package/lib/commands/reach.js +2 -1
  89. package/lib/commands/router.js +77 -0
  90. package/lib/commands/rows.js +358 -0
  91. package/lib/commands/run.js +64 -9
  92. package/lib/commands/server.js +292 -0
  93. package/lib/commands/setup.js +33 -7
  94. package/lib/commands/skills.js +48 -5
  95. package/lib/commands/slot.js +260 -7
  96. package/lib/commands/status.js +1 -1
  97. package/lib/commands/team.js +3 -1
  98. package/lib/commands/tips.js +23 -0
  99. package/lib/commands/tool.js +133 -45
  100. package/lib/commands/trace.js +12 -7
  101. package/lib/commands/trigger.js +95 -0
  102. package/lib/commands/update.js +46 -0
  103. package/lib/commands/vault.js +31 -8
  104. package/lib/commands/vm.js +237 -0
  105. package/lib/commands/voice.js +34 -3
  106. package/lib/commands/volt.js +45 -3
  107. package/lib/commands/walkthrough.js +32 -0
  108. package/lib/commands/watch.js +73 -21
  109. package/lib/commands/welcome.js +14 -0
  110. package/lib/commands/workflow.js +189 -32
  111. package/lib/commands/x.js +13 -4
  112. package/lib/core/actingAs.js +2 -2
  113. package/lib/core/activeSummary.js +186 -0
  114. package/lib/core/aside.js +54 -0
  115. package/lib/core/askDecision.js +60 -0
  116. package/lib/core/auth.js +164 -13
  117. package/lib/core/callerSource.js +1 -1
  118. package/lib/core/cliBuild.js +62 -0
  119. package/lib/core/client.js +144 -35
  120. package/lib/core/commandRegistrars.js +121 -0
  121. package/lib/core/config.js +142 -24
  122. package/lib/core/customHats.js +168 -0
  123. package/lib/core/doneEngineFetch.js +8 -0
  124. package/lib/core/errors.js +329 -20
  125. package/lib/core/global-flags.js +105 -5
  126. package/lib/core/hyperion.js +102 -0
  127. package/lib/core/inboxResolution.js +65 -0
  128. package/lib/core/lazyCommands.js +4 -1
  129. package/lib/core/offload.js +504 -0
  130. package/lib/core/org-override.js +1 -1
  131. package/lib/core/orgResolve.js +70 -0
  132. package/lib/core/output.js +4 -4
  133. package/lib/core/peek.js +126 -0
  134. package/lib/core/profileUsers.js +51 -0
  135. package/lib/core/promptCancel.js +18 -0
  136. package/lib/core/repoPath.js +11 -0
  137. package/lib/core/residentSeed.js +3 -1
  138. package/lib/core/rootOptions.js +39 -0
  139. package/lib/core/router.js +168 -0
  140. package/lib/core/selfUpdate.js +188 -0
  141. package/lib/core/sessionsOverview.js +64 -0
  142. package/lib/core/slotKeeper.js +248 -3
  143. package/lib/core/slotWake.js +120 -0
  144. package/lib/core/supervisorEngine.js +71 -2
  145. package/lib/core/version.js +1 -1
  146. package/lib/core/vm.js +361 -0
  147. package/lib/core/vmTui.js +86 -0
  148. package/lib/core/walkthrough.js +35 -0
  149. package/lib/hyperion/deck.js +29 -0
  150. package/lib/lucid/LucidCard.jsx +68 -0
  151. package/lib/lucid/LucidIntro.jsx +45 -58
  152. package/lib/lucid/cards.js +86 -0
  153. package/lib/lucid/markdown.js +81 -0
  154. package/lib/lucid/motion.js +56 -0
  155. package/lib/quoxon/activity.js +14 -2
  156. package/lib/quoxon/aocl.js +117 -0
  157. package/lib/quoxon/api.js +504 -25
  158. package/lib/quoxon/app.js +3000 -230
  159. package/lib/quoxon/approvalFacts.js +94 -0
  160. package/lib/quoxon/approvalsView.js +325 -0
  161. package/lib/quoxon/asideView.js +40 -0
  162. package/lib/quoxon/ask.js +121 -0
  163. package/lib/quoxon/away.js +52 -0
  164. package/lib/quoxon/bang.js +224 -0
  165. package/lib/quoxon/bulkRisk.js +17 -0
  166. package/lib/quoxon/capabilities.js +13 -6
  167. package/lib/quoxon/cells.js +53 -0
  168. package/lib/quoxon/commandShort.js +112 -0
  169. package/lib/quoxon/commands.js +363 -38
  170. package/lib/quoxon/connect.js +210 -23
  171. package/lib/quoxon/connectCard.js +19 -2
  172. package/lib/quoxon/consent.js +86 -0
  173. package/lib/quoxon/copy.js +4 -3
  174. package/lib/quoxon/countdownLine.js +11 -0
  175. package/lib/quoxon/demo.autoplay.js +29 -0
  176. package/lib/quoxon/demo.fixtures.js +1 -1
  177. package/lib/quoxon/demo.js +44 -3
  178. package/lib/quoxon/devObserver.js +20 -0
  179. package/lib/quoxon/dividerAocl.js +191 -0
  180. package/lib/quoxon/dividerRule.js +57 -0
  181. package/lib/quoxon/dragonModes.js +150 -0
  182. package/lib/quoxon/dragonView.js +41 -0
  183. package/lib/quoxon/earlyInput.js +40 -0
  184. package/lib/quoxon/echo.js +15 -0
  185. package/lib/quoxon/egg.js +2 -2
  186. package/lib/quoxon/events.js +244 -25
  187. package/lib/quoxon/feedback.js +50 -0
  188. package/lib/quoxon/gallery.js +21 -7
  189. package/lib/quoxon/handoffOffer.js +78 -0
  190. package/lib/quoxon/hatSwitch.js +37 -0
  191. package/lib/quoxon/headless.js +161 -0
  192. package/lib/quoxon/hostUrls.js +41 -0
  193. package/lib/quoxon/indicator.js +6 -2
  194. package/lib/quoxon/intent.js +12 -1
  195. package/lib/quoxon/keys.js +49 -6
  196. package/lib/quoxon/launcher.js +103 -0
  197. package/lib/quoxon/main.js +92 -160
  198. package/lib/quoxon/markdown.js +45 -21
  199. package/lib/quoxon/markdownSettle.js +19 -0
  200. package/lib/quoxon/matrixDomains.json +46 -0
  201. package/lib/quoxon/mentions.js +90 -0
  202. package/lib/quoxon/mind.js +103 -0
  203. package/lib/quoxon/mode.js +46 -0
  204. package/lib/quoxon/model.js +132 -26
  205. package/lib/quoxon/modelPrefs.js +345 -0
  206. package/lib/quoxon/motion/clock.js +3 -2
  207. package/lib/quoxon/motion/level.js +91 -0
  208. package/lib/quoxon/mouse.js +211 -0
  209. package/lib/quoxon/noai.js +57 -0
  210. package/lib/quoxon/objects/away.js +72 -0
  211. package/lib/quoxon/objects/fetch.js +17 -5
  212. package/lib/quoxon/objects/fleet.js +149 -0
  213. package/lib/quoxon/objects/health.js +186 -0
  214. package/lib/quoxon/objects/index.js +24 -3
  215. package/lib/quoxon/objects/mirror.js +137 -0
  216. package/lib/quoxon/objects/peek.js +17 -0
  217. package/lib/quoxon/objects/prove.js +130 -0
  218. package/lib/quoxon/objects/replay.js +125 -0
  219. package/lib/quoxon/objects/sitrep.js +58 -0
  220. package/lib/quoxon/objects/view.js +11 -0
  221. package/lib/quoxon/onboarding/copy.js +32 -4
  222. package/lib/quoxon/onboarding/credential.js +17 -4
  223. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  224. package/lib/quoxon/onboarding/failureCause.js +33 -0
  225. package/lib/quoxon/onboarding/finish.js +15 -0
  226. package/lib/quoxon/onboarding/index.js +34 -7
  227. package/lib/quoxon/onboarding/installRunner.js +64 -5
  228. package/lib/quoxon/onboarding/preflight.js +17 -3
  229. package/lib/quoxon/onboarding/probe.js +65 -0
  230. package/lib/quoxon/onboarding/view.js +76 -19
  231. package/lib/quoxon/orgLabel.js +10 -0
  232. package/lib/quoxon/orgSwitch.js +33 -0
  233. package/lib/quoxon/ownBroadcast.js +22 -0
  234. package/lib/quoxon/palette.js +43 -0
  235. package/lib/quoxon/print.js +9 -5
  236. package/lib/quoxon/profileSwitch.js +53 -0
  237. package/lib/quoxon/promptHistory.js +41 -0
  238. package/lib/quoxon/providers.js +48 -0
  239. package/lib/quoxon/qart.js +20 -51
  240. package/lib/quoxon/qdragon.js +410 -0
  241. package/lib/quoxon/qdragonFly.js +61 -0
  242. package/lib/quoxon/qdragonScenes.js +282 -0
  243. package/lib/quoxon/qeyes.js +213 -0
  244. package/lib/quoxon/qmotion.js +58 -37
  245. package/lib/quoxon/render.js +638 -119
  246. package/lib/quoxon/resend.js +72 -0
  247. package/lib/quoxon/roster.js +95 -9
  248. package/lib/quoxon/route.js +6 -5
  249. package/lib/quoxon/rows.js +711 -0
  250. package/lib/quoxon/rowsStore.js +80 -0
  251. package/lib/quoxon/screenContext.js +150 -0
  252. package/lib/quoxon/screenFrames.js +254 -0
  253. package/lib/quoxon/session.js +172 -2
  254. package/lib/quoxon/sessionDead.js +17 -0
  255. package/lib/quoxon/signin.js +125 -0
  256. package/lib/quoxon/slash/agents.js +112 -0
  257. package/lib/quoxon/slash/argSources.js +65 -0
  258. package/lib/quoxon/slash/args.js +65 -0
  259. package/lib/quoxon/slash/automate.js +89 -0
  260. package/lib/quoxon/slash/broadcast.js +140 -0
  261. package/lib/quoxon/slash/broadcastRun.js +108 -0
  262. package/lib/quoxon/slash/card.js +30 -0
  263. package/lib/quoxon/slash/challenge.js +114 -0
  264. package/lib/quoxon/slash/cliCommand.js +53 -0
  265. package/lib/quoxon/slash/cliLeaves.json +948 -0
  266. package/lib/quoxon/slash/compact.js +1 -1
  267. package/lib/quoxon/slash/confirm.js +30 -0
  268. package/lib/quoxon/slash/crystallize.js +69 -0
  269. package/lib/quoxon/slash/dashboard.js +136 -0
  270. package/lib/quoxon/slash/draft.js +54 -0
  271. package/lib/quoxon/slash/hatWalk.js +206 -0
  272. package/lib/quoxon/slash/inverses.js +77 -0
  273. package/lib/quoxon/slash/killer.js +24 -0
  274. package/lib/quoxon/slash/killerApi.js +16 -0
  275. package/lib/quoxon/slash/ledger.js +13 -8
  276. package/lib/quoxon/slash/listView.js +3 -1
  277. package/lib/quoxon/slash/modelCmd.js +261 -0
  278. package/lib/quoxon/slash/palette.js +21 -7
  279. package/lib/quoxon/slash/paletteController.js +117 -7
  280. package/lib/quoxon/slash/panels.js +21 -6
  281. package/lib/quoxon/slash/picker.js +20 -2
  282. package/lib/quoxon/slash/restart.js +71 -13
  283. package/lib/quoxon/slash/rowCmd.js +157 -0
  284. package/lib/quoxon/slash/rowWalk.js +175 -0
  285. package/lib/quoxon/slash/settings.js +16 -6
  286. package/lib/quoxon/slash/spawn.js +177 -0
  287. package/lib/quoxon/slash/stepUp.js +274 -0
  288. package/lib/quoxon/slash/stepUpCard.js +60 -0
  289. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  290. package/lib/quoxon/slash/undoApi.js +38 -0
  291. package/lib/quoxon/slash/undoCmd.js +49 -0
  292. package/lib/quoxon/slash/undoWish.js +15 -0
  293. package/lib/quoxon/slash/watch.js +164 -0
  294. package/lib/quoxon/slash/wish.js +23 -0
  295. package/lib/quoxon/slash/wishCmd.js +32 -0
  296. package/lib/quoxon/slotChat.js +300 -0
  297. package/lib/quoxon/slotFormat.js +195 -0
  298. package/lib/quoxon/slotRun.js +131 -0
  299. package/lib/quoxon/sound/mapping.js +2 -1
  300. package/lib/quoxon/stray.js +22 -0
  301. package/lib/quoxon/suggestCommand.js +59 -0
  302. package/lib/quoxon/term.js +46 -11
  303. package/lib/quoxon/textBuffer.js +176 -2
  304. package/lib/quoxon/theme.js +58 -25
  305. package/lib/quoxon/tips.js +433 -0
  306. package/lib/quoxon/toolError.js +41 -0
  307. package/lib/quoxon/transcript.js +42 -4
  308. package/lib/quoxon/web/qface.js +18 -10
  309. package/lib/quoxon/welcome.js +1 -1
  310. package/lib/quoxon/whatsnew.js +136 -0
  311. package/lib/quoxon/whatsnew.jsonl +89 -0
  312. package/lib/quoxon/windowOrg.js +113 -0
  313. package/lib/server/diag.js +129 -0
  314. package/lib/server/doctor.js +71 -0
  315. package/lib/server/install.js +149 -0
  316. package/lib/server/pluginEnable.js +161 -0
  317. package/lib/server/skew.js +56 -0
  318. package/lib/server/snapshot.js +131 -0
  319. package/lib/server/status.js +128 -0
  320. package/lib/server/sys.js +65 -0
  321. package/lib/server/uninstall.js +63 -0
  322. package/lib/server/update.js +234 -0
  323. package/lib/toolOutputShape.js +315 -0
  324. package/lib/tui/views/HyperionDeck.jsx +79 -0
  325. package/lib/tui/views/LucidDeck.jsx +209 -217
  326. package/package.json +4 -1
  327. package/lib/core/websiteSession.js +0 -148
@@ -1,18 +1,22 @@
1
1
  /**
2
- * QuoxHat commands (QUOXHAT #721 b): READ-ONLY views of the org's hats.
2
+ * QuoxHat commands (QUOXHAT #721 b; CUSTOM-HATS 2026-10-17).
3
3
  *
4
4
  * quox hat list
5
5
  * quox hat show <id|key> [--rev <n>]
6
- * quox hat reset <id|key> --yes (audited reset-to-template)
6
+ * quox hat reset <id|key> --yes (audited reset-to-template, built-ins)
7
+ * quox hat create <key> --name "<Name>" [--from <hat>] [--agents a,b] [--tools x,y] [--mode m] [--offer o]
8
+ * quox hat edit <hat> [--name N] [--add-agents|--remove-agents|--agents] [--add-tools|--remove-tools|--tools] [--mode m]
9
+ * quox hat delete <hat> --yes (custom hats only)
10
+ * quox hat use <hat> --conversation <id> [--yes]
7
11
  *
8
- * Shows what each hat presents (explicit tools, bundles, connector families),
9
- * its authorization mode, revision, template status and the org's enforcement
10
- * and selection mode. Editing hat definitions is deliberately NOT here: edits
11
- * go through the dashboard (Settings, QuoxHat Modes) where they are validated
12
- * and audited. The one write is `reset`, which returns an org-edited built-in
13
- * to its current template through the same audited route the dashboard uses.
12
+ * Custom hats (owner 2026-10-17): users who build their own agents organise them under their own
13
+ * hats, so a turn under "security" carries only the security agents, not every agent in the org.
14
+ * create/edit/delete act on CUSTOM hats only; a built-in is reset to its template, or copied with --from.
15
+ * Everything is server-side, org-scoped, revisioned and audited (services/auth/routes/quoxhat.js);
16
+ * who may create or change an org hat is the existing hat-edit permission (org manage-settings),
17
+ * a private hat (--private) only its owner. This file never decides permission itself.
14
18
  *
15
- * Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/quoxhat.js).
19
+ * Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/hat.js).
16
20
  */
17
21
 
18
22
  import chalk from 'chalk';
@@ -59,6 +63,32 @@ export function findHat(hats, ref) {
59
63
  return (hats || []).find(h => h.id === r) || (hats || []).find(h => h.key === r) || null;
60
64
  }
61
65
 
66
+
67
+ export { DELEGATION_TOOLS, HAT_MODES, HAT_OFFERS, parseList, memberTools, applyMode, buildCustomDefinition, applyHatEdits } from '../core/customHats.js';
68
+ import { parseList, memberTools, buildCustomDefinition, applyHatEdits } from '../core/customHats.js';
69
+
70
+ /** Which of `ids` no registered or org agent matches. checked=false when neither list could be read. */
71
+ export async function findUnknownAgents(client, orgId, ids) {
72
+ if (!ids.length) return { unknown: [], checked: true };
73
+ const known = new Set();
74
+ let read = 0;
75
+ try {
76
+ const ep = resolveEndpoint('v1.agents.registry');
77
+ const d = await client.get(ep.path, { service: ep.service });
78
+ if (Array.isArray(d?.agents)) { read++; for (const a of d.agents) if (a?.agent_id) known.add(String(a.agent_id).toLowerCase()); }
79
+ } catch { /* named below */ }
80
+ try {
81
+ const ep = resolveEndpoint('agents.list', { orgId });
82
+ const d = await client.get(ep.path, { service: ep.service });
83
+ const rows = Array.isArray(d?.agents) ? d.agents : (Array.isArray(d) ? d : null);
84
+ if (rows) { read++; for (const a of rows) { const id = a?.agent_id || a?.agentId; if (id) known.add(String(id).toLowerCase()); } }
85
+ } catch { /* named below */ }
86
+ if (!read) return { unknown: [], checked: false };
87
+ return { unknown: ids.filter(id => !known.has(id)), checked: true };
88
+ }
89
+
90
+ function isBuiltin(h) { return !!h?.template_key; }
91
+
62
92
  export function registerHatCommands(program, ctx) {
63
93
  const { client, auth } = ctx;
64
94
 
@@ -154,6 +184,8 @@ export function registerHatCommands(program, ctx) {
154
184
  out.push(`Enforcement: ${enforcement} (org)`);
155
185
  out.push(`Transitions: confirm=${def.transitions?.confirm || 'none'} privileged=${def.transitions?.privileged ? 'yes' : 'no'}`);
156
186
  out.push(`Agents: orchestrator=${def.agents?.orchestrator || '-'} specialists=${(def.agents?.specialists || []).join(', ') || 'none'}`);
187
+ out.push(`Offer: ${def.tools?.offer === 'members' ? 'members (an orchestrator turn is offered only this hat\'s tools)' : 'authorized (hat tools first, then the rest of the grant)'}`);
188
+ out.push(`Kind: ${h.template_key ? 'built-in' : 'custom'}${h.visibility === 'private' ? ' (private)' : ''}`);
157
189
  out.push(`Budgets: initial_tool_target=${def.budgets?.initial_tool_target ?? '-'} max_manifest=${def.budgets?.max_manifest ?? '-'}`);
158
190
  out.push('');
159
191
  const explicit = def.tools?.explicit || [];
@@ -202,4 +234,141 @@ export function registerHatCommands(program, ctx) {
202
234
  if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
203
235
  console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
204
236
  });
237
+ // ── hat create (custom) ───────────────────────────────────────────────────────
238
+ hat
239
+ .command('create <key>')
240
+ .description('Create a custom hat: the agents and tools it lists are the only ones a turn under it carries')
241
+ .requiredOption('--name <name>', 'display name, e.g. "Security"')
242
+ .option('--from <hat>', 'start from a copy of this hat (key or id)')
243
+ .option('--agents <ids>', 'comma-separated agent ids this hat puts in scope (registered or your own agents)')
244
+ .option('--tools <names>', 'comma-separated tool names the orchestrator is offered under this hat')
245
+ .option('--mode <mode>', 'permissive (default: each agent keeps its own grant) or restrictive (only the listed tools are authorized, for every agent under the hat)')
246
+ .option('--offer <offer>', 'members (default: offer only this hat\'s tools) or authorized')
247
+ .option('--purpose <text>', 'one line: what this hat is for')
248
+ .option('--private', 'a private hat only you can see and wear (needs no org settings permission)')
249
+ .option('--reason <text>', 'why (kept in the revision history)')
250
+ .action(async (key, options) => {
251
+ auth.requireAuth();
252
+ const orgId = resolveOrgId(auth, program);
253
+ const k = String(key || '').trim().toLowerCase();
254
+ if (!/^[a-z0-9][a-z0-9-]{1,40}$/.test(k)) throw new Error(`Hat key '${key}' must be a lowercase slug (a-z, 0-9, hyphen), 2-41 characters.`);
255
+ const list = await fetchList(client, orgId);
256
+ const hats = list?.hats || [];
257
+ if (findHat(hats, k)) throw new Error(`A hat with key '${k}' already exists in this org. Use quox hat edit ${k}.`);
258
+ let base = null;
259
+ if (options.from) {
260
+ const from = findHat(hats, options.from);
261
+ if (!from) throw new Error(`No hat '${options.from}' to copy. Available: ${hats.map(h => h.key).join(', ') || 'none'}`);
262
+ base = from.definition;
263
+ }
264
+ const agents = options.agents !== undefined ? parseList(options.agents) : undefined;
265
+ const tools = options.tools !== undefined ? parseList(options.tools) : undefined;
266
+ const { definition, autoAdded } = buildCustomDefinition({ base, name: options.name, purpose: options.purpose, agents, tools, mode: options.mode, offer: options.offer });
267
+ const named = definition.agents.specialists;
268
+ const check = await findUnknownAgents(client, orgId, named);
269
+ if (check.unknown.length) throw new Error(`Unknown agent(s): ${check.unknown.join(', ')}. See quox agent list.`);
270
+ const ep = resolveEndpoint('hat.create', { orgId });
271
+ const data = await client.post(ep.path, { service: ep.service, body: { key: k, name: String(options.name).trim(), definition, visibility: options.private ? 'private' : 'org' } });
272
+ const h = data?.hat;
273
+ if (!h) throw new Error(`Create of '${k}' returned no hat`);
274
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h, warnings: data.warnings || [], auto_added_tools: autoAdded }, null, 2)); return; }
275
+ const d = h.definition || definition;
276
+ console.log(` Ⓠ QUOX │ hat ${h.key} created │ rev ${h.current_revision} · agents ${(d.agents?.specialists || []).join(', ') || 'none'} · ${memberTools(d).length} tool(s) · offer ${d.tools?.offer || 'authorized'} · ${authzMode(d)} ✔`);
277
+ if (autoAdded.length) printInfo(`Added ${autoAdded.join(', ')} so the orchestrator can reach the hat's agents.`);
278
+ if (!check.checked) printInfo('Agent ids were not checked: the agent lists could not be read.');
279
+ if (d.authz?.mode === 'restrictive') printInfo('Restrictive: every agent working under this hat, its own agents included when delegated to, may only run the tools listed here.');
280
+ for (const w of data.warnings || []) printInfo(`warning: ${w}`);
281
+ printInfo(`Wear it: /hat ${h.key} in quox, or quox hat use ${h.key} --conversation <id>.`);
282
+ });
283
+
284
+ // ── hat edit (custom) ─────────────────────────────────────────────────────────
285
+ hat
286
+ .command('edit <hat>')
287
+ .description('Change a custom hat: add or remove agents and tools, rename, mode (appends an audited revision)')
288
+ .option('--name <name>', 'rename the hat')
289
+ .option('--purpose <text>', 'new one-line purpose')
290
+ .option('--agents <ids>', 'replace the agent list')
291
+ .option('--add-agents <ids>', 'comma-separated agents to add')
292
+ .option('--remove-agents <ids>', 'comma-separated agents to remove')
293
+ .option('--tools <names>', 'replace the explicit tool list')
294
+ .option('--add-tools <names>', 'comma-separated tools to add')
295
+ .option('--remove-tools <names>', 'comma-separated tools to remove')
296
+ .option('--mode <mode>', 'permissive or restrictive')
297
+ .option('--offer <offer>', 'members or authorized')
298
+ .option('--reason <text>', 'why (kept in the revision history)')
299
+ .action(async (ref, options) => {
300
+ auth.requireAuth();
301
+ const orgId = resolveOrgId(auth, program);
302
+ const list = await fetchList(client, orgId);
303
+ const found = findHat(list?.hats, ref);
304
+ if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
305
+ if (isBuiltin(found)) throw new Error(`'${found.key}' is a built-in hat: quox hat edit changes custom hats only. Reset it with quox hat reset ${found.key} --yes, or make your own from it: quox hat create <key> --name "<Name>" --from ${found.key}.`);
306
+ const opts = {
307
+ agents: options.agents !== undefined ? parseList(options.agents) : undefined,
308
+ addAgents: parseList(options.addAgents),
309
+ removeAgents: parseList(options.removeAgents),
310
+ tools: options.tools !== undefined ? parseList(options.tools) : undefined,
311
+ addTools: parseList(options.addTools),
312
+ removeTools: parseList(options.removeTools),
313
+ mode: options.mode,
314
+ offer: options.offer,
315
+ purpose: options.purpose,
316
+ };
317
+ const { definition, changes } = applyHatEdits(found.definition, opts);
318
+ const rename = options.name !== undefined && String(options.name).trim() !== found.name ? String(options.name).trim() : undefined;
319
+ if (rename !== undefined) changes.unshift(`name "${found.name}" -> "${rename}"`);
320
+ if (!changes.length) throw new Error(`Nothing to change on '${found.key}'.`);
321
+ const newAgents = [...(opts.agents || []), ...opts.addAgents];
322
+ const check = await findUnknownAgents(client, orgId, newAgents);
323
+ if (check.unknown.length) throw new Error(`Unknown agent(s): ${check.unknown.join(', ')}. See quox agent list.`);
324
+ const ep = resolveEndpoint('hat.update', { orgId, hatId: found.id });
325
+ const data = await client.put(ep.path, { service: ep.service, body: { definition, ...(rename !== undefined ? { name: rename } : {}), change_reason: options.reason || changes.join('; ') } });
326
+ const h = data?.hat;
327
+ if (!h) throw new Error(`Edit of '${found.key}' returned no hat`);
328
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h, changes, warnings: data.warnings || [] }, null, 2)); return; }
329
+ console.log(` Ⓠ QUOX │ hat ${h.key} edited │ rev ${h.current_revision} · ${changes.join(' · ')} ✔`);
330
+ for (const w of data.warnings || []) printInfo(`warning: ${w}`);
331
+ });
332
+
333
+ // ── hat delete (custom) ───────────────────────────────────────────────────────
334
+ hat
335
+ .command('delete <hat>')
336
+ .description('Delete a custom hat (built-ins cannot be deleted; reset them instead). Audited.')
337
+ .option('--yes', 'confirm: conversations wearing it fall back to the default hat')
338
+ .action(async (ref, options) => {
339
+ auth.requireAuth();
340
+ const orgId = resolveOrgId(auth, program);
341
+ const list = await fetchList(client, orgId);
342
+ const found = findHat(list?.hats, ref);
343
+ if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
344
+ if (isBuiltin(found)) throw new Error(`'${found.key}' is a built-in hat and cannot be deleted. quox hat reset ${found.key} --yes returns it to its template.`);
345
+ if (!options.yes) throw new Error(`Deleting '${found.key}' moves every conversation wearing it back to the default hat. Re-run with --yes to confirm.`);
346
+ const ep = resolveEndpoint('hat.delete', { orgId, hatId: found.id });
347
+ const data = await client.delete(ep.path, { service: ep.service });
348
+ const d = data?.deleted;
349
+ if (!d) throw new Error(`Delete of '${found.key}' returned no confirmation`);
350
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ deleted: d }, null, 2)); return; }
351
+ console.log(` Ⓠ QUOX │ hat ${d.key} deleted │ was rev ${d.revision} · ${d.bindings_cleared} conversation(s) moved to the default · ${d.user_prefs_cleared} preference(s) cleared ✔`);
352
+ });
353
+
354
+ // ── hat use ───────────────────────────────────────────────────────────────────
355
+ hat
356
+ .command('use <hat>')
357
+ .description('Wear a hat in one conversation (the same audited switch as /hat <name> in quox)')
358
+ .requiredOption('--conversation <id>', 'the conversation to switch')
359
+ .option('--yes', 'confirm a privileged hat')
360
+ .action(async (ref, options) => {
361
+ auth.requireAuth();
362
+ const orgId = resolveOrgId(auth, program);
363
+ const list = await fetchList(client, orgId);
364
+ const found = findHat(list?.hats, ref);
365
+ if (!found) throw new Error(`No hat '${ref}' in this org. Available: ${(list?.hats || []).map(h => h.key).join(', ') || 'none'}`);
366
+ if (found.definition?.transitions?.privileged && !options.yes) throw new Error(`'${found.key}' is a privileged hat. Re-run with --yes to wear it.`);
367
+ const ep = resolveEndpoint('hat.bind', { orgId, conversationId: options.conversation });
368
+ const data = await client.put(ep.path, { service: ep.service, body: { hat_id: found.id } });
369
+ const b = data?.binding;
370
+ if (!b) throw new Error(`Switch to '${found.key}' returned no binding`);
371
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ binding: b, hat: { id: found.id, key: found.key } }, null, 2)); return; }
372
+ console.log(` Ⓠ QUOX │ conversation ${options.conversation} now wears ${found.key} │ rev ${b.revision} · seq ${b.seq} ✔`);
373
+ });
205
374
  }
@@ -2,7 +2,7 @@
2
2
  * Shared helpers for command modules
3
3
  */
4
4
 
5
- import { QuoxValidationError } from '../core/errors.js';
5
+ import { QuoxValidationError, QuoxNetworkError } from '../core/errors.js';
6
6
 
7
7
  /**
8
8
  * Resolve the active org ID from --org flag or stored auth.
@@ -77,3 +77,20 @@ export function buildListQuery(options) {
77
77
  if (options.sort) query.sort = options.sort;
78
78
  return query;
79
79
  }
80
+
81
+ /**
82
+ * Run a request; if it times out, rethrow with what timed out and how to narrow
83
+ * it (still a QuoxNetworkError, so the exit code stays 7). A bare
84
+ * "Request timed out after 30000ms" does not tell the operator which call hung
85
+ * or what to try (quox#806, quox#807).
86
+ */
87
+ export async function withTimeoutHint(what, hint, fn) {
88
+ try {
89
+ return await fn();
90
+ } catch (err) {
91
+ if (err instanceof QuoxNetworkError && /timed out/i.test(err.message || '')) {
92
+ throw new QuoxNetworkError(`${what} timed out (${err.message}). ${hint}`, err);
93
+ }
94
+ throw err;
95
+ }
96
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `quox hyperion`: everything HYPERION (the governed Proxmox operator, agent id atlas) is doing in one place.
3
+ * Collector route GET /api/v1/proxmox/activity (org-scoped, read only).
4
+ *
5
+ * quox hyperion [status] [--json] VMs and who holds them, node load, pool counts, last 5 ops
6
+ * quox hyperion deck live full-screen view, refreshes every 10s (q quits, r refreshes)
7
+ */
8
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
9
+ import { fetchActivity, statusLines, summaryText } from '../core/hyperion.js';
10
+
11
+ export function registerHyperionCommands(program, ctx) {
12
+ const { client, auth } = ctx;
13
+
14
+ const hy = program
15
+ .command('hyperion')
16
+ .description('HYPERION activity: which VMs exist, who holds each and why, node load and recent operations')
17
+ .option('--json', 'print the full JSON response');
18
+
19
+ const wantJson = (cmd) => {
20
+ const o = cmd.optsWithGlobals ? cmd.optsWithGlobals() : cmd.opts();
21
+ return Boolean(o.json) || program.opts().output === 'json';
22
+ };
23
+
24
+ // An error body is not data: report the status and stop.
25
+ async function show(cmd) {
26
+ auth.requireAuth();
27
+ let data;
28
+ try {
29
+ data = await fetchActivity(client);
30
+ } catch (err) {
31
+ const status = err && (err.status || err.statusCode);
32
+ if (!status) throw err;
33
+ console.error(`HYPERION request failed: HTTP ${status}${err.message ? ` ${err.message}` : ''}`);
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
38
+ printEvent(glyphLine('hyperion', 'activity', { result: summaryText(data) }));
39
+ console.log(statusLines(data).join('\n'));
40
+ }
41
+
42
+ hy.action(async (_o, cmd) => show(cmd));
43
+
44
+ hy
45
+ .command('status')
46
+ .description('Same as bare `quox hyperion`')
47
+ .option('--json', 'print the full JSON response')
48
+ .action(async (_o, cmd) => show(cmd));
49
+
50
+ hy
51
+ .command('deck')
52
+ .description('Live full-screen HYPERION deck (refreshes every 10s; r refreshes, q quits)')
53
+ .action(async () => {
54
+ auth.requireAuth();
55
+ const { launchHyperionDeck } = await import('../hyperion/deck.js');
56
+ await launchHyperionDeck({ activity: () => fetchActivity(client) });
57
+ });
58
+ }
@@ -5,39 +5,52 @@
5
5
  import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
6
  import { resolveOrgId } from './helpers.js';
7
7
  import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { attachWalkthrough } from './walkthrough.js';
8
9
  import { getApprovalContext, formatApprovalContext, approvalListColumns } from '../core/approvalContext.js';
10
+ import { QuoxUserError } from '../core/errors.js';
11
+ import { resolutionFromItem, alreadyResolvedError } from '../core/inboxResolution.js';
12
+ import chalk from 'chalk';
9
13
 
10
14
  export function registerInboxCommands(program, ctx) {
11
15
  const { client, auth } = ctx;
12
16
 
13
- // An agent hands back a tool approvalId (tool_approvals.approval_id), which is
14
- // stored on the inbox card as aee_correlation_id, not its item id. No server
15
- // lookup-by-correlation route exists, so on a 404 we scan the org's approval
16
- // cards (all statuses) client-side. Falls back to the id as given.
17
+ // An agent hands back a tool approvalId (tool_approvals.approval_id), stored on the card as
18
+ // aee_correlation_id. The server now resolves it on GET and respond (CLI-ERRORS); this client-side
19
+ // scan stays as the fallback for an older server. A failed scan is reported, never read as "no card".
20
+ async function findByApprovalId(orgId, id) {
21
+ const list = resolveEndpoint('inbox.list', { orgId });
22
+ const data = await client.get(list.path, { service: list.service, query: { status: 'all', kind: 'approval', limit: 200 } });
23
+ const items = (data && (data.items || data)) || [];
24
+ const hit = Array.isArray(items) && items.find((it) => it.aee_correlation_id === id || (it.payload && it.payload.approval_id === id));
25
+ return hit ? hit.id : null;
26
+ }
27
+
28
+ function notFound(id, cause) {
29
+ const err = new QuoxUserError(`No inbox item or approval with id ${id} in this org, open or resolved.`, {
30
+ next: "if it came from another org, switch with `quox org switch <org>`; `quox inbox list --status all` lists this org's items.",
31
+ exitCode: 5, code: 'not_found', status: 404, body: cause?.body,
32
+ });
33
+ return err;
34
+ }
35
+
17
36
  async function resolveItemId(orgId, id) {
18
37
  const direct = resolveEndpoint('inbox.get', { orgId, itemId: id });
19
38
  try {
20
- await client.get(direct.path, { service: direct.service });
21
- return id;
39
+ const data = await client.get(direct.path, { service: direct.service });
40
+ // the server resolves an approvalId to its card; post to the card's own id
41
+ return (data && data.item && data.item.id) || id;
22
42
  } catch (err) {
23
43
  if (!err || err.status !== 404) throw err;
24
- const list = resolveEndpoint('inbox.list', { orgId });
25
- let data;
26
- try {
27
- data = await client.get(list.path, { service: list.service, query: { status: 'all', kind: 'approval', limit: 200 } });
28
- } catch { data = null; }
29
- const items = (data && (data.items || data)) || [];
30
- const hit = Array.isArray(items) && items.find((it) => it.aee_correlation_id === id || (it.payload && it.payload.approval_id === id));
31
- if (hit) return hit.id;
32
- err.message += `\nHint: ${id} is not an inbox item id, and no approval card in your recent approvals carries it as an approvalId. `
33
- + 'Try `quox inbox list --status all` to find the card.';
34
- throw err;
44
+ const hit = await findByApprovalId(orgId, id);
45
+ if (hit) return hit;
46
+ throw notFound(id, err);
35
47
  }
36
48
  }
37
49
 
38
50
  const inbox = program
39
51
  .command('inbox')
40
52
  .description('Manage inbox and approval requests');
53
+ attachWalkthrough(inbox, ctx, program);
41
54
 
42
55
  inbox
43
56
  .command('list')
@@ -102,14 +115,52 @@ export function registerInboxCommands(program, ctx) {
102
115
  .action(async (givenId, options) => {
103
116
  auth.requireAuth();
104
117
  const orgId = resolveOrgId(auth, program);
105
- const itemId = await resolveItemId(orgId, givenId);
106
- await client.post(resolveEndpoint('inbox.respond', { orgId, itemId }).path, {
107
- service: resolveEndpoint('inbox.respond', { orgId, itemId }).service,
108
- body: { action: options.action, comment: options.comment },
109
- });
110
- printSuccess(`Item ${options.action}ed.`);
118
+ const format = program.opts().output;
119
+ const post = (itemId) => {
120
+ const ep = resolveEndpoint('inbox.respond', { orgId, itemId });
121
+ return client.post(ep.path, { service: ep.service, body: { action: options.action, comment: options.comment } });
122
+ };
123
+ // Resolve an approvalId to its card (server-side on a current server, else the client scan). When
124
+ // this profile cannot see the card, still ask the server: it knows WHY (an agent login, another
125
+ // org, already decided) and says so, where "not found" would hide it.
126
+ let itemId = givenId;
127
+ try { itemId = await resolveItemId(orgId, givenId); } catch (err) { if (err?.code !== 'not_found') throw err; }
128
+ let result;
129
+ try {
130
+ result = await post(itemId);
131
+ } catch (err) {
132
+ if (err?.status === 404) throw notFound(givenId, err);
133
+ if (!result) {
134
+ const resolution = await alreadyResolvedFrom(err, orgId, itemId, options.action);
135
+ if (!resolution) throw err;
136
+ if (!resolution.matches) throw alreadyResolvedError(resolution, err);
137
+ if (format === 'json') console.log(JSON.stringify({ success: true, already_resolved: true, ...resolution }));
138
+ else if (format !== 'quiet') console.log(chalk.green(`✔ ${resolution.message}`));
139
+ return;
140
+ }
141
+ }
142
+ if (result?.item?.status === 'expired') {
143
+ throw new QuoxUserError(result.message || 'This approval had expired, so nothing ran.', {
144
+ next: 're-run the original command to ask for a fresh approval.', exitCode: 1, code: 'approval_expired',
145
+ });
146
+ }
147
+ const PAST = { approve: 'approved', reject: 'rejected', deny: 'denied', defer: 'deferred', resolve: 'resolved', answer: 'answered' };
148
+ printSuccess(`Item ${PAST[options.action] || `${options.action} recorded`}.`);
111
149
  });
112
150
 
151
+ // A respond on a closed card: the server's structured answer, else (older server) read the card.
152
+ async function alreadyResolvedFrom(err, orgId, givenId, action) {
153
+ if (!err || err.status !== 400) return null;
154
+ const body = err.body || {};
155
+ if (body.code === 'already_resolved' && body.resolution) return { ...body.resolution, message: body.message };
156
+ if (!/not open for response/i.test(`${body.error || ''} ${err.message || ''}`)) return null;
157
+ try {
158
+ const ep = resolveEndpoint('inbox.get', { orgId, itemId: givenId });
159
+ const data = await client.get(ep.path, { service: ep.service });
160
+ return resolutionFromItem(data.item || data, action);
161
+ } catch { return null; }
162
+ }
163
+
113
164
  inbox
114
165
  .command('stats')
115
166
  .description('Show inbox statistics')
@@ -5,53 +5,26 @@
5
5
  * Phase 1 (instance-local): status reads the lease (never the key), refresh
6
6
  * runs the renewal, seats lists/cleans band holders (dry run default).
7
7
  *
8
- * Phase 2 (owner-only, talks to the quox.ai WEBSITE admin API with a separate
9
- * website session, see core/websiteSession.js): reissue re-bands/extends a
10
- * licence record, catalogue-sign re-signs the product catalogue. Both are DRY
11
- * RUN unless --apply. The signing key never leaves the website VM; the CLI
12
- * never sees, prints or sends key material or licence keys.
8
+ * Phase 2 (owner-only vendor tooling for the quox.ai website) lives in
9
+ * lib/vendor/licenceOwner.js and is registered only when that file is present.
10
+ * lib/vendor/ is excluded from the npm package (quox#1134): the public build
11
+ * never carries it.
13
12
  */
14
13
 
15
- import { writeFileSync } from 'node:fs';
16
14
  import { formatOutput, formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
17
15
  import { resolveEndpoint } from '../api/endpoints.js';
18
16
  import { QuoxValidationError } from '../core/errors.js';
19
- import {
20
- DEFAULT_WEBSITE_URL, DEFAULT_WEBSITE_PROFILE, normaliseWebsiteUrl, websiteLogin, websitePost,
21
- saveWebsiteSession, clearWebsiteSession, getWebsiteSession,
22
- } from '../core/websiteSession.js';
23
17
 
24
- const OWNER_HELP = `
25
- Owner-only (website admin session, dry run unless --apply):
26
- website-login log in to the quox.ai admin API once (password is not stored)
27
- reissue <licenseId> band bare paid ids / add ids, re-signed on the website VM
28
- catalogue-sign re-sign the product catalogue with the live key
29
- See the quox-licensing skill (~/.claude/skills/quox-licensing/SKILL.md).
30
- `;
31
-
32
- const ID_LIST_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
33
-
34
- export function parseIdList(value, flag) {
35
- if (value === undefined) return undefined;
36
- const ids = String(value).split(',').map((s) => s.trim()).filter(Boolean);
37
- for (const id of ids) {
38
- if (!ID_LIST_RE.test(id)) throw new QuoxValidationError(`${flag}: invalid plugin id "${id}"`);
39
- }
40
- return ids;
41
- }
42
-
43
- export function parseOrgs(value) {
44
- if (value === undefined) return undefined;
45
- const n = Number(value);
46
- if (!Number.isInteger(n) || n < 1 || n > 1000) throw new QuoxValidationError('--orgs must be an integer between 1 and 1000');
47
- return n;
18
+ // quox#1134: the owner-only website verbs are vendor tooling, kept out of the published package (package.json
19
+ // "files" excludes lib/vendor/). Present in the dev tree, absent for every npm user.
20
+ let VENDOR_OWNER = null;
21
+ try {
22
+ VENDOR_OWNER = await import('../vendor/licenceOwner.js');
23
+ } catch (e) {
24
+ if (!(e?.code === 'ERR_MODULE_NOT_FOUND' && String(e?.message || '').includes('licenceOwner'))) throw e;
48
25
  }
49
26
 
50
- function list(arr) {
51
- return arr && arr.length ? arr.join(', ') : '(none)';
52
- }
53
-
54
- export function registerLicenceCommands(program, ctx) {
27
+ export function registerLicenceCommands(program, ctx, { owner = VENDOR_OWNER } = {}) {
55
28
  const { client, auth } = ctx;
56
29
  const fetchImpl = ctx.websiteFetch || globalThis.fetch;
57
30
  const json = () => program.opts().output === 'json';
@@ -60,8 +33,8 @@ export function registerLicenceCommands(program, ctx) {
60
33
  .command('licence')
61
34
  .alias('license')
62
35
  .description('Instance licence status, lease refresh and seat bands')
63
- .addHelpText('after', OWNER_HELP)
64
36
  .action(() => { licence.help(); });
37
+ if (owner) licence.addHelpText('after', owner.OWNER_HELP);
65
38
 
66
39
  licence
67
40
  .command('status')
@@ -162,119 +135,5 @@ export function registerLicenceCommands(program, ctx) {
162
135
  }
163
136
  });
164
137
 
165
- // ---------------- Phase 2: owner-only, website admin API ----------------
166
-
167
- licence
168
- .command('website-login')
169
- .description('Log in to the quox.ai website admin API for reissue/catalogue-sign (stores the session cookie only, 0600)')
170
- .option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
171
- .option('--url <url>', 'website base URL', DEFAULT_WEBSITE_URL)
172
- .option('--username <name>', 'website admin username')
173
- .option('--password-stdin', 'read the password from stdin (one line) instead of prompting')
174
- .action(async (opts) => {
175
- normaliseWebsiteUrl(opts.url); // refuse a bad or plain-http URL before asking for a password
176
- let { username } = opts;
177
- let password;
178
- if (opts.passwordStdin) {
179
- const chunks = [];
180
- for await (const c of process.stdin) chunks.push(Buffer.from(c));
181
- password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
182
- }
183
- if (!username || !password) {
184
- const { default: inquirer } = await import('inquirer');
185
- if (!username) username = (await inquirer.prompt([{ type: 'input', name: 'u', message: 'Website admin username:' }])).u;
186
- if (!password) password = (await inquirer.prompt([{ type: 'password', name: 'p', message: 'Password:', mask: '*' }])).p;
187
- }
188
- if (!username || !password) throw new QuoxValidationError('username and password are required');
189
- const session = await websiteLogin({ url: opts.url, username, password, fetchImpl });
190
- password = null;
191
- saveWebsiteSession(opts.websiteProfile, session);
192
- if (json()) { console.log(JSON.stringify({ profile: opts.websiteProfile, url: session.url, username: session.username, must_change_password: session.must_change_password }, null, 2)); return; }
193
- printSuccess(`Website session saved for ${session.username} at ${session.url} (profile ${opts.websiteProfile}, valid ~24h). Password not stored.`);
194
- if (session.must_change_password) printWarning('This admin must change their password on the website before licence operations will be allowed (HTTP 403 until then).');
195
- });
196
-
197
- licence
198
- .command('website-logout')
199
- .description('End the website admin session and delete the stored cookie')
200
- .option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
201
- .action(async (opts) => {
202
- if (getWebsiteSession(opts.websiteProfile)) {
203
- try { await websitePost(opts.websiteProfile, '/api/admin/logout', {}, { fetchImpl }); } catch { /* session may already be dead */ }
204
- }
205
- const had = clearWebsiteSession(opts.websiteProfile);
206
- printSuccess(had ? `Website session ${opts.websiteProfile} removed.` : `No website session ${opts.websiteProfile} was stored.`);
207
- });
208
-
209
- licence
210
- .command('reissue')
211
- .description('Owner-only: band bare paid ids and add ids on a licence record, re-signed on the website (dry run unless --apply)')
212
- .argument('<licenseId>', 'licence id, e.g. lic_abc123')
213
- .option('--orgs <n>', 'org band for every banded/added id (default 10)')
214
- .option('--add <ids>', 'comma-separated ids to ADD as <id>@orgs=N')
215
- .option('--band <ids>', 'comma-separated ids already present to FORCE-band')
216
- .option('--apply', 'actually reissue (otherwise dry run, nothing changes)')
217
- .option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
218
- .action(async (licenseId, opts) => {
219
- const body = { dryRun: !opts.apply };
220
- const orgs = parseOrgs(opts.orgs);
221
- if (orgs !== undefined) body.orgs = orgs;
222
- const add = parseIdList(opts.add, '--add');
223
- if (add) body.add = add;
224
- const band = parseIdList(opts.band, '--band');
225
- if (band) body.band = band;
226
-
227
- const d = await websitePost(opts.websiteProfile, `/api/admin/licenses/${encodeURIComponent(licenseId)}/reissue`, body, { fetchImpl });
228
- if (json()) { console.log(JSON.stringify(d, null, 2)); return; }
229
-
230
- printInfo(`Licence ${d.license_id}${d.customer_email ? ` (${d.customer_email})` : ''}: max_servers ${d.max_servers ?? '?'}, expires ${d.expires_at || 'never'}`);
231
- const removed = new Set(d.diff?.removed || []);
232
- const added = new Set(d.diff?.added || []);
233
- for (const e of d.current || []) console.log(` ${removed.has(e) ? '-' : ' '} ${e}`);
234
- for (const e of d.next || []) if (added.has(e)) console.log(` + ${e}`);
235
- if (d.unknown_ids?.length) printWarning(`Not issuable (unknown to the signed catalogue), apply will be refused: ${d.unknown_ids.join(', ')}`);
236
-
237
- if (!d.changed) { printSuccess('No change needed: the licence already has these entitlements.'); return; }
238
- if (d.dry_run) { printInfo('DRY RUN: nothing changed. Re-run with --apply to reissue.'); return; }
239
- printSuccess(`Reissued ${d.license_id}. Audit/backup id: ${d.backup_id}`);
240
- printInfo('Instances pick this up on their next lease renewal (~6h). To pull it now, on the instance: quox licence refresh');
241
- });
242
-
243
- licence
244
- .command('catalogue-sign')
245
- .alias('catalog-sign')
246
- .description('Owner-only: re-sign the product catalogue with the live key on the website (dry run unless --apply)')
247
- .option('--apply', 'actually sign and publish to the website API (otherwise dry run)')
248
- .option('--out <path>', 'after --apply, save the signed catalogue (public artifact) here for committing')
249
- .option('--website-profile <name>', 'website session profile', DEFAULT_WEBSITE_PROFILE)
250
- .action(async (opts) => {
251
- if (opts.out && !opts.apply) throw new QuoxValidationError('--out is only meaningful with --apply');
252
- const d = await websitePost(opts.websiteProfile, '/api/admin/catalog/sign', { dryRun: !opts.apply }, { fetchImpl });
253
- if (opts.out && d.artifact) writeFileSync(opts.out, JSON.stringify(d.artifact, null, 2) + '\n', 'utf8');
254
- if (json()) {
255
- const { artifact, ...rest } = d;
256
- console.log(JSON.stringify({ ...rest, artifact_saved_to: opts.out && artifact ? opts.out : null }, null, 2));
257
- return;
258
- }
259
- const p = d.published || {};
260
- formatDetail({
261
- published: p.exists ? `${p.verified ? 'verified' : 'NOT VERIFIED'}, sha256 ${p.sha256 ? p.sha256.slice(0, 16) : '?'}..., ${p.generated_at || '?'}` : '(none)',
262
- products: `${p.product_count ?? 0} -> ${d.candidate?.product_count ?? '?'}`,
263
- governed: `${p.governed_count ?? 0} -> ${d.candidate?.governed_count ?? '?'}`,
264
- products_added: list(d.diff?.products_added),
265
- products_removed: list(d.diff?.products_removed),
266
- products_changed: list(d.diff?.products_changed),
267
- governed_added: list(d.diff?.governed_added),
268
- governed_removed: list(d.diff?.governed_removed),
269
- signing_key: d.signing ? (d.signing.trusted ? `trusted (${d.signing.trusted_key_prefix}...)` : (d.signing.configured ? 'NOT the trusted key: apply will be refused' : 'not configured: apply will be refused')) : '?',
270
- }, { title: d.dry_run ? 'Catalogue sign (DRY RUN)' : 'Catalogue sign' });
271
- if (d.diff?.governed_added?.length) {
272
- printWarning(`Newly governed: ${d.diff.governed_added.join(', ')}. Reissue the owner licence with these ids (quox licence reissue <id> --add ...) and refresh its lease BEFORE instances install this catalogue, or they lock out.`);
273
- }
274
- if (!d.changed) { printSuccess('No change: the published catalogue already matches.'); return; }
275
- if (d.dry_run) { printInfo('DRY RUN: nothing signed. Re-run with --apply to sign.'); return; }
276
- printSuccess(`Catalogue signed. sha256 ${d.sha256}, governed ${d.governed_count}, audit id ${d.audit_id}`);
277
- if (opts.out) printInfo(`Signed catalogue saved to ${opts.out}.`);
278
- printInfo('The website container copy does not survive a rebuild: commit the artifact to quox-website api/data/signed-catalog.json and quox-dashboard services/auth/data/signed-catalog.json (use --out to save it).');
279
- });
138
+ if (owner) owner.registerLicenceOwnerCommands(licence, { json, fetchImpl });
280
139
  }