quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
package/lib/quoxon/api.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * QUOXON server calls. Thin wrapper over the CLI client so the app can be
3
3
  * driven by a fake (demo + tests). All routes per docs/QUOXON_EVENTS_v1.md.
4
4
  */
5
+ import { otherOrgsWaiting } from '../core/crossOrgApprovals.js';
5
6
  import { resolveEndpoint } from '../api/endpoints.js';
6
7
  import { rememberEmail } from '../core/auth.js';
7
8
  import { isSessionDead } from './sessionDead.js'; // not signin.js: that loads React + Ink (QUOXON-LATENCY)
@@ -9,23 +10,28 @@ import { buildStreamQuery } from '../commands/chat.js';
9
10
  import { loadBundledAssistantsRoster } from '../core/agentAlias.js';
10
11
  import { bundledIdentities } from './objects/fetch.js';
11
12
  import { recordHeldSlot } from '../commands/slot.js';
12
- import { askGripe, settleGripe } from '../commands/gripe.js';
13
+ import { askGripe, settleGripe, fileDeterministically } from '../commands/gripe.js';
14
+ import { gripeQueue, fileOrQueue, makeRetryFiler } from './gripeQueue.js';
13
15
  import { sendFeedback } from './feedback.js';
14
16
  import { fromObjective, fromExecution, fromApproval, sortRows } from './roster.js';
15
17
  import { normChoice } from './model.js';
16
18
  import { normPrefs as normModelPrefs } from './modelPrefs.js';
17
19
  import { createRowsStore } from './rowsStore.js';
18
20
  import { createObjectsApi } from './objects/fetch.js';
21
+ import { createDragonReads } from './dragonReads.js';
19
22
  import { createKillerApi } from './slash/killerApi.js';
20
23
  import { createUndoOps } from './slash/undoApi.js';
21
24
  import { resolveDashboardBase, resolveCollectorBase, detectLanIp } from './hostUrls.js';
22
25
  import { grantBody } from './consent.js';
26
+ import { runCallMe } from '../core/callMe.js';
23
27
  import { loadJourney } from './onboarding/journey.js';
24
28
  import { joinServiceUrl } from '../core/config.js';
25
29
  import { runVmCommand } from '../core/vmTui.js';
26
30
  import { fetchActivity, heldLines } from '../core/hyperion.js';
27
31
  import { createPeekFetchers, peekId } from '../core/peek.js';
28
32
  import { askAside } from '../core/aside.js';
33
+ import { exportConversationBundle } from '../evidence/bundleExport.js';
34
+ import { addActionItem, listBoard, moveActionItem } from '../core/actionItems.js';
29
35
 
30
36
  const missing = (e) => e?.status === 404 || e?.status === 405 || e?.status === 501;
31
37
  /** Server steer refusal label -> client reason. Unknown labels are 'ambiguous', never "finished". */
