quoxcli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,654 @@
1
+ /**
2
+ * /connect: connections status, the secure key card's logic, the composer paste guard and the
3
+ * gentle "connect it now?" offer. Pure, apart from the api object passed in.
4
+ *
5
+ * Where a key goes (the path /chat/stream reads): the org's DEFAULT QuoxVault credential of the
6
+ * provider's integration type. The collector resolves OpenAI via auth /internal/resolve ->
7
+ * credentialResolver.resolveProviderKey(org, 'openai') -> getDefaultCredential(org, 'openai').
8
+ * The write is auth POST /api/orgs/:orgId/vault (alias of /integrations: SECRETS_WRITE gated,
9
+ * audited, AEE envelope carrying integration_type + label only). The label is "<Name> …last4",
10
+ * so the audit trail holds a fingerprint and never the key.
11
+ *
12
+ * The key itself lives only inside a secret box (closure). It is never dispatched, never put in
13
+ * the composer buffer or history, never sent to the chat stream, never written to a transcript.
14
+ */
15
+
16
+ import { askText } from './ask.js';
17
+ import { consentCovers, connectConsentNote, needsConsent, providerLabel } from './consent.js';
18
+ import { providerState, SUBSCRIPTION_NAME } from './providers.js';
19
+ import { isLoopbackUrl, detectLanIp } from './hostUrls.js';
20
+ import { cmd } from './palette.js';
21
+
22
+ export const PROVIDERS = {
23
+ openai: {
24
+ id: 'openai', type: 'openai', name: 'OpenAI', field: 'api_key', card: true, minLen: 20,
25
+ why: 'Quox needs an OpenAI key to use GPT models for your org.',
26
+ keyUrl: 'https://platform.openai.com/api-keys'
27
+ },
28
+ anthropic: {
29
+ id: 'anthropic', type: 'anthropic', name: 'Claude API', field: 'api_key', card: true, minLen: 20,
30
+ why: 'Quox needs an Anthropic API key to call Claude models for your org.',
31
+ keyUrl: 'https://console.anthropic.com/settings/keys'
32
+ },
33
+ // B3: no key; a server URL. /connect ollama <url> probes it server side, then saves.
34
+ ollama: { id: 'ollama', type: 'ollama', name: 'Ollama', card: false, url: true },
35
+ // B5: hosted OpenAI-compatible presets. Same secure key card; the third-party data path is stated on it.
36
+ openrouter: hosted('openrouter', 'OpenRouter', 'api_key', 'https://openrouter.ai/settings/keys',
37
+ "OpenRouter, which passes them to the model's provider"),
38
+ huggingface: hosted('huggingface', 'Hugging Face', 'api_token', 'https://huggingface.co/settings/tokens',
39
+ "Hugging Face, which passes them to the inference provider that serves the model"),
40
+ groq: hosted('groq', 'Groq', 'api_key', 'https://console.groq.com/keys', 'Groq'),
41
+ together: hosted('together', 'Together', 'api_key', 'https://api.together.ai/settings/api-keys', 'Together AI'),
42
+ // B5: self-hosted OpenAI-compatible servers: no key, a URL. Probed server side, then saved.
43
+ lmstudio: selfHosted('lmstudio', 'LM Studio', 1234),
44
+ vllm: selfHosted('vllm', 'vLLM', 8000),
45
+ // B5: any https OpenAI-compatible base URL, key optional (asked for only when the server needs one).
46
+ custom: {
47
+ id: 'custom', type: 'openai_compatible', name: 'Custom', field: 'api_key', card: false, url: true, custom: true, minLen: 8,
48
+ why: 'This endpoint wants an API key. It is stored in your org vault with the base URL.',
49
+ keyUrl: 'the provider of that endpoint',
50
+ disclosure: 'Custom endpoint: your prompts and tool results leave this server for that URL. Unless it is your own server, that is a third party. Your own AI data sharing consent applies (/consent).'
51
+ }
52
+ };
53
+ function hosted(id, name, field, keyUrl, who) {
54
+ return {
55
+ id, type: id, name, field, card: true, hosted: true, minLen: 20, keyUrl,
56
+ why: `Quox needs a ${name} key to use ${name} models for your org.`,
57
+ disclosure: `Hosted: your prompts and tool results leave this server for ${who}. Your own AI data sharing consent applies (/consent).`
58
+ };
59
+ }
60
+ function selfHosted(id, name, port) {
61
+ return { id, type: id, name, card: false, url: true, selfHosted: true, port };
62
+ }
63
+ const ALIASES = {
64
+ 'anthropic-api': 'anthropic', claude: 'anthropic', gpt: 'openai', chatgpt: 'openai',
65
+ hf: 'huggingface', 'lm-studio': 'lmstudio', 'openai-compatible': 'custom', openai_compatible: 'custom'
66
+ };
67
+ export const providerFor = (name) => PROVIDERS[ALIASES[String(name || '').toLowerCase()] || String(name || '').toLowerCase()] || null;
68
+
69
+ // ---- paste guard -------------------------------------------------------------------------------
70
+ const ANTHROPIC_RE = /\bsk-ant-[A-Za-z0-9_-]{16,}/;
71
+ const OPENROUTER_RE = /\bsk-or-(?:v1-)?[A-Za-z0-9_-]{16,}/;
72
+ const GROQ_RE = /\bgsk_[A-Za-z0-9]{20,}/;
73
+ const HF_RE = /\bhf_[A-Za-z0-9]{20,}/;
74
+ const OPENAI_RE = /\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}/;
75
+
76
+ /** -> { provider } when the text carries something API-key shaped, else null. */
77
+ export function looksLikeKey(text) {
78
+ const s = String(text || '');
79
+ if (ANTHROPIC_RE.test(s)) return { provider: 'anthropic' };
80
+ if (OPENROUTER_RE.test(s)) return { provider: 'openrouter' }; // must win over the generic sk- shape
81
+ if (GROQ_RE.test(s)) return { provider: 'groq' };
82
+ if (HF_RE.test(s)) return { provider: 'huggingface' };
83
+ if (OPENAI_RE.test(s)) return { provider: 'openai' };
84
+ return null;
85
+ }
86
+ export const keyWarning = (hit) => `That looks like an API key. I didn't send it. Use /connect ${hit?.provider || 'openai'} to store it safely.`;
87
+
88
+ // ---- masked input ------------------------------------------------------------------------------
89
+ /** Bullets while typing; after a paste, four bullets and the last 4 (enough to recognise the key). */
90
+ export function maskSecret(value, pasted) {
91
+ const v = String(value || '');
92
+ if (!v) return '';
93
+ if (pasted && v.length >= 8) return `••••${v.slice(-4)}`;
94
+ return '•'.repeat(Math.min(v.length, 32));
95
+ }
96
+
97
+ /** Holds the secret in a closure. Serialising the box shows nothing. */
98
+ export function createSecretBox() {
99
+ let v = ''; let pasted = false;
100
+ const clean = (t) => String(t || '').replace(/[\s\x00-\x1f\x7f]+/g, '');
101
+ return {
102
+ type(t) { v += clean(t); },
103
+ paste(t) { v += clean(t); pasted = true; },
104
+ backspace() { v = [...v].slice(0, -1).join(''); if (!v) pasted = false; },
105
+ clear() { v = ''; pasted = false; },
106
+ length: () => v.length,
107
+ last4: () => v.slice(-4),
108
+ mask: () => maskSecret(v, pasted),
109
+ /** Hands the value out once and empties the box. */
110
+ take() { const out = v; v = ''; pasted = false; return out; },
111
+ toJSON: () => '[secret]',
112
+ toString: () => '[secret]'
113
+ };
114
+ }
115
+
116
+ // ---- connections status ------------------------------------------------------------------------
117
+ const testTag = (r) => r.test_status === 'success' ? 'tested ✔' : r.test_status === 'failed' ? 'last test failed' : 'untested';
118
+ export const pickRow = (rows, type) => {
119
+ const of = rows.filter(r => r.integration_type === type);
120
+ return { def: of.find(r => r.is_default === 1 || r.is_default === true) || null, any: of[0] || null };
121
+ };
122
+
123
+ /**
124
+ * -> [[text, tone?]] lines for /connect. `res` is { ok:true, rows } from the vault list, or { ok:false, error };
125
+ * `caps` is the server's capabilities answer, the source of truth for whether each provider is usable
126
+ * (providers.js, shared with /model and /doctor). Anything neither can answer is "? unknown".
127
+ */
128
+ export function connectionLines(res, caps = null) {
129
+ const STATUS_PROVIDERS = [PROVIDERS.openai, PROVIDERS.anthropic, PROVIDERS.ollama, PROVIDERS.openrouter, PROVIDERS.huggingface, PROVIDERS.groq, PROVIDERS.together, PROVIDERS.lmstudio, PROVIDERS.vllm, PROVIDERS.custom];
130
+ const out = [];
131
+ const ok = res && res.ok && Array.isArray(res.rows);
132
+ const rows = ok ? res.rows : [];
133
+ out.push(['Connections for this org:']);
134
+ for (const p of STATUS_PROVIDERS) {
135
+ const hint = p.custom ? '/connect custom https://<host>/v1' : p.card ? `/connect ${p.id}` : p.url ? `/connect ${p.id} http://<host>:${p.port || 11434}` : `quox vault create --type ${p.type}`;
136
+ const st = providerState({ cap: p.id, vault: p.type }, caps, res);
137
+ const def = st.row;
138
+ if (st.state === 'unknown') out.push([`? ${p.name} unknown (vault not readable)`, 'warn']);
139
+ else if (st.state === 'connected' && def) out.push([`● ${p.name} connected · ${def.label || def.id} · ${testTag(def)}`, def.test_status === 'failed' ? 'warn' : 'ok']);
140
+ else if (st.state === 'connected') out.push([`● ${p.name} ready on this server (an instance-wide key, not in your org vault)`, 'ok']);
141
+ else if (st.state === 'stored') out.push([`◐ ${p.name} stored but not the default · quox vault set-default ${st.row.id}`, 'warn']);
142
+ else if (st.state === 'unavailable') out.push([`○ ${p.name} not available here${st.reason ? `: ${st.reason}` : ''}`]);
143
+ else out.push([`○ ${p.name} not connected · ${hint}`]);
144
+ }
145
+ const sub = providerState({ cap: 'claude_subscription' }, caps, res);
146
+ if (sub.state === 'connected') out.push([`● ${SUBSCRIPTION_NAME} ready on this server · /model claude-cli`, 'ok']);
147
+ else if (sub.state === 'unknown') out.push([`? ${SUBSCRIPTION_NAME} unknown (the server did not say)`, 'warn']);
148
+ else out.push([`○ ${SUBSCRIPTION_NAME} ${sub.reason || 'not signed in on this server'} · sign in from the dashboard Terminal`]);
149
+ if (!ok) { out.push([` could not read the vault: ${res?.error || 'no answer'}`, 'warn']); return out; }
150
+ const known = new Set(Object.values(PROVIDERS).map(p => p.type));
151
+ const others = new Map();
152
+ for (const r of rows) {
153
+ if (known.has(r.integration_type)) continue;
154
+ const cur = others.get(r.integration_type);
155
+ if (!cur || (r.is_default && !cur.is_default)) others.set(r.integration_type, r);
156
+ }
157
+ const list = [...others.values()];
158
+ if (list.length) {
159
+ out.push(['Integrations in your vault:']);
160
+ for (const r of list.slice(0, 12)) out.push([`● ${r.integration_type} ${r.label || r.id} · ${testTag(r)}`, r.test_status === 'failed' ? 'warn' : undefined]);
161
+ if (list.length > 12) out.push([` +${list.length - 12} more · quox vault list`]);
162
+ } else {
163
+ out.push(['No other integrations yet. Connect Slack, GitHub, email and more with quox vault create --type <type>.']);
164
+ }
165
+ out.push([' An instance-wide key set by your admin can also serve chat; it is not visible from here.']);
166
+ return out;
167
+ }
168
+
169
+ // ---- gentle offer ------------------------------------------------------------------------------
170
+ const MISSING = [
171
+ [/openai[^.]*?(api key )?(not configured|not connected|missing|no api key)/i, 'openai'],
172
+ [/(anthropic|claude api)[^.]*?(api key )?(not configured|not connected|missing|no api key)/i, 'anthropic']
173
+ ];
174
+ export const offerText = (p) => `Ⓠ ${p.name} isn't connected, so I can't use it yet. Connect it now? /connect ${p.id}`;
175
+
176
+ /** -> the one-line offer when an event says a provider is missing, else null. */
177
+ export function connectOfferFor(ev) {
178
+ if (!ev || typeof ev !== 'object') return null;
179
+ if (ev.available === false && ev.provider) { const p = providerFor(ev.provider); return p?.card ? offerText(p) : null; }
180
+ if (ev.type !== 'error' && ev.type !== 'turn.finished') return null;
181
+ const s = [ev.error, ev.message, ev.detail].filter(x => typeof x === 'string').join(' ');
182
+ for (const [re, id] of MISSING) if (re.test(s)) return offerText(PROVIDERS[id]);
183
+ return null;
184
+ }
185
+
186
+ // ---- save flow ---------------------------------------------------------------------------------
187
+ const scrubOut = (s, secret) => {
188
+ let t = String(s || '');
189
+ if (secret) t = t.split(secret).join('[key]');
190
+ return t.replace(/\b(?:sk-|gsk_|hf_)[A-Za-z0-9_-]{6,}/g, '[key]');
191
+ };
192
+
193
+ /**
194
+ * Server-side live test first (nothing persisted), then the governed vault write as the org default.
195
+ * -> { ok, provider, last4, text } ; text is safe to show and to record.
196
+ */
197
+ export async function connectFlow(api, providerId, secret, extra) {
198
+ const p = providerFor(providerId);
199
+ if (!p?.card && !p?.custom) return { ok: false, provider: providerId, text: `${providerId} cannot be connected from here.` };
200
+ const key = String(secret || '');
201
+ if (key.length < p.minLen) return { ok: false, provider: p.id, text: `That is too short to be a ${p.name} key. Paste the whole key, or Esc to cancel.` };
202
+ const last4 = key.slice(-4);
203
+ const credentials = { ...(p.custom && extra ? { base_url: extra.base_url, ...(extra.default_model ? { default_model: extra.default_model } : {}) } : {}), [p.field]: key };
204
+ const label = p.custom && extra?.host ? `${p.name} · ${extra.host} …${last4}` : `${p.name} …${last4}`;
205
+ let t;
206
+ try { t = await api.testProviderKey({ type: p.type, credentials }); }
207
+ catch (e) { t = { ok: false, unreachable: true, message: e?.message }; }
208
+ if (!t?.ok) {
209
+ const why = scrubOut(t?.message || 'no reason given', key);
210
+ return { ok: false, provider: p.id, last4, text: t?.unreachable || t?.status >= 500
211
+ ? `✖ Could not test the key (${why}). Nothing was saved.`
212
+ : `✖ ${p.name} rejected the key: ${why}. Nothing was saved.` };
213
+ }
214
+ let s;
215
+ try { s = await api.saveProviderKey({ type: p.type, label, credentials, isDefault: true }); }
216
+ catch (e) { s = { ok: false, status: e?.status, message: e?.message }; }
217
+ if (!s?.ok && s?.status === 409) return { ok: false, provider: p.id, last4, text: `This key is already in your vault as "${label}". /connect shows its status.` };
218
+ if (!s?.ok) {
219
+ const why = scrubOut(s?.message || 'failed', key);
220
+ const role = s?.status === 403 ? ' Storing org keys needs the secrets.write capability; ask an org admin.' : '';
221
+ return { ok: false, provider: p.id, last4, text: `✖ The key tested fine but was not saved (${s?.status ? `HTTP ${s.status}: ` : ''}${why}).${role}` };
222
+ }
223
+ return { ok: true, provider: p.id, last4, text: `✔ ${p.name} connected · key …${last4} · tested`, ...(p.disclosure ? { disclosure: p.disclosure } : {}) };
224
+ }
225
+
226
+ /** QUOXON-Z2H lane R: the one consent question asked after a cloud connect. Default No: cloud processing stays opt-in. */
227
+ export const consentAskLine = (type) => askText(`Allow Quox to send your conversations to ${providerLabel(type)}?`, { keys: 'y/N', who: 'Quox', action: `send your messages and any tool results to ${providerLabel(type)}, a third party, when it answers`, reason: 'Nothing is sent until you say yes; withdraw it any time in Settings > Privacy.' }) // no consent yet: nothing to chat with;
228
+
229
+ // ---- card controller ---------------------------------------------------------------------------
230
+ /**
231
+ * Owns the open card and its secret box. The app routes every input event here while the card is
232
+ * open (onEvent returns true = consumed), and renders view(). note(text, tone) shows a line; emit(a)
233
+ * dispatches a transcript-visible action (only connect.started / connect.saved, provider + last4).
234
+ */
235
+ export function createConnectController({ api, note, emit, refresh }) {
236
+ const box = createSecretBox();
237
+ let card = null;
238
+ const close = () => { box.clear(); card = null; refresh(); };
239
+ async function submit() {
240
+ if (!box.length()) { card.error = 'Paste your key first, or Esc to cancel.'; refresh(); return; }
241
+ card.phase = 'saving'; card.error = null; refresh();
242
+ const p = card.provider;
243
+ const extra = card.extra;
244
+ const r = await connectFlow(api, p.id, box.take(), extra);
245
+ if (!card) return;
246
+ if (r.ok) {
247
+ card = null; refresh();
248
+ emit({ type: 'connect.saved', provider: p.id, last4: r.last4 });
249
+ note(r.text, 'ok');
250
+ if (r.disclosure) note(r.disclosure, 'warn');
251
+ note('Your next turns can use it. Servers cache keys briefly, so allow up to a minute.');
252
+ await askConsent(p.type); // never meet the consent error cold: ask now, once
253
+ return;
254
+ }
255
+ card.phase = 'input'; card.error = r.text; refresh();
256
+ }
257
+ /**
258
+ * QUOXON-Z2H lane R: when `type` is a cloud provider and this user's OK is not in place, open the card on
259
+ * the consent question. y records it (the user's own direct act, POST /api/consent); anything else leaves
260
+ * it off and says how to give it later. -> true when the question opened.
261
+ */
262
+ async function askConsent(type) {
263
+ if (!needsConsent(type) || !api.consentStatus) return false;
264
+ let rec = null; try { rec = await api.consentStatus(); } catch { rec = null; }
265
+ if (consentCovers(rec, type)) return false;
266
+ if (!api.grantConsent) { note(connectConsentNote(type), 'warn'); return false; }
267
+ box.clear();
268
+ card = { provider: providerFor(type) || Object.values(PROVIDERS).find((x) => x.type === type) || { id: type, type, name: providerLabel(type) }, phase: 'consent', consentType: type, error: null, question: consentAskLine(type) };
269
+ refresh();
270
+ return true;
271
+ }
272
+ async function answerConsent(yes) {
273
+ const type = card.consentType;
274
+ if (!yes) { close(); note(`Left off: nothing goes to ${providerLabel(type)} until you say so. ${connectConsentNote(type)}`, 'warn'); return; }
275
+ card.phase = 'saving'; refresh();
276
+ try {
277
+ await api.grantConsent(type);
278
+ close(); note(`✔ Your OK is recorded: ${providerLabel(type)} can now answer. Withdraw it any time: quox consent list, or Settings > Privacy.`, 'ok');
279
+ } catch (e) {
280
+ close(); note(`Quox could not record your OK (${e?.message || e}). Nothing was sent. Try /consent grant ${type}.`, 'warn');
281
+ }
282
+ }
283
+ /**
284
+ * #1288: "Connect to <host>? It's on your private network." y/N. y runs offer.retry() (the save with a
285
+ * confirm for that exact origin) and hands its result to after(); n, Enter or Esc saves nothing.
286
+ */
287
+ function askPrivate(offer, after) {
288
+ box.clear();
289
+ card = {
290
+ provider: PROVIDERS.ollama || { id: 'ollama', type: 'ollama', name: 'Ollama' },
291
+ phase: 'private', error: null, privateOffer: offer, privateAfter: after,
292
+ question: askText(`Connect to ${offer.host}?`, { keys: 'y/N', who: 'Quox', reason: "It's on your private network", action: `save ${offer.origin} as your org's Ollama. Only this address is allowed, after the server checks it answers as Ollama. Needs an org admin.` })
293
+ };
294
+ refresh();
295
+ return true;
296
+ }
297
+ async function answerPrivate(yes) {
298
+ const { privateOffer: offer, privateAfter: after } = card;
299
+ if (!yes) { close(); note(`Left blocked: ${offer.host} was not connected. Nothing was saved.`); return; }
300
+ card.phase = 'saving'; refresh();
301
+ let r;
302
+ try { r = await offer.retry(); } catch (e) { r = { ok: false, lines: [[`✖ ${e?.message || e}. Nothing was saved.`, 'warn']] }; }
303
+ close();
304
+ for (const [t, tone] of r.lines || []) note(t, tone);
305
+ if (after) await after(r);
306
+ }
307
+ return {
308
+ askConsent,
309
+ askPrivate,
310
+ isOpen: () => Boolean(card),
311
+ view: () => (card ? { card: { ...card }, mask: box.mask() } : null),
312
+ /** -> true when a card opened. */
313
+ open(name) {
314
+ const p = providerFor(name);
315
+ if (!p) { note(`Nothing called "${name}" to connect. /connect lists what is available.`, 'warn'); return false; }
316
+ if (p.custom) { note('Custom endpoints take a URL first: /connect custom https://<host>/v1 [model]'); return false; }
317
+ if (!p.card) { note(p.url ? `${p.name} takes a server URL, not a key: /connect ${p.id} http://<host>:${p.port || 11434} [model]` : `${p.name} takes a server URL, not a key. Add it with: quox vault create --type ${p.type}`); return false; }
318
+ if (!api.testProviderKey || !api.saveProviderKey) { note('Connecting is not available in this mode.', 'warn'); return false; }
319
+ box.clear(); card = { provider: p, phase: 'input', error: null };
320
+ emit({ type: 'connect.started', provider: p.id });
321
+ refresh();
322
+ return true;
323
+ },
324
+ /** B5: open the key card for a custom endpoint that answered 401/403; extra = { base_url, host, default_model? }. */
325
+ openCustom(extra) {
326
+ if (!api.testProviderKey || !api.saveProviderKey) { note('Connecting is not available in this mode.', 'warn'); return false; }
327
+ box.clear(); card = { provider: PROVIDERS.custom, phase: 'input', error: null, extra, subtitle: extra.host };
328
+ emit({ type: 'connect.started', provider: 'custom' });
329
+ refresh();
330
+ return true;
331
+ },
332
+ onEvent(e) {
333
+ if (!card) return false;
334
+ if (card.phase === 'saving') return true;
335
+ if (card.phase === 'private') {
336
+ if (e.type === 'text' && /^y(es)?$/i.test(String(e.text).trim())) { answerPrivate(true); return true; }
337
+ if ((e.type === 'text' && /^n(o)?$/i.test(String(e.text).trim())) || (e.type === 'key' && ['enter', 'escape', 'ctrl-c'].includes(e.name))) { answerPrivate(false); return true; }
338
+ return true; // the question takes only y or n
339
+ }
340
+ if (card.phase === 'consent') {
341
+ if (e.type === 'text' && /^y(es)?$/i.test(String(e.text).trim())) { answerConsent(true); return true; }
342
+ if ((e.type === 'text' && /^n(o)?$/i.test(String(e.text).trim())) || (e.type === 'key' && ['enter', 'escape', 'ctrl-c'].includes(e.name))) { answerConsent(false); return true; }
343
+ return true; // the question takes only y or n
344
+ }
345
+ if (e.type === 'paste') { box.paste(e.text); card.error = null; refresh(); return true; }
346
+ if (e.type === 'text') { box.type(e.text); card.error = null; refresh(); return true; }
347
+ if (e.type !== 'key') return true;
348
+ if (e.name === 'escape' || e.name === 'ctrl-c') { close(); note('Cancelled. Nothing was stored.'); return true; }
349
+ if (e.name === 'enter') { submit(); return true; }
350
+ if (e.name === 'backspace') { box.backspace(); refresh(); return true; }
351
+ if (e.name === 'kill-line' || e.name === 'delete-word') { box.clear(); refresh(); return true; }
352
+ return true; // every other key is swallowed by the card
353
+ }
354
+ };
355
+ }
356
+
357
+
358
+ // ---- private-network refusal: explain it, offer the instance gate, retry once --------------------
359
+ /** The vault refused the base URL because the address is private (HTTP 400, SSRF guard). */
360
+ export const isPrivateNetworkError = (s) => s?.status === 400 && /private|internal/i.test(s?.message || '');
361
+
362
+ const WHO_CAN = [
363
+ 'Only an instance admin can allow this. Ask one to run:',
364
+ ' quox server config lan-targets on'
365
+ ];
366
+
367
+ /**
368
+ * The save failed because the address is on a private network. -> { ok:false, lines, lanOffer? }.
369
+ * An admin gets a card and { lanOffer } (the app keeps it for "/connect yes"); anyone else is told
370
+ * who can. `retry` re-runs the same connect once the gate is on.
371
+ */
372
+ /** The true scope of a saved private address: the instance gate is server-wide, not per connection. */
373
+ function privateScopeNote(origin) {
374
+ return [
375
+ [` ${origin} is a private address. This Quox server allows private-network`],
376
+ [' addresses for AI and monitoring connections. Turn off: quox server config lan-targets off']
377
+ ];
378
+ }
379
+
380
+ export async function lanGateCard(api, name, retry, { retried } = {}) {
381
+ const lines = [[`✖ ${name} is on a private network, so Quox did not save it.`, 'warn']];
382
+ const st = api.lanTargets ? await api.lanTargets() : null;
383
+ if (retried || st?.enabled) {
384
+ lines.push([' Private-network targets are already on, so Quox still refuses this address.']);
385
+ lines.push([' Loopback and link-local addresses are always blocked. Use the machine\'s LAN address.']);
386
+ return { ok: false, lines };
387
+ }
388
+ lines.push([' Quox blocks private addresses by default, so an AI cannot be pointed at internal services.']);
389
+ if (!api.setLanTargets || st?.status === 403) {
390
+ for (const l of WHO_CAN) lines.push([l, l.startsWith(' ') ? undefined : 'warn']);
391
+ return { ok: false, lines };
392
+ }
393
+ lines.push([' Allowing it applies to every AI and monitoring connection on this Quox server,']);
394
+ lines.push([' not just this one. Loopback and link-local stay blocked. Turn off later:']);
395
+ lines.push([' quox server config lan-targets off']);
396
+ lines.push([' Allow it and connect now? /connect yes (leave it blocked: /connect no)', 'warn']);
397
+ return { ok: false, lines, lanOffer: { name, retry } };
398
+ }
399
+
400
+ /** "/connect yes": set the gate, then run the connect once more. Never offers a second time. */
401
+ export async function confirmLanAllow(api, offer) {
402
+ if (!offer?.retry) return { ok: false, lines: [['Nothing to confirm. Run /connect again.', 'warn']] };
403
+ const set = await api.setLanTargets(true);
404
+ if (!set?.ok) {
405
+ if (set?.status === 403) return { ok: false, lines: [['✖ Only an instance admin can allow that. Nothing was changed.', 'warn'], ...WHO_CAN.map(l => [l])] };
406
+ return { ok: false, lines: [[`✖ Could not turn it on (${set?.status ? `HTTP ${set.status}: ` : ''}${set?.message || 'failed'}). Nothing was saved.`, 'warn']] };
407
+ }
408
+ const r = await offer.retry();
409
+ return { ...r, lines: [['✔ Private-network targets are on for this instance. Retrying…', 'ok'], ...r.lines] };
410
+ }
411
+
412
+ // ---- Ollama (B3): a server URL, no key -----------------------------------------------------------
413
+ /** "/connect ollama <url> [model]" rest -> { url, model } ; a bare host gets http:// and :11434. */
414
+ export function parseOllamaArg(rest) {
415
+ const [rawUrl, model] = String(rest || '').trim().split(/\s+/);
416
+ if (!rawUrl) return { url: null, model: null };
417
+ let url = rawUrl;
418
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
419
+ url = `http://${url}`;
420
+ try { const u = new URL(url); if (!u.port) { u.port = '11434'; url = u.toString().replace(/\/$/, ''); } } catch { /* the server says why */ }
421
+ }
422
+ return { url, model: model || null };
423
+ }
424
+
425
+ // Families whose Ollama builds accept tools (prefer one as the chat default).
426
+ const TOOL_FAMILIES = /^(qwen2\.5|qwen3|llama3\.[123]|mistral|mistral-nemo|mistral-small|command-r|hermes3|granite3|firefunction|nemotron|gpt-oss|smollm2)/i;
427
+ export const likelyToolCapable = (name) => TOOL_FAMILIES.test(String(name || ''));
428
+
429
+ /** The chat default: the asked model, else the first likely tool-capable one, else the first. */
430
+ export function pickOllamaDefault(models, asked) {
431
+ const names = (Array.isArray(models) ? models : []).map(m => (typeof m === 'string' ? m : m?.name)).filter(Boolean);
432
+ if (asked) {
433
+ if (names.includes(asked)) return asked;
434
+ return names.includes(`${asked}:latest`) ? `${asked}:latest` : null;
435
+ }
436
+ return names.find(likelyToolCapable) || names[0] || null;
437
+ }
438
+
439
+ const gb = (b) => (Number.isFinite(b) && b > 0 ? `${(b / 1e9).toFixed(1)} GB` : '');
440
+ /** -> [[text, tone?]] for an installed-model list. */
441
+ export function ollamaModelRows(models, current) {
442
+ const out = [];
443
+ for (const m of (models || []).slice(0, 20)) {
444
+ const facts = [m.parameter_size, gb(m.size), likelyToolCapable(m.name) ? 'tools' : null].filter(Boolean).join(' · ');
445
+ out.push([`${m.name === current ? '▸' : ' '} ${m.name}${facts ? ` ${facts}` : ''}`, m.name === current ? 'ok' : undefined]);
446
+ }
447
+ if ((models || []).length > 20) out.push([` +${models.length - 20} more`]);
448
+ return out;
449
+ }
450
+
451
+ /**
452
+ * Z2H-R4: how to make Ollama listen on the network. The official Linux installer runs it as a systemd
453
+ * service on 127.0.0.1, so `OLLAMA_HOST=0.0.0.0 ollama serve` fails there ("address already in use");
454
+ * the service override comes first, the plain serve only for a non-service run.
455
+ */
456
+ export const OLLAMA_LISTEN_LINES = [
457
+ 'Linux (installed as a service): ' + cmd('sudo systemctl edit ollama'),
458
+ ' add under [Service]: ' + cmd('Environment="OLLAMA_HOST=0.0.0.0"'),
459
+ ' then: ' + cmd('sudo systemctl restart ollama'),
460
+ 'Not a service: ' + cmd('OLLAMA_HOST=0.0.0.0 ollama serve')
461
+ ];
462
+
463
+ export const OLLAMA_REACH_HINT = [
464
+ 'On the Ollama machine, make it listen on the network:',
465
+ ...OLLAMA_LISTEN_LINES.map(l => ` ${l}`),
466
+ 'and allow port 11434 through its firewall. "localhost" is not reachable from the server; use the machine\'s LAN address.'
467
+ ];
468
+
469
+ /** Z2H-R3: the hint for a loopback Ollama URL, with this machine's LAN address filled in. */
470
+ export function ollamaLoopbackHint(lan) {
471
+ return [
472
+ 'The Quox server runs in Docker, so "localhost" there is the container, not this machine.',
473
+ 'Make Ollama listen on the network:',
474
+ ...OLLAMA_LISTEN_LINES.map(l => ` ${l}`),
475
+ `Then connect with this machine's LAN address: ${cmd(`/connect ollama http://${lan}:11434`)}`
476
+ ];
477
+ }
478
+
479
+ /** Z2H-R4: bare /connect ollama, the whole setup from nothing, with this machine's LAN address filled in. */
480
+ export function ollamaSetupCard(lan) {
481
+ return [
482
+ ['Ollama runs AI models on your own machine, no account or key. To set it up:'],
483
+ [' 1. Install it: ' + cmd('curl -fsSL https://ollama.com/install.sh | sh')],
484
+ [' 2. Get a model that can use tools: ' + cmd('ollama pull qwen2.5')],
485
+ [' 3. Let the Quox server reach it:'],
486
+ ...OLLAMA_LISTEN_LINES.map(l => [` ${l}`]),
487
+ [` 4. Connect: ${cmd(`/connect ollama http://${lan}:11434`)}`],
488
+ [' (Ollama on another machine? Use that machine\'s address.)']
489
+ ];
490
+ }
491
+
492
+ /**
493
+ * Server-side probe (GET <url>/api/tags; human org admin; nothing stored), then the governed vault
494
+ * write as the org default: { base_url, default_model, allow_private_origin? }. A private address is
495
+ * allow-listed only for the exact origin probed. -> { ok, lines: [[text, tone?]] }
496
+ */
497
+ export async function connectOllamaFlow(api, rest, { retried = false, confirmOrigin = null } = {}) {
498
+ const { url, model } = parseOllamaArg(rest);
499
+ if (!url) return { ok: false, lines: ollamaSetupCard((api.lanIp ? api.lanIp() : detectLanIp()) || '<lan-ip>') };
500
+ if (!api.ollamaProbe || !api.saveProviderKey) return { ok: false, lines: [['Connecting is not available in this mode.', 'warn']] };
501
+ const p = await api.ollamaProbe(url);
502
+ if (!p?.ok) {
503
+ if (p?.status === 403) return { ok: false, lines: [['✖ Connecting Ollama needs an org admin. Nothing was saved.', 'warn']] };
504
+ const lines = [[`✖ ${p?.message || 'Ollama could not be reached'}. Nothing was saved.`, 'warn']];
505
+ if (isLoopbackUrl(url)) { // Z2H-R3: the server is in Docker, so loopback is the container; give the exact command
506
+ const lan = (api.lanIp ? api.lanIp() : detectLanIp()) || '<lan-ip>';
507
+ for (const h of ollamaLoopbackHint(lan)) lines.push([` ${h}`]);
508
+ } else if (p?.error === 'ollama_unreachable' || p?.status === 502) for (const h of OLLAMA_REACH_HINT) lines.push([` ${h}`]);
509
+ return { ok: false, lines };
510
+ }
511
+ const models = Array.isArray(p.models) ? p.models : [];
512
+ const def = pickOllamaDefault(models, model);
513
+ if (model && !def) {
514
+ return { ok: false, lines: [[`✖ ${model} is not installed on ${p.host}. On that machine run: ollama pull ${model} Nothing was saved.`, 'warn'], ...ollamaModelRows(models)] };
515
+ }
516
+ const credentials = { base_url: p.base_url, ...(def ? { default_model: def } : {}), ...(p.private ? { allow_private_origin: p.origin } : {}) };
517
+ const label = `Ollama · ${p.host}`;
518
+ const confirm = confirmOrigin ? { confirmPrivateOrigin: confirmOrigin } : {};
519
+ let s = await api.saveProviderKey({ type: 'ollama', label, credentials, isDefault: true, ...confirm });
520
+ if (!s?.ok && s?.status === 409 && api.connections && api.updateProviderCredential) {
521
+ // Same host connected before: refresh that credential (URL, default model, allowance) instead.
522
+ const c = await api.connections();
523
+ const row = c?.ok ? c.rows.find(r => r.integration_type === 'ollama' && r.label === label) : null;
524
+ s = row ? await api.updateProviderCredential({ id: row.id, credentials, isDefault: true, ...confirm }) : s;
525
+ }
526
+ if (!s?.ok && s?.confirmable && !confirmOrigin) {
527
+ // #1288: the server lets an org admin allow THIS one private address for their own Ollama. Ask y/N.
528
+ const origin = s.origin || p.origin;
529
+ return { ok: false, lines: [], privateAsk: { host: p.host, origin, retry: () => connectOllamaFlow(api, rest, { retried: true, confirmOrigin: origin }) } };
530
+ }
531
+ if (!s?.ok && confirmOrigin) {
532
+ return { ok: false, lines: [[`✖ Ollama at ${p.host} was not saved (${s?.status ? `HTTP ${s.status}: ` : ''}${s?.message || 'failed'}).`, 'warn']] };
533
+ }
534
+ if (!s?.ok && isPrivateNetworkError(s)) {
535
+ return lanGateCard(api, `Ollama at ${p.host}`, () => connectOllamaFlow(api, rest, { retried: true }), { retried });
536
+ }
537
+ if (!s?.ok) {
538
+ const role = s?.status === 403 ? ' Storing org connections needs the secrets.write capability; ask an org admin.' : '';
539
+ return { ok: false, lines: [[`✖ Ollama answered but the connection was not saved (${s?.status ? `HTTP ${s.status}: ` : ''}${s?.message || 'failed'}).${role}`, 'warn']] };
540
+ }
541
+ const lines = [[`✔ Ollama connected · ${p.host} · ${models.length} model${models.length === 1 ? '' : 's'}${def ? ` · default ${def}` : ''}`, 'ok']];
542
+ if (p.private && confirmOrigin) lines.push([` ${confirmOrigin} is on your private network. Quox allows only this address, for this connection.`]);
543
+ else if (p.private) lines.push(...privateScopeNote(p.origin));
544
+ if (!models.length) lines.push([' No models installed yet. On that machine run: ollama pull qwen2.5', 'warn']);
545
+ else {
546
+ lines.push(...ollamaModelRows(models, def));
547
+ if (def && !likelyToolCapable(def)) lines.push([` ${def} may not support tools; turns then run without them. qwen2.5 or llama3.1 do.`, 'warn']);
548
+ }
549
+ lines.push([' Use it: /model ollama' + (def ? '' : ' <model>')]);
550
+ return { ok: true, host: p.host, model: def, lines };
551
+ }
552
+
553
+ // ---- B5: self-hosted OpenAI-compatible servers (LM Studio, vLLM) and custom endpoints ------------
554
+ const REACH_HINTS = {
555
+ lmstudio: [
556
+ 'On the LM Studio machine: in the Developer tab enable "Serve on Local Network" and start the server (port 1234).',
557
+ 'Allow that port through its firewall. "localhost" is not reachable from the server; use the machine\'s LAN address.'
558
+ ],
559
+ vllm: [
560
+ 'On the vLLM machine: start it listening on the network, vllm serve <model> --host 0.0.0.0 --port 8000',
561
+ 'and allow port 8000 through its firewall. "localhost" is not reachable from the server; use the machine\'s LAN address.'
562
+ ],
563
+ custom: ['Check the URL (it usually ends in /v1) and that the endpoint is reachable from the server.']
564
+ };
565
+ export const SELF_HOSTED_NOTE = 'Self-hosted: your prompts stay between this server and your machine. No third party is added.';
566
+
567
+ /** "/connect <lmstudio|vllm|custom> <url> [model]" rest -> { url, model, error? }. Bare host: http:// and the preset's port. */
568
+ export function parseCompatArg(presetId, rest) {
569
+ const p = PROVIDERS[presetId];
570
+ const [rawUrl, model] = String(rest || '').trim().split(/\s+/);
571
+ if (!rawUrl) return { url: null, model: null };
572
+ let url = rawUrl;
573
+ if (p?.custom) {
574
+ if (/^http:\/\//i.test(url)) return { url: null, model: null, error: 'Custom endpoints must be https://. For a server on your own network use /connect lmstudio or /connect vllm.' };
575
+ if (!/^https:\/\//i.test(url)) return { url: null, model: null, error: 'Custom endpoints take a full https:// base URL, for example /connect custom https://api.example.com/v1' };
576
+ return { url, model: model || null };
577
+ }
578
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
579
+ url = `http://${url}`;
580
+ try { const u = new URL(url); if (!u.port) { u.port = String(p?.port || 80); url = u.toString().replace(/\/$/, ''); } } catch { /* the server says why */ }
581
+ }
582
+ return { url, model: model || null };
583
+ }
584
+
585
+ /** Compact model rows for a compat /models list: up to 20, a tools tag when the server says so. */
586
+ export function compatModelRows(models, current) {
587
+ const out = [];
588
+ for (const m of (models || []).slice(0, 20)) {
589
+ const id = typeof m === 'string' ? m : m?.id;
590
+ if (!id) continue;
591
+ out.push([`${id === current ? '▸' : ' '} ${id}${m?.tools === true ? ' tools' : ''}`, id === current ? 'ok' : undefined]);
592
+ }
593
+ if ((models || []).length > 20) out.push([` +${models.length - 20} more`]);
594
+ return out;
595
+ }
596
+
597
+ /**
598
+ * /connect lmstudio|vllm|custom <url> [model]. Server-side probe (human org admin; nothing stored), then the
599
+ * governed vault write as the org default. Custom endpoints that answer 401/403 come back as
600
+ * { ok:false, needsKey:true, extra } so the app can open the key card carrying base_url.
601
+ * -> { ok, lines: [[text, tone?]], needsKey?, extra? }
602
+ */
603
+ export async function connectCompatFlow(api, presetId, rest, { retried = false } = {}) {
604
+ const p = PROVIDERS[presetId];
605
+ if (!p?.url || p.id === 'ollama') return { ok: false, lines: [[`${presetId} cannot be connected by URL.`, 'warn']] };
606
+ const { url, model, error } = parseCompatArg(p.id, rest);
607
+ const usage = p.custom ? '/connect custom https://<host>/v1 [model]' : `/connect ${p.id} http://<host>:${p.port} [model]`;
608
+ if (error) return { ok: false, lines: [[`✖ ${error}`, 'warn']] };
609
+ if (!url) return { ok: false, lines: [[`Usage: ${usage}`, 'warn']] };
610
+ if (!api.compatProbe || !api.saveProviderKey) return { ok: false, lines: [['Connecting is not available in this mode.', 'warn']] };
611
+ const pr = await api.compatProbe(p.id, url);
612
+ if (!pr?.ok) {
613
+ if (pr?.status === 403) return { ok: false, lines: [[`✖ Connecting ${p.name} needs an org admin. Nothing was saved.`, 'warn']] };
614
+ const lines = [[`✖ ${pr?.message || `${p.name} could not be reached`}. Nothing was saved.`, 'warn']];
615
+ if (pr?.error === 'provider_unreachable' || pr?.status === 502) for (const h of REACH_HINTS[p.id] || []) lines.push([` ${h}`]);
616
+ return { ok: false, lines };
617
+ }
618
+ const models = Array.isArray(pr.models) ? pr.models : [];
619
+ const ids = models.map(m => m.id);
620
+ if (pr.needs_key) {
621
+ if (p.custom) {
622
+ return { ok: false, needsKey: true, extra: { base_url: pr.base_url, host: pr.host, ...(model ? { default_model: model } : {}) },
623
+ lines: [[`${pr.host} answered but wants an API key. Opening the key card.`]] };
624
+ }
625
+ return { ok: false, lines: [[`✖ ${pr.host} wants an API key, which ${p.name} connections do not take here. Use /connect custom ${pr.base_url} instead. Nothing was saved.`, 'warn']] };
626
+ }
627
+ if (model && ids.length && !ids.includes(model)) {
628
+ return { ok: false, lines: [[`✖ ${model} is not served by ${pr.host}. Nothing was saved.`, 'warn'], ...compatModelRows(models)] };
629
+ }
630
+ const credentials = {
631
+ base_url: pr.base_url, ...(model ? { default_model: model } : {}),
632
+ ...(pr.private && !p.custom ? { allow_private_origin: pr.origin } : {})
633
+ };
634
+ const label = `${p.name} · ${pr.host}`;
635
+ let s = await api.saveProviderKey({ type: p.type, label, credentials, isDefault: true });
636
+ if (!s?.ok && s?.status === 409 && api.connections && api.updateProviderCredential) {
637
+ const c = await api.connections();
638
+ const row = c?.ok ? c.rows.find(r => r.integration_type === p.type && r.label === label) : null;
639
+ s = row ? await api.updateProviderCredential({ id: row.id, credentials, isDefault: true }) : s;
640
+ }
641
+ if (!s?.ok && isPrivateNetworkError(s)) {
642
+ return lanGateCard(api, `${p.name} at ${pr.host}`, () => connectCompatFlow(api, presetId, rest, { retried: true }), { retried });
643
+ }
644
+ if (!s?.ok) {
645
+ const role = s?.status === 403 ? ' Storing org connections needs the secrets.write capability; ask an org admin.' : '';
646
+ return { ok: false, lines: [[`✖ ${p.name} answered but the connection was not saved (${s?.status ? `HTTP ${s.status}: ` : ''}${s?.message || 'failed'}).${role}`, 'warn']] };
647
+ }
648
+ const lines = [[`✔ ${p.name} connected · ${pr.host} · ${models.length} model${models.length === 1 ? '' : 's'}${model ? ` · default ${model}` : ''}`, 'ok']];
649
+ if (pr.private && !p.custom) lines.push(...privateScopeNote(pr.origin));
650
+ lines.push(p.custom ? [` ${p.disclosure}`, 'warn'] : [` ${SELF_HOSTED_NOTE}`]);
651
+ lines.push(...compatModelRows(models, model));
652
+ lines.push([` Use it: /model ${p.id}${model ? '' : ' [model]'}`]);
653
+ return { ok: true, host: pr.host, model: model || null, lines };
654
+ }