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/commands/x.js CHANGED
@@ -389,6 +389,56 @@ export function registerXCommands(program, ctx) {
389
389
  }
390
390
  });
391
391
 
392
+ // ── x whoami / accounts ───────────────────────────────────────────────────
393
+ // quox#867: a focused read of the connected social account (platform, handle,
394
+ // connection status), without the posting-budget half of `x status`. Backs the
395
+ // chat read-parity lane (an agent/CLI asking "which X account am I connected as?").
396
+ x
397
+ .command('whoami')
398
+ .alias('accounts')
399
+ .description('Show the connected X account: platform, handle and connection status')
400
+ .action(async () => {
401
+ auth.requireAuth();
402
+ const orgId = resolveOrgId(auth, program);
403
+ const ep = resolveEndpoint('v1.tools.execute');
404
+
405
+ let cred = null;
406
+ let credError = null;
407
+ try {
408
+ const credData = await client.post(ep.path, {
409
+ service: ep.service,
410
+ body: { tool_name: 'twitter_verify_credentials', input: {}, org_id: orgId },
411
+ });
412
+ if (credData && credData.success === false) {
413
+ credError = credData.error || 'unknown error';
414
+ } else {
415
+ cred = credData && credData.result ? credData.result : credData;
416
+ }
417
+ } catch (err) {
418
+ credError = err.message;
419
+ }
420
+
421
+ const account = {
422
+ platform: 'x',
423
+ connected: !credError && !!cred,
424
+ handle: cred && cred.username ? `@${cred.username}` : null,
425
+ name: (cred && cred.name) || null,
426
+ id: (cred && cred.id) || null,
427
+ error: credError,
428
+ };
429
+
430
+ if (program.opts().output === 'json') {
431
+ formatDetail({ accounts: [account] }, { format: 'json' });
432
+ return;
433
+ }
434
+
435
+ if (credError) {
436
+ printWarning(`X: not connected (twitter_verify_credentials via POST ${ep.path}): ${credError}`);
437
+ } else {
438
+ printSuccess(`X: connected as ${account.handle}${account.name ? ` (${account.name})` : ''}`);
439
+ }
440
+ });
441
+
392
442
  // ── x media-root ────────────────────────────────────────────────────────
393
443
 
394
444
  x