@@ -71,13 +77,39 @@ const vaultFailure = (e) => ({ ok: false, status: e?.status, message: e?.message
71
77
  const slotsUnlicensed = (e) => e?.status === 403 && /not licen[cs]ed/i.test(`${e?.code || ''} ${e?.body?.error || ''} ${e?.message || ''}`);
72
78
 
73
79
  /** PRESENCE: only a person at a real terminal is "back". --print, pipes, proof runs and agents are not. */
80
+ const NOTES_OFF = 'NotesQ is not available for this org. Ask an org admin to enable the NotesQ plugin (dashboard, Plugins), or check `quox notes ls` in a terminal.';
81
+ export function noteParts(text) {
82
+ const t = String(text || '').trim();
83
+ const first = t.split('\n')[0].trim();
84
+ const title = first.length > 60 ? `${first.slice(0, 60).trimEnd()}…` : first;
85
+ return { title, markdown: `# ${title}\n\n${t}\n` };
86
+ }
87
+ async function createNoteFrom(client, text) {
88
+ const { title, markdown } = noteParts(text);
89
+ try {
90
+ const ep = resolveEndpoint('notesq.notes.create');
91
+ const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { scope: 'org', markdown, properties: {} } });
92
+ const n = d?.note || {};
93
+ if (!n.id) return { ok: false, message: 'NotesQ answered without a note id, so nothing is confirmed saved. Check quox notes ls.' };
94
+ return { ok: true, id: n.id, title: n.title || title };
95
+ } catch (e) {
96
+ if (e?.status === 402) return { ok: false, message: 'NotesQ is not licensed for this org. ' + NOTES_OFF };
97
+ if (e?.status === 403 || e?.status === 404 || e?.status === 503) return { ok: false, message: NOTES_OFF };
98
+ if (!e?.status && /timed out|timeout|ECONNRESET|socket hang up|network|fetch failed|ETIMEDOUT/i.test(e?.message || '')) return { ok: false, maybeSaved: true, message: 'The save may still have gone through (the server was slow). /notes shows your recent notes; I won\'t retry automatically so you don\'t get a duplicate.' }; // request was sent: the outcome is unknown, not a failure
99
+ return { ok: false, message: `Could not save the note: ${e?.message || 'request failed'}${e?.status ? ` (HTTP ${e.status})` : ''}.` };
100
+ }
101
+ }
74
102
  export function isInteractiveHuman({ stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
75
103
  return Boolean(stdin?.isTTY && stdout?.isTTY) && !env.CI;
76
104
  }
77
105
 
78
- export function createApi({ client, config, auth, windowOrg = null, reattachBackoff = REATTACH_BACKOFF_MS }) {
106
+ /** A one-line title within the tasks service's 200 character cap. */
107
+ const clipTitle = (g) => { const t = String(g ?? '').replace(/\s+/g, ' ').trim(); return t.length > 200 ? `${t.slice(0, 199)}…` : t; };
108
+
109
+ export function createApi({ client, config, auth, windowOrg = null, clientForOrg = null, reattachBackoff = REATTACH_BACKOFF_MS, gripeQueuePath = undefined, listSessions = null, awayRetryMs = 3000 }) {
79
110
  let holderSessionFn = null;
80
111
  const orgId = () => { try { return auth.getOrgId(); } catch { return null; } };
112
+ const gripeStore = gripeQueue(gripeQueuePath);
81
113
  const rowsStore = createRowsStore({ client, config, orgId });
82
114
  let credsSeen = null; // credentials-file mtime last seen (credentialsChanged)
83
115
  const userId = () => { try { return config.getAuth?.()?.user_id || null; } catch { return null; } };
@@ -134,12 +166,13 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
134
166
  err.code = 'reattach_failed';
135
167
  throw err;
136
168
  }
169
+ const HUMAN_TUI = { 'X-Quox-Human-Action': 'tui' }; // DND contract: only a human action in the TUI sends this, never a restart or internal path
137
170
  /** PRESENCE auto-clear: the human's own first real action. -> { ok, cleared, welcome } (welcome once: { away_text, via }). Never throws. */
138
171
  async function noteActivity(action, { quiet = false, interactive = isInteractiveHuman() } = {}) {
139
172
  if (!interactive) return { ok: false, cleared: false, welcome: null, skipped: 'not interactive' };
140
173
  try {
141
174
  const ep = resolveEndpoint('presence.activity');
142
- const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
175
+ const d = await client.post(ep.path, { service: ep.service, retries: 0, timeout: 6000, headers: HUMAN_TUI, body: { action, org_id: orgId(), ...(quiet ? { quiet: true } : {}) } });
143
176
  return { ok: true, cleared: d?.cleared === true, welcome: d?.welcome || null };
144
177
  } catch (e) { return { ok: false, cleared: false, welcome: null, status: e?.status || null }; }
145
178
  }
@@ -147,9 +180,13 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
147
180
  ...createKillerApi({ client }),
148
181
  orgId,
149
182
  undoOps: createUndoOps({ client, orgId }), // /undo
183
+ /** /note: same call as `quox notes new` (org scope). -> { ok, id, title } | { ok:false, message } */
184
+ createNote: (text) => createNoteFrom(client, text),
150
185
  userId,
151
186
  /** /peek <id>: the same lookup as `quox peek`, in this window's org. -> peekId result */
152
187
  peek: (id) => peekId(id, createPeekFetchers({ client, orgId })),
188
+ /** /evidence, /ward: the signed bundle of one conversation, written to ~/quox-evidence (same export as `quox evidence bundle`). */
189
+ evidenceExport: (corr) => exportConversationBundle({ client, corr }),
153
190
  /** /aside: a no-tools, no-conversation question (core/aside.js). turns = recent exchanges, read-only context. */
154
191
  aside: (question, turns = []) => askAside(client, { question, turns }),
155
192
  /** @mention hosts: the Bastion inventory (same read as `quox fleet hosts`). -> [{ id, group, health }] */
@@ -158,6 +195,25 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
158
195
  const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
159
196
  return (listOf(d, 'hosts') || []).map(h => ({ id: h.id, group: h.group || '', health: h.status?.health || '' }));
160
197
  },
198
+ /** QUOXON objective offer: `quox objective create <goal> --checkpoint` + `start` (same endpoints). -> { id, status } */
199
+ async startObjective(goal, loop = null) {
200
+ const c = resolveEndpoint('objectives.create');
201
+ // QLOOP: a loop offer creates a LOOP (server-derived scope + goal), never a plan under the loop's name.
202
+ const body = loop ? { title: String(goal).slice(0, 200), description: String(goal), execution_mode: 'loop', capability_scope: loop.capability_scope, goal_condition: loop.goal_condition,
203
+ ...(loop.until ? { until: loop.until } : {}), ...(loop.every_seconds ? { every_seconds: loop.every_seconds } : {}), ...(loop.max_iterations ? { max_iterations: loop.max_iterations } : {}), ...(loop.max_duration_seconds ? { max_duration_ms: loop.max_duration_seconds * 1000 } : {}), ...(Array.isArray(loop.done_check) && loop.done_check.length ? { done_check: loop.done_check } : {}) }
204
+ : { title: clipTitle(goal), description: String(goal), checkpoint_after_each_task: true }; // the tasks service caps a title at 200; the whole ask rides in the description
205
+ const made = await client.post(c.path, { service: c.service, body });
206
+ if (!made?.id) throw new Error('the server answered without an objective id');
207
+ const st = resolveEndpoint('objectives.start', { id: made.id });
208
+ const started = await client.post(st.path, { service: st.service, body: {} });
209
+ return { id: made.id, status: started?.status || 'started' };
210
+ },
211
+ /** An objective with its tasks (the same read as `quox objective get`), for the chat's report-back. */
212
+ async objectiveGet(id) {
213
+ const ep = resolveEndpoint('objectives.get', { id: encodeURIComponent(id) });
214
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 8000 });
215
+ return d?.objective || d;
216
+ },
161
217
  /** @mention tasks: the newest objectives (one read, cached by the composer for the session). -> [{ id, title, status }] */
162
218
  async mentionTasks() {
163
219
  const ep = resolveEndpoint('objectives.list');
@@ -203,6 +259,7 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
203
259
  },
204
260
  /** Inline object views (objects/): skill, workflow, crystal, agent, memory, rules, proof, reconcile. */
205
261
  objects: createObjectsApi({ client, orgId, profile: config?._effectiveProfile || null }),
262
+ dragonReads: createDragonReads({ client }), // the companion dragons' jobs: bounded read-only reads (dragonReads.js)
206
263
  /** /model choice persisted on the active profile ({ provider, model? } only, never a key); null = auto. */
