quoxcli 0.2.0 → 0.4.0

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 (442) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/actions.js +15 -0
  5. package/lib/api/endpoints/admin.js +46 -0
  6. package/lib/api/endpoints/agent-schedules.js +26 -0
  7. package/lib/api/endpoints/agents.js +7 -0
  8. package/lib/api/endpoints/assistants.js +12 -0
  9. package/lib/api/endpoints/away.js +15 -0
  10. package/lib/api/endpoints/blog.js +16 -0
  11. package/lib/api/endpoints/chat.js +14 -0
  12. package/lib/api/endpoints/contactsq.js +6 -0
  13. package/lib/api/endpoints/conversations.js +14 -0
  14. package/lib/api/endpoints/devServerRegistry.js +25 -0
  15. package/lib/api/endpoints/engagement.js +26 -0
  16. package/lib/api/endpoints/evidence.js +20 -0
  17. package/lib/api/endpoints/executions.js +13 -0
  18. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  19. package/lib/api/endpoints/files.js +14 -0
  20. package/lib/api/endpoints/hat.js +44 -0
  21. package/lib/api/endpoints/inbox.js +20 -0
  22. package/lib/api/endpoints/index.js +26 -0
  23. package/lib/api/endpoints/loops.js +26 -0
  24. package/lib/api/endpoints/memory.js +19 -0
  25. package/lib/api/endpoints/mind.js +13 -0
  26. package/lib/api/endpoints/mon.js +179 -0
  27. package/lib/api/endpoints/objectives.js +12 -0
  28. package/lib/api/endpoints/plugins.js +18 -0
  29. package/lib/api/endpoints/presence.js +33 -0
  30. package/lib/api/endpoints/quoxplan.js +7 -0
  31. package/lib/api/endpoints/quoxseo.js +12 -0
  32. package/lib/api/endpoints/router.js +17 -0
  33. package/lib/api/endpoints/safety.js +7 -0
  34. package/lib/api/endpoints/server.js +21 -0
  35. package/lib/api/endpoints/setup.js +6 -0
  36. package/lib/api/endpoints/skills.js +20 -0
  37. package/lib/api/endpoints/stepup.js +20 -0
  38. package/lib/api/endpoints/teams.js +2 -2
  39. package/lib/api/endpoints/triggers.js +26 -0
  40. package/lib/api/endpoints/v1.js +120 -0
  41. package/lib/api/endpoints/ward.js +7 -0
  42. package/lib/api/endpoints/workflows.js +7 -0
  43. package/lib/api/endpoints/x.js +8 -0
  44. package/lib/commands/actions.js +131 -0
  45. package/lib/commands/admin.js +16 -0
  46. package/lib/commands/agent.js +170 -27
  47. package/lib/commands/agentAttach.js +140 -13
  48. package/lib/commands/approvals.js +78 -1
  49. package/lib/commands/aside.js +24 -0
  50. package/lib/commands/ask.js +146 -0
  51. package/lib/commands/assistant.js +59 -4
  52. package/lib/commands/auth.js +8 -2
  53. package/lib/commands/backup.js +8 -4
  54. package/lib/commands/blog.js +110 -0
  55. package/lib/commands/brain.js +105 -14
  56. package/lib/commands/build.js +19 -5
  57. package/lib/commands/chat.js +180 -26
  58. package/lib/commands/chatCompanion.js +27 -0
  59. package/lib/commands/chatState.js +45 -0
  60. package/lib/commands/claude.js +35 -38
  61. package/lib/commands/contacts.js +134 -4
  62. package/lib/commands/contactsSendAt.js +21 -0
  63. package/lib/commands/conversation.js +35 -4
  64. package/lib/commands/dashboard.js +43 -0
  65. package/lib/commands/department.js +28 -7
  66. package/lib/commands/devResident.js +15 -3
  67. package/lib/commands/devSupervisor.js +13 -1
  68. package/lib/commands/discord.js +75 -39
  69. package/lib/commands/done.js +98 -28
  70. package/lib/commands/engagement.js +107 -6
  71. package/lib/commands/evidence.js +154 -0
  72. package/lib/commands/ext.js +23 -0
  73. package/lib/commands/fetchAllowlist.js +71 -0
  74. package/lib/commands/file.js +38 -1
  75. package/lib/commands/fleet.js +336 -4
  76. package/lib/commands/governance.js +12 -3
  77. package/lib/commands/gripe.js +44 -13
  78. package/lib/commands/hat.js +254 -9
  79. package/lib/commands/helpers.js +18 -1
  80. package/lib/commands/hyperion.js +58 -0
  81. package/lib/commands/image.js +68 -0
  82. package/lib/commands/inbox.js +74 -23
  83. package/lib/commands/kuma.js +1050 -0
  84. package/lib/commands/kumaSla.js +174 -0
  85. package/lib/commands/licence.js +52 -155
  86. package/lib/commands/loop.js +291 -0
  87. package/lib/commands/lucid.js +10 -4
  88. package/lib/commands/memory.js +85 -8
  89. package/lib/commands/mind.js +71 -0
  90. package/lib/commands/mon.js +69 -2
  91. package/lib/commands/monitoring.js +1 -1
  92. package/lib/commands/motion.js +2 -2
  93. package/lib/commands/nucleus.js +48 -17
  94. package/lib/commands/objective.js +59 -1
  95. package/lib/commands/offload.js +89 -0
  96. package/lib/commands/onboard.js +2 -1
  97. package/lib/commands/org.js +30 -3
  98. package/lib/commands/peek.js +28 -0
  99. package/lib/commands/plan.js +38 -0
  100. package/lib/commands/plugin.js +110 -0
  101. package/lib/commands/presence.js +89 -0
  102. package/lib/commands/provider.js +217 -0
  103. package/lib/commands/qlarity.js +19 -3
  104. package/lib/commands/quoxchat.js +13 -4
  105. package/lib/commands/quoxseo.js +69 -0
  106. package/lib/commands/reach.js +2 -1
  107. package/lib/commands/research.js +79 -0
  108. package/lib/commands/router.js +77 -0
  109. package/lib/commands/rows.js +367 -0
  110. package/lib/commands/run.js +82 -10
  111. package/lib/commands/scope.js +2 -2
  112. package/lib/commands/server.js +405 -0
  113. package/lib/commands/setup.js +33 -7
  114. package/lib/commands/shortcut.js +41 -0
  115. package/lib/commands/skills.js +68 -9
  116. package/lib/commands/slot.js +260 -7
  117. package/lib/commands/status.js +1 -1
  118. package/lib/commands/team.js +3 -1
  119. package/lib/commands/tips.js +23 -0
  120. package/lib/commands/tool.js +133 -45
  121. package/lib/commands/trace.js +12 -7
  122. package/lib/commands/trigger.js +95 -0
  123. package/lib/commands/update.js +46 -0
  124. package/lib/commands/vault.js +31 -8
  125. package/lib/commands/vm.js +237 -0
  126. package/lib/commands/voice.js +34 -3
  127. package/lib/commands/volt.js +45 -3
  128. package/lib/commands/walkthrough.js +32 -0
  129. package/lib/commands/watch.js +73 -21
  130. package/lib/commands/welcome.js +14 -0
  131. package/lib/commands/workflow.js +189 -32
  132. package/lib/commands/x.js +13 -4
  133. package/lib/commands/youtube.js +172 -0
  134. package/lib/core/actingAs.js +2 -2
  135. package/lib/core/actionItems.js +115 -0
  136. package/lib/core/activeSummary.js +186 -0
  137. package/lib/core/agentAlias.js +2 -0
  138. package/lib/core/agentFamilies.js +7 -0
  139. package/lib/core/aside.js +54 -0
  140. package/lib/core/askDecision.js +60 -0
  141. package/lib/core/auth.js +164 -13
  142. package/lib/core/callerSource.js +1 -1
  143. package/lib/core/cliBuild.js +62 -0
  144. package/lib/core/client.js +152 -35
  145. package/lib/core/commandRegistrars.js +130 -0
  146. package/lib/core/config.js +143 -25
  147. package/lib/core/customHats.js +168 -0
  148. package/lib/core/doneEngineFetch.js +8 -0
  149. package/lib/core/errors.js +329 -20
  150. package/lib/core/findingReconcile.js +90 -0
  151. package/lib/core/global-flags.js +105 -5
  152. package/lib/core/governedSession.js +1 -3
  153. package/lib/core/hyperion.js +102 -0
  154. package/lib/core/inboxResolution.js +65 -0
  155. package/lib/core/lazyCommands.js +4 -1
  156. package/lib/core/liveSessionsReport.js +1 -1
  157. package/lib/core/loopGlyph.js +97 -0
  158. package/lib/core/offload.js +533 -0
  159. package/lib/core/org-override.js +1 -1
  160. package/lib/core/orgResolve.js +70 -0
  161. package/lib/core/output.js +11 -11
  162. package/lib/core/peek.js +126 -0
  163. package/lib/core/profileUsers.js +51 -0
  164. package/lib/core/promptCancel.js +18 -0
  165. package/lib/core/repoPath.js +11 -0
  166. package/lib/core/residentSeed.js +3 -1
  167. package/lib/core/rootOptions.js +39 -0
  168. package/lib/core/router.js +168 -0
  169. package/lib/core/selfUpdate.js +188 -0
  170. package/lib/core/sessionHeartbeat.js +12 -8
  171. package/lib/core/sessionsOverview.js +64 -0
  172. package/lib/core/shortcut.js +83 -0
  173. package/lib/core/slotKeeper.js +248 -3
  174. package/lib/core/slotWake.js +120 -0
  175. package/lib/core/supervisorEngine.js +71 -2
  176. package/lib/core/version.js +1 -1
  177. package/lib/core/vm.js +366 -0
  178. package/lib/core/vmTui.js +86 -0
  179. package/lib/core/walkthrough.js +35 -0
  180. package/lib/dragon/companionView.js +34 -0
  181. package/lib/dragon/frames.js +118 -0
  182. package/lib/dragon/jobs.js +123 -0
  183. package/lib/dragon/renderWorker.js +15 -0
  184. package/lib/dragon/select.js +72 -0
  185. package/lib/evidence/bundleExport.js +47 -0
  186. package/lib/evidence/bundleVerify.js +77 -0
  187. package/lib/hyperion/deck.js +29 -0
  188. package/lib/lucid/LucidCard.jsx +68 -0
  189. package/lib/lucid/LucidIntro.jsx +45 -58
  190. package/lib/lucid/cards.js +86 -0
  191. package/lib/lucid/markdown.js +81 -0
  192. package/lib/lucid/motion.js +56 -0
  193. package/lib/quoxon/EXTENSIONS.md +23 -0
  194. package/lib/quoxon/activity.js +14 -2
  195. package/lib/quoxon/aocl.js +117 -0
  196. package/lib/quoxon/api.js +690 -25
  197. package/lib/quoxon/app.js +3586 -250
  198. package/lib/quoxon/approvalFacts.js +96 -0
  199. package/lib/quoxon/approvalSentence.js +44 -0
  200. package/lib/quoxon/approvalsView.js +384 -0
  201. package/lib/quoxon/asideView.js +40 -0
  202. package/lib/quoxon/ask.js +121 -0
  203. package/lib/quoxon/away.js +52 -0
  204. package/lib/quoxon/bang.js +224 -0
  205. package/lib/quoxon/bulkRisk.js +17 -0
  206. package/lib/quoxon/capabilities.js +13 -6
  207. package/lib/quoxon/cells.js +53 -0
  208. package/lib/quoxon/commandShort.js +124 -0
  209. package/lib/quoxon/commands.js +429 -39
  210. package/lib/quoxon/connect.js +210 -23
  211. package/lib/quoxon/connectCard.js +19 -2
  212. package/lib/quoxon/consent.js +86 -0
  213. package/lib/quoxon/copy.js +4 -3
  214. package/lib/quoxon/countdownLine.js +20 -0
  215. package/lib/quoxon/deck.js +30 -0
  216. package/lib/quoxon/demo.autoplay.js +29 -0
  217. package/lib/quoxon/demo.fixtures.js +1 -1
  218. package/lib/quoxon/demo.js +46 -3
  219. package/lib/quoxon/devObserver.js +20 -0
  220. package/lib/quoxon/dividerAocl.js +192 -0
  221. package/lib/quoxon/dividerRule.js +57 -0
  222. package/lib/quoxon/dragonCompanion.js +255 -0
  223. package/lib/quoxon/dragonModes.js +151 -0
  224. package/lib/quoxon/dragonMoments.js +82 -0
  225. package/lib/quoxon/dragonReads.js +68 -0
  226. package/lib/quoxon/dragonView.js +41 -0
  227. package/lib/quoxon/earlyInput.js +40 -0
  228. package/lib/quoxon/echo.js +15 -0
  229. package/lib/quoxon/egg.js +2 -2
  230. package/lib/quoxon/events.js +266 -26
  231. package/lib/quoxon/extensions.js +276 -0
  232. package/lib/quoxon/feedback.js +50 -0
  233. package/lib/quoxon/gallery.js +21 -7
  234. package/lib/quoxon/handoffOffer.js +78 -0
  235. package/lib/quoxon/hatSwitch.js +37 -0
  236. package/lib/quoxon/headless.js +161 -0
  237. package/lib/quoxon/hostUrls.js +41 -0
  238. package/lib/quoxon/inboxView.js +79 -0
  239. package/lib/quoxon/indicator.js +6 -2
  240. package/lib/quoxon/installCli.js +79 -0
  241. package/lib/quoxon/intent.js +12 -1
  242. package/lib/quoxon/keys.js +51 -8
  243. package/lib/quoxon/launcher.js +103 -0
  244. package/lib/quoxon/loopCard.js +456 -0
  245. package/lib/quoxon/loopCards.js +180 -0
  246. package/lib/quoxon/loopStream.js +87 -0
  247. package/lib/quoxon/main.js +101 -160
  248. package/lib/quoxon/markdown.js +45 -21
  249. package/lib/quoxon/markdownSettle.js +19 -0
  250. package/lib/quoxon/matrixDomains.json +46 -0
  251. package/lib/quoxon/mentions.js +90 -0
  252. package/lib/quoxon/mind.js +103 -0
  253. package/lib/quoxon/mode.js +46 -0
  254. package/lib/quoxon/model.js +132 -26
  255. package/lib/quoxon/modelPrefs.js +345 -0
  256. package/lib/quoxon/motion/clock.js +3 -2
  257. package/lib/quoxon/motion/level.js +91 -0
  258. package/lib/quoxon/mouse.js +222 -0
  259. package/lib/quoxon/noai.js +57 -0
  260. package/lib/quoxon/objectiveOffer.js +82 -0
  261. package/lib/quoxon/objects/away.js +72 -0
  262. package/lib/quoxon/objects/fetch.js +17 -5
  263. package/lib/quoxon/objects/fleet.js +149 -0
  264. package/lib/quoxon/objects/health.js +186 -0
  265. package/lib/quoxon/objects/index.js +46 -7
  266. package/lib/quoxon/objects/mirror.js +137 -0
  267. package/lib/quoxon/objects/model.js +33 -0
  268. package/lib/quoxon/objects/peek.js +17 -0
  269. package/lib/quoxon/objects/proof.js +12 -4
  270. package/lib/quoxon/objects/prove.js +130 -0
  271. package/lib/quoxon/objects/replay.js +125 -0
  272. package/lib/quoxon/objects/sitrep.js +58 -0
  273. package/lib/quoxon/objects/view.js +11 -0
  274. package/lib/quoxon/onboarding/copy.js +32 -4
  275. package/lib/quoxon/onboarding/credential.js +17 -4
  276. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  277. package/lib/quoxon/onboarding/failureCause.js +33 -0
  278. package/lib/quoxon/onboarding/finish.js +15 -0
  279. package/lib/quoxon/onboarding/index.js +34 -7
  280. package/lib/quoxon/onboarding/installRunner.js +64 -5
  281. package/lib/quoxon/onboarding/preflight.js +17 -3
  282. package/lib/quoxon/onboarding/probe.js +65 -0
  283. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  284. package/lib/quoxon/onboarding/view.js +76 -19
  285. package/lib/quoxon/orgLabel.js +10 -0
  286. package/lib/quoxon/orgSwitch.js +59 -0
  287. package/lib/quoxon/ownBroadcast.js +22 -0
  288. package/lib/quoxon/palette.js +43 -0
  289. package/lib/quoxon/planSteps.js +51 -0
  290. package/lib/quoxon/print.js +12 -5
  291. package/lib/quoxon/profileSwitch.js +53 -0
  292. package/lib/quoxon/promptHistory.js +41 -0
  293. package/lib/quoxon/providers.js +48 -0
  294. package/lib/quoxon/qart.js +20 -51
  295. package/lib/quoxon/qdragon.js +410 -0
  296. package/lib/quoxon/qdragonFly.js +62 -0
  297. package/lib/quoxon/qdragonScenes.js +282 -0
  298. package/lib/quoxon/qeyes.js +213 -0
  299. package/lib/quoxon/qmotion.js +75 -37
  300. package/lib/quoxon/render.js +740 -124
  301. package/lib/quoxon/resend.js +72 -0
  302. package/lib/quoxon/roster.js +117 -17
  303. package/lib/quoxon/route.js +6 -5
  304. package/lib/quoxon/rows.js +954 -0
  305. package/lib/quoxon/rowsStore.js +80 -0
  306. package/lib/quoxon/screenContext.js +150 -0
  307. package/lib/quoxon/screenFrames.js +254 -0
  308. package/lib/quoxon/serverChip.js +42 -0
  309. package/lib/quoxon/session.js +182 -2
  310. package/lib/quoxon/sessionDead.js +17 -0
  311. package/lib/quoxon/signin.js +125 -0
  312. package/lib/quoxon/slash/agents.js +112 -0
  313. package/lib/quoxon/slash/argSources.js +65 -0
  314. package/lib/quoxon/slash/args.js +65 -0
  315. package/lib/quoxon/slash/automate.js +89 -0
  316. package/lib/quoxon/slash/broadcast.js +140 -0
  317. package/lib/quoxon/slash/broadcastRun.js +108 -0
  318. package/lib/quoxon/slash/card.js +30 -0
  319. package/lib/quoxon/slash/challenge.js +114 -0
  320. package/lib/quoxon/slash/cliCommand.js +94 -0
  321. package/lib/quoxon/slash/cliLeaves.json +1023 -0
  322. package/lib/quoxon/slash/compact.js +1 -1
  323. package/lib/quoxon/slash/confirm.js +30 -0
  324. package/lib/quoxon/slash/crystallize.js +69 -0
  325. package/lib/quoxon/slash/dashboard.js +136 -0
  326. package/lib/quoxon/slash/draft.js +54 -0
  327. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  328. package/lib/quoxon/slash/hatWalk.js +206 -0
  329. package/lib/quoxon/slash/inverses.js +77 -0
  330. package/lib/quoxon/slash/killer.js +24 -0
  331. package/lib/quoxon/slash/killerApi.js +16 -0
  332. package/lib/quoxon/slash/ledger.js +13 -8
  333. package/lib/quoxon/slash/listView.js +3 -1
  334. package/lib/quoxon/slash/modelCmd.js +261 -0
  335. package/lib/quoxon/slash/palette.js +22 -8
  336. package/lib/quoxon/slash/paletteController.js +117 -7
  337. package/lib/quoxon/slash/panels.js +21 -6
  338. package/lib/quoxon/slash/picker.js +20 -2
  339. package/lib/quoxon/slash/qpeCmd.js +35 -0
  340. package/lib/quoxon/slash/restart.js +95 -13
  341. package/lib/quoxon/slash/resume.js +18 -1
  342. package/lib/quoxon/slash/rowCmd.js +157 -0
  343. package/lib/quoxon/slash/rowWalk.js +175 -0
  344. package/lib/quoxon/slash/settings.js +16 -6
  345. package/lib/quoxon/slash/spawn.js +177 -0
  346. package/lib/quoxon/slash/stepUp.js +274 -0
  347. package/lib/quoxon/slash/stepUpCard.js +60 -0
  348. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  349. package/lib/quoxon/slash/todoCmd.js +122 -0
  350. package/lib/quoxon/slash/undoApi.js +38 -0
  351. package/lib/quoxon/slash/undoCmd.js +49 -0
  352. package/lib/quoxon/slash/undoWish.js +15 -0
  353. package/lib/quoxon/slash/watch.js +164 -0
  354. package/lib/quoxon/slash/wish.js +23 -0
  355. package/lib/quoxon/slash/wishCmd.js +32 -0
  356. package/lib/quoxon/slotChat.js +300 -0
  357. package/lib/quoxon/slotFormat.js +195 -0
  358. package/lib/quoxon/slotRun.js +131 -0
  359. package/lib/quoxon/sound/mapping.js +2 -1
  360. package/lib/quoxon/stray.js +22 -0
  361. package/lib/quoxon/suggestCommand.js +90 -0
  362. package/lib/quoxon/term.js +46 -11
  363. package/lib/quoxon/textBuffer.js +176 -2
  364. package/lib/quoxon/theme.js +58 -25
  365. package/lib/quoxon/tips.js +433 -0
  366. package/lib/quoxon/toolCards.js +30 -0
  367. package/lib/quoxon/toolError.js +41 -0
  368. package/lib/quoxon/transcript.js +42 -4
  369. package/lib/quoxon/web/qface.js +18 -10
  370. package/lib/quoxon/welcome.js +1 -1
  371. package/lib/quoxon/whatsnew.js +136 -0
  372. package/lib/quoxon/whatsnew.jsonl +134 -0
  373. package/lib/quoxon/windowOrg.js +123 -0
  374. package/lib/server/devServers.js +137 -0
  375. package/lib/server/diag.js +129 -0
  376. package/lib/server/doctor.js +71 -0
  377. package/lib/server/install.js +149 -0
  378. package/lib/server/pluginEnable.js +161 -0
  379. package/lib/server/skew.js +56 -0
  380. package/lib/server/snapshot.js +131 -0
  381. package/lib/server/status.js +128 -0
  382. package/lib/server/sys.js +65 -0
  383. package/lib/server/uninstall.js +63 -0
  384. package/lib/server/update.js +234 -0
  385. package/lib/toolOutputShape.js +315 -0
  386. package/lib/tui/views/HyperionDeck.jsx +79 -0
  387. package/lib/tui/views/LucidDeck.jsx +209 -217
  388. package/package.json +5 -1
  389. package/vendor/dragonlab/SOURCE +4 -0
  390. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  391. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  392. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  393. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  394. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  395. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  396. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  397. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  398. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  399. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  400. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  401. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  402. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  403. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  404. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  405. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  406. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  407. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  408. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  409. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  410. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  411. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  412. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  413. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  414. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  415. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  416. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  417. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  418. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  419. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  420. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  421. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  422. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  423. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  424. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  425. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  426. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  427. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  428. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  429. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  430. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  431. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  432. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  433. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  434. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  435. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  436. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  437. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  438. package/vendor/dragonlab/lib/power-effects.js +294 -0
  439. package/vendor/dragonlab/lib/power-registry.js +74 -0
  440. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  441. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
  442. package/lib/core/websiteSession.js +0 -148
