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,340 @@
1
+ /**
2
+ * `quox seo` — the witnessed SEO warehouse from the terminal (QUOXSEO S2).
3
+ *
4
+ * Every call goes through the collector proxy: the governed boundary that
5
+ * resolves the org and enforces the licence gate. No direct-to-:3110 flag,
6
+ * deliberately (BLUEPRINT defect 5.1: a standalone CLI defaulting to the
7
+ * ungoverned path). The repo-local `quoxseo` bin in quox-seo/ is the
8
+ * OPERATOR tool on the box that hosts the warehouse; this command is the
9
+ * governed parity surface every install gets.
10
+ */
11
+
12
+ import { resolveEndpoint } from '../api/endpoints.js';
13
+
14
+ function annotate(err) {
15
+ const msg = err?.message || String(err);
16
+ if (/not licensed|entitlement/i.test(msg)) {
17
+ err.message = `${msg}\nThe service is refusing you: that is the licence gate working, not a fault.`;
18
+ } else if (/org_gate|org_required/i.test(msg)) {
19
+ err.message = `${msg}\nNo organisation context. Run \`quox auth login\` or select an org first.`;
20
+ } else if (/unavailable/i.test(msg)) {
21
+ err.message = `${msg}\nThe QuoxSEO backend (:3110) is not reachable from the collector on this install.`;
22
+ }
23
+ throw err;
24
+ }
25
+
26
+ async function get(client, id, params = {}) {
27
+ const ep = resolveEndpoint(id, params);
28
+ return client.get(ep.path, { service: ep.service });
29
+ }
30
+
31
+ const short = (h, n = 12) => (typeof h === 'string' ? `${h.slice(0, n)}…` : '-');
32
+
33
+ export function registerQuoxseoCommands(program, ctx) {
34
+ const { client } = ctx;
35
+
36
+ // Honour the global --json / -o json flag on every seo subcommand: if set,
37
+ // emit the raw API payload and skip human rendering (MOAT: `--json seo
38
+ // changes` returned text). Returns true when it handled output.
39
+ const jsonOut = (data) => {
40
+ if (program.opts().output === 'json' || program.opts().json) {
41
+ console.log(JSON.stringify(data, null, 2));
42
+ return true;
43
+ }
44
+ return false;
45
+ };
46
+
47
+ const s = program
48
+ .command('seo')
49
+ .description('QuoxSEO: the URL warehouse with a witnessed history');
50
+
51
+ s.command('status')
52
+ .description('Latest crawl summary: health, coverage, orphans, witness state')
53
+ .action(async () => {
54
+ try {
55
+ const data = await get(client, 'quoxseo.summary');
56
+ if (jsonOut(data)) return;
57
+ const { summary, empty } = data;
58
+ if (empty || !summary) { console.log('Warehouse is empty; the daily ingest has not run yet.'); return; }
59
+ const c = summary.crawl;
60
+ console.log(`crawl #${c.id} ${c.started_at} ${summary.ok_count}/${summary.url_count} ok`);
61
+ console.log(`ward: ${c.ward_seq != null ? `seq ${c.ward_seq} (witnessed ${c.witnessed_at})` : 'UNWITNESSED'}`);
62
+ console.log(`root: ${c.root_hash}`);
63
+ console.log(`orphans ${summary.orphans} · noindex ${summary.noindex} · no meta ${summary.missing_meta_description} · discovered-not-in-sitemap ${summary.discovered_not_in_sitemap}`);
64
+ console.log(`gsc: ${summary.with_gsc} urls with data · ${summary.totals.impressions} impressions · ${summary.totals.clicks} clicks (${c.gsc_range_start}..${c.gsc_range_end})`);
65
+ } catch (err) { annotate(err); }
66
+ });
67
+
68
+ // quox#583: registration parity. The backend was multi-site from day one
69
+ // but the only entrance was an ungoverned loopback curl on the warehouse
70
+ // host; these two verbs are the governed door.
71
+ s.command('sites')
72
+ .description("The org's registered properties")
73
+ .action(async () => {
74
+ try {
75
+ const data = await get(client, 'quoxseo.sites');
76
+ if (jsonOut(data)) return;
77
+ const props = data.properties || [];
78
+ if (!props.length) { console.log('No properties registered. Add one: quox seo add-site <origin>'); return; }
79
+ for (const p of props) {
80
+ console.log(`#${p.id} ${p.origin}${p.gsc_property ? ` gsc:${p.gsc_property}` : ''}`);
81
+ }
82
+ } catch (err) { annotate(err); }
83
+ });
84
+
85
+ s.command('add-site <origin>')
86
+ .description('Register a site for this org (governed write, witnessed at the collector boundary)')
87
+ .option('--gsc <property>', 'GSC property (e.g. sc-domain:example.com)')
88
+ .option('--ga4 <property>', 'GA4 property id')
89
+ .action(async (origin, opts) => {
90
+ try {
91
+ const ep = resolveEndpoint('quoxseo.sites.add');
92
+ const data = await client.post(ep.path, {
93
+ service: ep.service,
94
+ body: { origin, gsc_property: opts.gsc ?? null, ga4_property: opts.ga4 ?? null },
95
+ });
96
+ if (jsonOut(data)) return;
97
+ const site = data.site || {};
98
+ console.log(`registered #${site.id ?? '?'} ${site.origin ?? origin}${site.gsc_property ? ` gsc:${site.gsc_property}` : ''}${site.ga4_property ? ` ga4:${site.ga4_property}` : ''}`);
99
+ console.log('The daily ingest will pick this site up on its next run.');
100
+ } catch (err) { annotate(err); }
101
+ });
102
+
103
+ s.command('urls')
104
+ .description('Warehouse rows for the latest crawl')
105
+ .option('--filter <f>', 'orphans | noindex | errors | no-gsc | discovered')
106
+ .option('--q <text>', 'substring match on path/title')
107
+ .option('--sort <s>', 'impressions')
108
+ .option('--limit <n>', 'max rows', '50')
109
+ .action(async (opts) => {
110
+ try {
111
+ const qs = new URLSearchParams();
112
+ if (opts.filter) qs.set('filter', opts.filter);
113
+ if (opts.q) qs.set('q', opts.q);
114
+ if (opts.sort) qs.set('sort', opts.sort);
115
+ qs.set('limit', opts.limit);
116
+ const data = await get(client, 'quoxseo.urls', { query: `?${qs}` });
117
+ if (jsonOut(data)) return;
118
+ const { urls, total } = data;
119
+ console.log(`${total} rows${urls.length < total ? ` (showing ${urls.length})` : ''}`);
120
+ for (const u of urls) {
121
+ const flags = [u.orphan ? 'ORPHAN' : null, u.in_sitemap ? null : 'NOT-IN-SITEMAP',
122
+ u.meta_robots && /noindex/i.test(u.meta_robots) ? 'NOINDEX' : null].filter(Boolean).join(',');
123
+ console.log(`${String(u.status ?? 'ERR').padEnd(4)} ${u.path} imp=${u.gsc_impressions ?? '-'} pos=${u.gsc_position ?? '-'} inlinks=${u.inlinks_contextual}/${u.inlinks_all}${flags ? ` [${flags}]` : ''}`);
124
+ }
125
+ } catch (err) { annotate(err); }
126
+ });
127
+
128
+ s.command('url <path>')
129
+ .description('Full latest warehouse record for one URL path')
130
+ .action(async (urlPath) => {
131
+ try {
132
+ const { url } = await get(client, 'quoxseo.url.detail', { path: urlPath });
133
+ console.log(JSON.stringify(url, null, 2));
134
+ } catch (err) { annotate(err); }
135
+ });
136
+
137
+ s.command('crawls')
138
+ .description('Ingest runs with root hashes and witness sequence')
139
+ .action(async () => {
140
+ try {
141
+ const data = await get(client, 'quoxseo.crawls');
142
+ if (jsonOut(data)) return;
143
+ const { crawls } = data;
144
+ for (const c of crawls) {
145
+ console.log(`#${c.id} ${c.started_at} ${c.status} ${c.ok_count}/${c.url_count} root ${short(c.root_hash, 16)} ward ${c.ward_seq != null ? `seq ${c.ward_seq}` : 'UNWITNESSED'}`);
146
+ }
147
+ } catch (err) { annotate(err); }
148
+ });
149
+
150
+ s.command('changes')
151
+ .description('Signed change events (the witnessed change log)')
152
+ .option('--crawl <id>', 'events for one crawl instead of the recent log')
153
+ .option('--limit <n>', 'max events', '50')
154
+ .action(async (opts) => {
155
+ try {
156
+ const qs = opts.crawl ? `?crawl_id=${encodeURIComponent(opts.crawl)}` : `?limit=${encodeURIComponent(opts.limit)}`;
157
+ const data = await get(client, 'quoxseo.changes', { query: qs });
158
+ if (jsonOut(data)) return;
159
+ const { changes } = data;
160
+ if (!changes.length) { console.log('No change events yet. The first real site change will land here, signed.'); return; }
161
+ for (const c of changes) {
162
+ console.log(c.type === 'field_changed'
163
+ ? `~ ${c.path} ${c.field}: ${JSON.stringify(c.from)} -> ${JSON.stringify(c.to)} (crawl #${c.crawl_id})`
164
+ : `${c.type === 'url_added' ? '+' : '-'} ${c.path} (crawl #${c.crawl_id})`);
165
+ }
166
+ } catch (err) { annotate(err); }
167
+ });
168
+
169
+ s.command('trends')
170
+ .description('GSC winners & losers over time (period compare: last 28d vs prior 28d)')
171
+ .option('--scope <s>', 'url | query (default: url)', 'url')
172
+ .option('--limit <n>', 'max movers', '25')
173
+ .action(async (opts) => {
174
+ try {
175
+ const qs = new URLSearchParams();
176
+ qs.set('scope', opts.scope);
177
+ qs.set('limit', opts.limit);
178
+ const data = await get(client, 'quoxseo.trends', { query: `?${qs}` });
179
+ if (jsonOut(data)) return;
180
+ if (!data.crawl_id) { console.log('No trend data yet; the ingest with Search Console access has not run.'); return; }
181
+ const w = data.windows;
182
+ if (w) console.log(`period ${w.previous.start}..${w.previous.end} -> ${w.current.start}..${w.current.end}`);
183
+ const rows = opts.scope === 'query' ? data.queries : data.urls;
184
+ const sum = opts.scope === 'query' ? data.query_summary : data.url_summary;
185
+ if (sum) console.log(`winners ${sum.winner} · losers ${sum.loser} · new ${sum.new} · lost ${sum.lost} · flat ${sum.flat}`);
186
+ if (!rows || !rows.length) { console.log('No movers above the impression floor this period.'); return; }
187
+ const arrow = (r) => (r.position_improved ? '↑' : '');
188
+ for (const r of rows) {
189
+ const key = r.key;
190
+ const tag = r.status.toUpperCase().padEnd(7);
191
+ console.log(`${tag} ${key} impr ${r.prev_impressions}→${r.impressions} clk ${r.prev_clicks}→${r.clicks} pos ${r.prev_position ?? '-'}→${r.position ?? '-'} ${arrow(r)}`);
192
+ }
193
+ } catch (err) { annotate(err); }
194
+ });
195
+
196
+ s.command('coverage')
197
+ .description("Google index coverage: what's indexed, and where Google disagrees with the page")
198
+ .option('--filter <f>', 'indexed | not-indexed')
199
+ .option('--limit <n>', 'max rows', '30')
200
+ .action(async (opts) => {
201
+ try {
202
+ const qs = new URLSearchParams();
203
+ if (opts.filter) qs.set('filter', opts.filter);
204
+ qs.set('limit', opts.limit);
205
+ const data = await get(client, 'quoxseo.inspection', { query: `?${qs}` });
206
+ if (jsonOut(data)) return;
207
+ const c = data.coverage || { total: 0 };
208
+ if (!c.total) { console.log('No URL Inspection data yet; the ingest inspects a rolling batch each run.'); return; }
209
+ console.log(`coverage: ${c.indexed}/${c.total} indexed · ${c.not_indexed} not indexed`);
210
+ for (const [state, n] of Object.entries(c.by_state || {}).sort((a, b) => b[1] - a[1])) {
211
+ console.log(` ${String(n).padStart(4)} ${state}`);
212
+ }
213
+ if (data.conflicts?.length) {
214
+ console.log(`\nconflicts (Google vs the page), ${data.conflicts.length}:`);
215
+ for (const x of data.conflicts) console.log(` [${x.severity}] ${x.path} — ${x.type}: ${x.detail}`);
216
+ }
217
+ } catch (err) { annotate(err); }
218
+ });
219
+
220
+ s.command('verify')
221
+ .description('Re-verify the WARD chain, signatures and root hashes server-side')
222
+ .action(async () => {
223
+ try {
224
+ const r = await get(client, 'quoxseo.verify');
225
+ if (jsonOut(r)) return;
226
+ console.log(`chain ${r.chain_status} · ${r.crawls.length} witnessed crawls · ${r.all_ok ? 'ALL OK' : 'FAILURES'}`);
227
+ for (const c of r.crawls) {
228
+ console.log(` crawl #${c.crawl_id} seq ${c.ward_seq} statement ${c.statement_hash_ok ? 'ok' : 'FAIL'} root ${c.root_hash_ok ? 'ok' : 'FAIL'}`);
229
+ }
230
+ if (!r.all_ok) process.exitCode = 1;
231
+ } catch (err) { annotate(err); }
232
+ });
233
+
234
+ // Paid layer (DataForSEO), read-only over data already bought and persisted
235
+ // by the CLI/scheduler on the warehouse host. None of these three trigger a
236
+ // paid DataForSEO call; they only read what has already been stored.
237
+ s.command('keywords')
238
+ .description("Stored keyword universe for the org's primary property (DataForSEO)")
239
+ .option('--limit <n>', 'max rows', '200')
240
+ .action(async (opts) => {
241
+ try {
242
+ const qs = new URLSearchParams();
243
+ qs.set('limit', opts.limit);
244
+ const data = await get(client, 'quoxseo.keywords', { query: `?${qs}` });
245
+ if (jsonOut(data)) return;
246
+ const { property, keywords = [] } = data;
247
+ if (!property) { console.log('No property registered. Add one: quox seo add-site <origin>'); return; }
248
+ if (!keywords.length) { console.log('No keyword data yet. DataForSEO keywords are bought and stored by the CLI/scheduler, not fetched live here.'); return; }
249
+ for (const k of keywords) {
250
+ console.log(`${String(k.volume ?? '-').padStart(7)} ${k.keyword}${k.difficulty != null ? ` kd=${k.difficulty}` : ''}${k.cpc != null ? ` cpc=${k.cpc}` : ''}${k.intent ? ` ${k.intent}` : ''}`);
251
+ }
252
+ } catch (err) { annotate(err); }
253
+ });
254
+
255
+ s.command('gaps [competitor]')
256
+ .description('Competitor keyword and page gaps against the latest witnessed crawl (no argument lists known competitors)')
257
+ .action(async (competitor) => {
258
+ try {
259
+ const qs = competitor ? `?competitor=${encodeURIComponent(competitor)}` : '';
260
+ const data = await get(client, 'quoxseo.gaps', { query: qs });
261
+ if (jsonOut(data)) return;
262
+ if (!data.property) { console.log('No property registered. Add one: quox seo add-site <origin>'); return; }
263
+ if (!competitor) {
264
+ const comps = data.competitors || [];
265
+ if (!comps.length) { console.log('No competitor keyword data yet. That set is bought and stored by the CLI/scheduler, not fetched live here.'); return; }
266
+ for (const c of comps) console.log(`${c.competitor} ${c.keywords} keywords fetched ${c.fetched_at}`);
267
+ return;
268
+ }
269
+ const sum = data.summary || {};
270
+ console.log(`${data.competitor}: ${sum.gap_keywords ?? 0} gap keywords, ${sum.gap_volume ?? 0} combined volume (they beat us ${sum.they_beat_us ?? 0}, we are absent ${sum.we_are_absent ?? 0})`);
271
+ for (const g of data.keyword_gaps || []) {
272
+ console.log(`${String(g.volume ?? '-').padStart(7)} ${g.keyword} them=${g.their_position} us=${g.our_position ?? '-'}`);
273
+ }
274
+ } catch (err) { annotate(err); }
275
+ });
276
+
277
+ s.command('backlinks [competitor]')
278
+ .description('Referring-domain profile, or the link gap against a competitor (no argument shows our own profile)')
279
+ .action(async (competitor) => {
280
+ try {
281
+ const qs = competitor ? `?competitor=${encodeURIComponent(competitor)}` : '';
282
+ const data = await get(client, 'quoxseo.backlinks', { query: qs });
283
+ if (jsonOut(data)) return;
284
+ if (!data.property) { console.log('No property registered. Add one: quox seo add-site <origin>'); return; }
285
+ if (!competitor) {
286
+ console.log(`${data.subject}: ${(data.referring_domains || []).length} referring domains`);
287
+ for (const d of data.referring_domains || []) {
288
+ console.log(` rank=${d.rank ?? '-'} backlinks=${d.backlinks ?? '-'}${d.dofollow ? '' : ' nofollow'} ${d.domain}`);
289
+ }
290
+ if (data.subjects?.length) {
291
+ console.log('\nsubjects with stored data:');
292
+ for (const sub of data.subjects) console.log(` ${sub.subject} ${sub.domains} domains fetched ${sub.fetched_at}`);
293
+ }
294
+ return;
295
+ }
296
+ const sum = data.summary || {};
297
+ console.log(`${data.competitor}: ${sum.missing_domains ?? 0} domains link to them but not us (${sum.missing_backlinks ?? 0} backlinks, top rank ${sum.top_rank ?? 0})`);
298
+ for (const d of data.link_gap || []) console.log(` rank=${d.rank ?? '-'} backlinks=${d.backlinks ?? '-'} ${d.domain}`);
299
+ } catch (err) { annotate(err); }
300
+ });
301
+
302
+ // quox#676: the governed report-delivery endpoint had no CLI verb, so
303
+ // delivering meant calling the collector with the internal service key from
304
+ // inside its container. Unset options are omitted so the server applies its
305
+ // own defaults (recipient = your account email, its default channels).
306
+ const report = s.command('report').description('QuoxSEO reports');
307
+ report.command('deliver')
308
+ .description('Deliver the latest SEO report (email via instance SMTP and/or the inbox; witnessed)')
309
+ .option('--recipient <email>', 'who receives the email (default: your account email)')
310
+ .option('--org <id>', 'org whose warehouse to report on (default: your session org)')
311
+ .option('--channels <list>', 'comma-separated: email,inbox (default: server default)')
312
+ .action(async (opts) => {
313
+ try {
314
+ const ep = resolveEndpoint('quoxseo.report.deliver');
315
+ const body = {};
316
+ if (opts.recipient) body.recipient = opts.recipient;
317
+ if (opts.org) body.org_id = opts.org;
318
+ if (opts.channels) body.channels = String(opts.channels).split(',').map((c) => c.trim()).filter(Boolean);
319
+ const data = await client.post(ep.path, { service: ep.service, body });
320
+ if (jsonOut(data)) return;
321
+ if (data.email) {
322
+ console.log(data.email.success === false
323
+ ? `email: FAILED (${data.email.error || 'no reason given'})`
324
+ : `email: sent${body.recipient ? ` to ${body.recipient}` : ''}`);
325
+ }
326
+ if (data.inbox) console.log(data.inbox.error ? `inbox: FAILED (${data.inbox.error})` : 'inbox: posted');
327
+ if (data.csvIncluded != null) console.log(`csv attached: ${data.csvIncluded ? 'yes' : 'no'}`);
328
+ } catch (err) { annotate(err); }
329
+ });
330
+
331
+ s.command('health')
332
+ .description('Backend health through the governed proxy')
333
+ .action(async () => {
334
+ try {
335
+ console.log(JSON.stringify(await get(client, 'quoxseo.health')));
336
+ } catch (err) { annotate(err); }
337
+ });
338
+ }
339
+
340
+ export default registerQuoxseoCommands;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `quox reach` — an agent's governed blast radius from the terminal
3
+ * (BLAST-RADIUS-P2).
4
+ *
5
+ * Every call goes through the collector's governed route (GET
6
+ * /orgs/:orgId/agents/:agentId/reach), which is the same evaluation the
7
+ * approval gate and the reach_agent agent tool trigger — one runtime
8
+ * (services/collector/lib/reach/runtime.js), witnessed the same way
9
+ * regardless of door. This command doesn't catch API errors itself: they
10
+ * propagate to bin/quox.js's global handleError(), which already prints the
11
+ * real status + the server's own message rather than rendering an error
12
+ * body as data (see lib/core/errors.js).
13
+ */
14
+
15
+ import { resolveEndpoint } from '../api/endpoints.js';
16
+ import { resolveOrgId } from './helpers.js';
17
+
18
+ /** Top N display names for a `may.<source>` items array (credentials/hosts). */
19
+ function topNames(items, n = 3) {
20
+ if (!Array.isArray(items)) return [];
21
+ return items.slice(0, n).map((it) => (it && (it.name || it.id)) || 'unknown');
22
+ }
23
+
24
+ /**
25
+ * Reads the GLOBAL --json flag (program.opts().json, mapped to output=json
26
+ * by bin/quox.js's preAction hook) rather than redefining a local --json
27
+ * option on this subcommand: a subcommand-local option of the same name as
28
+ * a parent's silently shadows the parent's value (commander quirk), so
29
+ * `isJsonOutput` is the same pattern lib/commands/plan.js already uses.
30
+ */
31
+ function isJsonOutput(program) {
32
+ const opts = program.opts();
33
+ return opts.output === 'json' || opts.json === true;
34
+ }
35
+
36
+ export function registerReachCommands(program, ctx) {
37
+ const { client, auth } = ctx;
38
+
39
+ const r = program
40
+ .command('reach')
41
+ .description("An agent's governed blast radius: what it may touch, may not, and what cannot be statically known");
42
+
43
+ r.command('agent <agentId>')
44
+ .description('Compute an agent\'s reach: tools, credentials, hosts, egress origins, delegation')
45
+ .action(async (agentId) => {
46
+ const orgId = resolveOrgId(auth, program);
47
+ const ep = resolveEndpoint('reach.agent.get', { orgId, agentId });
48
+ const result = await client.get(ep.path, { service: ep.service });
49
+
50
+ if (isJsonOutput(program)) {
51
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
52
+ return;
53
+ }
54
+
55
+ const sources = result.sources || {};
56
+ const may = result.may || {};
57
+ const badge = result.badge || { level: 'R0', working: [] };
58
+ const t = sources.tools?.count ?? 0;
59
+ const c = sources.credentials?.count ?? 0;
60
+ const h = sources.hosts?.count ?? 0;
61
+ const e = sources.egress?.count ?? 0;
62
+ const ms = result.duration_ms ?? 0;
63
+
64
+ // House QuoxGlyph grammar, exact separators (U+2502).
65
+ process.stdout.write(
66
+ `\u{1F4A5} REACH │ agent ${agentId} │ ${badge.level} · ${t} tools ${c} creds ${h} hosts ${e} origins · ${ms}ms\n`
67
+ );
68
+
69
+ if (badge.working?.length) {
70
+ process.stdout.write('\nbadge working:\n');
71
+ for (const w of badge.working) process.stdout.write(` · ${w}\n`);
72
+ }
73
+
74
+ process.stdout.write('\nmay:\n');
75
+ process.stdout.write(` tools: ${t}${sources.tools?.ok === false ? ' (source failed)' : ''}\n`);
76
+ const credNames = topNames(may.credentials);
77
+ process.stdout.write(` credentials: ${c}${sources.credentials?.ok === false ? ' (source failed)' : ''}${credNames.length ? ` top: ${credNames.join(', ')}` : ''}\n`);
78
+ const hostNames = topNames(may.hosts);
79
+ process.stdout.write(` hosts: ${h}${sources.hosts?.ok === false ? ' (source failed)' : ''}${hostNames.length ? ` top: ${hostNames.join(', ')}` : ''}\n`);
80
+ process.stdout.write(` origins: ${e}${sources.egress?.ok === false ? ' (source failed)' : ''}\n`);
81
+ const delegateCount = may.delegates?.direct?.length ?? sources.delegation?.count ?? 0;
82
+ process.stdout.write(` delegates: ${delegateCount}${sources.delegation?.ok === false ? ' (source failed)' : ''}\n`);
83
+
84
+ if (result.cannot?.length) {
85
+ process.stdout.write('\ncannot:\n');
86
+ for (const item of result.cannot) process.stdout.write(` × ${item.statement}\n`);
87
+ }
88
+
89
+ if (result.unknown?.length) {
90
+ process.stdout.write('\nunknown:\n');
91
+ for (const item of result.unknown) process.stdout.write(` ? [${item.zone}] ${item.statement}\n`);
92
+ }
93
+ });
94
+
95
+ // BLAST-RADIUS-P4: predicted-vs-actual least-privilege report. Every
96
+ // layer prints its coverage FIRST — 'not-observed' layers (hosts, egress,
97
+ // delegates today) are never rendered as "unused", and never carry a
98
+ // recommendation. See lib/reach/leastPrivilege.js's HONESTY CONTRACT.
99
+ r.command('unused <agentId>')
100
+ .description('Predicted-vs-actual least-privilege report: which granted tools/credentials show zero observed use')
101
+ .option('--days <n>', 'trailing window in days', '30')
102
+ .action(async (agentId, options) => {
103
+ const orgId = resolveOrgId(auth, program);
104
+ const days = parseInt(options.days, 10) || 30;
105
+ const ep = resolveEndpoint('reach.agent.leastPrivilege', { orgId, agentId, days });
106
+ const result = await client.get(ep.path, { service: ep.service });
107
+
108
+ if (isJsonOutput(program)) {
109
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
110
+ return;
111
+ }
112
+
113
+ const layers = result.layers || {};
114
+ const tools = layers.tools || {};
115
+ const creds = layers.credentials || {};
116
+ const ms = result.duration_ms ?? 0;
117
+
118
+ // House QuoxGlyph grammar, exact separators (U+2502).
119
+ process.stdout.write(
120
+ `\u{1F4A5} REACH │ unused ${agentId} ${result.window_days ?? days}d │ tools ${tools.used ?? 0}/${tools.predicted ?? 0} used · creds ${creds.used ?? 0}/${creds.predicted ?? 0} · ${ms}ms\n`
121
+ );
122
+
123
+ if (result.note) {
124
+ process.stdout.write(`\n⚠ ${result.note}\n`);
125
+ }
126
+
127
+ const layerOrder = ['tools', 'credentials', 'hosts', 'egress', 'delegates'];
128
+ for (const name of layerOrder) {
129
+ const layer = layers[name];
130
+ if (!layer) continue;
131
+ process.stdout.write(`\n${name}:\n`);
132
+ process.stdout.write(` coverage: ${layer.coverage}\n`);
133
+ if (layer.reason) process.stdout.write(` reason: ${layer.reason}\n`);
134
+ if (Array.isArray(layer.gaps) && layer.gaps.length) {
135
+ for (const gap of layer.gaps) process.stdout.write(` gap: ${gap}\n`);
136
+ }
137
+ if (typeof layer.predicted === 'number') process.stdout.write(` predicted: ${layer.predicted}\n`);
138
+ if (typeof layer.used === 'number') process.stdout.write(` used: ${layer.used}\n`);
139
+ if (Array.isArray(layer.unused_list) && layer.unused_list.length) {
140
+ process.stdout.write(` unused: ${layer.unused_list.join(', ')}\n`);
141
+ }
142
+ }
143
+
144
+ if (result.recommendations?.length) {
145
+ process.stdout.write('\nrecommendations:\n');
146
+ for (const rec of result.recommendations) {
147
+ process.stdout.write(` · [${rec.layer}] ${rec.statement}\n`);
148
+ process.stdout.write(` edit: ${rec.edit}\n`);
149
+ }
150
+ } else {
151
+ process.stdout.write('\nrecommendations: none\n');
152
+ }
153
+ });
154
+
155
+ return r;
156
+ }
157
+
158
+ export default registerReachCommands;