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,188 @@
1
+ /**
2
+ * The one CLI updater (owner 2026-10-16 "we need /update", "or quox --update"): shared by /update in the
3
+ * conversation client and `quox update` / `quox --update` in the shell.
4
+ *
5
+ * It updates THIS quox the way it was installed, and never builds a second path where one already exists:
6
+ * npm <root> is node_modules/quoxcli -> npm install -g quoxcli@<latest>
7
+ * shared-git the box's shared CLI tree -> quox-cli-refresh (it refuses a dirty tree itself)
8
+ * git any other git checkout -> git -C <root> pull --ff-only (clean and on main only)
9
+ * server a self-hosted install, at a v* tag -> points at `quox server update` (that is its updater)
10
+ * bundle a dev-server bundle (.bundle-sha) -> points at the server's refresh
11
+ * It never runs git stash, reset or checkout. Every external call is injectable (deps.run / spawn) for tests.
12
+ *
13
+ * Network: `npm view quoxcli ...` (the npm registry) and `git fetch origin main` (the tree's own remote), only
14
+ * when the user asks for an update. Nothing is sent but those requests.
15
+ */
16
+ import { spawnSync, execFile } from 'node:child_process';
17
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import os from 'node:os';
20
+ import { RUNNING_ROOT, findOnPath } from '../quoxon/slash/restart.js';
21
+ import { cmpSemver } from '../server/install.js';
22
+ import { askText } from '../quoxon/ask.js';
23
+
24
+ export const NPM_PACKAGE = 'quoxcli';
25
+ export const NOTES_MAX = 5;
26
+
27
+ function defaultRun(cmd, args, { timeout = 30000 } = {}) {
28
+ const r = spawnSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout });
29
+ if (r.error) return { code: null, stdout: '', stderr: `${r.error.code || 'failed'}: ${r.error.message}` };
30
+ return { code: r.status, stdout: String(r.stdout || '').trim(), stderr: String(r.stderr || '').trim() };
31
+ }
32
+
33
+ /** The same, without blocking: the check runs while the conversation client keeps painting. */
34
+ function asyncRun(cmd, args, { timeout = 30000 } = {}) {
35
+ return new Promise((done) => {
36
+ execFile(cmd, args, { encoding: 'utf8', timeout }, (err, stdout, stderr) => {
37
+ if (err && typeof err.code !== 'number') return done({ code: null, stdout: '', stderr: `${err.code || (err.killed ? 'timed out' : 'failed')}: ${String(stderr || err.message).trim()}` });
38
+ done({ code: err ? err.code : 0, stdout: String(stdout || '').trim(), stderr: String(stderr || '').trim() });
39
+ });
40
+ });
41
+ }
42
+
43
+ function withDeps(deps = {}, run = defaultRun) {
44
+ return {
45
+ run,
46
+ exists: existsSync,
47
+ readFile: (p) => readFileSync(p, 'utf8'),
48
+ realpath: (p) => { try { return realpathSync(p); } catch { return p; } },
49
+ findOnPath,
50
+ home: os.homedir(),
51
+ ...deps,
52
+ };
53
+ }
54
+
55
+ const gitOut = (d, dir, args, timeout = 5000) => {
56
+ const r = d.run('git', ['-C', dir, ...args], { timeout });
57
+ return r.code === 0 ? String(r.stdout || '').trim() : null;
58
+ };
59
+ const gitOutA = async (d, dir, args, timeout = 5000) => {
60
+ const r = await d.run('git', ['-C', dir, ...args], { timeout });
61
+ return r.code === 0 ? String(r.stdout || '').trim() : null;
62
+ };
63
+ const lastLine = (s) => String(s || '').trim().split('\n').filter(Boolean).pop() || '';
64
+
65
+ /** How this quox was installed. -> { kind, root, version, branch?, refresh?, tag? } */
66
+ export function detectInstall({ root = RUNNING_ROOT, env = process.env, deps = {} } = {}) {
67
+ const d = withDeps(deps);
68
+ const real = d.realpath(root);
69
+ let version = null;
70
+ try { version = JSON.parse(d.readFile(join(root, 'package.json'))).version || null; } catch { /* no package.json */ }
71
+ if (/[/\\]node_modules[/\\]quoxcli$/.test(real)) return { kind: 'npm', root: real, version };
72
+ if (d.exists(join(root, '.bundle-sha'))) return { kind: 'bundle', root: real, version };
73
+ const top = gitOut(d, real, ['rev-parse', '--show-toplevel']);
74
+ if (top) {
75
+ const branch = gitOut(d, top, ['rev-parse', '--abbrev-ref', 'HEAD']);
76
+ const refresh = d.findOnPath('quox-cli-refresh', env);
77
+ const shared = d.realpath(env.QUOX_CLI_DIR || join(d.home, 'quox-dashboard-cli'));
78
+ if (refresh && d.realpath(top) === shared) return { kind: 'shared-git', root: top, refresh, branch, version };
79
+ if (branch === 'HEAD') {
80
+ const tag = gitOut(d, top, ['describe', '--tags', '--exact-match']);
81
+ if (tag && /^v\d/.test(tag)) return { kind: 'server', root: top, tag, version };
82
+ }
83
+ return { kind: 'git', root: top, branch, version };
84
+ }
85
+ return { kind: 'unknown', root: real, version };
86
+ }
87
+
88
+ async function checkNpm(info, d) {
89
+ const cur = info.version || '0.0.0';
90
+ const v = await d.run('npm', ['view', NPM_PACKAGE, 'version'], { timeout: 20000 });
91
+ if (v.code !== 0 || !v.stdout) return { ok: false, kind: 'npm', current: cur, error: `npm view ${NPM_PACKAGE} version failed: ${lastLine(v.stderr) || `exit ${v.code}`}` };
92
+ const latest = lastLine(v.stdout);
93
+ const cmd = `npm install -g ${NPM_PACKAGE}@${latest}`;
94
+ const base = { ok: true, kind: 'npm', current: cur, latest, notes: [], manual: cmd };
95
+ const c = cmpSemver(latest, cur);
96
+ if (c !== null && c <= 0) return { ...base, upToDate: true, ahead: c < 0, count: 0 };
97
+ let count = 1;
98
+ const all = await d.run('npm', ['view', NPM_PACKAGE, 'versions', '--json'], { timeout: 20000 });
99
+ try {
100
+ const list = JSON.parse(all.stdout);
101
+ const n = (Array.isArray(list) ? list : [list]).filter((x) => cmpSemver(x, cur) === 1 && cmpSemver(x, latest) <= 0).length;
102
+ if (n > 0) count = n;
103
+ } catch { /* keep 1 */ }
104
+ return { ...base, upToDate: false, count, command: { cmd: 'npm', args: ['install', '-g', `${NPM_PACKAGE}@${latest}`], display: cmd } };
105
+ }
106
+
107
+ async function checkGit(info, d) {
108
+ const { root, kind } = info;
109
+ const f = await d.run('git', ['-C', root, 'fetch', 'origin', 'main', '--quiet'], { timeout: 30000 });
110
+ if (f.code !== 0) return { ok: false, kind, error: `git -C ${root} fetch origin main failed: ${lastLine(f.stderr) || `exit ${f.code}`}` };
111
+ const current = await gitOutA(d, root, ['rev-parse', '--short', 'HEAD']) || '?';
112
+ const latest = await gitOutA(d, root, ['rev-parse', '--short', 'origin/main']) || '?';
113
+ const count = Number(await gitOutA(d, root, ['rev-list', '--count', 'HEAD..origin/main']) || 0) || 0;
114
+ const pull = `git -C ${root} pull --ff-only`;
115
+ const shared = kind === 'shared-git';
116
+ const manual = shared ? 'quox-cli-refresh' : pull;
117
+ const base = { ok: true, kind, root, current, latest, count, manual };
118
+ if (count === 0) return { ...base, upToDate: true, notes: [] };
119
+ const notes = (await gitOutA(d, root, ['log', '--format=%s', '-n', String(NOTES_MAX), 'HEAD..origin/main']) || '').split('\n').filter(Boolean);
120
+ if (shared) {
121
+ return { ...base, upToDate: false, notes, scope: `This runs quox-cli-refresh, which updates quox for every session on this box (${root}).`, command: { cmd: info.refresh, args: [], display: 'quox-cli-refresh' } };
122
+ }
123
+ let refused = null;
124
+ if (info.branch !== 'main') refused = `${root} is on branch ${info.branch || '(unknown)'}, not main. Switch to main yourself, then update again. Nothing was changed.`;
125
+ else {
126
+ const dirty = (await gitOutA(d, root, ['status', '--porcelain', '--untracked-files=no']) || '').split('\n').filter(Boolean);
127
+ if (dirty.length) refused = `${root} has ${dirty.length} file${dirty.length === 1 ? '' : 's'} with uncommitted changes. Commit or move them, then update again. Nothing was changed.`;
128
+ }
129
+ return { ...base, upToDate: false, notes, refused, command: refused ? null : { cmd: 'git', args: ['-C', root, 'pull', '--ff-only'], display: pull } };
130
+ }
131
+
132
+ /**
133
+ * What an update would do. -> { ok, kind, current, latest, count, notes[], upToDate, command?, manual, scope?, refused?, error? }
134
+ * refused: this install has its own updater (or is not in a state to update): say why and the manual way.
135
+ */
136
+ export async function checkUpdate(info, { deps = {} } = {}) {
137
+ const d = withDeps(deps, asyncRun);
138
+ switch (info?.kind) {
139
+ case 'npm': return checkNpm(info, d);
140
+ case 'git': case 'shared-git': return checkGit(info, d);
141
+ case 'server': return { ok: true, kind: 'server', current: info.tag, manual: 'quox server update', refused: `This quox is part of your self-hosted Quox install (${info.tag} at ${info.root}), so it updates with the server: run quox server update (it shows the latest release and asks first).` };
142
+ case 'bundle': return { ok: true, kind: 'bundle', manual: null, refused: `This quox is the dev-server bundle in ${info.root}; the Quox server refreshes it while dev mode is on. Ask your admin, or install your own copy with npm install -g ${NPM_PACKAGE}@latest.` };
143
+ default: return { ok: true, kind: 'unknown', manual: `npm install -g ${NPM_PACKAGE}@latest`, refused: `Could not tell how this quox was installed (${info?.root || 'unknown path'}). To install the published one: npm install -g ${NPM_PACKAGE}@latest` };
144
+ }
145
+ }
146
+
147
+ /** Run the update with the terminal handed over (prompts, sudo). -> { ok, code } or { ok:false, error } */
148
+ export function runUpdate(plan, { spawn = spawnSync, env = process.env } = {}) {
149
+ const r = spawn(plan.command.cmd, plan.command.args, { stdio: 'inherit', env });
150
+ if (r.error) return { ok: false, error: `${r.error.code || 'failed'}: ${r.error.message}` };
151
+ const code = r.status ?? (r.signal ? 128 + (os.constants.signals[r.signal] || 0) : 1);
152
+ return { ok: code === 0, code };
153
+ }
154
+
155
+ const isGit = (p) => p.kind === 'git' || p.kind === 'shared-git';
156
+
157
+ /** The lines shown before the ask (or the one line when there is nothing to do). */
158
+ export function updateLines(plan) {
159
+ if (!plan.ok) return [`Could not check for updates: ${plan.error}`];
160
+ if (plan.refused && plan.kind !== 'git') return [plan.refused];
161
+ if (plan.upToDate) return [isGit(plan) ? `quox ${plan.current} is up to date with origin/main.` : plan.ahead ? `quox ${plan.current} is newer than the published ${plan.latest}. Nothing to update.` : `quox ${plan.current} is the latest.`];
162
+ const out = [];
163
+ const notes = plan.notes || [];
164
+ if (notes.length) out.push(`What's new (${isGit(plan) ? 'commits on origin/main' : 'releases'}):`, ...notes.slice(0, NOTES_MAX).map((n) => ` · ${n}`));
165
+ if (plan.count > notes.length && notes.length) out.push(` · and ${plan.count - notes.length} more`);
166
+ if (plan.kind === 'npm') out.push(`Runs: ${plan.command.display}. Release notes show after the restart (/whatsnew).`);
167
+ if (plan.scope) out.push(plan.scope);
168
+ if (plan.refused) out.push(`quox ${plan.current} -> ${plan.latest}: ${plan.count} change${plan.count === 1 ? '' : 's'}, not updated. ${plan.refused}`);
169
+ return out;
170
+ }
171
+
172
+ /** "▶ quox <cur> -> <latest>: <n> changes. Update now and restart? y/N/c" (the shared ask shape). */
173
+ export function updateAsk(plan, { restart = true } = {}) {
174
+ const n = plan.count;
175
+ return askText(`Update now${restart ? ' and restart' : ''}?`, { reason: `quox ${plan.current} -> ${plan.latest}: ${n} change${n === 1 ? '' : 's'}.` });
176
+ }
177
+
178
+ /** `quox --update [...]` is `quox update [...]` when it comes before any subcommand. */
179
+ export function rewriteUpdateFlag(argv) {
180
+ const VALUE = new Set(['-p', '--profile', '-o', '--output', '--org']);
181
+ for (let i = 0; i < argv.length; i++) {
182
+ const a = argv[i];
183
+ if (a === '--update') return [...argv.slice(0, i), 'update', ...argv.slice(i + 1)];
184
+ if (VALUE.has(a)) { i++; continue; }
185
+ if (!a.startsWith('-')) return argv; // a subcommand came first: its own flags are its business
186
+ }
187
+ return argv;
188
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * QUOXON J1: the `quox claude` wrapper's session heartbeat. The wrapper starts
3
+ * ONE detached `quox sessions heartbeat <tmux-session> --loop` per tmux session
4
+ * it launches; that child beats POST /api/v1/sessions/heartbeat every interval
5
+ * while the tmux session exists, then calls /api/v1/sessions/end and exits.
6
+ * The server expires a row after 3 missed beats, so a killed child or a
7
+ * powered-off box drops out on its own.
8
+ *
9
+ * Loop hygiene (house rule: failure accounting, backoff, retirement):
10
+ * - a failed beat is counted and logged, never treated as success;
11
+ * - consecutive failures back off exponentially, capped at maxBackoffS;
12
+ * - the loop retires after maxFailures consecutive failures, when the tmux
13
+ * session ends, or when it never appears within the start grace;
14
+ * - a pidfile lock keeps one heartbeat per session across reattaches, and is
15
+ * released on every exit path.
16
+ * Pure parts take injected tmux output / held slots / attach lookup.
17
+ */
18
+ import { spawn as realSpawn, spawnSync as realSpawnSync } from 'node:child_process';
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, statSync, appendFileSync, truncateSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { join } from 'node:path';
22
+ import { slotsBySession, readAttachSafe, readHeldSlotsSafe } from './liveSessionsReport.js';
23
+
24
+ export const PROBE_FORMAT = '#{session_name}\t#{session_created}\t#{session_attached}\t#{session_activity}\t#{pane_pid}\t#{pane_current_command}';
25
+ export const DEFAULT_INTERVAL_S = 60;
26
+ const HB_DIR = () => join(homedir(), '.quox', 'heartbeat');
27
+ const LOG_CAP_BYTES = 256 * 1024;
28
+
29
+ const isoSec = (v) => {
30
+ const n = Number(v);
31
+ return Number.isFinite(n) && n > 0 ? new Date(n * 1000).toISOString() : null;
32
+ };
33
+
34
+ /** One line of PROBE_FORMAT output -> probe record, or null. */
35
+ export function parseProbe(out) {
36
+ const first = String(out || '').split('\n').find((l) => l.trim());
37
+ if (!first) return null;
38
+ const c = first.split('\t');
39
+ if (c.length < 6 || !c[0]) return null;
40
+ const pid = Number.parseInt(c[4], 10);
41
+ return {
42
+ name: c[0],
43
+ started_at: isoSec(c[1]),
44
+ attached: Number(c[2]) > 0,
45
+ last_active: isoSec(c[3]),
46
+ pid: Number.isInteger(pid) ? pid : null,
47
+ command: c[5] || null,
48
+ };
49
+ }
50
+
51
+ /** The wrapped tmux session's bare name, as the held-slots ledger records it. */
52
+ const bareOf = (name) => String(name).replace(/^quox-/, '');
53
+
54
+ export function buildBeat({ probe, backend = null, agent = null, slot = null, held = {}, attachLookup = readAttachSafe, intervalS = DEFAULT_INTERVAL_S }) {
55
+ const by = slotsBySession(held);
56
+ const bare = bareOf(probe.name);
57
+ const hit = by[probe.name] || by[bare] || by[bare.replace(/^(codex-|gemini-|tui-)/, '')] || null;
58
+ // The pane runs claude directly, so its pid IS the attach-kit pid; the slot entry's pid is the fallback.
59
+ const att = (probe.pid != null && attachLookup(probe.pid)) || (hit && hit.pid != null && attachLookup(hit.pid)) || null;
60
+ return {
61
+ name: probe.name,
62
+ program: backend || probe.command || null,
63
+ pid: probe.pid,
64
+ started_at: probe.started_at,
65
+ last_active: probe.last_active,
66
+ attached: probe.attached,
67
+ slot: (hit && hit.slot) || slot || null,
68
+ agent: (att && att.agent) || agent || null,
69
+ interval_s: intervalS,
70
+ };
71
+ }
72
+
73
+ /** Live probe of one tmux session (exact match). null when it does not exist. */
74
+ export function probeTmux(name, spawnSync = realSpawnSync) {
75
+ const r = spawnSync('tmux', ['display-message', '-p', '-t', `=${name}`, PROBE_FORMAT], { encoding: 'utf8' });
76
+ if (!r || r.error || r.status !== 0) return null;
77
+ const p = parseProbe(r.stdout);
78
+ return p && p.name === name ? p : null;
79
+ }
80
+
81
+ export async function runHeartbeatLoop({
82
+ tmuxName, backend = null, agent = null, slot = null, intervalS = DEFAULT_INTERVAL_S,
83
+ probe = (n) => probeTmux(n), post, end, held = () => readHeldSlotsSafe(), attachLookup = readAttachSafe,
84
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)), log = () => {},
85
+ maxFailures = 30, maxBackoffS = 600, graceMs = 30_000, gracePollMs = 2000, maxTicks = Infinity,
86
+ lock = { acquire: () => true, release: () => {} },
87
+ }) {
88
+ if (!lock.acquire()) {
89
+ log(`another heartbeat already holds ${tmuxName}; exiting`);
90
+ return { exit: 'duplicate', beats: 0, failures: 0 };
91
+ }
92
+ let beats = 0;
93
+ let failures = 0;
94
+ let consecutive = 0;
95
+ let ticks = 0;
96
+ let seen = false;
97
+ let waited = 0;
98
+ try {
99
+ while (ticks < maxTicks) {
100
+ const p = probe(tmuxName);
101
+ if (!p) {
102
+ if (!seen && waited < graceMs) { waited += gracePollMs; await sleep(gracePollMs); continue; }
103
+ if (!seen) { log(`${tmuxName} never appeared; exiting`); return { exit: 'never_started', beats, failures }; }
104
+ try { await end(); } catch (e) { log(`end failed: ${e && e.status ? `HTTP ${e.status} ` : ''}${e && e.message}`); }
105
+ log(`${tmuxName} ended after ${beats} beats (${failures} failed)`);
106
+ return { exit: 'ended', beats, failures };
107
+ }
108
+ seen = true;
109
+ ticks += 1;
110
+ let beat;
111
+ try { beat = buildBeat({ probe: p, backend, agent, slot, held: held(), attachLookup, intervalS }); } catch (e) { beat = null; log(`beat build failed: ${e.message}`); }
112
+ try {
113
+ if (!beat) throw new Error('no beat');
114
+ await post(beat);
115
+ beats += 1;
116
+ consecutive = 0;
117
+ } catch (e) {
118
+ failures += 1;
119
+ consecutive += 1;
120
+ log(`beat failed (${consecutive} in a row, ${failures} total): ${e && e.status ? `HTTP ${e.status} ` : ''}${e && e.message}`);
121
+ if (consecutive >= maxFailures) {
122
+ log(`retiring after ${consecutive} consecutive failures`);
123
+ return { exit: 'retired', beats, failures };
124
+ }
125
+ }
126
+ const delayS = consecutive ? Math.min(intervalS * 2 ** Math.min(consecutive, 10), maxBackoffS) : intervalS;
127
+ await sleep(delayS * 1000);
128
+ }
129
+ return { exit: 'max_ticks', beats, failures };
130
+ } finally {
131
+ lock.release();
132
+ }
133
+ }
134
+
135
+ const safeFile = (name) => String(name).replace(/[^A-Za-z0-9._-]/g, '_');
136
+
137
+ /** Pidfile lock under ~/.quox/heartbeat/<session>.pid; a dead holder's file is taken over. */
138
+ export function pidLock(name, { dir = HB_DIR(), pid = process.pid, alive = isAlive } = {}) {
139
+ const file = join(dir, `${safeFile(name)}.pid`);
140
+ return {
141
+ acquire() {
142
+ try {
143
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
144
+ if (existsSync(file)) {
145
+ const other = Number.parseInt(readFileSync(file, 'utf8'), 10);
146
+ if (Number.isInteger(other) && other !== pid && alive(other)) return false;
147
+ }
148
+ writeFileSync(file, String(pid), { mode: 0o600 });
149
+ return true;
150
+ } catch { return false; }
151
+ },
152
+ release() {
153
+ try {
154
+ if (existsSync(file) && Number.parseInt(readFileSync(file, 'utf8'), 10) === pid) unlinkSync(file);
155
+ } catch { /* best effort */ }
156
+ },
157
+ };
158
+ }
159
+
160
+ function isAlive(pid) {
161
+ try { process.kill(pid, 0); return true; } catch (e) { return e && e.code === 'EPERM'; }
162
+ }
163
+
164
+ /** Append-only per-session log, truncated when it passes LOG_CAP_BYTES. */
165
+ export function fileLogger(name, dir = HB_DIR()) {
166
+ const file = join(dir, `${safeFile(name)}.log`);
167
+ return (msg) => {
168
+ try {
169
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
170
+ if (existsSync(file) && statSync(file).size > LOG_CAP_BYTES) truncateSync(file, 0);
171
+ appendFileSync(file, `${new Date().toISOString()} ${msg}\n`, { mode: 0o600 });
172
+ } catch { /* logging must never kill the loop */ }
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Start the detached heartbeat child for a tmux session the wrapper launched.
178
+ * The child is this same quox binary; it survives the wrapper exiting on detach.
179
+ */
180
+ export function startHeartbeat({ tmuxSession, backend, agent, slot }, { spawn = realSpawn, argv = process.argv, execPath = process.execPath } = {}) {
181
+ if (!argv[1]) return false;
182
+ const args = [argv[1], 'sessions', 'heartbeat', tmuxSession, '--loop'];
183
+ if (backend) args.push('--backend', backend);
184
+ if (agent) args.push('--agent', agent);
185
+ if (slot) args.push('--slot', slot);
186
+ try {
187
+ const child = spawn(execPath, args, { detached: true, stdio: 'ignore' });
188
+ if (child && child.on) child.on('error', () => {});
189
+ if (child && child.unref) child.unref();
190
+ return true;
191
+ } catch { return false; }
192
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * QUOXON J2: `quox sessions` overview of GET /api/v1/sessions, grouped by
3
+ * agent, newest activity first. overviewLines() is plain text + a tone per
4
+ * line, shared by the CLI renderer (ANSI colour) and the quox conversation
5
+ * client's `/sessions all` (which colours by tone itself).
6
+ */
7
+ import { hostname as osHostname } from 'node:os';
8
+ import { line } from './quoxglyph.js';
9
+ import { formatAge } from './claudeLs.js';
10
+ import { loadNames } from '../quoxon/session.js';
11
+
12
+ export const GLYPH = { working: '◐', waiting: '⏸', resting: '○', failing: '✖' };
13
+ const TONE = { working: 'ok', waiting: 'warn', failing: 'warn', resting: 'dim' };
14
+ const ANSI = { ok: '\x1b[32m', warn: '\x1b[33m', fail: '\x1b[31m', dim: '\x1b[2m', accent: '\x1b[36m', reset: '\x1b[0m' };
15
+ const stripAnsi = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, '');
16
+
17
+ const clip = (v, n) => {
18
+ const a = [...String(v ?? '')];
19
+ return a.length > n ? a.slice(0, n - 1).join('') + '…' : String(v ?? '');
20
+ };
21
+ const pad = (v, n) => clip(v, n).padEnd(n);
22
+
23
+ function kindLabel(r) {
24
+ if (r.kind === 'tmux') return r.backend ? `tmux·${r.backend}` : 'tmux';
25
+ return r.kind;
26
+ }
27
+
28
+ function hostLabel(r, localHost) {
29
+ if (!r.host) return r.kind === 'tmux' ? '?' : 'server';
30
+ return localHost && r.host === localHost ? 'this host' : r.host;
31
+ }
32
+
33
+ export const RECENT_CONVERSATIONS = 5;
34
+ const KEEP_KINDS = new Set(['tmux', 'slot', 'resident', 'objective', 'loop', 'workflow']);
35
+
36
+ /** One-line title, clipped on a word boundary when one is near the end, never mid-surrogate. */
37
+ export function clipTitle(raw, max = 34) {
38
+ const s = String(raw ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').replace(/\s+/g, ' ').trim();
39
+ const a = [...s];
40
+ if (a.length <= max) return s;
41
+ const cut = a.slice(0, max - 1).join('');
42
+ const sp = cut.lastIndexOf(' ');
43
+ return (sp >= Math.floor(max / 2) ? cut.slice(0, sp) : cut).replace(/[\s,.;:-]+$/, '') + '…';
44
+ }
45
+
46
+ const timeOf = (r) => { const t = Date.parse(r.last_active); return Number.isFinite(t) ? t : -Infinity; };
47
+
48
+ /**
49
+ * The default view shows what is useful or eventful: every tmux session, slot,
50
+ * resident and running piece of work, every working/waiting conversation, and
51
+ * only the RECENT_CONVERSATIONS most recent other conversations. `all` keeps
52
+ * everything. Conversation names use the local /rename name when known, else
53
+ * the server title. Regrouped by each row's own agent; "no agent" goes last.
54
+ * -> { groups, hiddenConversations, shown }
55
+ */
56
+ export function prepareIndex(index, { all = false, names = {}, keep = RECENT_CONVERSATIONS } = {}) {
57
+ const rows = (index.groups || []).flatMap((g) => g.rows).map((r) => (r.kind === 'conversation'
58
+ // The hint is this client's own command, so set it here too (correct against an older server's --print hint).
59
+ ? { ...r, name: clipTitle((names && names[r.id] && names[r.id].name) || r.name || '(untitled)'), attach_hint: `quox --conversation ${r.id}` }
60
+ : r));
61
+ let hidden = 0;
62
+ let kept = rows;
63
+ if (!all) {
64
+ const quiet = rows.filter((r) => r.kind === 'conversation' && r.state !== 'working' && r.state !== 'waiting').sort((a, b) => timeOf(b) - timeOf(a));
65
+ const keepIds = new Set(quiet.slice(0, keep));
66
+ hidden = quiet.length - keepIds.size;
67
+ kept = rows.filter((r) => KEEP_KINDS.has(r.kind) || r.kind !== 'conversation' || r.state === 'working' || r.state === 'waiting' || keepIds.has(r));
68
+ }
69
+ const by = new Map();
70
+ for (const r of [...kept].sort((a, b) => timeOf(b) - timeOf(a))) {
71
+ const k = r.agent || null;
72
+ if (!by.has(k)) by.set(k, []);
73
+ by.get(k).push(r);
74
+ }
75
+ const groups = [...by.entries()].map(([agent, rs]) => ({ agent, rows: rs }))
76
+ .sort((a, b) => (a.agent === null) - (b.agent === null) || timeOf(b.rows[0]) - timeOf(a.rows[0]));
77
+ return { groups, hiddenConversations: hidden, shown: kept.length };
78
+ }
79
+
80
+ /** -> [{ text, tone, state? }] */
81
+ export function overviewLines(index, { now = Date.now(), localHost = null, all = false, names = {} } = {}) {
82
+ const prepared = prepareIndex(index, { all, names });
83
+ const counts = index.counts || {};
84
+ const total = counts.total || 0;
85
+ const out = [
86
+ { text: stripAnsi(line('quox', 'sessions', { result: `${total} live` })), tone: 'accent' },
87
+ { text: `${total} live · ${counts.working || 0} working · ${counts.waiting || 0} waiting · ${counts.resting || 0} resting · ${counts.failing || 0} failing`, tone: 'dim' },
88
+ ];
89
+ if (!total) out.push({ text: 'No live sessions in this org.', tone: 'dim' });
90
+ for (const g of prepared.groups) {
91
+ out.push({ text: `${g.agent || 'no agent'} (${g.rows.length})`, tone: 'accent' });
92
+ for (const r of g.rows) {
93
+ const t = Date.parse(r.last_active);
94
+ const age = Number.isFinite(t) ? formatAge((now - t) / 1000) : '-';
95
+ const extra = [r.slot ? `slot ${r.slot}` : null, r.state_reason || null].filter(Boolean).join(', ');
96
+ const text = ` ${GLYPH[r.state] || '?'} ${pad(r.state, 8)} ${pad(r.name, 34)} ${pad(kindLabel(r), 13)} ${pad(hostLabel(r, localHost), 12)} ${pad(age, 5)} ${r.attach_hint || ''}${extra ? ` (${extra})` : ''}`;
97
+ out.push({ text, tone: TONE[r.state] || 'dim', state: r.state });
98
+ }
99
+ }
100
+ if (prepared.hiddenConversations > 0) {
101
+ out.push({ text: ` +${prepared.hiddenConversations} more conversations (last 24 h) · quox sessions --all`, tone: 'dim' });
102
+ }
103
+ const bad = (index.sources || []).filter((s) => s.state !== 'connected');
104
+ if (bad.length) {
105
+ out.push({ text: `sources: ${bad.map((s) => `${s.id}: ${s.state}${s.note ? ` (${s.note})` : ''}`).join(' · ')}`, tone: 'dim' });
106
+ }
107
+ return out;
108
+ }
109
+
110
+ /**
111
+ * SESSIONS (owner 2026-10-15): the quox conversation client's bare `/sessions`.
112
+ * Human first: every row leads with the session NAME and WHERE it runs, then
113
+ * how long it has been up and when it was last active. Ids and attach commands
114
+ * only appear with `refs` (one dim line under each row). Sections by what the
115
+ * thing is (terminal sessions, slots, work, chats), newest activity first.
116
+ * A row with no host says "host unknown"; nothing is guessed.
117
+ * -> [{ text, tone, state? }]
118
+ */
119
+ const SECTIONS = [
120
+ { title: 'Terminal sessions', test: (r) => r.kind === 'tmux' },
121
+ { title: 'Slots', test: (r) => r.kind === 'slot' },
122
+ { title: 'Work', test: (r) => r.kind !== 'tmux' && r.kind !== 'slot' && r.kind !== 'conversation' },
123
+ { title: 'Chats', test: (r) => r.kind === 'conversation' },
124
+ ];
125
+
126
+ function whereLabel(r, localHost) {
127
+ if (r.kind === 'conversation') return 'on the Quox server';
128
+ if (!r.host) return 'host unknown';
129
+ return localHost && r.host === localHost ? `on ${r.host} (this machine)` : `on ${r.host}`;
130
+ }
131
+
132
+ function sessionName(r) {
133
+ if (r.kind === 'slot') return r.holder_session || r.name || '(unnamed slot)';
134
+ return r.name || '(unnamed)';
135
+ }
136
+
137
+ export function humanSessionLines(index, { now = Date.now(), localHost = null, all = false, refs = false, names = {} } = {}) {
138
+ const prepared = prepareIndex(index, { all, names });
139
+ const rows = prepared.groups.flatMap((g) => g.rows).sort((a, b) => timeOf(b) - timeOf(a));
140
+ const counts = index.counts || {};
141
+ const ago = (v) => { const t = Date.parse(v); return Number.isFinite(t) ? formatAge((now - t) / 1000) : null; };
142
+ const out = [
143
+ { text: stripAnsi(line('quox', 'sessions', { result: `${counts.total || 0} live` })), tone: 'accent' },
144
+ { text: `${counts.working || 0} working · ${counts.waiting || 0} waiting · ${counts.resting || 0} resting · ${counts.failing || 0} failing`, tone: 'dim' },
145
+ ];
146
+ if (!rows.length) out.push({ text: 'No live sessions in this org.', tone: 'dim' });
147
+ for (const sec of SECTIONS) {
148
+ const rs = rows.filter(sec.test);
149
+ if (!rs.length) continue;
150
+ out.push({ text: `${sec.title} (${rs.length})`, tone: 'accent' });
151
+ for (const r of rs) {
152
+ const bits = [whereLabel(r, localHost)];
153
+ if (r.kind === 'slot' && r.holder_session && r.name && r.name !== r.holder_session) bits.push(`holds slot ${r.name}`);
154
+ const up = r.started_at ? ago(r.started_at) : null;
155
+ if (up) bits.push(`up ${up}`);
156
+ const act = ago(r.last_active);
157
+ bits.push(act ? `active ${act} ago` : 'no activity recorded');
158
+ const who = [r.agent ? `agent ${r.agent}` : null, r.kind === 'tmux' && r.slot ? `slot ${r.slot}` : null, r.backend || null].filter(Boolean);
159
+ if (who.length) bits.push(who.join(', '));
160
+ if (r.unread) bits.push(`${r.unread} unread`);
161
+ if (r.turns) bits.push(`${r.turns} turn${r.turns === 1 ? '' : 's'}`);
162
+ if (r.state_reason && r.state === 'failing') bits.push(r.state_reason);
163
+ out.push({ text: ` ${GLYPH[r.state] || '?'} ${sessionName(r)} ${bits.join(' · ')}`, tone: TONE[r.state] || 'dim', state: r.state });
164
+ if (refs) out.push({ text: ` ${[r.attach_hint || null, r.id ? `id ${r.id}` : null].filter(Boolean).join(' · ')}`, tone: 'dim' });
165
+ }
166
+ }
167
+ if (prepared.hiddenConversations > 0) out.push({ text: ` +${prepared.hiddenConversations} older chats (last 24 h) · /sessions all`, tone: 'dim' });
168
+ const bad = (index.sources || []).filter((s) => s.state !== 'connected');
169
+ if (bad.length) out.push({ text: `could not read: ${bad.map((s) => `${s.id} (${s.state}${s.note ? `: ${s.note}` : ''})`).join(' · ')}`, tone: 'dim' });
170
+ if (!refs && rows.length) out.push({ text: '/sessions refs adds ids and attach commands · /sessions all includes every chat', tone: 'dim' });
171
+ return out;
172
+ }
173
+
174
+ export function renderOverview(index, { now = Date.now(), localHost = osHostname(), color = false, all = false, names = {} } = {}) {
175
+ return overviewLines(index, { now, localHost, all, names }).map((l) => {
176
+ if (!color) return l.text;
177
+ const c = l.state === 'failing' ? ANSI.fail : ANSI[l.tone] || '';
178
+ return `${c}${l.text}${ANSI.reset}`;
179
+ }).join('\n');
180
+ }
181
+
182
+ const errText = (e) => `${e && e.status ? `HTTP ${e.status}: ` : ''}${(e && e.message) || 'request failed'}`;
183
+
184
+ /**
185
+ * Bare `quox sessions`. --json prints the raw index; --watch redraws every 2 s
186
+ * until interrupted. An error is printed with its status (never shown as an
187
+ * empty list) and exits 1; in --watch it is shown and the next refresh retries.
188
+ */
189
+ export async function runSessionsOverview({ json = false, watch = false, all = false } = {}, over = {}) {
190
+ const d = {
191
+ write: (s) => process.stdout.write(s),
192
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
193
+ now: () => Date.now(),
194
+ localHost: osHostname(),
195
+ color: !!process.stdout.isTTY && !process.env.NO_COLOR,
196
+ iterations: Infinity,
197
+ names: undefined,
198
+ ...over,
199
+ };
200
+ const names = () => d.names ?? loadNames();
201
+ if (!d.fetchIndex) throw new Error('fetchIndex required');
202
+ if (!watch) {
203
+ try {
204
+ const idx = await d.fetchIndex();
205
+ d.write(json ? JSON.stringify(idx, null, 2) + '\n' : renderOverview(idx, { now: d.now(), localHost: d.localHost, color: d.color, all, names: names() }) + '\n');
206
+ return 0;
207
+ } catch (e) {
208
+ d.write(`quox sessions: ${errText(e)}\n`);
209
+ return 1;
210
+ }
211
+ }
212
+ for (let i = 0; i < d.iterations; i++) {
213
+ let body;
214
+ try {
215
+ const idx = await d.fetchIndex();
216
+ body = json ? JSON.stringify(idx) : renderOverview(idx, { now: d.now(), localHost: d.localHost, color: d.color, all, names: names() });
217
+ } catch (e) {
218
+ body = `quox sessions: ${errText(e)} (retrying)`;
219
+ }
220
+ d.write(`\x1b[2J\x1b[H${body}\n\n(refreshing every 2 s, Ctrl-C to stop)\n`);
221
+ await d.sleep(2000);
222
+ }
223
+ return 0;
224
+ }