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,528 @@
1
+ /**
2
+ * VOLT commands: verify evidence bundles, inspect chains, export reports
3
+ *
4
+ * quox volt <subcommand> [options]
5
+ *
6
+ * Works with VOLT evidence bundle files (JSON) and raw NDJSON chain exports.
7
+ */
8
+
9
+ import chalk from 'chalk';
10
+ import { readFile, writeFile } from 'fs/promises';
11
+ import { printSuccess, printWarning, printInfo, formatDetail } from '../core/output.js';
12
+ import { resolveEndpoint } from '../api/endpoints.js';
13
+
14
+ /**
15
+ * Peel a fetched VOLT payload down to the verifiable bundle shape
16
+ * ({ manifest, events_ndjson?, signature? }), not the storage row around it.
17
+ * `/volt/runs/:runId/bundle` returns { bundle: <db row> } with the actual
18
+ * bundle nested in `row.data`; `/volt/bundles/:bundleId/download` returns the
19
+ * bundle directly. Shared by `volt fetch` for both the initial fetch and the
20
+ * follow-up download after `volt.bundle.create` (fleet #447).
21
+ */
22
+ function unwrapVoltBundle(data) {
23
+ let bundle = (data && data.bundle) ? data.bundle : data;
24
+ if (bundle && bundle.data && typeof bundle.data === 'object' && bundle.data.manifest) {
25
+ bundle = bundle.data;
26
+ }
27
+ return bundle;
28
+ }
29
+
30
+ export function registerVoltCommands(program, ctx = {}) {
31
+ const { client, auth } = ctx;
32
+
33
+ const volt = program
34
+ .command('volt')
35
+ .description('VOLT evidence bundle verification and inspection');
36
+
37
+ // ── volt verify ─────────────────────────────────────────────────────────────
38
+
39
+ volt
40
+ .command('verify')
41
+ .description('Verify an evidence bundle or NDJSON chain file')
42
+ .argument('<file>', 'path to bundle JSON or events NDJSON file')
43
+ .option('--strict', 'fail on warnings too; unsigned or unpinned signatures fail')
44
+ .option('--key <pubkey|file>', 'trusted Ed25519 public key (base64 raw, or file containing it); repeatable', (v, acc) => (acc || []).concat(v))
45
+ .option('--trusted-keys <file>', 'JSON file: array of base64 public keys or {publicKey, keyId?} objects')
46
+ .action(async (file, options) => {
47
+ const raw = await readFile(file, 'utf8');
48
+
49
+ // Detect if this is a bundle (JSON with manifest) or raw NDJSON
50
+ const isBundle = raw.trimStart().startsWith('{');
51
+
52
+ if (isBundle) {
53
+ // fleet #447: a persisted per-run bundle can be manifest_only (no
54
+ // events_ndjson, see voltContentStore.js). Handing that straight to
55
+ // verifyEvidenceBundle produced a bare MISSING_EVENTS error even
56
+ // though the engine is working correctly, there is simply nothing to
57
+ // chain-verify. Catch it here and name the remedy instead.
58
+ let manifestOnly = null;
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ const hasEvents = typeof parsed.events_ndjson === 'string' && parsed.events_ndjson.trim().length > 0;
62
+ if (parsed && parsed.manifest && !hasEvents) {
63
+ manifestOnly = parsed.manifest;
64
+ }
65
+ } catch {
66
+ // Not valid JSON, let verifyEvidenceBundle report its own PARSE_ERROR below.
67
+ }
68
+
69
+ if (manifestOnly) {
70
+ console.log('');
71
+ console.log(chalk.bold('VOLT Evidence Bundle Verification'));
72
+ console.log(chalk.gray('─'.repeat(40)));
73
+ console.log('');
74
+ const mode = manifestOnly.bundle_mode ? ` (bundle_mode: ${manifestOnly.bundle_mode})` : '';
75
+ printWarning(`Manifest-only bundle${mode}: no events_ndjson embedded, so there is no chain to verify.`);
76
+ const remedy = manifestOnly.run_id
77
+ ? `quox volt fetch ${manifestOnly.run_id}`
78
+ : 'quox volt fetch <run_id>';
79
+ printInfo(`Remedy: ${remedy} to build a verifiable bundle.`);
80
+ console.log('');
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+
85
+ const { verifyEvidenceBundle } = await import('../../../src/lib/volt/volt-verify.js');
86
+ const trustedKeys = [];
87
+ const addKey = (k) => {
88
+ if (typeof k === 'string') trustedKeys.push({ publicKey: k.trim() });
89
+ else if (k && k.publicKey) trustedKeys.push({ publicKey: k.publicKey, ...(k.keyId ? { keyId: k.keyId } : {}) });
90
+ };
91
+ try {
92
+ for (const k of options.key || []) {
93
+ let v = k;
94
+ try { v = (await readFile(k, 'utf8')).trim(); } catch { /* literal key */ }
95
+ addKey(v);
96
+ }
97
+ if (options.trustedKeys) {
98
+ const list = JSON.parse(await readFile(options.trustedKeys, 'utf8'));
99
+ (Array.isArray(list) ? list : list.keys || []).forEach(addKey);
100
+ }
101
+ } catch (err) {
102
+ console.log(chalk.red(`Could not load trusted keys: ${err.message}`));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const result = await verifyEvidenceBundle(raw, {
107
+ strict: !!options.strict,
108
+ ...(trustedKeys.length ? { keys: trustedKeys } : {})
109
+ });
110
+
111
+ console.log('');
112
+ console.log(chalk.bold('VOLT Evidence Bundle Verification'));
113
+ console.log(chalk.gray('─'.repeat(40)));
114
+
115
+ if (result.summary.bundle_id) {
116
+ console.log(` Bundle: ${chalk.cyan(result.summary.bundle_id)}`);
117
+ }
118
+ if (result.summary.run_id) {
119
+ console.log(` Run: ${chalk.cyan(result.summary.run_id)}`);
120
+ }
121
+ console.log(` Version: ${result.summary.volt_version || 'unknown'}`);
122
+ console.log(` Events: ${result.summary.event_count}`);
123
+ console.log(` Chain: ${result.summary.chain_valid ? chalk.green('PASS') : chalk.red('FAIL')}`);
124
+ {
125
+ const sigs = result.summary.signatures || [];
126
+ if (sigs.length === 0) {
127
+ console.log(` Signature: ${chalk.yellow('NONE')} (bundle is unsigned)`);
128
+ }
129
+ for (const sg of sigs) {
130
+ const key = `${String(sg.key_id || 'unknown').slice(0, 4)}...`;
131
+ if (sg.valid) {
132
+ const how = sg.trust === 'trusted' ? 'trusted' : 'embedded, not pinned';
133
+ console.log(` Signature: ${chalk.green('VALID')} (key ${key}, ${how})`);
134
+ } else {
135
+ console.log(` Signature: ${chalk.red('INVALID')} (key ${key}, see errors)`);
136
+ }
137
+ }
138
+ }
139
+ console.log(` Timestamps: ${result.summary.timestamps_checked || 0}`);
140
+
141
+ if (result.errors.length > 0) {
142
+ console.log('');
143
+ console.log(chalk.red(` ${result.errors.length} error(s):`));
144
+ for (const err of result.errors) {
145
+ console.log(chalk.red(` ✗ [${err.code}] ${err.message}`));
146
+ if (err.detail) console.log(chalk.gray(` ${err.detail}`));
147
+ }
148
+ }
149
+ if (result.warnings.length > 0) {
150
+ console.log('');
151
+ console.log(chalk.yellow(` ${result.warnings.length} warning(s):`));
152
+ for (const w of result.warnings) {
153
+ console.log(chalk.yellow(` ⚠ [${w.code}] ${w.message}`));
154
+ }
155
+ }
156
+
157
+ console.log('');
158
+ const pass = result.valid && (!options.strict || result.warnings.length === 0);
159
+ if (pass) {
160
+ printSuccess(`RESULT: PASS — bundle is valid (${result.summary.event_count} events verified)`);
161
+ } else {
162
+ console.log(chalk.red.bold(` RESULT: FAIL`));
163
+ process.exitCode = 1;
164
+ }
165
+ } else {
166
+ // Raw NDJSON chain
167
+ const { verifyNDJSON, formatReport } = await import('../../../src/lib/volt/volt-verifier.js');
168
+ const result = await verifyNDJSON(raw);
169
+ console.log('');
170
+ console.log(formatReport(result));
171
+ console.log('');
172
+
173
+ if (result.result === 'PASS') {
174
+ printSuccess('Chain verified successfully');
175
+ } else {
176
+ console.log(chalk.red.bold('Chain verification FAILED'));
177
+ process.exitCode = 1;
178
+ }
179
+ }
180
+ });
181
+
182
+ // ── volt inspect ────────────────────────────────────────────────────────────
183
+
184
+ volt
185
+ .command('inspect')
186
+ .description('Inspect events inside a bundle or NDJSON file')
187
+ .argument('<file>', 'path to bundle JSON or events NDJSON file')
188
+ .option('--events', 'list all events')
189
+ .option('--event <seq>', 'show a single event by sequence number', parseInt)
190
+ .option('--manifest', 'show bundle manifest only')
191
+ .action(async (file, options) => {
192
+ const raw = await readFile(file, 'utf8');
193
+ const isBundle = raw.trimStart().startsWith('{');
194
+
195
+ let events;
196
+ let manifest = null;
197
+
198
+ if (isBundle) {
199
+ const parsed = JSON.parse(raw);
200
+ manifest = parsed.manifest;
201
+ events = parsed.events_ndjson
202
+ .trim()
203
+ .split('\n')
204
+ .filter(l => l.trim())
205
+ .map(l => JSON.parse(l));
206
+ } else {
207
+ events = raw.trim().split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
208
+ }
209
+
210
+ // --manifest: show just the bundle manifest
211
+ if (options.manifest) {
212
+ if (!manifest) {
213
+ printWarning('Not a bundle file — no manifest available');
214
+ return;
215
+ }
216
+ formatDetail(manifest, {
217
+ format: program.opts().output,
218
+ title: `Bundle Manifest: ${manifest.bundle_id || 'unknown'}`,
219
+ fields: [
220
+ 'bundle_id', 'run_id', 'volt_version', 'event_count', 'hash_alg',
221
+ 'first_event_hash', 'last_event_hash', 'merkle_root',
222
+ 'agents_involved', 'run_status', 'started_at', 'completed_at',
223
+ 'signed', 'redactions_present', 'created_ts'
224
+ ],
225
+ });
226
+ return;
227
+ }
228
+
229
+ // --event <seq>: show a single event
230
+ if (options.event != null) {
231
+ const ev = events.find(e => e.seq === options.event);
232
+ if (!ev) {
233
+ printWarning(`No event with seq=${options.event} (range: 1-${events.length})`);
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ console.log(JSON.stringify(ev, null, 2));
238
+ return;
239
+ }
240
+
241
+ // --events or default: list events summary
242
+ console.log('');
243
+ if (manifest) {
244
+ console.log(chalk.bold(`Bundle: ${manifest.bundle_id || 'N/A'}`));
245
+ console.log(chalk.gray(`Run: ${manifest.run_id || 'N/A'} — ${events.length} events`));
246
+ } else {
247
+ console.log(chalk.bold(`NDJSON Chain — ${events.length} events`));
248
+ if (events.length > 0) {
249
+ console.log(chalk.gray(`Run: ${events[0].run_id || 'N/A'}`));
250
+ }
251
+ }
252
+ console.log('');
253
+
254
+ // Table header
255
+ console.log(
256
+ chalk.gray(' SEQ ') +
257
+ chalk.gray('EVENT TYPE'.padEnd(28)) +
258
+ chalk.gray('ACTOR'.padEnd(20)) +
259
+ chalk.gray('HASH (first 12)')
260
+ );
261
+ console.log(chalk.gray(' ' + '─'.repeat(75)));
262
+
263
+ for (const ev of events) {
264
+ const seqStr = String(ev.seq).padStart(4);
265
+ const typeStr = (ev.event_type || 'unknown').padEnd(28);
266
+ const actor = (ev.actor?.actor_id || '—').padEnd(20);
267
+ const hashShort = ev.hash ? ev.hash.slice(0, 12) + '…' : '—';
268
+
269
+ console.log(` ${seqStr} ${typeStr}${actor}${chalk.gray(hashShort)}`);
270
+ }
271
+ console.log('');
272
+ printInfo(`${events.length} events total`);
273
+ });
274
+
275
+ // ── volt export ─────────────────────────────────────────────────────────────
276
+
277
+ volt
278
+ .command('export')
279
+ .description('Re-export a bundle in a different format')
280
+ .argument('<file>', 'path to bundle JSON file')
281
+ .option('--ndjson <outfile>', 'export events as NDJSON')
282
+ .option('--manifest <outfile>', 'export manifest as JSON')
283
+ .option('--pretty', 'pretty-print JSON output')
284
+ .action(async (file, options) => {
285
+ const raw = await readFile(file, 'utf8');
286
+ const parsed = JSON.parse(raw);
287
+
288
+ if (!parsed.manifest || !parsed.events_ndjson) {
289
+ printWarning('File does not appear to be a VOLT evidence bundle');
290
+ process.exitCode = 1;
291
+ return;
292
+ }
293
+
294
+ if (options.ndjson) {
295
+ await writeFile(options.ndjson, parsed.events_ndjson);
296
+ printSuccess(`Events exported to ${options.ndjson} (${parsed.manifest.event_count} events)`);
297
+ }
298
+
299
+ if (options.manifest) {
300
+ const content = options.pretty
301
+ ? JSON.stringify(parsed.manifest, null, 2)
302
+ : JSON.stringify(parsed.manifest);
303
+ await writeFile(options.manifest, content + '\n');
304
+ printSuccess(`Manifest exported to ${options.manifest}`);
305
+ }
306
+
307
+ if (!options.ndjson && !options.manifest) {
308
+ printInfo('Specify --ndjson <outfile> and/or --manifest <outfile> to export');
309
+ console.log('');
310
+ console.log('Examples:');
311
+ console.log(' quox volt export bundle.json --ndjson events.ndjson');
312
+ console.log(' quox volt export bundle.json --manifest manifest.json --pretty');
313
+ console.log(' quox volt export bundle.json --ndjson events.ndjson --manifest manifest.json');
314
+ }
315
+ });
316
+
317
+ // ── volt fetch ──────────────────────────────────────────────────────────────
318
+ //
319
+ // 2026-08-19 (proof-loop). `quox volt verify` only accepts a LOCAL FILE, and
320
+ // no verb existed to obtain one from the running instance. The collector has
321
+ // served /volt/runs/:runId/bundle and /volt/bundles/:bundleId/download all
322
+ // along with nothing wired to them, so the chain a customer needs —
323
+ // run -> bundle -> verify — was broken at exactly the step where they check
324
+ // a receipt for themselves. That is the "keep the receipt" promise, and it
325
+ // could not be kept from the canonical CLI.
326
+
327
+ volt
328
+ .command('fetch')
329
+ .description('Download a run\'s evidence bundle so `quox volt verify` can check it')
330
+ .argument('<runId>', 'VOLT run id (see `quox governance cert list`)')
331
+ .option('--out <file>', 'write the bundle here (default: <runId>-bundle.json)')
332
+ .option('--bundle-id <id>', 'fetch a specific bundle id instead of the run\'s current one')
333
+ .option('--manifest-only', 'write the persisted manifest-only bundle as-is, skip building a verifiable one')
334
+ .action(async (runId, options) => {
335
+ auth.requireAuth();
336
+
337
+ const ep = options.bundleId
338
+ ? resolveEndpoint('volt.bundle.download', { bundleId: options.bundleId })
339
+ : resolveEndpoint('volt.run.bundle.get', { runId });
340
+
341
+ const data = await client.get(ep.path, { service: ep.service });
342
+
343
+ // Unwrap to the VERIFIABLE bundle, not the storage row around it.
344
+ //
345
+ // /volt/runs/:runId/bundle returns { bundle: <db row> } where the row is
346
+ // { bundle_id, run_id, created_ts, data, stored_at } and the thing
347
+ // `quox volt verify` actually needs (manifest + events_ndjson +
348
+ // signatures) is nested in `data`. Writing the row produced a file that
349
+ // failed verification with MISSING_MANIFEST — the verifier was right and
350
+ // the fetch was wrong. Peel both layers, and only accept the result if it
351
+ // carries a manifest, so this can never hand someone a file that looks
352
+ // like evidence and is not.
353
+ let bundle = unwrapVoltBundle(data);
354
+ if (!bundle) {
355
+ printWarning(`No bundle found for ${options.bundleId || runId}`);
356
+ process.exitCode = 1;
357
+ return;
358
+ }
359
+ if (!bundle.manifest) {
360
+ printWarning(`Fetched payload for ${options.bundleId || runId} has no manifest — refusing to write a file that would fail verification.`);
361
+ printInfo(`Payload keys: ${Object.keys(bundle).join(', ')}`);
362
+ process.exitCode = 1;
363
+ return;
364
+ }
365
+
366
+ const hasEvents = typeof bundle.events_ndjson === 'string' && bundle.events_ndjson.trim().length > 0;
367
+
368
+ // fleet #447: the bundle persisted at run completion is bundle_mode:
369
+ // 'manifest_only' (see voltContentStore.js), so a plain fetch handed
370
+ // `quox volt verify` a file it could never chain-verify. Build the full
371
+ // bundle on demand instead, unless the caller asked for a specific
372
+ // bundle id (that bundle is what it is) or opted out with
373
+ // --manifest-only.
374
+ if (!hasEvents) {
375
+ if (options.bundleId) {
376
+ const out = options.out || `${options.bundleId}-bundle.json`;
377
+ await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
378
+ const mode = bundle.manifest.bundle_mode ? ` (bundle_mode: ${bundle.manifest.bundle_mode})` : '';
379
+ printSuccess(`Bundle written to ${out}`);
380
+ printWarning(`Bundle ${options.bundleId} is manifest-only${mode}, no events_ndjson to verify.`);
381
+ const runHint = bundle.manifest.run_id;
382
+ printInfo(runHint ? `For a verifiable bundle: quox volt fetch ${runHint}` : 'Fetch by run id for a verifiable bundle.');
383
+ return;
384
+ }
385
+
386
+ if (options.manifestOnly) {
387
+ const out = options.out || `${runId}-bundle.json`;
388
+ await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
389
+ printSuccess(`Bundle written to ${out}`);
390
+ printWarning('This is a manifest-only bundle: `quox volt verify` cannot chain-verify it (no events_ndjson).');
391
+ return;
392
+ }
393
+
394
+ printInfo(`Persisted bundle for ${runId} is manifest-only, building a verifiable bundle from stored events.`);
395
+
396
+ const createEp = resolveEndpoint('volt.bundle.create');
397
+ const created = await client.post(createEp.path, { service: createEp.service, body: { run_id: runId } });
398
+ const bundleId = created && created.bundle_id;
399
+ if (!bundleId) {
400
+ printWarning(`Could not build a verifiable bundle for ${runId}.`);
401
+ process.exitCode = 1;
402
+ return;
403
+ }
404
+
405
+ const downloadEp = resolveEndpoint('volt.bundle.download', { bundleId });
406
+ const fullData = await client.get(downloadEp.path, { service: downloadEp.service });
407
+ const fullBundle = unwrapVoltBundle(fullData);
408
+ if (!fullBundle || !fullBundle.manifest) {
409
+ printWarning(`Built bundle ${bundleId} but could not download it, no manifest in the response.`);
410
+ process.exitCode = 1;
411
+ return;
412
+ }
413
+
414
+ const out = options.out || `${runId}-bundle.json`;
415
+ await writeFile(out, JSON.stringify(fullBundle, null, 2) + '\n');
416
+ const eventCount = fullBundle.manifest.event_count;
417
+ printSuccess(`Bundle ${bundleId} written to ${out}${eventCount != null ? ` (${eventCount} events)` : ''}`);
418
+ printInfo(`Verify it: quox volt verify ${out}`);
419
+ return;
420
+ }
421
+
422
+ const out = options.out || `${options.bundleId || runId}-bundle.json`;
423
+ await writeFile(out, JSON.stringify(bundle, null, 2) + '\n');
424
+
425
+ const events = bundle.manifest && bundle.manifest.event_count;
426
+ printSuccess(`Bundle written to ${out}${events != null ? ` (${events} events)` : ''}`);
427
+ printInfo(`Verify it: quox volt verify ${out}`);
428
+ });
429
+
430
+ // ── volt summary ────────────────────────────────────────────────────────────
431
+
432
+ volt
433
+ .command('summary')
434
+ .description('Quick summary of a bundle (no verification)')
435
+ .argument('<file>', 'path to bundle JSON file')
436
+ .action(async (file) => {
437
+ const raw = await readFile(file, 'utf8');
438
+ const parsed = JSON.parse(raw);
439
+
440
+ if (!parsed.manifest) {
441
+ printWarning('File does not appear to be a VOLT evidence bundle');
442
+ process.exitCode = 1;
443
+ return;
444
+ }
445
+
446
+ const m = parsed.manifest;
447
+ const events = parsed.events_ndjson
448
+ ?.trim()
449
+ .split('\n')
450
+ .filter(l => l.trim()) || [];
451
+
452
+ // Event type breakdown
453
+ const typeCounts = {};
454
+ for (const line of events) {
455
+ try {
456
+ const ev = JSON.parse(line);
457
+ typeCounts[ev.event_type] = (typeCounts[ev.event_type] || 0) + 1;
458
+ } catch { /* skip */ }
459
+ }
460
+
461
+ console.log('');
462
+ console.log(chalk.bold('VOLT Bundle Summary'));
463
+ console.log(chalk.gray('─'.repeat(40)));
464
+ console.log(` Bundle ID: ${chalk.cyan(m.bundle_id || 'N/A')}`);
465
+ console.log(` Run ID: ${chalk.cyan(m.run_id || 'N/A')}`);
466
+ console.log(` Version: ${m.volt_version || 'N/A'}`);
467
+ console.log(` Status: ${m.run_status || 'N/A'}`);
468
+ console.log(` Events: ${m.event_count || events.length}`);
469
+ console.log(` Agents: ${(m.agents_involved || []).join(', ') || 'none'}`);
470
+ console.log(` Policies: ${(m.policy_ids_evaluated || []).length} evaluated`);
471
+ console.log(` HITL: ${m.hitl_decisions || 0} decisions`);
472
+ console.log(` Signed: ${m.signed ? 'yes' : 'no'}`);
473
+ console.log(` Redacted: ${m.redactions_present ? 'yes' : 'no'}`);
474
+ console.log(` Created: ${m.created_ts || 'N/A'}`);
475
+ console.log(` Started: ${m.started_at || 'N/A'}`);
476
+ console.log(` Completed: ${m.completed_at || 'N/A'}`);
477
+
478
+ if (Object.keys(typeCounts).length > 0) {
479
+ console.log('');
480
+ console.log(chalk.bold(' Event Types:'));
481
+ const sorted = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
482
+ for (const [type, count] of sorted) {
483
+ console.log(` ${String(count).padStart(4)} ${type}`);
484
+ }
485
+ }
486
+ console.log('');
487
+ });
488
+
489
+ // ── volt accept / acceptance ────────────────────────────────────────────────
490
+ // SEO-MOAT (owner ruling 2026-09-03): the "did it work" layer. The chain
491
+ // proves what happened; this records whether it achieved its intent — an
492
+ // attestation, not automated evaluation. The server derives WHO attested
493
+ // from the credential class (human/agent/service) and witnesses the
494
+ // verdict as its own VOLT run pinning the target run's head_hash, so an
495
+ // agent marking its own work "pass" is visible as exactly that.
496
+
497
+ volt
498
+ .command('accept')
499
+ .description('Attest a terminal run\'s outcome: did it achieve its intent?')
500
+ .argument('<runId>', 'VOLT run id (VR_...) — see `quox volt fetch`')
501
+ .requiredOption('--verdict <verdict>', "'pass' or 'fail'")
502
+ .option('--reason <text>', 'what you checked and what you saw')
503
+ .action(async (runId, options) => {
504
+ auth.requireAuth();
505
+ const ep = resolveEndpoint('volt.run.acceptance.post', { runId });
506
+ const data = await client.post(ep.path, {
507
+ service: ep.service,
508
+ body: { verdict: options.verdict, reason: options.reason || null },
509
+ });
510
+ const v = data.verdict || {};
511
+ printSuccess(`Acceptance recorded: ${v.verdict} on ${runId} (attested by ${v.actor_type}.${v.actor_id})`);
512
+ if (v.evidence_run_id) printInfo(`Witnessed as VOLT run ${v.evidence_run_id} (pins target head_hash ${String(v.target_head_hash || '').slice(0, 12)}...)`);
513
+ });
514
+
515
+ volt
516
+ .command('acceptance')
517
+ .description('Show a run\'s acceptance verdict history (latest is the standing verdict)')
518
+ .argument('<runId>', 'VOLT run id (VR_...)')
519
+ .action(async (runId) => {
520
+ auth.requireAuth();
521
+ const ep = resolveEndpoint('volt.run.acceptance.get', { runId });
522
+ const data = await client.get(ep.path, { service: ep.service });
523
+ formatDetail(data.latest, { format: program.opts().output });
524
+ if ((data.history || []).length > 1) {
525
+ printInfo(`${data.history.length} verdicts total — latest supersedes; history is append-only.`);
526
+ }
527
+ });
528
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Watch commands — real-time monitoring dashboards
3
+ *
4
+ * quox watch fleet Live fleet status (polling)
5
+ * quox watch services Live service health
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { resolveEndpoint } from '../api/endpoints.js';
10
+
11
+ export function registerWatchCommands(program, { client, auth, config }) {
12
+ const watch = program
13
+ .command('watch')
14
+ .description('Real-time monitoring dashboards');
15
+
16
+ watch
17
+ .command('fleet')
18
+ .description('Live fleet status dashboard')
19
+ .option('--interval <seconds>', 'Refresh interval', '5')
20
+ .action(async (opts) => {
21
+ auth.requireAuth();
22
+ const interval = Math.max(2, parseInt(opts.interval, 10) || 5) * 1000;
23
+
24
+ console.log(chalk.cyan('Fleet Watch') + chalk.dim(' (Ctrl+C to stop)\n'));
25
+
26
+ const tick = async () => {
27
+ try {
28
+ const data = await client.get(resolveEndpoint('v1.fleet.status.list').path, { service: resolveEndpoint('v1.fleet.status.list').service });
29
+ const agents = data.agents || [];
30
+
31
+ // Clear screen and redraw
32
+ process.stdout.write('\x1B[2J\x1B[H');
33
+ console.log(chalk.cyan.bold('QuoxCLI Fleet Watch') + chalk.dim(` ${new Date().toLocaleTimeString()} (Ctrl+C to stop)`));
34
+ console.log(chalk.dim('─'.repeat(72)));
35
+
36
+ if (agents.length === 0) {
37
+ console.log(chalk.yellow(' No agents reporting'));
38
+ } else {
39
+ const header = ` ${'HOST'.padEnd(16)} ${'STATUS'.padEnd(10)} ${'CPU'.padEnd(7)} ${'MEM'.padEnd(7)} ${'UPTIME'.padEnd(10)} ${'VERSION'}`;
40
+ console.log(chalk.dim(header));
41
+ console.log(chalk.dim(' ' + '─'.repeat(68)));
42
+
43
+ for (const a of agents) {
44
+ const statusColor = a.status === 'healthy' ? chalk.green : a.status === 'degraded' ? chalk.yellow : chalk.red;
45
+ const cpuColor = (a.cpu_percent || 0) > 80 ? chalk.red : (a.cpu_percent || 0) > 60 ? chalk.yellow : chalk.white;
46
+ const memColor = (a.memory_percent || 0) > 80 ? chalk.red : (a.memory_percent || 0) > 60 ? chalk.yellow : chalk.white;
47
+
48
+ console.log(
49
+ ` ${chalk.white(String(a.host_id || '').padEnd(16))} ` +
50
+ `${statusColor(String(a.status || 'unknown').padEnd(10))} ` +
51
+ `${cpuColor(String((a.cpu_percent || 0) + '%').padEnd(7))} ` +
52
+ `${memColor(String((a.memory_percent || 0) + '%').padEnd(7))} ` +
53
+ `${String(a.uptime || '-').padEnd(10)} ` +
54
+ `${chalk.dim(a.version || '-')}`
55
+ );
56
+ }
57
+ }
58
+
59
+ console.log(chalk.dim('\n ' + '─'.repeat(68)));
60
+ console.log(chalk.dim(` ${agents.length} agents | refreshing every ${interval / 1000}s`));
61
+ } catch (err) {
62
+ console.error(chalk.red(` Error: ${err.message}`));
63
+ }
64
+ };
65
+
66
+ await tick();
67
+ const timer = setInterval(tick, interval);
68
+ process.on('SIGINT', () => { clearInterval(timer); process.exit(0); });
69
+ });
70
+
71
+ watch
72
+ .command('services')
73
+ .description('Live service health dashboard')
74
+ .option('--interval <seconds>', 'Refresh interval', '10')
75
+ .action(async (opts) => {
76
+ const interval = Math.max(5, parseInt(opts.interval, 10) || 10) * 1000;
77
+ const services = [
78
+ { name: 'auth', port: 3101, path: '/health' },
79
+ { name: 'collector', port: 9848, path: '/health' },
80
+ { name: 'memory', port: 3103, path: '/health' },
81
+ { name: 'files', port: 3102, path: '/health' },
82
+ { name: 'orchestrator', port: 3100, path: '/health' },
83
+ ];
84
+
85
+ const tick = async () => {
86
+ process.stdout.write('\x1B[2J\x1B[H');
87
+ console.log(chalk.cyan.bold('QuoxCLI Service Watch') + chalk.dim(` ${new Date().toLocaleTimeString()} (Ctrl+C to stop)`));
88
+ console.log(chalk.dim('─'.repeat(50)));
89
+
90
+ for (const svc of services) {
91
+ try {
92
+ const svcUrl = config.getServiceUrl(svc.name);
93
+ const fullUrl = new URL(svc.path, svcUrl);
94
+ const start = Date.now();
95
+ const res = await fetch(fullUrl.href, { signal: AbortSignal.timeout(5000) });
96
+ const latency = Date.now() - start;
97
+
98
+ if (res.ok) {
99
+ console.log(` ${chalk.green('●')} ${svc.name.padEnd(14)} ${chalk.green('healthy')} ${chalk.dim(latency + 'ms')}`);
100
+ } else {
101
+ console.log(` ${chalk.yellow('●')} ${svc.name.padEnd(14)} ${chalk.yellow(res.status)} ${chalk.dim(latency + 'ms')}`);
102
+ }
103
+ } catch {
104
+ console.log(` ${chalk.red('●')} ${svc.name.padEnd(14)} ${chalk.red('down')}`);
105
+ }
106
+ }
107
+
108
+ console.log(chalk.dim('\n refreshing every ' + (interval / 1000) + 's'));
109
+ };
110
+
111
+ await tick();
112
+ const timer = setInterval(tick, interval);
113
+ process.on('SIGINT', () => { clearInterval(timer); process.exit(0); });
114
+ });
115
+ }