quoxcli 0.2.0 → 0.3.1

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 (327) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/bin/quox.js +99 -126
  4. package/lib/api/endpoints/admin.js +26 -0
  5. package/lib/api/endpoints/agent-schedules.js +26 -0
  6. package/lib/api/endpoints/agents.js +7 -0
  7. package/lib/api/endpoints/assistants.js +12 -0
  8. package/lib/api/endpoints/away.js +15 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/contactsq.js +2 -0
  11. package/lib/api/endpoints/conversations.js +14 -0
  12. package/lib/api/endpoints/executions.js +13 -0
  13. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  14. package/lib/api/endpoints/files.js +14 -0
  15. package/lib/api/endpoints/hat.js +30 -0
  16. package/lib/api/endpoints/inbox.js +20 -0
  17. package/lib/api/endpoints/index.js +14 -0
  18. package/lib/api/endpoints/memory.js +19 -0
  19. package/lib/api/endpoints/mind.js +13 -0
  20. package/lib/api/endpoints/mon.js +6 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/plugins.js +18 -0
  23. package/lib/api/endpoints/presence.js +33 -0
  24. package/lib/api/endpoints/quoxplan.js +7 -0
  25. package/lib/api/endpoints/quoxseo.js +6 -0
  26. package/lib/api/endpoints/router.js +17 -0
  27. package/lib/api/endpoints/safety.js +7 -0
  28. package/lib/api/endpoints/skills.js +20 -0
  29. package/lib/api/endpoints/stepup.js +20 -0
  30. package/lib/api/endpoints/teams.js +2 -2
  31. package/lib/api/endpoints/triggers.js +26 -0
  32. package/lib/api/endpoints/v1.js +86 -0
  33. package/lib/api/endpoints/ward.js +7 -0
  34. package/lib/api/endpoints/workflows.js +7 -0
  35. package/lib/api/endpoints/x.js +8 -0
  36. package/lib/commands/actions.js +61 -0
  37. package/lib/commands/admin.js +16 -0
  38. package/lib/commands/agent.js +163 -27
  39. package/lib/commands/agentAttach.js +94 -6
  40. package/lib/commands/approvals.js +73 -1
  41. package/lib/commands/aside.js +24 -0
  42. package/lib/commands/ask.js +144 -0
  43. package/lib/commands/assistant.js +59 -4
  44. package/lib/commands/auth.js +8 -2
  45. package/lib/commands/backup.js +8 -4
  46. package/lib/commands/brain.js +105 -14
  47. package/lib/commands/build.js +19 -5
  48. package/lib/commands/chat.js +57 -2
  49. package/lib/commands/claude.js +34 -8
  50. package/lib/commands/contacts.js +49 -0
  51. package/lib/commands/conversation.js +35 -4
  52. package/lib/commands/dashboard.js +43 -0
  53. package/lib/commands/department.js +28 -7
  54. package/lib/commands/devResident.js +15 -3
  55. package/lib/commands/devSupervisor.js +13 -1
  56. package/lib/commands/discord.js +75 -39
  57. package/lib/commands/done.js +98 -28
  58. package/lib/commands/engagement.js +17 -6
  59. package/lib/commands/fetchAllowlist.js +71 -0
  60. package/lib/commands/file.js +38 -1
  61. package/lib/commands/fleet.js +311 -4
  62. package/lib/commands/governance.js +12 -3
  63. package/lib/commands/gripe.js +44 -13
  64. package/lib/commands/hat.js +178 -9
  65. package/lib/commands/helpers.js +18 -1
  66. package/lib/commands/hyperion.js +58 -0
  67. package/lib/commands/inbox.js +74 -23
  68. package/lib/commands/licence.js +14 -155
  69. package/lib/commands/lucid.js +10 -4
  70. package/lib/commands/memory.js +85 -8
  71. package/lib/commands/mind.js +71 -0
  72. package/lib/commands/mon.js +69 -2
  73. package/lib/commands/monitoring.js +1 -1
  74. package/lib/commands/motion.js +2 -2
  75. package/lib/commands/nucleus.js +48 -17
  76. package/lib/commands/objective.js +11 -0
  77. package/lib/commands/offload.js +89 -0
  78. package/lib/commands/onboard.js +2 -1
  79. package/lib/commands/org.js +11 -1
  80. package/lib/commands/peek.js +28 -0
  81. package/lib/commands/plan.js +38 -0
  82. package/lib/commands/plugin.js +110 -0
  83. package/lib/commands/presence.js +89 -0
  84. package/lib/commands/provider.js +159 -0
  85. package/lib/commands/qlarity.js +19 -3
  86. package/lib/commands/quoxchat.js +13 -4
  87. package/lib/commands/quoxseo.js +38 -0
  88. package/lib/commands/reach.js +2 -1
  89. package/lib/commands/router.js +77 -0
  90. package/lib/commands/rows.js +358 -0
  91. package/lib/commands/run.js +64 -9
  92. package/lib/commands/server.js +292 -0
  93. package/lib/commands/setup.js +33 -7
  94. package/lib/commands/skills.js +48 -5
  95. package/lib/commands/slot.js +260 -7
  96. package/lib/commands/status.js +1 -1
  97. package/lib/commands/team.js +3 -1
  98. package/lib/commands/tips.js +23 -0
  99. package/lib/commands/tool.js +133 -45
  100. package/lib/commands/trace.js +12 -7
  101. package/lib/commands/trigger.js +95 -0
  102. package/lib/commands/update.js +46 -0
  103. package/lib/commands/vault.js +31 -8
  104. package/lib/commands/vm.js +237 -0
  105. package/lib/commands/voice.js +34 -3
  106. package/lib/commands/volt.js +45 -3
  107. package/lib/commands/walkthrough.js +32 -0
  108. package/lib/commands/watch.js +73 -21
  109. package/lib/commands/welcome.js +14 -0
  110. package/lib/commands/workflow.js +189 -32
  111. package/lib/commands/x.js +13 -4
  112. package/lib/core/actingAs.js +2 -2
  113. package/lib/core/activeSummary.js +186 -0
  114. package/lib/core/aside.js +54 -0
  115. package/lib/core/askDecision.js +60 -0
  116. package/lib/core/auth.js +164 -13
  117. package/lib/core/callerSource.js +1 -1
  118. package/lib/core/cliBuild.js +62 -0
  119. package/lib/core/client.js +144 -35
  120. package/lib/core/commandRegistrars.js +121 -0
  121. package/lib/core/config.js +142 -24
  122. package/lib/core/customHats.js +168 -0
  123. package/lib/core/doneEngineFetch.js +8 -0
  124. package/lib/core/errors.js +329 -20
  125. package/lib/core/global-flags.js +105 -5
  126. package/lib/core/hyperion.js +102 -0
  127. package/lib/core/inboxResolution.js +65 -0
  128. package/lib/core/lazyCommands.js +4 -1
  129. package/lib/core/offload.js +504 -0
  130. package/lib/core/org-override.js +1 -1
  131. package/lib/core/orgResolve.js +70 -0
  132. package/lib/core/output.js +4 -4
  133. package/lib/core/peek.js +126 -0
  134. package/lib/core/profileUsers.js +51 -0
  135. package/lib/core/promptCancel.js +18 -0
  136. package/lib/core/repoPath.js +11 -0
  137. package/lib/core/residentSeed.js +3 -1
  138. package/lib/core/rootOptions.js +39 -0
  139. package/lib/core/router.js +168 -0
  140. package/lib/core/selfUpdate.js +188 -0
  141. package/lib/core/sessionsOverview.js +64 -0
  142. package/lib/core/slotKeeper.js +248 -3
  143. package/lib/core/slotWake.js +120 -0
  144. package/lib/core/supervisorEngine.js +71 -2
  145. package/lib/core/version.js +1 -1
  146. package/lib/core/vm.js +361 -0
  147. package/lib/core/vmTui.js +86 -0
  148. package/lib/core/walkthrough.js +35 -0
  149. package/lib/hyperion/deck.js +29 -0
  150. package/lib/lucid/LucidCard.jsx +68 -0
  151. package/lib/lucid/LucidIntro.jsx +45 -58
  152. package/lib/lucid/cards.js +86 -0
  153. package/lib/lucid/markdown.js +81 -0
  154. package/lib/lucid/motion.js +56 -0
  155. package/lib/quoxon/activity.js +14 -2
  156. package/lib/quoxon/aocl.js +117 -0
  157. package/lib/quoxon/api.js +504 -25
  158. package/lib/quoxon/app.js +3000 -230
  159. package/lib/quoxon/approvalFacts.js +94 -0
  160. package/lib/quoxon/approvalsView.js +325 -0
  161. package/lib/quoxon/asideView.js +40 -0
  162. package/lib/quoxon/ask.js +121 -0
  163. package/lib/quoxon/away.js +52 -0
  164. package/lib/quoxon/bang.js +224 -0
  165. package/lib/quoxon/bulkRisk.js +17 -0
  166. package/lib/quoxon/capabilities.js +13 -6
  167. package/lib/quoxon/cells.js +53 -0
  168. package/lib/quoxon/commandShort.js +112 -0
  169. package/lib/quoxon/commands.js +363 -38
  170. package/lib/quoxon/connect.js +210 -23
  171. package/lib/quoxon/connectCard.js +19 -2
  172. package/lib/quoxon/consent.js +86 -0
  173. package/lib/quoxon/copy.js +4 -3
  174. package/lib/quoxon/countdownLine.js +11 -0
  175. package/lib/quoxon/demo.autoplay.js +29 -0
  176. package/lib/quoxon/demo.fixtures.js +1 -1
  177. package/lib/quoxon/demo.js +44 -3
  178. package/lib/quoxon/devObserver.js +20 -0
  179. package/lib/quoxon/dividerAocl.js +191 -0
  180. package/lib/quoxon/dividerRule.js +57 -0
  181. package/lib/quoxon/dragonModes.js +150 -0
  182. package/lib/quoxon/dragonView.js +41 -0
  183. package/lib/quoxon/earlyInput.js +40 -0
  184. package/lib/quoxon/echo.js +15 -0
  185. package/lib/quoxon/egg.js +2 -2
  186. package/lib/quoxon/events.js +244 -25
  187. package/lib/quoxon/feedback.js +50 -0
  188. package/lib/quoxon/gallery.js +21 -7
  189. package/lib/quoxon/handoffOffer.js +78 -0
  190. package/lib/quoxon/hatSwitch.js +37 -0
  191. package/lib/quoxon/headless.js +161 -0
  192. package/lib/quoxon/hostUrls.js +41 -0
  193. package/lib/quoxon/indicator.js +6 -2
  194. package/lib/quoxon/intent.js +12 -1
  195. package/lib/quoxon/keys.js +49 -6
  196. package/lib/quoxon/launcher.js +103 -0
  197. package/lib/quoxon/main.js +92 -160
  198. package/lib/quoxon/markdown.js +45 -21
  199. package/lib/quoxon/markdownSettle.js +19 -0
  200. package/lib/quoxon/matrixDomains.json +46 -0
  201. package/lib/quoxon/mentions.js +90 -0
  202. package/lib/quoxon/mind.js +103 -0
  203. package/lib/quoxon/mode.js +46 -0
  204. package/lib/quoxon/model.js +132 -26
  205. package/lib/quoxon/modelPrefs.js +345 -0
  206. package/lib/quoxon/motion/clock.js +3 -2
  207. package/lib/quoxon/motion/level.js +91 -0
  208. package/lib/quoxon/mouse.js +211 -0
  209. package/lib/quoxon/noai.js +57 -0
  210. package/lib/quoxon/objects/away.js +72 -0
  211. package/lib/quoxon/objects/fetch.js +17 -5
  212. package/lib/quoxon/objects/fleet.js +149 -0
  213. package/lib/quoxon/objects/health.js +186 -0
  214. package/lib/quoxon/objects/index.js +24 -3
  215. package/lib/quoxon/objects/mirror.js +137 -0
  216. package/lib/quoxon/objects/peek.js +17 -0
  217. package/lib/quoxon/objects/prove.js +130 -0
  218. package/lib/quoxon/objects/replay.js +125 -0
  219. package/lib/quoxon/objects/sitrep.js +58 -0
  220. package/lib/quoxon/objects/view.js +11 -0
  221. package/lib/quoxon/onboarding/copy.js +32 -4
  222. package/lib/quoxon/onboarding/credential.js +17 -4
  223. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  224. package/lib/quoxon/onboarding/failureCause.js +33 -0
  225. package/lib/quoxon/onboarding/finish.js +15 -0
  226. package/lib/quoxon/onboarding/index.js +34 -7
  227. package/lib/quoxon/onboarding/installRunner.js +64 -5
  228. package/lib/quoxon/onboarding/preflight.js +17 -3
  229. package/lib/quoxon/onboarding/probe.js +65 -0
  230. package/lib/quoxon/onboarding/view.js +76 -19
  231. package/lib/quoxon/orgLabel.js +10 -0
  232. package/lib/quoxon/orgSwitch.js +33 -0
  233. package/lib/quoxon/ownBroadcast.js +22 -0
  234. package/lib/quoxon/palette.js +43 -0
  235. package/lib/quoxon/print.js +9 -5
  236. package/lib/quoxon/profileSwitch.js +53 -0
  237. package/lib/quoxon/promptHistory.js +41 -0
  238. package/lib/quoxon/providers.js +48 -0
  239. package/lib/quoxon/qart.js +20 -51
  240. package/lib/quoxon/qdragon.js +410 -0
  241. package/lib/quoxon/qdragonFly.js +61 -0
  242. package/lib/quoxon/qdragonScenes.js +282 -0
  243. package/lib/quoxon/qeyes.js +213 -0
  244. package/lib/quoxon/qmotion.js +58 -37
  245. package/lib/quoxon/render.js +638 -119
  246. package/lib/quoxon/resend.js +72 -0
  247. package/lib/quoxon/roster.js +95 -9
  248. package/lib/quoxon/route.js +6 -5
  249. package/lib/quoxon/rows.js +711 -0
  250. package/lib/quoxon/rowsStore.js +80 -0
  251. package/lib/quoxon/screenContext.js +150 -0
  252. package/lib/quoxon/screenFrames.js +254 -0
  253. package/lib/quoxon/session.js +172 -2
  254. package/lib/quoxon/sessionDead.js +17 -0
  255. package/lib/quoxon/signin.js +125 -0
  256. package/lib/quoxon/slash/agents.js +112 -0
  257. package/lib/quoxon/slash/argSources.js +65 -0
  258. package/lib/quoxon/slash/args.js +65 -0
  259. package/lib/quoxon/slash/automate.js +89 -0
  260. package/lib/quoxon/slash/broadcast.js +140 -0
  261. package/lib/quoxon/slash/broadcastRun.js +108 -0
  262. package/lib/quoxon/slash/card.js +30 -0
  263. package/lib/quoxon/slash/challenge.js +114 -0
  264. package/lib/quoxon/slash/cliCommand.js +53 -0
  265. package/lib/quoxon/slash/cliLeaves.json +948 -0
  266. package/lib/quoxon/slash/compact.js +1 -1
  267. package/lib/quoxon/slash/confirm.js +30 -0
  268. package/lib/quoxon/slash/crystallize.js +69 -0
  269. package/lib/quoxon/slash/dashboard.js +136 -0
  270. package/lib/quoxon/slash/draft.js +54 -0
  271. package/lib/quoxon/slash/hatWalk.js +206 -0
  272. package/lib/quoxon/slash/inverses.js +77 -0
  273. package/lib/quoxon/slash/killer.js +24 -0
  274. package/lib/quoxon/slash/killerApi.js +16 -0
  275. package/lib/quoxon/slash/ledger.js +13 -8
  276. package/lib/quoxon/slash/listView.js +3 -1
  277. package/lib/quoxon/slash/modelCmd.js +261 -0
  278. package/lib/quoxon/slash/palette.js +21 -7
  279. package/lib/quoxon/slash/paletteController.js +117 -7
  280. package/lib/quoxon/slash/panels.js +21 -6
  281. package/lib/quoxon/slash/picker.js +20 -2
  282. package/lib/quoxon/slash/restart.js +71 -13
  283. package/lib/quoxon/slash/rowCmd.js +157 -0
  284. package/lib/quoxon/slash/rowWalk.js +175 -0
  285. package/lib/quoxon/slash/settings.js +16 -6
  286. package/lib/quoxon/slash/spawn.js +177 -0
  287. package/lib/quoxon/slash/stepUp.js +274 -0
  288. package/lib/quoxon/slash/stepUpCard.js +60 -0
  289. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  290. package/lib/quoxon/slash/undoApi.js +38 -0
  291. package/lib/quoxon/slash/undoCmd.js +49 -0
  292. package/lib/quoxon/slash/undoWish.js +15 -0
  293. package/lib/quoxon/slash/watch.js +164 -0
  294. package/lib/quoxon/slash/wish.js +23 -0
  295. package/lib/quoxon/slash/wishCmd.js +32 -0
  296. package/lib/quoxon/slotChat.js +300 -0
  297. package/lib/quoxon/slotFormat.js +195 -0
  298. package/lib/quoxon/slotRun.js +131 -0
  299. package/lib/quoxon/sound/mapping.js +2 -1
  300. package/lib/quoxon/stray.js +22 -0
  301. package/lib/quoxon/suggestCommand.js +59 -0
  302. package/lib/quoxon/term.js +46 -11
  303. package/lib/quoxon/textBuffer.js +176 -2
  304. package/lib/quoxon/theme.js +58 -25
  305. package/lib/quoxon/tips.js +433 -0
  306. package/lib/quoxon/toolError.js +41 -0
  307. package/lib/quoxon/transcript.js +42 -4
  308. package/lib/quoxon/web/qface.js +18 -10
  309. package/lib/quoxon/welcome.js +1 -1
  310. package/lib/quoxon/whatsnew.js +136 -0
  311. package/lib/quoxon/whatsnew.jsonl +89 -0
  312. package/lib/quoxon/windowOrg.js +113 -0
  313. package/lib/server/diag.js +129 -0
  314. package/lib/server/doctor.js +71 -0
  315. package/lib/server/install.js +149 -0
  316. package/lib/server/pluginEnable.js +161 -0
  317. package/lib/server/skew.js +56 -0
  318. package/lib/server/snapshot.js +131 -0
  319. package/lib/server/status.js +128 -0
  320. package/lib/server/sys.js +65 -0
  321. package/lib/server/uninstall.js +63 -0
  322. package/lib/server/update.js +234 -0
  323. package/lib/toolOutputShape.js +315 -0
  324. package/lib/tui/views/HyperionDeck.jsx +79 -0
  325. package/lib/tui/views/LucidDeck.jsx +209 -217
  326. package/package.json +4 -1
  327. package/lib/core/websiteSession.js +0 -148
