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,32 @@
1
+ /**
2
+ * Launcher for the LUCID deck TUI. Alt screen via fullscreen-ink when the
3
+ * terminal supports raw mode; restores the terminal on every exit path.
4
+ */
5
+ export async function launchDeck(api, { intervalMs = 10000, noIntro = false } = {}) {
6
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== 'function' || !process.stdout.isTTY) {
7
+ console.error('quox lucid deck needs an interactive terminal. Use `quox lucid --sitrep` for a one-shot snapshot.');
8
+ process.exitCode = 1;
9
+ return;
10
+ }
11
+ const { shouldIntro } = await import('./fmt.js');
12
+ const intro = shouldIntro({ noIntro: noIntro || process.argv.includes('--no-intro'), isTTY: true });
13
+ // A long-running TUI must use React's production build: the development build keeps debug
14
+ // records per render and grew the heap steadily (measured flat at 14MB in production).
15
+ if (!process.env.NODE_ENV) process.env.NODE_ENV = 'production';
16
+ await import('../tui/index.js'); // registers the .jsx loader hook
17
+ const React = (await import('react')).default;
18
+ const { LucidDeck } = await import('../tui/views/LucidDeck.jsx');
19
+ const { withFullScreen } = await import('fullscreen-ink');
20
+ const app = withFullScreen(React.createElement(LucidDeck, { api, intervalMs, intro }), { exitOnCtrlC: true });
21
+ const restore = () => { try { process.stdout.write('\x1b[?1049l\x1b[?25h'); } catch { /* best effort */ } };
22
+ process.once('SIGTERM', () => { restore(); process.exit(143); });
23
+ process.once('SIGHUP', () => { restore(); process.exit(129); });
24
+ process.once('exit', restore);
25
+ process.once('uncaughtException', (e) => { restore(); console.error(e); process.exit(1); });
26
+ try {
27
+ await app.start();
28
+ await app.waitUntilExit();
29
+ } finally {
30
+ restore();
31
+ }
32
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * LUCID presentation helpers shared by `quox lucid --sitrep` and the deck TUI.
3
+ * Pure functions, no I/O. Colour is a parameter, never read from globals, so
4
+ * renderers are deterministic under test.
5
+ */
6
+
7
+ export const GLYPH = { ok: '✔', attention: '⚠', unknown: '?', blind: '✖' };
8
+ export const STATE_HEX = { ok: '#22C55E', attention: '#FBBF24', unknown: '#A1A1AA', blind: '#EF4444' };
9
+ export const URGENCY = {
10
+ now: { label: 'NOW', hex: '#EF4444' },
11
+ today: { label: 'TODAY', hex: '#FBBF24' },
12
+ 'this week': { label: 'THIS WEEK', hex: '#38BDF8' },
13
+ whenever: { label: 'WHENEVER', hex: '#71717A' },
14
+ fyi: { label: 'FYI', hex: '#71717A' },
15
+ };
16
+ export const PALETTE = {
17
+ brand: '#38BDF8', accent: '#C084FC', text: '#E4E4E7', muted: '#A1A1AA', dim: '#71717A', code: '#7DD3FC',
18
+ };
19
+
20
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
21
+ export const stripAnsi = (s) => String(s).replace(ANSI_RE, '');
22
+ export const vlen = (s) => [...stripAnsi(s)].length;
23
+
24
+ /** Truncate a plain string to max visible chars with an ellipsis. */
25
+ export function clip(s, max) {
26
+ const a = [...String(s ?? '')];
27
+ if (a.length <= max) return a.join('');
28
+ return max <= 1 ? a.slice(0, max).join('') : a.slice(0, max - 1).join('') + '…';
29
+ }
30
+
31
+ /** Word-wrap plain text to width; hard-breaks over-long words. Returns lines. */
32
+ export function wrap(text, width) {
33
+ width = Math.max(4, width);
34
+ const out = [];
35
+ for (const para of String(text ?? '').split('\n')) {
36
+ let cur = '';
37
+ for (let w of para.split(/\s+/).filter(Boolean)) {
38
+ while ([...w].length > width) {
39
+ if (cur) { out.push(cur); cur = ''; }
40
+ out.push([...w].slice(0, width).join(''));
41
+ w = [...w].slice(width).join('');
42
+ }
43
+ if (!cur) cur = w;
44
+ else if ([...cur].length + 1 + [...w].length <= width) cur += ' ' + w;
45
+ else { out.push(cur); cur = w; }
46
+ }
47
+ out.push(cur);
48
+ }
49
+ return out.length ? out : [''];
50
+ }
51
+
52
+ export function padEnd(s, width) {
53
+ const n = width - vlen(s);
54
+ return n > 0 ? s + ' '.repeat(n) : s;
55
+ }
56
+
57
+ /** "3m", "2h", "4d" from a ms timestamp (relative to now). */
58
+ export function ageOf(ms, now = Date.now()) {
59
+ if (ms == null) return '';
60
+ const s = Math.max(0, Math.floor((now - ms) / 1000));
61
+ if (s < 60) return `${s}s`;
62
+ const m = Math.floor(s / 60);
63
+ if (m < 60) return `${m}m`;
64
+ const h = Math.floor(m / 60);
65
+ if (h < 24) return `${h}h`;
66
+ return `${Math.floor(h / 24)}d`;
67
+ }
68
+
69
+ /** ▰▰▰▰▱ meter, `cells` wide, filled proportionally (never rounds a gap to full). */
70
+ export function meter(live, expected, cells = 5) {
71
+ if (!expected) return '▱'.repeat(cells);
72
+ let f = Math.round((live / expected) * cells);
73
+ if (live < expected && f >= cells) f = cells - 1;
74
+ if (live > 0 && f === 0) f = 1;
75
+ return '▰'.repeat(f) + '▱'.repeat(cells - f);
76
+ }
77
+
78
+ export function clockLondon(ms = Date.now()) {
79
+ try {
80
+ return new Intl.DateTimeFormat('en-GB', {
81
+ timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', hour12: false,
82
+ }).format(new Date(ms));
83
+ } catch { return new Date(ms).toISOString().slice(11, 16); }
84
+ }
85
+
86
+ /** Plain-words account of a measure for the "do" line. */
87
+ export function firstMeasure(item) {
88
+ const ms = (item && item.L2 && item.L2.measures) || [];
89
+ return ms[0] || null;
90
+ }
91
+
92
+ export function whenText(w, now = Date.now()) {
93
+ if (!w) return '';
94
+ const parts = [];
95
+ if (w.clock) parts.push(w.clock);
96
+ else if (w.since) parts.push(`${ageOf(w.since, now)} ago`);
97
+ if (w.waited_ms != null && !w.clock) parts.push(`waiting ${ageOf(now - w.waited_ms, now)}`);
98
+ return parts.join(' · ');
99
+ }
100
+
101
+ /** Label for a changed[] entry in plain words. */
102
+ export function changedWords(c) {
103
+ const o = c.outcome ? `${c.outcome}` : (c.to ? `${c.from ? c.from + ' → ' : ''}${c.to}` : '');
104
+ return o;
105
+ }
106
+
107
+ /** Blind spot sentence list from cannot_see, names not counts where we have names. */
108
+ export function blindSpots(cs) {
109
+ cs = cs || {};
110
+ const nm = (x) => (typeof x === 'string' ? x : x && (x.feed || x.name || x.id || x.label)) || '';
111
+ const parts = [];
112
+ const silent = (cs.silent || []).map(nm).filter(Boolean);
113
+ const failing = (cs.failing || []).map(nm).filter(Boolean);
114
+ const stale = (cs.stale || []).map(nm).filter(Boolean);
115
+ const unwired = (cs.unwired || []).map(nm).filter(Boolean);
116
+ if (failing.length) parts.push({ k: 'failing', text: `failing: ${failing.join(', ')}` });
117
+ if (silent.length) parts.push({ k: 'silent', text: `silent: ${silent.join(', ')}` });
118
+ if (stale.length) parts.push({ k: 'stale', text: `stale: ${stale.join(', ')}` });
119
+ if (unwired.length) parts.push({ k: 'unwired', text: `not wired up: ${unwired.join(', ')}` });
120
+ return parts;
121
+ }
122
+
123
+ /** HH:MM:SS London clock for the living deck header. */
124
+ export function clockLondonSec(ms = Date.now()) {
125
+ try {
126
+ return new Intl.DateTimeFormat('en-GB', {
127
+ timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
128
+ }).format(new Date(ms));
129
+ } catch { return new Date(ms).toISOString().slice(11, 19); }
130
+ }
131
+
132
+ export const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
133
+
134
+ /** Whether the startup intro should play. Pure so it is testable. */
135
+ export function shouldIntro({ noIntro = false, env = process.env, isTTY = true } = {}) {
136
+ if (noIntro || !isTTY) return false;
137
+ if (env.NO_COLOR || env.QUOX_TUI_LITE) return false;
138
+ return true;
139
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `quox lucid --sitrep`: a one-shot, width-aware ANSI snapshot of the LUCID
3
+ * situation, built to read well in a terminal and when pasted into a chat.
4
+ * Pure: renderSitrep(situation, opts) -> string. Colour is explicit (opts.color).
5
+ */
6
+ import {
7
+ GLYPH, STATE_HEX, URGENCY, PALETTE, wrap, clip, padEnd, vlen, ageOf, meter,
8
+ clockLondon, firstMeasure, whenText, changedWords, blindSpots,
9
+ } from './fmt.js';
10
+
11
+ const R = '\x1b[0m';
12
+
13
+ export function makePainter(color) {
14
+ const rgb = (hex) => {
15
+ const n = parseInt(hex.slice(1), 16);
16
+ return `\x1b[38;2;${(n >> 16) & 255};${(n >> 8) & 255};${n & 255}m`;
17
+ };
18
+ const wrapSeq = (seq, s) => (color && s !== '' ? `${seq}${s}${R}` : s);
19
+ return {
20
+ color,
21
+ fg: (hex, s) => wrapSeq(rgb(hex), s),
22
+ bold: (s) => wrapSeq('\x1b[1m', s),
23
+ dim: (s) => wrapSeq(rgb(PALETTE.dim), s),
24
+ muted: (s) => wrapSeq(rgb(PALETTE.muted), s),
25
+ boldFg: (hex, s) => wrapSeq(`\x1b[1m${rgb(hex)}`, s),
26
+ pill: (hex, s) => (color ? `\x1b[48;2;${parseInt(hex.slice(1, 3), 16)};${parseInt(hex.slice(3, 5), 16)};${parseInt(hex.slice(5, 7), 16)}m\x1b[38;2;17;24;39m\x1b[1m[${s}]${R}` : `[${s}]`),
27
+ };
28
+ }
29
+
30
+ function tile(area, w, p, now) {
31
+ const inner = w - 4;
32
+ const hex = STATE_HEX[area.state] || STATE_HEX.unknown;
33
+ const glyph = GLYPH[area.state] || GLYPH.unknown;
34
+ const label = clip(area.label || area.area, inner - 3);
35
+ const topPlain = `─ ${glyph} ${label} `;
36
+ const top = p.fg(PALETTE.dim, '╭') + p.fg(PALETTE.dim, '─ ') + p.boldFg(hex, glyph) + ' ' + p.bold(label) + ' '
37
+ + p.fg(PALETTE.dim, '─'.repeat(Math.max(0, w - 2 - [...topPlain].length)) + '╮');
38
+ let body = wrap(area.L0 || 'No signal yet.', inner);
39
+ if (body.length > 3) body = [...body.slice(0, 2), clip(body[2] + ' ' + body.slice(3).join(' '), inner)];
40
+ const rows = body.map((l) => p.fg(area.state === 'ok' ? PALETTE.muted : PALETTE.text, l));
41
+ const last = area.last;
42
+ if (last) {
43
+ const tail = `${last.outcome ? ` (${last.outcome})` : ''} ${ageOf(last.ts, now)} ago`;
44
+ rows.push(p.dim(`last: ${clip(last.L0, Math.max(6, inner - 6 - [...tail].length))}${tail}`));
45
+ } else rows.push(p.dim('nothing yet'));
46
+ const v = p.fg(PALETTE.dim, '│');
47
+ const mid = rows.map((r) => `${v} ${padEnd(r, inner)} ${v}`);
48
+ const bot = p.fg(PALETTE.dim, '╰' + '─'.repeat(w - 2) + '╯');
49
+ return [top, ...mid, bot];
50
+ }
51
+
52
+ function grid(tiles, width, p, now) {
53
+ const gap = 1;
54
+ const tw = Math.floor((width - gap) / 2);
55
+ const out = [];
56
+ for (let i = 0; i < tiles.length; i += 2) {
57
+ const a = tile(tiles[i], tw, p, now);
58
+ const b = tiles[i + 1] ? tile(tiles[i + 1], tw, p, now) : [];
59
+ const h = Math.max(a.length, b.length);
60
+ const fill = (t, idx, wd) => {
61
+ if (t[idx] !== undefined) return t[idx];
62
+ // pad short tile with an empty bordered row before its bottom edge
63
+ return ' '.repeat(wd);
64
+ };
65
+ // equalise heights by inserting blank rows before the bottom border
66
+ const eq = (t) => {
67
+ if (!t.length) return t;
68
+ const bottom = t[t.length - 1];
69
+ const row = `${p.fg(PALETTE.dim, '│')}${' '.repeat(tw - 2)}${p.fg(PALETTE.dim, '│')}`;
70
+ const body = t.slice(0, -1);
71
+ while (body.length < h - 1) body.push(row);
72
+ return [...body, bottom];
73
+ };
74
+ const ea = eq(a), eb = eq(b);
75
+ for (let k = 0; k < h; k++) out.push(`${fill(ea, k, tw)}${' '.repeat(gap)}${eb.length ? fill(eb, k, tw) : ''}`.trimEnd());
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function section(title, width, p, extra = '') {
81
+ const t = ` ${title}${extra ? ' ' + extra : ''} `;
82
+ return p.boldFg(PALETTE.accent, title) + (extra ? ' ' + p.dim(extra) : '') + ' ' + p.fg(PALETTE.dim, '─'.repeat(Math.max(2, width - vlen(t))));
83
+ }
84
+
85
+ export function renderSitrep(data, opts = {}) {
86
+ const width = Math.max(60, Math.min(opts.width || 100, 140));
87
+ const now = opts.now ?? Date.now();
88
+ const p = makePainter(opts.color !== false);
89
+ const sit = (data && data.situation) || data || {};
90
+ const cs = sit.cannot_see || {};
91
+ const cov = cs.coverage || { live: 0, expected: 0 };
92
+ const lines = [];
93
+
94
+ const sep = p.dim('│');
95
+ const left = `${p.boldFg(PALETTE.brand, '◉ LUCID')} ${sep} ${p.bold('sitrep')} ${sep} ${clockLondon(now)} ${p.dim('·')} `
96
+ + `${p.fg(cov.live < cov.expected ? STATE_HEX.attention : STATE_HEX.ok, `${cov.live}/${cov.expected}`)} sources`;
97
+ lines.push(left);
98
+ lines.push(p.fg(PALETTE.dim, '━'.repeat(width)));
99
+
100
+ lines.push('');
101
+ for (const l of wrap(sit.L0 || 'No briefing available.', width)) lines.push(p.fg(PALETTE.text, l));
102
+ lines.push('');
103
+
104
+ const areas = sit.areas || [];
105
+ if (areas.length) {
106
+ lines.push(...grid(areas, width, p, now));
107
+ lines.push('');
108
+ }
109
+
110
+ const needs = sit.needs_you || [];
111
+ lines.push(section('NEEDS YOU', width, p, needs.length ? `${needs.length}` : ''));
112
+ if (!needs.length) {
113
+ lines.push(p.fg(STATE_HEX.ok, ' Nothing needs you right now.') + (cs.silent && cs.silent.length ? p.dim(' (but see what I can\'t see)') : ''));
114
+ }
115
+ const iw = width - 5;
116
+ needs.forEach((it, i) => {
117
+ const u = URGENCY[it.urgency] || URGENCY.whenever;
118
+ const num = String(i + 1).padStart(2);
119
+ lines.push('');
120
+ lines.push(`${p.dim(num + '.')} ${p.pill(u.hex, u.label)} ${p.bold(clip(it.L0 || '', width - 8 - u.label.length))}`);
121
+ const a = it.L1 || {};
122
+ const field = (k, v) => {
123
+ if (!v) return;
124
+ const wl = wrap(v, iw - 6);
125
+ wl.forEach((l, j) => lines.push(` ${j === 0 ? p.dim(k.padEnd(5)) : ' '}${p.fg(PALETTE.muted, l)}`));
126
+ };
127
+ field('Who', a.who); field('What', a.what); field('Why', a.why); field('When', whenText(a.when, now));
128
+ const m = firstMeasure(it);
129
+ if (m) {
130
+ const say = m.say || m.label;
131
+ lines.push(` ${p.boldFg(PALETTE.brand, '→ do:')} ${p.fg(PALETTE.text, clip(say, iw - 8))}`);
132
+ const cmd = m.command || m.url;
133
+ if (cmd) lines.push(` ${p.fg(PALETTE.code, clip(cmd, iw - 5))}`);
134
+ }
135
+ });
136
+ lines.push('');
137
+
138
+ const changed = sit.changed || [];
139
+ lines.push(section('SINCE YOU LOOKED', width, p, changed.length ? `${changed.length}` : ''));
140
+ if (!changed.length) lines.push(p.dim(' Nothing has changed.'));
141
+ for (const c of changed.slice(0, 8)) {
142
+ const o = changedWords(c);
143
+ const tail = `${o ? o + ' ' : ''}${ageOf(c.ts, now)} ago`;
144
+ lines.push(` ${p.dim('•')} ${clip(c.L0 || '', width - 6 - tail.length)} ${p.dim(tail)}`);
145
+ }
146
+ if (changed.length > 8) lines.push(p.dim(` … and ${changed.length - 8} more`));
147
+ lines.push('');
148
+
149
+ const da = sit.doesnt_add_up || [];
150
+ if (da.length) {
151
+ lines.push(section("DOESN'T ADD UP", width, p, `${da.length}`));
152
+ for (const x of da.slice(0, 5)) {
153
+ lines.push(` ${p.dim('•')} ${p.bold(clip(x.L0 || '', width - 6))}${x.urgent ? ' ' + p.pill(URGENCY.today.hex, 'TODAY') : ''}`);
154
+ for (const [k, v] of [['Claimed', x.claimed], ['Observed', x.observed]]) {
155
+ if (!v) continue;
156
+ wrap(v, width - 16).forEach((l, j) => lines.push(` ${j === 0 ? p.dim(k.padEnd(9)) : ' '.repeat(9)}${p.fg(PALETTE.muted, l)}`));
157
+ }
158
+ }
159
+ if (da.length > 5) lines.push(p.dim(` … and ${da.length - 5} more (quox lucid blind)`));
160
+ lines.push('');
161
+ }
162
+
163
+ lines.push(section("WHAT I CAN'T SEE", width, p, `${meter(cov.live, cov.expected)} ${cov.live}/${cov.expected}`));
164
+ const spots = blindSpots(cs);
165
+ if (!spots.length && cov.live >= cov.expected) lines.push(p.dim(' Every expected source is speaking.'));
166
+ else if (!spots.length) lines.push(p.fg(STATE_HEX.attention, ` ${cov.expected - cov.live} expected source(s) are not reporting.`));
167
+ for (const s of spots) {
168
+ for (const [j, l] of wrap(s.text, width - 4).entries()) {
169
+ lines.push(` ${p.fg(s.k === 'failing' ? STATE_HEX.blind : STATE_HEX.attention, l)}`);
170
+ }
171
+ }
172
+ return lines.join('\n') + '\n';
173
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Plugin loader for QuoxCLI
3
+ *
4
+ * Discovers and loads plugins from:
5
+ * 1. ~/.quox/plugins/ (user plugins)
6
+ * 2. QUOX_PLUGIN_DIR env var (custom path)
7
+ *
8
+ * Plugin format: each plugin is a directory with an index.js that exports:
9
+ * { name, version, description, register(program, context) }
10
+ */
11
+
12
+ import { existsSync, readdirSync, statSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { pathToFileURL } from 'node:url';
16
+
17
+ function getPluginDirs() {
18
+ const dirs = [join(homedir(), '.quox', 'plugins')];
19
+ if (process.env.QUOX_PLUGIN_DIR) {
20
+ dirs.push(process.env.QUOX_PLUGIN_DIR);
21
+ }
22
+ return dirs;
23
+ }
24
+
25
+ export async function discoverPlugins() {
26
+ const plugins = [];
27
+
28
+ for (const dir of getPluginDirs()) {
29
+ if (!existsSync(dir)) continue;
30
+
31
+ const entries = readdirSync(dir);
32
+ for (const entry of entries) {
33
+ const pluginPath = join(dir, entry);
34
+ if (!statSync(pluginPath).isDirectory()) continue;
35
+
36
+ const indexPath = join(pluginPath, 'index.js');
37
+ if (!existsSync(indexPath)) continue;
38
+
39
+ plugins.push({
40
+ name: entry,
41
+ path: pluginPath,
42
+ indexPath,
43
+ });
44
+ }
45
+ }
46
+
47
+ return plugins;
48
+ }
49
+
50
+ export async function loadPlugin(pluginInfo) {
51
+ try {
52
+ const moduleUrl = pathToFileURL(pluginInfo.indexPath).href;
53
+ const mod = await import(moduleUrl);
54
+
55
+ if (typeof mod.register !== 'function') {
56
+ throw new Error(`Plugin "${pluginInfo.name}" does not export a register() function`);
57
+ }
58
+
59
+ return {
60
+ name: mod.name || pluginInfo.name,
61
+ version: mod.version || '0.0.0',
62
+ description: mod.description || '',
63
+ register: mod.register,
64
+ };
65
+ } catch (err) {
66
+ return {
67
+ name: pluginInfo.name,
68
+ error: err.message,
69
+ };
70
+ }
71
+ }
72
+
73
+ export async function loadAllPlugins(program, context) {
74
+ const discovered = await discoverPlugins();
75
+ const loaded = [];
76
+ const errors = [];
77
+
78
+ for (const pluginInfo of discovered) {
79
+ const plugin = await loadPlugin(pluginInfo);
80
+ if (plugin.error) {
81
+ errors.push({ name: plugin.name, error: plugin.error });
82
+ } else {
83
+ try {
84
+ plugin.register(program, context);
85
+ loaded.push({ name: plugin.name, version: plugin.version, description: plugin.description });
86
+ } catch (err) {
87
+ errors.push({ name: plugin.name, error: err.message });
88
+ }
89
+ }
90
+ }
91
+
92
+ return { loaded, errors };
93
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * What is actually happening right now, as one honest word (CLI-QUOX indicator).
3
+ * Pure: the reducer feeds every real event through nextActivity and keeps the result on the turn.
4
+ * Rules: a word exists only because an event said so; between events the last true word stays (never
5
+ * invented, never rotated); unknown tools fall back to "Working" plus the tool name as detail.
6
+ * Words are capitalised present participles; the indicator appends the ellipsis.
7
+ *
8
+ * State: { word, detail, tone: 'work' | 'wait', flash: ms | null, since: ms }
9
+ */
10
+
11
+ const clipD = (s, n = 40) => { const t = String(s ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
12
+
13
+ // Ordered: first match wins, so specific families come before generic ones. [regex, word, detail?]
14
+ // Tool names are the real registry names (memory_search, bastion_ssh, quoxplan_set_status, ...).
15
+ const TOOL_TABLE = [
16
+ [/^(memory_(save|update)|brain_save|brain_garden_apply)/, 'Remembering'],
17
+ [/^memory_forget/, 'Forgetting'],
18
+ [/^(memory_|brain_|search_wisdom)/, 'Recalling'],
19
+ [/^(fleet_|bastion_).*(exec|ssh|run|restart|command)|^(bastion_ssh|fleet_exec)/, 'Running', 'host'],
20
+ [/^(fleet_|bastion_)/, 'Looking up'],
21
+ [/^(email_send|contacts_send)/, 'Sending'],
22
+ [/^contacts_dial/, 'Dialling'],
23
+ [/^(email_draft|inbox_propose|email_reply)/, 'Drafting'],
24
+ [/^(email_|inbox_)/, 'Checking mail'],
25
+ [/^(qlarity_|browser_(screenshot|lighthouse|pdf|find_element|list_tabs|monitor))/, 'Inspecting'],
26
+ [/^(browser_|http_|fetch_|web_)/, 'Fetching'],
27
+ [/^(seo_|quoxseo)/, 'Analysing'],
28
+ [/^(delegate|agent_handoff|agent_delegate|ask_agent|agent_call)|handoff/, 'Delegating', 'target'],
29
+ [/^(quoxflow_|workflow_)/, 'Orchestrating'],
30
+ [/^(quoxplan_|plan_)/, 'Planning'],
31
+ [/^(ward_|volt_|receipt|verify)/, 'Verifying'],
32
+ [/^approval/, 'Checking approvals'],
33
+ [/^calendar_/, 'Scheduling'],
34
+ [/^(search_|query_)|_search$|_find$/, 'Searching'],
35
+ [/(^|_)(write|edit|create)_(file|doc)|^file_(write|save)|^plan_doc/, 'Writing'],
36
+ [/(^|_)read(_|$)|^file_(read|get|open)/, 'Reading'],
37
+ [/(^|_)(list|get|status)$|^contacts_/, 'Looking up'],
38
+ ];
39
+
40
+ /** { word, detail } for a tool call. Unknown tools are "Working" with the tool name, never a guess. */
41
+ export function toolWord(tool, ev = {}) {
42
+ const name = String(tool || '').toLowerCase();
43
+ for (const [re, word, d] of TOOL_TABLE) {
44
+ if (re.test(name)) return { word, detail: d ? (clipD(ev[d] ?? ev.target ?? ev.host) || null) : null };
45
+ }
46
+ return { word: 'Working', detail: name || null };
47
+ }
48
+
49
+ const mk = (word, detail = null, now = 0, tone = 'work', flash = null) => ({ word, detail: detail ? clipD(detail) : null, tone, flash, since: now });
50
+ export const initialActivity = (now = 0) => mk('Sending', null, now); // the request was dispatched: true from turn.begin
51
+
52
+ /** Trace lines from the governance chain. Labels as the server sends them. */
53
+ function traceWord(ev) {
54
+ const label = String(ev.label || '').toLowerCase();
55
+ if (ev.protocol === 'aee' && /envelope opened/.test(label)) return ['Recording', 'envelope'];
56
+ if (ev.protocol === 'aocl') {
57
+ if (/\bl1\b|identity/.test(label)) return ['Verifying', 'L1 identity'];
58
+ if (/\bl3\b|policy/.test(label)) return ['Governing', 'L3 policy'];
59
+ return null;
60
+ }
61
+ if (ev.protocol === 'ward' && /witness/.test(label)) return ['Witnessing', null];
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Next activity for a stream event. ctx = { hasText } (has any answer text arrived yet).
67
+ * Returns prev unchanged for events that say nothing new.
68
+ */
69
+ export function nextActivity(prev, ev, now = 0, ctx = {}) {
70
+ const p = prev || initialActivity(now);
71
+ switch (ev?.type) {
72
+ case 'thinking': {
73
+ const stage = String(ev.stage || ''), msg = String(ev.message || '').replace(/[.…\s]+$/u, '');
74
+ if (stage === 'routing') return mk('Routing', (msg.match(/\bto\s+(.+)$/i) || [])[1] || null, now);
75
+ if (stage === 'routing_detail') return mk('Routing', msg || p.detail, now);
76
+ if (stage === 'processing') return mk('Reading', null, now);
77
+ if (stage === 'api_call') {
78
+ if (ctx.hasText) return p;
79
+ return mk('Thinking', (msg.match(/Calling (.+?) API/i) || [])[1] || null, now);
80
+ }
81
+ if (stage.includes('tool') && msg) { const t = toolWord(msg); return mk(t.word, t.detail, now); }
82
+ return p;
83
+ }
84
+ 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); }
86
+ case 'tool.finished': return ev.status === 'failed' ? { ...p, flash: now } : p; // keep the word, flash the Q only
87
+ case 'approval.requested': return mk('Waiting', 'for your approval', now, 'wait');
88
+ case 'approval.resolved': return mk('Resuming', null, now);
89
+ case 'steer.accepted': return mk('Adjusting', null, now);
90
+ case 'text.delta': case 'chunk': return p.word === 'Writing' && p.tone === 'work' ? p : mk('Writing', null, now);
91
+ case 'retry': case 'reconnecting': return mk('Reconnecting', null, now);
92
+ case 'resumed': return mk('Resuming', 'reattached', now);
93
+ case 'done': return mk('Confirming', 'waiting for the server', now);
94
+ case 'error': return { ...p, flash: now };
95
+ default: return p;
96
+ }
97
+ }
98
+
99
+ /** Client-side actions that are also true events (the human pressed Esc). */
100
+ export function actionActivity(prev, action, now = 0) {
101
+ if (action?.type === 'turn.begin') return initialActivity(action.now ?? now);
102
+ if (action?.type === 'interrupt.requested') return mk('Stopping', null, action.now ?? now);
103
+ return prev;
104
+ }