207
264
  modelChoice() { try { return normChoice(config.getActiveProfile?.()?.chat_model); } catch { return null; } },
208
265
  setModelChoice(choice) {
@@ -455,6 +512,27 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
455
512
  try { const r = await fetch(`${String(base).replace(/\/+$/, '')}/install`, { method: 'GET', signal: ctl.signal }); try { await r.body?.cancel?.(); } catch { /* drained */ } return { ok: r.ok, status: r.status }; }
456
513
  catch { return null; } finally { clearTimeout(t); }
457
514
  },
515
+ /** TUI-STATUS: the registered server this quox runs on -> { name, role, remote } | null (unregistered or any failure). */
516
+ async serverChip() {
517
+ const { resolveServerChip } = await import('./serverChip.js');
518
+ const { loadRegistry } = await import('../server/devServers.js');
519
+ let registry = { servers: {} };
520
+ try { registry = { servers: (await loadRegistry({ client })).servers }; } catch { /* unreadable registry: no purpose shown */ }
521
+ return resolveServerChip({
522
+ registry,
523
+ url: config.getActiveProfile?.()?.url || config.getBaseUrl?.() || null,
524
+ listHosts: async () => { const ep = resolveEndpoint('v1.fleet.hosts.list'); const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 }); return d?.hosts || d; }
525
+ });
526
+ },
527
+ /** DEVFLEET: the registered dev servers (same rows as `quox server list`) -> rows[]; throws on a Bastion read failure. */
528
+ async devServers() {
529
+ const [{ loadRegistry, buildRows, hereIdentity }] = [await import('../server/devServers.js')];
530
+ const ep = resolveEndpoint('v1.fleet.hosts.list');
531
+ const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 });
532
+ const hosts = d?.hosts || d;
533
+ if (!Array.isArray(hosts)) throw new Error('unexpected response');
534
+ { const reg = await loadRegistry({ client }); return buildRows(hosts, { servers: reg.servers, defaults: reg.defaults }, hereIdentity()); }
535
+ },
458
536
  serverUrl() { try { return new URL(config.getServiceUrl('collector')).origin; } catch { return null; } },
459
537
  /** /config settings on the active profile. */
460
538
  settings() { try { const q = config.getActiveProfile?.()?.quoxon; return q && typeof q === 'object' ? { ...q } : {}; } catch { return {}; } },
@@ -478,16 +556,23 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
478
556
  * file when the agent did not). The client asks the user first, so the agent is told to file without asking.
479
557
  * -> { ok:true, response, url, superseded, notes } | { ok:false, message }.
480
558
  */
481
- async gripe(text, { yes = true } = {}) {
482
- try {
483
- const options = { yes };
484
- const data = await askGripe({ client, complaint: text, yes, orgId: orgId() });
485
- const r = await settleGripe({ client, data, complaint: text, options, orgId: orgId() });
486
- return { ok: true, response: r.response, url: r.url, superseded: r.superseded, notes: r.notes };
487
- } catch (e) {
488
- return { ok: false, message: e?.status ? `HTTP ${e.status}: ${e.message}` : (e?.message || 'failed') };
489
- }
559
+ async gripe(text, { yes = true, repo = null } = {}) {
560
+ return fileOrQueue({
561
+ text, repo, queue: gripeStore,
562
+ run: async (key) => {
563
+ const options = { yes, idempotencyKey: key, queueRetryable: true, ...(repo ? { repo } : {}) };
564
+ const data = await askGripe({ client, complaint: text, yes, orgId: orgId() });
565
+ const r = await settleGripe({ client, data, complaint: text, options, orgId: orgId() });
566
+ return { response: r.response, url: r.url, superseded: r.superseded, notes: r.notes };
567
+ },
568
+ });
490
569
  },
570
+ /** GRIPE-QUEUE: file what is queued (at most once per 2 minutes). -> { ran, filed:[{text,url}], expired:[{text}], pending }. */
571
+ async gripeRetry({ force = false } = {}) {
572
+ return gripeStore.retry(makeRetryFiler({ fileDeterministically, client, orgId: orgId() }), { force });
573
+ },
574
+ /** GRIPE-QUEUE: queued plus recently filed gripes, oldest first. */
575
+ gripes() { return gripeStore.list(); },
491
576
  /**
492
577
  * /feedback: POST the previewed payload to the public quox.ai feedback API (not the org's server, not GitHub).
493
578
  * Anonymous by design: no token, cookie, org or profile goes with it. -> { ok:true, id, url } | { ok:false, message }.
@@ -503,6 +588,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
503
588
  if (!rows) throw new Error('unexpected response');
504
589
  return rows.filter(r => r.is_enabled !== false && r.is_enabled !== 0).map(r => r.plugin_id).filter(Boolean);
505
590
  },
591
+ /** /todo: add an Action Tracker item as the logged-in user (the same core as `quox actions add`). Throws on failure. -> the item. */
592
+ async todoAdd(text) { return addActionItem(client, { title: text }); },
593
+ /** /todos: the whole board { backlog, doing, done }, every page. Throws on failure so it never reads as "none". */
594
+ async todoList() { return listBoard(client); },
595
+ /** /todo move: move one item (real id) to todo, doing or done. -> the item. */
596
+ async todoMove(id, status) { return moveActionItem(client, id, status); },
597
+ /** /callmeforapprovals: { action, after, quiet } -> { data, changed } (same core as `quox approvals call-me`). Throws on failure. */
598
+ async callMe(args) { return runCallMe(client, args); },
599
+ /** -> [{ plugin_id, version, is_enabled, ... }] every plugin installed for the org (GET plugins.list, as `quox plugin list`). Throws on failure. */
600
+ async pluginList() {
601
+ const id = orgId();
602
+ if (!id) throw new Error('no org');
603
+ const ep = resolveEndpoint('plugins.list', { orgId: id });
604
+ const rows = listOf(await client.get(ep.path, { service: ep.service, retries: 0 }), 'plugins', 'data', 'items');
605
+ if (!rows) throw new Error('unexpected response');
606
+ return rows;
607
+ },
608
+ /** One installed plugin's record (GET plugins.get, as `quox plugin get`), or null when unknown (404). */
609
+ async pluginGet(pluginId) {
610
+ const id = orgId();
611
+ if (!id) throw new Error('no org');
612
+ const ep = resolveEndpoint('plugins.get', { orgId: id, pluginId });
613
+ try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.plugin || d; }
614
+ catch (e) { if (e?.status === 404) return null; throw e; }
615
+ },
506
616
  /** Fleet hosts already connected (tips skip the QuoxAgent tip when > 0). GET agents list; throws on failure so a failure never reads as "no hosts". */
