quoxcli 0.0.0-stage → 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 (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -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/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Config management for QuoxCLI
3
+ *
4
+ * Stores profiles and auth tokens in ~/.config/quox/config.json
5
+ * File permissions: 0600 (owner read/write only)
6
+ */
7
+
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, renameSync, openSync, closeSync, statSync, unlinkSync, copyFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir, networkInterfaces, hostname as osHostname } from 'node:os';
11
+
12
+ function getConfigDir() { return join(homedir(), '.config', 'quox'); }
13
+ function getConfigFile() { return join(getConfigDir(), 'config.json'); }
14
+
15
+ // quox#713: cross-process lock around read-merge-write of config.json.
16
+ const LOCK_STALE_MS = 10000;
17
+ const LOCK_WAIT_MS = 15000;
18
+ function sleepSync(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
19
+ function debugLog(msg) { if (process.env.QUOX_DEBUG) process.stderr.write(`[quox config] ${msg}\n`); }
20
+ function acquireLock(lockFile) {
21
+ const deadline = Date.now() + LOCK_WAIT_MS;
22
+ for (;;) {
23
+ try {
24
+ const fd = openSync(lockFile, 'wx', 0o600);
25
+ closeSync(fd);
26
+ return true;
27
+ } catch (e) {
28
+ if (e.code !== 'EEXIST') return false;
29
+ try {
30
+ if (Date.now() - statSync(lockFile).mtimeMs > LOCK_STALE_MS) {
31
+ debugLog('breaking stale config lock');
32
+ try { unlinkSync(lockFile); } catch { /* another breaker won */ }
33
+ continue;
34
+ }
35
+ } catch { continue; /* lock vanished, retry */ }
36
+ if (Date.now() > deadline) return false;
37
+ sleepSync(15);
38
+ }
39
+ }
40
+ }
41
+ function releaseLock(lockFile) { try { unlinkSync(lockFile); } catch { /* already gone */ } }
42
+ const J = (v) => JSON.stringify(v === undefined ? null : v);
43
+
44
+ const DEFAULT_CONFIG = {
45
+ version: 1,
46
+ activeProfile: 'local',
47
+ profiles: {
48
+ local: {
49
+ url: 'http://localhost:3000',
50
+ auth: null,
51
+ },
52
+ },
53
+ };
54
+
55
+ // Service port map matching docker-compose.yml
56
+ export const SERVICE_PORTS = {
57
+ auth: 3101,
58
+ files: 3102,
59
+ memory: 3103,
60
+ orchestrator: 3100,
61
+ tasks: 3104,
62
+ collector: 9848,
63
+ // WP2.2: quoxflow exposes 127.0.0.1:${QUOXFLOW_PORT:-3200}:3200 in
64
+ // docker-compose.yml. This entry was previously absent, which silently
65
+ // broke getServiceUrl('quoxflow') — step 3 below only rewrites the port
66
+ // when a SERVICE_PORTS entry exists, so without it the call fell through
67
+ // to the unmodified base URL (wrong port). doctor.js has referenced
68
+ // `service: 'quoxflow', port: 3200` since CLI-3b without this entry ever
69
+ // backing it — that health check was likely silently probing the wrong
70
+ // port. See cli/lib/api/endpoints/triggers.js header for the full
71
+ // quoxflow-access decision this fixes.
72
+ quoxflow: 3200,
73
+ quoxbrain: 3106,
74
+ };
75
+
76
+ // Hosts where a SERVICE_PORTS rewrite necessarily lands on a plain node HTTP
77
+ // server rather than on nginx. URL.hostname keeps the brackets for IPv6, so
78
+ // '[::1]' is the form to match. See getServiceUrl step 3 for the rule.
79
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
80
+
81
+ // #572/#623: RFC1918 private-LAN hosts (+ *.local). On a single-box Quox stack
82
+ // the compose services bind 127.0.0.1 and ONLY the edge (:3000) terminates TLS
83
+ // and answers on the LAN address — so a service-port derived from a private-LAN
84
+ // base (e.g. https://quox.example.com:3000) must point at the reachable loopback
85
+ // bind, not https://<lan-ip>:PORT (which returns 000/refused, made `quox doctor`
86
+ // cry CRITICAL and broke run/objective/memory/volt). PUBLIC/remote hosts are
87
+ // deliberately NOT matched: they may run a real per-port TLS-terminating
88
+ // deployment, and an explicit per-service override always wins regardless.
89
+ function isPrivateHost(hostname) {
90
+ if (/\.local$/i.test(hostname)) return true;
91
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
92
+ if (!m) return false;
93
+ const a = Number(m[1]); const b = Number(m[2]);
94
+ return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168);
95
+ }
96
+
97
+ // QUOXON-Z2H lane R (2026-10-14): where the edge (nginx on the profile's base
98
+ // URL) serves each compose service. Mirrors nginx.conf: /api/auth, /api/setup,
99
+ // /api/orgs ... go to auth at the SAME path, so auth's prefix is empty.
100
+ export const EDGE_PATHS = {
101
+ auth: '',
102
+ collector: '/collector',
103
+ memory: '/api/memory',
104
+ tasks: '/tasks',
105
+ orchestrator: '/orchestrator',
106
+ quoxflow: '/quoxflow',
107
+ quoxbrain: '/api/quoxbrain',
108
+ files: '/files-api',
109
+ };
110
+
111
+ // The local edge's default port. A loopback base on this port is the stack on
112
+ // THIS box, whose services are reachable on their plain-http loopback binds.
113
+ // Any other loopback port is a tunnel (ssh -L 13105:vm:3000) to another box.
114
+ const LOCAL_EDGE_PORTS = new Set(['3000']);
115
+
116
+ function localIdentity(opts = {}) {
117
+ let localAddrs = opts.localAddrs;
118
+ if (!localAddrs) {
119
+ localAddrs = [];
120
+ try {
121
+ for (const list of Object.values(networkInterfaces())) {
122
+ for (const a of list || []) localAddrs.push(a.address);
123
+ }
124
+ } catch { /* no interfaces readable: treat nothing as local */ }
125
+ }
126
+ let host = opts.hostname;
127
+ if (host === undefined) { try { host = osHostname(); } catch { host = ''; } }
128
+ return { localAddrs: new Set(localAddrs), hostname: String(host || '').toLowerCase() };
129
+ }
130
+
131
+ function isThisMachine(hostname, id) {
132
+ const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
133
+ if (LOOPBACK_HOSTS.has(h) || LOOPBACK_HOSTS.has(`[${h}]`)) return true;
134
+ if (id.localAddrs.has(h)) return true;
135
+ return !!id.hostname && (h === id.hostname || h === `${id.hostname}.local`);
136
+ }
137
+
138
+ /**
139
+ * True when a service URL lands on THIS machine (loopback bind, or a base whose
140
+ * host is one of this box's own addresses) rather than another host or a tunnel
141
+ * to one. Used to keep this box's ambient QUOX_SERVICE_KEY from being offered
142
+ * to a remote instance.
143
+ */
144
+ export function isLocalServiceTarget(serviceUrl, opts = {}) {
145
+ try {
146
+ const u = new URL(serviceUrl);
147
+ const id = localIdentity(opts);
148
+ if (LOOPBACK_HOSTS.has(u.hostname)) {
149
+ // Loopback is this box when it is the local edge or a compose service
150
+ // bind; any other loopback port is a tunnel to another box.
151
+ const port = u.port || (u.protocol === 'https:' ? '443' : '80');
152
+ return LOCAL_EDGE_PORTS.has(port) || Object.values(SERVICE_PORTS).map(String).includes(port);
153
+ }
154
+ return isThisMachine(u.hostname, id);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /** Join an absolute or relative path onto a service URL WITHOUT dropping its path prefix (new URL('/x', base) would). */
161
+ export function joinServiceUrl(serviceUrl, path) {
162
+ const base = new URL(serviceUrl);
163
+ const prefix = base.pathname.replace(/\/+$/, '');
164
+ const rel = new URL(String(path).replace(/^\/*/, '/'), 'http://x');
165
+ base.pathname = `${prefix}${rel.pathname}`;
166
+ base.search = rel.search;
167
+ base.hash = '';
168
+ return base;
169
+ }
170
+
171
+ // Derivation of a compose service's URL from a profile base URL (normally the
172
+ // ACTIVE profile's own `url`, never a global default).
173
+ //
174
+ // QUOXON-Z2H lane R: the base URL is the single source of truth. Port guessing
175
+ // is ONLY for the stack on this very machine:
176
+ // - loopback base on the local edge port (:3000): plain-http loopback port
177
+ // swap (BRAIN2-AUDIT: compose services are plain HTTP);
178
+ // - private-LAN base whose host is one of THIS box's addresses: the
179
+ // loopback bind (#572/#623: services bind 127.0.0.1, only :3000 is on LAN).
180
+ // Everything else (another LAN host, a tunnel on a loopback port, a public
181
+ // host) goes through that base's edge at the service's nginx path. The old
182
+ // rule rewrote ANY RFC1918 base to 127.0.0.1:<port>, so `quox -p vm9110 login`
183
+ // against https://192.0.2.68:3000 was sent to this box's own auth service
184
+ // and got 401 invalid_credentials, and a tunnel profile hit local ports.
185
+ //
186
+ // DEVSUITE-GRIPE-P7 / Bug A: computed from the profile url, not getBaseUrl(),
187
+ // so a stale QUOX_URL cannot defeat -p.
188
+ export function deriveServiceUrl(baseUrl, port, service, opts = {}) {
189
+ try {
190
+ const url = new URL(baseUrl);
191
+ const id = localIdentity(opts);
192
+ const basePort = url.port || (url.protocol === 'https:' ? '443' : '80');
193
+ if (LOOPBACK_HOSTS.has(url.hostname) && LOCAL_EDGE_PORTS.has(basePort)) {
194
+ url.port = String(port);
195
+ url.protocol = 'http:';
196
+ return url.origin;
197
+ }
198
+ if (!LOOPBACK_HOSTS.has(url.hostname) && isPrivateHost(url.hostname) && isThisMachine(url.hostname, id)) {
199
+ url.port = String(port);
200
+ url.protocol = 'http:';
201
+ url.hostname = '127.0.0.1';
202
+ return url.origin;
203
+ }
204
+ if (service && Object.prototype.hasOwnProperty.call(EDGE_PATHS, service)) {
205
+ return `${url.origin}${url.pathname.replace(/\/+$/, '')}${EDGE_PATHS[service]}`;
206
+ }
207
+ // A port-mapped service with no edge route: the old port swap is the only guess left.
208
+ url.port = String(port);
209
+ return url.origin;
210
+ } catch {
211
+ return baseUrl;
212
+ }
213
+ }
214
+
215
+ // -p/--profile must be resolved BEFORE commander parses argv: bin/quox.js
216
+ // builds the shared ctx (loadConfig) at module load, when program.opts() is
217
+ // still empty. Reading the flag there always yielded undefined, so -p
218
+ // silently fell back to activeProfile — a login aimed at a dedicated profile
219
+ // clobbered another profile's session on 2026-08-15. This pre-scan is the
220
+ // only correct source for the flag at ctx-build time.
221
+ // Commands whose remaining argv belongs to another program (`quox claude -p
222
+ // "prompt"` is claude's --print, not quox's --profile).
223
+ const WRAPPED_PROGRAM_COMMANDS = new Set(['claude', 'codex', 'gemini', 'wrap']);
224
+
225
+ export function resolveProfileFlagFromArgv(argv) {
226
+ for (let i = 0; i < argv.length; i++) {
227
+ const a = argv[i];
228
+ if (WRAPPED_PROGRAM_COMMANDS.has(a)) return undefined;
229
+ if (a === '-p' || a === '--profile') return argv[i + 1];
230
+ if (typeof a === 'string' && a.startsWith('--profile=')) return a.slice('--profile='.length);
231
+ }
232
+ return undefined;
233
+ }
234
+
235
+ // fleet #479: -p/--profile and QUOX_PROFILE are documented as a one-shot
236
+ // override, but loadConfig used to write the override straight onto
237
+ // config.activeProfile. Any later save() (e.g. a login) then persisted it,
238
+ // silently changing the user's default profile for every command after.
239
+ // An override is a lens, not a switch: it must never survive to disk unless
240
+ // the caller explicitly asks for that with setActiveProfile(). The fix
241
+ // keeps the persisted default in _data.activeProfile untouched and tracks
242
+ // the "profile this invocation is using" separately on the wrapper as
243
+ // _effectiveProfile. Every reader that means "current" reads that; only
244
+ // setActiveProfile() is allowed to move the persisted default.
245
+ // #736 (2026-10-09): a config file that cannot be parsed must never be
246
+ // replaced by defaults in silence. The disk filled, the file became
247
+ // unreadable for a moment, and the next save wrote a near-empty config over
248
+ // every profile. Recover from the last good copy, or keep the bad file aside.
249
+ function moveConfigAside(reason) {
250
+ try {
251
+ const aside = `${getConfigFile()}.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
252
+ renameSync(getConfigFile(), aside);
253
+ process.stderr.write(`Warning: ${getConfigFile()} was unreadable (${reason}); kept it as ${aside}.\n`);
254
+ } catch { /* nothing to move */ }
255
+ }
256
+ function recoverConfig(reason) {
257
+ const bak = `${getConfigFile()}.bak`;
258
+ try {
259
+ const parsed = JSON.parse(readFileSync(bak, 'utf-8'));
260
+ if (parsed && typeof parsed === 'object' && parsed.profiles) {
261
+ moveConfigAside(reason);
262
+ process.stderr.write(`Recovered quox config from ${bak}.\n`);
263
+ return parsed;
264
+ }
265
+ } catch { /* no usable backup */ }
266
+ moveConfigAside(reason);
267
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
268
+ }
269
+
270
+ export function loadConfig(profileOverride) {
271
+ let config;
272
+
273
+ if (existsSync(getConfigFile())) {
274
+ try {
275
+ config = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
276
+ if (!config || typeof config !== 'object') throw new Error('not an object');
277
+ } catch (err) {
278
+ config = recoverConfig(err.message);
279
+ }
280
+ } else {
281
+ config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
282
+ }
283
+
284
+ // Ensure structure
285
+ if (!config.profiles) config.profiles = {};
286
+ if (!config.activeProfile) config.activeProfile = 'local';
287
+
288
+ // Profile override from CLI flag or env. This is session-scoped state
289
+ // only: it must never be assigned onto config.activeProfile (the
290
+ // persisted default), see comment block above.
291
+ const effectiveProfile = profileOverride || process.env.QUOX_PROFILE || config.activeProfile;
292
+ // A NAMED override that doesn't exist yet is only legitimate for commands
293
+ // that create it (login, config profile add) — warn instead of silently
294
+ // proceeding on an empty profile, so a typo'd -p never masquerades as a
295
+ // logged-out state.
296
+ if (profileOverride && config.profiles && !config.profiles[profileOverride]) {
297
+ process.stderr.write(`Note: profile "${profileOverride}" does not exist yet; it will be created on login.\n`);
298
+ // Vivify in memory so login's setAuth() has a slot to write (persisted
299
+ // only when a mutation saves); read-only commands just see a logged-out
300
+ // profile, which is the accurate answer. A later save() (e.g. login)
301
+ // persists this profile entry, but never the activeProfile default.
302
+ config.profiles[profileOverride] = {
303
+ url: (config.profiles[config.activeProfile] || {}).url || DEFAULT_CONFIG.profiles.local.url,
304
+ auth: null,
305
+ };
306
+ }
307
+
308
+ return {
309
+ _data: config,
310
+ // The profile this invocation is using (persisted default, or the -p /
311
+ // QUOX_PROFILE override for this process only). Never written to _data.
312
+ _effectiveProfile: effectiveProfile,
313
+ // quox#805: a profile passed explicitly to loadConfig(profile) means "act as
314
+ // that profile's user", same as -p on the CLI (no implicit attach default).
315
+ _explicitProfile: Boolean(profileOverride),
316
+
317
+ getActiveProfile() {
318
+ const name = this._effectiveProfile;
319
+ return this._data.profiles[name] || null;
320
+ },
321
+
322
+ getActiveProfileName() {
323
+ return this._effectiveProfile;
324
+ },
325
+
326
+ // The ONLY deliberate persistent switch: moves the on-disk default AND
327
+ // the effective name, then saves. Everything else in this file is a
328
+ // session-scoped lens onto whichever name is current.
329
+ setActiveProfile(name) {
330
+ if (!this._data.profiles[name]) {
331
+ throw new Error(`Profile "${name}" does not exist.`);
332
+ }
333
+ this._data.activeProfile = name;
334
+ this._effectiveProfile = name;
335
+ this.save();
336
+ },
337
+
338
+ addProfile(name, opts = {}) {
339
+ if (this._data.profiles[name]) {
340
+ throw new Error(`Profile "${name}" already exists.`);
341
+ }
342
+ this._data.profiles[name] = {
343
+ url: opts.url || 'http://localhost:3000',
344
+ auth: null,
345
+ };
346
+ this.save();
347
+ },
348
+
349
+ removeProfile(name) {
350
+ if (name === this._effectiveProfile) {
351
+ throw new Error('Cannot remove the active profile. Switch first.');
352
+ }
353
+ delete this._data.profiles[name];
354
+ // quox#527: save() merges profiles it finds on disk but does not know
355
+ // about (so a parallel session's new profile survives our write). An
356
+ // EXPLICIT removal must not be resurrected by that merge.
357
+ this._removedProfiles.add(name);
358
+ this.save();
359
+ },
360
+
361
+ listProfiles() {
362
+ return Object.entries(this._data.profiles).map(([name, profile]) => ({
363
+ name,
364
+ url: profile.url,
365
+ active: name === this._effectiveProfile,
366
+ loggedIn: !!profile.auth?.access_token,
367
+ email: profile.auth?.email || null,
368
+ orgId: profile.auth?.org_id || null,
369
+ }));
370
+ },
371
+
372
+ // Resident profiles (auth.kind === 'resident') are the durable,
373
+ // passwordless credential for unattended automation: no refresh token and
374
+ // no server-side session row to revoke, they re-authenticate via a signed
375
+ // Ed25519 challenge on every 401 (see residentAuth.js). When a password
376
+ // session dead-ends (revoked/expired) the CLI used to loop agents through
377
+ // `quox login` — a human action an unattended agent cannot take — so this
378
+ // lets the dead-end paths point at the resident profile instead.
379
+ // Added devtooling-fix 2026-09-10 after the owner was re-logging-in for an
380
+ // agent repeatedly while a working resident profile sat unused.
381
+ listResidentProfiles() {
382
+ return Object.entries(this._data.profiles)
383
+ .filter(([, p]) => p?.auth?.kind === 'resident')
384
+ .map(([name, p]) => ({
385
+ name,
386
+ orgId: p.auth?.org_id || null,
387
+ email: p.auth?.email || null,
388
+ }));
389
+ },
390
+
391
+ getAuth() {
392
+ // Env var override
393
+ if (process.env.QUOX_TOKEN) {
394
+ return { access_token: process.env.QUOX_TOKEN };
395
+ }
396
+ return this.getActiveProfile()?.auth || null;
397
+ },
398
+
399
+ // quox#455 gap 2: a resident bootstrap carries per-service reach-back
400
+ // URLs discovered at enable time (the instance edge, the collector's
401
+ // real scheme+port). Persist them onto the ACTIVE profile so every
402
+ // later command resolves services from what the host actually reaches,
403
+ // instead of deriving them by port/scheme guesswork.
404
+ setProfileConnection({ url, services } = {}) {
405
+ const profile = this.getActiveProfile();
406
+ if (!profile) throw new Error('No active profile.');
407
+ if (url) profile.url = url;
408
+ if (services && typeof services === 'object') {
409
+ profile.services = { ...(profile.services || {}), ...services };
410
+ }
411
+ this.save();
412
+ },
413
+
414
+ // Stale-sign-in fix: another window or `quox login` may have written fresher credentials for this
415
+ // profile since this process loaded. Re-read ONLY the active profile's auth from disk and adopt it in
416
+ // place (callers hold profile refs). Returns true when the auth changed. Never logs or returns a token.
417
+ reloadAuth() {
418
+ try {
419
+ const name = this._effectiveProfile;
420
+ const disk = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
421
+ const dAuth = disk?.profiles?.[name]?.auth ?? null;
422
+ const prof = this._data.profiles?.[name];
423
+ if (!prof || dAuth == null) return false;
424
+ if (J(prof.auth) === J(dAuth)) return false;
425
+ prof.auth = dAuth;
426
+ // The loaded snapshot now agrees with disk for auth, so save() does not treat it as our change.
427
+ try {
428
+ const snap = JSON.parse(this._loadedProfiles[name] || '{}');
429
+ snap.auth = dAuth;
430
+ this._loadedProfiles[name] = J(snap);
431
+ } catch { /* snapshot is advisory */ }
432
+ this._clearedAuth.delete(name);
433
+ this._refreshedAuth.delete(name);
434
+ return true;
435
+ } catch { return false; }
436
+ },
437
+
438
+ /** mtime (ms) of the credentials file, or 0. A cheap stat for "did anything sign in elsewhere". */
439
+ credentialsMtime() {
440
+ try { return statSync(getConfigFile()).mtimeMs; } catch { return 0; }
441
+ },
442
+
443
+ /** Cross-process refresh lock file, next to the credentials. */
444
+ refreshLockFile() { return `${getConfigFile()}.refresh.lock`; },
445
+
446
+ setAuth(authData, opts = {}) {
447
+ const profile = this.getActiveProfile();
448
+ if (!profile) throw new Error('No active profile.');
449
+ profile.auth = authData;
450
+ if (opts.refresh) this._refreshedAuth.add(this._effectiveProfile);
451
+ else this._refreshedAuth.delete(this._effectiveProfile);
452
+ this.save();
453
+ },
454
+
455
+ // opts.refresh: the clear is a reaction to a refresh rejection from this
456
+ // process's (possibly stale) session; it must not wipe a newer login.
457
+ clearAuth(opts = {}) {
458
+ const profile = this.getActiveProfile();
459
+ if (profile) {
460
+ profile.auth = null;
461
+ this._clearedAuth.add(this._effectiveProfile);
462
+ if (opts.refresh) this._refreshedAuth.add(this._effectiveProfile);
463
+ else this._refreshedAuth.delete(this._effectiveProfile);
464
+ }
465
+ this.save();
466
+ },
467
+
468
+ getBaseUrl() {
469
+ // Env var override
470
+ if (process.env.QUOX_URL) return process.env.QUOX_URL;
471
+ return this.getActiveProfile()?.url || 'http://localhost:3000';
472
+ },
473
+
474
+ getServiceUrl(service) {
475
+ // 1. Per-service env var override (e.g. QUOX_AUTH_URL, QUOX_BASTION_URL)
476
+ const envKey = `QUOX_${service.toUpperCase()}_URL`;
477
+ if (process.env[envKey]) return process.env[envKey];
478
+
479
+ // 1.5. Per-profile service overrides (quox#455: stamped by resident
480
+ // login from the enable-time bootstrap; what the host truly reaches)
481
+ const profileServices = this.getActiveProfile()?.services;
482
+ if (profileServices && profileServices[service]) return profileServices[service];
483
+
484
+ // 2. Shared services config (for external services: bastion, quoxagent, etc.)
485
+ const services = this._data.services;
486
+ if (services && services[service]) return services[service];
487
+
488
+ // 3. Known service port map (for docker-compose services).
489
+ //
490
+ // DEVSUITE-GRIPE-P7 / Bug A: derive from the ACTIVE PROFILE's own url,
491
+ // never getBaseUrl() — getBaseUrl() honours the global QUOX_URL env
492
+ // override ahead of the profile, which let a stale QUOX_URL silently
493
+ // defeat an explicit -p/--profile selection for every service-port
494
+ // derivation (auth, memory, etc.) while the profile still correctly
495
+ // held its own token/services. -p must be authoritative here the same
496
+ // way it already is for the overrides above. A default-config fallback
497
+ // (no active profile at all) still falls back to the documented
498
+ // default base, matching getBaseUrl()'s own no-profile fallback.
499
+ const profileUrl = this.getActiveProfile()?.url
500
+ || (process.env.QUOX_URL) // no active profile: nothing more specific to prefer
501
+ || 'http://localhost:3000';
502
+ const port = SERVICE_PORTS[service];
503
+ if (!port) return this.getBaseUrl();
504
+
505
+ // Replace the port in the base URL.
506
+ //
507
+ // BRAIN2-AUDIT 2026-09-12: the port rewrite used to keep the base URL's
508
+ // scheme, so a profile pointed at https://localhost:3000 (nginx, the
509
+ // ONLY TLS terminator in docker-compose.yml) derived
510
+ // https://localhost:3106 for quoxbrain — every docker-compose service
511
+ // port is a plain node HTTP server, so the TLS handshake never
512
+ // completed and the call died as "fetch failed". `quox brain status`
513
+ // had plausibly never worked against this deployment shape (the
514
+ // product's own usage ledger recorded zero CLI brain calls).
515
+ // On a LOOPBACK host, a port-map rewrite therefore forces http:. This
516
+ // is deliberately narrow: it applies only to this derivation step, so
517
+ // env-var overrides (step 1), per-profile service overrides (step 1.5)
518
+ // and the shared services config (step 2) still say exactly what they
519
+ // say, and a remote/non-loopback host keeps its scheme (a real
520
+ // deployment may terminate TLS per port).
521
+ return deriveServiceUrl(profileUrl, port, service);
522
+ },
523
+
524
+ get(key) {
525
+ // quox#570: url is a PROFILE field. Every reader (getBaseUrl, service
526
+ // derivation) prefers the active profile's url, so get must read the
527
+ // same place set writes or a set/get round-trip lies.
528
+ if (key === 'url') {
529
+ const profile = this.getActiveProfile();
530
+ if (profile) return profile.url;
531
+ }
532
+ return this._data[key];
533
+ },
534
+
535
+ set(key, value) {
536
+ const PROTECTED = new Set(['version', 'profiles', 'activeProfile']);
537
+ if (PROTECTED.has(key)) {
538
+ throw new Error(`Cannot set protected key "${key}". Use profile commands instead.`);
539
+ }
540
+ // quox#570: `config set url` used to write a top-level `url` key that
541
+ // no reader consults, so repointing a profile silently kept the old
542
+ // host. Route it to the effective profile (which honours -p) when one
543
+ // exists; top-level stays the fallback for a profile-less config.
544
+ if (key === 'url') {
545
+ const profile = this.getActiveProfile();
546
+ if (profile) {
547
+ profile.url = value;
548
+ this.save();
549
+ return;
550
+ }
551
+ }
552
+ this._data[key] = value;
553
+ this.save();
554
+ },
555
+
556
+ // quox#527: parallel sessions share this file. Two protections:
557
+ // (1) merge-on-save: profiles present on disk that THIS process has
558
+ // never seen are kept (a sibling session's fresh login/profile is
559
+ // not wiped by our unrelated write); explicit removals recorded in
560
+ // _removedProfiles win over the merge so deletion stays deletion.
561
+ // (2) atomic write: temp file + rename, so a concurrent reader never
562
+ // sees a torn/partial JSON file.
563
+ // The narrower last-writer-wins on a SHARED profile's fields remains —
564
+ // that needs a lock, not a merge — but "my profile vanished mid-session"
565
+ // (the filed symptom) is closed by (1).
566
+ _removedProfiles: new Set(),
567
+ // (3) 2026-10-05: a profile this process LOADED but never changed is not
568
+ // ours to write. Background processes refreshing `resident` were
569
+ // writing their stale copy of `moo` over a fresh `quox -p moo login`
570
+ // seconds later. Snapshot each profile at load; an unchanged one
571
+ // takes the on-disk version on save.
572
+ _loadedProfiles: Object.fromEntries(Object.entries(config.profiles || {}).map(([n, v]) => [n, JSON.stringify(v)])),
573
+
574
+ // quox#713: save() is PROFILE-SCOPED and FIELD-SCOPED. Under config.json.lock
575
+ // it re-reads the file and applies only the fields this process changed
576
+ // relative to what it loaded (snapshot diff). Rules:
577
+ // - an unchanged profile/field is never written;
578
+ // - auth becomes null only via an explicit clearAuth() on that profile;
579
+ // - a REFRESH (setAuth(a, {refresh:true})) never overwrites an on-disk
580
+ // auth that differs from the one this process loaded (a newer login or
581
+ // sibling refresh happened meanwhile): disk wins.
582
+ _clearedAuth: new Set(),
583
+ _refreshedAuth: new Set(),
584
+ _loadedTop: Object.fromEntries(Object.entries(config).filter(([k]) => k !== 'profiles').map(([k, v]) => [k, J(v)])),
585
+
586
+ save() {
587
+ if (!existsSync(getConfigDir())) {
588
+ mkdirSync(getConfigDir(), { recursive: true });
589
+ }
590
+ const lockFile = `${getConfigFile()}.lock`;
591
+ const locked = acquireLock(lockFile);
592
+ if (!locked) debugLog('could not acquire config lock, writing unlocked');
593
+ try {
594
+ let disk = null;
595
+ try {
596
+ disk = JSON.parse(readFileSync(getConfigFile(), 'utf-8'));
597
+ if (!disk || typeof disk !== 'object') disk = null;
598
+ } catch { /* no file yet, or unreadable JSON */ }
599
+ // #736: an existing file we cannot parse is kept aside, never overwritten.
600
+ if (!disk && existsSync(getConfigFile())) moveConfigAside('unreadable at save');
601
+ const out = disk || JSON.parse(JSON.stringify(this._data));
602
+ if (!out.profiles) out.profiles = {};
603
+ if (disk) {
604
+ // top-level keys (activeProfile, services, ...): only ones we changed
605
+ for (const [k, v] of Object.entries(this._data)) {
606
+ if (k === 'profiles') continue;
607
+ if (this._loadedTop[k] !== J(v)) out[k] = v;
608
+ }
609
+ for (const name of Object.keys(this._data.profiles)) {
610
+ if (this._removedProfiles.has(name)) continue;
611
+ const mine = this._data.profiles[name];
612
+ const loaded = this._loadedProfiles[name] === undefined ? undefined : JSON.parse(this._loadedProfiles[name]);
613
+ const wasCleared = this._clearedAuth.has(name);
614
+ if (loaded !== undefined && J(mine) === J(loaded) && !wasCleared) continue; // untouched
615
+ const target = out.profiles[name];
616
+ if (target === undefined) { out.profiles[name] = JSON.parse(JSON.stringify(mine)); continue; }
617
+ for (const key of new Set([...Object.keys(mine), ...Object.keys(loaded || {})])) {
618
+ if (loaded && J(mine[key]) === J(loaded[key]) && !(key === 'auth' && wasCleared)) continue;
619
+ if (key === 'auth') {
620
+ if (mine.auth == null && !wasCleared) { debugLog(`refusing implicit null auth for ${name}`); continue; }
621
+ if (this._refreshedAuth.has(name) && target.auth != null && J(target.auth) !== J(loaded?.auth)) {
622
+ debugLog(`keeping newer on-disk auth for ${name} over refresh from older session`);
623
+ continue;
624
+ }
625
+ }
626
+ if (mine[key] === undefined) delete target[key]; else target[key] = mine[key];
627
+ }
628
+ }
629
+ for (const name of this._removedProfiles) delete out.profiles[name];
630
+ }
631
+ // adopt merged result in memory (in place, callers hold profile refs)
632
+ for (const [name, prof] of Object.entries(out.profiles)) {
633
+ const cur = this._data.profiles[name];
634
+ if (cur && cur !== prof) {
635
+ for (const k of Object.keys(cur)) delete cur[k];
636
+ Object.assign(cur, prof);
637
+ } else if (!cur) {
638
+ this._data.profiles[name] = prof;
639
+ }
640
+ }
641
+ for (const [k, v] of Object.entries(out)) if (k !== 'profiles') this._data[k] = v;
642
+ const tmpFile = `${getConfigFile()}.tmp-${process.pid}`;
643
+ try {
644
+ writeFileSync(tmpFile, JSON.stringify(out, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
645
+ } catch (err) {
646
+ // ENOSPC and friends: never leave a stray tmp, never touch config.json.
647
+ try { unlinkSync(tmpFile); } catch { /* not created */ }
648
+ throw err;
649
+ }
650
+ try {
651
+ chmodSync(tmpFile, 0o600);
652
+ } catch {
653
+ // May fail on Windows, acceptable
654
+ }
655
+ // #736: keep the last GOOD config as .bak before replacing it.
656
+ if (disk) {
657
+ try { copyFileSync(getConfigFile(), `${getConfigFile()}.bak`); chmodSync(`${getConfigFile()}.bak`, 0o600); } catch { /* best effort */ }
658
+ }
659
+ renameSync(tmpFile, getConfigFile());
660
+ this._loadedProfiles = Object.fromEntries(Object.entries(this._data.profiles || {}).map(([n, v]) => [n, J(v)]));
661
+ this._loadedTop = Object.fromEntries(Object.entries(this._data).filter(([k]) => k !== 'profiles').map(([k, v]) => [k, J(v)]));
662
+ this._clearedAuth.clear();
663
+ this._refreshedAuth.clear();
664
+ } finally {
665
+ if (locked) releaseLock(lockFile);
666
+ }
667
+ },
668
+ };
669
+ }