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
@@ -7,15 +7,27 @@
7
7
  * differs from what was asked, the client says so once (never a silent fallback).
8
8
  * null choice = auto (no provider in the body; the server's policy picks).
9
9
  *
10
- * Availability is the same source as /connect: the org's default QuoxVault credential per provider.
10
+ * Availability is the same source as /connect, /doctor and /capabilities (providers.js): the server's
11
+ * capabilities, with the org vault for detail.
11
12
  * Model ids mirror src/config/modelCatalog.js (openai + anthropic rows); an id not listed is passed
12
13
  * through with a note, the server decides whether it exists.
13
14
  */
14
- import { PROVIDERS as CONNECT, pickRow, offerText } from './connect.js';
15
+ import { PROVIDERS as CONNECT, offerText } from './connect.js';
16
+ import { providerState, SUBSCRIPTION_NAME } from './providers.js';
17
+
18
+ /**
19
+ * No hardcoded model lists for the hosted providers: OpenAI and the Claude API are listed live by the server
20
+ * (GET /api/v1/providers/<p>/models, 1 h server cache, refreshed by /model). The Claude SUBSCRIPTION has no
21
+ * key to list with, so it takes the Claude CLI's family aliases, which the CLI resolves to the newest model.
22
+ */
23
+ export const CLAUDE_FAMILIES = ['fable', 'opus', 'sonnet', 'haiku'];
15
24
 
16
25
  export const CHOICES = [
17
- { id: 'openai', provider: 'openai', name: 'OpenAI', vault: 'openai', connect: 'openai', models: ['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano'] },
18
- { id: 'anthropic', provider: 'anthropic', name: 'Claude API', vault: 'anthropic', connect: 'anthropic', models: ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'] },
26
+ { id: 'openai', provider: 'openai', name: 'OpenAI', vault: 'openai', connect: 'openai', discover: true, models: [] },
27
+ { id: 'anthropic', provider: 'anthropic', name: 'Claude API', vault: 'anthropic', connect: 'anthropic', discover: true, models: [] },
28
+ // R3: the server's signed-in Claude subscription (dev only). The stream asks for anthropic; with no Claude API
29
+ // key the server runs it through the claude CLI on that sign-in. `via` only tells the client which row it is.
30
+ { id: 'claude-cli', provider: 'anthropic', via: 'subscription', cap: 'claude_subscription', name: SUBSCRIPTION_NAME, aliases: CLAUDE_FAMILIES, models: [] },
19
31
  // B3: served by the chat stream; models are whatever that Ollama has installed (listed by the server).
20
32
  { id: 'ollama', provider: 'ollama', name: 'Ollama', vault: 'ollama', connect: 'ollama', dynamic: true, hint: 'http://<host>:11434', models: [] },
21
33
  // B5: OpenAI-compatible presets. Models come from the provider's own /models (api.compatModels).
@@ -27,13 +39,16 @@ export const CHOICES = [
27
39
  ['custom', 'Custom', 'openai_compatible', false, ' https://<host>/v1']
28
40
  ].map(([id, name, vault, hosted, hint]) => ({ id, provider: id, name, vault, connect: id, dynamic: true, compat: true, hosted, selfHosted: !hosted, hint: hint.trim(), models: [] }))
29
41
  ];
30
- const ALIASES = { gpt: 'openai', chatgpt: 'openai', claude: 'anthropic', 'claude-api': 'anthropic', 'anthropic-api': 'anthropic', local: 'ollama', hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom', default: 'auto', policy: 'auto' };
42
+ const ALIASES = { 'claude-subscription': 'claude-cli', subscription: 'claude-cli', 'claude-code': 'claude-cli', gpt: 'openai', chatgpt: 'openai', claude: 'anthropic', 'claude-api': 'anthropic', 'anthropic-api': 'anthropic', local: 'ollama', hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom', default: 'auto', policy: 'auto' };
31
43
  const byProvider = (p) => CHOICES.find(c => c.provider === p) || null;
44
+ /** The CHOICES row a stored choice is (the subscription row shares provider anthropic). */
45
+ const rowOf = (ch) => (ch?.via === 'subscription' ? CHOICES.find(c => c.via === 'subscription') : byProvider(ch?.provider));
32
46
 
33
47
  /** Stored or flag value -> { provider, model? } | null. Only the two known fields survive. */
34
48
  export function normChoice(v) {
35
49
  if (!v || typeof v !== 'object' || typeof v.provider !== 'string' || !byProvider(v.provider)) return null;
36
50
  const out = typeof v.model === 'string' && v.model.trim() ? { provider: v.provider, model: v.model.trim() } : { provider: v.provider };
51
+ if (v.provider === 'anthropic' && v.via === 'subscription') out.via = 'subscription';
37
52
  // B3/B5: the Ollama / self-hosted host, for the status line before the first turn (turn.started.endpoint wins after).
38
53
  if ((v.provider === 'ollama' || byProvider(v.provider).selfHosted) && typeof v.host === 'string' && v.host.trim()) out.host = v.host.trim().slice(0, 120);
39
54
  // D3: prompt profile (compact | full); absent = the server default (compact on ollama / local presets, full elsewhere).
@@ -53,31 +68,46 @@ export function parseModelArg(arg) {
53
68
  if (id === 'auto') return { ok: true, choice: null };
54
69
  const c = CHOICES.find(x => x.id === id);
55
70
  if (!c) return { ok: false, error: `No provider called "${rawName}". Use ${CHOICES.map(x => x.id).join(', ')} or auto (/model lists them).` };
56
- const choice = model ? { provider: c.provider, model } : { provider: c.provider };
71
+ const resolved = model;
72
+ const choice = resolved ? { provider: c.provider, model: resolved } : { provider: c.provider };
73
+ if (c.via) choice.via = c.via;
57
74
  if (profile) choice.profile = profile;
58
75
  return { ok: true, choice };
59
76
  }
60
77
 
61
- /** -> 'connected' | 'stored' | 'missing' | 'unknown' (+ the row) for one choice. */
62
- function availability(res, c) {
63
- if (!res || !res.ok || !Array.isArray(res.rows)) return { state: 'unknown' };
64
- const { def, any } = pickRow(res.rows, c.vault);
65
- if (def) return { state: 'connected', row: def };
66
- if (any) return { state: 'stored', row: any };
67
- return { state: 'missing' };
68
- }
78
+ /** -> { state, row?, reason? } for one choice, from the shared provider status (providers.js). */
79
+ const availability = (res, c, caps) => providerState({ cap: c.cap || c.id, vault: c.vault }, caps, res);
80
+
81
+ /** QUOXON-FW6: a typed provider name or alias -> the CHOICES id, or null. */
82
+ export const providerId = (name) => {
83
+ const n = String(name || '').trim().toLowerCase();
84
+ const id = ALIASES[n] || n;
85
+ return CHOICES.some(c => c.id === id) ? id : null;
86
+ };
87
+ /** The CHOICES id a stored choice is (the subscription row shares provider anthropic). */
88
+ export const choiceId = (ch) => (ch ? rowOf(ch)?.id || null : null);
89
+ /** QUOXON-FW6: the CHOICES rows usable on this server now. Never lists a provider not connected. */
90
+ export const connectedChoices = (res, caps = null) =>
91
+ CHOICES.filter(c => c.served !== false && availability(res, c, caps).state === 'connected');
69
92
 
70
- const same = (a, b) => (a?.provider || null) === (b?.provider || null);
93
+ const same = (a, b) => (a?.provider || null) === (b?.provider || null) && (a?.via || null) === (b?.via || null);
71
94
 
72
- /** -> [[text, tone?]] lines for /model. */
73
- export function modelLines(res, current) {
95
+ /** -> [[text, tone?]] lines for /model. `caps` = api.capabilities() (the server's word on each provider). */
96
+ export function modelLines(res, current, caps = null) {
74
97
  const out = [['Model for your next turns (the server decides; turn.started shows what ran):']];
75
98
  const mark = (on) => (on ? '▸' : ' ');
76
99
  out.push([`${mark(!current)} auto server policy picks the provider and model · /model auto`]);
77
100
  for (const c of CHOICES) {
78
- const a = availability(res, c);
101
+ const a = availability(res, c, caps);
79
102
  const picked = same(current, c);
80
103
  const sel = picked && current.model ? ` · using ${current.model}` : '';
104
+ if (c.via === 'subscription') {
105
+ const api = availability(res, byProvider('anthropic'), caps).state === 'connected';
106
+ if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} ready on this server${sel} · /model claude-cli${api ? ' (the Claude API key is used first while it is connected)' : ''}`, 'ok']);
107
+ else if (a.state === 'unknown') out.push([`${mark(picked)} ? ${c.name} unknown (the server did not say)`, 'warn']);
108
+ else out.push([`${mark(picked)} ○ ${c.name} ${a.reason || 'not signed in on this server'} · sign in from the dashboard Terminal`]);
109
+ continue;
110
+ }
81
111
  if (c.served === false) {
82
112
  out.push([`${mark(picked)} ${c.name} ${a.state === 'connected' ? 'connected, but ' : ''}not served by the chat stream yet`]);
83
113
  continue;
@@ -85,17 +115,17 @@ export function modelLines(res, current) {
85
115
  if (c.dynamic) {
86
116
  if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected · ${a.row.label || a.row.id}${sel} · /model ${c.id} lists its ${c.compat ? '' : 'installed '}models`, 'ok']);
87
117
  else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
88
- else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`]);
118
+ else if (a.state === 'missing' || a.state === 'unavailable') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`]);
89
119
  else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
90
120
  continue;
91
121
  }
92
122
  const models = c.models.join(', ');
93
- if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected${sel} · /model ${c.id} [${models}]`, 'ok']);
123
+ if (a.state === 'connected') out.push([`${mark(picked)} ● ${c.name} connected${sel} · /model ${c.id} [${models || 'live list'}]`, 'ok']);
124
+ else if (a.state === 'unavailable') out.push([`${mark(picked)} ○ ${c.name} ${a.reason || 'not available on this server'}`]);
94
125
  else if (a.state === 'stored') out.push([`${mark(picked)} ◐ ${c.name} stored but not the default · quox vault set-default ${a.row.id}`, 'warn']);
95
126
  else if (a.state === 'missing') out.push([`${mark(picked)} ○ ${c.name} not connected · /connect ${c.connect}`]);
96
127
  else out.push([`${mark(picked)} ? ${c.name} unknown (vault not readable)${sel}`, 'warn']);
97
128
  }
98
- out.push([' ? Claude (subscription) dev-only · reached via auto or anthropic when no Claude API key is stored; no status route']);
99
129
  if (!res?.ok) out.push([` could not read the vault: ${res?.error || 'no answer'}`, 'warn']);
100
130
  return out;
101
131
  }
@@ -104,22 +134,76 @@ export function modelLines(res, current) {
104
134
  * The /model <arg> decision. -> { ok, choice?, text, tone? }. A provider the vault says is not usable is
105
135
  * refused (with the /connect offer), never quietly swapped for another.
106
136
  */
107
- export function selectModel(res, arg) {
137
+ export function selectModel(res, arg, caps = null) {
108
138
  const p = parseModelArg(arg);
109
139
  if (!p.ok) return { ok: false, text: p.error, tone: 'warn' };
110
140
  if (!p.choice) return { ok: true, choice: null, text: 'Model: auto. The server policy picks the provider and model.', tone: 'ok' };
111
- const c = byProvider(p.choice.provider);
141
+ const c = rowOf(p.choice);
112
142
  if (c.served === false) return { ok: false, text: `${c.name} is not served by the chat stream yet, so I can't use it here. /model lists what can.`, tone: 'warn' };
113
- const a = availability(res, c);
143
+ const a = availability(res, c, caps);
144
+ const label0 = p.choice.model ? `${c.id} ${p.choice.model}` : c.id;
145
+ if (c.via === 'subscription') {
146
+ if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label0}. Could not confirm the ${c.name} is signed in; the server will say if it is not.`, tone: 'warn' };
147
+ if (a.state !== 'connected') return { ok: false, text: `${c.name} is not ready on this server (${a.reason || 'not signed in'}). Sign in from the dashboard Terminal.`, tone: 'warn' };
148
+ const api = availability(res, byProvider('anthropic'), caps).state === 'connected';
149
+ return { ok: true, choice: p.choice, text: `Model: ${c.name}${p.choice.model ? ` · ${p.choice.model}` : ''}. ${api ? 'The Claude API key is connected, so the server uses it first.' : 'Your next turns ask for it.'}`, tone: api ? 'warn' : 'ok' };
150
+ }
151
+ if (c.id === 'anthropic' && a.state === 'missing' && availability(res, rowOf({ via: 'subscription' }), caps).state === 'connected') {
152
+ // what --provider anthropic does: no API key, so the server runs Claude on its signed-in subscription
153
+ return { ok: true, choice: { ...p.choice, via: 'subscription' }, text: `Model: ${label0}. The Claude API isn't connected, so your next turns run on the ${SUBSCRIPTION_NAME}.`, tone: 'warn' };
154
+ }
114
155
  if (c.dynamic && a.state === 'missing') return { ok: false, text: `${c.name} isn't connected, so I can't use it yet. Connect it: /connect ${c.connect}${c.hint ? ` ${c.hint}` : ''}`, tone: 'warn' };
115
156
  if (a.state === 'missing') return { ok: false, text: offerText(CONNECT[c.connect]), tone: 'warn' };
157
+ if (a.state === 'unavailable') return { ok: false, text: `${c.name} isn't available on this server${a.reason ? `: ${a.reason}` : ''}.`, tone: 'warn' };
116
158
  if (a.state === 'stored') return { ok: false, text: `${c.name} has a key stored, but not as the org default, so the server will not use it. Fix: quox vault set-default ${a.row.id}`, tone: 'warn' };
117
159
  const label = p.choice.model ? `${c.id} ${p.choice.model}` : c.id;
118
- const extra = p.choice.model && !c.dynamic && !c.models.includes(p.choice.model) ? ` ${p.choice.model} is not in this client's list; the server decides whether it exists.` : '';
119
- if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label}. Could not confirm ${c.name} is connected (vault not readable); the server will say if it is not.${extra}`, tone: 'warn' };
160
+ const extra = p.choice.model && !c.dynamic && !c.discover && !c.models.includes(p.choice.model) ? ` ${p.choice.model} is not in this client's list; the server decides whether it exists.` : '';
161
+ if (a.state === 'unknown') return { ok: true, choice: p.choice, text: `Model: ${label}. Could not confirm ${c.name} is connected (the server did not say); the server will say if it is not.${extra}`, tone: 'warn' };
120
162
  return { ok: true, choice: p.choice, text: `Model: ${label}. Your next turns ask for it.${extra}`, tone: extra ? 'warn' : 'ok' };
121
163
  }
