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
@@ -137,6 +137,23 @@ function renderRoadmapsList(roadmaps) {
137
137
  return lines;
138
138
  }
139
139
 
140
+ function renderSearchResults(data) {
141
+ const results = data.results || [];
142
+ if (results.length === 0) return [chalk.dim(` No streams match "${data.q}".`)];
143
+ const lines = [''];
144
+ for (const r of results) {
145
+ const g = statusGlyph(r.status);
146
+ const where = [...new Set((r.matches || []).map((m) => m.where))].join(', ');
147
+ const first = (r.matches || []).find((m) => m.snippet) || {};
148
+ lines.push(
149
+ ` ${g.color(g.glyph)} ${pad(truncate(r.id, ID_WIDTH), ID_WIDTH)} ${pad(truncate(r.name, NAME_WIDTH), NAME_WIDTH)} `
150
+ + `${pad(r.status, 15)} ${pad(where, 18)} ${chalk.dim(truncate(first.snippet || '', 60))}`
151
+ );
152
+ }
153
+ lines.push('', chalk.dim(` ${results.length} stream${results.length === 1 ? '' : 's'} matched "${data.q}"`));
154
+ return lines;
155
+ }
156
+
140
157
  function streamLine(s, { indent = 0, showReasons = false } = {}) {
141
158
  const prefix = indent > 0 ? ' '.repeat(indent) + '└ ' : ' ';
142
159
  const g = statusGlyph(s.status);
@@ -546,6 +563,27 @@ export function registerPlanCommands(program, ctx) {
546
563
  printSuccess(`Created roadmap ${created.id || options.id} for org ${orgId}`);
547
564
  });
548
565
 
566
+ // ── search ───────────────────────────────────────────────────────────
567
+
568
+ plan
569
+ .command('search <text...>')
570
+ .description('Search streams by text (id, name, notes, plan, phase titles)')
571
+ .option('--roadmap <id>', 'limit to one roadmap')
572
+ .option('--status <status>', `limit to one status (${VALID_STATUSES.join(', ')})`)
573
+ .action(async (textParts, options) => {
574
+ auth.requireAuth();
575
+ if (options.status && !VALID_STATUSES.includes(options.status)) {
576
+ throw new Error(`invalid --status "${options.status}" (expected ${VALID_STATUSES.join(', ')})`);
577
+ }
578
+ const query = { q: textParts.join(' ') };
579
+ if (options.roadmap) query.roadmap = options.roadmap;
580
+ if (options.status) query.status = options.status;
581
+ const ep = resolveEndpoint('quoxplan.search');
582
+ const data = await client.get(ep.path, { service: ep.service, query });
583
+ if (isJsonOutput(program)) return printJson(data);
584
+ console.log(renderSearchResults(data).join('\n'));
585
+ });
586
+
549
587
  // ── list ─────────────────────────────────────────────────────────────
550
588
 
551
589
  plan
@@ -2,10 +2,12 @@
2
2
  * Plugin commands: org-scoped plugin install registry (PD-1..PD-8)
3
3
  *
4
4
  * quox plugin list|get|install|update|uninstall|mine
5
+ * quox plugin enable <id> (PLUGIN-IMG #1012: prebuilt private image, licence first)
5
6
  * quox plugin data list|get|set|delete
6
7
  * quox plugin secrets list|set|reveal|delete
7
8
  * quox plugin assign list|add|remove
8
9
  * quox plugin trusted-keys list|add|remove
10
+ * quox plugin revocations list|add|remove
9
11
  *
10
12
  * Backend: services/auth/routes/plugins.js, mounted org-scoped at
11
13
  * `/api/orgs/:orgId/...` (server.js). Install/update/uninstall and all
@@ -18,6 +20,9 @@ import { readFileSync } from 'node:fs';
18
20
  import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
19
21
  import { resolveOrgId, parseJSON } from './helpers.js';
20
22
  import { resolveEndpoint } from '../api/endpoints.js';
23
+ import { enablePlugin, entitlementFrom } from '../server/pluginEnable.js';
24
+ import { resolveInstall, readEnvKeys } from '../server/install.js';
25
+ import { createSys } from '../server/sys.js';
21
26
 
22
27
  /** Read + JSON.parse a file, wrapping errors with the file path for context. */
23
28
  function readJsonFile(filePath, label) {
@@ -113,6 +118,56 @@ export function registerPluginCommands(program, ctx) {
113
118
  }, { title: `Licence: ${pluginId}` });
114
119
  });