507
617
  async hostCount() {
508
618
  const ep = resolveEndpoint('v1.agents.list');
@@ -522,24 +632,108 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
522
632
  * ("quox1 (Adam)"); `fast` answers once the line is in the slot, without waiting on the chat-app bridges.
523
633
  * -> { ok:true, message } | { ok:false, message, status }.
524
634
  */
635
+ /**
636
+ * Does this slot name exist in one of the user's OWN orgs? Reads only, never sends. Same rules as slotSay: name@org
637
+ * targets one membership; two other orgs holding the name is { found:true, ambiguous:true } (the send path then asks).
638
+ * -> { found:boolean, ambiguous?:true }. Throws when the membership list itself cannot be read.
639
+ */
640
+ async slotLocate(name) {
641
+ const slug = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
642
+ const clientFor = async (oid) => (oid === orgId() ? client : clientForOrg(oid));
643
+ const listed = async (oid, nm) => {
644
+ const ep = resolveEndpoint('v1.slots.list');
645
+ const rows = listOf(await (await clientFor(oid)).get(ep.path, { service: ep.service, retries: 0 }), 'slots') || [];
646
+ return rows.some(r => String(r?.name || '').toLowerCase() === String(nm).toLowerCase());
647
+ };
648
+ const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
649
+ const orgs = (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || [])).filter(o => o?.id).map(o => ({ id: o.id, name: o.name || o.id }));
650
+ const at = String(name).indexOf('@');
651
+ if (at > 0) {
652
+ const raw = String(name).slice(at + 1); const frag = slug(raw);
653
+ const hit = frag ? orgs.filter(o => o.id === raw || slug(o.name) === frag) : [];
654
+ const pick = hit.length ? hit : orgs.filter(o => frag && slug(o.name).startsWith(frag));
655
+ if (pick.length !== 1 || !clientForOrg) return { found: false };
656
+ return { found: await listed(pick[0].id, String(name).slice(0, at)) };
657
+ }
658
+ if (await listed(orgId(), name)) return { found: true };
659
+ if (!clientForOrg) return { found: false };
660
+ const hits = [];
661
+ for (const o of orgs) { if (o.id === orgId()) continue; try { if (await listed(o.id, name)) hits.push(o); } catch { /* an org that cannot be listed is not a candidate */ } }
662
+ return hits.length > 1 ? { found: true, ambiguous: true } : { found: hits.length === 1 };
663
+ },
525
664
  /** SLOTS-REPLY2: the window registers how to name its checkout session; slotSay then sends it as holder_session. */
526
665
  bindHolderSession(fn) { holderSessionFn = typeof fn === 'function' ? fn : null; },