@@ -0,0 +1,172 @@
1
+ /**
2
+ * `quox youtube` — YouTube plugin verbs, YT-O-6 (tiktok/linkedin CLI parity).
3
+ *
4
+ * quox youtube upload "<title>" --file <path-in-media-root> [--description t] [--privacy private|unlisted|public] [--thumbnail <image-path>]
5
+ * quox youtube thumbnail <videoId> --image <path-in-media-root>
6
+ * quox youtube videos [--limit <n>]
7
+ * quox youtube channel
8
+ * quox youtube analytics [--metrics a,b,c] [--video <id>]
9
+ * quox youtube comment <commentId> "<text>"
10
+ *
11
+ * The write verbs (upload, thumbnail, comment) execute the youtube connector
12
+ * tool through the governed collector endpoint (POST /api/v1/tools/execute ->
13
+ * connector bridge). The bridge gates them on requiresApproval: it files an
14
+ * owner approval card and returns { pending:true, status:'awaiting_approval',
15
+ * approvalId, inbox_item_id }. The YouTube API write only happens once an
16
+ * approver says yes (dashboard inbox or `quox inbox respond <id> --action approve`).
17
+ *
18
+ * Honest limit on --thumbnail: YouTube assigns the video id only when the
19
+ * upload actually executes, i.e. AFTER approval. While the upload card is
20
+ * pending there is no id to attach a thumbnail to, so the chained
21
+ * set_thumbnail step runs only if the upload came back executed with a
22
+ * video id; otherwise the CLI says so and names the follow-up verb.
23
+ */
24
+
25
+ import { formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
26
+ import { resolveEndpoint } from '../api/endpoints.js';
27
+ import { resolveOrgId } from './helpers.js';
28
+
29
+ const PRIVACY = ['private', 'unlisted', 'public'];
30
+
31
+ async function executeYouTubeTool({ client, auth, program }, toolName, input) {
32
+ auth.requireAuth();
33
+ const orgId = resolveOrgId(auth, program);
34
+ const ep = resolveEndpoint('v1.tools.execute');
35
+ const body = { tool_name: toolName, input, org_id: orgId };
36
+ const data = await client.post(ep.path, { service: ep.service, body });
37
+ if (data && data.success === false && !data.pending) {
38
+ throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
39
+ }
40
+ return data;
41
+ }
42
+
43
+ function printAwaitingApproval(action, data) {
44
+ const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
45
+ printSuccess(`${action} filed for owner approval - inbox item ${itemRef} (awaiting approval). Nothing is sent to YouTube until an approver says yes.`);
46
+ if (data.inbox_item_id) {
47
+ printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
48
+ }
49
+ }
50
+
51
+ export function registerYouTubeCommands(program, ctx) {
52
+ const { client, auth } = ctx;
53
+ const run = (tool, input) => executeYouTubeTool({ client, auth, program }, tool, input);
54
+ const asJson = () => program.opts().output === 'json';
55
+
56
+ const youtube = program
57
+ .command('youtube')
58
+ .description('YouTube uploads, thumbnails, comments and reads: writes are governed and HITL-approved via HITL Inbox, on your own vaulted Google credential');
59
+
60
+ youtube
61
+ .command('upload')
62
+ .description('Upload a video from a file under the org media root. Files an owner approval card and returns immediately; never uploads directly.')
63
+ .argument('<title>', 'video title (max 100 characters)')
64
+ .requiredOption('--file <path>', 'video file path, relative to the org media root')
65
+ .option('--description <text>', 'video description')
66
+ .option('--privacy <status>', `privacy status: ${PRIVACY.join('|')} (default private)`, 'private')
67
+ .option('--thumbnail <path>', 'thumbnail image path (media root); chained set_thumbnail, only possible once the upload has a video id')
68
+ .action(async (title, options) => {
69
+ const privacy = options.privacy || 'private';
70
+ if (!PRIVACY.includes(privacy)) {
71
+ throw new Error(`--privacy must be one of ${PRIVACY.join(', ')} (got ${privacy})`);
72
+ }
73
+ const input = { title, file_path: options.file, privacy_status: privacy };
74
+ if (options.description) input.description = options.description;
75
+
76
+ const data = await run('youtube_upload_video', input);
77
+ const videoId = data?.result?.video_id || data?.video_id || null;
78
+ let thumb = null;
79
+ if (options.thumbnail && videoId && !data.pending) {
80
+ thumb = await run('youtube_set_thumbnail', { video_id: videoId, image_path: options.thumbnail });
81
+ }
82
+
83
+ if (asJson()) {
84
+ formatDetail(thumb ? { upload: data, thumbnail: thumb } : data, { format: 'json' });
85
+ return;
86
+ }
87
+ if (data.pending) {
88
+ printAwaitingApproval('Upload', data);
89
+ if (options.thumbnail) {
90
+ printWarning(`Thumbnail not filed: the video id does not exist until the upload is approved and runs. After approval run: quox youtube thumbnail <videoId> --image ${options.thumbnail} (that files a second, separate approval).`);
91
+ }
92
+ return;
93
+ }
94
+ printSuccess(`Upload executed${videoId ? ` - video ${videoId}` : ''}.`);
95
+ if (thumb) {
96
+ if (thumb.pending) printAwaitingApproval('Thumbnail', thumb);
97
+ else printSuccess('Thumbnail set.');
98
+ } else if (options.thumbnail) {
99
+ printWarning(`Upload returned no video id, so the thumbnail was not set. Run: quox youtube thumbnail <videoId> --image ${options.thumbnail}`);
100
+ }
101
+ });
102
+
103
+ youtube
104
+ .command('thumbnail')
105
+ .description('Set a custom thumbnail on an existing video from an image under the org media root. Files an owner approval card; never sets it directly.')
106
+ .argument('<videoId>', 'YouTube video id')
107
+ .requiredOption('--image <path>', 'image path (.jpg/.jpeg/.png, max 2MB), relative to the org media root')
108
+ .action(async (videoId, options) => {
109
+ const data = await run('youtube_set_thumbnail', { video_id: videoId, image_path: options.image });
110
+ if (asJson()) { formatDetail(data, { format: 'json' }); return; }
111
+ printAwaitingApproval('Thumbnail', data);
112
+ });
113
+
114
+ youtube
115
+ .command('videos')
116
+ .description('List the connected channel\'s recent videos (read-only, no approval needed).')
117
+ .option('--limit <n>', 'max videos to return (max 50)', '20')
118
+ .action(async (options) => {
119
+ const count = Math.min(parseInt(options.limit, 10) || 20, 50);
120
+ const data = await run('youtube_list_videos', { count });
121
+ if (asJson()) { formatDetail(data, { format: 'json' }); return; }
122
+ const videos = data.result?.videos || [];
123
+ if (videos.length === 0) { printInfo('No videos returned.'); return; }
124
+ for (const v of videos) {
125
+ console.log(`${v.video_id} ${v.title || '(no title)'} [${v.privacy_status || 'unknown'}]`);
126
+ console.log(` views: ${v.view_count ?? 0} likes: ${v.like_count ?? 0} comments: ${v.comment_count ?? 0}`);
127
+ }
128
+ });
129
+
130
+ youtube
131
+ .command('channel')
132
+ .description('Show the connected YouTube channel (also the cheapest live credential check).')
133
+ .action(async () => {
134
+ const data = await run('youtube_get_channel', {});
135
+ if (asJson()) { formatDetail(data, { format: 'json' }); return; }
136
+ const ch = data.result || {};
137
+ if (!ch.channel_id && !ch.title) { printInfo('No channel returned.'); return; }
138
+ console.log(`${ch.title || 'unknown'} (${ch.channel_id || 'unknown'})`);
139
+ console.log(` subscribers: ${ch.subscriber_count ?? 'unknown'} videos: ${ch.video_count ?? 'unknown'} views: ${ch.view_count ?? 'unknown'}`);
140
+ });
141
+
142
+ youtube
143
+ .command('analytics')
144
+ .description('Channel (or single video) analytics totals for the last 28 days.')
145
+ .option('--metrics <list>', 'comma-separated YouTube Analytics metric names (default views,estimatedMinutesWatched,averageViewDuration,likes,subscribersGained)')
146
+ .option('--video <id>', 'restrict to one video id')
147
+ .action(async (options) => {
148
+ const input = {};
149
+ if (options.metrics) input.metrics = options.metrics;
150
+ if (options.video) input.video_id = options.video;
151
+ const data = await run('youtube_get_analytics', input);
152
+ if (asJson()) { formatDetail(data, { format: 'json' }); return; }
153
+ const r = data.result || {};
154
+ const metrics = r.metrics || {};
155
+ const keys = Object.keys(metrics);
156
+ if (keys.length === 0) { printInfo('No analytics returned.'); return; }
157
+ console.log(`${r.start_date || '?'} to ${r.end_date || '?'}${r.video_id ? ` video ${r.video_id}` : ''}`);
158
+ for (const k of keys) console.log(` ${k}: ${metrics[k]}`);
159
+ if (r.missing_metrics?.length) printWarning(`Not returned by YouTube: ${r.missing_metrics.join(', ')}`);
160
+ });
161
+
162
+ youtube
163
+ .command('comment')
164
+ .description('Reply to a comment thread. Files an owner approval card and returns immediately; never posts directly.')
165
+ .argument('<commentId>', 'comment thread id to reply to (the id of the top-level comment)')
166
+ .argument('<text>', 'reply text')
167
+ .action(async (parentId, text) => {
168
+ const data = await run('youtube_reply_comment', { parent_id: parentId, text });
169
+ if (asJson()) { formatDetail(data, { format: 'json' }); return; }
170
+ printAwaitingApproval('Comment reply', data);
171
+ });
172
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Action Tracker item writes and open-item reads, shared by `quox actions
3
+ * add|done` and the Quox TUI /todo and /todos. One core, so chat, CLI and TUI
4
+ * create the same item through the same collector route
5
+ * (POST /api/v1/actions, PATCH /api/v1/actions/:id), as the logged-in user.
6
+ */
7
+
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+ import { QuoxError } from './errors.js';
10
+
11
+ export const PRIORITIES = ['low', 'medium', 'high', 'urgent'];
12
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
13
+
14
+ const pad = (n) => String(n).padStart(2, '0');
15
+ const dayOf = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
16
+
17
+ /** "today", "tomorrow" or YYYY-MM-DD -> YYYY-MM-DD. Anything else is a usage error, never guessed. */
18
+ export function parseDue(value, now = new Date()) {
19
+ if (value === undefined || value === null || value === '') return null;
20
+ const v = String(value).trim().toLowerCase();
21
+ if (v === 'today') return dayOf(now);
22
+ if (v === 'tomorrow') return dayOf(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1));
23
+ if (ISO_DAY.test(v) && !Number.isNaN(Date.parse(`${v}T00:00:00Z`))) return v;
24
+ throw new QuoxError(`--due must be YYYY-MM-DD, today or tomorrow (got "${value}")`, 2);
25
+ }
26
+
27
+ /** Words typed after the verb -> a title. Empty or blank is a usage error. */
28
+ export function titleFromWords(words) {
29
+ const title = (Array.isArray(words) ? words.join(' ') : String(words || '')).trim();
30
+ if (!title) throw new QuoxError('Usage: quox actions add <text...> [--due <date>] [--priority low|medium|high|urgent]', 2);
31
+ return title;
32
+ }
33
+
34
+ /** Throw unless the response carries an item with an id: an error body is never a created item. */
35
+ function itemOf(res, what) {
36
+ const item = res && res.item;
37
+ if (!item || !item.id || res.success === false) {
38
+ throw new QuoxError(`${what} failed: ${(res && (res.error || res.message)) || 'the server returned no item'}`);
39
+ }
40
+ return item;
41
+ }
42
+
43
+ /** Create one Action Tracker item (status todo) as the logged-in user. -> the item. */
44
+ export async function addActionItem(client, { title, due, priority, description } = {}) {
45
+ const t = titleFromWords(title);
46
+ if (priority && !PRIORITIES.includes(priority)) {
47
+ throw new QuoxError(`--priority must be one of ${PRIORITIES.join(', ')}`, 2);
48
+ }
49
+ const body = { title: t };
50
+ const dueDate = parseDue(due);
51
+ if (dueDate) body.due_date = dueDate;
52
+ if (priority) body.priority = priority;
53
+ if (description) body.description = description;
54
+ const ep = resolveEndpoint('actions.create');
55
+ return itemOf(await client.post(ep.path, { service: ep.service, body }), 'Adding the action item');
56
+ }
57
+
58
+ export const STATUS_COLUMNS = ['todo', 'doing', 'done'];
59
+
60
+ /** Move one item to todo, doing or done. -> the item. */
61
+ export async function moveActionItem(client, id, status) {
62
+ const clean = String(id || '').trim();
63
+ if (!clean) throw new QuoxError('Usage: quox actions move <id> <todo|doing|done>', 2);
64
+ if (!STATUS_COLUMNS.includes(status)) throw new QuoxError(`status must be one of ${STATUS_COLUMNS.join(', ')}`, 2);
65
+ const ep = resolveEndpoint('actions.update', { id: clean });
66
+ return itemOf(await client.patch(ep.path, { service: ep.service, body: { status } }), 'Moving the action item');
67
+ }
68
+
69
+ /** Mark one item done. -> the item. */
70
+ export async function completeActionItem(client, id) {
71
+ if (!String(id || '').trim()) throw new QuoxError('Usage: quox actions done <id>', 2);
72
+ return moveActionItem(client, id, 'done');
73
+ }
74
+
75
+ /** Every item in one column (or all), paged to the end; never a silent cap. -> [{id,title,status,priority,due_date}]. */
76
+ export async function listActionItems(client, status = 'all', { pageSize = 100, maxPages = 50 } = {}) {
77
+ const ep = resolveEndpoint('actions.list');
78
+ const out = [];
79
+ for (let page = 1; page <= maxPages; page++) {
80
+ const query = { limit: pageSize, page };
81
+ if (status !== 'all') query.status = status;
82
+ const res = await client.get(ep.path, { service: ep.service, query, retries: 0 });
83
+ if (!res || !Array.isArray(res.items)) {
84
+ throw new QuoxError(`Listing action items failed: ${(res && (res.error || res.message)) || 'unexpected response'}`);
85
+ }
86
+ out.push(...res.items);
87
+ if (!(res.pagination && res.pagination.has_more)) return out;
88
+ }
89
+ throw new QuoxError(`More than ${maxPages * pageSize} action items; narrow with quox actions list --status.`);
90
+ }
91
+
92
+ /** The board by column. -> { backlog (status todo), doing, done } each a full list. */
93
+ export async function listBoard(client) {
94
+ const backlog = await listActionItems(client, 'todo');
95
+ const doing = await listActionItems(client, 'doing');
96
+ const done = await listActionItems(client, 'done');
97
+ return { backlog, doing, done };
98
+ }
99
+
100
+ /** Open items (todo then doing), all of them. */
101
+ export async function listOpenActionItems(client) {
102
+ return [...(await listActionItems(client, 'todo')), ...(await listActionItems(client, 'doing'))];
103
+ }
104
+
105
+ /** Delete one item (server soft-deletes; creator or admin only). Checks the status before trusting the body. -> { id, title }. */
106
+ export async function deleteActionItem(client, id) {
107
+ const clean = String(id || '').trim();
108
+ if (!clean) throw new QuoxError('Usage: quox actions delete <id> --yes', 2);
109
+ const ep = resolveEndpoint('actions.delete', { id: clean });
110
+ const res = await client.delete(ep.path, { service: ep.service });
111
+ if (!res || res.success === false || res.deleted !== true) {
112
+ throw new QuoxError(`Deleting the action item failed: ${(res && (res.error || res.message)) || 'the server did not confirm the delete'}`);
113
+ }
114
+ return { id: clean, title: res.title || '' };
115
+ }
@@ -58,6 +58,8 @@ export function loadBundledAssistantsRoster(dir = DEFAULT_ASSISTANTS_DIR) {
58
58
  id: raw.id,
59
59
  name: raw.identity?.name || null,
60
60
  shortName: raw.identity?.shortName || null,
61
+ // AGENT-TAXONOMY: present only when the file declares one.
62
+ ...(raw.family ? { family: raw.family } : {}),
61
63
  });