115
120
 
121
+ // ── plugin enable (PLUGIN-IMG #1012) ────────────────────────────────────────
122
+ // Starts a licensed plugin from its prebuilt private image on this
123
+ // self-hosted install. Licence first: unlicensed means nothing runs.
124
+
125
+ plugin
126
+ .command('enable')
127
+ .description('Start a licensed plugin from its prebuilt image on this self-hosted install (QuoxPlan for now). Unlicensed: refused, nothing pulled.')
128
+ .argument('<pluginId>', 'plugin id, e.g. quoxplan')
129
+ .option('--dir <path>', 'the install directory (default: the one Getting started recorded)')
130
+ .action(async (pluginId, opts) => {
131
+ auth.requireAuth();
132
+ const deps = ctx.serverDeps || {};
133
+ const env = deps.env || process.env;
134
+ const sys = deps.sys || createSys();
135
+ const checkEntitlement = async (id) => {
136
+ const ep = resolveEndpoint('setup.licensing.entitlements.list');
137
+ const data = await client.get(ep.path, { service: ep.service });
138
+ return entitlementFrom(data, id);
139
+ };
140
+ // Licence before anything that touches the box: an unlicensed enable must not even look for the install.
141
+ let ent;
142
+ try { ent = await checkEntitlement(pluginId); } catch (err) { ent = err; }
143
+ const resolveFirst = !(ent instanceof Error) && ent && ent.entitled;
144
+ let install = null;
145
+ if (resolveFirst) {
146
+ const i = await resolveInstall({ dir: opts.dir, env, sys, journey: deps.journey });
147
+ if (i.error) { const e = new Error(i.error); e.exitCode = 2; throw e; }
148
+ install = i;
149
+ }
150
+ const tag = install ? (env.QUOX_IMAGE_TAG || readEnvKeys(install.repoDir, ['QUOX_IMAGE_TAG']).QUOX_IMAGE_TAG || '') : '';
151
+ const result = await enablePlugin({
152
+ pluginId,
153
+ checkEntitlement: async () => { if (ent instanceof Error) throw ent; return ent; },
154
+ install: install || { repoDir: '' },
155
+ sys,
156
+ tag,
157
+ ...(deps.getPullCredential ? { getPullCredential: deps.getPullCredential } : {}),
158
+ ...(deps.sleep ? { sleep: deps.sleep } : {}),
159
+ log: (l) => printInfo(l),
160
+ });
161
+ if (program.opts().output === 'json') {
162
+ console.log(JSON.stringify({ plugin: pluginId, ok: result.ok, stage: result.stage, message: result.message }, null, 2));
163
+ } else if (result.ok) {
164
+ printSuccess(result.message);
165
+ } else {
166
+ printWarning(result.message);
167
+ }
168
+ if (!result.ok) process.exitCode = result.stage === 'licence' ? 3 : 1;
169
+ });
170
+
116
171
  // ── plugin list ─────────────────────────────────────────────────────────────
117
172
 
118
173
  plugin
@@ -536,4 +591,59 @@ export function registerPluginCommands(program, ctx) {
536
591
  await client.delete(ep.path, reqOpts);
537
592
  printSuccess('Trusted key removed.');
538
593
  });