527
- async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null } = {}) {
528
- const ep = resolveEndpoint('v1.slots.say', { name });
529
- try {
666
+ async slotSay(name, text, { from = null, fast = false, roomSession = null, fromSlot = null, holderSession = holderSessionFn ? holderSessionFn() : null, anyOrg = true } = {}) {
667
+ const send = async (cl, oid) => {
668
+ const ep = resolveEndpoint('v1.slots.say', { name });
530
669
  // SLOTS-REPLY: fromSlot is this window's held slot, its reply address (the server verifies the hold).
531
- const d = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: orgId(), ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
670
+ const d = await cl.post(ep.path, { service: ep.service, retries: 0, body: { text, org_id: oid, ...(roomSession ? { room_session: roomSession } : from ? { from } : {}), ...(!roomSession && fromSlot ? { from_slot: fromSlot } : {}), ...(!roomSession && holderSession ? { holder_session: holderSession } : {}), ...(fast ? { bridge_report: false } : {}) } });
532
671
  return { ok: true, message: d?.message || null, roomPending: d?.room_pending === true, presence: d?.presence || null, humanPresence: d?.human_presence || null };
533
- } catch (e) {
672
+ };
673
+ const fail = (e) => {
534
674
  if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
535
675
  return { ok: false, message: e?.status === 404 ? `no slot named ${name}` : (e?.message || 'failed'), status: e?.status || null };
676
+ };
677
+ // The user's own memberships (orgs.list is scoped to the caller); nothing outside this list is ever tried.
678
+ const memberOrgs = async () => {
679
+ try {
680
+ const oe = resolveEndpoint('orgs.list'); const d = await client.get(oe.path, { service: oe.service });
681
+ return (Array.isArray(d) ? d : (d?.organizations || d?.orgs || d?.data || [])).filter(o => o?.id).map(o => ({ id: o.id, name: o.name || o.id }));
682
+ } catch { return []; }
683
+ };
684
+ const slug = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
685
+ const clientFor = async (oid) => (oid === orgId() ? client : clientForOrg(oid));
686
+ const at = String(name).indexOf('@');
687
+ if (at > 0) { // name@org: exactly that membership, never the others
688
+ const base = String(name).slice(0, at); const frag = slug(String(name).slice(at + 1));
689
+ const orgs = anyOrg && clientForOrg ? await memberOrgs() : [];
690
+ const hit = frag ? orgs.filter(o => o.id === String(name).slice(at + 1) || slug(o.name) === frag) : [];
691
+ const pick = hit.length ? hit : orgs.filter(o => frag && slug(o.name).startsWith(frag));
692
+ if (pick.length !== 1) return { ok: false, status: 404, message: pick.length ? `"${name}" matches several of your orgs: ${pick.map(o => `${base}@${slug(o.name)}`).join(', ')}` : `no slot named ${name}` };
693
+ try { const r = await send(await clientFor(pick[0].id), pick[0].id); return { ...r, viaOrgId: pick[0].id, viaOrgName: pick[0].name }; } catch (e) { return { ...fail(e), viaOrgId: pick[0].id, viaOrgName: pick[0].name }; }
536
694
  }
695
+ let first;
696
+ try { return await send(client, orgId()); } catch (e) { first = fail(e); }
697
+ // HITL-INBOX-BAR: a window in another org cannot see a slot that lives in the profile's home org. Same user, same
698
+ // sessions (not a cross-tenant leak): look in the user's other orgs only, each on a token for THAT org.
699
+ if (!anyOrg || first.status !== 404 || !clientForOrg) return first;
700
+ const tried = new Set([orgId()]);
701
+ const orgs = await memberOrgs();
702
+ const nameOf = (oid) => orgs.find(o => o.id === oid)?.name || null;
703
+ // 1. Find it without sending: list slots per other org. Two orgs hold the name: ask, never pick.
704
+ const hits = [];
705
+ for (const o of orgs) {
706
+ if (tried.has(o.id)) continue;
707
+ try {
708
+ const ep = resolveEndpoint('v1.slots.list');
709
+ const rows = listOf(await (await clientFor(o.id)).get(ep.path, { service: ep.service, retries: 0 }), 'slots') || [];
710
+ if (rows.some(r => String(r?.name || '').toLowerCase() === String(name).toLowerCase())) hits.push(o);
711
+ } catch { /* an org that cannot be listed is not a candidate here; the probe below still covers it */ }
712
+ }
713
+ if (hits.length > 1) {
714
+ const choices = hits.map(o => ({ orgId: o.id, orgName: o.name, ref: `${name}@${slug(o.name)}` }));
715
+ return { ok: false, ambiguous: true, choices, status: null, message: `"${name}" exists in more than one of your orgs: ${choices.map(c => `${c.ref} (${c.orgName})`).join(', ')}. Say which, e.g. /say ${choices[0].ref} ...` };
716
+ }
717
+ const order = hits.map(o => o.id);
718
+ if (!order.length) { // nothing listed it: fall back to trying the home org, then each membership
719
+ try { const home = config.getAuth?.()?.org_id; if (home) order.push(home); } catch { /* no home org known */ }
720
+ for (const o of orgs) order.push(o.id);
721
+ }
722
+ for (const oid of order) {
723
+ if (tried.has(oid)) continue; tried.add(oid);
724
+ try {
725
+ const cl = await clientFor(oid);
726
+ const r = await send(cl, oid);
727
+ return { ...r, viaOrgId: oid, viaOrgName: nameOf(oid) };
728
+ } catch (e) { const f = fail(e); if (f.status !== 404) return { ...f, viaOrgId: oid, viaOrgName: nameOf(oid) }; } // the slot is there but refused: that is the real answer
729
+ }
730
+ return first;
537
731
  },
538
732
  /** QUOXON-BROADCAST: fan a message out to the org's held slots. -> { ok:true, data } | { ok:false, message, status }. */
539
- async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, from = null } = {}) {
733
+ async slotBroadcast({ text, slots = null, priority = 'normal', ack = false, active = false, dryRun = false, from = null } = {}) {
540
734
  const ep = resolveEndpoint('v1.slots.broadcast');
541
735
  try {
542
- const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
736
+ const data = await client.post(ep.path, { service: ep.service, retries: 0, body: { text, priority, ack, ...(active ? { active: true } : {}), ...(dryRun ? { dry_run: true } : {}), org_id: orgId(), ...(slots ? { slots } : {}), ...(from ? { from } : {}) } });
543
737
  return { ok: true, data };
544
738
  } catch (e) {
545
739
  if (slotsUnlicensed(e)) return { ok: false, unlicensed: true, status: 403, message: SLOTS_UNLICENSED };
@@ -563,6 +757,18 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
563
757
  const ep = resolveEndpoint('v1.slots.broadcast.ack', { id });
564
758
  try { return { ok: true, data: await client.post(ep.path, { service: ep.service, retries: 0, body: { slot, org_id: orgId() } }) }; } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
565
759
  },
760
+ /** QLOOP-LIVE: the org's loops, newest first (GET /api/v1/loops) -> { loops:[{ objective_id, title, state, created_at }] }. */
761
+ async loopList() {
762
+ return client.get('/api/v1/loops', { service: 'collector', retries: 0, timeout: 8000, query: { limit: 20 } });
763
+ },
764
+ /** QLOOP-LIVE: one loop's snapshot (GET /api/v1/loops/:id). Throws on failure (the card decides what a failure means). */
765
+ async loopSnapshot(id) {
766
+ return client.get(`/api/v1/loops/${encodeURIComponent(id)}`, { service: 'collector', retries: 0, timeout: 8000 });
767
+ },
768
+ /** QLOOP-LIVE: one loop's events (SSE; ?since resumes). Yields { event_seq, type, ... }; ends after loop.finished or a drop. */
769
+ async *loopEvents(id, { since = null } = {}, signal) {
770
+ yield* client.stream(`/api/v1/loops/${encodeURIComponent(id)}/events`, { service: 'collector', query: { since }, signal, untilClose: true });
771
+ },
566
772
  /** QUOXON-MESH: the slot's live messages (SSE). Yields { type: 'connected' | 'slot.message' | 'reconnect' }. */
567
773
  async *slotStream(name, { since = null, excludeFrom = null, session = null } = {}, signal) {
568
774
  const ep = resolveEndpoint('v1.slots.stream', { name });
@@ -678,6 +884,20 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
678
884
  if (!windowOrg) { const e = new Error('Org switching is not available in this mode.'); e.code = 'no_window_org'; throw e; }
679
885
  return windowOrg.switchTo(targetId);
680
886
  },
887
+ /**
888
+ * "also make default" from the /org picker: the same profile-wide change as `quox org switch <org>` in a shell
889
+ * (moves the shared session and rewrites the profile's stored org + token). This window's own org is untouched.
890
+ */
891
+ async makeOrgDefault(targetId) {
892
+ const ep = resolveEndpoint('auth.switch-org');
893
+ const data = await client.post(ep.path, { service: ep.service, body: { org_id: targetId } });
894
+ if (!data?.org?.id) { const e = new Error('The server did not confirm the switch.'); e.code = 'switch_failed'; throw e; }
895
+ const a = config.getAuth() || {};
896
+ a.org_id = data.org.id; a.org_name = data.org.name; a.org_slug = data.org.slug; a.org_role = data.role;
897
+ if (data.access_token) a.access_token = data.access_token;
898
+ config.setAuth(a);
899
+ return { id: data.org.id, name: data.org.name };
900
+ },
681
901
  /** The window's own org id when it differs from the profile's (carried across /restart), else null. */
682
902
  windowOrgId: () => windowOrg?.current()?.id || null,
683
903
  /** -> [{ id, name, description, edition, version, decayState }] ; the org's QSKL procedural skills (read-only list; Crystals are a separate store). */
@@ -716,8 +936,26 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
716
936
  return l.map(e => fromExecution(e, now, { userId: userId() })).filter(Boolean);
717
937
  })
