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,324 @@
1
+ /**
2
+ * `quox gripe` — QUOXIFY PASS 2 (DEVSUITE phase 15).
3
+ *
4
+ * The bundle audit (note #2840, theme S9) found Gripe had no `quox` verb at
5
+ * all, despite one being in its own definition of done. The customer-facing
6
+ * filing path was chat-agent-only, which meant a user had to know an agent
7
+ * existed and open a chat surface to report that something was broken. The
8
+ * fastest possible complaint is a one-liner in the terminal you are already
9
+ * in, which is exactly what the original terminal-only gripe skill was for.
10
+ *
11
+ * This wraps the WORKING path rather than building a second one: it talks to
12
+ * the GRIPE agent over the same governed chat route the dashboard and the
13
+ * Discord/Telegram bridges use, and GRIPE files the issue through the GitHub
14
+ * connector. No parallel issue store, no second credential model (the audit
15
+ * flagged three entry paths with three credential models; this adds a fourth
16
+ * ENTRY but reuses the third's governed path deliberately).
17
+ *
18
+ * Honest prerequisite, per the audit's "disclose the prereq" finding: filing
19
+ * needs a configured GitHub connector for the org. If it is missing, the
20
+ * agent says so and this command surfaces that plainly instead of pretending
21
+ * an issue was created.
22
+ */
23
+
24
+ import { formatDetail, printInfo, printWarning } from '../core/output.js';
25
+ import { events, line as glyphLine, printEvent } from '../core/quoxglyph.js';
26
+ import { resolveEndpoint } from '../api/endpoints.js';
27
+ import { resolveOrgId } from './helpers.js';
28
+
29
+ export function registerGripeCommands(program, { client, auth }) {
30
+ program
31
+ .command('gripe')
32
+ .description('File a one-liner complaint as a GitHub issue (via the GRIPE agent)')
33
+ .argument('<complaint...>', 'the gripe, e.g. "sidebar tooltip clips on resize"')
34
+ .option('--repo <owner/name>', 'target repo (GRIPE defaults sensibly when omitted)')
35
+ .option('--label <label>', 'issue label, e.g. bug, idea, fleet')
36
+ .option('--yes', 'skip the confirmation step and let GRIPE file it directly')
37
+ .action(async (complaintParts, options) => {
38
+ auth.requireAuth();
39
+ const complaint = complaintParts.join(' ').trim();
40
+ if (!complaint) throw new Error('say what is wrong: quox gripe "the thing that is broken"');
41
+
42
+ printEvent(glyphLine('quox', 'filing gripe via GRIPE'));
43
+ const orgId = resolveOrgId(auth, program);
44
+ const data = await askGripe({ client, complaint, repo: options.repo, label: options.label, yes: !!options.yes, orgId });
45
+
46
+ if (program.opts().output === 'json') {
47
+ formatDetail(data, { format: 'json' });
48
+ return;
49
+ }
50
+
51
+ const r = await settleGripe({ client, data, complaint, options, orgId });
52
+ console.log('');
53
+ if (r.superseded) printInfo(SUPERSEDED_MESSAGE);
54
+ else console.log(r.response || '(no response)');
55
+ console.log('');
56
+ for (const n of r.notes) {
57
+ if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url));
58
+ else if (n.kind === 'info') printInfo(n.text);
59
+ else printWarning(n.text);
60
+ }
61
+ });
62
+ }
63
+
64
+ const SUPERSEDED_MESSAGE = 'GRIPE agent did not file this itself; its reply is superseded by the direct filing below.';
65
+
66
+ /**
67
+ * Ask the GRIPE agent to file `complaint` over the governed chat route. -> the raw chat response.
68
+ * Shared by `quox gripe` and the quox client's /gripe, so there is one filing path.
69
+ */
70
+ export async function askGripe({ client, complaint, repo, label, yes = false, orgId = null }) {
71
+ // GRIPE's whole discipline is speed: it does not investigate, it files.
72
+ // So the prompt states everything up front rather than inviting a
73
+ // back-and-forth the terminal is a bad place for.
74
+ const parts = [`File this gripe: ${complaint}`];
75
+ if (repo) parts.push(`Repo: ${repo}.`);
76
+ if (label) parts.push(`Label: ${label}.`);
77
+ if (yes) parts.push('File it now without asking me to confirm.');
78
+ else parts.push('Confirm the repo, label and title with me before filing.');
79
+ const ep = resolveEndpoint('chat.claude');
80
+ const body = { prompt: parts.join(' '), selected_agent: 'gripe' };
81
+ // The collector tracks which tools ran PER caller-supplied session_id;
82
+ // sending none meant tools_used always came back [] and classification
83
+ // fell back to parsing prose (DEVSUITE-GRIPE phase 5). One id per run.
84
+ body.session_id = `GRIPE_CLI_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
85
+ if (orgId) body.org_id = orgId;
86
+ return client.post(ep.path, { service: ep.service, body, timeout: 120000 });
87
+ }
88
+
89
+ /**
90
+ * Decide the outcome of a GRIPE run and, with yes, run the deterministic file when the agent did not.
91
+ * -> { response, outcome, superseded, url, notes: [{ kind: 'created', url } | { kind: 'info'|'warn', text }] }.
92
+ * Prints nothing: the caller renders the notes (console for `quox gripe`, history lines for /gripe).
93
+ */
94
+ export async function settleGripe({ client, data, complaint, options = {}, orgId = null }) {
95
+ const response = data?.result || data?.response || data?.text || '';
96
+ const yes = !!options.yes;
97
+ // tools_used is the collector's own record of which tools ran. Passing it
98
+ // is what stops the outcome from depending on how the agent phrased its
99
+ // reply. Absent (older collector), classification falls back to prose.
100
+ const toolsUsed = Array.isArray(data?.tools_used) ? data.tools_used : null;
101
+ const outcome = classifyGripeResponse(response, { assumedYes: yes, toolsUsed });
102
+ // quox#668: with --yes, a non-filing agent run is followed by the
103
+ // deterministic file. Printing the agent's prose first ("no tool, cannot
104
+ // file") right above "issue created" contradicted the real outcome. In
105
+ // that case the agent's reply is superseded, so say so instead of
106
+ // echoing a claim the next lines disprove.
107
+ const superseded = yes && ['no-connector', 'awaiting-confirm', 'not-filed'].some((k) => outcome.kind === k || outcome.hint === k);
108
+ const notes = [];
109
+ let url = null;
110
+ const det = async () => {
111
+ const r = await fileDeterministically({ client, complaint, options, orgId, quiet: true });
112
+ notes.push(...r.notes); if (r.url) url = r.url;
113
+ return r;
114
+ };
115
+ if (outcome.kind === 'filed') {
116
+ url = outcome.url; notes.push({ kind: 'created', url });
117
+ } else if (outcome.kind === 'filed-unknown') {
118
+ notes.push({ kind: 'warn', text: 'GRIPE called github_create_issue but no issue link came back. Check the repo before re-running: '
119
+ + 'the issue may exist, and a re-run would file it twice.' });
120
+ } else if (outcome.hint === 'no-connector' || outcome.kind === 'no-connector') {
121
+ // quox#621: "no connector" from the AGENT is not proof the org lacks
122
+ // one. The agent's tool-resolution can transiently fail (the gripe
123
+ // filed on an immediate retry, same org) or the github_create_issue
124
+ // tool may not be wired to this agent (#581), while a healthy github
125
+ // credential sits in the vault. The deterministic path resolves that
126
+ // credential server-side over a DIFFERENT route (the /github/issues
127
+ // proxy, not the agent tool), so with --yes we try it before believing
128
+ // the agent: if a connector really exists this files it (turning a
129
+ // silent-loss-then-retry into a first-try success); only if the vault
130
+ // path ALSO cannot file is the connector genuinely missing, and only
131
+ // then do we send the user to Settings.
132
+ if (yes) {
133
+ const r = await det();
134
+ if (!r.filed) notes.push({ kind: 'warn', text: NO_CONNECTOR_MESSAGE });
135
+ } else {
136
+ notes.push({ kind: 'warn', text: NO_CONNECTOR_MESSAGE });
137
+ }
138
+ } else if (outcome.hint === 'auth-unavailable' || outcome.kind === 'auth-unavailable') {
139
+ notes.push({ kind: 'warn', text: AUTH_UNAVAILABLE_MESSAGE });
140
+ } else if (outcome.hint === 'awaiting-confirm' || outcome.kind === 'awaiting-confirm') {
141
+ if (yes) await det();
142
+ else notes.push({ kind: 'info', text: 'GRIPE is asking you to confirm. Re-run with --yes to file it directly.' });
143
+ } else if (outcome.kind === 'not-filed') {
144
+ if (yes) await det();
145
+ else notes.push({ kind: 'warn', text: 'Not filed: github_create_issue was never called on this run.' });
146
+ } else {
147
+ notes.push({ kind: 'warn', text: 'No issue URL came back, so treat this as NOT filed until you can see the issue.' });
148
+ }
149
+ return { response, outcome, superseded, url, notes };
150
+ }
151
+
152
+ // quox#478: --yes must GUARANTEE a headless file, not merely ask the model
153
+ // nicely. When the LLM run verifiably did not file (tools_used is the
154
+ // collector's own record), fall back to the governed GitHub proxy
155
+ // (POST /github/issues: user-authed, entitlement-gated, credential resolved
156
+ // from the org vault server-side) and file deterministically. Deliberately
157
+ // NOT run after 'filed-unknown' (the issue may exist; a second file would
158
+ // duplicate it). It IS now run after 'no-connector' with --yes (quox#621):
159
+ // the proxy resolves the vault credential over a DIFFERENT route than the
160
+ // agent tool, so it succeeds when the agent merely failed to resolve/​wire the
161
+ // tool (#581) — and the caller falls back to NO_CONNECTOR_MESSAGE only if this
162
+ // also cannot file. Still NOT auto-run after auth-unavailable (a 403/429/503
163
+ // is a platform outage the proxy would hit too; retrying shortly is the fix).
164
+ // Returns { filed, url?, error? } so the caller can decide the message.
165
+ export async function fileDeterministically({ client, complaint, options, orgId, quiet = false }) {
166
+ const notes = [];
167
+ const say = (n) => { notes.push(n); if (quiet) return; if (n.kind === 'created') printEvent(events.created('quox', 'issue', n.url)); else if (n.kind === 'info') printInfo(n.text); else printWarning(n.text); };
168
+ const repoArg = options.repo || 'quoxai/quox';
169
+ const [owner, repo] = repoArg.includes('/') ? repoArg.split('/', 2) : ['quoxai', repoArg];
170
+ const firstLine = complaint.split('\n')[0];
171
+ const title = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
172
+ const ep = resolveEndpoint('github.issue.create');
173
+ try {
174
+ const data = await client.post(ep.path, {
175
+ service: ep.service,
176
+ body: {
177
+ owner,
178
+ repo,
179
+ title,
180
+ body: `## Gripe\n${complaint}\n\n## Context\n- Filed deterministically by \`quox gripe --yes\` (the GRIPE agent run did not file; quox#478 guarantee path)\n- Priority: minor / off-task observation\n\n## Suggested Action\nInvestigate and fix if confirmed.`,
181
+ labels: [options.label || 'fleet/qa'],
182
+ ...(orgId ? { org_id: orgId } : {}),
183
+ },
184
+ });
185
+ const url = data?.issue?.html_url || data?.issue?.url || null;
186
+ if (url) {
187
+ say({ kind: 'created', url });
188
+ say({ kind: 'info', text: 'Filed via the deterministic path (the GRIPE agent run did not file; --yes now guarantees the file).' });
189
+ return { filed: true, url, notes };
190
+ }
191
+ say({ kind: 'warn', text: 'Deterministic file returned no issue URL; treat as NOT filed until you can see the issue.' });
192
+ return { filed: false, notes };
193
+ } catch (err) {
194
+ say({ kind: 'warn', text: `Not filed: the GRIPE run did not file and the deterministic fallback failed too (${err.message}).` });
195
+ return { filed: false, error: err.message, notes };
196
+ }
197
+ }
198
+
199
+ const NO_CONNECTOR_MESSAGE =
200
+ 'No issue was filed: the org has no GitHub connector configured. Set one up in Settings > Integrations, then re-run.';
201
+
202
+ const AUTH_UNAVAILABLE_MESSAGE =
203
+ 'No issue was filed: a platform authorization step failed (membership verify or credential resolve did not answer). '
204
+ + 'This is NOT a missing GitHub connector. Retry shortly; if it persists, check auth service health.';
205
+
206
+ /**
207
+ * Decide what actually happened from GRIPE's reply.
208
+ *
209
+ * Split out of the action handler so it can be tested against real agent
210
+ * wording. The missing-connector branch used to match
211
+ * /not configured|no github|credential|connector/i, which does not match what
212
+ * the agent actually says. Observed live on 2026-08-23:
213
+ *
214
+ * "GitHub isn't connected for this org, no issue-filing tool is available
215
+ * right now. Connect GitHub in Settings > Integrations and I'll file it
216
+ * immediately after. I'm not fabricating an issue number."
217
+ *
218
+ * None of the four alternatives appear in that sentence, so the specific,
219
+ * actionable "connect GitHub in Settings" advice was skipped and the user got
220
+ * the generic "treat this as NOT filed" instead. Both are honest, which is why
221
+ * this never showed up as a bug: it degraded to a vaguer truth rather than a
222
+ * falsehood. It still costs the user the one sentence that says what to do.
223
+ *
224
+ * Matching prose with a regex is fragile by nature and this will drift again
225
+ * when the agent's phrasing changes. It is deliberately biased toward the
226
+ * generic branch: a missed connector hint is a papercut, whereas wrongly
227
+ * claiming a connector problem would send someone to Settings for a fault
228
+ * that is not there. The URL check stays first and stays authoritative, since
229
+ * a real issue link is the only thing that proves filing.
230
+ */
231
+ export function classifyGripeResponse(response, { assumedYes = false, toolsUsed = null } = {}) {
232
+ const text = String(response ?? '');
233
+ const url = (text.match(/https:\/\/github\.com\/\S+\/issues\/\d+/) || [])[0];
234
+ if (url) return { kind: 'filed', url, certain: true };
235
+
236
+ // The Claude CLI's own error for a NATIVE call to a tool that is not native.
237
+ // In CLI-tools mode the catalogue runs only via `quox tool exec`, so this
238
+ // string means github_create_issue never ran (2026-10-10: every --yes gripe
239
+ // fell to "unknown" on it and never reached the direct filing).
240
+ if (/No such tool available/i.test(text)) {
241
+ return { kind: 'not-filed', certain: true, hint: null };
242
+ }
243
+
244
+ // Structured evidence beats prose. The collector returns `tools_used` on
245
+ // every chat response, so whether github_create_issue actually RAN is a fact,
246
+ // not something to infer from English. Reading the outcome out of the agent's
247
+ // wording is what this function got wrong twice: once when the
248
+ // missing-connector phrasing changed, once when the bare word "connector"
249
+ // matched a healthy reply. A third regex patch would be the same mistake
250
+ // again, so the outcome now comes from the tool list and prose is demoted to
251
+ // a HINT about what to advise, where being wrong costs a sentence rather than
252
+ // a false claim.
253
+ // An EMPTY list is NOT evidence. The collector tracks MCP tool calls per
254
+ // caller-supplied session_id, so a request that sends none gets [] back
255
+ // whether or not the tool ran. Reading that as "never called" would replace a
256
+ // hedged truth with a confident falsehood and invite a duplicate issue.
257
+ // Absence of evidence is not evidence of absence.
258
+ if (Array.isArray(toolsUsed) && toolsUsed.length > 0) {
259
+ const ran = toolsUsed.some((t) => String(t).endsWith('github_create_issue'));
260
+ if (ran) {
261
+ // The tool ran and returned no link. Saying "filed" would invent an
262
+ // issue; saying "not filed" would send the user to file a duplicate.
263
+ return { kind: 'filed-unknown', certain: true };
264
+ }
265
+ return { kind: 'not-filed', certain: true, hint: adviceHint(text, assumedYes) };
266
+ }
267
+
268
+ // No structured evidence available (an older collector, or a surface that
269
+ // does not return tools_used). Fall back to the prose-only classification,
270
+ // unchanged, rather than degrading every caller to "unknown".
271
+ const hint = adviceHint(text, assumedYes);
272
+ if (hint === 'no-connector') return { kind: 'no-connector' };
273
+ if (hint === 'auth-unavailable') return { kind: 'auth-unavailable' };
274
+ if (hint === 'awaiting-confirm') return { kind: 'awaiting-confirm' };
275
+ return { kind: 'unknown' };
276
+ }
277
+
278
+ /**
279
+ * What to ADVISE when nothing was filed. This is a hint, never an outcome:
280
+ * getting it wrong costs the user one unhelpful sentence, where getting the
281
+ * outcome wrong costs them a duplicate issue or a phantom one.
282
+ *
283
+ * @returns {'no-connector'|'awaiting-confirm'|null}
284
+ */
285
+ function adviceHint(text, assumedYes) {
286
+ // Platform-side authorization/availability failures beat the no-connector
287
+ // guess. On 2026-09-21 auth's rate limiter 429'd the collector's membership
288
+ // verify, the tool call 403'd with "no active membership", GRIPE narrated
289
+ // "GitHub isn't connected", and this function turned a platform outage into
290
+ // "the org has no GitHub connector configured" — a confident falsehood while
291
+ // the vault held three healthy credentials (quox#565). These shapes come
292
+ // from the collector's own error strings, so match them FIRST.
293
+ if (
294
+ /membership/i.test(text) ||
295
+ /verification .{0,20}unavailable/i.test(text) ||
296
+ /auth service did not answer/i.test(text) ||
297
+ /\b(403|503|429)\b/.test(text)
298
+ ) {
299
+ return 'auth-unavailable';
300
+ }
301
+
302
+ // Every alternative here must carry its own NEGATION. The bare words
303
+ // `credential` and `connector` used to be alternatives on their own, and on
304
+ // 2026-08-23 that fired on a reply where GRIPE said "Yes, I can see it" and
305
+ // then echoed the user's word inside the proposed issue title. The CLI told
306
+ // the user to go configure a connector that was working. `connector` and
307
+ // `credential` are what this feature is ABOUT, so they appear in healthy
308
+ // replies constantly; only a negation makes them evidence of anything.
309
+ const noConnector =
310
+ /not configured/i.test(text) ||
311
+ /no github/i.test(text) ||
312
+ /github (is|isn't|is not|was not|wasn't)\s*(n't)?\s*connect/i.test(text) ||
313
+ /no issue[- ]filing tool/i.test(text) ||
314
+ /connect github/i.test(text) ||
315
+ /no (\w+ ){0,2}(credential|connector|integration)/i.test(text) ||
316
+ /(credential|connector|integration)s?\s+(is|are)\s+(not|un)(configured|available|connected|set)/i.test(text);
317
+ if (noConnector) return 'no-connector';
318
+
319
+ // Only meaningful when the caller did NOT pass --yes: with --yes the prompt
320
+ // already told GRIPE to file without asking, so a reply with no URL is an
321
+ // unknown outcome, not a confirmation prompt.
322
+ if (!assumedYes) return 'awaiting-confirm';
323
+ return null;
324
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * QuoxHat commands (QUOXHAT #721 b): READ-ONLY views of the org's hats.
3
+ *
4
+ * quox hat list
5
+ * quox hat show <id|key> [--rev <n>]
6
+ * quox hat reset <id|key> --yes (audited reset-to-template)
7
+ *
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.
14
+ *
15
+ * Backend: services/auth/routes/quoxhat.js (see cli/lib/api/endpoints/quoxhat.js).
16
+ */
17
+
18
+ import chalk from 'chalk';
19
+ import { formatOutput, printInfo } from '../core/output.js';
20
+ import { resolveOrgId } from './helpers.js';
21
+ import { resolveEndpoint } from '../api/endpoints.js';
22
+
23
+ /** Authorization mode a hat declares; no authz block means permissive (grant ceiling). */
24
+ export function authzMode(def) {
25
+ const a = def && typeof def.authz === 'object' ? def.authz : null;
26
+ if (!a) return 'permissive';
27
+ return a.mode === 'restrictive' ? 'restrictive' : 'permissive (declared)';
28
+ }
29
+
30
+ /** "v9", "v8 (template v9 available)" or "custom". */
31
+ export function templateLabel(hat) {
32
+ if (!hat.template_key) return 'custom';
33
+ const cur = hat.template_version;
34
+ const latest = hat.template_latest;
35
+ // The latest revision's author (only on `show`, which carries history):
36
+ // anything not 'template:*' means the org edited this built-in, which
37
+ // freezes it out of automatic template upgrades until reset-to-template.
38
+ const latestAuthor = Array.isArray(hat.revisions) && hat.revisions[0] ? hat.revisions[0].created_by : null;
39
+ const customised = typeof latestAuthor === 'string' && !latestAuthor.startsWith('template:');
40
+ if (latest && cur && cur < latest) return `v${cur} (template v${latest} available)`;
41
+ return customised ? `v${cur || '?'} (edited)` : `v${cur || '?'}`;
42
+ }
43
+
44
+ function toolCounts(def) {
45
+ const explicit = Array.isArray(def?.tools?.explicit) ? def.tools.explicit.length : 0;
46
+ const bundles = Array.isArray(def?.tools?.bundles) ? def.tools.bundles : [];
47
+ const bundled = bundles.reduce((n, b) => n + (Array.isArray(b?.tools) ? b.tools.length : 0), 0);
48
+ return { explicit, bundles: bundles.length, bundled };
49
+ }
50
+
51
+ async function fetchList(client, orgId) {
52
+ const ep = resolveEndpoint('hat.list', { orgId });
53
+ return client.get(ep.path, { service: ep.service });
54
+ }
55
+
56
+ /** Resolve a hat argument (ULID or key) to its id using the org's hat list. */
57
+ export function findHat(hats, ref) {
58
+ const r = String(ref || '').trim();
59
+ return (hats || []).find(h => h.id === r) || (hats || []).find(h => h.key === r) || null;
60
+ }
61
+
62
+ export function registerHatCommands(program, ctx) {
63
+ const { client, auth } = ctx;
64
+
65
+ const hat = program
66
+ .command('hat')
67
+ .description('QuoxHat: read the org hats (tools, mode, revision, enforcement); reset a built-in to its template');
68
+
69
+ // ── hat list ────────────────────────────────────────────────────────────────
70
+ hat
71
+ .command('list')
72
+ .description('List the org hats with revision, template status, mode and tool counts')
73
+ .action(async () => {
74
+ auth.requireAuth();
75
+ const orgId = resolveOrgId(auth, program);
76
+ const data = await fetchList(client, orgId);
77
+ const hats = Array.isArray(data?.hats) ? data.hats : [];
78
+ const settings = data?.settings || {};
79
+ const format = program.opts().output;
80
+
81
+ if (format === 'json') {
82
+ console.log(JSON.stringify({ hats, settings }, null, 2));
83
+ return;
84
+ }
85
+
86
+ const rows = hats.map(h => {
87
+ const c = toolCounts(h.definition);
88
+ return {
89
+ key: h.key + (settings.default_hat_id === h.id ? ' *' : ''),
90
+ name: h.name,
91
+ rev: String(h.current_revision),
92
+ status: h.status,
93
+ template: templateLabel(h),
94
+ mode: authzMode(h.definition),
95
+ privileged: h.definition?.transitions?.privileged ? 'yes' : 'no',
96
+ tools: `${c.explicit} + ${c.bundled} in ${c.bundles} bundle(s)`,
97
+ id: h.id,
98
+ };
99
+ });
100
+ formatOutput({
101
+ rows,
102
+ columns: ['key', 'name', 'rev', 'status', 'template', 'mode', 'privileged', 'tools', 'id'],
103
+ headers: ['Key', 'Name', 'Rev', 'Status', 'Template', 'Mode', 'Privileged', 'Tools (explicit + bundled)', 'ID'],
104
+ }, { format, emptyMessage: 'No hats.' });
105
+ printInfo(`Enforcement: ${settings.enforcement || 'preview'} · selection: ${settings.selection_mode || 'manual'}${settings.default_hat_id ? ' · * = org default' : ' · org default: built-in Default'}`);
106
+ });
107
+
108
+ // ── hat show ────────────────────────────────────────────────────────────────
109
+ hat
110
+ .command('show <hat>')
111
+ .description('Show one hat (id or key): tools, bundles, mode, revision history, enforcement')
112
+ .option('--rev <n>', 'show a historical revision instead of the current one')
113
+ .action(async (ref, options) => {
114
+ auth.requireAuth();
115
+ const orgId = resolveOrgId(auth, program);
116
+ let rev;
117
+ if (options.rev !== undefined) {
118
+ rev = Number(options.rev);
119
+ if (!Number.isInteger(rev) || rev < 1) throw new Error(`--rev must be a positive integer (got '${options.rev}')`);
120
+ }
121
+
122
+ const list = await fetchList(client, orgId);
123
+ const found = findHat(list?.hats, ref);
124
+ if (!found) {
125
+ const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
126
+ throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
127
+ }
128
+
129
+ const ep = resolveEndpoint('hat.get', { orgId, hatId: found.id });
130
+ const data = await client.get(ep.path, {
131
+ service: ep.service,
132
+ ...(rev ? { query: { revision: rev } } : {}),
133
+ });
134
+ const h = data?.hat;
135
+ if (!h) throw new Error(`Hat '${ref}' not found`);
136
+ const enforcement = data.enforcement || list?.settings?.enforcement || 'preview';
137
+
138
+ if (program.opts().output === 'json') {
139
+ console.log(JSON.stringify({ hat: h, enforcement, settings: list?.settings || null }, null, 2));
140
+ return;
141
+ }
142
+
143
+ const def = h.definition || {};
144
+ const shown = h.revision_shown || h.current_revision;
145
+ const out = [];
146
+ out.push(chalk.bold(`${h.name} (${h.key})`) + chalk.dim(` ${h.id}`));
147
+ if (h.purpose) out.push(h.purpose);
148
+ out.push('');
149
+ out.push(`Revision: ${shown}${shown !== h.current_revision ? chalk.yellow(` (historical; current is ${h.current_revision})`) : ' (current)'}`);
150
+ out.push(`Digest: ${h.digest || '-'}`);
151
+ out.push(`Status: ${h.status}`);
152
+ out.push(`Template: ${templateLabel(h)}`);
153
+ out.push(`Mode: ${authzMode(def)}`);
154
+ out.push(`Enforcement: ${enforcement} (org)`);
155
+ out.push(`Transitions: confirm=${def.transitions?.confirm || 'none'} privileged=${def.transitions?.privileged ? 'yes' : 'no'}`);
156
+ out.push(`Agents: orchestrator=${def.agents?.orchestrator || '-'} specialists=${(def.agents?.specialists || []).join(', ') || 'none'}`);
157
+ out.push(`Budgets: initial_tool_target=${def.budgets?.initial_tool_target ?? '-'} max_manifest=${def.budgets?.max_manifest ?? '-'}`);
158
+ out.push('');
159
+ const explicit = def.tools?.explicit || [];
160
+ out.push(chalk.bold(`Explicit tools (${explicit.length}):`));
161
+ out.push(` ${explicit.join(', ') || 'none'}`);
162
+ const bundles = def.tools?.bundles || [];
163
+ out.push(chalk.bold(`Bundles (${bundles.length}, loaded per conversation with hat_load_bundle):`));
164
+ for (const b of bundles) out.push(` ${b.key}: ${(b.tools || []).join(', ')}`);
165
+ if (!bundles.length) out.push(' none');
166
+ const fams = def.tools?.connector_families || [];
167
+ out.push(chalk.bold('Connector families:') + ` ${fams.join(', ') || 'none'}`);
168
+ if (def.authz && typeof def.authz === 'object') {
169
+ if (Array.isArray(def.authz.allow)) out.push(`Authz allow: ${def.authz.allow.join(', ')}`);
170
+ if (Array.isArray(def.authz.deny)) out.push(`Authz deny: ${def.authz.deny.join(', ')}`);
171
+ }
172
+ const revisions = Array.isArray(h.revisions) ? h.revisions : [];
173
+ if (revisions.length) {
174
+ out.push('');
175
+ out.push(chalk.bold('Revisions:'));
176
+ for (const r of revisions) {
177
+ out.push(` ${String(r.revision).padStart(3)} ${r.created_at || ''} ${r.created_by || ''} ${r.change_reason || ''}`);
178
+ }
179
+ }
180
+ console.log(out.join('\n'));
181
+ });
182
+
183
+ // ── hat reset ───────────────────────────────────────────────────────────────
184
+ hat
185
+ .command('reset <hat>')
186
+ .description('Reset an org-edited built-in hat to its current template (drops the org edits; audited)')
187
+ .option('--yes', 'confirm: the org edits on this hat are replaced by the template')
188
+ .action(async (ref, options) => {
189
+ auth.requireAuth();
190
+ if (!options.yes) throw new Error(`Resetting '${ref}' replaces its org edits with the template. Re-run with --yes to confirm.`);
191
+ const orgId = resolveOrgId(auth, program);
192
+ const list = await fetchList(client, orgId);
193
+ const found = findHat(list?.hats, ref);
194
+ if (!found) {
195
+ const keys = (list?.hats || []).map(h => h.key).join(', ') || 'none';
196
+ throw new Error(`No hat '${ref}' in this org. Available: ${keys}`);
197
+ }
198
+ const ep = resolveEndpoint('hat.reset', { orgId, hatId: found.id });
199
+ const data = await client.post(ep.path, { service: ep.service, body: {} });
200
+ const h = data?.hat;
201
+ if (!h) throw new Error(`Reset of '${ref}' returned no hat`);
202
+ if (program.opts().output === 'json') { console.log(JSON.stringify({ hat: h }, null, 2)); return; }
203
+ console.log(` Ⓠ QUOX │ hat ${h.key} reset to template │ rev ${h.current_revision} · ${templateLabel(h)} ✔`);
204
+ });
205
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Shared helpers for command modules
3
+ */
4
+
5
+ import { QuoxValidationError } from '../core/errors.js';
6
+
7
+ /**
8
+ * Resolve the active org ID from --org flag or stored auth.
9
+ * Throws if no org context is available.
10
+ */
11
+ export function resolveOrgId(auth, program) {
12
+ const orgId = auth.getOrgId(program.opts().org);
13
+ if (!orgId) {
14
+ // Ask about AUTH before blaming the org, because a logged-out user has no
15
+ // org for the obvious reason, and telling them to switch org sends them
16
+ // down a path that cannot work.
17
+ //
18
+ // 2026-08-18: a new-user setup audit ran `quox slot create audit-demo` on
19
+ // a shell with no token. It answered "No active org. Use --org <id> or
20
+ // `quox org switch <id>` first." Following that instruction gave a
21
+ // DIFFERENT error, "No authentication token provided ... Run `quox login`".
22
+ // Two errors, two dead ends, and the real first step named only in the
23
+ // second one. The owner's standing complaint about this bundle is that it
24
+ // is not "easy as pie" to set up; this was the very first command in that
25
+ // path, and it misdirected.
26
+ //
27
+ // Every command that needs an org goes through here, so the fix lands once
28
+ // for all of them.
29
+ auth.requireAuth();
30
+
31
+ // requireAuth() passing does NOT mean a USER is logged in. isLoggedIn()
32
+ // also accepts an ambient QUOX_SERVICE_KEY as the automation fallback, and
33
+ // a service key carries no organisation — so on any box that exports one
34
+ // globally (every dev box here does) a logged-out user sails past the auth
35
+ // check and lands on "No active org", which sends them to `quox org
36
+ // switch` when the real first step is `quox login`.
37
+ //
38
+ // 2026-08-19: reproduced exactly that with `quox slot list --profile local`
39
+ // on a shell with QUOX_SERVICE_KEY exported. This is the SAME misdirection
40
+ // the 2026-08-18 note above set out to kill, arriving through the one door
41
+ // that check cannot see: the fix asked "are you authenticated?" when the
42
+ // question that matters is "authenticated AS WHOM?".
43
+ const hasUserToken = Boolean(auth.getTokens?.()?.access_token);
44
+ if (!hasUserToken) {
45
+ throw new Error(
46
+ 'Authenticated with a service key, which has no organisation. '
47
+ + 'Run `quox login` to act as a user, or pass --org <id> to name the org explicitly.'
48
+ );
49
+ }
50
+
51
+ throw new Error('No active org. Use --org <id>, or pick one with `quox org list` then `quox org switch <id>`.');
52
+ }
53
+ return orgId;
54
+ }
55
+
56
+ /**
57
+ * Safely parse a JSON string from CLI input.
58
+ * Throws QuoxValidationError with a clear message on invalid JSON.
59
+ */
60
+ export function parseJSON(value, label = 'JSON') {
61
+ try {
62
+ return JSON.parse(value);
63
+ } catch {
64
+ throw new QuoxValidationError(`Invalid ${label}: not valid JSON. Check your input and try again.`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Build a query object from common list options.
70
+ */
71
+ export function buildListQuery(options) {
72
+ const query = {};
73
+ if (options.limit) query.limit = options.limit;
74
+ if (options.offset) query.offset = options.offset;
75
+ if (options.search) query.search = options.search;
76
+ if (options.status) query.status = options.status;
77
+ if (options.sort) query.sort = options.sort;
78
+ return query;
79
+ }