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,119 @@
1
+ /**
2
+ * Entity commands: CRUD, search, relate
3
+ */
4
+
5
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
6
+ import { parseJSON } from './helpers.js';
7
+ import { resolveEndpoint } from '../api/endpoints.js';
8
+
9
+ export function registerEntityCommands(program, ctx) {
10
+ const { client, auth } = ctx;
11
+
12
+ const entity = program
13
+ .command('entity')
14
+ .description('Manage entities in the knowledge graph');
15
+
16
+ entity
17
+ .command('list')
18
+ .description('List entities')
19
+ .option('--type <type>', 'filter by type')
20
+ .option('--limit <n>', 'max results', '50')
21
+ .action(async (options) => {
22
+ auth.requireAuth();
23
+ const query = { limit: options.limit };
24
+ if (options.type) query.type = options.type;
25
+ const data = await client.get(resolveEndpoint('entity.list').path, { service: resolveEndpoint('entity.list').service, query });
26
+ formatOutput({
27
+ // fleet #392: entities has entity_type/first_seen, not type/created_at.
28
+ columns: ['id', 'entity_type', 'name', 'first_seen'],
29
+ headers: ['ID', 'Type', 'Name', 'First Seen'],
30
+ rows: data.entities || data,
31
+ }, { format: program.opts().output });
32
+ });
33
+
34
+ entity
35
+ .command('get')
36
+ .description('Get entity details')
37
+ .argument('<id>', 'entity ID')
38
+ .action(async (id) => {
39
+ auth.requireAuth();
40
+ const data = await client.get(resolveEndpoint('entity.get', { id }).path, { service: resolveEndpoint('entity.get', { id }).service });
41
+ formatDetail(data.entity || data, { format: program.opts().output });
42
+ });
43
+
44
+ entity
45
+ .command('create')
46
+ .description('Create an entity')
47
+ .requiredOption('--type <type>', 'entity type')
48
+ .requiredOption('--name <name>', 'entity name')
49
+ .option('--metadata <json>', 'metadata as JSON')
50
+ .action(async (options) => {
51
+ auth.requireAuth();
52
+ const body = { type: options.type, name: options.name };
53
+ if (options.metadata) body.metadata = parseJSON(options.metadata, '--metadata');
54
+ await client.post(resolveEndpoint('entity.create').path, { service: resolveEndpoint('entity.create').service, body });
55
+ printSuccess(`Entity "${options.name}" created.`);
56
+ });
57
+
58
+ entity
59
+ .command('delete')
60
+ .description('Delete an entity, soft by default (also removes its relations)')
61
+ .argument('<id>', 'entity ID')
62
+ .option('--hard', 'permanently delete, no soft-delete')
63
+ .action(async (id, options) => {
64
+ auth.requireAuth();
65
+ const query = {};
66
+ if (options.hard) query.hard = 'true';
67
+ await client.delete(resolveEndpoint('entity.delete', { id }).path, { service: resolveEndpoint('entity.delete', { id }).service, query });
68
+ printSuccess('Entity deleted.');
69
+ });
70
+
71
+ entity
72
+ .command('search')
73
+ .description('Search entities')
74
+ .requiredOption('--query <q>', 'search query')
75
+ .option('--type <type>', 'filter by type')
76
+ .action(async (options) => {
77
+ auth.requireAuth();
78
+ const query = { query: options.query };
79
+ if (options.type) query.type = options.type;
80
+ const data = await client.get(resolveEndpoint('entity.search.list').path, { service: resolveEndpoint('entity.search.list').service, query });
81
+ formatOutput({
82
+ // fleet #392: search returns entity_type and bm25(...) AS rank.
83
+ columns: ['id', 'entity_type', 'name', 'rank'],
84
+ headers: ['ID', 'Type', 'Name', 'Rank'],
85
+ rows: data.entities || data.results || data,
86
+ }, { format: program.opts().output });
87
+ });
88
+
89
+ entity
90
+ .command('relate')
91
+ .description('Create a relation between entities')
92
+ .argument('<fromId>', 'source entity ID')
93
+ .argument('<toId>', 'target entity ID')
94
+ .requiredOption('--type <relType>', 'relation type')
95
+ .action(async (fromId, toId, options) => {
96
+ auth.requireAuth();
97
+ await client.post(resolveEndpoint('entity.relation').path, {
98
+ service: resolveEndpoint('entity.relation').service,
99
+ body: { from_id: fromId, to_id: toId, type: options.type },
100
+ });
101
+ printSuccess('Relation created.');
102
+ });
103
+
104
+ entity
105
+ .command('unrelate')
106
+ .description('Delete a relation between entities, soft by default')
107
+ .argument('<relationId>', 'relation ID')
108
+ .option('--hard', 'permanently delete, no soft-delete')
109
+ .action(async (relationId, options) => {
110
+ auth.requireAuth();
111
+ const query = {};
112
+ if (options.hard) query.hard = 'true';
113
+ await client.delete(resolveEndpoint('entity.relation.delete', { id: relationId }).path, {
114
+ service: resolveEndpoint('entity.relation.delete', { id: relationId }).service,
115
+ query,
116
+ });
117
+ printSuccess('Relation deleted.');
118
+ });
119
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `quox facebook` — Facebook Pages plugin verbs, FB-3 (linkedin CLI parity).
3
+ *
4
+ * quox facebook post "<message>" --page <page_id> [--link <url>] [--image <https url>]
5
+ * quox facebook pages [--limit <n>]
6
+ * quox facebook posts --page <page_id> [--limit <n>]
7
+ * quox facebook insights --page <page_id> [--metrics <a,b,c>] [--period <period>]
8
+ * quox facebook lead-forms --page <page_id>
9
+ * quox facebook leads --form <form_id>
10
+ *
11
+ * The write verb executes the facebook_pages connector tool through the
12
+ * governed collector endpoint (POST /api/v1/tools/execute -> connector
13
+ * bridge). The bridge gates it on requiresApproval: it files an owner
14
+ * approval card and returns immediately with
15
+ * { pending:true, status:'awaiting_approval', approvalId, inbox_item_id }.
16
+ * The actual Facebook Graph API write only happens once the owner approves
17
+ * via the dashboard inbox or `quox inbox respond <id> --action approve`.
18
+ * Reads (pages, posts, insights, lead-forms, leads) execute directly.
19
+ *
20
+ * Pages are addressed by numeric Page ID by design: the CLI has no
21
+ * org-scoped default-page lookup yet, so --page/--form are required on
22
+ * every verb rather than silently guessed.
23
+ */
24
+
25
+ import { formatDetail, printSuccess, printInfo } from '../core/output.js';
26
+ import { resolveEndpoint } from '../api/endpoints.js';
27
+ import { resolveOrgId } from './helpers.js';
28
+
29
+ /**
30
+ * Execute one facebook_pages connector tool via the governed collector
31
+ * endpoint. A pending-approval response is a SUCCESS shape here (the card
32
+ * was filed); only a non-pending success:false is an error.
33
+ */
34
+ async function executeFacebookTool({ client, auth, program }, toolName, input) {
35
+ auth.requireAuth();
36
+ const orgId = resolveOrgId(auth, program);
37
+ const ep = resolveEndpoint('v1.tools.execute');
38
+ const body = { tool_name: toolName, input, org_id: orgId };
39
+ const data = await client.post(ep.path, { service: ep.service, body });
40
+ if (data && data.success === false && !data.pending) {
41
+ throw new Error(`${toolName} via POST ${ep.path} returned success:false: ${data.error || 'unknown error'}`);
42
+ }
43
+ return data;
44
+ }
45
+
46
+ function printAwaitingApproval(action, data) {
47
+ const itemRef = data.inbox_item_id || data.approvalId || 'unknown';
48
+ printSuccess(`${action} filed for owner approval - inbox item ${itemRef} (awaiting approval). Nothing publishes until an approver says yes.`);
49
+ if (data.inbox_item_id) {
50
+ printInfo(`Review and approve: quox inbox get ${data.inbox_item_id} (or: quox inbox respond ${data.inbox_item_id} --action approve)`);
51
+ }
52
+ }
53
+
54
+ export function registerFacebookCommands(program, ctx) {
55
+ const { client, auth } = ctx;
56
+
57
+ const facebook = program
58
+ .command('facebook')
59
+ .description('Facebook Pages publishing (organic): governed, HITL-approved via HITL Inbox');
60
+
61
+ // ── facebook post ────────────────────────────────────────────────────────
62
+
63
+ facebook
64
+ .command('post')
65
+ .description('Draft a Facebook Page post. Files an owner approval card and returns immediately; never posts directly.')
66
+ .argument('<message>', 'post text')
67
+ .requiredOption('--page <page_id>', 'Facebook Page ID to post to')
68
+ .option('--link <url>', 'attach a link URL')
69
+ .option('--image <url>', 'attach an image from an https URL; the Graph API fetches it server-side')
70
+ .action(async (message, options) => {
71
+ const input = { page_id: options.page, message };
72
+ if (options.link) input.link = options.link;
73
+ if (options.image) input.image_url = options.image;
74
+
75
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_publish_page_post', input);
76
+
77
+ if (program.opts().output === 'json') {
78
+ formatDetail(data, { format: 'json' });
79
+ return;
80
+ }
81
+ printAwaitingApproval('Post', data);
82
+ });
83
+
84
+ // ── facebook pages (read) ────────────────────────────────────────────────
85
+
86
+ facebook
87
+ .command('pages')
88
+ .description('List the Facebook Pages this token can manage (read-only, no approval needed).')
89
+ .option('--limit <n>', 'max pages to return (max 100)', '25')
90
+ .action(async (options) => {
91
+ const limit = Math.min(parseInt(options.limit, 10) || 25, 100);
92
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_get_pages', { limit });
93
+
94
+ if (program.opts().output === 'json') {
95
+ formatDetail(data, { format: 'json' });
96
+ return;
97
+ }
98
+ const pages = data.result || [];
99
+ if (pages.length === 0) {
100
+ printInfo('No pages returned.');
101
+ return;
102
+ }
103
+ for (const p of pages) {
104
+ console.log(`${p.id} ${p.name} ${p.category || ''} followers:${p.followers ?? 'unknown'}`);
105
+ }
106
+ });
107
+
108
+ // ── facebook posts (read) ────────────────────────────────────────────────
109
+
110
+ facebook
111
+ .command('posts')
112
+ .description('List recent posts on a Facebook Page (read-only, no approval needed).')
113
+ .requiredOption('--page <page_id>', 'Facebook Page ID')
114
+ .option('--limit <n>', 'number of posts (max 50)', '10')
115
+ .action(async (options) => {
116
+ const limit = Math.min(parseInt(options.limit, 10) || 10, 50);
117
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_get_page_posts', {
118
+ page_id: options.page,
119
+ limit,
120
+ });
121
+
122
+ if (program.opts().output === 'json') {
123
+ formatDetail(data, { format: 'json' });
124
+ return;
125
+ }
126
+ const posts = data.result || [];
127
+ if (posts.length === 0) {
128
+ printInfo('No posts returned.');
129
+ return;
130
+ }
131
+ for (const p of posts) {
132
+ console.log(`${p.id} ${p.created_time || ''}`);
133
+ console.log(` ${String(p.message || '').replace(/\n/g, '\n ')}`);
134
+ }
135
+ });
136
+
137
+ // ── facebook insights (read) ─────────────────────────────────────────────
138
+
139
+ facebook
140
+ .command('insights')
141
+ .description('Show organic Page Insights (impressions, engagement, fans) for a Facebook Page.')
142
+ .requiredOption('--page <page_id>', 'Facebook Page ID')
143
+ .option('--metrics <metrics>', 'comma-separated metric names')
144
+ .option('--period <period>', 'day|week|days_28|month|lifetime')
145
+ .action(async (options) => {
146
+ const input = { page_id: options.page };
147
+ if (options.metrics) input.metrics = options.metrics;
148
+ if (options.period) input.period = options.period;
149
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_get_page_insights', input);
150
+
151
+ if (program.opts().output === 'json') {
152
+ formatDetail(data, { format: 'json' });
153
+ return;
154
+ }
155
+ const series = data.result || [];
156
+ if (series.length === 0) {
157
+ printInfo('No insights returned.');
158
+ return;
159
+ }
160
+ for (const m of series) {
161
+ const latest = (m.values || [])[m.values.length - 1];
162
+ console.log(`${m.title || m.name}: ${latest ? JSON.stringify(latest.value) : 'unknown'}`);
163
+ }
164
+ });
165
+
166
+ // ── facebook lead-forms (read) ───────────────────────────────────────────
167
+
168
+ facebook
169
+ .command('lead-forms')
170
+ .description('List the Lead Ads forms attached to a Facebook Page.')
171
+ .requiredOption('--page <page_id>', 'Facebook Page ID')
172
+ .action(async (options) => {
173
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_get_lead_forms', { page_id: options.page });
174
+
175
+ if (program.opts().output === 'json') {
176
+ formatDetail(data, { format: 'json' });
177
+ return;
178
+ }
179
+ const forms = data.result || [];
180
+ if (forms.length === 0) {
181
+ printInfo('No lead forms returned.');
182
+ return;
183
+ }
184
+ for (const f of forms) {
185
+ console.log(`${f.id} ${f.name} status:${f.status || 'unknown'} leads:${f.leads_count ?? 'unknown'}`);
186
+ }
187
+ });
188
+
189
+ // ── facebook leads (read) ────────────────────────────────────────────────
190
+
191
+ facebook
192
+ .command('leads')
193
+ .description('Retrieve submitted leads for a Lead Ads form.')
194
+ .requiredOption('--form <form_id>', 'Lead form ID (from lead-forms)')
195
+ .action(async (options) => {
196
+ const data = await executeFacebookTool({ client, auth, program }, 'facebook_pages_get_leads', { form_id: options.form });
197
+
198
+ if (program.opts().output === 'json') {
199
+ formatDetail(data, { format: 'json' });
200
+ return;
201
+ }
202
+ const leads = data.result || [];
203
+ if (leads.length === 0) {
204
+ printInfo('No leads returned.');
205
+ return;
206
+ }
207
+ for (const l of leads) {
208
+ console.log(`${l.id} ${l.created_time || ''}`);
209
+ console.log(` ${JSON.stringify(l.fields || {})}`);
210
+ }
211
+ });
212
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * File commands: upload, download, list, delete
3
+ */
4
+
5
+ import { createWriteStream } from 'node:fs';
6
+ import { basename } from 'node:path';
7
+ import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
8
+ import { resolveEndpoint } from '../api/endpoints.js';
9
+
10
+ export function registerFileCommands(program, ctx) {
11
+ const { client, auth } = ctx;
12
+
13
+ const file = program
14
+ .command('file')
15
+ .description('Manage files');
16
+
17
+ file
18
+ .command('list')
19
+ .description('List files')
20
+ .action(async () => {
21
+ auth.requireAuth();
22
+ const data = await client.get(resolveEndpoint('files.list').path, { service: resolveEndpoint('files.list').service });
23
+ formatOutput({
24
+ columns: ['id', 'name', 'size', 'type', 'created_at'],
25
+ headers: ['ID', 'Name', 'Size', 'Type', 'Created'],
26
+ rows: data.files || data,
27
+ }, { format: program.opts().output });
28
+ });
29
+
30
+ file
31
+ .command('upload')
32
+ .description('Upload a file')
33
+ .argument('<path>', 'local file path')
34
+ .option('--name <name>', 'override filename')
35
+ .action(async (filePath, options) => {
36
+ auth.requireAuth();
37
+ // fleet #381: node:buffer exports Blob but has NEVER exported
38
+ // FormData (it's a global from undici on Node >= 18). Destructuring
39
+ // it here yielded undefined and shadowed the working global, so
40
+ // `new FormData()` threw "FormData is not a constructor" — this
41
+ // command had never once worked.
42
+ const { Blob } = await import('node:buffer');
43
+ const { readFile } = await import('node:fs/promises');
44
+
45
+ const content = await readFile(filePath);
46
+ const fileName = options.name || basename(filePath);
47
+
48
+ const formData = new FormData();
49
+ // Field name must be 'files' (plural): the files service parses with
50
+ // multer's `upload.array('files', 100)`. Sending 'file' leaves
51
+ // req.files undefined, and the route's `for (const file of req.files)`
52
+ // then throws — measured against the live service as a 500 with zero
53
+ // files stored, vs 200 + 1 stored for 'files'. So this is a genuine
54
+ // second bug: fixing the FormData crash alone would have swapped a
55
+ // client-side crash for a server-side 500, still with no upload.
56
+ formData.append('files', new Blob([content]), fileName);
57
+
58
+ const data = await client.post(resolveEndpoint('files.upload').path, {
59
+ service: resolveEndpoint('files.upload').service,
60
+ body: formData,
61
+ timeout: 120000,
62
+ });
63
+ // The route responds {success, files: [{id, originalName, ...}]}.
64
+ // Report the id the server actually stored, and refuse to claim
65
+ // success when it stored nothing — the old fallback to the local
66
+ // fileName would print "File uploaded" for an empty result set.
67
+ const uploaded = data.files?.[0];
68
+ if (!uploaded?.id) {
69
+ throw new Error(`Upload failed: server accepted the request but stored no file (got: ${JSON.stringify(data).slice(0, 120)})`);
70
+ }
71
+ printSuccess(`File uploaded: ${uploaded.id}${uploaded.originalName ? ` (${uploaded.originalName})` : ''}`);
72
+ });
73
+
74
+ file
75
+ .command('download')
76
+ .description('Download a file')
77
+ .argument('<fileId>', 'file ID')
78
+ // fleet #384: this used to be `--output <path>`, which collided with the
79
+ // global `-o, --output <format>` (table|json|csv|quiet) declared in
80
+ // bin/quox.js. Commander binds the value to the PARENT option, so the
81
+ // subcommand's options.output was always undefined and the file-write
82
+ // branch below was unreachable — every download fell through to stdout.
83
+ // Renamed rather than made clever: guessing whether an --output value is
84
+ // "a format" or "a path" is exactly the kind of heuristic that breaks
85
+ // later. --output never worked here, so nothing can depend on it.
86
+ .option('--dest <path>', 'write the file to this path (the global -o/--output sets output FORMAT, not a path)')
87
+ .action(async (fileId, options) => {
88
+ auth.requireAuth();
89
+ const res = await client.get(resolveEndpoint('file.get', { fileId }).path, { service: resolveEndpoint('file.get', { fileId }).service, raw: true });
90
+
91
+ if (!res.ok) {
92
+ throw new Error(`Download failed: ${res.status}`);
93
+ }
94
+
95
+ const { pipeline } = await import('node:stream/promises');
96
+
97
+ if (options.dest) {
98
+ const writer = createWriteStream(options.dest);
99
+ await pipeline(res.body, writer);
100
+ printSuccess(`Downloaded to ${options.dest}`);
101
+ } else {
102
+ // Stream the raw body. This used to be `res.text()`, which UTF-8
103
+ // decodes: any byte sequence that isn't valid UTF-8 became U+FFFD,
104
+ // so a piped binary came out corrupted AND larger than the original
105
+ // (measured 1.77x on a PNG). Piping the body preserves bytes exactly.
106
+ await pipeline(res.body, process.stdout);
107
+ }
108
+ });
109
+
110
+ file
111
+ .command('delete')
112
+ .description('Delete a file')
113
+ .argument('<fileId>', 'file ID')
114
+ .action(async (fileId) => {
115
+ auth.requireAuth();
116
+ await client.delete(resolveEndpoint('file.delete', { fileId }).path, { service: resolveEndpoint('file.delete', { fileId }).service });
117
+ printSuccess('File deleted.');
118
+ });
119
+
120
+ file
121
+ .command('stats')
122
+ .description('Show file storage stats')
123
+ .action(async () => {
124
+ auth.requireAuth();
125
+ const data = await client.get(resolveEndpoint('files.stats').path, { service: resolveEndpoint('files.stats').service });
126
+ formatDetail(data.stats || data, { format: program.opts().output, title: 'File Storage Stats' });
127
+ });
128
+ }