594
+
595
+ // ── plugin revocations (SDK-TEAM W5) ────────────────────────────────────────
596
+
597
+ const revocations = plugin
598
+ .command('revocations')
599
+ .description('Revoke a plugin signer key or one bundle digest for this org (admin only). A revoked plugin cannot be installed and stops loading on the next page load.');
600
+
601
+ revocations
602
+ .command('list')
603
+ .description('List revocations')
604
+ .action(async () => {
605
+ auth.requireAuth();
606
+ const orgId = resolveOrgId(auth, program);
607
+ const ep = resolveEndpoint('plugins.revocations.list', { orgId });
608
+ const result = await client.get(ep.path, { service: ep.service });
609
+ const rows = result.revocations || result || [];
610
+ formatOutput({
611
+ columns: ['id', 'kind', 'value', 'reason', 'revoked_by', 'revoked_at'],
612
+ headers: ['ID', 'Kind', 'Value', 'Reason', 'Revoked By', 'Revoked'],
613
+ rows,
614
+ }, { format: program.opts().output, emptyMessage: 'No revocations.' });
615
+ });
616
+
617
+ revocations
618
+ .command('add')
619
+ .description('Revoke a signer key or a bundle digest')
620
+ .option('--signer-key <hex>', 'raw Ed25519 public key (64 hex), as in signature.json public_key_hex')
621
+ .option('--bundle-digest <hex>', 'sha256 of plugin.esm.js (64 hex), as shown by quox plugin list')
622
+ .option('--reason <text>', 'why (kept in the audit trail)')
623
+ .action(async (options) => {
624
+ auth.requireAuth();
625
+ if (!!options.signerKey === !!options.bundleDigest) {
626
+ throw new Error('Specify exactly one of --signer-key or --bundle-digest.');
627
+ }
628
+ const orgId = resolveOrgId(auth, program);
629
+ const body = options.signerKey
630
+ ? { kind: 'signer_key', value: options.signerKey, reason: options.reason }
631
+ : { kind: 'bundle_digest', value: options.bundleDigest, reason: options.reason };
632
+ const ep = resolveEndpoint('plugins.revocations.create', { orgId });
633
+ const result = await client.post(ep.path, { service: ep.service, body });
634
+ printSuccess('Revoked.');
635
+ formatDetail(result.revocation || result, { format: program.opts().output });
636
+ });
637
+
638
+ revocations
639
+ .command('remove')
640
+ .description('Lift a revocation')
641
+ .argument('<revocationId>', 'revocation ID')
642
+ .action(async (revocationId) => {
643
+ auth.requireAuth();
644
+ const orgId = resolveOrgId(auth, program);
645
+ const ep = resolveEndpoint('plugins.revocations.delete', { orgId, revocationId });
646
+ await client.delete(ep.path, { service: ep.service });
647
+ printSuccess('Revocation lifted.');
648
+ });
539
649
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `quox presence [who]` / `quox presence set present|away|sleeping`: PRESENCE (owner 2026-10-16).
3
+ * A human says they are not at their terminal (/afk, /sleep in quox do the same). Sessions and agents
4
+ * read it so they stop waiting on approvals, which still queue normally: nothing is ever auto-approved.
5
+ * Collector routes GET/PUT /api/v1/presence; the org is the caller's, only a human login sets its own.
6
+ */
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { resolveOrgId } from './helpers.js';
9
+
10
+ const STATES = ['present', 'away', 'sleeping'];
11
+
12
+ function stamp(iso) {
13
+ const d = new Date(iso);
14
+ return Number.isNaN(d.getTime()) ? String(iso || '') : `${d.toISOString().slice(0, 10)} ${d.toISOString().slice(11, 16)} UTC`;
15
+ }
16
+
17
+ /** One plain line for a presence record. */
18
+ export function presenceText(p) {
19
+ if (!p || !p.away) return `${p?.who || 'they'}: present`;
20
+ const note = p.note ? `, "${p.note}"` : '';
21
+ return `${p.who}: ${p.auto ? 'away (idle)' : p.state} since ${stamp(p.since)}${note} (approvals will queue)`;
22
+ }
23
+
24
+ /** The fields an approval request reports, from GET /api/v1/presence. */
25
+ export function approvalPresenceFields(list) {
26
+ const away = (list && Array.isArray(list.presence) ? list.presence : []).filter((p) => p && p.away);
27
+ if (!away.length) return { owner_away: false };
28
+ const p = away.find((x) => x.who === 'owner') || away[0];
29
+ return { owner_away: true, owner_away_since: p.since, owner_presence: p.state, owner_away_notice: list.notice || presenceText(p) };
30
+ }
31
+
32
+ /** Read the org's presence; null on any failure (a presence read never fails the caller). */
33
+ export async function readPresence(client, orgId) {
34
+ try {
35
+ const ep = resolveEndpoint('presence.list');
36
+ return await client.get(ep.path, { service: ep.service, query: orgId ? { org_id: orgId } : undefined, retries: 0, timeout: 5000 });
37
+ } catch { return null; }
38
+ }
39
+
40
+ export function registerPresenceCommands(program, { client, auth }) {
41
+ const pr = program
42
+ .command('presence')
43
+ .description('Is a person at their terminal? With no name: everyone with presence on record. When someone is away, approvals queue; work on something else')
44
+ .argument('[who]', 'user id, email, email name, "owner" or "me"')
45
+ .action(async (who) => {
46
+ auth.requireAuth();
47
+ const orgId = resolveOrgId(auth, program);
48
+ const json = program.opts().output === 'json';
49
+ if (who) {
50
+ const ep = resolveEndpoint('presence.get', { who });
51
+ const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
52
+ if (json) { console.log(JSON.stringify(d, null, 2)); return; }
53
+ console.log(presenceText(d.presence));
54
+ return;
55
+ }
56
+ const ep = resolveEndpoint('presence.list');
57
+ const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId } });
58
+ if (json) { console.log(JSON.stringify(d, null, 2)); return; }
59
+ const list = Array.isArray(d.presence) ? d.presence : [];
60
+ if (!list.some((p) => p.away)) console.log('Everyone is present: nobody has marked themselves away.');
61
+ for (const p of list) console.log(presenceText(p));
62
+ });
63
+
64
+ pr
65
+ .command('set')
66
+ .description('Say where you are: present, away or sleeping (a human login only). Same as /afk, /sleep, /back in quox')
67
+ .argument('<state>', 'present | away | sleeping')
68
+ .option('--note <text>', 'a short note sessions will see ("school run")')
69
+ .option('--slots <list>', 'your own window slots, comma separated (hitl and triage-q always count)')
70
+ .option('--auto-away <minutes>', 'read as away after this many idle minutes; "off" to disable (default off)')
71
+ .action(async (state, options) => {
72
+ if (!STATES.includes(state)) throw new Error('state must be present, away or sleeping');
73
+ auth.requireAuth();
74
+ const orgId = resolveOrgId(auth, program);
75
+ const body = { state };
76
+ if (options.note !== undefined) body.note = options.note;
77
+ if (options.slots) body.slots = String(options.slots).split(',').map((s) => s.trim()).filter(Boolean);
78
+ if (options.autoAway !== undefined) {
79
+ const off = /^(off|0|no)$/i.test(String(options.autoAway));
80
+ const n = Number(options.autoAway);
81
+ if (!off && (!Number.isInteger(n) || n < 1 || n > 1440)) throw new Error('--auto-away is minutes (1 to 1440) or off');
82
+ body.auto_away_minutes = off ? 0 : n;
83
+ }
84
+ const ep = resolveEndpoint('presence.set');
85
+ const d = await client.put(ep.path, { service: ep.service, body, query: { org_id: orgId } });
86
+ if (program.opts().output === 'json') { console.log(JSON.stringify(d, null, 2)); return; }
87
+ console.log(presenceText(d.presence));
88
+ });
89
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * quox provider list | connect <kind> | test [<kind>] (quox#1281, CLI verb first)
3
+ *
4
+ * The SAME backend calls as the TUI's /connect: cli/lib/quoxon/connect.js flows (connectFlow, connectOllamaFlow,
5
+ * connectCompatFlow, connectionLines) over cli/lib/quoxon/api.js createApi (the auth vault create/update with
6
+ * confirm_private_origin, the server-side key test and Ollama / OpenAI-compatible probes, capabilities, consent).
7
+ * Nothing here talks to the backend on its own, so the TUI and the CLI cannot drift. The vault routes audit every
8
+ * write server side (integration.private_origin for a private address), org-scoped by the caller's session.
9
+ *
10
+ * Secrets: a key is never an argv flag or argument. It arrives on stdin (--key-from-stdin) or from a hidden prompt,
11
+ * is sent once to the server test and the vault, and is never printed (connectFlow scrubs it from any message).
12
+ */
13
+ import { formatDetail, printInfo } from '../core/output.js';
14
+ import { resolveOrgId } from './helpers.js';
15
+ import { resolveEndpoint } from '../api/endpoints.js';
16
+ import { createApi, isInteractiveHuman } from '../quoxon/api.js';
17
+ import { PROVIDERS, providerFor, looksLikeKey, connectionLines, connectFlow, connectOllamaFlow, connectCompatFlow } from '../quoxon/connect.js';
18
+ import { needsConsent, consentCovers, connectConsentNote, providerLabel } from '../quoxon/consent.js';
19
+ import { readStdin } from '../quoxon/print.js';
20
+
21
+ const KINDS = Object.keys(PROVIDERS);
22
+ const SAFE_ROW = ['id', 'integration_type', 'label', 'is_default', 'test_status', 'last_tested_at', 'created_at'];
23
+ const CONTAINER_NOTE = 'Quox runs in containers: 127.0.0.1 or localhost means the Quox container, not your machine. Use the host\'s LAN IP, e.g. http://192.0.2.10:11434';
24
+
25
+ function defaultIO() {
26
+ return {
27
+ isTTY: () => isInteractiveHuman(),
28
+ readStdin: () => readStdin(),
29
+ promptSecret: async (message) => {
30
+ const { default: inquirer } = await import('inquirer');
31
+ const a = await inquirer.prompt([{ type: 'password', name: 'v', message, mask: '*' }]);
32
+ return a.v;
33
+ },
34
+ confirm: async (message) => {
35
+ const { default: inquirer } = await import('inquirer');
36
+ const a = await inquirer.prompt([{ type: 'confirm', name: 'v', message, default: false }]);
37
+ return Boolean(a.v);
38
+ },
39
+ };
40
+ }
41
+
42
+ const say = (lines) => { for (const [t] of lines || []) printInfo(t); };
43
+
44
+ export function registerProviderCommands(program, ctx) {
45
+ const { client, auth, config } = ctx;
46
+ const io = ctx.providerIO || defaultIO();
47
+ const api = () => (ctx.makeApi ? ctx.makeApi() : createApi({ client, config, auth }));
48
+
49
+ const provider = program
50
+ .command('provider')
51
+ .description('Connect the AI providers your org uses (the same as /connect in the Quox terminal)');
52
+
53
+ // ── provider list ──────────────────────────────────────────────────────────
54
+ provider
55
+ .command('list')
56
+ .description('Show which AI providers this org can use, and how each is connected')
57
+ .action(async () => {
58
+ auth.requireAuth();
59
+ const a = api();
60
+ const [vault, caps] = await Promise.all([a.connections(), a.capabilities ? a.capabilities() : null]);
61
+ if (program.opts().output === 'json') {
62
+ const rows = vault?.ok ? vault.rows.map((r) => Object.fromEntries(SAFE_ROW.filter((k) => r[k] !== undefined).map((k) => [k, r[k]]))) : null;
63
+ formatDetail({ connections: rows, ...(vault?.ok ? {} : { error: vault?.error || 'vault not readable' }), capabilities: caps?.ok ? caps.capabilities : null }, { format: 'json' });
64
+ return;
65
+ }
66
+ say(connectionLines(vault, caps));
67
+ });
68
+
69
+ // ── provider connect ───────────────────────────────────────────────────────
70
+ const readKey = async (p, opts) => {
71
+ if (opts.keyFromStdin) return String(await io.readStdin() || '').replace(/\r?\n$/, '').trim();
72
+ if (io.isTTY()) return String(await io.promptSecret(`${p.name} key (hidden, never shown or stored on this machine):`) || '').trim();
73
+ throw new Error(`No key given. Pipe it in with --key-from-stdin (for example: quox provider connect ${p.id} --key-from-stdin < keyfile), or run this in a terminal to be asked. A key is never taken as an argument.`);
74
+ };
75
+ const consentAfter = async (type) => {
76
+ const a = api();
77
+ if (!needsConsent(type) || !a.consentStatus) return;
78
+ let rec = null; try { rec = await a.consentStatus(); } catch { rec = null; }
79
+ if (consentCovers(rec, type)) return;
80
+ // --yes never grants consent: it is this person's own OK to send their conversations to a third party
81
+ if (!io.isTTY() || !a.grantConsent) { printInfo(connectConsentNote(type)); return; }
82
+ const yes = await io.confirm(`Allow Quox to send your conversations to ${providerLabel(type)}? Your messages and tool results go to ${providerLabel(type)}, a third party, when it answers. Nothing is sent until you say yes.`);
83
+ if (!yes) { printInfo(`Left off: nothing goes to ${providerLabel(type)} until you say so. ${connectConsentNote(type)}`); return; }
84
+ try { await a.grantConsent(type); printInfo(`✔ Your OK is recorded: ${providerLabel(type)} can now answer. Withdraw it any time: quox consent list.`); }
85
+ catch (e) { printInfo(`Quox could not record your OK (${e?.message || e}). Try: quox consent grant ${type}`); }
86
+ };
87
+
88
+ provider
89
+ .command('connect')
90
+ .description('Connect an AI provider for this org: a keyed provider (openai, anthropic, groq...) takes its key on stdin or a hidden prompt; a self-hosted one (ollama, lmstudio, vllm, custom) takes a URL')
91
+ .argument('<kind>', `provider: ${KINDS.join(', ')}`)
92
+ .argument('[url]', 'server URL for ollama / lmstudio / vllm / custom (same as --url)')
93
+ .option('--url <url>', `server URL for a self-hosted provider. ${CONTAINER_NOTE}`)
94
+ .option('--model <model>', 'default model to use on that server')
95
+ .option('--key-from-stdin', 'read the API key from stdin (never pass a key as an argument)')
96
+ .option('--yes', 'confirm connecting to an address on your private network (the server still checks it and audits the decision)')
97
+ .addHelpText('after', `\n${CONTAINER_NOTE}\nKeys: --key-from-stdin, or run in a terminal to be asked with a hidden prompt.`)
98
+ .action(async (kind, urlArg, opts) => {
99
+ auth.requireAuth();
100
+ const p = providerFor(kind);
101
+ if (!p) throw new Error(`No provider called "${kind}". Use one of: ${KINDS.join(', ')}`);
102
+ if (urlArg && (looksLikeKey(urlArg) || (p.card && !p.url))) throw new Error('never pass a key as an argument (it would sit in your shell history and process list). Use --key-from-stdin, or run in a terminal to be asked.');
103
+ const a = api();
104
+ const url = opts.url || urlArg || '';
105
+ let r;
106
+ if (p.id === 'ollama') {
107
+ r = await connectOllamaFlow(a, [url, opts.model].filter(Boolean).join(' '));
108
+ say(r.lines);
109
+ if (r.privateAsk) {
110
+ const q = `Connect to ${r.privateAsk.host}? It's on your private network. Quox saves only ${r.privateAsk.origin}, after the server checks it answers as Ollama.`;
111
+ let yes = Boolean(opts.yes);
112
+ if (!yes && io.isTTY()) yes = await io.confirm(q);
113
+ if (!yes && !io.isTTY()) throw new Error(`${r.privateAsk.host} is on your private network. Run again with --yes to confirm this one address. Nothing was saved.`);
114
+ if (!yes) throw new Error(`Left blocked: ${r.privateAsk.host} was not connected. Nothing was saved.`);
115
+ r = await r.privateAsk.retry();
116
+ say(r.lines);
117
+ }
118
+ } else if (p.url) {
119
+ r = await connectCompatFlow(a, p.id, [url, opts.model].filter(Boolean).join(' '));
120
+ say(r.lines);
121
+ if (r.needsKey) { r = await connectFlow(a, 'custom', await readKey(PROVIDERS.custom, opts), r.extra); printInfo(r.text); if (r.disclosure) printInfo(r.disclosure); }
122
+ } else {
123
+ r = await connectFlow(a, p.id, await readKey(p, opts));
124
+ printInfo(r.text);
125
+ if (r.ok && r.disclosure) printInfo(r.disclosure);
126
+ }
127
+ if (r?.lanOffer) printInfo(' From the CLI an instance admin allows private targets with: quox server config lan-targets on then run this again.');
128
+ if (!r?.ok) throw new Error(r?.text || `${p.name} was not connected. Nothing was saved.`);
129
+ await consentAfter(p.type);
130
+ });
131
+
132
+ // ── provider test ──────────────────────────────────────────────────────────
133
+ provider
134
+ .command('test')
135
+ .description('Test the stored connection for a provider (or every default connection) through the vault test')
136
+ .argument('[kind]', 'provider to test (default: every connected provider)')
137
+ .action(async (kind) => {
138
+ auth.requireAuth();
139
+ const p = kind ? providerFor(kind) : null;
140
+ if (kind && !p) throw new Error(`No provider called "${kind}". Use one of: ${KINDS.join(', ')}`);
141
+ const vault = await api().connections();
142
+ if (!vault?.ok) throw new Error(`Could not read the vault: ${vault?.error || 'no answer'}`);
143
+ const types = new Set(p ? [p.type] : Object.values(PROVIDERS).map((x) => x.type));
144
+ const rows = vault.rows.filter((r) => types.has(r.integration_type) && (r.is_default === 1 || r.is_default === true));
145
+ if (!rows.length) throw new Error(p ? `No ${p.id} connection is stored for this org. Connect one: quox provider connect ${p.id}` : 'No provider connection is stored for this org. Connect one: quox provider connect <kind>');
146
+ const orgId = resolveOrgId(auth, program);
147
+ let failed = 0;
148
+ for (const row of rows) {
149
+ const name = (Object.values(PROVIDERS).find((x) => x.type === row.integration_type) || {}).name || row.integration_type;
150
+ const ep = resolveEndpoint('vault.test', { orgId, id: row.id });
151
+ let res;
152
+ try { res = await client.post(ep.path, { service: ep.service, retries: 0 }); }
153
+ catch (e) { res = { success: false, message: e?.message || 'failed' }; }
154
+ if (res?.success === false) { failed++; printInfo(`✖ ${name} ${row.label || row.id} failed: ${res?.message || 'no reason given'}`); }
155
+ else printInfo(`✔ ${name} ${row.label || row.id} ok${res?.message ? `: ${res.message}` : ''}`);
156
+ }
157
+ if (failed) process.exitCode = 1;
158
+ });
159
+ }
@@ -49,9 +49,12 @@ function annotate(err) {
49
49
  throw err;
50
50
  }