718
938
  ]);
939
+ // Attached agents (org list, owner/admin) and this box's live `claude --resume` processes. Neither is a work row:
940
+ // they ride on the source as `items` and /roster prints them (rosterSessions.js).
941
+ const itemsSrc = (id, label, fn) => fn().then(
942
+ (items) => ({ id, label, state: 'connected', rows: [], items, count: items.length }),
943
+ (e) => ({ id, label, rows: [], items: [], count: 0, state: missing(e) ? 'unavailable' : 'error', note: missing(e) ? 'no list endpoint on this server' : (e?.message || 'failed') }));
944
+ const extra = await Promise.all([
945
+ itemsSrc('agents', 'attached agents', async () => {
946
+ const ep = resolveEndpoint('agent-attach.list');
947
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
948
+ const l = listOf(d, 'attaches'); if (!l) throw new Error('unexpected response');
949
+ return l;
950
+ }),
951
+ itemsSrc('boxsessions', "this box's sessions", async () => {
952
+ if (listSessions) return await listSessions();
953
+ const [{ listLiveSessions }, { spawnSync }] = await Promise.all([import('../core/governedSession.js'), import('node:child_process')]);
954
+ return listLiveSessions({ spawnSync, pid: process.pid });
955
+ })
956
+ ]);
719
957
  const unlisted = [
720
- { id: 'agents', label: 'attached agents', state: 'unavailable', count: 0, note: 'no org-wide list endpoint' },
958
+ ...extra.map(({ rows, ...rest }) => rest),
721
959
  { id: 'devsessions', label: 'dev sessions', state: 'unavailable', count: 0, note: 'listed per host only (quox dev sessions <host>)' }
722
960
  ];
723
961
  return { rows: sortRows(parts.flatMap(p => p.rows)), sources: [...parts.map(({ rows, ...rest }) => rest), ...unlisted] };
@@ -753,18 +991,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
753
991
  return { situation, approvals };
754
992
  },
755
993
  /** PRESENCE: PUT /api/v1/presence (/afk, /sleep, /back, a typed turn). -> { ok:true, presence, notice } | { ok:false, message, status }. */
