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,504 @@
1
+ /**
2
+ * Engine for `quox offload`: run a heavy command on a pool worker VM as if it were local.
3
+ * Every side effect (REST, ssh, rsync, ssh-keygen, fs, sleep) is injected through `deps`
4
+ * so the whole flow is unit-testable without a network or a VM.
5
+ *
6
+ * Security: the worker is reached with a DEDICATED keypair (~/.config/quox/offload_ed25519),
7
+ * generated on first use. Nothing under ~/.ssh is ever read, and key material is never printed.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import crypto from 'node:crypto';
14
+ import { spawn, spawnSync } from 'node:child_process';
15
+ import { poolStatus, poolLease, poolDone } from './vm.js';
16
+
17
+ export const REMOTE_USER = 'hyp';
18
+ export const REMOTE_ROOT = '/home/hyp/offload';
19
+ export const RSYNC_EXCLUDES = ['node_modules', '.git', 'dist', '.env*', '*.pem', '*.key', 'coverage', '.offload-busy'];
20
+ // Documented example env files carry no secrets and tests read them: rsync takes the first matching rule, so these
21
+ // includes go before the '.env*' exclude (BRAIN2 triage 2026-10-15: .env.example never reached the worker).
22
+ export const RSYNC_INCLUDES = ['.env.example', '.env.sample', '.env.template', '*.env.example'];
23
+ export function rsyncFilterArgs() {
24
+ return [...RSYNC_INCLUDES.flatMap((e) => ['--include', e]), ...RSYNC_EXCLUDES.flatMap((e) => ['--exclude', e])];
25
+ }
26
+ export const NO_ROUTE_RE = /no route to host|network is unreachable|connection timed out|operation timed out|could not resolve/i;
27
+ export const NOT_READY_RE = /connection refused|connection reset|connection closed|kex_exchange_identification/i;
28
+
29
+ export class OffloadError extends Error {}
30
+
31
+ export function configDir(home = os.homedir()) { return path.join(home, '.config', 'quox'); }
32
+ export function keyPath(home) { return path.join(configDir(home), 'offload_ed25519'); }
33
+ // A named worker (--worker / QUOX_OFFLOAD_WORKER) keeps its own state, lease and size; unnamed = the shared one.
34
+ export const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]{0,31}$/;
35
+ export function statePath(home, worker) { return path.join(configDir(home), worker ? `offload-${worker}.json` : 'offload.json'); }
36
+
37
+ export function defaultLessee(env = process.env) {
38
+ return env.QUOX_SESSION || `${env.USER || os.userInfo().username}@${os.hostname()}`;
39
+ }
40
+
41
+ /** Single-quote a string for a POSIX shell. */
42
+ export function shq(s) { return `'${String(s).replace(/'/g, `'\\''`)}'`; }
43
+
44
+ export function loadState(deps) {
45
+ try { return JSON.parse(deps.fs.readFileSync(statePath(deps.home, deps.worker), 'utf8')) || {}; } catch { return {}; }
46
+ }
47
+ export function saveState(deps, st) {
48
+ deps.fs.mkdirSync(configDir(deps.home), { recursive: true, mode: 0o700 });
49
+ deps.fs.writeFileSync(statePath(deps.home, deps.worker), JSON.stringify(st, null, 2), { mode: 0o600 });
50
+ }
51
+
52
+ /** Create the dedicated keypair once; returns the PUBLIC key text only. */
53
+ export async function ensureKey(deps) {
54
+ const key = keyPath(deps.home);
55
+ if (!deps.fs.existsSync(key) || !deps.fs.existsSync(`${key}.pub`)) {
56
+ deps.fs.mkdirSync(configDir(deps.home), { recursive: true, mode: 0o700 });
57
+ const r = await deps.run('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'quox-offload', '-f', key]);
58
+ if (r.status !== 0) throw new OffloadError(`ssh-keygen failed (exit ${r.status}): ssh-keygen -q -t ed25519 -N '' -C quox-offload -f ${key}`);
59
+ try { deps.fs.chmodSync(key, 0o600); } catch { /* best effort */ }
60
+ }
61
+ return String(deps.fs.readFileSync(`${key}.pub`, 'utf8')).trim();
62
+ }
63
+
64
+ export function sshOpts(deps) {
65
+ return [
66
+ '-i', keyPath(deps.home), '-o', 'IdentitiesOnly=yes', '-o', 'BatchMode=yes',
67
+ '-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'LogLevel=ERROR',
68
+ '-o', 'ConnectTimeout=15', '-o', 'ServerAliveInterval=30',
69
+ ];
70
+ }
71
+ export function sshArgs(deps, ip, extra = [], remoteCmd) {
72
+ const a = [...sshOpts(deps), ...extra, `${REMOTE_USER}@${ip}`];
73
+ if (remoteCmd !== undefined) a.push(remoteCmd);
74
+ return a;
75
+ }
76
+ const fmtCmd = (cmd, args) => `${cmd} ${args.map((x) => (/[^\w@%+=:,./-]/.test(x) ? shq(x) : x)).join(' ')}`;
77
+
78
+ async function ssh(deps, ip, remoteCmd, extra) {
79
+ const args = sshArgs(deps, ip, extra, remoteCmd);
80
+ const r = await deps.run('ssh', args);
81
+ return { ...r, cmdline: fmtCmd('ssh', args) };
82
+ }
83
+
84
+ function vmOf(status, vmid) {
85
+ return (status?.vms || []).find((v) => Number(v.vmid) === Number(vmid)) || null;
86
+ }
87
+
88
+ export const HUMAN_LEASE_MSG = 'Leasing a new worker needs a human profile: run `quox -p <human-profile> offload ...` once to create the shared worker; agent sessions then reuse it.';
89
+
90
+ function poolError(e) {
91
+ const msg = String(e?.message || e);
92
+ if (/pool.*(disabled|not enabled)|disabled/i.test(msg)) return new OffloadError(`the VM pool is disabled for this org (enable it with: quox vm pool config --set '{"enabled":1}'): ${msg}`);
93
+ if (/capacity|max_pool|no_capacity|exhaust/i.test(msg)) return new OffloadError(`the VM pool has no capacity right now: ${msg}`);
94
+ return new OffloadError(`pool request failed${e?.status ? ` (HTTP ${e.status})` : ''}: ${msg}`);
95
+ }
96
+
97
+ function leaseError(e) {
98
+ if (e?.status === 403) return new OffloadError(`${HUMAN_LEASE_MSG} (HTTP 403: ${String(e.message || e)})`);
99
+ return poolError(e);
100
+ }
101
+
102
+ async function waitLeased(deps, vmid, { timeoutMs = 20 * 60 * 1000, intervalMs = 5000 } = {}) {
103
+ const t0 = deps.now();
104
+ for (;;) {
105
+ const st = await poolStatus(deps.rest);
106
+ const v = vmOf(st, vmid);
107
+ if (v?.state === 'leased' && v.ip) return v;
108
+ if (v?.state === 'failed') throw new OffloadError(`pool VM ${vmid} failed to build: ${v.last_error || 'unknown error'}`);
109
+ if (deps.now() - t0 > timeoutMs) throw new OffloadError(`pool VM ${vmid} is still ${v?.state || 'missing'} after ${Math.round(timeoutMs / 60000)} minutes; check: quox vm pool status`);
110
+ await deps.sleep(intervalMs);
111
+ }
112
+ }
113
+
114
+ /** Reuse this user's warm worker when it is still leased by us, else lease one. Returns {vmid, ip, lessee, expires, reused}. */
115
+ export async function ensureWorker(deps, { cores, memoryGb, diskGb, fresh = false } = {}) {
116
+ const lessee = defaultLessee(deps.env);
117
+ const st = loadState(deps);
118
+ // --fresh: keep the old worker until a NEW lease succeeds (an agent identity cannot lease/release;
119
+ // releasing first would strand it with no worker).
120
+ const oldVmid = st.worker && fresh ? st.worker.vmid : null;
121
+ if (st.worker && !fresh) {
122
+ let status;
123
+ try { status = await poolStatus(deps.rest); } catch (e) { throw poolError(e); }
124
+ const v = vmOf(status, st.worker.vmid);
125
+ // Reuse is judged against the lessee recorded when the worker was leased: the state file is
126
+ // shared by every session of this unix user, but QUOX_SESSION differs per session.
127
+ const owner = st.worker.lessee || lessee;
128
+ const live = v && v.state === 'leased' && v.lessee === owner && v.ip
129
+ && (!v.lease_expires_at || Date.parse(v.lease_expires_at) > deps.now());
130
+ if (live) {
131
+ const w = { vmid: v.vmid, ip: v.ip, lessee: owner, expires: v.lease_expires_at || null };
132
+ st.worker = { ...st.worker, ...w }; saveState(deps, st);
133
+ return { ...w, reused: true };
134
+ }
135
+ delete st.worker; st.dirs = {}; saveState(deps, st); // stale: a new VM has a clean disk
136
+ }
137
+ const pubkey = await ensureKey(deps);
138
+ let res;
139
+ try {
140
+ res = await poolLease(deps.rest, { lessee, pubkey, cores, memoryGb, diskGb, purpose: 'quox offload' });
141
+ } catch (e) { throw leaseError(e); }
142
+ let vm = res?.lease || null;
143
+ if (!vm && res?.vmid != null) {
144
+ deps.log(`building a worker VM ${res.vmid} (this takes a few minutes)...`);
145
+ try { vm = await waitLeased(deps, res.vmid); } catch (e) { throw e instanceof OffloadError ? e : poolError(e); }
146
+ }
147
+ if (!vm?.vmid) throw new OffloadError(`unexpected lease response from the pool: ${JSON.stringify(res)}`);
148
+ const w = { vmid: vm.vmid, ip: vm.ip || null, lessee, expires: vm.lease_expires_at || vm.expires || null };
149
+ const next = { ...loadState(deps), worker: w, dirs: {} };
150
+ saveState(deps, next);
151
+ if (oldVmid != null && Number(oldVmid) !== Number(vm.vmid)) {
152
+ try { await poolDone(deps.rest, oldVmid); } catch { /* old worker already gone or not releasable */ }
153
+ }
154
+ return { ...w, reused: false };
155
+ }
156
+
157
+ async function refreshIp(deps, w) {
158
+ const v = vmOf(await poolStatus(deps.rest), w.vmid);
159
+ if (!v || v.state !== 'leased') throw new OffloadError(`worker VM ${w.vmid} is no longer leased; run again to lease a new one`);
160
+ if (v.ip) {
161
+ w.ip = v.ip;
162
+ const st = loadState(deps);
163
+ if (st.worker) { st.worker.ip = v.ip; saveState(deps, st); }
164
+ }
165
+ return w.ip;
166
+ }
167
+
168
+ /**
169
+ * Make sure ssh reaches the worker. Always re-reads the current IP first; on a no-route style
170
+ * failure refreshes once more. A freshly built VM may refuse connections while sshd starts, so
171
+ * those are retried for up to ~2 minutes.
172
+ */
173
+ export async function connect(deps, w) {
174
+ await refreshIp(deps, w);
175
+ let refreshed = false;
176
+ for (let attempt = 0; attempt < 24; attempt++) {
177
+ const r = await ssh(deps, w.ip, 'true');
178
+ if (r.status === 0) return w;
179
+ const err = `${r.stderr || ''}${r.stdout || ''}`;
180
+ if (NO_ROUTE_RE.test(err) && !refreshed) {
181
+ refreshed = true;
182
+ const old = w.ip;
183
+ await refreshIp(deps, w);
184
+ if (w.ip !== old) continue;
185
+ }
186
+ if (NOT_READY_RE.test(err) && attempt < 23) { await deps.sleep(5000); continue; }
187
+ throw new OffloadError(`ssh to the worker failed (exit ${r.status}): ${r.cmdline}\n${err.trim()}`);
188
+ }
189
+ throw new OffloadError(`ssh to the worker never became ready: ssh ${REMOTE_USER}@${w.ip}`);
190
+ }
191
+
192
+ export function setupScript(nodeMajor, { docker = false } = {}) {
193
+ return [
194
+ 'set -e',
195
+ 'SUDO=""; [ "$(id -u)" = 0 ] || SUDO="sudo -n"',
196
+ `NEED=${Number(nodeMajor)}`,
197
+ 'export DEBIAN_FRONTEND=noninteractive',
198
+ 'MISSING=""',
199
+ 'for p in git rsync build-essential python3 curl ca-certificates; do dpkg -s $p >/dev/null 2>&1 || MISSING="$MISSING $p"; done',
200
+ 'if [ -n "$MISSING" ]; then $SUDO apt-get update -qq && $SUDO apt-get install -y -qq $MISSING; fi',
201
+ 'HAVE=$(node -p "process.versions.node.split(\'.\')[0]" 2>/dev/null || echo none)',
202
+ 'if [ "$HAVE" != "$NEED" ]; then curl -fsSL https://deb.nodesource.com/setup_$NEED.x | $SUDO -E bash - && $SUDO apt-get install -y -qq nodejs; fi',
203
+ ...(docker ? [
204
+ // --docker: the daemon plus buildx (the sandbox Dockerfiles use COPY --chmod, which needs BuildKit)
205
+ 'DMISSING=""',
206
+ 'for p in docker.io docker-buildx; do dpkg -s $p >/dev/null 2>&1 || DMISSING="$DMISSING $p"; done',
207
+ 'if [ -n "$DMISSING" ]; then $SUDO apt-get update -qq && $SUDO apt-get install -y -qq $DMISSING; fi',
208
+ '$SUDO systemctl enable --now docker >/dev/null 2>&1 || true',
209
+ `id -nG ${REMOTE_USER} | grep -qw docker || $SUDO usermod -aG docker ${REMOTE_USER}`,
210
+ ] : []),
211
+ `mkdir -p ${REMOTE_ROOT}`,
212
+ ].join('\n');
213
+ }
214
+
215
+ /**
216
+ * --docker: build quox-sandbox:restricted and :standard from <root>/services/sandbox when missing or when the
217
+ * Dockerfile or its build context changed (content hash kept in the image label quox.src). No sandbox dir: no-op.
218
+ * `docker` is the command prefix (`sudo -n docker` on the worker: the group joined this run is not in this session).
219
+ */
220
+ export function sandboxImagesScript(root, sudo = 'sudo -n') {
221
+ const d = `${root}/services/sandbox`;
222
+ const docker = `${sudo ? `${sudo} ` : ''}docker`;
223
+ return [
224
+ `D=${shq(d)}`,
225
+ '[ -f "$D/Dockerfile.restricted" ] || exit 0',
226
+ 'H=$(cd "$D" && find . -type f -not -name "Dockerfile.*" | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-16)',
227
+ 'for v in restricted standard; do',
228
+ ' [ -f "$D/Dockerfile.$v" ] || continue',
229
+ ' S=$H-$(sha256sum "$D/Dockerfile.$v" | cut -c1-16)',
230
+ ` CUR=$(${docker} image inspect --format "{{index .Config.Labels \"quox.src\"}}" quox-sandbox:$v 2>/dev/null || true)`,
231
+ ' if [ "$CUR" != "$S" ]; then',
232
+ ' echo "building quox-sandbox:$v"',
233
+ ` ${docker} build -q -t quox-sandbox:$v --label quox.src=$S -f "$D/Dockerfile.$v" "$D" >/dev/null || { echo "build of quox-sandbox:$v failed"; exit 1; }`,
234
+ ' fi',
235
+ 'done',
236
+ ].join('\n');
237
+ }
238
+
239
+ export async function ensureTooling(deps, w, { docker = false } = {}) {
240
+ const st = loadState(deps);
241
+ const major = String(deps.nodeVersion).replace(/^v/, '').split('.')[0];
242
+ const done = String(st.worker?.setup || '');
243
+ // setup is recorded as "<node major>" or "<node major>+docker"; a docker run needs the +docker form
244
+ if (st.worker?.vmid === w.vmid && done.split('+')[0] === major && (!docker || done.endsWith('+docker'))) return false;
245
+ const withDocker = docker || done.endsWith('+docker');
246
+ deps.log(`preparing the worker (node, git, rsync, build tools${withDocker ? ', docker' : ''})...`);
247
+ const r = await ssh(deps, w.ip, `bash -c ${shq(setupScript(major, { docker: withDocker }))}`);
248
+ if (r.status !== 0) throw new OffloadError(`worker setup failed (exit ${r.status}): ${r.cmdline}\n${(r.stderr || '').trim()}`);
249
+ const st2 = loadState(deps);
250
+ if (st2.worker) { st2.worker.setup = withDocker ? `${major}+docker` : major; saveState(deps, st2); }
251
+ return true;
252
+ }
253
+
254
+ // Each synced worktree carries its own node_modules (~2G); dozens of them filled the worker's disk (ENOSPC, 2026-10-15).
255
+ // Before a sync, drop synced dirs idle longer than the limit (dir mtime = last use, touched at every sync), never the
256
+ // one being synced and never one a run is busy in (.offload-busy younger than 6h; an older one is a crashed run).
257
+ export const PRUNE_IDLE_MIN = 120;
258
+ export function pruneScript(root, keep, maxIdleMin = PRUNE_IDLE_MIN) {
259
+ return [
260
+ `cd ${shq(root)} 2>/dev/null || exit 0`,
261
+ 'for d in */; do d=${d%/}; [ -d "$d" ] || continue',
262
+ ` [ "$d" = ${shq(keep)} ] && continue`,
263
+ ' [ -e "$d/.offload-busy" ] && [ -n "$(find "$d/.offload-busy" -mmin -360 2>/dev/null)" ] && continue',
264
+ ` [ -n "$(find "$d" -maxdepth 0 -mmin +${Number(maxIdleMin)} 2>/dev/null)" ] && rm -rf -- "$d"`,
265
+ 'done',
266
+ // cache entries nobody has used for a day go too (each worktree's copy is only hard links into these)
267
+ `find ${shq(`${root}/.nm-cache`)} -mindepth 1 -maxdepth 1 -mmin +1440 -exec rm -rf {} + 2>/dev/null; true`,
268
+ ].join('\n');
269
+ }
270
+
271
+ export async function syncDir(deps, w, dir) {
272
+ const remote = `${REMOTE_ROOT}/${path.basename(dir)}`;
273
+ const pr = await ssh(deps, w.ip, `bash -c ${shq(pruneScript(REMOTE_ROOT, path.basename(dir)))}`);
274
+ if (pr.status !== 0) deps.log(`could not prune idle synced dirs on the worker (exit ${pr.status}); continuing`);
275
+ const args = [
276
+ '-az', '--delete', ...rsyncFilterArgs(),
277
+ '-e', fmtCmd('ssh', sshOpts(deps)),
278
+ `${dir.replace(/\/+$/, '')}/`, `${REMOTE_USER}@${w.ip}:${remote}/`,
279
+ ];
280
+ const mk = await ssh(deps, w.ip, `mkdir -p ${shq(remote)} && touch ${shq(remote)} && date +%s > ${shq(`${remote}/.offload-busy`)}`);
281
+ if (mk.status !== 0) throw new OffloadError(`could not create ${remote} on the worker: ${mk.cmdline}\n${(mk.stderr || '').trim()}`);
282
+ const r = await deps.run('rsync', args);
283
+ if (r.status !== 0) throw new OffloadError(`rsync failed (exit ${r.status}): ${fmtCmd('rsync', args)}\n${(r.stderr || '').trim()}`);
284
+ return remote;
285
+ }
286
+
287
+ // A repo may name sibling checkouts its tests read (skillrights next to the repo, the dev skill pack sources) in
288
+ // .offload-siblings.json: {"siblings":[{"name":"skillrights","path":"~/skillrights"}]}. Each lands at
289
+ // <offload root>/<name>, next to the synced checkout, and only its git-TRACKED files travel (never untracked keys
290
+ // or local state); the usual secret excludes still apply. A missing or non-git sibling is skipped with a log line.
291
+ // A name is one or more '/'-separated segments (claude-root/metron), each a plain file name: no '..', no absolute path.
292
+ const SIBLING_SEGMENT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
293
+ const siblingNameOk = (name) => name.split('/').every((seg) => SIBLING_SEGMENT_RE.test(seg) && !seg.includes('..'));
294
+ export function readSiblings(root, home = os.homedir()) {
295
+ const f = path.join(root, '.offload-siblings.json');
296
+ if (!fs.existsSync(f)) return [];
297
+ const list = JSON.parse(fs.readFileSync(f, 'utf8')).siblings || [];
298
+ return list.map((s) => {
299
+ const name = String(s?.name ?? '');
300
+ if (!siblingNameOk(name)) throw new OffloadError(`bad sibling name in ${f}: ${JSON.stringify(name)}`);
301
+ const p = String(s.path || '').replace(/^~(?=$|\/)/, home);
302
+ return { name, path: path.resolve(p) };
303
+ });
304
+ }
305
+
306
+ export async function syncSiblings(deps, w, root) {
307
+ const done = [];
308
+ for (const sib of readSiblings(root, deps.home)) {
309
+ if (!fs.existsSync(sib.path)) { deps.log(`sibling ${sib.name} skipped: ${sib.path} does not exist here`); continue; }
310
+ // git ls-files run in the dir lists its tracked files relative to it, so a repo's subdir works too
311
+ const ls = await deps.run('git', ['-C', sib.path, 'ls-files', '-z']);
312
+ if (ls.status !== 0) { deps.log(`sibling ${sib.name} skipped: ${sib.path} is not in a git checkout (exit ${ls.status})`); continue; }
313
+ const listFile = path.join(os.tmpdir(), `quox-offload-sib-${crypto.randomBytes(4).toString('hex')}`);
314
+ fs.writeFileSync(listFile, ls.stdout || '');
315
+ const remote = `${REMOTE_ROOT}/${sib.name}`;
316
+ try {
317
+ // touch the top-level dir too: the idle prune (pruneScript) judges <offload root>/<first segment> by its mtime
318
+ const top = `${REMOTE_ROOT}/${sib.name.split('/')[0]}`;
319
+ const mk = await ssh(deps, w.ip, `mkdir -p ${shq(remote)} && touch ${shq(remote)} ${shq(top)}`);
320
+ if (mk.status !== 0) throw new OffloadError(`could not create ${remote} on the worker: ${mk.cmdline}\n${(mk.stderr || '').trim()}`);
321
+ const args = ['-az', '--from0', `--files-from=${listFile}`, ...rsyncFilterArgs(), '-e', fmtCmd('ssh', sshOpts(deps)),
322
+ `${sib.path.replace(/\/+$/, '')}/`, `${REMOTE_USER}@${w.ip}:${remote}/`];
323
+ const r = await deps.run('rsync', args);
324
+ if (r.status !== 0) throw new OffloadError(`rsync of sibling ${sib.name} failed (exit ${r.status})\n${(r.stderr || '').trim()}`);
325
+ } finally {
326
+ try { fs.unlinkSync(listFile); } catch { /* best effort */ }
327
+ }
328
+ done.push(sib.name);
329
+ }
330
+ return done;
331
+ }
332
+
333
+ // One node_modules per distinct lockfile (+ node major), hard-linked into each synced worktree: dozens of worktrees
334
+ // at ~2G each filled the worker's disk twice on 2026-10-15. npm ci runs only on a cache miss and then fills the
335
+ // cache atomically (tmp + rename, so a racing run never sees a half-copied entry).
336
+ export const NM_CACHE_DIR = `${REMOTE_ROOT}/.nm-cache`;
337
+ export function depsInstallScript(dirPath, cacheRoot, key) {
338
+ const c = `${cacheRoot}/${key}`;
339
+ return [
340
+ 'set -e',
341
+ `cd ${shq(dirPath)}`,
342
+ `C=${shq(c)}`,
343
+ 'if [ -d "$C/node_modules" ]; then',
344
+ ' rm -rf node_modules; cp -al "$C/node_modules" node_modules; touch "$C"; echo NM_CACHE_HIT',
345
+ 'else',
346
+ ' npm ci --no-audit --no-fund',
347
+ ` mkdir -p ${shq(cacheRoot)}; T="$C.tmp.$$"; rm -rf "$T"; mkdir -p "$T"`,
348
+ ' if cp -al node_modules "$T/node_modules" && mv -T "$T" "$C" 2>/dev/null; then echo NM_CACHE_FILLED; else rm -rf "$T"; echo NM_CACHE_SKIPPED; fi',
349
+ 'fi',
350
+ ].join('\n');
351
+ }
352
+
353
+ /** package.json dirs that also have a package-lock.json, up to depth 3, relative to root ('.' for root). */
354
+ export function findLockDirs(deps, root, maxDepth = 3) {
355
+ const out = [];
356
+ const skip = new Set(['node_modules', '.git', 'dist', 'coverage']);
357
+ const walk = (rel, depth) => {
358
+ const abs = path.join(root, rel);
359
+ if (deps.fs.existsSync(path.join(abs, 'package.json')) && deps.fs.existsSync(path.join(abs, 'package-lock.json'))) out.push(rel || '.');
360
+ if (depth >= maxDepth) return;
361
+ let ents = [];
362
+ try { ents = deps.fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
363
+ for (const e of ents) if (e.isDirectory() && !skip.has(e.name) && !e.name.startsWith('.')) walk(path.join(rel, e.name), depth + 1);
364
+ };
365
+ walk('', 0);
366
+ return out;
367
+ }
368
+
369
+ export const hashFile = (deps, f) => crypto.createHash('sha256').update(deps.fs.readFileSync(f)).digest('hex');
370
+
371
+ /** npm ci only where package-lock.json changed since the last sync, or node_modules is missing remotely. */
372
+ export async function installDeps(deps, w, dir, remote, { fresh = false } = {}) {
373
+ const dirs = findLockDirs(deps, dir);
374
+ if (!dirs.length) return [];
375
+ const st = loadState(deps);
376
+ const rec = (st.dirs ||= {})[dir] ||= { remote, hashes: {} };
377
+ const hashes = rec.hashes ||= {};
378
+ const probe = dirs.map((d) => `[ -d ${shq(`${remote}/${d}/node_modules`)} ] || echo ${shq(d)}`).join('; ');
379
+ const pr = await ssh(deps, w.ip, probe);
380
+ const missing = new Set(String(pr.stdout || '').split('\n').map((s) => s.trim()).filter(Boolean));
381
+ const ran = [];
382
+ for (const d of dirs) {
383
+ const h = hashFile(deps, path.join(dir, d, 'package-lock.json'));
384
+ if (!fresh && hashes[d] === h && !missing.has(d)) continue;
385
+ deps.log(`npm ci in ${d}`);
386
+ const major = String(deps.nodeVersion || '').replace(/^v/, '').split('.')[0] || 'x';
387
+ const key = `${String(h).replace(/[^a-f0-9]/gi, '').slice(0, 32)}-node${major}`;
388
+ const r = await ssh(deps, w.ip, `bash -c ${shq(depsInstallScript(`${remote}/${d}`, NM_CACHE_DIR, key))}`);
389
+ if (r.status !== 0) throw new OffloadError(`npm ci failed in ${d} (exit ${r.status}): ${r.cmdline}\n${(r.stderr || r.stdout || '').trim().split('\n').slice(-15).join('\n')}`);
390
+ hashes[d] = h; ran.push(d);
391
+ saveState(deps, st); // persist per dir so an interrupted install resumes
392
+ }
393
+ rec.remote = remote; rec.synced_at = new Date(deps.now()).toISOString();
394
+ saveState(deps, st);
395
+ return ran;
396
+ }
397
+
398
+ // Pool default timeouts (SITE-CLI ruling 2026-10-16, policy rule C: fix the harness, never an assertion). On a
399
+ // shared pool worker vitest's 5 s test / 10 s hook defaults turned ~55 slow-but-green collector tests red per run.
400
+ // A vitest command gets 30 s defaults unless the caller set its own; per-test timeouts still win, no assertion moves.
401
+ const POOL_VITEST_TIMEOUTS = [['--testTimeout', '30000'], ['--hookTimeout', '30000']];
402
+ export function withPoolTimeouts(command) {
403
+ if (!command.some((a) => /(^|\/)vitest(\.m?js)?$/.test(String(a)))) return command;
404
+ const add = POOL_VITEST_TIMEOUTS.filter(([flag]) => !command.some((a) => String(a) === flag || String(a).startsWith(`${flag}=`)))
405
+ .map(([flag, ms]) => `${flag}=${ms}`);
406
+ return [...command, ...add];
407
+ }
408
+
409
+ /** Full flow. Returns the remote command's exit code. */
410
+ export async function runOffload(deps, { dir, cwd, command, fresh, cores, memoryGb, diskGb, docker = false }) {
411
+ if (!command?.length) throw new OffloadError('no command given: quox offload [options] -- <command...>');
412
+ const root = path.resolve(dir);
413
+ const w = await ensureWorker(deps, { cores, memoryGb, diskGb, fresh });
414
+ deps.log(w.reused ? `reusing worker VM ${w.vmid}` : `leased worker VM ${w.vmid}`);
415
+ await connect(deps, w);
416
+ await ensureTooling(deps, w, { docker });
417
+ const sibs = await syncSiblings(deps, w, root);
418
+ if (sibs.length) deps.log(`synced sibling repos: ${sibs.join(', ')}`);
419
+ const remote = await syncDir(deps, w, root);
420
+ await installDeps(deps, w, root, remote, { fresh });
421
+ if (docker) {
422
+ const r = await ssh(deps, w.ip, `bash -c ${shq(sandboxImagesScript(remote))}`);
423
+ if (r.status !== 0) throw new OffloadError(`sandbox image build failed (exit ${r.status}): ${r.cmdline}\n${((r.stdout || '') + (r.stderr || '')).trim()}`);
424
+ if ((r.stdout || '').trim()) deps.log(r.stdout.trim());
425
+ }
426
+ const rel = path.relative(root, path.resolve(cwd || root));
427
+ const workdir = rel && !rel.startsWith('..') ? `${remote}/${rel}` : remote;
428
+ const envPrefix = deps.env.FORCE_COLOR ? `FORCE_COLOR=${shq(deps.env.FORCE_COLOR)} ` : '';
429
+ const user = withPoolTimeouts(command).map(shq).join(' ');
430
+ const tty = !!deps.isTTY;
431
+ let remoteCmd; let pidFile = null;
432
+ if (tty) {
433
+ remoteCmd = `cd ${shq(workdir)} && ${envPrefix}${user}`;
434
+ } else {
435
+ pidFile = `/tmp/quox-offload-${crypto.randomBytes(4).toString('hex')}.pid`;
436
+ // setsid makes the command its own process group (pgid = pid) so Ctrl-C can kill the whole group.
437
+ remoteCmd = `cd ${shq(workdir)} && exec setsid -w bash -c ${shq(`echo $$ > ${pidFile}; ${envPrefix}exec "$@"`)} _ ${user}`;
438
+ }
439
+ const args = sshArgs(deps, w.ip, tty ? ['-t'] : [], remoteCmd);
440
+ const off = deps.onSigint(async () => {
441
+ if (pidFile) await ssh(deps, w.ip, `kill -INT -- -$(cat ${pidFile}) 2>/dev/null; true`);
442
+ });
443
+ try {
444
+ return await deps.runLive('ssh', args);
445
+ } finally {
446
+ off();
447
+ try { await ssh(deps, w.ip, `rm -f ${pidFile ? `${pidFile} ` : ''}${shq(`${remote}/.offload-busy`)}`); } catch { /* best effort */ }
448
+ }
449
+ }
450
+
451
+ export async function releaseWorker(deps) {
452
+ const st = loadState(deps);
453
+ if (!st.worker) return null;
454
+ const vmid = st.worker.vmid;
455
+ await poolDone(deps.rest, vmid);
456
+ delete st.worker; st.dirs = {};
457
+ saveState(deps, st);
458
+ return vmid;
459
+ }
460
+
461
+ export async function workerStatus(deps) {
462
+ const st = loadState(deps);
463
+ if (!st.worker) return { worker: null };
464
+ let live = null;
465
+ try { live = vmOf(await poolStatus(deps.rest), st.worker.vmid); } catch { /* report cached */ }
466
+ return {
467
+ worker: {
468
+ vmid: st.worker.vmid, ip: live?.ip || st.worker.ip, state: live?.state || 'unknown',
469
+ lessee: st.worker.lessee, lease_expires_at: live?.lease_expires_at || st.worker.expires,
470
+ },
471
+ dirs: Object.entries(st.dirs || {}).map(([local, r]) => ({ local, remote: r.remote, synced_at: r.synced_at || null })),
472
+ };
473
+ }
474
+
475
+ /** Real side effects. */
476
+ export function realDeps({ rest, env = process.env, log = (m) => process.stderr.write(`offload: ${m}\n`) }) {
477
+ return {
478
+ rest, env, log, fs, home: os.homedir(), nodeVersion: process.version, isTTY: !!process.stdout.isTTY,
479
+ now: () => Date.now(),
480
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
481
+ run: (cmd, args) => new Promise((resolve) => {
482
+ const p = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
483
+ let stdout = ''; let stderr = '';
484
+ p.stdout.on('data', (d) => { stdout += d; }); p.stderr.on('data', (d) => { stderr += d; });
485
+ p.on('error', (e) => resolve({ status: 127, stdout, stderr: `${stderr}${e.message}` }));
486
+ p.on('close', (code) => resolve({ status: code ?? 1, stdout, stderr }));
487
+ }),
488
+ runLive: (cmd, args) => new Promise((resolve, reject) => {
489
+ const p = spawn(cmd, args, { stdio: 'inherit' });
490
+ p.on('error', (e) => reject(new OffloadError(`${cmd} failed to start: ${e.message}`)));
491
+ p.on('close', (code, sig) => resolve(code ?? (sig ? 128 : 1)));
492
+ }),
493
+ onSigint: (fn) => {
494
+ const h = () => { Promise.resolve(fn()).catch(() => {}); };
495
+ process.on('SIGINT', h);
496
+ return () => process.off('SIGINT', h);
497
+ },
498
+ };
499
+ }
500
+
501
+ export function gitToplevel(cwd) {
502
+ const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
503
+ return r.status === 0 ? r.stdout.trim() : cwd;
504
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * org-override.js — what `--org` is allowed to mean (GRIPE-709).
3
+ *
4
+ * A user access token is bound to ONE org (its org_id claim). `--org` used to
5
+ * be read only by verbs that build /api/orgs/<id>/... paths; every other verb
6
+ * (notes, whoami, ...) ignored it and answered from the session org, so
7
+ * `quox --org B notes ls` silently returned org A's data. The server offers no
8
+ * per-request org header, only a governed switch (`quox org switch`), so the
9
+ * safe contract is:
10
+ *
11
+ * - flag absent -> fine
12
+ * - flag names the session org -> fine (id, slug or name)
13
+ * - flag names ANY OTHER org while a
14
+ * user session is active -> REFUSE, never fall back
15
+ * - service-key-only invocation -> allowed (no session org to disagree
16
+ * with; the key's verbs take the org explicitly)
17
+ */
18
+ import { QuoxValidationError } from './errors.js';
19
+
20
+ // Commands where --org legitimately means something other than "act in".
21
+ export const EXEMPT = new Set(['login', 'logout', 'signup', 'org switch', 'invites list', 'invites accept']);
22
+
23
+ export function commandPath(cmd) {
24
+ const parts = [];
25
+ for (let c = cmd; c && c.parent; c = c.parent) parts.unshift(c.name());
26
+ return parts.join(' ');
27
+ }
28
+
29
+ /**
30
+ * @param {object} args
31
+ * @param {string|undefined} args.override value of --org
32
+ * @param {object|null} args.authData stored auth (org_id/org_slug/org_name)
33
+ * @param {boolean} args.hasUserToken
34
+ * @param {string} [args.command] e.g. 'notes ls'
35
+ * @throws {QuoxValidationError} when the override differs from the session org
36
+ */
37
+ export function assertOrgOverrideSafe({ override, authData, hasUserToken, command = '' }) {
38
+ if (!override || !hasUserToken || EXEMPT.has(command)) return;
39
+ const want = String(override).toLowerCase();
40
+ const sid = authData?.org_id || null;
41
+ const known = [sid, authData?.org_slug, authData?.org_name]
42
+ .filter(Boolean).map(v => String(v).toLowerCase());
43
+ if (known.includes(want)) return;
44
+ const current = sid
45
+ ? `${authData.org_slug || authData.org_name || sid}${authData.org_slug || authData.org_name ? ` (${sid})` : ''}`
46
+ : 'none';
47
+ throw new QuoxValidationError(
48
+ `--org ${override} differs from your session org ${current}; `
49
+ + `run \`quox org switch ${override}\` or use a profile for that org. `
50
+ + 'Refusing to act in a different org than the one named.'
51
+ );
52
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * orgResolve.js: turn a --org / QUOX_ORG_ID value into an org ID (#1141).
3
+ *
4
+ * The root flag is documented as `--org <slug>`, but the value used to be sent
5
+ * verbatim as the org ID, so `quox --org quox-development vault types` hit
6
+ * /api/orgs/quox-development/... and answered 404 org_not_found.
7
+ *
8
+ * - ID-shaped (ULID or UUID) -> unchanged, no call
9
+ * - the session org's slug or name -> its id, no call
10
+ * - otherwise -> the caller's org list, matched by the
11
+ * same matchOrg the TUI `/org switch` uses (exact slug, then exact id/slug/
12
+ * name, then every word in the name or slug), cached per profile (10 min)
13
+ * - unknown -> error listing the caller's org slugs
14
+ * - ambiguous -> error listing the matches
15
+ */
16
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { QuoxValidationError } from './errors.js';
20
+ import { matchOrg } from '../quoxon/orgSwitch.js';
21
+
22
+ const ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
23
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ export const ORG_CACHE_TTL_MS = 10 * 60 * 1000;
25
+
26
+ export const isOrgId = (v) => ULID.test(String(v ?? '').trim()) || UUID.test(String(v ?? '').trim());
27
+
28
+ const defaultCachePath = () => join(homedir(), '.config', 'quox', 'org-cache.json');
29
+ const readCache = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { return {}; } };
30
+ const writeCache = (p, data) => {
31
+ try { mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, JSON.stringify(data), { mode: 0o600 }); } catch { /* cache only */ }
32
+ };
33
+ const label = (o) => o.slug || o.name || o.id;
34
+
35
+ /**
36
+ * @param {string} value the --org / QUOX_ORG_ID value
37
+ * @param {object} opts
38
+ * @param {string} [opts.profile] cache key (the effective profile name)
39
+ * @param {object|null} [opts.authData] stored auth (org_id, org_slug, org_name)
40
+ * @param {() => Promise<object[]>} opts.listOrgs the caller's orgs ({ id, slug, name })
41
+ * @param {string} [opts.source] how the value was given, for error text
42
+ * @returns {Promise<string>} the org ID
43
+ */
44
+ export async function resolveOrgRef(value, { profile = 'default', authData = null, listOrgs, source = '--org', cachePath = defaultCachePath(), now = Date.now } = {}) {
45
+ const ref = String(value ?? '').trim();
46
+ if (!ref || isOrgId(ref)) return value;
47
+
48
+ const low = ref.toLowerCase();
49
+ if (authData?.org_id && (authData.org_slug === ref || String(authData.org_name || '').toLowerCase() === low)) return authData.org_id;
50
+
51
+ const cache = readCache(cachePath);
52
+ const hit = cache?.[profile]?.[low];
53
+ if (hit?.id && now() - (hit.at || 0) < ORG_CACHE_TTL_MS) return hit.id;
54
+
55
+ let list;
56
+ try { list = await listOrgs(); } catch (e) {
57
+ throw new QuoxValidationError(`Could not look up ${source} '${ref}' in your organisations (${e?.message || e}). Pass the org ID instead (\`quox org list\`).`);
58
+ }
59
+ const m = matchOrg(ref, list);
60
+ if (m.ambiguous) {
61
+ throw new QuoxValidationError(`${source} '${ref}' matches more than one of your organisations: ${m.ambiguous.map(label).join(', ')}. Use the full slug or the org ID.`);
62
+ }
63
+ if (!m.org) {
64
+ const yours = (Array.isArray(list) ? list : []).map(label);
65
+ throw new QuoxValidationError(`${source} '${ref}' is not one of your organisations. Yours: ${yours.length ? yours.join(', ') : 'none'}.`);
66
+ }
67
+ cache[profile] = { ...(cache[profile] || {}), [low]: { id: m.org.id, at: now() } };
68
+ writeCache(cachePath, cache);
69
+ return m.org.id;
70
+ }