51
51
 
52
- async function call(client, id, params = {}, body) {
52
+ // quox#907: a browser login is slower than a read; well above the client's 30s default.
53
+ const LOGIN_TIMEOUT_MS = 120000;
54
+
55
+ async function call(client, id, params = {}, body, extra = {}) {
53
56
  const ep = resolveEndpoint(id, params);
54
- const opts = { service: ep.service };
57
+ const opts = { service: ep.service, ...extra };
55
58
  if (body) opts.body = body;
56
59
  switch (ep.method) {
57
60
  case 'POST': return client.post(ep.path, opts);
@@ -634,15 +637,28 @@ export function registerQlarityCommands(program, ctx) {
634
637
  (await call(client, 'qlarity.sessions.list')).sessions || [],
635
638
  normalizedOrigin,
636
639
  );
640
+ let fresh = false;
637
641
  if (!sessionId) {
638
642
  const s = await call(client, 'qlarity.session.create', {}, {});
639
643
  sessionId = s.id;
644
+ fresh = true;
640
645
  }
641
646
 
642
647
  const body = { origin: normalizedOrigin, credential_id: credentialId };
643
648
  if (options.loginPath) body.login_path = options.loginPath;
644
649
 
645
- const r = await call(client, 'qlarity.session.login', { sessionId }, body);
650
+ // quox#907: a real login (page load, form fill, cookie wait) can outlast the CLI's 30s default on a loaded
651
+ // box, and the server kept finishing it, so every timed-out retry left a headless browser behind. Give login
652
+ // its own budget, and close a session THIS call created when the login call fails (as openSession does, #680).
653
+ let r;
654
+ try {
655
+ r = await call(client, 'qlarity.session.login', { sessionId }, body, { timeout: LOGIN_TIMEOUT_MS });
656
+ } catch (loginErr) {
657
+ if (fresh) {
658
+ try { await call(client, 'qlarity.session.close', { sessionId }); } catch { /* the server reaper will get it */ }
659
+ }
660
+ throw loginErr;
661
+ }
646
662
 
647
663
  if (r.logged_in) {
648
664
  loggedIn[normalizedOrigin] = { sessionId, logged_in_at: new Date().toISOString() };
@@ -131,11 +131,19 @@ export function registerQuoxchatCommands(program, ctx) {
131
131
  .action(async () => {
132
132
  auth.requireAuth();
133
133
  const orgId = resolveOrgId(auth, program);
134
- const data = await client.get(resolveEndpoint('quoxchat.widgets.list', { orgId }).path, { service: resolveEndpoint('quoxchat.widgets.list', { orgId }).service });
134
+ // QC-BRUTALOOP-W15: the route pages at 20; listing only page 1 silently
135
+ // under-reported the org's widgets (20 of 34). Walk every page.
136
+ const ep = resolveEndpoint('quoxchat.widgets.list', { orgId });
137
+ const rows = [];
138
+ for (let page = 1; page <= 50; page++) {
139
+ const data = await client.get(page === 1 ? ep.path : `${ep.path}?page=${page}`, { service: ep.service });
140
+ rows.push(...(Array.isArray(data) ? data : (data.items || [])));
141
+ if (Array.isArray(data) || !data.pagination?.has_more) break;
142
+ }
135
143
  formatOutput({
136
144
  columns: ['id', 'name', 'assistant_name', 'status', 'operator_online', 'created_at'],
137
145
  headers: ['ID', 'Name', 'Assistant', 'Status', 'Operator online', 'Created'],
138
- rows: data.items || data,
146
+ rows,
139
147
  }, { format: program.opts().output, emptyMessage: 'No widgets found.' });
140
148
  });
141
149
 
@@ -209,7 +217,8 @@ export function registerQuoxchatCommands(program, ctx) {
209
217
  .description('Print a conversation transcript in chronological order')
210
218
  .argument('<widgetId>', 'widget ID')
211
219
  .argument('<conversationId>', 'conversation ID')
212
- .action(async (widgetId, conversationId) => {
220
+ .option('--full', 'do not truncate long messages in table output (JSON output is never truncated)')
221
+ .action(async (widgetId, conversationId, cmdOpts = {}) => {
213
222
  auth.requireAuth();
214
223
  const orgId = resolveOrgId(auth, program);
215
224
 
@@ -236,7 +245,7 @@ export function registerQuoxchatCommands(program, ctx) {
236
245
  from: m.role === 'user' ? visitorName : m.role === 'operator' ? (m.sender_name || 'Operator') : 'AI assistant',
237
246
  content: m.content || '',
238
247
  })),
239
- }, { format: program.opts().output, emptyMessage: 'No messages in this conversation yet.' });
248
+ }, { format: program.opts().output, full: cmdOpts.full === true, emptyMessage: 'No messages in this conversation yet.' });
240
249
  });
241
250
 
242
251
  quoxchat
@@ -100,6 +100,44 @@ export function registerQuoxseoCommands(program, ctx) {
100
100
  } catch (err) { annotate(err); }
101
101
  });