756
- async setPresence(state, { note, slots } = {}) {
994
+ async setPresence(state, { note, slots, dnd = false, human = false } = {}) {
757
995
  const ep = resolveEndpoint('presence.set');
758
996
  try {
759
- const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, body: { state, org_id: orgId(), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
997
+ const d = await client.put(ep.path, { service: ep.service, retries: 0, timeout: 8000, ...(human ? { headers: HUMAN_TUI } : {}), body: { state, org_id: orgId(), ...(dnd ? { dnd: true } : {}), ...(note ? { note } : {}), ...(Array.isArray(slots) && slots.length ? { slots } : {}) } });
760
998
  return { ok: true, presence: d?.presence || null, notice: d?.notice || null };
761
999
  } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
762
1000
  },
1001
+ /** AFK-SURVIVES-RESTART: read-only GET /api/v1/presence/me. -> { ok:true, presence } | { ok:false, status }. Never throws, never writes. */
1002
+ async getPresence() {
1003
+ try {
1004
+ const ep = resolveEndpoint('presence.get', { who: 'me' });
1005
+ const d = await client.get(ep.path, { service: ep.service, query: { org_id: orgId() }, retries: 0, timeout: 6000 });
1006
+ return { ok: true, presence: d?.presence || null };
1007
+ } catch (e) { return { ok: false, message: e?.message || 'failed', status: e?.status || null }; }
1008
+ },
763
1009
  noteActivity,
764
1010
  /** E5 /away: GET /api/v1/away?since=<iso> -> { since, until, items, activity, blind }. Throws on non-2xx. */
765
1011
  async away(since) {
766
1012
  const ep = resolveEndpoint('away.digest');
767
- return client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
1013
+ const read = () => client.get(ep.path, { service: ep.service, query: { since }, retries: 0, timeout: 15000 });
1014
+ const first = await read();
1015
+ // AWAY-RETRY: the first calls after a fresh sign-in can time out; try those sources once more before saying "Can't see".
1016
+ if (!(first?.blind || []).some((b) => /timed? ?out|timeout/i.test(String(b?.reason || '')))) return first;
1017
+ await new Promise((r) => setTimeout(r, awayRetryMs));
1018
+ try { return (await read()) || first; } catch { return first; } // the retry also failing leaves the honest first answer
768
1019
  },
769
1020
  /** Facts for /observers, each part on its own: { lucid, learning:[{ id, label, state, text }] }. */
770
1021
  async observers() {
@@ -823,6 +1074,31 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
823
1074
  }
824
1075
  },
825
1076
  /** /connect status: the org vault list (the only source a user can read). -> { ok:true, rows } | { ok:false, error }. */
1077
+ /** LAZY-AI-CLI: which vendor CLIs (claude-code, codex) this server has. Never throws. */
1078
+ async providerCliStatus() {
1079
+ const ep = resolveEndpoint('v1.providers.cli.status');
1080
+ try {
1081
+ const d = await client.get(ep.path, { service: ep.service, retries: 0 });
1082
+ return Array.isArray(d?.tools) ? { ok: true, tools: d.tools } : { ok: false, error: 'unexpected response' };
1083
+ } catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.status ? `HTTP ${e.status} ${e.message || ''}`.trim() : (e?.message || 'failed') }; }
1084
+ },
1085
+ /** The install plan: source, exact package + version, download size. Instance admin only (403 otherwise). Never throws. */
1086
+ async providerCliPlan(tool) {
1087
+ const ep = resolveEndpoint('v1.providers.cli.plan', { tool });
1088
+ try { const d = await client.get(ep.path, { service: ep.service, retries: 0 }); return d?.ok ? d : { ok: false, error: d?.message || 'unexpected response' }; }
1089
+ catch (e) { return { ok: false, status: e?.status, missing: missing(e), error: e?.body?.message || e?.message || 'failed' }; }
1090
+ },
1091
+ /** Start an install job (the server audits it). Never throws. */
1092
+ async providerCliInstall(tool, { approvalId = null } = {}) {
1093
+ const ep = resolveEndpoint('v1.providers.cli.install', { tool });
1094
+ try { return await client.post(ep.path, { service: ep.service, body: approvalId ? { approval_id: approvalId } : {}, retries: 0 }); }
1095
+ catch (e) { return { ok: false, status: e?.status, error: e?.body?.message || e?.message || 'failed' }; }
1096
+ },
1097
+ async providerCliJob(id) {
1098
+ const ep = resolveEndpoint('v1.providers.cli.job', { id });
1099
+ try { return await client.get(ep.path, { service: ep.service, retries: 0 }); }
1100
+ catch (e) { return { state: 'unknown', error: e?.message || 'failed', status: e?.status }; }
1101
+ },
826
1102
  async connections() {
827
1103
  try {
828
1104
  const ep = resolveEndpoint('vault.list', { orgId: orgId() });
@@ -905,12 +1181,68 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
905
1181
  /** /hyperion: held VMs and who holds them, one line each (read only, GET /api/v1/proxmox/activity). */
906
1182
  async hyperion() { const data = await fetchActivity(client); return { lines: heldLines(data), tone: data.nodes_error || data.pool_error ? 'warn' : undefined }; },
907
1183
  approvals: listApprovals,
1184
+ /** HITL-INBOX-BAR: the owner's latest inbox items (any kind, active), newest first. Throws on failure, never reads as empty. */
1185
+ async inboxItems({ limit = 30, page = 1 } = {}) {
1186
+ const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
1187
+ const data = await client.get(ep.path, { service: ep.service, query: page > 1 ? { limit, page } : { limit } });
1188
+ const items = data?.items || data?.data || data || [];
1189
+ return Array.isArray(items) ? items : [];
1190
+ },
1191
+ /** TUI-MAIL: connected InboxQ mailboxes (GET /api/v1/inboxq/accounts, the `quox mail accounts` read). Throws on failure, never reads as none. */
1192
+ async mailAccounts() {
1193
+ const ep = resolveEndpoint('inboxq.accounts.list');
1194
+ const data = await client.get(ep.path, { service: ep.service, retries: 0 });
1195
+ return Array.isArray(data?.accounts) ? data.accounts : [];
1196
+ },
1197
+ /** TUI-MAIL: triaged messages (GET /api/v1/inboxq/messages, the `quox mail list` read). Throws on failure. */
1198
+ async mailMessages({ limit = 10, zone } = {}) {
1199
+ const ep = resolveEndpoint('inboxq.messages.list');
1200
+ const data = await client.get(ep.path, { service: ep.service, retries: 0, query: { limit, offset: 0, ...(zone ? { zone } : {}) } });
1201
+ return Array.isArray(data?.messages) ? data.messages : [];
1202
+ },
1203
+ /** TUI-MAIL: one message with its stored body (GET /api/v1/inboxq/messages/:id, read only: no body fetch from the mailbox). */
1204
+ async mailMessage(id) {
1205
+ const ep = resolveEndpoint('inboxq.messages.get', { id });
1206
+ const data = await client.get(ep.path, { service: ep.service, retries: 0 });
1207
+ return data?.message || null;
1208
+ },
1209
+ /** Every inbox item, paged (100 per page, capped at maxPages). Throws on failure, never returns a partial list as the whole. */
1210
+ async inboxAllItems({ pageSize = 100, maxPages = 20 } = {}) {
1211
+ const ep = resolveEndpoint('inbox.list', { orgId: orgId() });
1212
+ const all = [];
1213
+ for (let page = 1; page <= maxPages; page++) {
1214
+ const data = await client.get(ep.path, { service: ep.service, query: { limit: pageSize, page } });
1215
+ const items = data?.items || data?.data || (Array.isArray(data) ? data : []);
1216
+ all.push(...items);
1217
+ if (!(data?.pagination?.has_more) || !items.length) break;
1218
+ }
1219
+ return all;
1220
+ },
1221
+ /** One inbox item in full (the server marks it read for this user as a side effect). */
1222
+ async inboxItem(itemId) {
1223
+ const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
1224
+ const data = await client.get(ep.path, { service: ep.service });
1225
+ return data?.item || data;
1226
+ },
1227
+ /** Mark one inbox item read (PATCH .../inbox/:id/read). */
1228
+ async inboxMarkRead(itemId, isRead = true) {
1229
+ const ep = resolveEndpoint('inbox.get', { orgId: orgId(), itemId: encodeURIComponent(itemId) });
1230
+ return client.patch(`${ep.path}/read`, { service: ep.service, body: { is_read: Boolean(isRead) } });
1231
+ },
908
1232
  /** Org-wide approvals awaiting a human (the dashboard header's server-side count, inbox stats awaiting_human). */
909
1233
  async approvalStats() {
910
1234
  const ep = resolveEndpoint('inbox.stats.list', { orgId: orgId() });
911
1235
  const d = await client.get(ep.path, { service: ep.service });
912
1236
  return { pending: Number(d?.pending_approvals ?? 0), awaiting: Number(d?.awaiting_human ?? 0) };
913
1237
  },
1238
+ /** Approvals waiting in the user's OTHER orgs: counts only, per-org stats on that org's own token (core/crossOrgApprovals). */
1239
+ async otherOrgApprovals() { return otherOrgsWaiting({ client, currentOrgId: orgId() }); },
1240
+ /** APPROVALS-AUTO: another org's open approvals, read with the caller's OWN token (auth checks membership of the URL org, not the token's org claim). */
1241
+ async approvalsInOrg(org) {
1242
+ const ep = resolveEndpoint('inbox.approvals.open', { orgId: org });
1243
+ const data = await client.get(ep.path, { service: ep.service, retries: 0 });
1244
+ return Array.isArray(data?.approvals) ? data.approvals.map((a) => a.item).filter(Boolean) : [];
1245
+ },
914
1246
  /** GET /api/v1/approvals/:id/result -> { status, result, executed_at } ; null when unknown (404) */
915
1247
  async approvalResult(approvalId) {
916
1248
  const ep = resolveEndpoint('v1.approvals.result', { id: approvalId });
@@ -918,14 +1250,14 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
918
1250
  catch (e) { if (e?.status === 404) return null; throw e; }
919
1251
  },
920
1252
  /** quox ask: answer a decision with one of its option keys (and an optional note). */
921
- async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false } = {}) {
922
- const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
1253
+ async answerDecision(itemId, option, { note = null, stepUp = null, bulk = false, org = null } = {}) {
1254
+ const ep = resolveEndpoint('inbox.respond', { orgId: org || orgId(), itemId }); // org: APPROVALS-AUTO answers another org's card by its URL org (the server checks membership of THAT org)
923
1255
  const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action: 'answer', option, ...(bulk ? { bulk: true } : {}), ...(note ? { comment: String(note).slice(0, 4000) } : {}) } });