@@ -0,0 +1,86 @@
1
+ /**
2
+ * QUOXON `/vm <verb> <target> [args]`: parse the words, call the shared engine
3
+ * (lib/core/vm.js, same as `quox vm`), return { lines, tone } for say().
4
+ * A held call becomes the approval hint (tone 'warn'); other failures throw.
5
+ */
6
+ import {
7
+ VM_ACTIONS, NODE_ACTIONS, VmPendingError, pendingHint, makeExec,
8
+ listVms, vmInfo, vmDoctor, doctorLines, vmAction, vmSnapshot, poolStatus, poolLease, poolDone, poolConfig,
9
+ nodePower, readPubkey, vmListLines, textOf,
10
+ } from './vm.js';
11
+
12
+ /** Split argv into positionals and --flag value pairs. */
13
+ export function splitFlags(argv) {
14
+ const pos = []; const flags = {};
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i];
17
+ if (a.startsWith('--')) {
18
+ const eq = a.indexOf('=');
19
+ if (eq > 0) flags[a.slice(2, eq)] = a.slice(eq + 1);
20
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) flags[a.slice(2)] = argv[++i];
21
+ else flags[a.slice(2)] = true;
22
+ } else pos.push(a);
23
+ }
24
+ return { pos, flags };
25
+ }
26
+
27
+ const asLines = (t) => String(t).split('\n');
28
+ export const VM_USAGE = 'Usage: /vm <list|info|doctor|start|stop|reboot|shutdown|snapshot|pool|node> <target> [args]';
29
+
30
+ export async function runVmCommand({ client, orgId, argv, exec: injected }) {
31
+ const exec = injected || makeExec({ client, orgId });
32
+ const { pos, flags } = splitFlags(argv || []);
33
+ const [verb, ...rest] = pos;
34
+ const reason = typeof flags.reason === 'string' ? flags.reason : undefined;
35
+ const need = (v, what) => { if (!v) throw new Error(`${what} is required. ${VM_USAGE}`); return v; };
36
+ try {
37
+ if (!verb) return { lines: [VM_USAGE], tone: 'warn' };
38
+ if (verb === 'list') {
39
+ return { lines: vmListLines(await listVms(exec, { node: flags.node, type: flags.type })) };
40
+ }
41
+ if (verb === 'info') {
42
+ const { vm, resources } = await vmInfo(exec, need(rest[0], 'a VM name or vmid'));
43
+ return { lines: [`${vm.name || vm.vmid} vmid ${vm.vmid} node ${vm.node} ${vm.type}`, ...asLines(textOf(resources))] };
44
+ }
45
+ if (verb === 'doctor') {
46
+ const report = await vmDoctor(exec, need(rest[0], 'a VM name or vmid'));
47
+ return { lines: doctorLines(report), tone: report.verdict === 'healthy' ? undefined : 'warn' };
48
+ }
49
+ if (VM_ACTIONS.includes(verb)) {
50
+ const { vm, result } = await vmAction(exec, verb, need(rest[0], 'a VM name or vmid'), { reason });
51
+ return { lines: [`${verb} ${vm.name || vm.vmid} (vmid ${vm.vmid} on ${vm.node})`, ...asLines(textOf(result))] };
52
+ }
53
+ if (verb === 'snapshot') {
54
+ const { vm, result } = await vmSnapshot(exec, need(rest[0], 'a VM name or vmid'), { name: typeof flags.name === 'string' ? flags.name : undefined, reason });
55
+ return { lines: [`snapshot ${vm.name || vm.vmid} (vmid ${vm.vmid})`, ...asLines(textOf(result))] };
56
+ }
57
+ if (verb === 'pool') {
58
+ const sub = rest[0];
59
+ if (sub === 'status') return { lines: asLines(textOf(await poolStatus(exec))) };
60
+ if (sub === 'lease') {
61
+ const pubkey = readPubkey(typeof flags['pubkey-file'] === 'string' ? flags['pubkey-file'] : undefined);
62
+ const p = await poolLease(exec, {
63
+ lessee: flags.lessee, pubkey, cores: flags.cores, memoryGb: flags['memory-gb'],
64
+ diskGb: flags['disk-gb'], purpose: flags.purpose, reason,
65
+ });
66
+ return { lines: asLines(textOf(p)) };
67
+ }
68
+ if (sub === 'done') return { lines: asLines(textOf(await poolDone(exec, need(rest[1], 'a vmid'), { reason }))) };
69
+ if (sub === 'config') {
70
+ let input = {};
71
+ if (flags.set) { try { input = JSON.parse(flags.set); } catch { throw new Error('--set must be a JSON object'); } }
72
+ return { lines: asLines(textOf(await poolConfig(exec, input, { reason }))) };
73
+ }
74
+ return { lines: ['Usage: /vm pool <status|lease|done <vmid>|config>'], tone: 'warn' };
75
+ }
76
+ if (verb === 'node') {
77
+ const [action, name] = rest;
78
+ if (!NODE_ACTIONS.includes(action)) return { lines: ['Usage: /vm node <reboot|shutdown> <node>'], tone: 'warn' };
79
+ return { lines: asLines(textOf(await nodePower(exec, action, need(name, 'a node name'), { reason }))) };
80
+ }
81
+ return { lines: [`Unknown /vm verb "${verb}". ${VM_USAGE}`], tone: 'warn' };
82
+ } catch (e) {
83
+ if (e instanceof VmPendingError) return { lines: [pendingHint(e)], tone: 'warn' };
84
+ throw e;
85
+ }
86
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * WALKTHROUGH for a plain terminal (`quox approvals walkthrough`, `quox inbox walkthrough`): the waiting decisions one
3
+ * card at a time, using the same card and reply rules as the TUI (quoxon/approvalsView.js). Identity is the decision id.
4
+ * io = { readLine(prompt) -> Promise<string|null>, write(line), answer(item, key, note) -> Promise<void> }.
5
+ */
6
+ import { isDecision, walkCard, walkReply, walkNotYesNo, walkNoCatch, walkSummary } from '../quoxon/approvalsView.js';
7
+
8
+ export async function runWalkthrough(items, io) {
9
+ const decs = (items || []).filter(isDecision);
10
+ if (!decs.length) { io.write('No question is waiting for you.'); return { answered: [], skipped: [], left: [] }; }
11
+ const st = { answered: [], skipped: [], left: [] };
12
+ io.write(`${decs.length} question${decs.length === 1 ? '' : 's'} waiting for you. One at a time: a choice, then an optional note.`);
13
+ for (let i = 0; i < decs.length; i++) {
14
+ const it = decs[i]; const card = walkCard(it, i + 1, decs.length);
15
+ io.write(''); card.lines.forEach((l) => io.write(l));
16
+ for (;;) {
17
+ const line = await io.readLine('Your choice? ');
18
+ if (line == null) { for (let j = i; j < decs.length; j++) st.left.push(decs[j].title || decs[j].id); io.write(''); walkSummary(st).forEach((l) => io.write(l)); return st; }
19
+ const rep = walkReply(it, line);
20
+ if (!rep) { io.write(walkNoCatch(it)); continue; }
21
+ if (rep.notYesNo) { io.write(walkNotYesNo(it)); continue; }
22
+ if (rep.defer) { st.skipped.push(it.title || it.id); break; }
23
+ if (rep.ask) { io.write(` Asked by ${it.payload.session || 'a session'} (id ${it.id}). ${it.payload.why || 'No further reason was given.'}`); continue; }
24
+ if (it.payload.high_risk === true && rep.key !== 'n') { io.write(' High risk: a yes needs your PIN, which this plain terminal cannot take. Answer it in `quox` with /approvals walkthrough, or skip it here.'); continue; }
25
+ try { await io.answer(it, rep.key, rep.note || null); }
26
+ catch (e) { io.write(` Not answered (${e.message}). The question stays open.`); continue; }
27
+ const label = it.payload.options.find((o) => o.key === rep.key).label;
28
+ io.write(`✔ ${it.title || it.id}: ${label}`);
29
+ st.answered.push({ title: it.title || it.id, word: label });
30
+ break;
31
+ }
32
+ }
33
+ io.write(''); walkSummary(st).forEach((l) => io.write(l));
34
+ return st;
35
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Launcher for the HYPERION deck TUI (`quox hyperion deck`). Same shape as lucid/deck.js: alt screen via
3
+ * fullscreen-ink when the terminal supports raw mode; the terminal is restored on every exit path.
4
+ */
5
+ export async function launchHyperionDeck(api, { intervalMs = 10000 } = {}) {
6
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function' || !process.stdout.isTTY) {
7
+ console.error('quox hyperion deck needs an interactive terminal. Use `quox hyperion` for a one-shot view.');
8
+ process.exitCode = 1;
9
+ return;
10
+ }
11
+ // A long-running TUI must use React's production build (the development build grows the heap per render).
12
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
13
+ await import('../tui/index.js'); // registers the .jsx loader hook
14
+ const React = (await import('react')).default;
15
+ const { HyperionDeck } = await import('../tui/views/HyperionDeck.jsx');
16
+ const { withFullScreen } = await import('fullscreen-ink');
17
+ const app = withFullScreen(React.createElement(HyperionDeck, { api, intervalMs }), { exitOnCtrlC: true });
18
+ const restore = () => { try { process.stdout.write('\x1b[?1049l\x1b[?25h'); } catch { /* best effort */ } };
19
+ process.once('SIGTERM', () => { restore(); process.exit(143); });
20
+ process.once('SIGHUP', () => { restore(); process.exit(129); });
21
+ process.once('exit', restore);
22
+ process.once('uncaughtException', (e) => { restore(); console.error(e); process.exit(1); });
23
+ try {
24
+ await app.start();
25
+ await app.waitUntilExit();
26
+ } finally {
27
+ restore();
28
+ }
29
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * <LucidCard/>: draws a Quoxon object-card view ({ box, rows }, see cards.js) for the deck.
3
+ *
4
+ * Same geometry and roles as quoxon/objects/view.js, plus what a navigable list needs:
5
+ * box.sel 'focus' | 'dim' the title sits on a bar (and inverse under NO_COLOR) so the selected card is unmistakable
6
+ * role 'sel' the highlighted action bar
7
+ * t (seconds) drop-in reveal: rows decode through half-width katakana, then a short colour
8
+ * afterglow on the real characters. Infinity = settled (what idle always shows).
9
+ * Colour comes from the theme roles (light/dark, 256-colour, NO_COLOR); the characters never change.
10
+ */
11
+ import React from 'react';
12
+ import { Text } from 'ink';
13
+ import { c, mono } from '../quoxon/theme.js';
14
+ import { agentColor } from '../quoxon/theme.js';
15
+ import { decodeRow, padRow, fit, len } from '../quoxon/objects/model.js';
16
+ import { rowStart } from '../quoxon/objects/view.js';
17
+ import { hexRgb, mix, rgbStr } from '../quoxon/motion/color.js';
18
+
19
+ const h = React.createElement;
20
+ export const BAR = { focus: '#1E3A8A', dim: '#27272A' }; // selection bars (white text on both)
21
+
22
+ function roleColor(role) {
23
+ if (mono()) return undefined;
24
+ if (typeof role === 'string' && role.startsWith('agent:')) return agentColor(role.slice(6));
25
+ return c(role || 'value');
26
+ }
27
+ /** A role colour lifted towards white by the afterglow (hex colours only; anything else stays as is). */
28
+ function glowColor(role, g) {
29
+ const base = roleColor(role);
30
+ return typeof base === 'string' && base[0] === '#' ? rgbStr(mix(hexRgb(base), [255, 255, 255], 0.65 * g)) : base;
31
+ }
32
+
33
+ function runs(cells, key) {
34
+ const out = [];
35
+ let run = '', col, bold = false, bg, k = 0;
36
+ const flush = () => { if (run) out.push(h(Text, { key: `${key}-${k++}`, color: col, bold, backgroundColor: bg }, run)); run = ''; };
37
+ for (const cl of cells) {
38
+ let color, back;
39
+ if (cl.role === 'sel') { color = mono() ? undefined : '#FFFFFF'; back = mono() ? undefined : BAR.focus; }
40
+ else if (cl.glyph) color = roleColor('teal'); // the katakana flicker
41
+ else color = cl.glow > 0.05 ? glowColor(cl.role, cl.glow) : roleColor(cl.role);
42
+ const b = Boolean(cl.bold);
43
+ if (color !== col || b !== bold || back !== bg) { flush(); col = color; bold = b; bg = back; }
44
+ run += cl.ch;
45
+ }
46
+ flush();
47
+ return out;
48
+ }
49
+
50
+ export function LucidCard({ view, t = Infinity }) {
51
+ const b = view.box, w = b.width, inner = w - 4;
52
+ const rows = view.rows || [];
53
+ const bc = roleColor(b.border || 'faint');
54
+ const right = b.right ? ` ${b.right} ` : '';
55
+ const title = b.title ? ` ${fit(b.title, Math.max(4, w - 6 - (right ? len(right) + 1 : 0)))} ` : '';
56
+ const fill = Math.max(0, w - 3 - len(title) - len(right) - 1);
57
+ const bar = b.sel ? BAR[b.sel] : null;
58
+ const top = h(Text, { key: 'top', wrap: 'truncate' },
59
+ h(Text, { color: bc }, '╭─'),
60
+ h(Text, { color: bar && !mono() ? '#FFFFFF' : roleColor(b.titleRole || 'value'), backgroundColor: bar && !mono() ? bar : undefined, inverse: Boolean(bar) && mono(), bold: true }, title),
61
+ h(Text, { color: bc }, '─'.repeat(fill)),
62
+ h(Text, { color: roleColor(b.rightRole || 'muted') }, right),
63
+ h(Text, { color: bc }, '─╮'));
64
+ const body = rows.map((r, i) => h(Text, { key: `b${i}`, wrap: 'truncate' },
65
+ h(Text, { color: bc }, '│ '), ...runs(decodeRow(padRow(r, inner), t, rowStart(i, rows.length), 0.25, 70 + i), `b${i}`), h(Text, { color: bc }, ' │')));
66
+ const bottom = h(Text, { key: 'bot', color: bc }, '╰' + '─'.repeat(w - 2) + '╯');
67
+ return h(React.Fragment, null, top, ...body, bottom);
68
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * LucidIntro: the ~1.2s startup for `quox lucid deck`.
2
+ * LucidIntro: the ~1.6s startup for `quox lucid deck`.
3
3
  *
4
- * Frames (50ms): the QUOX block wordmark is revealed by a gradient sweep, the
5
- * ◉ LUCID mark fades in under it, then "tuning in…" resolves into
4
+ * Frames (50ms): the Quoxon particle collapse (particles swarm in and lock into the horned Q, tail last),
5
+ * the ◉ LUCID mark fades in under it, then "tuning in…" resolves into
6
6
  * "◉ LUCID │ N sources live · M need you" once the first fetch has landed.
7
7
  * The fetch is owned by the deck and runs in parallel, never gated on this.
8
8
  * onDone fires once: at the natural end, on a hard 2.5s cap, or on a key
@@ -10,38 +10,25 @@
10
10
  */
11
11
  import React, { useState, useEffect, useRef } from 'react';
12
12
  import { Box, Text } from 'ink';
13
- import chalk from 'chalk';
14
- import { BLOCK_FONT, gradientColor } from '../tui/intro/Intro.jsx';
15
- import { PALETTE, STATE_HEX } from './fmt.js';
13
+ import { c, gradient, mono } from '../quoxon/theme.js';
14
+ import { QGlyph } from '../quoxon/qmotion.js';
15
+ import { Clock } from '../quoxon/motion/clock.js';
16
+ import { particleCellsAt, qCells, cellsToAnsi, labSize } from '../quoxon/qart.js';
16
17
 
17
18
  export const FRAME_MS = 50;
18
- const SWEEP_FRAMES = 12;
19
- const MIN_FRAMES = 24; // 1.2s
20
- const MAX_FRAMES = 50; // 2.5s cap if the fetch never lands
19
+ const PARTICLE_MS = 1100; // the collapse; the Q is fully settled ~470ms later
20
+ const MIN_FRAMES = 32; // 1.6s: the Q settles, then the live line shows
21
+ const MAX_FRAMES = 50; // 2.5s cap if the fetch never lands
21
22
 
22
- const LETTERS = ['Q', 'U', 'O', 'X'];
23
- const rgb = (c, s) => chalk.rgb(c[0], c[1], c[2])(s);
24
-
25
- function titleRow(row, frame, colsRevealed) {
26
- let out = ''; let col = 0;
27
- const total = LETTERS.reduce((n, ch) => n + BLOCK_FONT[ch][0].length + 1, 0);
28
- for (const ch of LETTERS) {
29
- const g = BLOCK_FONT[ch][row];
30
- for (let i = 0; i < g.length; i++) {
31
- const x = col + i;
32
- const lit = g[i] !== ' ';
33
- if (x > colsRevealed) { out += ' '; continue; }
34
- if (!lit) { out += ' '; continue; }
35
- const edge = colsRevealed - x < 2 && colsRevealed < total;
36
- out += edge ? chalk.white('██') : rgb(gradientColor(x / total + frame * 0.02), '██');
37
- }
38
- out += ' ';
39
- col += g.length + 1;
40
- }
41
- return out;
23
+ /** Rows of the particle Q at frame `frame` (motion off: the settled Q). Under 12 rows the compact Q. */
24
+ export function introQ(cols, rows, frame, motion = true) {
25
+ const size = labSize(cols, Math.max(0, rows - 8));
26
+ const color = !mono();
27
+ const t = frame * FRAME_MS;
28
+ return cellsToAnsi(motion ? particleCellsAt(size, t, { dur: PARTICLE_MS, color }) : qCells(size, { color }));
42
29
  }
43
30
 
44
- export function LucidIntro({ data, err, onDone, rows = 24 }) {
31
+ export function LucidIntro({ data, err, onDone, rows = 24, cols = 100, motion = true }) {
45
32
  const [frame, setFrame] = useState(0);
46
33
  const doneRef = useRef(false);
47
34
  const readyAt = useRef(null);
@@ -49,10 +36,11 @@ export function LucidIntro({ data, err, onDone, rows = 24 }) {
49
36
 
50
37
  if (sit && readyAt.current == null) readyAt.current = frame;
51
38
 
52
- useEffect(() => {
53
- const id = setInterval(() => setFrame((f) => f + 1), FRAME_MS);
54
- return () => clearInterval(id);
55
- }, []);
39
+ // The deck's shared clock drives the intro too: one timer, armed only while the intro still has frames to play.
40
+ const clockRef = useRef(null);
41
+ if (!clockRef.current) clockRef.current = new Clock(() => setFrame((f) => f + 1));
42
+ useEffect(() => { if (!doneRef.current) clockRef.current.schedule([{ every: FRAME_MS }]); });
43
+ useEffect(() => () => clockRef.current.stop(), []);
56
44
 
57
45
  const finishAt = readyAt.current != null
58
46
  ? Math.max(MIN_FRAMES, readyAt.current + 6)
@@ -62,44 +50,43 @@ export function LucidIntro({ data, err, onDone, rows = 24 }) {
62
50
  if (frame >= finishAt && !doneRef.current) { doneRef.current = true; onDone(); }
63
51
  }, [frame, finishAt, onDone]);
64
52
 
65
- const tall = rows >= 16;
66
- const total = LETTERS.reduce((n, ch) => n + BLOCK_FONT[ch][0].length + 1, 0);
67
- const revealed = Math.round((Math.min(frame, SWEEP_FRAMES) / SWEEP_FRAMES) * (total + 2)) - 1;
68
- const markOn = frame >= 6;
69
- const resolved = sit && frame >= 14 && frame >= (readyAt.current ?? 0) + 2;
53
+ const qRows = introQ(cols, rows, frame, motion);
54
+ const markOn = frame >= 14;
55
+ const resolved = sit && frame >= 22 && frame >= (readyAt.current ?? 0) + 2;
70
56
  const cov = (sit && sit.cannot_see && sit.cannot_see.coverage) || { live: 0, expected: 0 };
71
57
  const dots = '.'.repeat((frame % 3) + 1).padEnd(3, ' ');
72
58
 
73
- const mark = [...'◉ LUCID'].map((ch, i) => (
74
- <Text key={i} bold color={'#' + gradientColor((i / 7) + frame * 0.03).map((v) => v.toString(16).padStart(2, '0')).join('')}>{ch}</Text>
75
- ));
59
+ const mark = (
60
+ <Text>
61
+ <QGlyph state="working" ms={Date.now()} quiet={!motion} />
62
+ <Text> </Text>
63
+ {gradient('LUCID').map((g, i) => <Text key={i} bold color={g.color}>{g.ch}</Text>)}
64
+ </Text>
65
+ );
76
66
 
77
67
  return (
78
68
  <Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1} paddingY={1}>
79
- {tall ? (
80
- <Box flexDirection="column" alignItems="center">
81
- {Array.from({ length: 5 }, (_, r) => <Text key={r}>{titleRow(r, frame, revealed)}</Text>)}
82
- </Box>
83
- ) : (
84
- <Text bold color={PALETTE.brand}>Q U O X</Text>
85
- )}
69
+ <Box flexDirection="column" alignItems="center">
70
+ {qRows.map((l, r) => <Text key={r} wrap="truncate">{l}</Text>)}
71
+ </Box>
86
72
  <Box marginTop={1}>{markOn ? mark : <Text> </Text>}</Box>
87
73
  <Box marginTop={1}>
88
- {frame < 10 ? <Text> </Text> : resolved ? (
74
+ {frame < 16 ? <Text> </Text> : resolved ? (
89
75
  <Text>
90
- <Text bold color={PALETTE.brand}>◉ LUCID</Text>
91
- <Text color={PALETTE.dim}> │ </Text>
92
- <Text color={STATE_HEX.ok}>{cov.live} sources live</Text>
93
- <Text color={PALETTE.dim}> · </Text>
94
- <Text bold color={(sit.needs_you || []).length ? STATE_HEX.attention : STATE_HEX.ok}>{(sit.needs_you || []).length} need you</Text>
76
+ <QGlyph state={(sit.needs_you || []).length ? 'waiting' : 'resting'} ms={Date.now()} quiet />
77
+ <Text bold color={c('sky')}> LUCID</Text>
78
+ <Text color={c('muted')}> │ </Text>
79
+ <Text color={c('ok')}>{cov.live} sources live</Text>
80
+ <Text color={c('muted')}> · </Text>
81
+ <Text bold color={(sit.needs_you || []).length ? c('warn') : c('ok')}>{(sit.needs_you || []).length} need you</Text>
95
82
  </Text>
96
83
  ) : err && !sit ? (
97
- <Text color={STATE_HEX.blind}>LUCID unreachable, still trying</Text>
84
+ <Text color={c('error')}>LUCID unreachable, still trying</Text>
98
85
  ) : (
99
- <Text color={PALETTE.muted}>tuning in{dots}</Text>
86
+ <Text color={c('muted')}>tuning in{dots}</Text>
100
87
  )}
101
88
  </Box>
102
- <Box marginTop={1}><Text color={PALETTE.dim}>any key to skip</Text></Box>
89
+ <Box marginTop={1}><Text color={c('muted')}>any key to skip</Text></Box>
103
90
  </Box>
104
91
  );
105
92
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * LUCID object cards: pure views for the deck, in the Quoxon object-card shape ({ box, rows } of
3
+ * { text, role } segments, see quoxon/objects/model.js). An area is a tile card; a Needs-you item is a card
4
+ * that opens in place into who / what / why / when and the actions. Nothing here reads a clock or does I/O.
5
+ *
6
+ * Roles are theme roles; the renderer (LucidCard.jsx) also understands 'sel' (the selected action bar).
7
+ * Text is WRAPPED to the card, never clipped, except for a single action line and the command line.
8
+ */
9
+ import { seg, fit, padRow } from '../quoxon/objects/model.js';
10
+ import { wrap, whenText, ageOf, GLYPH, clip } from './fmt.js';
11
+
12
+ export const URGENCY_ROLE = { now: 'error', today: 'warn', 'this week': 'sky', whenever: 'muted', fyi: 'muted' };
13
+ export const URGENCY_LABEL = { now: 'NOW', today: 'TODAY', 'this week': 'THIS WEEK', whenever: 'WHENEVER', fyi: 'FYI' };
14
+ export const STATE_ROLE = { ok: 'ok', attention: 'warn', unknown: 'muted', blind: 'error' };
15
+ export const STATE_WORD = { ok: 'ok', attention: 'attention', unknown: 'no signal', blind: 'blind' };
16
+
17
+ const nm = (x) => (typeof x === 'string' ? x : (x && (x.label || x.name || x.L0 || x.id)) || '');
18
+ const LW = 8; // label column of the open card
19
+
20
+ /**
21
+ * An area tile. d: { area, label, state, L0, last, n (needs), active (filter on), selected, focused,
22
+ * expanded, now }. A tile is one status row; the selected tile in a focused pane opens to its sentence.
23
+ */
24
+ export function lucidArea(d, cols) {
25
+ const w = Math.max(18, cols), inner = w - 4;
26
+ const all = d.area == null;
27
+ const glyph = all ? '◉' : (GLYPH[d.state] || '?');
28
+ const role = all ? 'sky' : (STATE_ROLE[d.state] || 'muted');
29
+ const right = `${d.active ? '● ' : ''}${d.n ? d.n : '·'}`;
30
+ const rows = [];
31
+ const word = all ? 'everything' : (STATE_WORD[d.state] || 'unknown');
32
+ const age = d.last && d.last.ts != null && d.now != null ? `last ${ageOf(d.last.ts, d.now)}` : '';
33
+ const line = age && [...`${word} · ${age}`].length <= inner ? `${word} · ${age}` : word;
34
+ rows.push([seg(line, d.selected ? 'value' : role === 'ok' ? 'muted' : role)]);
35
+ if (d.expanded && d.L0) for (const ln of wrap(d.L0, inner)) rows.push([seg(ln, 'value')]);
36
+ const mark = d.selected ? '▶ ' : '';
37
+ return {
38
+ box: {
39
+ width: w, title: `${mark}${glyph} ${fit(d.label || d.area || 'All areas', Math.max(4, inner - 8))}`, titleRole: role,
40
+ right, rightRole: d.n ? 'warn' : 'muted', border: d.selected ? 'sky' : 'faint', sel: d.selected ? (d.focused ? 'focus' : 'dim') : null,
41
+ },
42
+ rows,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * A Needs-you card. d: { item, areaLabel, isNew, selected, focused, open, mSel, now }.
48
+ * Collapsed: the wrapped sentence. Open: the sentence, who/what/why/when, affects, then the actions with
49
+ * the highlighted one as a bar and its exact command beneath.
50
+ */
51
+ export function lucidNeed(d, cols) {
52
+ const it = d.item, w = Math.max(24, cols), inner = w - 4;
53
+ const urg = it.urgency || 'whenever';
54
+ const a = it.L1 || {};
55
+ const ms = (it.L2 && it.L2.measures) || [];
56
+ const mark = d.selected ? '▶ ' : '';
57
+ const rows = [];
58
+ const text = (d.isNew ? '● ' : '') + (it.L0 || '');
59
+ wrap(text, inner).forEach((ln, i) => rows.push([seg(ln, d.isNew ? 'ok' : d.selected ? 'value' : 'text', d.selected)]));
60
+ if (d.open) {
61
+ const detail = (lbl, v) => wrap(v, inner - LW).forEach((ln, i) => rows.push([seg(i ? ' '.repeat(LW) : lbl.padEnd(LW), i ? 'muted' : 'label'), seg(ln, 'text')]));
62
+ for (const [lbl, v] of [['Who', a.who], ['What', a.what], ['Why', a.why], ['When', whenText(a.when, d.now)]]) if (v) detail(lbl, v);
63
+ if ((a.affected || []).length) rows.push([seg('Affects'.padEnd(LW), 'label'), seg(clip(a.affected.map(nm).filter(Boolean).join(', '), inner - LW), 'muted')]);
64
+ rows.push(ms.length ? [seg('What you can do', 'key', true)] : [seg('No actions to offer here. Esc closes.', 'muted')]);
65
+ ms.forEach((m, mi) => {
66
+ const on = mi === d.mSel;
67
+ const risk = ` ${m.risk}`;
68
+ const label = fit(m.say || m.label, Math.max(8, inner - 2 - risk.length));
69
+ if (on) rows.push([seg(`❯ ${label}${risk}`.padEnd(inner, ' '), 'sel', true)]);
70
+ else rows.push([seg(' ', 'muted'), seg(label, 'text'), seg(risk, m.risk === 'dangerous' ? 'error' : m.risk === 'gated' ? 'warn' : 'muted')]);
71
+ });
72
+ const cmd = ms[d.mSel] && ms[d.mSel].command;
73
+ if (cmd) rows.push([seg(' $ ', 'muted'), seg(clip(cmd, inner - 4), 'code')]);
74
+ else if (ms.some((m) => m.command)) rows.push([seg(' ', 'muted')]);
75
+ }
76
+ return {
77
+ box: {
78
+ width: w, title: `${mark}${URGENCY_LABEL[urg] || String(urg).toUpperCase()} · ${it.kind || 'item'}`, titleRole: URGENCY_ROLE[urg] || 'muted',
79
+ right: d.areaLabel || '', rightRole: 'muted', border: d.selected ? 'sky' : (urg === 'now' ? 'error' : 'faint'), sel: d.selected ? (d.focused ? 'focus' : 'dim') : null,
80
+ },
81
+ rows,
82
+ };
83
+ }
84
+
85
+ export const cardHeight = (view) => view.rows.length + 2;
86
+ export { padRow };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The sitrep as chat Markdown (Discord, Matrix, slots): short, scannable, under one Discord message.
3
+ * Owner 2026-10-17: "Format your sitrep" after the spoken paragraph was posted to Discord as a wall of text.
4
+ * Pure: data in, string out. No colour codes, no box drawing (chat clients mangle both).
5
+ */
6
+
7
+ const URGENCY = { now: '🔴', today: '🟠', this_week: '🟡', week: '🟡', whenever: '⚪', fyi: '⚪' };
8
+ const STATE = { attention: '⚠️', ok: '✅', partial: '🟡', unknown: '❔', blind: '⛔' };
9
+ export const MAX_CHARS = 1900; // Discord's limit is 2000 per message
10
+
11
+ const clip = (s, n) => {
12
+ const t = String(s || '').replace(/\s+/g, ' ').trim();
13
+ return t.length > n ? `${t.slice(0, n - 1)}…` : t;
14
+ };
15
+
16
+ function age(ms) {
17
+ if (!(ms > 0)) return '';
18
+ const m = Math.floor(ms / 60000);
19
+ if (m < 60) return `${m}m`;
20
+ const h = Math.floor(m / 60);
21
+ if (h < 48) return `${h}h`;
22
+ return `${Math.floor(h / 24)}d`;
23
+ }
24
+
25
+ function clockLondon(now) {
26
+ try {
27
+ return new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }).format(new Date(now));
28
+ } catch { return new Date(now).toISOString().slice(11, 16) + ' UTC'; }
29
+ }
30
+
31
+ /** opts: { now, maxNeeds (default 6), maxChars (default MAX_CHARS) } */
32
+ export function renderSitrepMarkdown(data, opts = {}) {
33
+ const sit = (data && data.situation) || data || {};
34
+ const now = opts.now ?? Date.now();
35
+ const maxNeeds = opts.maxNeeds ?? 6;
36
+ const maxChars = opts.maxChars ?? MAX_CHARS;
37
+ const cov = sit.coverage || (sit.cannot_see && sit.cannot_see.coverage) || { live: 0, expected: 0 };
38
+ const missing = sit.coverage && Array.isArray(sit.coverage.missing) ? sit.coverage.missing : [];
39
+ const needs = Array.isArray(sit.needs_you) ? sit.needs_you : [];
40
+ const low = needs.filter((n) => n.urgency === 'whenever' || n.urgency === 'fyi').length;
41
+
42
+ const out = [];
43
+ out.push(`**◉ LUCID sitrep** · ${clockLondon(now)} · ${cov.live}/${cov.expected} sources`);
44
+ out.push('');
45
+ if (!needs.length) {
46
+ out.push('**Needs you:** nothing right now.');
47
+ } else {
48
+ out.push(`**Needs you (${needs.length}${low ? `, ${low} low priority` : ''})**`);
49
+ needs.slice(0, maxNeeds).forEach((n, i) => {
50
+ const w = n.L1 && n.L1.when && n.L1.when.waited_ms;
51
+ const tail = [n.area, age(w)].filter(Boolean).join(', ');
52
+ out.push(`${i + 1}. ${URGENCY[n.urgency] || '•'} ${clip(n.L0, 90)}${tail ? ` _(${tail})_` : ''}`);
53
+ });
54
+ if (needs.length > maxNeeds) out.push(`…and ${needs.length - maxNeeds} more: \`quox sitrep\``);
55
+ }
56
+
57
+ const areas = Array.isArray(sit.areas) ? sit.areas : [];
58
+ if (areas.length) {
59
+ out.push('');
60
+ out.push('**At a glance**');
61
+ for (const a of areas) out.push(`${STATE[a.state] || '•'} **${a.label || a.area}**: ${clip(a.L0, 80)}`);
62
+ }
63
+
64
+ const dau = Array.isArray(sit.doesnt_add_up) ? sit.doesnt_add_up : [];
65
+ if (dau.length) {
66
+ out.push('');
67
+ out.push(`**Doesn't add up (${dau.length}):** ${clip(dau[0].L0, 90)}${dau.length > 1 ? ' …' : ''}`);
68
+ }
69
+ out.push('');
70
+ out.push(missing.length ? `**Can't see:** ${missing.join(', ')}` : `**Can't see:** nothing, every source is reporting.`);
71
+
72
+ // Stay inside one chat message: drop "at a glance" rows from the bottom, then needs, never the header.
73
+ let text = out.join('\n');
74
+ while (text.length > maxChars && out.length > 3) {
75
+ const i = out.findLastIndex((l) => /^(✅|⚠️|🟡|❔|⛔|•) \*\*/.test(l));
76
+ if (i < 0) break;
77
+ out.splice(i, 1);
78
+ text = out.join('\n');
79
+ }
80
+ return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
81
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * LUCID deck motion: pure helpers for the deck's ONE clock (quoxon/motion/clock.js).
3
+ *
4
+ * Idle is still. The deck declares a demand each render; the clock keeps one timer for the soonest demand and
5
+ * none at all when nothing asks. Frames (~12 fps) are demanded ONLY while something is visibly moving: a fetch
6
+ * in flight (the Q works), the Q settling after a state change (a short window, then still colour), or a
7
+ * card dropping in. Everything else is a one-shot wake (toast expiry, ghost expiry, the minute tick).
8
+ *
9
+ * LUCID state -> Q state (qglyph.js): needs you = waiting, fetching = working, unreachable = failing,
10
+ * quiet = resting. Failing wins over working so an outage never reads as busy.
11
+ */
12
+ import { FRAME_MS } from '../quoxon/motion/clock.js';
13
+
14
+ export { FRAME_MS };
15
+ /** After a state change the Q animates this long, then holds a still colour (no timer, no writes). */
16
+ export const SETTLE_MS = 2400;
17
+ /** A card dropping in is revealing for this long from its start (rows decode, then a short afterglow). */
18
+ export const REVEAL_MS = 1500;
19
+ /** Cards of one load start this far apart so the board fills top to bottom. */
20
+ export const STAGGER_MS = 70;
21
+
22
+ export function glyphStateOf({ err = null, fetching = false, needs = 0 } = {}) {
23
+ if (err) return 'failing';
24
+ if (fetching) return 'working';
25
+ return needs > 0 ? 'waiting' : 'resting';
26
+ }
27
+
28
+ /** Is the Q moving at `now`? Working moves while it lasts; every other state settles after SETTLE_MS. */
29
+ export function glyphAnimating({ state, since, now, motion }) {
30
+ if (!motion) return false;
31
+ if (state === 'working') return true;
32
+ return since != null && now - since < SETTLE_MS;
33
+ }
34
+
35
+ /** Seconds into a card's reveal, or Infinity when settled (or motion is off). A future start is negative. */
36
+ export function revealT(startAt, now, motion) {
37
+ if (!motion || startAt == null) return Infinity;
38
+ const ms = now - startAt;
39
+ return ms >= REVEAL_MS ? Infinity : ms / 1000;
40
+ }
41
+
42
+ /**
43
+ * The demands for this render. frames: something is moving right now. at: epoch ms of one-shot wakes
44
+ * (past or null entries are ignored). Without motion there are no frame demands at all.
45
+ */
46
+ export function deckDemands({ motion = false, frames = false, at = [], now = Date.now() } = {}) {
47
+ const d = [];
48
+ if (motion && frames) d.push({ every: FRAME_MS });
49
+ let soonest = null;
50
+ for (const a of at) if (Number.isFinite(a) && a > now && (soonest == null || a < soonest)) soonest = a;
51
+ if (soonest != null) d.push({ at: soonest });
52
+ return d;
53
+ }
54
+
55
+ /** Next wall-clock minute boundary for a displayed time (the injected clock, so tests stay deterministic). */
56
+ export const nextMinuteAt = (wall, shown) => wall + (60000 - (shown % 60000));
@@ -37,6 +37,13 @@ const TOOL_TABLE = [
37
37
  [/(^|_)(list|get|status)$|^contacts_/, 'Looking up'],
38
38
  ];
39
39
 
40
+ /** QUOXON-FW3: "Delegating <Agent> · <task gist>" instead of an anonymous delegate_to_agent. */
41
+ function delegateTarget(ev) {
42
+ const id = ev.agent_id || (String(ev.summary || '').match(/\bagent_id=(\S+)/) || [])[1] || '';
43
+ const name = ev.agent_name || id;
44
+ return { ...ev, target: [name, ev.task_gist].filter(Boolean).join(' \u00b7 ') };
45
+ }
46
+
40
47
  /** { word, detail } for a tool call. Unknown tools are "Working" with the tool name, never a guess. */
41
48
  export function toolWord(tool, ev = {}) {
42
49
  const name = String(tool || '').toLowerCase();
@@ -46,7 +53,7 @@ export function toolWord(tool, ev = {}) {
46
53
  return { word: 'Working', detail: name || null };
47
54
  }
48
55
 
49
- const mk = (word, detail = null, now = 0, tone = 'work', flash = null) => ({ word, detail: detail ? clipD(detail) : null, tone, flash, since: now });
56
+ const mk = (word, detail = null, now = 0, tone = 'work', flash = null, tool = null) => ({ word, detail: detail ? clipD(detail) : null, tone, flash, since: now, ...(tool ? { tool: clipD(tool, 40) } : {}) });
50
57
  export const initialActivity = (now = 0) => mk('Sending', null, now); // the request was dispatched: true from turn.begin
51
58
 
52
59
  /** Trace lines from the governance chain. Labels as the server sends them. */
@@ -82,7 +89,12 @@ export function nextActivity(prev, ev, now = 0, ctx = {}) {
82
89
  return p;
83
90
  }
84
91
  case 'trace': { const t = traceWord(ev); return t ? mk(t[0], t[1], now) : p; }
85
- case 'tool.started': { const t = toolWord(ev.tool, ev); return mk(t.word, t.detail, now); }
92
+ case 'tool.started': { const t = toolWord(ev.tool, ev.tool === 'delegate_to_agent' ? delegateTarget(ev) : ev); return mk(t.word, t.detail, now, 'work', null, ev.tool || null); }
93
+ case 'delegation.progress': { // QUOXON 2026-10-17: a specialist's real tool call, forwarded by the server
94
+ if (!ev.tool) return p;
95
+ const who = ev.agent_name || ev.agent_id || 'specialist';
96
+ return mk('Delegating', `${who} \u00b7 ${ev.tool}${ev.detail ? ` (${ev.detail})` : ''}`, now, 'work', null, ev.tool);
97
+ }
86
98
  case 'tool.finished': return ev.status === 'failed' ? { ...p, flash: now } : p; // keep the word, flash the Q only
87
99
  case 'approval.requested': return mk('Waiting', 'for your approval', now, 'wait');
88
100
  case 'approval.resolved': return mk('Resuming', null, now);