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,72 @@
1
+ /**
2
+ * QUOXON-FW3: a message typed while the server is restarting is kept, not dropped (pure helpers).
3
+ * A send is only retried when the connection failed BEFORE any event arrived (so no turn started and
4
+ * nothing can run twice); the caller enforces that.
5
+ */
6
+ export const SERVER_RESTARTING = "Quox server is restarting. I'll send this when it is back. Esc cancels.";
7
+ export const MAX_WAIT_MS = 180000;
8
+
9
+ /** True for "could not reach the server at all" (refused / fetch failed), never for an HTTP answer. */
10
+ export function isServerDown(e) {
11
+ if (!e || e.status) return false;
12
+ const code = e.cause?.code || e.code || '';
13
+ const msg = String(e.message || '');
14
+ return /ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|UND_ERR_SOCKET/.test(String(code)) || /Could not connect to|fetch failed|ECONNREFUSED/i.test(msg);
15
+ }
16
+
17
+ /** gh #1157: resume during a collector restart. Connection-class failures (refused, fetch failed, hang up, 502/503/504) are worth retrying; 401/404 are answers. */
18
+ export function isResumeRetryable(e) {
19
+ if (!e) return false;
20
+ if (e.status) return [502, 503, 504].includes(Number(e.status));
21
+ return isServerDown(e) || /socket hang up|ECONNRESET|other side closed|terminated/i.test(String(e.message || ''));
22
+ }
23
+ /** 2, 4, 8, 15, 30 s, then 30 s steps: about 2 minutes in all before giving up. */
24
+ export const RESUME_RETRY_DELAYS = [2000, 4000, 8000, 15000, 30000, 30000, 30000];
25
+ export const RESUME_RECONNECTING = 'Quox server restarting, reconnecting to your conversation…';
26
+
27
+ /** 1s, 2s, 4s, 8s, then 10s (capped). */
28
+ export const backoffMs = (i) => Math.min(10000, 1000 * 2 ** i);
29
+
30
+ const sleepFor = (ms, signal) => new Promise((r) => {
31
+ if (signal?.aborted) return r();
32
+ const t = setTimeout(done, ms);
33
+ function done() { clearTimeout(t); signal?.removeEventListener?.('abort', done); r(); }
34
+ signal?.addEventListener?.('abort', done, { once: true });
35
+ });
36
+
37
+ /**
38
+ * Poll health with backoff until it answers 2xx. -> 'up' | 'cancelled' | 'timeout'.
39
+ * delays: optional array (tests); else backoffMs. Total waiting is bounded by maxMs of scheduled sleep.
40
+ */
41
+ export async function waitForServer({ health, signal, delays = null, maxMs = MAX_WAIT_MS }) {
42
+ let waited = 0;
43
+ for (let i = 0; ; i++) {
44
+ if (signal?.aborted) return 'cancelled';
45
+ const d = delays ? delays[Math.min(i, delays.length - 1)] : backoffMs(i);
46
+ if (waited >= maxMs || (delays && i >= delays.length * 4)) return 'timeout';
47
+ await sleepFor(d, signal);
48
+ waited += d;
49
+ if (signal?.aborted) return 'cancelled';
50
+ let r = null;
51
+ try { r = await health(3000); } catch { r = null; }
52
+ if (r?.ok) return 'up';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * QUOXON-FW4: a stream that died mid-turn ("terminated" is undici's wording) gets a cause and a way
58
+ * forward instead of a bare word. Returns null when the error is not a transport/timeout failure.
59
+ */
60
+ export function explainTermination(e) {
61
+ if (!e || e.status) return null;
62
+ const msg = String(e.message || '');
63
+ const code = String(e.cause?.code || e.code || '');
64
+ if (e.name === 'TimeoutError' || /timed? ?out|ETIMEDOUT|UND_ERR_(HEADERS|BODY)_TIMEOUT/i.test(`${msg} ${code}`)) return `The turn timed out before the server finished.`;
65
+ if (/^terminated$/i.test(msg.trim()) || /ECONNRESET|UND_ERR_SOCKET|other side closed|premature close|aborted/i.test(`${msg} ${code}`)) {
66
+ return `The connection to the server was cut mid-turn (likely a server restart, proxy timeout or provider error).`;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /** The second line after a cut turn: the message is kept, and what to do next. */
72
+ export const RETRY_HINT = 'Your message is kept: press Up to recall it, then Enter to resend. Anything it started may or may not have finished; check before repeating it.';
@@ -7,6 +7,8 @@
7
7
  import { clip } from './theme.js';
8
8
 
9
9
  export const GLYPH = { running: '◐', waiting: '⏸', done: '✔', failed: '✖' };
10
+ /** A row that ended without finishing says HOW: failed is the only error; the rest are neutral (no error colour). */
11
+ export const OUTCOME_GLYPH = { failed: '✖', cancelled: '○', expired: '○', rejected: '⊘' };
10
12
  export const FRESH_MS = 10 * 60 * 1000; // finished rows linger this long
11
13
  export const USER_DONE_MS = 20 * 1000; // a user-started completion vanishes quickly: rows come and go
12
14
  export const PENDING_FRESH_MS = 24 * 3600 * 1000; // an older "pending" is stale, not waiting
@@ -36,7 +38,18 @@ export function elapsed(ms) {
36
38
 
37
39
  const ACTIVE_RE = /^(running|planning|decompos|executing|in_progress|active)/;
38
40
  const WAITING_RE = /^(paused|awaiting|blocked|plan_review|pending_approval|waiting)/;
39
- const FAILED = new Set(['failed', 'error', 'halted', 'cancelled', 'canceled', 'timed_out', 'timeout']);
41
+ const FAILED = new Set(['failed', 'error', 'halted', 'cancelled', 'canceled', 'timed_out', 'timeout', 'expired', 'rejected']);
42
+ /** The word for how an ended row ended (state stays 'failed' for the ended-badly group; this says which). */
43
+ export function outcomeOf(status) {
44
+ const st = String(status || '').toLowerCase();
45
+ if (st === 'cancelled' || st === 'canceled') return 'cancelled';
46
+ if (st === 'expired') return 'expired';
47
+ if (st === 'rejected') return 'rejected';
48
+ return 'failed';
49
+ }
50
+ /** Word, glyph and whether it is an error, for a row. Non-failed states return their own state. */
51
+ export function endWord(r) { return r.state === 'failed' ? outcomeOf(r.status) : r.state; }
52
+ export const glyphOf = (r) => (r.state === 'failed' ? OUTCOME_GLYPH[endWord(r)] : GLYPH[r.state]);
40
53
 
41
54
  /** How long a finished row lingers before it leaves the roster (null while running/waiting). */
42
55
  const expiryFor = (state, doneMs) => state === 'done' ? doneMs : state === 'failed' ? FRESH_MS : null;
@@ -59,6 +72,8 @@ function stateOf(status, { startedAt, createdAt, finishedAt, now, doneMs = FRESH
59
72
  return null;
60
73
  }
61
74
 
75
+ const latest = (...ts) => { const v = ts.filter((t) => t != null); return v.length ? Math.max(...v) : null; };
76
+
62
77
  export function fromObjective(o, now = Date.now()) {
63
78
  const startedAt = parseTime(o.started_at);
64
79
  const createdAt = parseTime(o.created_at);
@@ -78,6 +93,7 @@ export function fromObjective(o, now = Date.now()) {
78
93
  name: o.title || o.id, activity, status: o.status,
79
94
  startedAt: from, finishedAt, ageMs: from != null ? (state === 'done' || state === 'failed' ? (finishedAt ?? now) - from : now - from) : null,
80
95
  expiryMs: expiryFor(state, USER_DONE_MS),
96
+ progressAt: latest(parseTime(o.updated_at), parseTime(o.last_progress_at), parseTime(o.last_heartbeat_at)),
81
97
  detail: { description: o.description, error: o.error_message, tokens: o.tokens_used, toolCalls: o.tool_calls_used }
82
98
  };
83
99
  }
@@ -95,7 +111,8 @@ const BACKGROUND_BY = /^(schedule|cron|interval|webhook|event|system)\b/i;
95
111
  export function isBackgroundExecution(e, userId = null) {
96
112
  const by = e?.triggeredBy == null ? '' : String(e.triggeredBy);
97
113
  if (BACKGROUND_BY.test(by)) return true;
98
- if (userId && by && by === String(userId)) return false;
114
+ // quoxflow records a person-started run as user:<id> (RunActor); a bare id is an older row
115
+ if (userId && by && (by === String(userId) || by === `user:${userId}`)) return false;
99
116
  return !USER_TRIGGERS.has(String(e?.triggerData?.trigger || '').toLowerCase());
100
117
  }
101
118
 
@@ -114,6 +131,7 @@ export function fromExecution(e, now = Date.now(), { userId = null } = {}) {
114
131
  name: e.workflowName || e.workflowId || e.id, activity, status: e.status,
115
132
  startedAt, finishedAt, ageMs: startedAt != null ? (state === 'done' || state === 'failed' ? (finishedAt ?? now) - startedAt : now - startedAt) : null,
116
133
  expiryMs: expiryFor(state, background ? FRESH_MS : USER_DONE_MS),
134
+ progressAt: latest(parseTime(e.updatedAt), parseTime(e.lastHeartbeatAt)),
117
135
  detail: { workflowId: e.workflowId, correlationId: e.correlationId, trigger: e.triggerData?.trigger, error: e.error }
118
136
  };
119
137
  }
@@ -169,22 +187,83 @@ export function toneName(r, pos, now = Date.now()) {
169
187
 
170
188
  /** Live age text: a clock for running/waiting rows, recency ("done 3s ago") for finished ones. */
171
189
  export function ageText(r, now = Date.now()) {
172
- if (r.state === 'done' || r.state === 'failed') return r.finishedAt != null ? `${r.state === 'done' ? 'done' : 'failed'} ${elapsed(now - r.finishedAt)} ago` : '';
190
+ if (r.state === 'done' || r.state === 'failed') return r.finishedAt != null ? `${endWord(r)} ${elapsed(now - r.finishedAt)} ago` : '';
173
191
  return r.startedAt != null ? elapsed(now - r.startedAt) : '';
174
192
  }
175
193
 
194
+ /* ---- pulse: is it moving? ---------------------------------------------------------------- */
195
+
196
+ export const PULSE_MOVING_MS = 2 * 60 * 1000; // progress this recently = moving
197
+ export const PULSE_STALL_MS = 15 * 60 * 1000; // running with no progress this long = stalled
198
+ export const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
199
+ export const SPIN_MS = 120;
200
+
201
+ const sigOf = (r) => `${r.state}|${r.status}|${r.activity}`;
202
+
203
+ /**
204
+ * Remember what each row looked like at the last poll so "state changed / iteration advanced" is noticed.
205
+ * Sets r.progressAt (newest of the API's own timestamp and the change we saw; null = nothing seen yet) and
206
+ * r.watchSince (when we first saw the row). Mutates rows and memo (key -> { sig, watchSince, changedAt }).
207
+ */
208
+ export function applyProgress(rows, memo, now = Date.now()) {
209
+ const keep = new Set();
210
+ for (const r of rows) {
211
+ keep.add(r.key);
212
+ const sig = sigOf(r);
213
+ let m = memo.get(r.key);
214
+ if (!m) { m = { sig, watchSince: now, changedAt: null }; memo.set(r.key, m); }
215
+ else if (m.sig !== sig) { m.sig = sig; m.changedAt = now; }
216
+ r.progressAt = latest(r.progressAt, m.changedAt);
217
+ r.watchSince = m.watchSince;
218
+ }
219
+ for (const k of [...memo.keys()]) if (!keep.has(k)) memo.delete(k);
220
+ return rows;
221
+ }
222
+
223
+ /** -> { kind: 'moving' | 'idle' | 'stalled' | 'none', glyph, role }. motion=false gives static glyphs. */
224
+ export function pulseOf(r, now = Date.now(), { motion = false } = {}) {
225
+ if (r.kind === 'approval' || r.state === 'done' || r.state === 'failed') return { kind: 'none', glyph: ' ', role: 'dim' };
226
+ if (r.state !== 'running') return { kind: 'idle', glyph: '·', role: 'dim' };
227
+ const last = latest(r.progressAt, r.startedAt != null && now - r.startedAt < PULSE_MOVING_MS ? r.startedAt : null);
228
+ if (last != null && now - last < PULSE_MOVING_MS) return { kind: 'moving', glyph: motion ? SPIN[Math.floor(now / SPIN_MS) % SPIN.length] : '●', role: 'sky' };
229
+ const ref = last ?? r.watchSince;
230
+ if (ref != null && now - ref > PULSE_STALL_MS) return { kind: 'stalled', glyph: '!', role: 'warn' };
231
+ return { kind: 'idle', glyph: '·', role: 'dim' };
232
+ }
233
+
176
234
  export function countsLine(rows) {
177
235
  const n = (s) => rows.filter(r => r.state === s).length;
178
- return [['running', 'running'], ['waiting', 'waiting'], ['failed', 'failed'], ['done', 'finished']]
179
- .filter(([s]) => n(s)).map(([s, w]) => `${GLYPH[s]} ${n(s)} ${w}`).join(' · ');
236
+ const o = (w) => rows.filter(r => r.state === 'failed' && endWord(r) === w).length;
237
+ return [['running', 'running', n('running'), GLYPH.running], ['waiting', 'waiting', n('waiting'), GLYPH.waiting],
238
+ ['failed', 'failed', o('failed'), OUTCOME_GLYPH.failed], ['cancelled', 'cancelled', o('cancelled'), OUTCOME_GLYPH.cancelled],
239
+ ['expired', 'expired', o('expired'), OUTCOME_GLYPH.expired], ['rejected', 'rejected', o('rejected'), OUTCOME_GLYPH.rejected], ['done', 'finished', n('done'), GLYPH.done]]
240
+ .filter(([, , k]) => k).map(([, w, k, g]) => `${g} ${k} ${w}`).join(' · ');
180
241
  }
181
242
 
182
243
  /**
183
244
  * View model. mode: 'hidden' (off or nothing running) | 'summary' (narrow or short terminal) | 'full'.
184
245
  * lines: [{ state, text, selected }] where text is already clipped to cols.
185
246
  */
186
- export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = true, sel = 0, focus = false, now = Date.now() }) {
247
+ /**
248
+ * Two or more waiting approvals are ONE row ("⏸ 4 approvals waiting · oldest 41m · newest: <title>… · /approvals"), not
249
+ * one row each plus a summary line (owner, live 05:29): the bottom area must never grow with the inbox. A single approval
250
+ * keeps its own row. Focus (the user is walking the roster) shows the real rows so selection stays truthful.
251
+ */
252
+ export function collapseApprovals(rows, now = Date.now()) {
253
+ const aps = rows.filter(r => r.kind === 'approval' && r.state === 'waiting');
254
+ if (aps.length < 2) return rows;
255
+ const started = aps.map(r => r.startedAt).filter(x => x != null);
256
+ const newest = aps.reduce((a, b) => ((b.startedAt ?? -Infinity) > (a.startedAt ?? -Infinity) ? b : a));
257
+ const one = { key: 'approval:*', id: '*', source: 'approvals', kind: 'approval', state: 'waiting', background: false, collapsed: true, count: aps.length,
258
+ name: `${aps.length} approvals waiting`, activity: '/approvals', status: 'pending', startedAt: started.length ? Math.min(...started) : null, finishedAt: null,
259
+ oldestMs: started.length ? now - Math.min(...started) : null, newestTitle: String(newest.detail?.description || newest.name || ''), detail: {} };
260
+ let placed = false;
261
+ return rows.flatMap(r => (aps.includes(r) ? (placed ? [] : (placed = true, [one])) : [r]));
262
+ }
263
+
264
+ export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = true, sel = 0, focus = false, now = Date.now(), motion = false }) {
187
265
  if (!on || !rows.length) return { mode: 'hidden', lines: [], more: 0 };
266
+ if (!focus) rows = collapseApprovals(rows, now);
188
267
  if (cols < MIN_FULL_COLS || termRows < MIN_FULL_ROWS) {
189
268
  return { mode: 'summary', lines: [], more: 0, summary: clip(' ' + countsLine(rows) + ' · /roster', cols - 1) };
190
269
  }
@@ -197,12 +276,19 @@ export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = tru
197
276
  const tone = toneName(r, pos, now); if (r.kind !== 'approval') pos++;
198
277
  const actW = Math.min(18, Math.max(0, ...shown.map(x => [...(x.activity || '')].length)));
199
278
  const act = clip(r.activity || '', actW).padEnd(actW);
200
- const fixed = 1 + 1 + 1 + kindW + 1 + 1 + actW + 1 + ageW; // marker glyph kind name act elapsed
279
+ const fixed = 1 + 1 + 1 + kindW + 1 + 1 + actW + 1 + ageW + 2; // pulse column (space + glyph); marker glyph kind name act elapsed
201
280
  const nameW = Math.max(8, cols - 2 - fixed - 1);
202
281
  const name = clip(r.name, nameW).padEnd(nameW);
203
282
  const marker = focus && i === sel ? '›' : ' ';
283
+ if (r.collapsed) { // one summary row: count, oldest age, newest title, where to go; clipped to the line
284
+ const head = `${r.count} approvals waiting · oldest ${elapsed(r.oldestMs)} · newest: `, tail = ' · /approvals';
285
+ const room = cols - 5 - [...head].length - [...tail].length;
286
+ const text = room >= 12 ? `${head}${clip(r.newestTitle, room)}${tail}` : clip(`${r.count} approvals waiting · oldest ${elapsed(r.oldestMs)}${tail}`, cols - 4); // too narrow for a title
287
+ return { key: r.key, kind: 'approval', state: r.state, tone: toneName(r, pos, now), selected: false, marker: ' ', glyph: GLYPH[r.state], rest: ' ' + text, pulse: '', pulseRole: 'dim', pulseKind: 'none', text: ` ${GLYPH[r.state]} ${text}` };
288
+ }
204
289
  const rest = `${r.kind.padEnd(kindW)} ${name} ${act} ${el}`;
205
- return { key: r.key, state: r.state, tone, selected: focus && i === sel, marker, glyph: GLYPH[r.state], rest: ' ' + rest, text: `${marker}${GLYPH[r.state]} ${rest}` };
290
+ const p = pulseOf(r, now, { motion });
291
+ return { key: r.key, kind: r.kind, state: r.state, tone, selected: focus && i === sel, marker, glyph: glyphOf(r), glyphRole: r.state === 'failed' && endWord(r) !== 'failed' ? 'dim' : null, rest: ' ' + rest, pulse: ' ' + p.glyph, pulseRole: p.role, pulseKind: p.kind, text: `${marker}${glyphOf(r)} ${rest} ${p.glyph}` };
206
292
  });
207
293
  return { mode: 'full', lines, more: Math.max(0, rows.length - shown.length) };
208
294
  }
@@ -210,7 +296,7 @@ export function rosterView({ rows, cols, termRows = 40, max = MAX_ROWS, on = tru
210
296
  export function peekLines(row, now = Date.now()) {
211
297
  if (!row) return [];
212
298
  const age = ageText(row, now);
213
- const out = [`${row.kind} ${row.id}`, `status ${row.status} · ${row.state}${age ? ` · ${age}` : ''}`, `name ${row.name}`];
299
+ const out = [`${row.kind} ${row.id}`, `status ${row.status} · ${endWord(row)}${age ? ` · ${age}` : ''}`, `name ${row.name}`];
214
300
  if (row.activity) out.push(`activity ${row.activity}`);
215
301
  const d = row.detail || {};
216
302
  if (d.description) out.push(`goal ${d.description}`);
@@ -5,9 +5,9 @@
5
5
  */
6
6
  const VALUE_FLAGS = new Set(['-p', '--profile', '-n', '--name']);
7
7
  const LEVELS = new Set(['off', 'on', 'full']); // `--verbose` alone means on; it only takes a value that is a level
8
- const PRINT_VALUE_FLAGS = new Set(['--output', '--conversation', '--turn-id', '--timeout', '--provider', '--model']);
9
- const BOOL_FLAGS = new Set(['--demo', '--demo-onboarding', '--no-clear', '--color', '--service-key']);
10
- const ALL_FLAGS = new Set([...VALUE_FLAGS, ...PRINT_VALUE_FLAGS, ...BOOL_FLAGS, '--print', '--resume', '-h', '--help', '--']);
8
+ const PRINT_VALUE_FLAGS = new Set(['--output', '--conversation', '--turn-id', '--timeout', '--provider', '--model', '--agent', '--prompt-profile']);
9
+ const BOOL_FLAGS = new Set(['--demo', '--autoplay', '--demo-onboarding', '--no-clear', '--color', '--service-key']);
10
+ const ALL_FLAGS = new Set([...VALUE_FLAGS, ...PRINT_VALUE_FLAGS, ...BOOL_FLAGS, '--print', '--resume', '--autoplay-exit', '-h', '--help', '--']);
11
11
 
12
12
  export function routesToQuoxon(argv, isTTY) {
13
13
  let print = false;
@@ -18,17 +18,18 @@ export function routesToQuoxon(argv, isTTY) {
18
18
  for (let i = 0; i < argv.length; i++) {
19
19
  const a = argv[i];
20
20
  if (a === '--') { return print; } // everything after -- is the prompt
21
- if (a === '--print') { print = true; const n = argv[i + 1]; if (n !== undefined && !ALL_FLAGS.has(n) && !/^--(profile|verbose|output|conversation|turn-id|timeout|print|provider|model)=/.test(n)) i++; }
21
+ if (a === '--print') { print = true; const n = argv[i + 1]; if (n !== undefined && !ALL_FLAGS.has(n) && !/^--(profile|verbose|output|conversation|turn-id|timeout|print|provider|model|agent|prompt-profile)=/.test(n)) i++; }
22
22
  else if (a.startsWith('--print=')) print = true;
23
23
  else if (a === '-h' || a === '--help') help = true;
24
24
  else if (a === '--verbose') { if (LEVELS.has(argv[i + 1])) i++; }
25
25
  else if (a === '--resume') { const n = argv[i + 1]; if (n !== undefined && !n.startsWith('-')) i++; } // optional id or name
26
+ else if (a === '--autoplay-exit') { if (/^\d+(\.\d+)?$/.test(argv[i + 1] || '')) i++; } // optional seconds
26
27
  else if (a.startsWith('--resume=')) { /* inline value */ }
27
28
  else if (VALUE_FLAGS.has(a)) i++;
28
29
  else if (/^--(profile|verbose|name)=/.test(a)) { /* inline value */ }
29
30
  else if (BOOL_FLAGS.has(a)) { /* no value */ }
30
31
  else if (PRINT_VALUE_FLAGS.has(a)) i++;
31
- else if (/^--(output|conversation|turn-id|timeout|provider|model)=/.test(a)) { /* inline value */ }
32
+ else if (/^--(output|conversation|turn-id|timeout|provider|model|agent|prompt-profile)=/.test(a)) { /* inline value */ }
32
33
  else if (hasPrint && !a.startsWith('-')) { print = true; } // the prompt, wherever it sits among the flags
33
34
  else return false; // subcommand, positional, --help, --version, unknown flag
34
35
  }