122
164
 
165
+ /** Newest-first discovered rows -> ids. */
166
+ const ids = (res) => (res?.ok && Array.isArray(res.models) ? res.models.map(modelId).filter(Boolean) : []);
167
+
168
+ /**
169
+ * /model openai|anthropic [model] against the live list (res from api.providerModels(provider)).
170
+ * Match order: exact id, "latest" (newest listed), a family (opus, sonnet, mini, ...) or any prefix/substring ->
171
+ * the NEWEST match. No match -> a clear error with the list. Discovery failed -> any typed id is allowed with a
172
+ * plain note (the provider validates it on first use). -> { ok, choice?, text, tone, models?, current? }
173
+ */
174
+ export function selectDiscoveredModel(res, choice, row) {
175
+ const name = row.name;
176
+ const model = choice.model || null;
177
+ const list = ids(res);
178
+ const rows = res?.ok ? res.models : [];
179
+ if (!res?.ok || !list.length) {
180
+ const why = res?.message || res?.error || 'no answer';
181
+ if (!model) return { ok: true, choice, text: `Model: ${row.id}. Could not list ${name}'s models (${why}); the server uses its default.`, tone: 'warn' };
182
+ return { ok: true, choice, text: `Model: ${row.id} ${model}. Could not list ${name}'s models (${why}); ${model} is sent as typed and the provider will say if it does not exist.`, tone: 'warn' };
183
+ }
184
+ const newest = res.default_model || list[0];
185
+ if (!model) return { ok: true, choice, text: `Model: ${name} · ${newest} (the newest listed). Models below; pick one with /model ${row.id} <model|family|latest>.`, tone: 'ok', models: rows, current: newest };
186
+ const q = model.toLowerCase();
187
+ const hit = q === 'latest' ? list[0]
188
+ : list.find(i => i.toLowerCase() === q)
189
+ || rows.find(r => r.family && r.family === q)?.id
190
+ || list.find(i => i.toLowerCase().startsWith(q))
191
+ || list.find(i => i.toLowerCase().includes(q));
192
+ if (!hit) return { ok: false, text: `${model} is not in ${name}'s live list. Pick an id, a family (${[...new Set(rows.map(r => r.family).filter(Boolean))].slice(0, 6).join(', ') || 'see below'}) or latest.`, tone: 'warn', models: rows };
193
+ return { ok: true, choice: { ...choice, model: hit }, text: `Model: ${name} · ${hit}${hit !== model ? ` (from "${model}")` : ''}. Your next turns ask for it.`, tone: 'ok' };
194
+ }
195
+
196
+ /** Subscription path: a Claude family alias (the CLI resolves the newest) or a full claude-* id; anything else is an error with the aliases. */
197
+ export function selectSubscriptionModel(choice) {
198
+ const m = choice.model;
199
+ if (!m) return { ok: true, choice, text: 'Model: claude-cli. The server default applies (the newest Sonnet unless your org set CLAUDE_CLI_DEFAULT_MODEL).', tone: 'ok' };
200
+ const q = m.toLowerCase();
201
+ if (CLAUDE_FAMILIES.includes(q)) return { ok: true, choice: { ...choice, model: q }, text: `Model: claude-cli ${q} (the Claude CLI resolves the newest ${q}; turn.started shows what ran).`, tone: 'ok' };
202
+ if (q === 'latest') return { ok: true, choice: { provider: choice.provider, via: choice.via }, text: 'Model: claude-cli, the server default.', tone: 'ok' };
203
+ if (/^claude-[a-z0-9.\-]+$/i.test(m)) return { ok: true, choice, text: `Model: claude-cli ${m}. The CLI will say if it does not exist.`, tone: 'ok' };
204
+ return { ok: false, text: `Unknown Claude model "${m}". Use ${CLAUDE_FAMILIES.join(', ')} (the CLI picks the newest) or a full claude-* id.`, tone: 'warn' };
205
+ }
206
+
123
207
  /**
124
208
  * B3: /model ollama [model] against the server's installed-model list (res from api.ollamaModels()).
125
209
  * -> { ok, choice?, text, tone, rows? } ; rows = the installed list to show.
@@ -179,13 +263,35 @@ export function selectCompatModel(res, presetId, model) {
179
263
  return { ok: true, choice: withHost({ provider: c.provider, model }), text: `Model: ${c.name}${host ? ` · ${host}` : ''} · ${model}. Your next turns ask for it.`, tone: 'ok' };
180
264
  }
181
265
 
266
+ /**
267
+ * Z2H-3B: after a successful /connect, should the new provider become the active choice? Yes when the current
268
+ * selection is not a working connected provider (nothing connected, or a saved/phantom choice that is not connected),
269
+ * or when auto has no OTHER connected provider to pick. No provider is a silent default: with another working one
270
+ * active, nothing changes and the caller keeps its "Use it: /model <id>" hint.
271
+ * -> { adopt:true, choice, line } | { adopt:false }. `vault`/`caps` are re-read AFTER the connect.
272
+ */
273
+ export function adoptAfterConnect({ vault, caps, current, id, host = null, model = null }) {
274
+ const row = CHOICES.find(c => c.id === id);
275
+ if (!row) return { adopt: false };
276
+ const connected = (c) => availability(vault, c, caps).state === 'connected';
277
+ const cur = current ? rowOf(current) : null;
278
+ const curWorks = cur && cur.id !== row.id && connected(cur);
279
+ const othersConnected = CHOICES.some(c => c.id !== row.id && connected(c));
280
+ if (curWorks) return { adopt: false };
281
+ if (!current && othersConnected) return { adopt: false };
282
+ const choice = normChoice({ provider: row.provider, ...(model ? { model } : {}), ...(host ? { host } : {}) });
283
+ return { adopt: true, choice, line: `Using ${row.name}${model ? ` ${model}` : ''} now. /model to change.` };
284
+ }
285
+
182
286
  /** Status-line text: what the server said it ran, else what is selected; null for auto before any turn (keeps the line short). */
