quoxcli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Telegram Pro commands (QTG-PRO P0 CLI parity): onboarding status, owner
3
+ * binding, session leases, and webhook admin.
4
+ *
5
+ * quox tg status [--bot <botId>]
6
+ * quox tg bind-owner <tg_chat_id> [--bot <botId>]
7
+ * quox tg leases
8
+ * quox tg webhook-info [--bot <botId>]
9
+ * quox tg setup <webhook_url> [--bot <botId>]
10
+ *
11
+ * Backend: services/collector/server.js (QTG-PRO P0 sections) — every route
12
+ * is collector-direct (no org segment in the URL), requireServiceOrUserAuth()
13
+ * gated. Onboarding handler bodies live in
14
+ * services/collector/lib/telegramPro/onboardingService.js (getOnboardingStatus,
15
+ * bindOwner); session leases in services/collector/lib/telegramPro/sessionLeaseStore.js;
16
+ * webhook admin reuses the existing /webhook/telegram/* routes shared with
17
+ * the original Telegram integration setup flow.
18
+ *
19
+ * bind-owner 409s when a primary owner binding already exists for the
20
+ * bot/chat/thread — mirrored on the quoxchat `reply` 409 convention
21
+ * (cli/lib/commands/quoxchat.js): catch, surface a plain-language error, and
22
+ * point the caller at `quox tg status` rather than trying to unpack response
23
+ * detail the generic client error path doesn't forward (QuoxApiError only
24
+ * carries `detail`/`details`, not arbitrary body fields like `binding`).
25
+ */
26
+
27
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
28
+ import { resolveEndpoint } from '../api/endpoints.js';
29
+
30
+ function yesNo(value) {
31
+ return value ? 'yes' : 'no';
32
+ }
33
+
34
+ function isoOrDash(value) {
35
+ if (value === null || value === undefined) return '-';
36
+ const d = new Date(value);
37
+ return Number.isNaN(d.getTime()) ? String(value) : d.toISOString();
38
+ }
39
+
40
+ function renderStatus(data, botId) {
41
+ const lines = [];
42
+ lines.push('');
43
+ lines.push(`Bot: ${botId || 'default'}`);
44
+ if (data.bot) {
45
+ lines.push(` Username ${data.bot.username || '-'}`);
46
+ lines.push(` Name ${data.bot.name || '-'}`);
47
+ } else {
48
+ lines.push(' (no bot token configured)');
49
+ }
50
+ lines.push(` Webhook URL ${data.webhook?.url || '-'}`);
51
+ lines.push(` Webhook active ${yesNo(!!data.webhook?.active)}`);
52
+ if (data.webhook?.last_error_message) {
53
+ lines.push(` Last error ${data.webhook.last_error_message}`);
54
+ }
55
+ if (data.owner_binding) {
56
+ lines.push(` Owner chat ${data.owner_binding.tg_chat_id}${data.owner_binding.title ? ` (${data.owner_binding.title})` : ''}`);
57
+ } else {
58
+ lines.push(' Owner chat not bound');
59
+ }
60
+ return lines.join('\n');
61
+ }
62
+
63
+ export function registerTgCommands(program, ctx) {
64
+ const { client, auth } = ctx;
65
+
66
+ const tg = program
67
+ .command('tg')
68
+ .description('Manage Telegram Pro: onboarding status, owner binding, session leases, webhook admin');
69
+
70
+ // ── status ───────────────────────────────────────────────────────────────
71
+
72
+ tg
73
+ .command('status')
74
+ .description('Show Telegram Pro onboarding status: bot, webhook, seen chats, owner binding')
75
+ .option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
76
+ .action(async (options) => {
77
+ auth.requireAuth();
78
+ const query = {};
79
+ if (options.bot) query.bot_id = options.bot;
80
+
81
+ const ep = resolveEndpoint('tg.status');
82
+ const data = await client.get(ep.path, { service: ep.service, query });
83
+
84
+ const format = program.opts().output;
85
+ if (format === 'json') {
86
+ console.log(JSON.stringify(data, null, 2));
87
+ return;
88
+ }
89
+ if (format === 'quiet') {
90
+ console.log(data.bot?.username || '');
91
+ return;
92
+ }
93
+
94
+ console.log(renderStatus(data, options.bot));
95
+
96
+ formatOutput({
97
+ columns: ['tg_chat_id', 'title', 'is_forum', 'seen_at'],
98
+ headers: ['Chat ID', 'Title', 'Forum', 'Seen'],
99
+ rows: (data.chats_seen || []).map((c) => ({
100
+ tg_chat_id: c.tg_chat_id,
101
+ title: c.title || '-',
102
+ is_forum: yesNo(c.is_forum),
103
+ seen_at: c.seen_at || '-',
104
+ })),
105
+ }, { format, emptyMessage: 'No chats seen yet — message the bot from a chat first.' });
106
+
107
+ if (data.bots && data.bots.length) {
108
+ console.log('\nBots');
109
+ formatOutput({
110
+ columns: ['bot_id', 'username', 'kind', 'webhook_active', 'chats_bound'],
111
+ headers: ['Bot ID', 'Username', 'Kind', 'Webhook', 'Chats'],
112
+ rows: data.bots.map((b) => ({
113
+ bot_id: b.bot_id,
114
+ username: b.username || '-',
115
+ kind: b.kind,
116
+ webhook_active: yesNo(b.webhook_active),
117
+ chats_bound: b.chats_bound,
118
+ })),
119
+ }, { format, emptyMessage: 'No bots configured.' });
120
+ }
121
+ });
122
+
123
+ // ── bind-owner ───────────────────────────────────────────────────────────
124
+
125
+ tg
126
+ .command('bind-owner')
127
+ .description('Bind a Telegram chat as the owner target (the chat must have messaged the bot first)')
128
+ .argument('<tg_chat_id>', 'Telegram chat ID that has already messaged the bot')
129
+ .option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
130
+ .action(async (tgChatId, options) => {
131
+ auth.requireAuth();
132
+ const body = { tg_chat_id: tgChatId };
133
+ if (options.bot) body.bot_id = options.bot;
134
+
135
+ const ep = resolveEndpoint('tg.bindOwner');
136
+ try {
137
+ const data = await client.post(ep.path, { service: ep.service, body });
138
+ if (program.opts().output === 'json') {
139
+ console.log(JSON.stringify(data, null, 2));
140
+ return;
141
+ }
142
+ printSuccess(`Owner bound to chat ${tgChatId}.`);
143
+ if (data.binding) {
144
+ formatDetail(data.binding, {
145
+ format: program.opts().output,
146
+ title: 'Owner Binding',
147
+ });
148
+ }
149
+ } catch (err) {
150
+ if (err.status === 409) {
151
+ throw new Error(
152
+ `A primary owner binding already exists for this bot. Run \`quox tg status${options.bot ? ` --bot ${options.bot}` : ''}\` to see the current owner binding before rebinding.`
153
+ );
154
+ }
155
+ throw err;
156
+ }
157
+ });
158
+
159
+ // ── leases ───────────────────────────────────────────────────────────────
160
+
161
+ tg
162
+ .command('leases')
163
+ .description('List session leases for parallel terminal identities (T1-T8)')
164
+ .action(async () => {
165
+ auth.requireAuth();
166
+ const ep = resolveEndpoint('tg.leases.list');
167
+ const data = await client.get(ep.path, { service: ep.service });
168
+
169
+ formatOutput({
170
+ columns: ['terminal', 'session_id', 'stream', 'alive', 'expires_at'],
171
+ headers: ['Terminal', 'Session ID', 'Stream', 'Alive', 'Expires'],
172
+ rows: (data.leases || []).map((l) => ({
173
+ terminal: l.terminal,
174
+ session_id: l.session_id,
175
+ stream: l.stream || '-',
176
+ alive: yesNo(l.alive),
177
+ expires_at: isoOrDash(l.expires_at),
178
+ })),
179
+ }, { format: program.opts().output, emptyMessage: 'No active session leases.' });
180
+ });
181
+
182
+ // ── webhook-info ─────────────────────────────────────────────────────────
183
+
184
+ tg
185
+ .command('webhook-info')
186
+ .description('Show current Telegram webhook status (per-bot info is pending backend support; --bot is a no-op today)')
187
+ .option('--bot <botId>', 'target a specific bot ID (not yet honored server-side)')
188
+ .action(async (options) => {
189
+ auth.requireAuth();
190
+ const query = {};
191
+ if (options.bot) query.bot_id = options.bot;
192
+
193
+ const ep = resolveEndpoint('tg.webhook.info');
194
+ const data = await client.get(ep.path, { service: ep.service, query });
195
+
196
+ formatDetail({
197
+ active: data.active,
198
+ has_token: data.has_token,
199
+ bot_username: data.bot_username,
200
+ webhook_url: data.webhook_url,
201
+ pending_update_count: data.pending_update_count,
202
+ last_error_date: data.last_error_date,
203
+ last_error_message: data.last_error_message,
204
+ }, {
205
+ format: program.opts().output,
206
+ title: 'Telegram Webhook Info',
207
+ labels: {
208
+ active: 'Active',
209
+ has_token: 'Has token',
210
+ bot_username: 'Bot username',
211
+ webhook_url: 'Webhook URL',
212
+ pending_update_count: 'Pending updates',
213
+ last_error_date: 'Last error date',
214
+ last_error_message: 'Last error message',
215
+ },
216
+ });
217
+ });
218
+
219
+ // ── setup ────────────────────────────────────────────────────────────────
220
+
221
+ tg
222
+ .command('setup')
223
+ .description('Register the Telegram webhook URL for the bot')
224
+ .argument('<webhook_url>', 'HTTPS URL Telegram should POST updates to')
225
+ .option('--bot <botId>', 'target a specific bot ID instead of the default gateway bot')
226
+ .action(async (webhookUrl, options) => {
227
+ auth.requireAuth();
228
+ const body = { webhook_url: webhookUrl };
229
+ if (options.bot) body.bot_id = options.bot;
230
+
231
+ const ep = resolveEndpoint('tg.webhook.setup');
232
+ const data = await client.post(ep.path, { service: ep.service, body });
233
+
234
+ if (program.opts().output === 'json') {
235
+ console.log(JSON.stringify(data, null, 2));
236
+ return;
237
+ }
238
+ printSuccess(`Webhook registered: ${data.webhook_url} (bot: ${data.bot_username || 'unknown'}).`);
239
+ });
240
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * `quox tiktok` — TikTok plugin verbs, TT-4 (instagram CLI parity).
3
+ *
4
+ * quox tiktok publish "<title>" --file <path-in-media-root> [--privacy SELF_ONLY|MUTUAL_FOLLOW_FRIENDS|PUBLIC_TO_EVERYONE] [--no-duet] [--no-comment] [--no-stitch]
5
+ * quox tiktok draft "<title>" --file <path-in-media-root>
6
+ * quox tiktok profile
7
+ * quox tiktok videos [--limit <n>]
8
+ * quox tiktok stats --ids <id,id,...>
9
+ * quox tiktok status --publish-id <id>
10
+ *
11
+ * The write verbs (publish, draft) execute the tiktok connector tool through
12
+ * the governed collector endpoint (POST /api/v1/tools/execute -> connector
13
+ * bridge). The bridge gates both on requiresApproval: it files an owner
14
+ * approval card and returns immediately with { pending:true,
15
+ * status:'awaiting_approval', approvalId, inbox_item_id }. The actual TikTok
16
+ * API write (video/init -> upload -> publish, or the inbox/draft init ->
17
+ * upload) only happens once the owner approves via the dashboard inbox or
18
+ * `quox inbox respond <id> --action approve`.
19
+ *
20
+ * TikTok has its OWN dedicated vaulted credential; there is no shared
21
+ * credential with any other plugin. Two hard truths this CLI states
22
+ * honestly: an unaudited TikTok app is restricted to posting private
23
+ * (SELF_ONLY) content regardless of the privacy level requested, until
24
+ * TikTok approves the app audit; and access tokens live 24 hours with no
25
+ * automatic refresh built yet.
26
+ */
27
+
28
+ import { formatDetail, printSuccess, printInfo, printWarning } from '../core/output.js';
29
+ import { resolveEndpoint } from '../api/endpoints.js';
30
+ import { resolveOrgId } from './helpers.js';
31
+
32
+ const PRIVACY_LEVELS = ['SELF_ONLY', 'MUTUAL_FOLLOW_FRIENDS', 'PUBLIC_TO_EVERYONE'];
33
+
34
+ /**
35
+ * Execute one tiktok connector tool via the governed collector endpoint.
36
+ * A pending-approval response is a SUCCESS shape here (the card was filed);
37
+ * only a non-pending success:false is an error.
38
+ */
39
+ async function executeTikTokTool({ client, auth, program }, toolName, input) {
40
+ auth.requireAuth();
41
+ const orgId = resolveOrgId(auth, program);
42
+ const ep = resolveEndpoint('v1.tools.execute');
43
+ const body = { tool_name: toolName, input, org_id: orgId };
44
+ const data = await client.post(ep.path, { service: ep.service, body });
45
+ if (data && data.success === false && !data.pending) {
46
+ throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
47
+ }
48
+ return data;
49
+ }
50
+
51
+ function printAwaitingApproval(action, data) {
52
+ const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
53
+ printSuccess(`${action} filed for owner approval - inbox item ${itemRef} (awaiting approval). Nothing publishes until an approver says yes.`);
54
+ if (data.inbox_item_id) {
55
+ printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
56
+ }
57
+ }
58
+
59
+ export function registerTikTokCommands(program, ctx) {
60
+ const { client, auth } = ctx;
61
+
62
+ const tiktok = program
63
+ .command('tiktok')
64
+ .description('TikTok video publishing and draft uploads: governed, HITL-approved via HITL Inbox, on your own dedicated TikTok credential');
65
+
66
+ // ── tiktok publish ────────────────────────────────────────────────────
67
+
68
+ tiktok
69
+ .command('publish')
70
+ .description('Publish a video from a file under the org TikTok media root. Files an owner approval card and returns immediately; never publishes directly.')
71
+ .argument('<title>', 'video title/caption')
72
+ .requiredOption('--file <path>', 'video file path, relative to the org TikTok media root')
73
+ .option('--privacy <level>', `privacy level: ${PRIVACY_LEVELS.join('|')} (default SELF_ONLY)`, 'SELF_ONLY')
74
+ .option('--no-duet', 'disable duets on this video')
75
+ .option('--no-comment', 'disable comments on this video')
76
+ .option('--no-stitch', 'disable stitching on this video')
77
+ .action(async (title, options) => {
78
+ const privacy = options.privacy || 'SELF_ONLY';
79
+ if (!PRIVACY_LEVELS.includes(privacy)) {
80
+ throw new Error(`--privacy must be one of ${PRIVACY_LEVELS.join(', ')} (got ${privacy})`);
81
+ }
82
+
83
+ const input = {
84
+ title,
85
+ video_path: options.file,
86
+ privacy_level: privacy,
87
+ disable_duet: !options.duet,
88
+ disable_comment: !options.comment,
89
+ disable_stitch: !options.stitch,
90
+ };
91
+
92
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_publish_video', input);
93
+
94
+ if (program.opts().output === 'json') {
95
+ formatDetail(data, { format: 'json' });
96
+ return;
97
+ }
98
+ printAwaitingApproval('Publish', data);
99
+ if (privacy !== 'SELF_ONLY') {
100
+ printWarning(`Requested privacy_level=${privacy} is sent to TikTok exactly as asked, but an unaudited TikTok app is restricted to private (SELF_ONLY) viewing until TikTok approves the app audit. Check the outcome with: quox tiktok status --publish-id <id>`);
101
+ }
102
+ });
103
+
104
+ // ── tiktok draft ──────────────────────────────────────────────────────
105
+
106
+ tiktok
107
+ .command('draft')
108
+ .description('Upload a video to the account\'s TikTok inbox as a draft, finished manually in the TikTok app. Files an owner approval card and returns immediately; never uploads directly.')
109
+ .argument('<title>', 'working title for the draft (display only)')
110
+ .requiredOption('--file <path>', 'video file path, relative to the org TikTok media root')
111
+ .action(async (title, options) => {
112
+ const input = { title, video_path: options.file };
113
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_upload_draft', input);
114
+
115
+ if (program.opts().output === 'json') {
116
+ formatDetail(data, { format: 'json' });
117
+ return;
118
+ }
119
+ printAwaitingApproval('Draft upload', data);
120
+ });
121
+
122
+ // ── tiktok profile (read; cheapest live credential check) ──────────────
123
+
124
+ tiktok
125
+ .command('profile')
126
+ .description('Show the connected TikTok profile (also the cheapest live credential check).')
127
+ .action(async () => {
128
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_profile', {});
129
+
130
+ if (program.opts().output === 'json') {
131
+ formatDetail(data, { format: 'json' });
132
+ return;
133
+ }
134
+ const result = data.result || {};
135
+ if (!result.open_id && !result.display_name) {
136
+ printInfo('No profile returned.');
137
+ return;
138
+ }
139
+ console.log(`${result.display_name || 'unknown'} (${result.open_id || 'unknown'})`);
140
+ console.log(` followers: ${result.followers ?? 'unknown'} following: ${result.following ?? 'unknown'} likes: ${result.likes ?? 'unknown'} videos: ${result.videos ?? 'unknown'}`);
141
+ });
142
+
143
+ // ── tiktok videos (read) ────────────────────────────────────────────────
144
+
145
+ tiktok
146
+ .command('videos')
147
+ .description('List the connected account\'s recent videos (read-only, no approval needed).')
148
+ .option('--limit <n>', 'max videos to return (max 20)', '10')
149
+ .action(async (options) => {
150
+ const maxCount = Math.min(parseInt(options.limit, 10) || 10, 20);
151
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_list_videos', { max_count: maxCount });
152
+
153
+ if (program.opts().output === 'json') {
154
+ formatDetail(data, { format: 'json' });
155
+ return;
156
+ }
157
+ const videos = data.result?.videos || [];
158
+ if (videos.length === 0) {
159
+ printInfo('No videos returned.');
160
+ return;
161
+ }
162
+ for (const v of videos) {
163
+ console.log(`${v.id} ${v.title || '(no title)'}`);
164
+ console.log(` views: ${v.views ?? 0} likes: ${v.likes ?? 0} comments: ${v.comments ?? 0} shares: ${v.shares ?? 0}`);
165
+ }
166
+ });
167
+
168
+ // ── tiktok stats (read) ──────────────────────────────────────────────────
169
+
170
+ tiktok
171
+ .command('stats')
172
+ .description('Show engagement stats for specific videos by id.')
173
+ .requiredOption('--ids <ids>', 'comma-separated TikTok video IDs')
174
+ .action(async (options) => {
175
+ const videoIds = options.ids.split(',').map((s) => s.trim()).filter(Boolean);
176
+ if (videoIds.length === 0) {
177
+ throw new Error('tiktok stats requires at least one id in --ids');
178
+ }
179
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_video_stats', { video_ids: videoIds });
180
+
181
+ if (program.opts().output === 'json') {
182
+ formatDetail(data, { format: 'json' });
183
+ return;
184
+ }
185
+ const videos = data.result || [];
186
+ if (videos.length === 0) {
187
+ printInfo('No stats returned.');
188
+ return;
189
+ }
190
+ for (const v of videos) {
191
+ console.log(`${v.id} ${v.title || '(no title)'}`);
192
+ console.log(` views: ${v.views ?? 0} likes: ${v.likes ?? 0} comments: ${v.comments ?? 0} shares: ${v.shares ?? 0}`);
193
+ }
194
+ });
195
+
196
+ // ── tiktok status (read) ─────────────────────────────────────────────────
197
+
198
+ tiktok
199
+ .command('status')
200
+ .description('Check the status of a video publish or draft upload by publish_id.')
201
+ .requiredOption('--publish-id <id>', 'the publish_id returned by publish or draft')
202
+ .action(async (options) => {
203
+ const data = await executeTikTokTool({ client, auth, program }, 'tiktok_get_publish_status', { publish_id: options.publishId });
204
+
205
+ if (program.opts().output === 'json') {
206
+ formatDetail(data, { format: 'json' });
207
+ return;
208
+ }
209
+ const result = data.result || {};
210
+ if (!result.status) {
211
+ printInfo('No status returned.');
212
+ return;
213
+ }
214
+ console.log(`${result.publish_id} status: ${result.status}`);
215
+ if (result.fail_reason) console.log(` fail_reason: ${result.fail_reason}`);
216
+ if (result.publicly_available_post_ids) console.log(` publicly_available_post_ids: ${result.publicly_available_post_ids}`);
217
+ });
218
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Tool commands: CRUD, catalog, install, publish
3
+ */
4
+
5
+ import { missingReasonWarning } from '../core/callerSource.js';
6
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
7
+ import { resolveOrgId, parseJSON } from './helpers.js';
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+
10
+ export function registerToolCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const tool = program
14
+ .command('tool')
15
+ .description('Manage tools and tool catalog');
16
+
17
+ tool
18
+ .command('list')
19
+ .description('List tools in the organization')
20
+ .option('--type <type>', 'filter by type: mcp, function, api, script, workflow')
21
+ .option('--status <status>', 'filter by status: active, draft')
22
+ .action(async (options) => {
23
+ auth.requireAuth();
24
+ const orgId = resolveOrgId(auth, program);
25
+ const query = {};
26
+ if (options.type) query.type = options.type;
27
+ if (options.status) query.status = options.status;
28
+ const data = await client.get(resolveEndpoint('tools.list', { orgId }).path, { service: resolveEndpoint('tools.list', { orgId }).service, query });
29
+ formatOutput({
30
+ // fleet #392: column is tool_type.
31
+ columns: ['id', 'name', 'tool_type', 'status', 'risk_level', 'created_at'],
32
+ headers: ['ID', 'Name', 'Type', 'Status', 'Risk', 'Created'],
33
+ rows: data.items || data.tools || data,
34
+ }, { format: program.opts().output });
35
+ });
36
+
37
+ tool
38
+ .command('get')
39
+ .description('Get tool details')
40
+ .argument('<toolId>', 'tool ID')
41
+ .action(async (toolId) => {
42
+ auth.requireAuth();
43
+ const orgId = resolveOrgId(auth, program);
44
+ const data = await client.get(resolveEndpoint('tools.get', { orgId, toolId }).path, { service: resolveEndpoint('tools.get', { orgId, toolId }).service });
45
+ formatDetail(data.tool || data, { format: program.opts().output, title: data.tool?.name || data.name });
46
+ });
47
+
48
+ tool
49
+ .command('create')
50
+ .description('Create a new tool')
51
+ .requiredOption('--name <name>', 'tool name')
52
+ .requiredOption('--type <type>', 'type: mcp, function, api, script, workflow')
53
+ .option('--description <desc>', 'description')
54
+ .option('--risk-level <level>', 'risk level: low, medium, high', 'low')
55
+ .option('--spec <json>', 'tool spec as JSON string')
56
+ .action(async (options) => {
57
+ auth.requireAuth();
58
+ const orgId = resolveOrgId(auth, program);
59
+ const body = {
60
+ name: options.name,
61
+ type: options.type,
62
+ risk_level: options.riskLevel,
63
+ };
64
+ if (options.description) body.description = options.description;
65
+ if (options.spec) body.spec = parseJSON(options.spec, '--spec');
66
+ await client.post(resolveEndpoint('tools.create', { orgId }).path, { service: resolveEndpoint('tools.create', { orgId }).service, body });
67
+ printSuccess(`Tool "${options.name}" created.`);
68
+ });
69
+
70
+ tool
71
+ .command('update')
72
+ .description('Update a tool')
73
+ .argument('<toolId>', 'tool ID')
74
+ .option('--name <name>', 'new name')
75
+ .option('--description <desc>', 'new description')
76
+ .option('--status <status>', 'new status')
77
+ .action(async (toolId, options) => {
78
+ auth.requireAuth();
79
+ const orgId = resolveOrgId(auth, program);
80
+ const body = {};
81
+ if (options.name) body.name = options.name;
82
+ if (options.description) body.description = options.description;
83
+ if (options.status) body.status = options.status;
84
+ await client.patch(resolveEndpoint('tools.update', { orgId, toolId }).path, { service: resolveEndpoint('tools.update', { orgId, toolId }).service, body });
85
+ printSuccess('Tool updated.');
86
+ });
87
+
88
+ tool
89
+ .command('delete')
90
+ .description('Delete a tool')
91
+ .argument('<toolId>', 'tool ID')
92
+ .action(async (toolId) => {
93
+ auth.requireAuth();
94
+ const orgId = resolveOrgId(auth, program);
95
+ await client.delete(resolveEndpoint('tools.delete', { orgId, toolId }).path, { service: resolveEndpoint('tools.delete', { orgId, toolId }).service });
96
+ printSuccess('Tool deleted.');
97
+ });
98
+
99
+ tool
100
+ .command('publish')
101
+ .description('Publish a tool')
102
+ .argument('<toolId>', 'tool ID')
103
+ .action(async (toolId) => {
104
+ auth.requireAuth();
105
+ const orgId = resolveOrgId(auth, program);
106
+ await client.post(resolveEndpoint('tools.publish', { orgId, toolId }).path, { service: resolveEndpoint('tools.publish', { orgId, toolId }).service });
107
+ printSuccess('Tool published.');
108
+ });
109
+
110
+ tool
111
+ .command('catalog')
112
+ .description('Browse the tool catalog')
113
+ .option('--search <query>', 'search tools')
114
+ .option('--category <cat>', 'filter by category')
115
+ .action(async (options) => {
116
+ auth.requireAuth();
117
+ const query = {};
118
+ if (options.search) query.search = options.search;
119
+ if (options.category) query.category = options.category;
120
+ const data = await client.get(resolveEndpoint('catalog.list').path, { service: resolveEndpoint('catalog.list').service, query });
121
+ // NOTE: display the business key `tool_id` (not the catalog row `id`)
122
+ // as the identifier column — `tool install <toolId>` and the
123
+ // POST /api/tools/install-to-scope handler both validate against
124
+ // tool_definitions.tool_id, not tool_definitions.id. Showing `id`
125
+ // here previously led users to copy the wrong value into `install`.
126
+ formatOutput({
127
+ columns: ['tool_id', 'name', 'category', 'risk_level', 'description'],
128
+ headers: ['Tool ID', 'Name', 'Category', 'Risk', 'Description'],
129
+ rows: data.items || data.tools || data,
130
+ }, { format: program.opts().output });
131
+ });
132
+
133
+ tool
134
+ .command('exec')
135
+ .description('Execute a governed tool via the collector (POST /api/v1/tools/execute)')
136
+ .argument('<name>', 'tool name/id (the TOOL_DEFS key, e.g. "codebase_query")')
137
+ .option('--args <json>', "tool arguments as a JSON string, e.g. '{\"query\":\"...\"}'", '{}')
138
+ // #620: docs/muscle-memory reach for --input; accept it as an alias for
139
+ // --args so the documented form works. --args still wins if both given.
140
+ .option('--input <json>', 'alias for --args (tool arguments as a JSON string)')
141
+ .option('--reason <text>', 'why you are running this; shown on the approval card if the call is held for a human')
142
+ .action(async (name, options) => {
143
+ auth.requireAuth();
144
+ // Prefer whichever was explicitly given; --args carries the '{}' default
145
+ // so it is always defined, hence check --input first.
146
+ const usedInput = options.input !== undefined && (options.args === undefined || options.args === '{}');
147
+ const raw = usedInput ? options.input : options.args;
148
+ const input = parseJSON(raw, usedInput ? '--input' : '--args');
149
+ const body = { tool_name: name, input };
150
+ if (options.reason) body.reason = String(options.reason);
151
+ { const w = missingReasonWarning(name, options.reason); if (w) console.error(w); }
152
+
153
+ // Same org-resolution precedence as `fleet exec` (lib/commands/fleet.js):
154
+ // the collector's /api/v1/tools/execute requires org_id in the body for
155
+ // service-key authenticated callers (isServiceKeyAuth = !req.user,
156
+ // services/collector/server.js ~8326) since there is no session to
157
+ // derive org from. A human-logged-in caller has req.user and the
158
+ // server derives org from the session instead — --org / QUOX_ORG_ID
159
+ // still wins when set explicitly.
160
+ const orgId = auth.getOrgId(program.opts().org || process.env.QUOX_ORG_ID);
161
+ if (!orgId && process.env.QUOX_SERVICE_KEY) {
162
+ throw new Error(
163
+ 'QUOX_SERVICE_KEY is set but no org could be resolved. '
164
+ + 'Set QUOX_ORG_ID=<org> for automation callers, or pass --org <id>.'
165
+ );
166
+ }
167
+ if (orgId) body.org_id = orgId;
168
+
169
+ // QUOXHAT T0 (2026-10-01): automation callers (the collector's
170
+ // CLI-tools chat path) carry the conversation + acting user in env,
171
+ // exactly like QUOX_ORG_ID above. The collector's execute route keys
172
+ // hat-binding resolution on body.session_id and user prefs on
173
+ // body.user_id; without them every CLI-tools execution ran against the
174
+ // org default hat and user-less. Interactive humans have neither set.
175
+ if (process.env.QUOX_SESSION_ID) body.session_id = process.env.QUOX_SESSION_ID;
176
+ if (process.env.QUOX_USER_ID) body.user_id = process.env.QUOX_USER_ID;
177
+ // QUOXHAT #657 P-C.2: forward the ACTING AGENT so a specialist's CLI-tools
178
+ // chat turn executes as that specialist, not the orchestrator. Without it
179
+ // the spawned CLI is a service caller with no agent_id, which the
180
+ // collector defaults to 'quox' — so under enforce a restricted specialist
181
+ // would silently run tools against the orchestrator's broad grant (the
182
+ // "CLI exec silently becomes quox" bypass). The collector sets this from
183
+ // its own selected_agent; a service-key caller's known agent_id is
184
+ // trusted, and the P-B gate then authorizes against the specialist's own
185
+ // grant ceiling ∩ the hat mode.
186
+ if (process.env.QUOX_AGENT_ID) body.agent_id = process.env.QUOX_AGENT_ID;
187
+
188
+ const ep = resolveEndpoint('v1.tools.execute');
189
+ const data = await client.post(ep.path, { service: ep.service, body, timeout: 120000 });
190
+
191
+ if (program.opts().output === 'json') {
192
+ console.log(JSON.stringify(data, null, 2));
193
+ return;
194
+ }
195
+ const result = data && (data.result !== undefined ? data.result : data);
196
+ if (typeof result === 'string') {
197
+ console.log(result);
198
+ } else {
199
+ console.log(JSON.stringify(result, null, 2));
200
+ }
201
+ });
202
+
203
+ tool
204
+ .command('install')
205
+ .description('Install a tool from the catalog')
206
+ .argument('<toolId>', 'catalog tool_id (the "Tool ID" column from `tool catalog`, not its row id)')
207
+ .action(async (toolId) => {
208
+ auth.requireAuth();
209
+ // org_id is derived server-side from the JWT — no :orgId in the path.
210
+ await client.post(resolveEndpoint('tools.install-to-scope').path, {
211
+ service: resolveEndpoint('tools.install-to-scope').service,
212
+ body: { tool_id: toolId },
213
+ });
214
+ printSuccess('Tool installed.');
215
+ });
216
+ }