quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -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/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,473 @@
1
+ /**
2
+ * QUOXON server calls. Thin wrapper over the CLI client so the app can be
3
+ * driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
4
+ */
5
+ import { resolveEndpoint } from '../api/endpoints.js';
6
+ import { buildStreamQuery } from '../commands/chat.js';
7
+ import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
8
+ import { recordHeldSlot } from '../commands/slot.js';
9
+ import { askGripe, settleGripe } from '../commands/gripe.js';
10
+ import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
11
+ import { normChoice } from './model.js';
12
+ import { createObjectsApi } from './objects/fetch.js';
13
+
14
+ const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
15
+ /** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
16
+ const steerReason = (label) => label === 'turn_finished' ? 'turn_finished' : (label === 'steer_unsupported' || label === 'unsupported') ? 'unsupported' : 'ambiguous';
17
+ const eventsPath = (c, t) => `/api/v1/conversations/${encodeURIComponent(c)}/turns/${encodeURIComponent(t)}/events`;
18
+ /** Reconnect backoff (ms) for a dropped durable turn; ~1 min in all, then the user is told /resume reattaches. */
19
+ export const REATTACH_BACKOFF_MS = [250, 500, 1000, 2000, 4000, 8000, 15000, 30000];
20
+
21
+ /**
22
+ * Drain one SSE connection of a turn into the consumer, exactly once by seq. Returns null when the turn
23
+ * finished (or the caller aborted), else { error } describing why the connection ended early.
24
+ * turn.finished arrives just after `done`: keep reading to the close, but never hang (3 s grace).
25
+ */
26
+ async function* readTurn(open, cur, signal) {
27
+ const inner = new AbortController();
28
+ const relay = () => inner.abort();
29
+ if (signal) { if (signal.aborted) inner.abort(); else signal.addEventListener('abort', relay, { once: true }); }
30
+ let grace = null;
31
+ try {
32
+ for await (const ev of open(inner.signal)) {
33
+ if (typeof ev?.seq === 'number') { if (ev.seq <= cur.lastSeq) continue; cur.lastSeq = ev.seq; }
34
+ if (ev?.type === 'turn.started' && ev.conversation_id) cur.conversationId = ev.conversation_id;
35
+ if (ev?.type === 'turn.finished') cur.finished = true;
36
+ yield ev;
37
+ if ((ev.type === 'done' || ev.type === 'error' || ev.type === 'turn.finished') && !grace) grace = setTimeout(() => inner.abort(), 3000);
38
+ if (ev.type === 'turn.finished') return null;
39
+ }
40
+ return signal?.aborted || grace ? null : { error: null }; // closed without turn.finished
41
+ } catch (e) {
42
+ if (signal?.aborted) return null;
43
+ if (grace && inner.signal.aborted) return null; // grace timeout, not a failure
44
+ if (e?.status) throw e; // the server answered with an HTTP error: not a dropped connection
45
+ return { error: e };
46
+ } finally { clearTimeout(grace); signal?.removeEventListener('abort', relay); inner.abort(); } // cancels the HTTP body
47
+ }
48
+ const sleepFor = (ms, signal) => new Promise((r) => { if (signal?.aborted) return r(); const t = setTimeout(done, ms); function done() { clearTimeout(t); signal?.removeEventListener?.('abort', done); r(); } signal?.addEventListener?.('abort', done, { once: true }); });
49
+ const listOf = (d, ...keys) => { for (const k of keys) if (Array.isArray(d?.[k])) return d[k]; return Array.isArray(d) ? d : null; };
50
+
51
+ export function createApi({ client, config, auth, reattachBackoff = REATTACH_BACKOFF_MS }) {
52
+ const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
53
+ const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
54
+ const listApprovals = async () => {
55
+ const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
56
+ const data = await client.get(ep.path, { service: ep.service, query: { status: 'open', kind: 'approval', limit: 20 } });
57
+ const items = data.items || data.data || data || [];
58
+ return Array.isArray(items) ? items : [];
59
+ };
60
+ const conv = (c, t) => `/api/v1/conversations/${encodeURIComponent(c)}/turns/${encodeURIComponent(t)}`;
61
+ /**
62
+ * After a drop: reconnect with backoff, replaying from the last seq we yielded. A 404/410 ends it with
63
+ * that error; running out of attempts throws err.code 'reattach_failed' (the turn may still be running).
64
+ */
65
+ async function* reattachLoop({ turnId, cur, signal, first, backoff = REATTACH_BACKOFF_MS }) {
66
+ let last = first;
67
+ for (let attempt = 1; attempt <= backoff.length; attempt++) {
68
+ if (signal?.aborted) return;
69
+ yield { type: 'reconnecting', turn_id: turnId, attempt, after: cur.lastSeq };
70
+ await sleepFor(backoff[attempt - 1], signal);
71
+ if (signal?.aborted) return;
72
+ let announced = false;
73
+ const before = cur.lastSeq;
74
+ let drop;
75
+ try {
76
+ drop = yield* readTurn((sig) => (async function* () {
77
+ for await (const ev of client.stream(eventsPath(cur.conversationId, turnId), { service: 'collector', query: { after: cur.lastSeq }, signal: sig, untilClose: true })) {
78
+ if (!announced) { announced = true; yield { type: 'resumed', turn_id: turnId, after: before }; }
79
+ yield ev;
80
+ }
81
+ })(), cur, signal);
82
+ } catch (e) {
83
+ if (e?.status === 404) { const err = new Error('The server no longer knows this turn (it may have been restarted).'); err.code = 'turn_not_found'; err.status = 404; throw err; }
84
+ if (e?.status === 410) { const err = new Error('This turn finished too long ago to replay; its answer is in the conversation history (/resume).'); err.code = 'turn_events_expired'; err.status = 410; throw err; }
85
+ if (e?.status === 401 || e?.status === 403) throw e;
86
+ drop = { error: e };
87
+ }
88
+ if (cur.finished || !drop || signal?.aborted) return;
89
+ last = drop.error || last;
90
+ if (cur.lastSeq > before) attempt = 0; // progress: start the backoff again
91
+ }
92
+ const err = new Error(`Lost the connection to the server${last?.message ? ` (${last.message})` : ''}. Quox keeps working on the turn; /resume reattaches.`);
93
+ err.code = 'reattach_failed';
94
+ throw err;
95
+ }
96
+ return {
97
+ orgId,
98
+ userId,
99
+ /** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
100
+ objects: createObjectsApi({ client, orgId }),
101
+ /** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
102
+ modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
103
+ setModelChoice(choice) {
104
+ const prof = config.getActiveProfile?.();
105
+ if (!prof) return false;
106
+ const c = normChoice(choice);
107
+ if (c) prof.chat_model = c; else delete prof.chat_model;
108
+ config.save?.();
109
+ return true;
110
+ },
111
+ /** Server-side QuoxSound setting (the dashboard's `soundProfile` preference), or null when unset. */
112
+ async soundSettings() {
113
+ const ep = resolveEndpoint('auth.me.list');
114
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
115
+ return (d?.user?.preferences ?? d?.preferences)?.soundProfile ?? null;
116
+ },
117
+ async setSoundSettings(value) {
118
+ const ep = resolveEndpoint('auth.me.update');
119
+ await client.patch(ep.path, { service: ep.service, body: { preferences: { soundProfile: value } } });
120
+ return true;
121
+ },
122
+ /** Local copy on the active CLI profile: offline start + fallback store. */
123
+ localSoundProfile() { try { return config.getActiveProfile?.()?.sound_profile || null; } catch { return null; } },
124
+ setLocalSoundProfile(p) { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.sound_profile = p; config.save?.(); return true; },
125
+ /**
126
+ * One turn. Durable by default (events v1.1, docs/QUOXON_EVENTS_v1_1.md): the server keeps the turn running
127
+ * if this connection drops, every event carries a `seq`, and a drop before turn.finished is followed by an
128
+ * automatic reattach (GET .../turns/:tid/events?after=<last seq>) with backoff. Events are yielded exactly once
129
+ * by seq. While reconnecting it yields { type:'reconnecting', attempt } and, once back, { type:'resumed', after }.
130
+ * A server without v1.1 (no seq on its events) gets the v1 behaviour: no reattach.
131
+ */
132
+ async *stream({ turnId, message, agent, conversationId, context, provider, model, promptProfile, durable = true }, signal) {
133
+ const body = buildStreamQuery({ queryId: turnId, message, agent: agent || 'quox', sessionId: conversationId, context });
134
+ body.turn_id = turnId;
135
+ if (durable) body.durable = true; // the server keeps the turn running when we disconnect (v1.1)
136
+ if (provider) body.provider = provider; // a request: the server policy decides, turn.started says what ran
137
+ if (provider && model) body.model = model;
138
+ if (promptProfile === 'compact' || promptProfile === 'full') body.prompt_profile = promptProfile; // D3: server default when absent
139
+ if (conversationId) body.conversation_id = conversationId;
140
+ const ep = resolveEndpoint('chat.stream');
141
+ const cur = { lastSeq: 0, finished: false, conversationId: conversationId || null };
142
+ const drop = yield* readTurn((sig) => client.stream(ep.path, { service: ep.service, body, signal: sig, untilClose: true }), cur, signal);
143
+ if (cur.finished || !drop || !durable || signal?.aborted) return;
144
+ if (!cur.lastSeq || !cur.conversationId) { if (drop.error) throw drop.error; return; } // v1 server: nothing to reattach to
145
+ yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
146
+ },
147
+ /**
148
+ * Reattach to a running (or just finished) turn and yield its events after `after`, exactly once, until
149
+ * turn.finished. Same reconnect behaviour as stream(). Throws err.code 'turn_not_found' (404) or
150
+ * 'turn_events_expired' (410).
151
+ */
152
+ async *reattach({ conversationId, turnId, after = 0 }, signal) {
153
+ const cur = { lastSeq: after, finished: false, conversationId };
154
+ const drop = yield* readTurn((sig) => client.stream(eventsPath(conversationId, turnId), { service: 'collector', query: { after }, signal: sig, untilClose: true }), cur, signal);
155
+ if (cur.finished || !drop || signal?.aborted) return;
156
+ yield* reattachLoop({ turnId, cur, signal, first: drop.error, backoff: reattachBackoff });
157
+ },
158
+ /** -> { running:true, turnId, lastSeq } | { running:false } | null (route missing or failed: never "not running"). */
159
+ async runningTurn(conversationId) {
160
+ if (!conversationId) return { running: false };
161
+ try {
162
+ const r = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/running`, { service: 'collector', retries: 0 });
163
+ if (r?.running === true && r.turn_id) return { running: true, turnId: r.turn_id, lastSeq: r.last_seq ?? null };
164
+ return r?.running === false ? { running: false } : null;
165
+ } catch { return null; }
166
+ },
167
+ /**
168
+ * -> { ok:true } | { ok:false, reason }. reason: 'unsupported' | 'turn_finished' (409, safe to queue)
169
+ * | 'denied' (403 steer_denied_by_policy) | 'ambiguous' (network, timeout, 5xx: may have been applied).
170
+ * NEVER retried automatically; steerId lets the server de-duplicate a deliberate resend.
171
+ */
172
+ async steer(conversationId, turnId, text, steerId = null) {
173
+ if (!conversationId) return { ok: false, reason: 'unsupported' };
174
+ try {
175
+ const body = steerId ? { text, steer_id: steerId } : { text };
176
+ const r = await client.post(`${conv(conversationId, turnId)}/steer`, { service: 'collector', body, retries: 0 });
177
+ if (r && r.queued === false) return { ok: false, reason: steerReason(r.reason) };
178
+ return { ok: true };
179
+ } catch (e) {
180
+ if (e?.status === 403) return { ok: false, reason: 'denied', message: e.code === 'steer_denied_by_policy' ? 'steer_denied_by_policy' : (e.message || 'forbidden') };
181
+ // Only say "finished" when the server says so; an unlabelled 409 may have been applied.
182
+ if (e?.status === 409) return { ok: false, reason: steerReason(e.code || e.detail?.code || e.detail?.reason || e.reason) };
183
+ if (e?.status === 404 || e?.status === 405) return { ok: false, reason: 'unsupported' };
184
+ return { ok: false, reason: 'ambiguous', message: e?.message || 'failed' };
185
+ }
186
+ },
187
+ /** -> { ok } ; ok:false means the server has no interrupt route (fall back to local abort) */
188
+ async interrupt(conversationId, turnId) {
189
+ if (!conversationId) return { ok: false };
190
+ try { await client.post(`${conv(conversationId, turnId)}/interrupt`, { service: 'collector', body: {} }); return { ok: true }; }
191
+ catch { return { ok: false }; }
192
+ },
193
+ /** -> { exists:true } | { exists:false } | { exists:null, reason } ; null = could not verify (never "not found"). */
194
+ async conversationExists(conversationId) {
195
+ try {
196
+ const r = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/turns`, { service: 'collector', query: { limit: 1 }, retries: 0 });
197
+ const turns = r?.turns;
198
+ if (!Array.isArray(turns)) return { exists: null, reason: 'unexpected response' };
199
+ return { exists: turns.length > 0 };
200
+ } catch (e) { return { exists: null, reason: e?.status === 404 || e?.status === 405 ? 'route not available' : (e?.message || 'failed') }; }
201
+ },
202
+ /**
203
+ * -> [{ agent_id, name }] ; every agent id you can talk to: the live core registry (the list `quox agent attach`
204
+ * validates against, with display names from the bundled roster) plus your org's own agents (`quox agent list`).
205
+ * Throws only when no source answered at all.
206
+ */
207
+ async agents() {
208
+ const roster = loadBundledAssistantsRoster();
209
+ const nameOf = new Map(roster.map(r => [r.id, r.name || r.shortName || r.id]));
210
+ const out = new Map(); let ok = 0; let lastErr = null;
211
+ try {
212
+ const ep = resolveEndpoint('v1.tools.agents.list');
213
+ const d = await client.get(ep.path, { service: ep.service, noAuth: true, retries: 0 });
214
+ for (const id of Object.keys(d?.agentTools || {})) out.set(id, { agent_id: id, name: nameOf.get(id) || id });
215
+ ok++;
216
+ } catch (e) { lastErr = e; for (const r of roster) out.set(r.id, { agent_id: r.id, name: nameOf.get(r.id) }); if (roster.length) ok++; }
217
+ try {
218
+ const ep = resolveEndpoint('agents.list', { orgId: orgId() });
219
+ const rows = listOf(await client.get(ep.path, { service: ep.service, query: { includeTools: 'false' }, retries: 0 }), 'agents', 'data', 'items');
220
+ for (const r of rows || []) if (r.agent_id && !out.has(r.agent_id)) out.set(r.agent_id, { agent_id: r.agent_id, name: r.name || r.agent_id });
221
+ if (rows) ok++;
222
+ } catch (e) { lastErr = lastErr || e; }
223
+ if (!ok) throw lastErr || new Error('no agent list available');
224
+ return [...out.values()];
225
+ },
226
+ /** -> saved turns of one of YOUR conversations, oldest first [{ id, role, content }]; [] when none or not yours. */
227
+ async conversationTurns(conversationId) {
228
+ const d = await client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/turns`, { service: 'collector', query: { limit: 200 }, retries: 0 });
229
+ const rows = listOf(d, 'turns');
230
+ if (!rows) throw new Error('unexpected response');
231
+ return rows;
232
+ },
233
+ /** -> the org-wide live index (GET /api/v1/sessions, QUOXON J1): { rows, groups, counts, sources }. */
234
+ async sessionsIndex() {
235
+ const d = await client.get('/api/v1/sessions', { service: 'collector', retries: 0 });
236
+ if (!d || !Array.isArray(d.groups)) throw new Error('unexpected response');
237
+ return d;
238
+ },
239
+ /** POST /api/v1/conversations/:id/compact (server-side summary; a model call, so a long timeout and never retried). */
240
+ async compactConversation(conversationId) {
241
+ return client.post(`/api/v1/conversations/${encodeURIComponent(conversationId)}/compact`, { service: 'collector', body: {}, retries: 0, timeout: 180000 });
242
+ },
243
+ /** GET /api/v1/conversations/:id/context -> { turns, compacted_turns, summary_present, summary_at, tokens, provider, model }. */
244
+ async conversationContext(conversationId) {
245
+ return client.get(`/api/v1/conversations/${encodeURIComponent(conversationId)}/context`, { service: 'collector', retries: 0 });
246
+ },
247
+ /** Persistent memory answers for this login (the same call as `quox memory stats`). -> { ok } | { ok:false, error }. */
248
+ async memoryStatus() {
249
+ try {
250
+ const ep = resolveEndpoint('memory.stats.list');
251
+ await client.get(ep.path, { service: ep.service, retries: 0 });
252
+ return { ok: true };
253
+ } catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status}` : (e?.message || 'failed') }; }
254
+ },
255
+ /** The `quox doctor` checks, run in this process. */
256
+ async doctorChecks() { const { runDoctorChecks } = await import('../commands/doctor.js'); return runDoctorChecks({ config }); },
257
+ serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
258
+ /** /config settings on the active profile. */
259
+ settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
260
+ saveSetting(id, value) {
261
+ try { const prof = config.getActiveProfile?.(); if (!prof) return false; prof.quoxon = { ...(prof.quoxon || {}), [id]: value }; config.save?.(); return true; } catch { return false; }
262
+ },
263
+ /** -> [{ id, title, updated_at, turn_count }] ; GET /api/v1/conversations (title is server-derived, no rename route). */
264
+ async conversations(limit = 20) {
265
+ const d = await client.get('/api/v1/conversations', { service: 'collector', query: { limit }, retries: 0 });
266
+ const rows = listOf(d, 'conversations');
267
+ if (!rows) throw new Error('unexpected response');
268
+ return rows;
269
+ },
270
+ /**
271
+ * Same call as `quox slot checkout <name> --create --session <s>` (cli/lib/commands/slot.js), then the same
272
+ * local held-slots record. The hold keeper and slot wait loop are NOT started here.
273
+ * -> { ok:true, slot } | { ok:false, message }.
274
+ */
275
+ /**
276
+ * /gripe: the same governed path as `quox gripe --yes` (the GRIPE agent over chat, then the deterministic
277
+ * file when the agent did not). The client asks the user first, so the agent is told to file without asking.
278
+ * -> { ok:true, response, url, superseded, notes } | { ok:false, message }.
279
+ */
280
+ async gripe(text, { yes = true } = {}) {
281
+ try {
282
+ const options = { yes };
283
+ const data = await askGripe({ client, complaint: text, yes, orgId: orgId() });
284
+ const r = await settleGripe({ client, data, complaint: text, options, orgId: orgId() });
285
+ return { ok: true, response: r.response, url: r.url, superseded: r.superseded, notes: r.notes };
286
+ } catch (e) {
287
+ return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
288
+ }
289
+ },
290
+ async checkoutSlot(name, session) {
291
+ const ep = resolveEndpoint('v1.slots.checkout', { name });
292
+ try {
293
+ const data = await client.post(ep.path, { service: ep.service, body: { holder_session: session, holder_kind: 'interactive', host_id: null, org_id: orgId(), force: false, create: true } });
294
+ recordHeldSlot(name, session);
295
+ return { ok: true, slot: data?.slot || data };
296
+ } catch (e) {
297
+ if (e?.status === 409) return { ok: false, message: e.detail?.assigned_to ? `slot ${name} is assigned to ${e.detail.assigned_to}` : `slot ${name} is held by ${e.detail?.holder_session || 'another session'}` };
298
+ return { ok: false, message: e?.message || 'failed' };
299
+ }
300
+ },
301
+ /** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
302
+ async skills() {
303
+ const ep = resolveEndpoint('quoxflow.skills.list');
304
+ const data = await client.get(ep.path, { service: ep.service, query: { orgId: orgId() } });
305
+ const rows = data?.data || data?.records || data;
306
+ return Array.isArray(rows) ? rows : [];
307
+ },
308
+ /** -> the full skill record, or null when unknown (404). */
309
+ async skill(id) {
310
+ const ep = resolveEndpoint('quoxflow.skills.get', { id });
311
+ try { const d = await client.get(ep.path, { service: ep.service, query: { orgId: orgId() } }); return d?.data || d; }
312
+ catch (e) { if (e?.status === 404) return null; throw e; }
313
+ },
314
+ /**
315
+ * Roster of what is running for this org -> { rows, sources:[{ id, label, state:'connected'|'unavailable'|'error', count, note }] }.
316
+ * Each source fails on its own; a source that cannot answer adds no rows (never a placeholder).
317
+ */
318
+ async roster(now = Date.now()) {
319
+ const src = (id, label, fn) => fn().then(
320
+ (rows) => ({ id, label, state: 'connected', rows, count: rows.length }),
321
+ (e) => ({ id, label, rows: [], count: 0, state: missing(e) ? 'unavailable' : 'error', note: missing(e) ? 'no list endpoint on this server' : (e?.message || 'failed') }));
322
+ const parts = await Promise.all([
323
+ src('approvals', 'pending approvals (inbox)', async () => (await listApprovals()).map(i => fromApproval(i, now)).filter(Boolean)),
324
+ src('objectives', 'objectives and loops (tasks)', async () => {
325
+ const ep = resolveEndpoint('objectives.list');
326
+ const d = await client.get(ep.path, { service: ep.service, query: { limit: 100 }, retries: 0 });
327
+ const l = listOf(d, 'objectives'); if (!l) throw new Error('unexpected response');
328
+ return l.map(o => fromObjective(o, now)).filter(Boolean);
329
+ }),
330
+ src('executions', 'workflow executions (quoxflow)', async () => {
331
+ const ep = resolveEndpoint('executions.list');
332
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
333
+ const l = listOf(d, 'data', 'items', 'executions'); if (!l) throw new Error('unexpected response');
334
+ return l.map(e => fromExecution(e, now, { userId: userId() })).filter(Boolean);
335
+ })
336
+ ]);
337
+ const unlisted = [
338
+ { id: 'agents', label: 'attached agents', state: 'unavailable', count: 0, note: 'no org-wide list endpoint' },
339
+ { id: 'devsessions', label: 'dev sessions', state: 'unavailable', count: 0, note: 'listed per host only (quox dev sessions <host>)' }
340
+ ];
341
+ return { rows: sortRows(parts.flatMap(p => p.rows)), sources: [...parts.map(({ rows, ...rest }) => rest), ...unlisted] };
342
+ },
343
+ /**
344
+ * Stop one roster row through the governed route that exists for it.
345
+ * -> { ok:true, message } | { ok:false, unavailable:true } | { ok:false, message }.
346
+ */
347
+ async stopRow(row) {
348
+ if (row.source === 'objectives' && row.kind === 'loop') {
349
+ try {
350
+ const ep = resolveEndpoint('objectives.halt', { id: row.id });
351
+ const r = await client.post(ep.path, { service: ep.service, body: { notes: 'via quoxon' }, retries: 0 });
352
+ return r?.status === 'halted' ? { ok: true, message: `Loop halted: ${row.name}` } : { ok: false, message: `Not halted${r?.reason ? ` (${r.reason})` : ''}` };
353
+ } catch (e) { return { ok: false, message: `halt refused: ${e?.message || 'failed'}` }; }
354
+ }
355
+ if (row.source === 'executions') {
356
+ try {
357
+ const ep = resolveEndpoint('runs.cancel', { orgId: orgId(), runId: row.id });
358
+ await client.post(ep.path, { service: ep.service, body: { reason: 'via quoxon' }, retries: 0 });
359
+ return { ok: true, message: `Cancel sent for ${row.name}` };
360
+ } catch (e) { return { ok: false, message: `cancel refused: ${e?.message || 'failed'}` }; }
361
+ }
362
+ return { ok: false, unavailable: true };
363
+ },
364
+ /** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
365
+ async observers() {
366
+ const part = async (fn) => { try { return { ok: true, v: await fn() }; } catch (e) { return { ok: false, unavailable: missing(e), error: e?.message || 'failed' }; } };
367
+ const stat = (id, label, name) => part(async () => {
368
+ const ep = resolveEndpoint(name); const d = await client.get(ep.path, { service: ep.service, retries: 0 });
369
+ return { id, label, text: d?.primaryMetric?.label || null, fields: d?.fields || {} };
370
+ });
371
+ const [sit, lessons, reflect, learn, insight] = await Promise.all([
372
+ part(async () => { const ep = resolveEndpoint('lucid.situation'); return client.get(ep.path, { service: ep.service, query: { since: 'last', surface: 'cli' }, retries: 0 }); }),
373
+ part(async () => { const ep = resolveEndpoint('lucid.lessons'); return client.get(ep.path, { service: ep.service, retries: 0 }); }),
374
+ stat('reflect', 'QuoxReflect', 'engine.reflect.stats'),
375
+ stat('learn', 'QuoxLearn', 'engine.learn.stats'),
376
+ stat('insight', 'QuoxInsight', 'engine.insight.stats')
377
+ ]);
378
+ return { situation: sit, lessons, learning: [reflect, learn, insight] };
379
+ },
380
+ async health(timeoutMs = 12000) {
381
+ const url = new URL('/health', config.getServiceUrl('collector'));
382
+ const ctrl = new AbortController();
383
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
384
+ try { const r = await fetch(url, { signal: ctrl.signal }); return { ok: r.ok, status: r.status, url: url.origin }; }
385
+ catch (e) {
386
+ const code = e.cause?.code || '';
387
+ return { ok: false, absent: /ECONNREFUSED|ENOTFOUND|EAI_AGAIN/.test(code), status: e.name === 'AbortError' ? 'slow to answer' : (code || e.message), url: url.origin };
388
+ } finally { clearTimeout(t); }
389
+ },
390
+ /** /capabilities (E1). -> { ok:true, capabilities } | { ok:false, status, missing?, error }. Never assumes. */
391
+ async capabilities() {
392
+ const ep = resolveEndpoint('v1.capabilities');
393
+ try {
394
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
395
+ const caps = listOf(d, 'capabilities');
396
+ return caps ? { ok: true, capabilities: caps, computedAt: d?.computed_at || null } : { ok: false, error: 'unexpected response' };
397
+ } catch (e) {
398
+ return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') };
399
+ }
400
+ },
401
+ /** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
402
+ async connections() {
403
+ try {
404
+ const ep = resolveEndpoint('vault.list', { orgId: orgId() });
405
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
406
+ const rows = listOf(d, 'credentials', 'data', 'items');
407
+ return rows ? { ok: true, rows } : { ok: false, error: 'unexpected response' };
408
+ } catch (e) { return { ok: false, error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
409
+ },
410
+ /**
411
+ * Server-side live test of a key, nothing persisted (auth test-credentials, SECRETS_WRITE).
412
+ * Sent once (no retries) and flagged sensitive so --debug never prints the body.
413
+ */
414
+ async testProviderKey({ type, credentials }) {
415
+ const ep = resolveEndpoint('vault.test-credentials', { orgId: orgId() });
416
+ try {
417
+ const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, credentials }, retries: 0, sensitive: true });
418
+ return { ok: r?.success === true, message: r?.message || null };
419
+ } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
420
+ },
421
+ /** The governed vault write (same route as `quox vault create`): audited, AEE envelope carries type + label only. */
422
+ async saveProviderKey({ type, label, credentials, isDefault }) {
423
+ const ep = resolveEndpoint('vault.create', { orgId: orgId() });
424
+ try {
425
+ const r = await client.post(ep.path, { service: ep.service, body: { integration_type: type, label, credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
426
+ return { ok: Boolean(r?.id), id: r?.id || null, message: r?.id ? null : 'unexpected response' };
427
+ } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
428
+ },
429
+ /** B3: refresh an existing vault credential (same governed route as `quox vault update`). */
430
+ async updateProviderCredential({ id, credentials, isDefault }) {
431
+ const ep = resolveEndpoint('vault.update', { orgId: orgId(), id });
432
+ try {
433
+ const r = await client.put(ep.path, { service: ep.service, body: { credentials, is_default: Boolean(isDefault) }, retries: 0, sensitive: true });
434
+ return { ok: Boolean(r && (r.id || r.success || r.credential)), id, message: null };
435
+ } catch (e) { return { ok: false, status: e?.status, message: e?.message || 'failed' }; }
436
+ },
437
+ /** B3: server-side probe of an Ollama URL (GET <url>/api/tags). Nothing stored. */
438
+ async ollamaProbe(baseUrl) {
439
+ const ep = resolveEndpoint('chat.providers.ollama.probe');
440
+ try { return await client.post(ep.path, { service: ep.service, body: { base_url: baseUrl }, retries: 0 }); }
441
+ catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
442
+ },
443
+ /** B3: the saved Ollama connection's installed models -> { ok, host, default_model, models } | { ok:false, error, message }. */
444
+ async ollamaModels() {
445
+ const ep = resolveEndpoint('chat.providers.ollama.models');
446
+ try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
447
+ catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
448
+ },
449
+ /** B5: server-side probe of a self-hosted / custom OpenAI-compatible base URL (lmstudio|vllm|custom). Nothing stored. */
450
+ async compatProbe(preset, baseUrl) {
451
+ const ep = resolveEndpoint('chat.providers.compat.probe', { preset });
452
+ try { return await client.post(ep.path, { service: ep.service, body: { base_url: baseUrl }, retries: 0 }); }
453
+ catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
454
+ },
455
+ /** B5: the saved connection's models for a preset -> { ok, host, default_model, models:[{id,context_length,tools}] } | { ok:false, error, message }. */
456
+ async compatModels(preset) {
457
+ const ep = resolveEndpoint('chat.providers.compat.models', { preset });
458
+ try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
459
+ catch (e) { return { ok: false, status: e?.status, error: e?.code || null, message: e?.message || 'failed' }; }
460
+ },
461
+ approvals: listApprovals,
462
+ /** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
463
+ async approvalResult(approvalId) {
464
+ const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
465
+ try { return await client.get(ep.path, { service: ep.service, query: { org_id: orgId() || undefined } }); }
466
+ catch (e) { if (e?.status === 404) return null; throw e; }
467
+ },
468
+ async respond(itemId, action) {
469
+ const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
470
+ return client.post(ep.path, { service: ep.service, body: { action, comment: 'via quoxon' } });
471
+ }
472
+ };
473
+ }