@@ -0,0 +1,27 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ /** Explicit local opt-in only. A display failure must not interrupt a chat. */
5
+ export async function loadChatCompanion(path, { output = process.stdout, warn = console.error, display = {} } = {}) {
6
+ if (!path || !output.isTTY) return null;
7
+ try {
8
+ const mod = await import(pathToFileURL(resolve(path)).href);
9
+ const c = await mod.createCompanion({ output, ...display });
10
+ if (!c || !['event', 'write', 'close', 'suspend'].every(k => typeof c[k] === 'function')) {
11
+ try { c?.close?.(); } catch {}
12
+ throw new Error('Expected createCompanion() with event/write/close/suspend');
13
+ }
14
+ let active = true;
15
+ const stop = async () => { active = false; try { await c.close(); } catch {} };
16
+ return {
17
+ event(e) { if (active) try { c.event(e); } catch { stop(); } },
18
+ write(text) { if (!active) output.write(text); else c.write(text); },
19
+ // Trace writes to stderr: relinquish the block first to prevent overlap.
20
+ suspend() { if (active) try { c.suspend(); } catch { stop(); } },
21
+ close: stop,
22
+ };
23
+ } catch (e) {
24
+ warn('Dragon companion unavailable; continuing normal chat: ' + e.message);
25
+ return null;
26
+ }
27
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Last conversation per (profile, cwd), so `quox chat --continue` keeps context without the user
3
+ * copying a session id. Best-effort: a read/write failure never breaks a chat turn.
4
+ */
5
+ import { homedir } from 'node:os';
6
+ import { join, dirname } from 'node:path';
7
+ import { readFileSync, writeFileSync, mkdirSync, readlinkSync } from 'node:fs';
8
+
9
+ const stateFile = () => process.env.QUOX_CHAT_STATE || join(homedir(), '.config', 'quox', 'chat-last.json');
10
+ /**
11
+ * Who is calling: $QUOX_SESSION when set, else this terminal's tty, else nothing (a headless
12
+ * caller with no session id falls back to profile + cwd only). Parallel terminals/sessions must
13
+ * never share a "last conversation".
14
+ */
15
+ export function callerScope(env = process.env) {
16
+ if (env.QUOX_SESSION) return `session:${String(env.QUOX_SESSION).slice(0, 64)}`;
17
+ for (const fd of [0, 1, 2]) {
18
+ try {
19
+ const t = readlinkSync(`/proc/self/fd/${fd}`);
20
+ if (/^\/dev\/(pts\/\d+|tty\w*)$/.test(t)) return `tty:${t}`;
21
+ } catch { /* not linux, or fd closed */ }
22
+ }
23
+ return '';
24
+ }
25
+ const keyOf = (profile, cwd) => `${profile || 'default'}|${cwd || process.cwd()}|${callerScope()}`;
26
+
27
+ function read() {
28
+ try { return JSON.parse(readFileSync(stateFile(), 'utf8')) || {}; } catch { return {}; }
29
+ }
30
+
31
+ export function lastConversation(profile, cwd) {
32
+ const v = read()[keyOf(profile, cwd)];
33
+ return typeof v === 'string' && v ? v : undefined;
34
+ }
35
+
36
+ export function saveLastConversation(profile, cwd, id) {
37
+ if (!id) return;
38
+ try {
39
+ const all = read();
40
+ all[keyOf(profile, cwd)] = id;
41
+ const f = stateFile();
42
+ mkdirSync(dirname(f), { recursive: true });
43
+ writeFileSync(f, JSON.stringify(all, null, 2));
44
+ } catch { /* best-effort */ }
45
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
- * quox claude / codex / gemini / run / sessions: governed tmux sessions. The runner
2
+ * quox claude / codex / run / sessions: governed tmux sessions. The runner
3
3
  * lives in core/governedSession.js; this file holds argument parsing and the
4
4
  * commander wiring. argv arrays only; user input never reaches a shell string.
5
5
  */
6
+ import { markForwardsRawArgs } from '../core/global-flags.js';
6
7
  import * as gs from '../core/governedSession.js';
7
8
  import { validateName, PROGRAMS } from '../core/governedSession.js';
8
9
  import { runClaudeLs } from '../core/claudeLs.js';
@@ -101,22 +102,6 @@ export const runClaude = ({ as, slot, ...opts } = {}, over) => gs.runGoverned({
101
102
  program: 'claude',
102
103
  ...(as || slot ? { preattach: { agent: as, slot, session: opts.name } } : {}),
103
104
  }, over);
104
- /**
105
- * quox gemini <args>: verbatim to `gemini`. Gemini resumes by index or
106
- * "latest" (`--resume latest`, `-r 5`), not by name, so the tmux session is
107
- * auto-named; reattach with `quox sessions attach NAME`.
108
- */
109
- export function parseGeminiArgs(raw) {
110
- let noTmux = false;
111
- const args = [];
112
- for (const a of raw) {
113
- if (a === '--no-tmux') noTmux = true;
114
- else args.push(a);
115
- }
116
- const resume = args.some((a) => a === '--resume' || a === '-r' || a.startsWith('--resume='));
117
- return { args, name: undefined, resume, noTmux };
118
- }
119
-
120
105
  /**
121
106
  * quox wrap <program> [args...]: wrap ANY command. Known programs use their
122
107
  * registry entry (so `quox wrap codex` == `quox codex`); unknown ones get the
@@ -134,12 +119,11 @@ export function parseRunArgs(raw) {
134
119
  }
135
120
 
136
121
  export function runRun({ bin, args = [], noTmux = false } = {}, over) {
137
- const known = ['claude', 'codex', 'gemini'].find((k) => PROGRAMS[k].bin === bin);
122
+ const known = ['claude', 'codex'].find((k) => PROGRAMS[k].bin === bin);
138
123
  if (known) return gs.runGoverned({ program: known, args, noTmux }, over);
139
124
  return gs.runGoverned({ program: 'run', bin, args, noTmux }, over);
140
125
  }
141
126
 
142
- export const runGemini = (opts, over) => gs.runGoverned({ ...opts, program: 'gemini' }, over);
143
127
  export const runCodex = (opts, over) => gs.runGoverned({ ...opts, program: 'codex' }, over);
144
128
  export const runTui = (opts, over) => gs.runGoverned({ ...opts, program: 'tui' }, over);
145
129
  export const runHelp = gs.runHelp;
@@ -178,8 +162,8 @@ export function registerClaudeCommands(program, ctx, over) {
178
162
  // `quox claude <args>` behaves exactly like `claude <args>`: every arg is
179
163
  // forwarded verbatim. Only --no-tmux is quox's own. Management lives in
180
164
  // `quox sessions` so it can never collide with claude's own arguments.
181
- program
182
- .command('claude')
165
+ markForwardsRawArgs(program
166
+ .command('claude'), 'forwards argv after the verb verbatim')
183
167
  .description('Run Claude Code (same arguments as claude) in a tmux session that survives sleep and drops')
184
168
  .argument('[args...]', 'passed to claude exactly as given (quox adds only --no-tmux, --as, --slot)')
185
169
  .addHelpText('after', `
@@ -205,8 +189,8 @@ Claude's own --agent is untouched and passes through to claude.`)
205
189
  const parsed = parseClaudeArgs(rest);
206
190
  process.exitCode = runClaude(parsed, over);
207
191
  });
208
- program
209
- .command('codex')
192
+ markForwardsRawArgs(program
193
+ .command('codex'), 'forwards argv after the verb verbatim')
210
194
  .description('Run Codex (same arguments as codex) in a tmux session that survives sleep and drops')
211
195
  .argument('[args...]', 'passed to codex exactly as given (quox adds only --no-tmux)')
212
196
  .helpOption(false)
@@ -218,20 +202,8 @@ Claude's own --agent is untouched and passes through to claude.`)
218
202
  const parsed = parseCodexArgs(i >= 0 ? raw.slice(i + 1) : []);
219
203
  process.exitCode = runCodex(parsed, over);
220
204
  });
221
- program
222
- .command('gemini')
223
- .description('Run Gemini CLI (same arguments as gemini) in a tmux session that survives sleep and drops')
224
- .argument('[args...]', 'passed to gemini exactly as given (quox adds only --no-tmux)')
225
- .helpOption(false)
226
- .allowUnknownOption()
227
- .allowExcessArguments()
228
- .action((_a, _o, cmd) => {
229
- const raw = (cmd.parent && cmd.parent.rawArgs) || process.argv;
230
- const i = raw.indexOf('gemini', 2);
231
- process.exitCode = runGemini(parseGeminiArgs(i >= 0 ? raw.slice(i + 1) : []), over);
232
- });
233
- program
234
- .command('wrap')
205
+ markForwardsRawArgs(program
206
+ .command('wrap'), 'forwards argv after the verb verbatim')
235
207
  .description('Run any program in a governed tmux session: quox wrap <program> [args...]')
236
208
  .argument('<program>', 'command name on PATH')
237
209
  .argument('[args...]', 'passed to the program exactly as given')
@@ -270,10 +242,35 @@ Claude's own --agent is untouched and passes through to claude.`)
270
242
  const json = !!o.json || program.opts().output === 'json';
271
243
  process.exitCode = await runSessionsOverview({ json, watch: !!o.watch, all: !!o.all }, { ...so, fetchIndex });
272
244
  });
245
+ // `quox active [sessions|slots|agents]`: one short summary of who is doing what (QUOXON-1208).
246
+ program.command('active [filter]')
247
+ .description('Who is doing what: one line per active worker, grouped working / waiting on you / idle; filter: sessions, slots, agents')
248
+ .option('--json', 'print the summary as JSON')
249
+ .option('--all', 'show every worker (default: top 10 per group)')
250
+ .option('--lines', 'the older one-line-per-worker layout with state and age columns')
251
+ .action(async (filter, o) => {
252
+ const so = (over && over.sessionsOverview) || {};
253
+ const fetchIndex = so.fetchIndex || (async () => {
254
+ const { client, auth } = ctx;
255
+ auth.requireAuth();
256
+ const ep = resolveEndpoint('v1.sessions.index');
257
+ const org = orgOrNull(auth);
258
+ return client.get(ep.path, { service: ep.service, retries: 0, ...(org ? { query: { org_id: org } } : {}) });
259
+ });
260
+ const json = !!o.json || program.opts().output === 'json';
261
+ const { runActive } = await import('../core/activeSummary.js');
262
+ const fetchSlots = so.fetchSlots || (async () => {
263
+ const { client, auth } = ctx;
264
+ const ep = resolveEndpoint('v1.slots.list');
265
+ const r = await client.get(ep.path, { service: ep.service, retries: 0 });
266
+ return Array.isArray(r) ? r : (r?.slots || r?.data || []);
267
+ });
268
+ process.exitCode = await runActive({ filter: filter || null, json, all: !!o.all, lines: !!o.lines }, { ...so, fetchIndex, fetchSlots });
269
+ });
273
270
  sessions.command('heartbeat <tmuxSession>', { hidden: true })
274
271
  .description('Heartbeat one wrapped tmux session to the org session index (started by quox claude)')
275
272
  .option('--loop', 'keep beating until the tmux session ends')
276
- .option('--backend <program>', 'claude, codex, gemini, ...')
273
+ .option('--backend <program>', 'claude, codex, ...')
277
274
  .option('--agent <agent>', 'agent the session started attached to (--as)')
278
275
  .option('--slot <slot>', 'slot the session started holding (--slot)')
279
276
  .action(async (name, o) => {
@@ -14,6 +14,7 @@ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
14
14
  import { resolveEndpoint } from '../api/endpoints.js';
15
15
  import { runSelftest } from './contactsSelftest.js';
16
16
  import { resolveOrgId } from './helpers.js';
17
+ import { atToSendAt } from './contactsSendAt.js';
17
18
 
18
19
  const KINDS = ['person', 'organisation'];
19
20
  const REASONS = ['dnc', 'do_not_market', 'bounced'];
@@ -38,6 +39,8 @@ async function call(fn, { forbidden } = {}) {
38
39
  return await fn();
39
40
  } catch (e) {
40
41
  if (e instanceof QuoxApiError) {
42
+ // A refusal that says why (scheduled/immediate send) is shown as said, never swapped for a generic line.
43
+ if ((e.status === 403 || e.status === 409) && e.body && typeof e.body.reason === 'string' && e.body.reason) throw new QuoxError(e.body.reason, 1);
41
44
  if (e.status === 403 && forbidden) throw new QuoxError(forbidden, 1);
42
45
  if (e.status === 404 && e.body && e.body.error === 'tombstone_not_found') {
43
46
  throw new QuoxError('No erasure tombstone exists for that identifier in this org.', 1);
@@ -107,13 +110,17 @@ export function registerContactsCommands(program, ctx) {
107
110
  const q = textQuery ?? options.q;
108
111
  if (q) query.q = q;
109
112
  if (options.status) query.status = options.status;
113
+ if (options.relationship) query.relationship = options.relationship;
110
114
  if (options.limit !== undefined) query.limit = Number(options.limit);
115
+ if (options.offset !== undefined) query.offset = Number(options.offset);
111
116
  const ep = resolveEndpoint('contactsq.contacts.list');
112
117
  const data = await call(() => client.get(ep.path, { service: ep.service, query }));
113
118
  if (isJson(program)) return printJson(data);
114
119
  const rows = (data.contacts || []).map(cardRow);
115
120
  formatOutput({ columns: CARD_COLS, headers: CARD_HEADS, rows }, { format: program.opts().output });
116
- announce('contacts list', `${rows.length} contacts`);
121
+ const total = Number.isInteger(data.total) ? data.total : rows.length;
122
+ const next = (data.offset || 0) + rows.length;
123
+ announce('contacts list', next < total ? `${rows.length} of ${total} contacts, next: --offset ${next}` : `${rows.length} of ${total} contacts`);
117
124
  };
118
125
 
119
126
  addNameOptions(contacts
@@ -185,8 +192,10 @@ export function registerContactsCommands(program, ctx) {
185
192
  .command('list')
186
193
  .description('List contacts')
187
194
  .option('--q <text>', 'name filter')
188
- .option('--status <s>', 'active | candidate | archived')
189
- .option('--limit <n>', 'max contacts')
195
+ .option('--status <s>', 'active | candidate | archived | all (default: everyone except archived)')
196
+ .option('--relationship <r>', 'primary relationship, e.g. customer | supplier | partner')
197
+ .option('--limit <n>', 'page size (default 25, max 100)')
198
+ .option('--offset <n>', 'skip this many (paging: the footer prints the next --offset)')
190
199
  .action((options) => list(options));
191
200
 
192
201
  contacts
@@ -373,6 +382,53 @@ export function registerContactsCommands(program, ctx) {
373
382
  announce('contacts brief', `${d.candidates_to_review} candidates, ${d.duplicates} duplicates`);
374
383
  });
375
384
 
385
+ contacts
386
+ .command('timeline <id>')
387
+ .description('Interaction history of a contact, newest first: surface, direction, outcome and time (refs only, no message bodies)')
388
+ .option('--limit <n>', 'max entries', '20')
389
+ .action(async (id, options) => {
390
+ auth.requireAuth();
391
+ const ep = resolveEndpoint('contactsq.timeline', { id });
392
+ const d = await call(() => client.get(ep.path, { service: ep.service, query: { limit: options.limit } }));
393
+ if (isJson(program)) return printJson(d);
394
+ const rows = d.timeline || [];
395
+ if (!rows.length) console.log('No interactions recorded.');
396
+ for (const t of rows) console.log(`${t.occurred_at || ''} ${t.surface || ''} ${t.direction || ''} ${t.outcome || ''}${t.agent_id ? ` (agent ${t.agent_id})` : ''}`);
397
+ announce('contacts timeline', `${rows.length} entries`);
398
+ });
399
+
400
+ contacts
401
+ .command('best-channel <id>')
402
+ .description('Which channel (voice, email or sms) may be used to reach a contact for a purpose, with every alternative and its reason codes. Read-only; never shows a number or address')
403
+ .option('--purpose <purpose>', 'service, transactional, marketing or research', 'service')
404
+ .action(async (id, options) => {
405
+ auth.requireAuth();
406
+ const ep = resolveEndpoint('contactsq.best_channel', { id });
407
+ const d = await call(() => client.get(ep.path, { service: ep.service, query: { purpose: options.purpose } }));
408
+ if (isJson(program)) return printJson(d);
409
+ console.log(`channel: ${d.channel || 'none allowed'}`);
410
+ for (const a of d.alternatives || []) console.log(` ${a.channel}: ${a.decision}${(a.reasons || []).length ? ` (${a.reasons.join(', ')})` : ''}`);
411
+ announce('contacts best-channel', d.channel || 'none allowed');
412
+ });
413
+
414
+ contacts
415
+ .command('count')
416
+ .description('How many contacts: the total (people and organisations, not archived or erased) with counts by kind, status and relationship. --kind narrows to person or organisation')
417
+ .option('--kind <kind>', 'person or organisation')
418
+ .action(async (options) => {
419
+ auth.requireAuth();
420
+ const ep = resolveEndpoint('contactsq.count');
421
+ const query = options.kind ? { kind: options.kind } : undefined;
422
+ const d = await call(() => client.get(ep.path, { service: ep.service, query }));
423
+ if (isJson(program)) return printJson(d);
424
+ const fmt = (o) => Object.entries(o || {}).map(([k, n]) => `${k} ${n}`).join(', ') || 'none';
425
+ formatDetail({
426
+ total: d.total, ...(d.kind ? { kind: d.kind } : {}), by_kind: fmt(d.by_kind), by_status: fmt(d.by_status),
427
+ by_relationship: fmt(d.by_relationship), candidates_to_review: d.candidates_to_review, generated_at: d.generated_at,
428
+ });
429
+ announce('contacts count', `${d.total} contacts${d.kind ? ` (${d.kind})` : ''}`);
430
+ });
431
+
376
432
  contacts
377
433
  .command('selftest')
378
434
  .description('Drive the read path (search, find, resolve, best channel, timeline, touchpoint, brief) against a demo contact. Never dials or sends. --agent runs it through the attached agent tool scope')
@@ -431,7 +487,7 @@ export function registerContactsCommands(program, ctx) {
431
487
  .description('Dial a contact by id through the phone bridge; you never see or type the number')
432
488
  .argument('<id>', 'contact id (find it with `quox contacts search`)')
433
489
  .option('--reason <text>', 'what the call is about (the agent opens with it)')
434
- .option('--purpose <p>', 'service | transactional | marketing | research', 'service')
490
+ .option('--purpose <p>', 'service | transactional | marketing | research | personal', 'service')
435
491
  .action(async (id, options) => {
436
492
  auth.requireAuth();
437
493
  const body = { contact_id: id, projection: 'dial', channel: 'voice', purpose: options.purpose };
@@ -446,6 +502,80 @@ export function registerContactsCommands(program, ctx) {
446
502
  announce('contacts call', `${l.id || '?'} queued for the phone bridge`);
447
503
  });
448
504
 
505
+ // ---- send: SMS or email now or at a time (the server delivers; you approve by running it) ----
506
+ const DEFAULT_TZ = 'Europe/London';
507
+ async function resolveContactArg(arg) {
508
+ if (/^ct_[a-z0-9_]+$/i.test(arg)) return arg;
509
+ const ep = resolveEndpoint('contactsq.search');
510
+ const data = await call(() => client.get(ep.path, { service: ep.service, query: { q: arg } }));
511
+ const c = data.candidates || [];
512
+ if (!c.length) throw new QuoxError(`No contact matches "${arg}". Try \`quox contacts search\`.`, 1);
513
+ if (data.ambiguous || c.length > 1) {
514
+ throw new QuoxError(`"${arg}" matches more than one contact (${c.slice(0, 5).map((x) => `${x.display_name} ${x.id}`).join('; ')}). Pass the contact id.`, 1);
515
+ }
516
+ return c[0].id;
517
+ }
518
+
519
+ contacts
520
+ .command('send')
521
+ .description('Send an SMS or email to a contact now, or at a time with --at (the server delivers it; the answer says exactly when)')
522
+ .argument('<contact>', 'contact id (ct_...) or an unambiguous name')
523
+ .argument('<text>', 'the message')
524
+ .option('--channel <c>', 'sms | email', 'sms')
525
+ .option('--at <time>', 'send later: ISO time with offset, or a clock time like 10am / 14:30 (next occurrence, in --tz)')
526
+ .option('--tz <zone>', 'IANA zone for --at', DEFAULT_TZ)
527
+ .option('--title <t>', 'subject (email)')
528
+ .option('--purpose <p>', 'service | transactional | marketing | research | personal', 'service')
529
+ .option('--reason <text>', 'why (recorded as a hash only)')
530
+ .action(async (contactArg, text, options) => {
531
+ auth.requireAuth();
532
+ checkEnum(options.channel, ['sms', 'email'], '--channel');
533
+ checkEnum(options.purpose, ['service', 'transactional', 'marketing', 'research', 'personal'], '--purpose');
534
+ const contactId = await resolveContactArg(contactArg);
535
+ const body = { contact_id: contactId, channel: options.channel, message: text, purpose: options.purpose };
536
+ if (options.title) body.title = options.title;
537
+ if (options.reason) body.reason = options.reason;
538
+ if (options.at) { body.send_at = atToSendAt(options.at, options.tz); body.timezone = options.tz; }
539
+ const ep = resolveEndpoint('contactsq.sends.create');
540
+ const data = await call(() => client.post(ep.path, { service: ep.service, body, retries: 0 }), {
541
+ forbidden: 'Blocked by contact policy, or you are not a human user. Run `quox contacts best-channel <id>` to see why.',
542
+ });
543
+ if (isJson(program)) return printJson(data);
544
+ if (data.job) {
545
+ formatDetail({ job: data.job.id, status: data.job.status, channel: data.job.channel, when: data.job.when, send_at: data.job.send_at });
546
+ return announce('contacts send', `scheduled ${data.job.id}: ${data.job.when}`);
547
+ }
548
+ if (data.status === 'sent') { formatDetail({ lease: data.lease_id, status: 'sent' }); return announce('contacts send', `${options.channel} sent`); }
549
+ formatDetail({ lease: data.lease_id, status: data.status, error: data.error, reason: data.reason });
550
+ announce('contacts send', `FAILED: ${data.reason || data.error}`);
551
+ process.exitCode = 1;
552
+ });
553
+
554
+ const scheduled = contacts.command('scheduled').description('Scheduled sends: what is queued, and cancel one before it goes out');
555
+ scheduled
556
+ .command('list')
557
+ .option('--status <s>', 'comma list of pending,sending,sent,failed,cancelled (default pending,sending)')
558
+ .action(async (options) => {
559
+ auth.requireAuth();
560
+ const ep = resolveEndpoint('contactsq.sends.scheduled');
561
+ const data = await call(() => client.get(ep.path, { service: ep.service, query: options.status ? { status: options.status } : {} }), { forbidden: 'Scheduled sends are for human users.' });
562
+ if (isJson(program)) return printJson(data);
563
+ const rows = (data.jobs || []).map((j) => ({ id: j.id, contact: j.contact_id, channel: j.channel, status: j.status, when: j.when || j.finished_at || '', outcome: j.outcome_reason || j.outcome_code || '' }));
564
+ formatOutput({ columns: ['id', 'contact', 'channel', 'status', 'when', 'outcome'], rows }, { format: program.opts().output });
565
+ announce('contacts scheduled list', `${rows.length} jobs`);
566
+ });
567
+ scheduled
568
+ .command('cancel')
569
+ .argument('<id>', 'scheduled send id (ss_...)')
570
+ .action(async (id) => {
571
+ auth.requireAuth();
572
+ const ep = resolveEndpoint('contactsq.sends.cancel', { id });
573
+ const data = await call(() => client.post(ep.path, { service: ep.service, body: {} }), { forbidden: 'Cancelling a scheduled send is for human users.' });
574
+ if (isJson(program)) return printJson(data);
575
+ formatDetail({ job: data.job.id, status: data.job.status });
576
+ announce('contacts scheduled cancel', data.job.id);
577
+ });
578
+
449
579
  contacts
450
580
  .command('leases')
451
581
  .description('Lease + use ledger for a contact (who was given access, when, why; never the values)')
@@ -0,0 +1,21 @@
1
+ import { QuoxError } from '../core/errors.js';
2
+
3
+ /** "10am", "10:30", "22:15" -> the next such wall-clock time in tz as a bare local ISO the server reads in that zone. ISO input passes through. */
4
+ export function atToSendAt(at, tz, nowMs = Date.now()) {
5
+ const raw = String(at).trim();
6
+ if (/^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(raw) || /(?:Z|[+-]\d{2}:?\d{2})$/i.test(raw)) return raw;
7
+ const m = /^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i.exec(raw);
8
+ if (!m) throw new QuoxError(`--at must be an ISO time (2026-10-25T10:00:00+00:00) or a clock time such as 10am or 14:30, got "${raw}"`, 2);
9
+ let h = Number(m[1]);
10
+ const mi = Number(m[2] || 0);
11
+ if (m[3]) { if (h < 1 || h > 12) throw new QuoxError('--at hour must be 1 to 12 with am/pm', 2); h = (h % 12) + (m[3].toLowerCase() === 'pm' ? 12 : 0); }
12
+ if (h > 23 || mi > 59) throw new QuoxError('--at is not a valid clock time', 2);
13
+ const parts = (ms) => Object.fromEntries(new Intl.DateTimeFormat('en-GB', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(ms)).map((p) => [p.type, p.value]));
14
+ const n = parts(nowMs);
15
+ const pad = (v) => String(v).padStart(2, '0');
16
+ let day = Date.UTC(Number(n.year), Number(n.month) - 1, Number(n.day));
17
+ if (h * 60 + mi <= Number(n.hour) * 60 + Number(n.minute)) day += 86400000; // already passed today: tomorrow
18
+ const d = new Date(day);
19
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${pad(h)}:${pad(mi)}`;
20
+ }
21
+
@@ -16,11 +16,29 @@ export function registerConversationCommands(program, ctx) {
16
16
 
17
17
  conv
18
18
  .command('list')
19
- .description('List conversations')
19
+ .description('List your chat conversations (newest first); --archive lists the imported conversation archive')
20
20
  .option('--limit <n>', 'Max results', '20')
21
- .option('--offset <n>', 'Skip results', '0')
21
+ .option('--offset <n>', 'Skip results (archive only)', '0')
22
+ .option('--archive', 'list the imported conversation archive instead of chat conversations')
22
23
  .action(async (opts) => {
23
24
  auth.requireAuth();
25
+ if (!opts.archive) {
26
+ const ep = resolveEndpoint('conversations.chat.list');
27
+ const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
28
+ const rows = data.conversations || [];
29
+ formatOutput({
30
+ columns: ['id', 'title', 'turns', 'agent', 'updated_at'],
31
+ headers: ['ID', 'Title', 'Turns', 'Agent', 'Updated'],
32
+ rows: rows.map((c) => ({
33
+ id: c.id,
34
+ title: (c.title || 'Untitled').slice(0, 50),
35
+ turns: c.turn_count ?? 0,
36
+ agent: c.agent || '-',
37
+ updated_at: c.updated_at ? new Date(c.updated_at).toLocaleString() : '-',
38
+ })),
39
+ }, { format: program.opts().output });
40
+ return;
41
+ }
24
42
  const query = { limit: opts.limit, offset: opts.offset };
25
43
  const data = await client.get(resolveEndpoint('conversations.list').path, { service: resolveEndpoint('conversations.list').service, query });
26
44
  const conversations = data.conversations || data || [];
@@ -39,9 +57,22 @@ export function registerConversationCommands(program, ctx) {
39
57
  conv
40
58
  .command('get')
41
59
  .argument('<id>', 'conversation ID')
42
- .description('Get conversation details')
43
- .action(async (id) => {
60
+ .description('Show a chat conversation\'s turns; --archive reads the imported conversation archive')
61
+ .option('--limit <n>', 'Max turns (chat only)', '50')
62
+ .option('--archive', 'read the imported conversation archive instead of chat conversations')
63
+ .action(async (id, opts) => {
44
64
  auth.requireAuth();
65
+ if (!opts.archive) {
66
+ const ep = resolveEndpoint('conversations.chat.turns', { id });
67
+ const data = await client.get(ep.path, { service: ep.service, query: { limit: opts.limit } });
68
+ const turns = data.turns || [];
69
+ formatOutput({
70
+ columns: ['ts', 'role', 'content'],
71
+ headers: ['Time', 'Role', 'Content'],
72
+ rows: turns.map((t) => ({ ts: t.ts ? new Date(t.ts).toLocaleString() : '-', role: t.role, content: t.content })),
73
+ }, { format: program.opts().output });
74
+ return;
75
+ }
45
76
  const data = await client.get(resolveEndpoint('conversations.get', { id }).path, { service: resolveEndpoint('conversations.get', { id }).service });
46
77
  formatDetail(data.conversation || data, { format: program.opts().output, title: 'Conversation' });
47
78
  });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `quox dashboard [page]`: open the web dashboard already signed in, via a one-time link
3
+ * (10 minutes, single use). Same engine as /dashboard in the Quox client (lib/quoxon/slash/dashboard.js).
4
+ * Over SSH it prints the link and does not open a browser. The link is a credential: it is printed
5
+ * to the terminal only, never logged.
6
+ */
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+ import { runDashboard, dashboardLines, PAGES } from '../quoxon/slash/dashboard.js';
9
+ import { supportsHyperlinks } from '../quoxon/markdown.js';
10
+ import { resolveDashboardBase, detectLanIp } from '../quoxon/hostUrls.js';
11
+ import { loadJourney } from '../quoxon/onboarding/journey.js';
12
+
13
+ /**
14
+ * Z2H-R4: the address to print, resolved exactly like /dashboard in the Quox client (api.js
15
+ * dashboardBase): server app URL, else a non-loopback profile URL, else the install finale's URL,
16
+ * else this machine's LAN IP. Over SSH a localhost link only works on the Quox box itself.
17
+ */
18
+ export async function commandDashboardBase({ client, config, journey = loadJourney, lanIp = detectLanIp() } = {}) {
19
+ const profileUrl = config.getActiveProfile?.()?.url || config.getBaseUrl();
20
+ let serverApp = null;
21
+ try { const ep = resolveEndpoint('admin.instance.basics.get'); const r = await client.get(ep.path, { service: ep.service, retries: 0 }); serverApp = r?.effective?.appBaseUrl || null; } catch { serverApp = null; }
22
+ let journeyUrl = null; try { journeyUrl = journey()?.dashboardUrl || null; } catch { journeyUrl = null; }
23
+ return resolveDashboardBase({ serverApp, profileUrl, journeyUrl, lanIp }).url;
24
+ }
25
+
26
+ export function registerDashboardCommands(program, ctx) {
27
+ const { client, auth, config, lanIp = detectLanIp, journey = loadJourney } = ctx;
28
+ program
29
+ .command('dashboard')
30
+ .description(`Open the web dashboard signed in (one-time link, 10 minutes). Pages: ${Object.keys(PAGES).join(', ')}`)
31
+ .argument('[page]', 'dashboard page to land on')
32
+ .action(async (page) => {
33
+ auth.requireAuth();
34
+ const baseUrl = await commandDashboardBase({ client, config, journey, lanIp: lanIp() });
35
+ const r = await runDashboard({ page, baseUrl, mint: () => { const ep = resolveEndpoint('auth.login-link'); return client.post(ep.path, { service: ep.service, body: {}, retries: 0 }); } });
36
+ if (program.opts().output === 'json') {
37
+ console.log(JSON.stringify({ kind: r.kind, url: r.url ?? null, opened: Boolean(r.opened), remote: Boolean(r.remote), error: r.error ?? null }, null, 2));
38
+ } else {
39
+ for (const l of dashboardLines(r, { hyperlinks: supportsHyperlinks() })) console.log(l.text);
40
+ }
41
+ if (r.kind === 'error') process.exitCode = 1;
42
+ });
43
+ }
@@ -15,17 +15,38 @@ export function registerDepartmentCommands(program, ctx) {
15
15
 
16
16
  dept
17
17
  .command('list')
18
- .description('List departments')
19
- .requiredOption('--division <divId>', 'division ID')
18
+ .description('List departments (all divisions unless --division; division ids: quox org divisions list)')
19
+ .option('--division <divId>', 'only this division')
20
20
  .action(async (options) => {
21
21
  auth.requireAuth();
22
22
  const orgId = resolveOrgId(auth, program);
23
- const data = await client.get(resolveEndpoint('divisions.departments.list', { orgId, division: options.division }).path, { service: resolveEndpoint('divisions.departments.list', { orgId, division: options.division }).service });
23
+ const listFor = async (division) => {
24
+ const ep = resolveEndpoint('divisions.departments.list', { orgId, division });
25
+ const data = await client.get(ep.path, { service: ep.service });
26
+ return data.departments || data || [];
27
+ };
28
+ if (options.division) {
29
+ formatOutput({
30
+ columns: ['id', 'name', 'description', 'created_at'],
31
+ headers: ['ID', 'Name', 'Description', 'Created'],
32
+ rows: await listFor(options.division),
33
+ }, { format: program.opts().output });
34
+ return;
35
+ }
36
+ // quox#922: no --division lists every division's departments, so the read
37
+ // needs no id the department group cannot show.
38
+ const divEp = resolveEndpoint('divisions.list', { orgId });
39
+ const divData = await client.get(divEp.path, { service: divEp.service });
40
+ const divisions = divData.divisions || divData || [];
41
+ const rows = [];
42
+ for (const d of divisions) {
43
+ for (const dept of await listFor(d.id)) rows.push({ ...dept, division: d.name || d.id });
44
+ }
24
45
  formatOutput({
25
- columns: ['id', 'name', 'description', 'created_at'],
26
- headers: ['ID', 'Name', 'Description', 'Created'],
27
- rows: data.departments || data,
28
- }, { format: program.opts().output });
46
+ columns: ['id', 'name', 'division', 'description', 'created_at'],
47
+ headers: ['ID', 'Name', 'Division', 'Description', 'Created'],
48
+ rows,
49
+ }, { format: program.opts().output, emptyMessage: 'No departments.' });
29
50
  });
30
51
 
31
52
  dept
@@ -164,10 +164,13 @@ export function registerDevResidentCommands(dev, ctx) {
164
164
  const slotName = resolveSlotName(options, hostId);
165
165
  const sessionName = options.sessionName || defaultSessionName(slotName);
166
166
 
167
+ let released = true;
168
+ let killed = true;
167
169
  try {
168
170
  const releaseEp = resolveEndpoint('v1.slots.release', { name: slotName });
169
171
  await client.post(releaseEp.path, { service: releaseEp.service, body: { org_id: orgId, force: true } });
170
172
  } catch (err) {
173
+ released = false;
171
174
  printWarning(`Releasing ${slotName} failed (continuing): ${err.message}`);
172
175
  }
173
176
 
@@ -175,10 +178,11 @@ export function registerDevResidentCommands(dev, ctx) {
175
178
  const killEp = resolveEndpoint('v1.dev.sessions.kill', { hostId, sessionId: sessionName, byName: true });
176
179
  await client.delete(killEp.path, { service: killEp.service });
177
180
  } catch (err) {
181
+ killed = false;
178
182
  printWarning(`Killing tmux session ${sessionName} on ${hostId} failed (continuing): ${err.message}`);
179
183
  }
180
184
 
181
- return { host_id: hostId, slot_name: slotName, session_name: sessionName };
185
+ return { host_id: hostId, slot_name: slotName, session_name: sessionName, released, killed };
182
186
  }
183
187
 
184
188
  resident
@@ -290,8 +294,16 @@ export function registerDevResidentCommands(dev, ctx) {
290
294
  .action(async (options) => {
291
295
  auth.requireAuth();
292
296
  const report = await releaseAndKill(options);
293
- formatDetail(report, { format: program.opts().output, title: `Resident retired on ${report.host_id}` });
294
- printSuccess(`Resident retired on ${report.host_id}: slot ${report.slot_name} released, session ${report.session_name} killed.`);
297
+ // #1018: only claim what happened, in the card title too; a retire that left the slot held or the session running failed.
298
+ const outcome = report.released && report.killed ? 'Resident retired'
299
+ : report.released || report.killed ? 'Resident retire incomplete' : 'Resident retire failed';
300
+ formatDetail(report, { format: program.opts().output, title: `${outcome} on ${report.host_id}` });
301
+ if (report.released && report.killed) {
302
+ printSuccess(`Resident retired on ${report.host_id}: slot ${report.slot_name} released, session ${report.session_name} killed.`);
303
+ } else {
304
+ printWarning(`Resident NOT fully retired on ${report.host_id}: slot ${report.slot_name} ${report.released ? 'released' : 'NOT released'}, session ${report.session_name} ${report.killed ? 'killed' : 'NOT killed'}.`);
305
+ process.exitCode = 1;
306
+ }
295
307
  });
296
308
 
297
309
  resident
@@ -29,7 +29,7 @@ import { resolveEndpoint } from '../api/endpoints.js';
29
29
  import { resolveOrgId } from './helpers.js';
30
30
  import { loadResidentProfile, expandHome, deriveSupervisorEnv } from '../core/residentAuth.js';
31
31
  import { createInitialPumpState, runPumpOnce } from '../core/residentPump.js';
32
- import { createInitialAlarmState, runWardenPass } from '../core/residentAlarm.js';
32
+ import { createInitialAlarmState, runWardenPass, sendAlarm } from '../core/residentAlarm.js';
33
33
  import {
34
34
  DEFAULT_SETTINGS,
35
35
  DEFAULT_IDLE_RECHECK_SECONDS,
@@ -344,7 +344,17 @@ export function registerDevSupervisorCommands(dev, ctx) {
344
344
  await client.post(ep.path, { service: ep.service, body: { ...fields, slot_name: slotName, org_id: orgId } });
345
345
  };
346
346
 
347
+ // GRIPE-797: one owner alert per auth-expiry episode, same transport
348
+ // order as the warden alarm (tg-send.sh first, slot say fallback).
349
+ const alertOwner = async (text) => {
350
+ const script = ctx.tgSendScriptPath || join(homedir(), '.claude', 'skills', 'telegram-owner', 'tg-send.sh');
351
+ await sendAlarm({
352
+ tgSend: async (session, t) => { await (ctx.execFileAsync || promisify(execFile))('bash', [script, session, t]); },
353
+ postSlotNotice: postReply, text, sessionName: `SUPERVISOR-${hostId}`, log,
354
+ });
355
+ };
347
356
  const commonPollArgs = {
357
+ alertOwner,
348
358
  hostId,
349
359
  slot: slotName,
350
360
  selfIdentity,
@@ -633,6 +643,8 @@ export function registerDevSupervisorCommands(dev, ctx) {
633
643
  }
634
644
  } catch (err) {
635
645
  log('error', `poll failed: ${err.message}`);
646
+ // #1018: a single pass that failed is a failed run, not a quiet success.
647
+ if (options.once) process.exitCode = 1;
636
648
  if (continuous && waitMode) {
637
649
  waitFailures += 1;
638
650
  if (waitFailures >= DEFAULT_WAIT_FAILURE_THRESHOLD) {