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,591 @@
1
+ /**
2
+ * `quox lucid` (alias `aware`): LUCID, the situational-awareness view.
3
+ * Collector routes under /api/v1/lucid (org-scoped).
4
+ *
5
+ * quox lucid | since-last what needs you / changed since you last looked (advances the cursor)
6
+ * quox lucid since <w|seq> same view from a window or seq, never advances
7
+ * quox lucid subjects|feeds|journal|ack
8
+ * quox lucid why <ref> [--evidence] | do <ref> <measure_id> | area <name>
9
+ * quox lucid teach <target> <field>=<value> | lessons | forget <id>
10
+ * quox lucid herald [set key=value... | test] when LUCID speaks first, and what it has said
11
+ * quox lucid blind sources live/silent/failing/never heard, and what doesn't add up
12
+ */
13
+ import { formatOutput, printSuccess } from '../core/output.js';
14
+ import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
15
+ import { resolveEndpoint } from '../api/endpoints.js';
16
+ import { confirmDestructive } from '../core/confirm.js';
17
+ import { resolveOrgId } from './helpers.js';
18
+
19
+ const SURFACE = 'cli';
20
+
21
+ function age(ms) {
22
+ if (ms == null) return '';
23
+ const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
24
+ if (s < 60) return `${s}s`;
25
+ const m = Math.floor(s / 60);
26
+ if (m < 60) return `${m}m`;
27
+ const h = Math.floor(m / 60);
28
+ if (h < 24) return `${h}h`;
29
+ return `${Math.floor(h / 24)}d`;
30
+ }
31
+
32
+ function ts(ms) {
33
+ return ms == null ? '' : new Date(ms).toISOString();
34
+ }
35
+
36
+ export function registerLucidCommands(program, ctx) {
37
+ const { client, auth } = ctx;
38
+
39
+ const lu = program
40
+ .command('lucid')
41
+ .alias('aware')
42
+ .description('LUCID: what needs you, what changed since you last looked, what is in motion, what I cannot see')
43
+ .option('--json', 'print the full JSON response')
44
+ .option('--agent', 'compact id-rich form for agents')
45
+ .option('--spoken', 'sentences only, as read on the phone')
46
+ .option('--no-advance', 'do not move the cursor when viewing')
47
+ .option('--sitrep', 'one-shot formatted snapshot for the terminal or pasting into a chat (never moves the cursor)');
48
+
49
+ const wantJson = (cmd) => {
50
+ const o = cmd.optsWithGlobals ? cmd.optsWithGlobals() : cmd.opts();
51
+ return Boolean(o.json) || program.opts().output === 'json';
52
+ };
53
+ const advanceOf = (cmd) => {
54
+ const o = cmd.optsWithGlobals ? cmd.optsWithGlobals() : cmd.opts();
55
+ return o.advance !== false;
56
+ };
57
+
58
+ // An error body is not data: report the status and stop.
59
+ async function call(method, key, { query, body, params } = {}) {
60
+ const ep = resolveEndpoint(key, params);
61
+ const q = { org_id: resolveOrgId(auth, program), ...query };
62
+ try {
63
+ const data = method === 'POST'
64
+ ? await client.post(ep.path, { service: ep.service, body, query: q })
65
+ : method === 'DELETE'
66
+ ? await client.delete(ep.path, { service: ep.service, query: q })
67
+ : await client.get(ep.path, { service: ep.service, query: q });
68
+ return data;
69
+ } catch (err) {
70
+ const status = err && (err.status || err.statusCode);
71
+ if (status) {
72
+ console.error(`LUCID request failed: HTTP ${status}${err.message ? ` ${err.message}` : ''}`);
73
+ process.exitCode = 1;
74
+ return null;
75
+ }
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ async function showSituation(cmd, since, advance) {
81
+ auth.requireAuth();
82
+ const data = await call('GET', 'lucid.situation', {
83
+ query: { since, surface: SURFACE, advance: advance ? '1' : undefined },
84
+ });
85
+ if (data === null) return;
86
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
87
+ const sit = data.situation || {};
88
+ const needs = (sit.needs_you || []).length;
89
+ const cov = sit.cannot_see && sit.cannot_see.coverage;
90
+ const feeds = cov ? ` · feeds ${cov.live}/${cov.expected}` : '';
91
+ // quoxglyph.js is a vendored lockstep copy (no LUCID icon yet), so swap the fallback Ⓠ for ◉ here.
92
+ printEvent(glyphLine('lucid', 'situation', { result: `${needs} need you${feeds}` }).replace('Ⓠ', '◉'));
93
+ const o = typeof cmd.optsWithGlobals === 'function' ? cmd.optsWithGlobals() : cmd.opts();
94
+ console.log((o.agent ? data.text_agent : o.spoken ? data.spoken : data.text) ?? data.text ?? '');
95
+ }
96
+
97
+ async function showSitrep(o = {}) {
98
+ auth.requireAuth();
99
+ const data = await call('GET', 'lucid.situation', { query: { since: 'last', surface: o.agent ? 'agent' : 'deck' } });
100
+ if (data === null) return;
101
+ // Agents: --agent (compact, id-rich), --json (structured), --spoken (sentences). Never moves the cursor.
102
+ if (o.json) { console.log(JSON.stringify(data, null, 2)); return; }
103
+ if (o.agent) { console.log(data.text_agent ?? data.text ?? ''); return; }
104
+ if (o.spoken) { console.log(data.spoken ?? data.text ?? ''); return; }
105
+ const { renderSitrep } = await import('../lucid/sitrep.js');
106
+ const tty = Boolean(process.stdout.isTTY);
107
+ process.stdout.write(renderSitrep(data, {
108
+ width: process.stdout.columns || 100,
109
+ color: tty && !process.env.NO_COLOR && process.env.QUOX_COLOR !== '0',
110
+ }));
111
+ }
112
+
113
+ lu.action(async (opts, cmd) => {
114
+ if (opts.sitrep) return showSitrep();
115
+ return showSituation(cmd, 'last', advanceOf(cmd));
116
+ });
117
+
118
+ const sitrepOpts = (c) => c
119
+ .option('--agent', 'compact, id-rich form for agents')
120
+ .option('--json', 'the full structured situation')
121
+ .option('--spoken', 'sentences only, as read on the phone');
122
+
123
+ sitrepOpts(lu
124
+ .command('sitrep')
125
+ .description('One-shot formatted snapshot (areas, what needs you, what changed, what I cannot see)'))
126
+ .action(async (o) => showSitrep(o));
127
+
128
+ // Top-level shortcut so any human or agent can ask for a sitrep: `quox sitrep [--agent|--json|--spoken]`.
129
+ sitrepOpts(program
130
+ .command('sitrep')
131
+ .description('LUCID situation report: what needs you, every area, what changed, what LUCID cannot see'))
132
+ .action(async (o) => showSitrep(o));
133
+
134
+ lu
135
+ .command('deck')
136
+ .description('Live full-screen LUCID deck (refreshes every 10s; m marks seen)')
137
+ .option('--no-intro', 'skip the startup animation')
138
+ .action(async (o) => {
139
+ auth.requireAuth();
140
+ const orgQ = { org_id: resolveOrgId(auth, program) };
141
+ const get = async (key, query) => {
142
+ const ep = resolveEndpoint(key);
143
+ return client.get(ep.path, { service: ep.service, query: { ...orgQ, ...query } });
144
+ };
145
+ const post = async (key, body, query) => {
146
+ const ep = resolveEndpoint(key);
147
+ return client.post(ep.path, { service: ep.service, body, query: { ...orgQ, ...query } });
148
+ };
149
+ const { launchDeck } = await import('../lucid/deck.js');
150
+ await launchDeck({
151
+ situation: () => get('lucid.situation', { since: 'last', surface: 'deck' }),
152
+ measure: (ref, measure_id) => post('lucid.measure', { ref, measure_id }),
153
+ teach: (body) => post('lucid.teach', body),
154
+ ack: (seq) => post('lucid.ack', { seq: Number(seq) }, { surface: 'deck' }),
155
+ }, { noIntro: o.intro === false });
156
+ });
157
+
158
+ lu
159
+ .command('since-last')
160
+ .description('Situation since you last looked (advances the cursor unless --no-advance)')
161
+ .option('--json', 'print the full JSON response')
162
+ .option('--no-advance', 'do not move the cursor')
163
+ .action(async (_opts, cmd) => showSituation(cmd, 'last', advanceOf(cmd)));
164
+
165
+ lu
166
+ .command('since <window>')
167
+ .description('Situation since a window (3h, 30m, 2d) or journal seq; never advances the cursor')
168
+ .option('--json', 'print the full JSON response')
169
+ .action(async (window, _opts, cmd) => showSituation(cmd, window, false));
170
+
171
+ lu
172
+ .command('subjects')
173
+ .description('Open subjects the journal is tracking')
174
+ .option('--ledger <ledger>', 'filter by ledger')
175
+ .option('--json', 'print the full JSON response')
176
+ .action(async (options, cmd) => {
177
+ auth.requireAuth();
178
+ const data = await call('GET', 'lucid.subjects', { query: { ledger: options.ledger } });
179
+ if (data === null) return;
180
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
181
+ const subjects = data.subjects || [];
182
+ formatOutput(subjects.map((s) => ({
183
+ LEDGER: s.ledger || '',
184
+ SUBJECT: s.subject || '',
185
+ STATUS: s.status || '',
186
+ SUMMARY: s.summary || '',
187
+ })), {
188
+ format: program.opts().output,
189
+ columns: ['LEDGER', 'SUBJECT', 'STATUS', 'SUMMARY'],
190
+ emptyMessage: 'No open subjects.',
191
+ raw: subjects,
192
+ });
193
+ });
194
+
195
+ lu
196
+ .command('feeds')
197
+ .description('Which feeds are speaking, and which are expected but silent')
198
+ .option('--json', 'print the full JSON response')
199
+ .action(async (_options, cmd) => {
200
+ auth.requireAuth();
201
+ const data = await call('GET', 'lucid.feeds');
202
+ if (data === null) return;
203
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
204
+ const feeds = data.feeds || [];
205
+ formatOutput(feeds.map((f) => ({
206
+ FEED: f.feed,
207
+ 'LAST HEARD': f.last_heard ? `${age(f.last_heard)} ago` : '',
208
+ 'EXPECTED BY': f.expected_by ? ts(f.expected_by) : '',
209
+ ERROR: f.last_error || '',
210
+ })), {
211
+ format: program.opts().output,
212
+ columns: ['FEED', 'LAST HEARD', 'EXPECTED BY', 'ERROR'],
213
+ emptyMessage: 'No feeds have been heard from.',
214
+ raw: feeds,
215
+ });
216
+ const heard = new Set(feeds.map((f) => f.feed));
217
+ const never = (data.expected || []).filter((n) => !heard.has(n));
218
+ if (never.length) console.log(`Expected but never heard: ${never.join(', ')}`);
219
+ });
220
+
221
+ lu
222
+ .command('blind')
223
+ .description("What I can and cannot hear (live / silent / failing / never heard) and what doesn't add up")
224
+ .option('--json', 'print the structured data')
225
+ .action(async (_options, cmd) => {
226
+ auth.requireAuth();
227
+ const sitData = await call('GET', 'lucid.situation', { query: { since: 'last', surface: 'blind' } });
228
+ if (sitData === null) return;
229
+ const feedData = await call('GET', 'lucid.feeds');
230
+ if (feedData === null) return;
231
+ const payload = { situation: sitData.situation || {}, feeds: feedData.feeds || [], expected: feedData.expected || [] };
232
+ if (wantJson(cmd)) { console.log(JSON.stringify(payload, null, 2)); return; }
233
+ const { renderBlind } = await import('../lucid/blind.js');
234
+ process.stdout.write(renderBlind(payload));
235
+ });
236
+
237
+ lu
238
+ .command('ask')
239
+ .description('Ask LUCID anything about your situation, in plain words (routed to the LUCID agent)')
240
+ .argument('<question...>', 'your question, e.g. "what changed while I was out?"')
241
+ .option('--conversation <id>', 'continue a conversation')
242
+ .action(async (words, o) => {
243
+ auth.requireAuth();
244
+ const { consumeStream } = await import('./chat.js');
245
+ try {
246
+ await consumeStream(client, { message: words.join(' '), agent: 'lucid', sessionId: o.conversation }, { onAgentLabel: () => {} });
247
+ } catch (err) {
248
+ console.error(`LUCID request failed: ${err.message}`);
249
+ process.exitCode = 1;
250
+ }
251
+ });
252
+
253
+ lu
254
+ .command('replay')
255
+ .description('What LUCID knew at a past moment (rebuilt from the journal)')
256
+ .requiredOption('--at <when>', 'ISO time, epoch ms, or HH:MM today (London)')
257
+ .option('--json', 'print the structured data')
258
+ .action(async (o, cmd) => {
259
+ auth.requireAuth();
260
+ const data = await call('GET', 'lucid.replay', { query: { at: o.at } });
261
+ if (data === null) return;
262
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
263
+ printEvent(glyphLine('lucid', `replay at ${o.at}`, { result: 'rebuilt from the journal' }).replace('Ⓠ', '◉'));
264
+ console.log(data.text || '');
265
+ });
266
+
267
+ lu
268
+ .command('told')
269
+ .description('What LUCID told you (herald steps and briefings), with witness ids')
270
+ .option('--since <window>', 'e.g. 6h, 2d', '24h')
271
+ .option('--json', 'print the structured data')
272
+ .action(async (o, cmd) => {
273
+ auth.requireAuth();
274
+ const data = await call('GET', 'lucid.told', { query: { since: o.since } });
275
+ if (data === null) return;
276
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
277
+ const told = data.told || [];
278
+ if (!told.length) { console.log(`LUCID told you nothing in the last ${o.since}.`); return; }
279
+ const fmt = (ts) => new Date(ts).toLocaleString('en-GB', { timeZone: 'Europe/London', weekday: 'short', hour: '2-digit', minute: '2-digit' });
280
+ for (const t of told) {
281
+ const STEP = { l1_inbox: 'Approvals inbox', l2_slot: 'slot nudge', l3_call: 'phone call', resolved: 'withdrawn', l2_failed: 'nudge failed', l3_failed: 'call failed' };
282
+ const what = t.type === 'briefing' ? `situation shown (${t.kind || 'view'})` : (t.summary || t.ref || '');
283
+ const how = t.type === 'briefing' ? `briefing · ${t.surface || ''}` : (STEP[t.status] || t.status || '');
284
+ console.log(` ${fmt(t.ts)} ${String(how).padEnd(16)} ${what}${t.ward_entry_id ? ' ✔ witnessed' : ' (not witnessed)'}`);
285
+ }
286
+ const c = data.counts || {};
287
+ console.log(`\n ${c.herald_steps || 0} herald steps · ${c.briefings || 0} briefings · ${c.unwitnessed || 0} not witnessed`);
288
+ });
289
+
290
+ lu
291
+ .command('journal')
292
+ .description('Raw journal entries')
293
+ .option('--after <seq>', 'only entries after this seq')
294
+ .option('--limit <n>', 'max entries')
295
+ .option('--json', 'print the full JSON response')
296
+ .action(async (options, cmd) => {
297
+ auth.requireAuth();
298
+ const data = await call('GET', 'lucid.journal', { query: { after: options.after, limit: options.limit } });
299
+ if (data === null) return;
300
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
301
+ const entries = data.entries || [];
302
+ formatOutput(entries.map((e) => ({
303
+ SEQ: e.seq,
304
+ TIME: ts(e.ts),
305
+ KIND: e.kind || '',
306
+ SUBJECT: e.subject || '',
307
+ SUMMARY: e.summary || '',
308
+ })), {
309
+ format: program.opts().output,
310
+ columns: ['SEQ', 'TIME', 'KIND', 'SUBJECT', 'SUMMARY'],
311
+ emptyMessage: 'No journal entries.',
312
+ raw: entries,
313
+ });
314
+ });
315
+
316
+ lu
317
+ .command('ack <seq>')
318
+ .description('Advance your cursor to a journal seq')
319
+ .action(async (seq, _opts, cmd) => {
320
+ auth.requireAuth();
321
+ const data = await call('POST', 'lucid.ack', { body: { seq: Number(seq) }, query: { surface: SURFACE } });
322
+ if (data === null) return;
323
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
324
+ printSuccess(`Cursor at ${data.cursor ?? seq}`);
325
+ });
326
+
327
+ const measuresOf = (item) => (item && item.L2 && item.L2.measures) || [];
328
+
329
+ lu
330
+ .command('why <ref>')
331
+ .description('Why is this on the list: who, what, why, when, what it affects, and what you can do')
332
+ .option('--evidence', 'also show the L3 evidence block')
333
+ .option('--json', 'print the full JSON response')
334
+ .action(async (ref, options, cmd) => {
335
+ auth.requireAuth();
336
+ const data = await call('GET', 'lucid.item', { query: { ref } });
337
+ if (data === null) return;
338
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
339
+ const item = data.item || {};
340
+ const a = item.L1 || {};
341
+ console.log(item.L0 || ref);
342
+ console.log('');
343
+ console.log(`Who: ${a.who ?? ''}`);
344
+ console.log(`What: ${a.what ?? ''}`);
345
+ console.log(`Why: ${a.why ?? ''}`);
346
+ const w = a.when || {};
347
+ const whenParts = [];
348
+ if (w.clock) whenParts.push(w.clock);
349
+ if (w.since) whenParts.push(`since ${ts(w.since)}`);
350
+ if (w.waited_ms != null) whenParts.push(`waiting ${age(Date.now() - w.waited_ms)}`);
351
+ if (w.deadline) whenParts.push(`deadline ${ts(w.deadline)}`);
352
+ console.log(`When: ${whenParts.join(' · ')}`);
353
+ const aff = a.affected || [];
354
+ console.log(`Affected: ${aff.length ? aff.map((x) => `${x.label} (${x.how}${x.inferred ? ', inferred' : ''})`).join('; ') : 'nothing known'}`);
355
+ const ms = measuresOf(item);
356
+ if (ms.length) {
357
+ console.log('');
358
+ console.log('What you can do:');
359
+ ms.forEach((m, i) => {
360
+ console.log(` ${i + 1}. ${m.label} [${m.id}]${m.say ? ` - ${m.say}` : ''}${m.command ? `\n ${m.command}` : ''}`);
361
+ });
362
+ }
363
+ if (options.evidence) {
364
+ const e = item.L3 || {};
365
+ console.log('');
366
+ console.log('Evidence:');
367
+ console.log(` Status: ${e.status ?? ''}`);
368
+ console.log(` Feed: ${e.feed ?? ''}`);
369
+ const c = e.confidence || {};
370
+ console.log(` Confidence: last heard ${c.last_heard ? `${age(c.last_heard)} ago` : 'never'}${c.stale ? ' (STALE)' : ''}`);
371
+ for (const t of e.transitions || []) {
372
+ console.log(` ${ts(t.ts)} ${t.from ?? '-'} -> ${t.to}${t.held_ms != null ? ` (held ${age(Date.now() - t.held_ms)})` : ''}`);
373
+ }
374
+ }
375
+ });
376
+
377
+ lu
378
+ .command('do <ref> <measure_id>')
379
+ .description('Carry out one of the measures offered by `lucid why`')
380
+ .option('-y, --yes', 'skip the confirmation for run measures')
381
+ .option('--json', 'print the full JSON response')
382
+ .action(async (ref, measureId, options, cmd) => {
383
+ auth.requireAuth();
384
+ const itemData = await call('GET', 'lucid.item', { query: { ref } });
385
+ if (itemData === null) return;
386
+ const m = measuresOf(itemData.item).find((x) => x.id === measureId);
387
+ if (m && m.kind === 'run') {
388
+ const ok = await confirmDestructive(program, { force: options.yes === true }, `Run "${m.label}" for ${ref}?`);
389
+ if (!ok) { console.log('Cancelled.'); return; }
390
+ }
391
+ const data = await call('POST', 'lucid.measure', { body: { ref, measure_id: measureId } });
392
+ if (data === null) return;
393
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
394
+ if (data.status === 'filed') {
395
+ printSuccess(`Filed${data.approval_id ? ` (approval ${data.approval_id})` : ''}. Approve it in Approvals.`);
396
+ } else if (data.status === 'manual') {
397
+ console.log('This one is manual. Run this yourself:');
398
+ console.log(` ${data.command ?? ''}`);
399
+ } else if (data.status === 'opened') {
400
+ console.log(`Opened: ${data.url ?? ''}`);
401
+ } else {
402
+ console.log(data.say || `Status: ${data.status}`);
403
+ }
404
+ });
405
+
406
+ lu
407
+ .command('teach <target> <assignment>')
408
+ .description('Teach LUCID a rule, e.g. quox lucid teach hitl:abc urgency=fyi --scope subject')
409
+ .option('--scope <scope>', 'subject | kind | area', 'subject')
410
+ .option('--reason <reason>', 'why')
411
+ .option('--json', 'print the full JSON response')
412
+ .action(async (target, assignment, options, cmd) => {
413
+ auth.requireAuth();
414
+ const eq = assignment.indexOf('=');
415
+ if (eq < 1) {
416
+ console.error('LUCID teach needs <field>=<value>, for example urgency=fyi');
417
+ process.exitCode = 1;
418
+ return;
419
+ }
420
+ const body = {
421
+ scope: options.scope, target,
422
+ field: assignment.slice(0, eq), value: assignment.slice(eq + 1),
423
+ reason: options.reason,
424
+ };
425
+ const data = await call('POST', 'lucid.teach', { body });
426
+ if (data === null) return;
427
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
428
+ printSuccess(`Learned: ${body.field}=${body.value} for ${body.scope} ${target}${data.id ? ` (lesson ${data.id})` : ''}`);
429
+ });
430
+
431
+ lu
432
+ .command('lessons')
433
+ .description('What you have taught LUCID')
434
+ .option('--json', 'print the full JSON response')
435
+ .action(async (_options, cmd) => {
436
+ auth.requireAuth();
437
+ const data = await call('GET', 'lucid.lessons');
438
+ if (data === null) return;
439
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
440
+ const lessons = data.lessons || [];
441
+ formatOutput(lessons.map((l) => ({
442
+ ID: l.id,
443
+ SCOPE: l.scope || '',
444
+ TARGET: l.target || '',
445
+ RULE: `${l.field}=${l.value}`,
446
+ REASON: l.reason || '',
447
+ })), {
448
+ format: program.opts().output,
449
+ columns: ['ID', 'SCOPE', 'TARGET', 'RULE', 'REASON'],
450
+ emptyMessage: 'No lessons taught yet.',
451
+ raw: lessons,
452
+ });
453
+ });
454
+
455
+ lu
456
+ .command('forget <id>')
457
+ .description('Forget a lesson')
458
+ .action(async (id, _opts, cmd) => {
459
+ auth.requireAuth();
460
+ const data = await call('DELETE', 'lucid.lessons.delete', { params: { id } });
461
+ if (data === null) return;
462
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
463
+ printSuccess(`Forgot lesson ${id}`);
464
+ });
465
+
466
+ lu
467
+ .command('area <name>')
468
+ .description('One area of the situation: headline, what needs you, measures, last outcome')
469
+ .option('--json', 'print the full JSON response')
470
+ .action(async (name, _opts, cmd) => {
471
+ auth.requireAuth();
472
+ const data = await call('GET', 'lucid.situation', { query: { since: 'last', surface: SURFACE } });
473
+ if (data === null) return;
474
+ const areas = (data.situation && data.situation.areas) || [];
475
+ const area = areas.find((x) => x.area === name || (x.label || '').toLowerCase() === name.toLowerCase());
476
+ if (!area) {
477
+ console.error(`LUCID has no area "${name}". Areas: ${areas.map((x) => x.area).join(', ')}`);
478
+ process.exitCode = 1;
479
+ return;
480
+ }
481
+ if (wantJson(cmd)) { console.log(JSON.stringify(area, null, 2)); return; }
482
+ console.log(`${area.label || area.area} (${area.state})`);
483
+ console.log(area.L0 || '');
484
+ const needs = (area.L1 && area.L1.needs) || [];
485
+ if (needs.length) {
486
+ console.log('');
487
+ console.log('Needs you:');
488
+ for (const n of needs) console.log(` - ${n.L0} [${n.ref}]`);
489
+ }
490
+ const ms = (area.L2 && area.L2.measures) || [];
491
+ if (ms.length) {
492
+ console.log('');
493
+ console.log('Measures:');
494
+ ms.forEach((m, i) => console.log(` ${i + 1}. ${m.label} [${m.id}]${m.command ? ` ${m.command}` : ''}`));
495
+ }
496
+ if (area.last) console.log(`\nLast: ${area.last.L0}${area.last.outcome ? ` (${area.last.outcome})` : ''} ${area.last.ts ? `${age(area.last.ts)} ago` : ''}`);
497
+ const st = Object.entries(area.stats || {});
498
+ if (st.length) console.log(`Stats: ${st.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(',') : v}`).join(' · ')}`);
499
+ });
500
+
501
+ // ---- herald: when LUCID reaches out on its own ----
502
+ const LEVELS = { l1_inbox: 'filed in your Approvals inbox', l2_slot: 'nudged you in your slot', l3_call: 'called you', resolved: 'resolved' };
503
+ const levelWords = (l) => LEVELS[l] || l || 'not yet reached out';
504
+ const nextWords = (e) => {
505
+ if (!e.next_step) return 'nothing further';
506
+ const nxt = { l2_slot: 'nudge in your slot', l3_call: 'phone call' }[e.next_step] || e.next_step;
507
+ return e.next_step_at ? `${nxt} at ${ts(e.next_step_at)}` : nxt;
508
+ };
509
+ const coerce = (v) => {
510
+ if (v === 'true') return true;
511
+ if (v === 'false') return false;
512
+ return /^-?\d+$/.test(v) ? Number(v) : v;
513
+ };
514
+
515
+ const herald = lu
516
+ .command('herald')
517
+ .description('When LUCID speaks first: your herald settings and what it has escalated')
518
+ .option('--json', 'print the full JSON response')
519
+ .action(async (_opts, cmd) => {
520
+ auth.requireAuth();
521
+ const data = await call('GET', 'lucid.herald');
522
+ if (data === null) return;
523
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
524
+ const s = data.settings || {};
525
+ console.log(`Herald is ${s.enabled ? 'ON' : 'OFF'}.`);
526
+ console.log(` Approvals inbox: ${s.inbox ? 'on' : 'off'}`);
527
+ console.log(` Slot nudge: ${s.slot ? `on (slot ${s.slot_name})` : 'off'}`);
528
+ console.log(` Phone call: ${s.call ? 'on' : 'off (opt-in)'}`);
529
+ console.log(` Quiet hours: ${s.quiet_start} to ${s.quiet_end} (${s.tz})`);
530
+ console.log(` Morning briefing: ${s.morning_at}`);
531
+ console.log(` Max new inbox items per hour: ${s.max_items_per_hour}`);
532
+ console.log('');
533
+ const esc = data.escalations || [];
534
+ formatOutput(esc.map((e) => ({
535
+ WHAT: e.L0 || e.ref,
536
+ URGENCY: e.urgency || '',
537
+ 'SO FAR': levelWords(e.level),
538
+ SINCE: e.since ? `${age(e.since)} ago` : '',
539
+ NEXT: nextWords(e),
540
+ })), {
541
+ format: program.opts().output,
542
+ columns: ['WHAT', 'URGENCY', 'SO FAR', 'SINCE', 'NEXT'],
543
+ emptyMessage: 'LUCID has not escalated anything to you.',
544
+ raw: esc,
545
+ });
546
+ });
547
+
548
+ herald
549
+ .command('set <pairs...>')
550
+ .description('Change herald settings, e.g. quox lucid herald set call=true quiet_start=23:00')
551
+ .option('--json', 'print the full JSON response')
552
+ .action(async (pairs, _opts, cmd) => {
553
+ auth.requireAuth();
554
+ const body = {};
555
+ for (const p of pairs) {
556
+ const eq = p.indexOf('=');
557
+ if (eq < 1) {
558
+ console.error(`LUCID herald set needs key=value pairs, got "${p}"`);
559
+ process.exitCode = 1;
560
+ return;
561
+ }
562
+ body[p.slice(0, eq)] = coerce(p.slice(eq + 1));
563
+ }
564
+ const data = await call('POST', 'lucid.herald.settings', { body });
565
+ if (data === null) return;
566
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
567
+ printSuccess(`Herald updated: ${Object.entries(body).map(([k, v]) => `${k}=${v}`).join(' ')}`);
568
+ });
569
+
570
+ herald
571
+ .command('test')
572
+ .description('Dry run: what the herald would do right now (files and sends nothing)')
573
+ .option('--json', 'print the full JSON response')
574
+ .action(async (_opts, cmd) => {
575
+ auth.requireAuth();
576
+ const data = await call('POST', 'lucid.herald.test', { body: {} });
577
+ if (data === null) return;
578
+ if (wantJson(cmd)) { console.log(JSON.stringify(data, null, 2)); return; }
579
+ // The collector answers { enabled, items: [{ L0, urgency, budget_min, waited_min, would }] }.
580
+ const list = data.items || data.would || data.steps || data.actions || [];
581
+ if (data.enabled === false) console.log('The herald is off (quox lucid herald set enabled=true).');
582
+ if (!list.length) { console.log('Nothing needs you, so nothing would fire.'); return; }
583
+ console.log('What the herald would do right now (nothing was sent):');
584
+ for (const w of list) {
585
+ const what = w.L0 || w.ref || '';
586
+ const act = typeof w.would === 'string' ? w.would : levelWords(w.level || w.step);
587
+ const wait = w.waited_min != null ? ` · waited ${w.waited_min}m${w.budget_min != null ? ` of ${w.budget_min}m` : ''}` : '';
588
+ console.log(` - ${what}${wait}\n → ${act}${w.message ? `\n ${w.message}` : ''}`);
589
+ }
590
+ });
591
+ }