183
287
  export function modelLabel(ctx, choice) {
288
+ if (ctx?.model && ctx.provider === 'anthropic' && ctx.auth === 'subscription') return `claude-cli ${ctx.model}`;
184
289
  if (ctx?.model && ctx.provider === 'ollama') return `Ollama · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
185
290
  const cc = ctx?.provider ? byProvider(ctx.provider) : null;
186
291
  if (ctx?.model && cc?.compat) return cc.hosted ? `${cc.name} · ${ctx.model}` : `${cc.name} · ${ctx.endpoint || choice?.host || '?'} · ${ctx.model}`;
187
292
  if (ctx?.model) return ctx.provider ? `${ctx.provider} ${ctx.model}` : ctx.model;
188
293
  if (!choice) return null;
294
+ if (choice.via === 'subscription') return choice.model ? `claude-cli ${choice.model}` : 'claude-cli';
189
295
  if (choice.provider === 'ollama') return `Ollama · ${choice.host || '?'} · ${choice.model || 'default model'}`;
190
296
  const ch = byProvider(choice.provider);
191
297
  if (ch?.compat) return ch.hosted ? `${ch.name} · ${choice.model || 'pick a model'}` : `${ch.name} · ${choice.host || '?'} · ${choice.model || 'default model'}`;
@@ -0,0 +1,345 @@
1
+ /**
2
+ * QUOXON-FW6: model favourites and the default model. Pure helpers plus one quiet live-list reader.
3
+ *
4
+ * The preference is { favourites: [{ provider, model }], default: { provider, model } | null }.
5
+ * `provider` is a /model provider id (openai, anthropic, claude-cli, ollama, a compat preset).
6
+ * Persistence is the caller's (api.modelPrefs / api.setModelPrefs: the user's server-side
7
+ * preferences, with the profile as a cache). There is no model list in this file: ids come from
8
+ * the provider's own live list (liveModels) or from what the user typed. A typed id that is not on
9
+ * the live list is kept with a warning, never refused.
10
+ */
11
+ import { CHOICES, CLAUDE_FAMILIES, providerId } from './model.js';
12
+
13
+ export const MAX_FAVS = 20;
14
+ const nameOf = (id) => CHOICES.find((c) => c.id === id)?.name || id;
15
+
16
+ const cleanOne = (v) => {
17
+ const provider = providerId(v?.provider);
18
+ const model = typeof v?.model === 'string' ? v.model.trim().slice(0, 200) : '';
19
+ return provider && model && !/\s/.test(model) ? { provider, model } : null;
20
+ };
21
+ export const sameModel = (a, b) =>
22
+ Boolean(a && b) && a.provider === b.provider && a.model === b.model;
23
+
24
+ /** Anything stored or received -> a clean prefs object (bad rows dropped, duplicates removed). */
25
+ export function normPrefs(v) {
26
+ const favs = [];
27
+ for (const f of Array.isArray(v?.favourites) ? v.favourites : []) {
28
+ const c = cleanOne(f);
29
+ if (c && !favs.some((x) => sameModel(x, c)) && favs.length < MAX_FAVS) favs.push(c);
30
+ }
31
+ return { favourites: favs, default: cleanOne(v?.default) };
32
+ }
33
+
34
+ export const label = (m) => `${m.provider} ${m.model}`;
35
+
36
+ /** -> { prefs, added, text, tone }. */
37
+ export function addFav(prefs, provider, model) {
38
+ const c = cleanOne({ provider, model });
39
+ if (!c)
40
+ return { prefs, added: false, text: 'Usage: /model fav add <provider> <model>', tone: 'warn' };
41
+ const at = prefs.favourites.findIndex((f) => sameModel(f, c));
42
+ if (at >= 0)
43
+ return {
44
+ prefs,
45
+ added: false,
46
+ text: `${label(c)} is already favourite ${at + 1}.`,
47
+ tone: 'plain',
48
+ };
49
+ if (prefs.favourites.length >= MAX_FAVS)
50
+ return {
51
+ prefs,
52
+ added: false,
53
+ text: `You have ${MAX_FAVS} favourites already. /model fav rm <n> makes room.`,
54
+ tone: 'warn',
55
+ };
56
+ const next = { ...prefs, favourites: [...prefs.favourites, c] };
57
+ const n = next.favourites.length;
58
+ return {
59
+ prefs: next,
60
+ added: true,
61
+ text: `Added ${label(c)} as favourite ${n}. /model ${n} switches to it.`,
62
+ tone: 'ok',
63
+ };
64
+ }
65
+
66
+ /**
67
+ * ref = a 1-based number, or a model id (exact, "provider model", "provider/model", or a
68
+ * unique substring).
69
+ */
70
+ export function rmFav(prefs, ref) {
71
+ const q = String(ref ?? '')
72
+ .trim()
73
+ .toLowerCase();
74
+ const favs = prefs.favourites;
75
+ if (!q) return { prefs, removed: null, text: 'Usage: /model fav rm <n|model>', tone: 'warn' };
76
+ let idx = -1;
77
+ if (/^\d+$/.test(q)) idx = Number(q) - 1;
78
+ else {
79
+ const exact = favs
80
+ .map((f, i) => i)
81
+ .filter(
82
+ (i) =>
83
+ favs[i].model.toLowerCase() === q ||
84
+ label(favs[i]).toLowerCase() === q ||
85
+ `${favs[i].provider}/${favs[i].model}`.toLowerCase() === q,
86
+ );
87
+ const part = favs.map((f, i) => i).filter((i) => favs[i].model.toLowerCase().includes(q));
88
+ const hits = exact.length ? exact : part;
89
+ if (hits.length > 1)
90
+ return {
91
+ prefs,
92
+ removed: null,
93
+ text:
94
+ `"${ref}" matches ${hits.map((i) => `${i + 1} ${label(favs[i])}`).join(', ')}. ` +
95
+ 'Use the number.',
96
+ tone: 'warn',
97
+ };
98
+ idx = hits.length ? hits[0] : -1;
99
+ }
100
+ if (idx < 0 || idx >= favs.length)
101
+ return {
102
+ prefs,
103
+ removed: null,
104
+ text: favs.length
105
+ ? `No favourite "${ref}". /model fav lists them.`
106
+ : 'You have no favourites yet. /model fav add <provider> <model>',
107
+ tone: 'warn',
108
+ };
109
+ const removed = favs[idx];
110
+ return {
111
+ prefs: { ...prefs, favourites: favs.filter((_, i) => i !== idx) },
112
+ removed,
113
+ text: `Removed ${label(removed)}. The numbers after it moved up by one.`,
114
+ tone: 'ok',
115
+ };
116
+ }
117
+
118
+ export function setDefault(prefs, provider, model) {
119
+ const c = cleanOne({ provider, model });
120
+ if (!c)
121
+ return { prefs, ok: false, text: 'Usage: /model default <provider> <model>', tone: 'warn' };
122
+ return {
123
+ prefs: { ...prefs, default: c },
124
+ ok: true,
125
+ text:
126
+ `Default model: ${label(c)}. New sessions start on it when ${nameOf(c.provider)} is ` +
127
+ `connected. /model ${label(c)} uses it now.`,
128
+ tone: 'ok',
129
+ };
130
+ }
131
+ export const clearDefault = (prefs) => ({
132
+ prefs: { ...prefs, default: null },
133
+ text: 'Default model cleared. New sessions use the last choice, else the server policy.',
134
+ tone: 'ok',
135
+ });
136
+
137
+ /** Toggle one model in the favourites -> { prefs, added }. */
138
+ export function toggleFav(prefs, m) {
139
+ const i = prefs.favourites.findIndex((f) => sameModel(f, m));
140
+ if (i >= 0)
141
+ return {
142
+ prefs: { ...prefs, favourites: prefs.favourites.filter((_, k) => k !== i) },
143
+ added: false,
144
+ };
145
+ return {
146
+ prefs: {
147
+ ...prefs,
148
+ favourites: [...prefs.favourites, { provider: m.provider, model: m.model }].slice(
149
+ 0,
150
+ MAX_FAVS,
151
+ ),
152
+ },
153
+ added: true,
154
+ };
155
+ }
156
+
157
+ /** /model fav lines. `current` = the stored choice ({ provider, model?, via? }) or null. */
158
+ export function favLines(prefs, currentKey = null) {
159
+ if (!prefs.favourites.length)
160
+ return [
161
+ [
162
+ 'No favourites yet. Add one: /model fav add <provider> <model> ' +
163
+ '(or open /model and press f)',
164
+ ],
165
+ ];
166
+ const out = [['Favourite models (/model <n> switches):']];
167
+ prefs.favourites.forEach((f, i) => {
168
+ const marks = [
169
+ currentKey && sameModel(f, currentKey) ? 'current' : null,
170
+ prefs.default && sameModel(f, prefs.default) ? 'default' : null,
171
+ ].filter(Boolean);
172
+ out.push([` ${i + 1} ${label(f)}${marks.length ? ` (${marks.join(', ')})` : ''}`, 'plain']);
173
+ });
174
+ out.push([
175
+ prefs.default
176
+ ? `Default: ${label(prefs.default)}`
177
+ : 'No default model set. /model default <provider> <model>',
178
+ ]);
179
+ return out;
180
+ }
181
+
182
+ const FAV_WORDS = new Set(['fav', 'favs', 'favourite', 'favourites', 'favorite', 'favorites']);
183
+ const SUBS_ONE = { list: 'list', status: 'list' };
184
+
185
+ /**
186
+ * The words after /model -> what to do. kinds: chooser list use fav-list fav-add fav-rm
187
+ * default-show default-set default-clear error pass.
188
+ */
189
+ export function parseModelSub(arg) {
190
+ const w = String(arg || '')
191
+ .trim()
192
+ .split(/\s+/)
193
+ .filter(Boolean);
194
+ if (!w.length) return { kind: 'chooser' };
195
+ const first = w[0].toLowerCase();
196
+ if (SUBS_ONE[first] && w.length === 1) return { kind: SUBS_ONE[first] };
197
+ if (/^\d+$/.test(first) && w.length === 1) return { kind: 'use', n: Number(first) };
198
+ if (FAV_WORDS.has(first)) {
199
+ const sub = (w[1] || '').toLowerCase();
200
+ if (!sub) return { kind: 'fav-list' };
201
+ if (/^\d+$/.test(sub) && w.length === 2) return { kind: 'use', n: Number(sub) };
202
+ if (sub === 'add') {
203
+ if (w.length !== 4)
204
+ return {
205
+ kind: 'error',
206
+ text:
207
+ 'Usage: /model fav add <provider> <model> ' +
208
+ 'e.g. /model fav add openai gpt-5.6-terra',
209
+ };
210
+ return { kind: 'fav-add', provider: w[2], model: w[3] };
211
+ }
212
+ if (['rm', 'remove', 'del', 'delete'].includes(sub))
213
+ return w.length < 3
214
+ ? { kind: 'error', text: 'Usage: /model fav rm <n|model>' }
215
+ : { kind: 'fav-rm', ref: w.slice(2).join(' ') };
216
+ return {
217
+ kind: 'error',
218
+ text: 'Usage: /model fav [add <provider> <model> | rm <n|model> | <n>]',
219
+ };
220
+ }
221
+ if (first === 'default') {
222
+ if (w.length === 1) return { kind: 'default-show' };
223
+ if (w.length === 2 && ['clear', 'none', 'off', 'unset'].includes(w[1].toLowerCase()))
224
+ return { kind: 'default-clear' };
225
+ if (w.length === 3) return { kind: 'default-set', provider: w[1], model: w[2] };
226
+ return { kind: 'error', text: 'Usage: /model default [<provider> <model> | clear]' };
227
+ }
228
+ return { kind: 'pass' };
229
+ }
230
+
231
+ /**
232
+ * A provider's list response (the shape differs per provider) -> ids, newest first as the
233
+ * server gave them.
234
+ */
235
+ export function idsOf(choiceId, res) {
236
+ if (choiceId === 'claude-cli') return [...CLAUDE_FAMILIES];
237
+ if (!res?.ok || !Array.isArray(res.models)) return [];
238
+ const raw =
239
+ choiceId === 'ollama'
240
+ ? res.models.map((m) => (typeof m === 'string' ? m : m?.name))
241
+ : res.models.map((m) => (typeof m === 'string' ? m : m?.id));
242
+ return raw.filter((x) => typeof x === 'string' && x);
243
+ }
244
+
245
+ /**
246
+ * The live model ids for one provider, never throws. -> { ok, ids, def }. ok=false means
247
+ * "could not list" (the caller decides how quiet to be); ids is [] then.
248
+ */
249
+ export async function liveModels(api, id, { refresh = false } = {}) {
250
+ try {
251
+ if (id === 'claude-cli') return { ok: true, ids: [...CLAUDE_FAMILIES], def: null };
252
+ const row = CHOICES.find((c) => c.id === id);
253
+ if (!row) return { ok: false, ids: [], def: null };
254
+ let res = null;
255
+ if (id === 'ollama') res = await api.ollamaModels?.();
256
+ else if (row.compat) res = await api.compatModels?.(id);
257
+ else if (row.discover)
258
+ res = await api.providerModels?.(row.provider, refresh ? { refresh: true } : undefined);
259
+ if (!res?.ok) return { ok: false, ids: [], def: null };
260
+ return { ok: true, ids: idsOf(id, res), def: res.default_model || null };
261
+ } catch {
262
+ return { ok: false, ids: [], def: null };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Typed id vs the live list -> { model, known: true|false|null, note? }. null = no list to
268
+ * check against.
269
+ */
270
+ export function checkModelId(ids, typed, providerName = '') {
271
+ if (!ids?.length) return { model: typed, known: null };
272
+ if (ids.includes(typed)) return { model: typed, known: true };
273
+ const ci =
274
+ ids.find((i) => i.toLowerCase() === typed.toLowerCase()) ||
275
+ ids.find((i) => i === `${typed}:latest`);
276
+ if (ci) return { model: ci, known: true };
277
+ return {
278
+ model: typed,
279
+ known: false,
280
+ note:
281
+ `${typed} is not on ${providerName || 'the provider'}'s live list right now; ` +
282
+ 'kept as typed in case it is new.',
283
+ };
284
+ }
285
+
286
+ /* ---- the /model chooser ---------------------------------------------------------- */
287
+
288
+ /**
289
+ * Selectable items, in display order: favourites first (numbered by their place in the full
290
+ * list; ones on a provider that is not connected are left out), then each connected provider's
291
+ * models.
292
+ * groups: [{ id, name, ids, note? }] for CONNECTED providers only.
293
+ */
294
+ export function chooserItems(prefs, groups) {
295
+ const connected = new Set(groups.map((g) => g.id));
296
+ const items = [];
297
+ prefs.favourites.forEach((f, i) => {
298
+ if (connected.has(f.provider))
299
+ items.push({ kind: 'fav', n: i + 1, provider: f.provider, model: f.model });
300
+ });
301
+ for (const g of groups) {
302
+ if (!g.ids.length)
303
+ items.push({
304
+ kind: 'provider',
305
+ provider: g.id,
306
+ model: null,
307
+ name: g.name,
308
+ note: g.note || 'no model list; the server picks',
309
+ });
310
+ else
311
+ for (const id of g.ids)
312
+ items.push({ kind: 'model', provider: g.id, model: id, name: g.name });
313
+ }
314
+ return items;
315
+ }
316
+
317
+ /**
318
+ * One picker row ({ key, text, note? }) for an item. `current` = { provider, model } of the
319
+ * stored choice, or null.
320
+ */
321
+ export function chooserRow(item, prefs, current) {
322
+ if (item.kind === 'provider')
323
+ return { key: item.provider, text: item.name, note: item.note, dim: false };
324
+ const here = current && current.model ? sameModel(current, item) : false;
325
+ const marks = [
326
+ here ? 'current' : null,
327
+ prefs.default && sameModel(prefs.default, item) ? 'default' : null,
328
+ item.kind === 'model' && prefs.favourites.some((f) => sameModel(f, item)) ? '★' : null,
329
+ ].filter(Boolean);
330
+ const body = item.kind === 'fav' ? label(item) : item.model;
331
+ return {
332
+ key: item.kind === 'fav' ? `★ ${item.n}` : item.provider,
333
+ text: body + (marks.length ? ` ${marks.join(' · ')}` : ''),
334
+ };
335
+ }
336
+
337
+ /**
338
+ * The stored choice as { provider: <CHOICES id>, model } for marking; null when it carries no
339
+ * model.
340
+ */
341
+ export function currentKey(choice, idOf) {
342
+ if (!choice?.model) return null;
343
+ const provider = idOf(choice);
344
+ return provider ? { provider, model: choice.model } : null;
345
+ }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * Demand shapes: { every: ms } (repeat while declared) or { at: epochMs } (one wake at that time).
8
8
  */