924
1256
  if (stepUp && Object.keys(stepUp).length) noteActivity('answer', { quiet: true }); // PRESENCE: answering is the owner being back
925
1257
  return r;
926
1258
  },
927
- async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false } = {}) {
928
- const ep = resolveEndpoint('inbox.respond', { orgId: orgId(), itemId });
1259
+ async respond(itemId, action, { via = null, stepUp = null, reason = null, bulk = false, org = null } = {}) {
1260
+ const ep = resolveEndpoint('inbox.respond', { orgId: org || orgId(), itemId });
929
1261
  const tag = via ? `via quoxon ${via}` : 'via quoxon';
930
1262
  const why = reason ? String(reason).trim().slice(0, 4000) : '';
931
1263
  const r = await client.post(ep.path, { service: ep.service, headers: stepUp || {}, body: { action, ...(bulk ? { bulk: true } : {}), comment: why ? `${why} (${tag})` : tag, ...(why ? { reason: why } : {}) } });
@@ -943,6 +1275,11 @@ export function createApi({ client, config, auth, windowOrg = null, reattachBack
943
1275
  const ep = resolveEndpoint('stepup.status');
944
1276
  return client.get(ep.path, { service: ep.service });
945
1277
  },
1278
+ /** /verify-email: the SAME endpoint `quox admin users verify-email` calls. The server's checks are the only gate; the caller supplies a fresh one-shot step-up token. */
1279
+ async verifyEmail({ email, reason, userId = null, stepUp = {} }) {
1280
+ const ep = resolveEndpoint('admin.users.verify_email');
1281
+ return client.post(ep.path, { service: ep.service, headers: stepUp, body: { email, reason, ...(userId ? { user_id: userId } : {}) } });
1282
+ },
946
1283
  /** QUOXON-STEPUP part B: override ONE blocked action. Needs a live override-scoped step-up token. */
947
1284
  async override({ blockId, blockType, reason, agentId = null, action = null, stepUp = {} }) {
948
1285
  const ep = resolveEndpoint('safety.override');