62
64
  }
63
65
  } catch {
@@ -0,0 +1,7 @@
1
+ // Agent family ids, mirrored from src/config/agentFamilies.js (the authority).
2
+ // The published CLI does not ship src/config, so it keeps this copy;
3
+ // cli/test/core/agentFamilies.test.js fails if the two drift.
4
+ export const AGENT_FAMILY_IDS = [
5
+ 'command', 'devops', 'monitoring', 'secops', 'development',
6
+ 'assistantq', 'knowledge', 'marketing', 'voice', 'governance',
7
+ ];
@@ -0,0 +1,86 @@
1
+ /**
2
+ * announce: the ONE renderer CLI verbs use to say "Quox did something".
3
+ *
4
+ * Owner order 2026-10-29: if anything Quox happens, it announces. A verb that
5
+ * writes or governs prints exactly one QuoxGlyph line:
6
+ * <icon> NAME │ <verb object> │ <result>[ · <duration>] <status>
7
+ * built on core/quoxglyph.js (the canonical grammar), never hand-typed.
8
+ *
9
+ * Output rules:
10
+ * - stderr, not stdout: scripted stdout (tables, ids, --json) is byte-for-byte unchanged;
11
+ * - silent under --json / --quiet;
12
+ * - brand-gradient name only when stderr is a TTY, plain when piped or redirected.
13
+ */
14
+
15
+ import { line } from './quoxglyph.js';
16
+ import { getOutputFormat } from './output.js';
17
+
18
+ // eslint-disable-next-line no-control-regex
19
+ const ANSI = /\x1b\[[0-9;]*m/g;
20
+
21
+ function errMessage(err) {
22
+ if (!err) return 'failed';
23
+ const body = err.body && (err.body.message || err.body.error);
24
+ return String(body || err.message || err).replace(/\s+/g, ' ').trim();
25
+ }
26
+
27
+ /** Render without printing. opts: {result, ms, status} (status defaults to ok). */
28
+ export function renderAnnounce(surface, verb, opts = {}) {
29
+ let s = line(surface, verb, { status: 'ok', ...opts });
30
+ if (!(process.stderr && process.stderr.isTTY)) s = s.replace(ANSI, '');
31
+ return tidy(s);
32
+ }
33
+
34
+ // No result: the grammar's third field is just the status glyph (avoid a double space after the separator).
35
+ const tidy = (s) => s.replace(/│ {2}(?=\S)/, '│ ');
36
+
37
+ export function announceMuted() { return muted(); }
38
+ function muted() {
39
+ const f = getOutputFormat();
40
+ return f === 'json' || f === 'quiet';
41
+ }
42
+
43
+ /** Print one announce line to stderr. Returns the rendered line (or null when muted). */
44
+ export function announce(surface, verb, opts = {}) {
45
+ if (muted()) return null;
46
+ const s = renderAnnounce(surface, verb, opts);
47
+ process.stderr.write(`${s}\n`);
48
+ return s;
49
+ }
50
+
51
+ /** The failure twin: one line, status fail, reason as the result. */
52
+ export function announceFailure(surface, verb, err) {
53
+ if (err && typeof err === 'object') { try { Object.defineProperty(err, 'quoxAnnounced', { value: true, configurable: true }); } catch { /* frozen */ } }
54
+ return announce(surface, verb, { result: errMessage(err), status: 'fail' });
55
+ }
56
+
57
+ /**
58
+ * Command-level wrapper: a verb whose SUCCESS announces itself inline gets its FAILURE exit announced here, once, so
59
+ * every exit (return or throw) of the verb prints exactly one QuoxGlyph line. `verb` is a string, or a function of the
60
+ * action's arguments. An error already announced (by an inner announceFailure) is not announced twice.
61
+ */
62
+ export function announceOnFail(surface, verb, fn) {
63
+ return async (...args) => {
64
+ try { return await fn(...args); } catch (err) {
65
+ if (err && typeof err === 'object' && !err.quoxAnnounced) {
66
+ announceFailure(surface, typeof verb === 'function' ? verb(...args) : verb, err);
67
+ } else if (!err || typeof err !== 'object') announceFailure(surface, typeof verb === 'function' ? verb(...args) : verb, err);
68
+ throw err;
69
+ }
70
+ };
71
+ }
72
+
73
+ // quox_event statuses arrive as QuoxGlyph status names (ok fail busy warn block).
74
+ const EVENT_STATUS = new Set(['ok', 'fail', 'busy', 'warn', 'block']);
75
+
76
+ /**
77
+ * The plain (no ANSI) QuoxGlyph line for a collector `quox_event` frame, so the TUI transcript
78
+ * and `--print` render a turn's Quox effects with the SAME renderer the verbs use.
79
+ * Returns null for a frame with nothing to say.
80
+ */
81
+ export function eventLine(ev) {
82
+ if (!ev || typeof ev !== 'object' || !ev.verb) return null;
83
+ const surface = String(ev.surface || 'quox').toLowerCase().replace(/[^a-z0-9_-]/g, '') || 'quox';
84
+ const status = EVENT_STATUS.has(ev.status) ? ev.status : 'ok';
85
+ return tidy(line(surface, String(ev.verb), { status, ...(ev.result ? { result: String(ev.result) } : {}) }).replace(ANSI, ''));
86
+ }
package/lib/core/auth.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { readFileSync, statSync, openSync, closeSync, unlinkSync } from 'node:fs';
9
9
  import { QuoxAuthError, QuoxApiError, QuoxTransientAuthError } from './errors.js';
10
+ import { rateLimitHint, retryAfterMsFrom } from './loginGuard.js';
10
11
  import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from './residentAuth.js';
11
12
 
12
13
  // A resident agent has no refresh token: its expired-access-token seam is
@@ -159,6 +160,17 @@ async function acquireRefreshLock(file, { staleMs = 20000, waitMs = 20000 } = {}
159
160
  }
160
161
  }
161
162
 
163
+ // H4: tag a failed bare fetch with the server and profile so the central error mapping can name the exact
164
+ // `quox connect` command when the cause is a certificate this machine does not trust.
165
+ async function fetchTagged(config, baseUrl, url, opts) {
166
+ try { return await fetch(url, opts); } catch (e) {
167
+ if (e && typeof e === 'object') {
168
+ try { e.quoxUrl = baseUrl; e.quoxProfile = config?.getActiveProfileName?.(); } catch { /* tagging is best effort */ }
169
+ }
170
+ throw e;
171
+ }
172
+ }
173
+
162
174
  export function createAuth(config) {
163
175
  return {
164
176
  async login(baseUrl, email, password, orgId) {
@@ -170,7 +182,7 @@ export function createAuth(config) {
170
182
  const body = { email, password, client: 'cli' };
171
183
  if (orgId) body.org_id = orgId;
172
184
 
173
- const res = await fetch(url, {
185
+ const res = await fetchTagged(config, baseUrl, url, {
174
186
  method: 'POST',
175
187
  headers: { 'Content-Type': 'application/json' },
176
188
  body: JSON.stringify(body),
@@ -179,6 +191,12 @@ export function createAuth(config) {
179
191
  const data = await res.json();
180
192
 
181
193
  if (!res.ok) {
194
+ if (res.status === 429) {
195
+ const retryAfterMs = retryAfterMsFrom(res, data);
196
+ const err = new QuoxApiError(429, data.error, rateLimitHint({ retryAfterMs }));
197
+ err.retryAfterMs = retryAfterMs;
198
+ throw err;
199
+ }
182
200
  throw new QuoxApiError(res.status, data.error, data.message || 'Login failed');
183
201
  }
184
202
 
@@ -204,7 +222,7 @@ export function createAuth(config) {
204
222
 
205
223
  async verifyMfa(baseUrl, token, code) {
206
224
  const url = `${baseUrl}/api/mfa/verify`;
207
- const res = await fetch(url, {
225
+ const res = await fetchTagged(config, baseUrl, url, {
208
226
  method: 'POST',
209
227
  headers: {
210
228
  'Content-Type': 'application/json',
@@ -0,0 +1,118 @@
1
+ /**
2
+ * callMe: the shared core behind `quox approvals call-me` and the TUI
3
+ * /callmeforapprovals. It reads and flips the ApprovalsQ call-me policy (the
4
+ * switch for the watcher that phones a human when a high-risk approval sits
5
+ * unanswered). The server route is human-org-admin only; agent identities and
6
+ * service keys are refused there, and every write is audited (AEE).
7
+ *
8
+ * It NEVER approves or denies anything. on/off flip `enabled` only (plus the
9
+ * optional --after / --quiet knobs); every other stored field is preserved.
10
+ */
11
+
12
+ import { resolveEndpoint } from '../api/endpoints.js';
13
+ import { QuoxUserError } from './errors.js';
14
+
15
+ const HHMM = /^(\d{1,2}):(\d{2})$/;
16
+ const ok = (s) => { const m = HHMM.exec(s); return Boolean(m && Number(m[1]) <= 23 && Number(m[2]) <= 59); };
17
+
18
+ /** "22:00-08:00" -> { start, end }, or throws a QuoxUserError naming the fix. */
19
+ export function parseQuiet(text) {
20
+ const m = /^(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})$/.exec(String(text || '').trim());
21
+ if (!m || !ok(m[1]) || !ok(m[2])) {
22
+ throw new QuoxUserError(`--quiet must look like 22:00-08:00 (got "${text}")`, { code: 'bad_quiet_hours' });
23
+ }
24
+ return { start: m[1], end: m[2] };
25
+ }
26
+
27
+ /** --after <minutes>: integer 1-1440, or throws. */
28
+ export function parseAfter(text) {
29
+ const n = Number(text);
30
+ if (!Number.isInteger(n) || n < 1 || n > 1440) {
31
+ throw new QuoxUserError(`--after must be whole minutes between 1 and 1440 (got "${text}")`, { code: 'bad_after' });
32
+ }
33
+ return n;
34
+ }
35
+
36
+ /** Build the PUT body for an action (on|off) plus options { after, quiet }. */
37
+ export function buildChange(action, { after, quiet } = {}) {
38
+ if (action !== 'on' && action !== 'off') {
39
+ throw new QuoxUserError(`Unknown action "${action}". Use status, on or off.`, { code: 'bad_action' });
40
+ }
41
+ const body = { enabled: action === 'on' };
42
+ if (after !== undefined && after !== null && after !== '') body.age_threshold_minutes = parseAfter(after);
43
+ if (quiet) body.quiet_hours = parseQuiet(quiet);
44
+ return body;
45
+ }
46
+
47
+ /** One honest line about the watcher. Unknown is said as unknown, never guessed as alive. */
48
+ export function watcherLine(w) {
49
+ if (!w || typeof w !== 'object') return 'watcher: unknown (the server did not report it)';
50
+ if (w.running === false) {
51
+ return w.reason === 'env_flag_off'
52
+ ? 'watcher: NOT RUNNING (QUOX_APPROVALSQ_WATCHER is off on the collector, so no call will be placed)'
53
+ : 'watcher: NOT RUNNING (it did not start, so no call will be placed)';
54
+ }
55
+ if (w.running === true) {
56
+ if (!w.last_tick_at) return 'watcher: started, no check completed yet';
57
+ const tail = w.stale ? ' STALE, it has stopped checking' : (w.last_tick_ok === false ? ' (last check had errors)' : '');
58
+ return `watcher: alive, last check ${w.last_tick_at}${tail}`;
59
+ }
60
+ return 'watcher: unknown (the server did not report it)';
61
+ }
62
+
63
+ /** Human-readable status lines from a { policy, watcher } response. */
64
+ export function statusLines(data) {
65
+ const p = (data && data.policy) || {};
66
+ const q = p.quiet_hours || {};
67
+ const lines = [
68
+ `call me for approvals: ${p.enabled ? 'ON' : 'OFF'}`,
69
+ `rings after: ${p.age_threshold_minutes} min unanswered`,
70
+ `quiet hours: ${q.start}-${q.end} ${q.timezone || ''}`.trimEnd(),
71
+ `min tier: ${p.min_tier}`,
72
+ watcherLine(data && data.watcher),
73
+ ];
74
+ if (p.enabled && p.destination_configured === false) lines.push('calls go to: the owner mobile (QuoxVoice default; no destination number set on this policy)');
75
+ return lines;
76
+ }
77
+
78
+ /** Plain-words refusal for a 401/403 from the server. */
79
+ function refusal(e) {
80
+ if (e && e.status === 402) {
81
+ return new QuoxUserError('QuoxVoice is not licensed for this org, so call-me for approvals is unavailable.', { code: 'license_required', status: 402 });
82
+ }
83
+ if (e && (e.status === 403 || e.status === 401)) {
84
+ return new QuoxUserError('Only a signed-in human org admin can change or read the call-me policy. Agent identities and service keys are refused.', { code: 'admin_only', status: e.status });
85
+ }
86
+ return e;
87
+ }
88
+
89
+ export async function getCallMe(client) {
90
+ const ep = resolveEndpoint('v1.approvals.call-me.get');
91
+ try { return await client.get(ep.path, { service: ep.service, retries: 0 }); } catch (e) { throw refusal(e); }
92
+ }
93
+
94
+ export async function setCallMe(client, change) {
95
+ const ep = resolveEndpoint('v1.approvals.call-me.put');
96
+ try { return await client.put(ep.path, { service: ep.service, body: change, retries: 0 }); } catch (e) { throw refusal(e); }
97
+ }
98
+
99
+ /** Parse TUI args: "[on|off|status] [--after N] [--quiet HH:MM-HH:MM]" -> { action, after, quiet }. */
100
+ export function parseSlashArgs(text) {
101
+ const toks = String(text || '').trim().split(/\s+/).filter(Boolean);
102
+ const out = { action: 'status' };
103
+ for (let i = 0; i < toks.length; i++) {
104
+ const t = toks[i];
105
+ if (t === 'on' || t === 'off' || t === 'status') out.action = t;
106
+ else if (t === '--after') out.after = toks[++i];
107
+ else if (t === '--quiet') out.quiet = toks[++i];
108
+ else throw new QuoxUserError(`Unknown argument "${t}". Usage: /callmeforapprovals [on|off|status] [--after <minutes>] [--quiet HH:MM-HH:MM]`, { code: 'bad_args' });
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /** Run an action end to end -> { data, changed }. status reads; on/off write. */
114
+ export async function runCallMe(client, { action = 'status', after, quiet } = {}) {
115
+ if (action === 'status') return { data: await getCallMe(client), changed: false };
116
+ const body = buildChange(action, { after, quiet });
117
+ return { data: await setCallMe(client, body), changed: true };
118
+ }