9
+ import { normaliseLevel } from './level.js';
9
10
  export const FRAME_MS = 83; // ~12 fps for anything that moves
10
11
  export const SECOND_MS = 1000;
11
12
 
@@ -49,8 +50,8 @@ export class Clock {
49
50
  get armed() { return this.id != null; }
50
51
  }
51
52
 
52
- /** Motion is decoration: off under QUOX_REDUCED_MOTION=1, QUOX_TUI_LITE, or when not on a real terminal. Colour is separate (NO_COLOR). */
53
- export const reducedMotion = (env = process.env) => env.QUOX_REDUCED_MOTION === '1' || Boolean(env.QUOX_TUI_LITE);
53
+ /** Motion is decoration: off at the quiet level (QUOX_MOTION=quiet, QUOX_REDUCED_MOTION=1, QUOX_TUI_LITE), or when not on a real terminal. Colour is separate (NO_COLOR). */
54
+ export const reducedMotion = (env = process.env) => env.QUOX_REDUCED_MOTION === '1' || Boolean(env.QUOX_TUI_LITE) || normaliseLevel(env.QUOX_MOTION) === 'quiet';
54
55
  export const motionAllowed = (isTTY, env = process.env) => Boolean(isTTY) && !reducedMotion(env);
55
56
 
56
57
  /**