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
@@ -12,6 +12,7 @@
12
12
  import { resolveActing, resolveActingToken } from './actingAs.js';
13
13
  import { detectCallerSource } from './callerSource.js';
14
14
  import { QuoxApiError, QuoxNetworkError, QuoxAuthError, QuoxTransientAuthError } from './errors.js';
15
+ import { joinServiceUrl, isLocalServiceTarget } from './config.js';
15
16
 
16
17
  const RETRY_STATUSES = new Set([429, 502, 503]);
17
18
  const MAX_RETRIES = 3;
@@ -33,8 +34,9 @@ export function describeNetworkError(service, baseUrl, err) {
33
34
  const code = err?.cause?.code || err?.code;
34
35
  let msg = `Could not connect to ${service} service at ${baseUrl}: ${err.message}`;
35
36
  if (code && TLS_TRUST_CODES.has(code)) {
36
- msg += `\n TLS: certificate not trusted (${code}). For a self-signed dev edge, trust its CA:`
37
- + `\n export NODE_EXTRA_CA_CERTS=~/.config/quox/quox-dev-edge.pem`
37
+ msg += `\n TLS: certificate not trusted (${code}). A new QuoxCORE uses its own self-signed certificate; trust that file`
38
+ + `\n (on the server, by default ~/quox-install/quox/ssl/quox.crt; copy it here if the server is another machine):`
39
+ + `\n export NODE_EXTRA_CA_CERTS=<path to quox.crt>`
38
40
  + `\n Never disable TLS verification (NODE_TLS_REJECT_UNAUTHORIZED=0) instead.`;
39
41
  } else if (code === 'ERR_SSL_WRONG_VERSION_NUMBER' || code === 'EPROTO') {
40
42
  // quox#532 defect 1: the profile says https:// but the service answered
@@ -128,10 +130,13 @@ export function createClient(config, auth) {
128
130
  noAuth = false,
129
131
  noActAs = false,
130
132
  sensitive = false,
133
+ // HYPERION #23: an outside-caller request (trigger webhook-fire) also drops the caller labels
134
+ // (X-Quox-Session / X-Quox-Profile): the receiving route may store every header in an org-readable record.
135
+ anonymous = false,
131
136
  } = options;
132
137
 
133
138
  const baseUrl = config.getServiceUrl(service);
134
- const url = new URL(path, baseUrl);
139
+ const url = joinServiceUrl(baseUrl, path);
135
140
 
136
141
  if (query) {
137
142
  for (const [key, value] of Object.entries(query)) {
@@ -142,7 +147,7 @@ export function createClient(config, auth) {
142
147
  const fetchHeaders = {
143
148
  'X-Quox-Correlation': correlationId,
144
149
  'X-Quox-Client': 'cli',
145
- ...callerContextHeaders(config),
150
+ ...(anonymous ? {} : callerContextHeaders(config)),
146
151
  ...extraHeaders,
147
152
  };
148
153
 
@@ -177,7 +182,7 @@ export function createClient(config, auth) {
177
182
  let usingServiceKey = false;
178
183
  // quox#772: opt-in acting identity (--as / QUOX_AS). Replaces the bearer
179
184
  // with the agent's attach token; throws (no request sent) if not attached.
180
- const acting = !noAuth && !noActAs ? await resolveActing() : { name: null };
185
+ const acting = !noAuth && !noActAs ? await resolveActing({ explicitProfile: Boolean(config._explicitProfile) }) : { name: null };
181
186
  const actingAs = acting.name;
182
187
  if (actingAs) {
183
188
  const heartbeat = {
@@ -195,7 +200,10 @@ export function createClient(config, auth) {
195
200
  usingServiceKey = true;
196
201
  } else if (tokens?.access_token) {
197
202
  fetchHeaders['Authorization'] = `Bearer ${tokens.access_token}`;
198
- } else if (serviceKey) {
203
+ } else if (serviceKey && isLocalServiceTarget(url.href)) {
204
+ // QUOXON-Z2H lane R: the implicit fallback is THIS box's key; never
205
+ // offer it to another host (a remote profile with no login). A caller
206
+ // that owns a remote key says so with QUOX_FORCE_SERVICE_KEY.
199
207
  fetchHeaders['X-Service-Key'] = serviceKey;
200
208
  usingServiceKey = true;
201
209
  }
@@ -218,6 +226,10 @@ export function createClient(config, auth) {
218
226
  }
219
227
 
220
228
  let lastError;
229
+ let staleReloaded = false;
230
+ // REL-424: one refresh per request, on whichever attempt the 401 lands. A token that expires while the
231
+ // client sleeps between 503 retries gets its 401 on attempt 1+, and gating refresh on attempt 0 surfaced it.
232
+ let refreshedThisRequest = false;
221
233
 
222
234
  for (let attempt = 0; attempt <= retries; attempt++) {
223
235
  try {
@@ -255,10 +267,32 @@ export function createClient(config, auth) {
255
267
  // profile over what is really just "wrong token for this route".
256
268
  // In that case, skip the refresh and fall through to the normal
257
269
  // error handling below so the original 401/403 is surfaced as-is.
258
- const looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
270
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
271
+ // The SERVER saying "invalid or expired token" outranks the local clock. expires_at is
272
+ // stamped after the refresh response lands, so for a moment the server rejects a token
273
+ // the client still thinks is good. 2026-10-14 07:32:03: three windows hit that gap
274
+ // together, skipped the refresh and declared the profile signed out. Refresh is safe
275
+ // here: a dead refresh token is still judged by auth.refresh() itself.
276
+ if (res.status === 401 && !looksExpired && !noAuth && !actingAs && !usingServiceKey) {
277
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
278
+ }
279
+
280
+ // Stale sign-in: a sibling window or `quox login` may have written a newer token for this profile.
281
+ // Adopt it and retry this request ONCE before any refresh (which would spend our stale refresh token).
282
+ if (res.status === 401 && !noAuth && !actingAs && !usingServiceKey && !staleReloaded
283
+ && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
284
+ staleReloaded = true;
285
+ const fresh = auth.getTokens?.();
286
+ if (fresh?.access_token) {
287
+ fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
288
+ attempt--; // this retry is free: it does not spend the caller's retry budget
289
+ continue;
290
+ }
291
+ }
259
292
 
260
293
  const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
261
- if (res.status === 401 && !noAuth && !actingAs && attempt === 0 && !refreshCoolingDown && looksExpired) {
294
+ if (res.status === 401 && !noAuth && !actingAs && !refreshedThisRequest && !refreshCoolingDown && looksExpired) {
295
+ refreshedThisRequest = true;
262
296
  try {
263
297
  if (!refreshing) {
264
298
  const serviceUrl = config.getServiceUrl('auth');
@@ -288,16 +322,25 @@ export function createClient(config, auth) {
288
322
  if (refreshErr && refreshErr.transient) {
289
323
  // Auth is only temporarily unavailable (429 / restart): no
290
324
  // cooldown, and keep the type so a listener can back off.
291
- throw new QuoxTransientAuthError(`Re-authentication temporarily unavailable. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
325
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retrying. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
292
326
  }
293
327
  refreshFailedAt = Date.now();
294
328
  const why = refreshErr?.message ? ` ${refreshErr.message}` : '';
295
- throw new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${why}`);
329
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${why}`);
330
+ if (refreshErr?.code) dead.code = refreshErr.code;
331
+ if (refreshErr?.reason) dead.reason = refreshErr.reason;
332
+ throw dead;
296
333
  }
297
334
  }
298
335
 
299
336
  // Retry transient errors
337
+ // quox#882: the server can say a failure won't clear by itself (a downstream refusing
338
+ // connections): body retryable:false skips the backoff retries.
339
+ let noRetry = false;
300
340
  if (RETRY_STATUSES.has(res.status) && attempt < retries) {
341
+ try { noRetry = (await res.clone().json())?.retryable === false; } catch { /* not JSON */ }
342
+ }
343
+ if (RETRY_STATUSES.has(res.status) && attempt < retries && !noRetry) {
301
344
  const delay = RETRY_DELAYS[attempt] || 9000;
302
345
  if (isDebug) {
303
346
  console.error(` Retrying in ${delay}ms (attempt ${attempt + 1}/${retries})...`);
@@ -315,7 +358,8 @@ export function createClient(config, auth) {
315
358
  try {
316
359
  data = await res.json();
317
360
  } catch {
318
- data = { error: 'unknown', message: res.statusText };
361
+ // CLI-ERRORS: a proxy error page, not a Quox body; named so the renderer can say so
362
+ data = { error: 'non_json_response', message: res.statusText };
319
363
  }
320
364
  // A permission error while we silently fell back to the SERVICE
321
365
  // KEY is almost never a permissions problem: the service key
@@ -327,7 +371,10 @@ export function createClient(config, auth) {
327
371
  // theory about who could grant it. He had held instance-admin
328
372
  // since March. His session had simply expired and the CLI had
329
373
  // quietly downgraded the request to a key with no user attached.
330
- let message = data.message || `Request failed with status ${res.status}`;
374
+ // CLI-ERRORS: a body with only a prose `error` ("Agent not found") leads with that, never
375
+ // "Request failed with status N" (the real reason used to sit on the dim Status line).
376
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
377
+ let message = data.message || (proseError ? data.error : `Request failed with status ${res.status}`);
331
378
  // quox#617: surface the server's machine-readable `reason`. A vault
332
379
  // update returns 403 with `reason: 'mfa_required'` for an MFA-enabled
333
380
  // owner (a step-up gate) vs `reason: 'role_<r>_lacks_capability_<cap>'`
@@ -344,18 +391,38 @@ export function createClient(config, auth) {
344
391
  + `The service key has no user identity, so anything gated on WHO you are will refuse it regardless of your real permissions. `
345
392
  + `Run \`quox login\` (or \`quox -p <profile> login\`) and retry before treating this as a permissions problem.`;
346
393
  }
394
+ // #1012: the paid backends (QuoxPlan, QuoxSEO, QLarity, QuoxSignal) are not part of the public
395
+ // install, so a licensed customer got a bare 502 from the collector proxy. This client cannot
396
+ // tell "not installed" from "installed but down", so it says both, and exits 1 (not 8).
397
+ const pluginDown = (res.status === 502 || res.status === 503) && typeof data.error === 'string'
398
+ && /^(QuoxPlan|QuoxSEO|QLarity|QuoxSignal) service unavailable$/i.exec(data.error);
399
+ if (pluginDown) {
400
+ const err = new QuoxApiError(
401
+ res.status,
402
+ 'PLUGIN_SERVICE_UNAVAILABLE',
403
+ `${pluginDown[1]} isn't reachable on this server. It isn't part of the public install (it ships separately), `
404
+ + 'or it is installed but down: run `quox health`. Nothing was changed.',
405
+ data.detail || data.details,
406
+ data
407
+ );
408
+ err.exitCode = 1;
409
+ throw err;
410
+ }
347
411
  // LL-3d: carry the WHOLE body, not just message/detail. Endpoints
348
412
  // that answer with structured context (the licensing 409's
349
413
  // bound_instance_name / bound[] / max_servers, the 402's plugin)
350
414
  // had it flattened away here, so command handlers could only print
351
415
  // "Request failed with status 409".
352
- throw new QuoxApiError(
416
+ const apiErr = new QuoxApiError(
353
417
  res.status,
354
418
  data.error || 'api_error',
355
419
  message,
356
420
  data.detail || data.details,
357
421
  data
358
422
  );
423
+ if (proseError) apiErr.reasonFromError = true;
424
+ if (usingServiceKey && (res.status === 403 || res.status === 401)) apiErr.serviceKeyFallback = true;
425
+ throw apiErr;
359
426
  }
360
427
 
361
428
  // Handle empty responses (204 No Content)
@@ -431,7 +498,7 @@ export function createClient(config, auth) {
431
498
  } = options;
432
499
 
433
500
  const baseUrl = config.getServiceUrl(service);
434
- const url = new URL(path, baseUrl);
501
+ const url = joinServiceUrl(baseUrl, path);
435
502
 
436
503
  if (query) {
437
504
  for (const [key, value] of Object.entries(query)) {
@@ -449,7 +516,7 @@ export function createClient(config, auth) {
449
516
  // Same precedence as request() above, including the QUOX_FORCE_SERVICE_KEY
450
517
  // override for daemons that own their key and must not borrow the shared
451
518
  // config's stored session.
452
- const streamActingR = await resolveActing();
519
+ const streamActingR = await resolveActing({ explicitProfile: Boolean(config._explicitProfile) });
453
520
  const streamActing = streamActingR.name;
454
521
  const streamTokens = auth.getTokens();
455
522
  let streamServiceKey = null;
@@ -462,7 +529,7 @@ export function createClient(config, auth) {
462
529
  fetchHeaders['X-Service-Key'] = streamServiceKey;
463
530
  } else if (streamTokens?.access_token) {
464
531
  fetchHeaders['Authorization'] = `Bearer ${streamTokens.access_token}`;
465
- } else if (process.env.QUOX_SERVICE_KEY) {
532
+ } else if (process.env.QUOX_SERVICE_KEY && isLocalServiceTarget(url.href)) {
466
533
  streamServiceKey = process.env.QUOX_SERVICE_KEY;
467
534
  fetchHeaders['X-Service-Key'] = streamServiceKey;
468
535
  }
@@ -472,22 +539,61 @@ export function createClient(config, auth) {
472
539
  console.error(`-> ${streamBody !== undefined ? 'POST' : 'GET'}(stream) ${url.toString()}`);
473
540
  }
474
541
 
475
- let res;
476
- try {
477
- res = await fetch(url.toString(), {
478
- method: streamBody !== undefined ? 'POST' : 'GET',
479
- headers: fetchHeaders,
480
- ...(streamBody !== undefined ? { body: JSON.stringify(streamBody) } : {}),
481
- signal: externalSignal,
482
- // Same redirect protection as the non-streaming path above: never let
483
- // a 3xx resend X-Service-Key to a host we did not choose.
484
- ...(streamServiceKey ? { redirect: 'manual' } : {}),
485
- });
486
- } catch (err) {
487
- throw new QuoxNetworkError(
488
- describeNetworkError(service, baseUrl, err),
489
- err
490
- );
542
+ const openStream = async () => {
543
+ try {
544
+ return await fetch(url.toString(), {
545
+ method: streamBody !== undefined ? 'POST' : 'GET',
546
+ headers: fetchHeaders,
547
+ ...(streamBody !== undefined ? { body: JSON.stringify(streamBody) } : {}),
548
+ signal: externalSignal,
549
+ // Same redirect protection as the non-streaming path above: never let
550
+ // a 3xx resend X-Service-Key to a host we did not choose.
551
+ ...(streamServiceKey ? { redirect: 'manual' } : {}),
552
+ });
553
+ } catch (err) {
554
+ throw new QuoxNetworkError(
555
+ describeNetworkError(service, baseUrl, err),
556
+ err
557
+ );
558
+ }
559
+ };
560
+ let res = await openStream();
561
+
562
+ // 2026-10-16: request() refreshed an expired session on a 401, the stream (the chat path) did not,
563
+ // so a chat turn right after the access token expired read "Not signed in" on a signed-in profile.
564
+ // Same rule as request(): one refresh for a user token that looks expired (locally, or the server
565
+ // says invalid_token), shared with any refresh already in flight, then one retry.
566
+ const usesUserToken = !streamActing && !streamServiceKey && !!streamTokens?.access_token;
567
+ if (res.status === 401 && usesUserToken && typeof config.reloadAuth === 'function' && config.reloadAuth()) {
568
+ // Stale sign-in: a sibling window or `quox login` wrote a newer token. Adopt it, retry once.
569
+ const fresh = auth.getTokens?.();
570
+ if (fresh?.access_token) { fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`; res = await openStream(); }
571
+ }
572
+ if (res.status === 401 && usesUserToken) {
573
+ let looksExpired = typeof auth.isExpired === 'function' ? auth.isExpired() : true;
574
+ if (!looksExpired) {
575
+ try { looksExpired = (await res.clone().json())?.error === 'invalid_token'; } catch { /* non-JSON body */ }
576
+ }
577
+ const refreshCoolingDown = refreshFailedAt && (Date.now() - refreshFailedAt < REFRESH_RETRY_COOLDOWN_MS);
578
+ if (looksExpired && !refreshCoolingDown) {
579
+ try {
580
+ if (!refreshing) {
581
+ refreshing = auth.refresh(config.getServiceUrl('auth')).finally(() => { refreshing = null; });
582
+ }
583
+ await refreshing;
584
+ } catch (refreshErr) {
585
+ if (refreshErr && refreshErr.transient) {
586
+ throw new QuoxTransientAuthError(`Quox server is slow or unreachable, retry shortly. ${refreshErr.message}`, refreshErr.retryAfterMs ?? null);
587
+ }
588
+ refreshFailedAt = Date.now();
589
+ const dead = new QuoxAuthError(`Session expired. Run \`quox login\` to re-authenticate.${refreshErr?.message ? ` ${refreshErr.message}` : ''}`);
590
+ dead.status = 401;
591
+ throw dead;
592
+ }
593
+ const fresh = auth.getTokens();
594
+ if (fresh?.access_token) fetchHeaders['Authorization'] = `Bearer ${fresh.access_token}`;
595
+ res = await openStream();
596
+ }
491
597
  }
492
598
 
493
599
  if (!res.ok) {
@@ -495,15 +601,18 @@ export function createClient(config, auth) {
495
601
  try {
496
602
  data = await res.json();
497
603
  } catch {
498
- data = { error: 'unknown', message: res.statusText };
604
+ data = { error: 'non_json_response', message: res.statusText };
499
605
  }
500
- throw new QuoxApiError(
606
+ const proseError = !data.message && typeof data.error === 'string' && /\s/.test(data.error);
607
+ const streamErr = new QuoxApiError(
501
608
  res.status,
502
609
  data.error || 'api_error',
503
- data.message || `Stream request failed with status ${res.status}`,
610
+ data.message || (proseError ? data.error : `Stream request failed with status ${res.status}`),
504
611
  data.detail || data.details,
505
612
  data
506
613
  );
614
+ if (proseError) streamErr.reasonFromError = true;
615
+ throw streamErr;
507
616
  }
508
617
 
509
618
  if (!res.body) {
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The CLI's command groups, in registration order (moved verbatim from bin/quox.js so
3
+ * one list feeds both the binary and cli/scripts/export-leaf-tree.mjs, which exports the
4
+ * machine-readable leaf tree the collector's quox_cli tool validates against).
5
+ * Paths are relative to THIS file; registerCommandGroups resolves them against the
6
+ * baseUrl it is given (COMMAND_REGISTRARS_BASE).
7
+ */
8
+ export const COMMAND_REGISTRARS = [
9
+ ['../commands/auth.js', 'registerAuthCommands'],
10
+ ['../commands/service.js', 'registerServiceCommands'],
11
+ ['../commands/status.js', 'registerStatusCommand'],
12
+ ['../commands/doctor.js', 'registerDoctorCommand'],
13
+ ['../commands/server.js', 'registerServerCommands'],
14
+ ['../commands/update.js', 'registerUpdateCommand'],
15
+ ['../commands/open.js', 'registerOpenCommand'],
16
+ ['../commands/peek.js', 'registerPeekCommand'],
17
+ ['../commands/aside.js', 'registerAsideCommand'],
18
+ ['../commands/motion.js', 'registerMotionCommand'],
19
+ ['../commands/welcome.js', 'registerWelcomeCommand'],
20
+ ['../commands/config-cmd.js', 'registerConfigCommands'],
21
+ ['../commands/org.js', 'registerOrgCommands'],
22
+ ['../commands/invites.js', 'registerInvitesCommands'],
23
+ ['../commands/team.js', 'registerTeamCommands'],
24
+ ['../commands/department.js', 'registerDepartmentCommands'],
25
+ ['../commands/agent.js', 'registerAgentCommands'],
26
+ ['../commands/tool.js', 'registerToolCommands'],
27
+ ['../commands/workflow.js', 'registerWorkflowCommands'],
28
+ ['../commands/objective.js', 'registerObjectiveCommands'],
29
+ ['../commands/assistant.js', 'registerAssistantCommands'],
30
+ ['../commands/fleet.js', 'registerFleetCommands'],
31
+ ['../commands/chat.js', 'registerChatCommands'],
32
+ ['../commands/memory.js', 'registerMemoryCommands'],
33
+ ['../commands/actions.js', 'registerActionsCommands'],
34
+ ['../commands/brain.js', 'registerBrainCommands'],
35
+ ['../commands/entity.js', 'registerEntityCommands'],
36
+ ['../commands/backup.js', 'registerBackupCommands'],
37
+ ['../commands/inbox.js', 'registerInboxCommands'],
38
+ ['../commands/approvals.js', 'registerApprovalsCommands'],
39
+ ['../commands/vm.js', 'registerVmCommands'],
40
+ ['../commands/router.js', 'registerRouterCommands'],
41
+ ['../commands/mail.js', 'registerMailCommands'],
42
+ ['../commands/calendar.js', 'registerCalendarCommands'],
43
+ ['../commands/notes.js', 'registerNotesCommands'],
44
+ ['../commands/contacts.js', 'registerContactsCommands'],
45
+ ['../commands/licence.js', 'registerLicenceCommands'],
46
+ ['../commands/run.js', 'registerRunCommands'],
47
+ ['../commands/file.js', 'registerFileCommands'],
48
+ ['../commands/admin.js', 'registerAdminCommands'],
49
+ ['../commands/aee.js', 'registerAeeCommands'],
50
+ ['../commands/integration.js', 'registerIntegrationCommands'],
51
+ ['../commands/consent.js', 'registerConsentCommands'],
52
+ ['../commands/setup.js', 'registerSetupCommands'],
53
+ ['../commands/watch.js', 'registerWatchCommands'],
54
+ ['../commands/tag.js', 'registerTagCommands'],
55
+ ['../commands/scope.js', 'registerScopeCommands'],
56
+ ['../commands/notification.js', 'registerNotificationCommands'],
57
+ ['../commands/prompt.js', 'registerPromptCommands'],
58
+ ['../commands/plugin.js', 'registerPluginCommands'],
59
+ ['../commands/powers.js', 'registerPowersCommands'],
60
+ ['../commands/conversation.js', 'registerConversationCommands'],
61
+ ['../commands/vault.js', 'registerVaultCommands'],
62
+ ['../commands/volt.js', 'registerVoltCommands'],
63
+ ['../commands/governance.js', 'registerGovernanceCommands'],
64
+ ['../commands/skills.js', 'registerSkillsCommands'],
65
+ ['../commands/claude.js', 'registerClaudeCommands'],
66
+ ['../commands/monitoring.js', 'registerMonitoringCommands'],
67
+ ['../commands/trigger.js', 'registerTriggerCommands'],
68
+ ['../commands/webhook.js', 'registerWebhookCommands'],
69
+ ['../commands/sso.js', 'registerSsoCommands'],
70
+ ['../commands/role.js', 'registerRoleCommands'],
71
+ ['../commands/domain.js', 'registerDomainCommands'],
72
+ ['../commands/security.js', 'registerSecurityCommands'],
73
+ ['../commands/safety.js', 'registerSafetyCommands'],
74
+ ['../commands/scim.js', 'registerScimCommands'],
75
+ ['../commands/llm-policy.js', 'registerLlmPolicyCommands'],
76
+ ['../commands/hat.js', 'registerHatCommands'],
77
+ ['../commands/rows.js', 'registerRowsCommands'],
78
+ ['../commands/settings.js', 'registerSettingsCommands'],
79
+ ['../commands/llm.js', 'registerLlmCommands'],
80
+ ['../commands/capabilities.js', 'registerCapabilitiesCommands'],
81
+ ['../commands/provider.js', 'registerProviderCommands'],
82
+ ['../commands/tips.js', 'registerTipsCommands'],
83
+ ['../commands/quoxchat.js', 'registerQuoxchatCommands'],
84
+ ['../commands/budget.js', 'registerBudgetCommands'],
85
+ ['../commands/trace.js', 'registerTraceCommands'],
86
+ ['../commands/mon.js', 'registerMonCommands'],
87
+ ['../commands/quoxbox.js', 'registerQuoxboxCommands'],
88
+ ['../commands/plan.js', 'registerPlanCommands'],
89
+ ['../commands/engagement.js', 'registerEngagementCommands'],
90
+ ['../commands/done.js', 'registerDoneCommands'],
91
+ ['../commands/discord.js', 'registerDiscordCommands'],
92
+ ['../commands/qlarity.js', 'registerQlarityCommands'],
93
+ ['../commands/fetchAllowlist.js', 'registerFetchAllowlistCommands'],
94
+ ['../commands/reach.js', 'registerReachCommands'],
95
+ ['../commands/inventory.js', 'registerInventoryCommands'],
96
+ ['../commands/policy.js', 'registerPolicyCommands'],
97
+ ['../commands/quoxseo.js', 'registerQuoxseoCommands'],
98
+ ['../commands/voice.js', 'registerVoiceCommands'],
99
+ ['../commands/awareness.js', 'registerAwarenessCommands'],
100
+ ['../commands/mind.js', 'registerMindCommands'],
101
+ ['../commands/lucid.js', 'registerLucidCommands'],
102
+ ['../commands/hyperion.js', 'registerHyperionCommands'],
103
+ ['../commands/tg.js', 'registerTgCommands'],
104
+ ['../commands/dev.js', 'registerDevCommands'],
105
+ ['../commands/build.js', 'registerBuildCommands'],
106
+ ['../commands/slot.js', 'registerSlotCommands'],
107
+ ['../commands/gripe.js', 'registerGripeCommands'],
108
+ ['../commands/ask.js', 'registerAskCommands'],
109
+ ['../commands/presence.js', 'registerPresenceCommands'],
110
+ ['../commands/dashboard.js', 'registerDashboardCommands'],
111
+ ['../commands/nucleus.js', 'registerNucleusCommands'],
112
+ ['../commands/onboard.js', 'registerOnboardCommands'],
113
+ ['../commands/x.js', 'registerXCommands'],
114
+ ['../commands/linkedin.js', 'registerLinkedInCommands'],
115
+ ['../commands/facebook.js', 'registerFacebookCommands'],
116
+ ['../commands/instagram.js', 'registerInstagramCommands'],
117
+ ['../commands/tiktok.js', 'registerTikTokCommands'],
118
+ ['../commands/offload.js', 'registerOffloadCommands'],
119
+ ];
120
+
121
+ export const COMMAND_REGISTRARS_BASE = import.meta.url;