102
102
 
103
+ s.command('remove-site <site>')
104
+ .description('Remove a registered site and its warehouse data for this org (governed, witnessed). <site> is an origin or property id')
105
+ .option('-y, --yes', 'skip the confirmation prompt')
106
+ .action(async (site, opts) => {
107
+ try {
108
+ const list = await get(client, 'quoxseo.sites');
109
+ const props = list.properties || [];
110
+ const norm = (v) => String(v).trim().replace(/\/+$/, '').toLowerCase();
111
+ const target = props.find((p) => String(p.id) === String(site).trim() || norm(p.origin) === norm(site));
112
+ if (!target) {
113
+ throw new Error(`Site not found: ${site} is not registered for this org. List them with: quox seo sites`);
114
+ }
115
+ const warn = `This permanently removes ${target.origin} (#${target.id}) from this org and deletes its crawl, GSC and GA4 warehouse data. This cannot be undone.`;
116
+ if (!opts.yes && program.opts().yes !== true) {
117
+ if (!process.stdin.isTTY) {
118
+ throw new Error(`${warn} Confirmation needed; re-run with --yes in a non-interactive shell: quox seo remove-site ${target.origin} --yes`);
119
+ }
120
+ console.log(warn);
121
+ const { confirmDestructive } = await import('../core/confirm.js');
122
+ const ok = await confirmDestructive(program, {}, `Remove ${target.origin}?`);
123
+ if (!ok) { console.log('Aborted. Nothing was removed.'); return; }
124
+ }
125
+ const ep = resolveEndpoint('quoxseo.sites.remove', { id: target.id });
126
+ let data;
127
+ try {
128
+ data = await client.delete(ep.path, { service: ep.service });
129
+ } catch (err) {
130
+ const code = err?.status ?? err?.statusCode ?? err?.response?.status;
131
+ if (code === 404 || code === 405 || /\b(404|405)\b|not found|read.only/i.test(err?.message || '')) {
132
+ err.message = `${err.message}\nThis install's QuoxSEO service does not support site removal (no DELETE route), or the site no longer exists. Nothing was removed.`;
133
+ }
134
+ throw err;
135
+ }
136
+ if (jsonOut(data)) return;
137
+ console.log(`removed #${target.id} ${target.origin}`);
138
+ } catch (err) { annotate(err); }
139
+ });
140
+
103
141
  s.command('urls')
104
142
  .description('Warehouse rows for the latest crawl')
105
143
  .option('--filter <f>', 'orphans | noindex | errors | no-gsc | discovered')
@@ -103,7 +103,8 @@ export function registerReachCommands(program, ctx) {
103
103
  const orgId = resolveOrgId(auth, program);
104
104
  const days = parseInt(options.days, 10) || 30;
105
105
  const ep = resolveEndpoint('reach.agent.leastPrivilege', { orgId, agentId, days });
106
- const result = await client.get(ep.path, { service: ep.service });
106
+ // #923: the report scans the usage window and outlived the 30 s default under load
107
+ const result = await client.get(ep.path, { service: ep.service, timeout: 120000 });
107
108
 
108
109
  if (isJsonOutput(program)) {
109
110
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);