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
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import React from 'react';
12
12
  import { Box, Static, Text, useStdin, useStdout } from 'ink';
13
- import { HistoryItem, Row, introAnimates, clampCols } from '../render.js';
13
+ import { HistoryItem, Row, introAnimates, clampCols, AskCard } from '../render.js';
14
14
  import { c, clip } from '../theme.js';
15
15
  import { parseChunk, initialKeyState, PASTE_ON, PASTE_OFF } from '../keys.js';
16
16
  import { packageVersion } from '../welcome.js';
@@ -18,10 +18,15 @@ import * as T from './copy.js';
18
18
  import { matchIntent, yesNo, parseConnect } from './intent.js';
19
19
  import { loadJourney, saveJourney, clearJourney, resumeStep } from './journey.js';
20
20
  import { pendingRows, runPreflight, preflightPassed, detectOllama, defaultDeps } from './preflight.js';
21
- import { credentialOptions, KEY_PROVIDERS, normaliseOllamaUrl } from './credential.js';
21
+ import { credentialOptions, KEY_PROVIDERS, normaliseOllamaUrl, nextSelectable } from './credential.js';
22
+ import { needsConsent, providerLabel } from '../consent.js';
22
23
  import { HelloIntro, helloIntroMs } from '../qmotion.js';
23
24
  import { launchEgg } from '../egg.js';
24
25
  import { passwordProblem, emailOk, generatePassword } from './finish.js';
26
+ import { dockerOffer } from './dockerOffer.js';
27
+
28
+ // FIRSTRUN: a number pick means its example ("1" is "watch my servers"); the request is replayed as the first real turn.
29
+ const requestText = (text) => { const n = /^\s*([1-9])\s*$/.exec(String(text ?? '')); return n && T.EXAMPLES[Number(n[1]) - 1] ? T.EXAMPLES[Number(n[1]) - 1] : text; };
25
30
 
26
31
  const h = React.createElement;
27
32
  const GLYPH = { ok: '✔', fail: '✖', warn: '⚠', running: '◐', pending: '·' };
@@ -55,6 +60,7 @@ function Item({ item, cols }) {
55
60
  case 'header': return h(Box, { flexDirection: 'column', marginTop: 1 },
56
61
  h(Text, null, h(Text, { color: c('violet'), bold: true }, T.LABEL), h(Text, { color: c('dim') }, ' ' + '─'.repeat(Math.max(1, cols - T.LABEL.length - 1)))),
57
62
  h(Text, { color: c('dim') }, clip(T.LABEL_SUB, cols)));
63
+ case 'ask': return h(AskCard, { text: item.text, cols }); // QUOXON-ASK: the same card as an approval
58
64
  case 'guide': return h(Box, { marginTop: item.gap === false ? 0 : 1, paddingLeft: 2 }, h(Para, { width: cols - 2, color: c(item.tone || 'text'), bold: Boolean(item.bold), text: item.text }));
59
65
  case 'dim': return h(Box, { paddingLeft: 2 }, h(Para, { width: cols - 2, color: c(item.tone || 'dim'), text: item.text }));
60
66
  case 'cmd': return h(Box, { paddingLeft: 4 }, h(Para, { width: cols - 4, color: c('sky'), text: item.text }));
@@ -95,7 +101,7 @@ const Hint = ({ text, cols }) => h(Box, { paddingLeft: 2 }, h(Text, { color: c('
95
101
 
96
102
  // ---- the view ---------------------------------------------------------------------------------
97
103
  export function GettingStarted({
98
- columns, version = '0.2.0', cwd = '', welcome = false, animateIntro, earlyInput = null,
104
+ columns, version = '0.3.1', cwd = '', welcome = false, animateIntro, earlyInput = null,
99
105
  journeyPath, dryRun = false, runner = null, preflightDeps, runChecks = runPreflight, findOllama = detectOllama,
100
106
  requestCredential, broker = null, finisher = null, onExit = () => {}, now = () => Date.now()
101
107
  }) {
@@ -114,7 +120,7 @@ export function GettingStarted({
114
120
  const items = [introItem, ...(welcome ? [{ id: 'welcome', kind: 'welcome', version: packageVersion() }] : []), { id: 'hdr', kind: 'header' }];
115
121
  if (resumeAt) {
116
122
  items.push({ id: 'resume', kind: 'guide', text: T.RESUME });
117
- if (saved.request) items.push({ id: 'resumeReq', kind: 'dim', text: T.resumeRequest(saved.request) });
123
+ if (saved.request) items.push({ id: 'resumeReq', kind: 'dim', text: T.resumeRequest(requestText(saved.request)) });
118
124
  } else {
119
125
  items.push({ id: 'greet', kind: 'guide', text: T.GREETING_REST }, { id: 'q', kind: 'guide', text: T.QUESTION }, { id: 'ex', kind: 'list', items: T.EXAMPLES, numbered: true });
120
126
  }
@@ -122,7 +128,7 @@ export function GettingStarted({
122
128
  };
123
129
 
124
130
  const S = React.useRef({
125
- history: [], step: 'ask', input: '', intent: saved?.intent || null, request: saved?.request || null, provider: null,
131
+ history: [], step: 'ask', input: '', intent: saved?.intent || null, request: saved?.request ? requestText(saved.request) : null, provider: saved?.provider === 'skip' ? 'skip' : null,
126
132
  rows: [], fixNote: null, options: [], sel: 1, flash: null, install: null, logs: [], showLog: false, done: false, secret: null,
127
133
  installStage: saved?.installStage || null, installDir: saved?.installDir || null, dashboardUrl: saved?.dashboardUrl || null,
128
134
  ownerEmail: saved?.ownerEmail || null, finRows: [], stopping: false, vault: null
@@ -139,7 +145,7 @@ export function GettingStarted({
139
145
 
140
146
  // ---- intro (Orbit & Lock), then the opening lines move into <Static> --------------------------
141
147
  const animating = animateIntro ?? introAnimates(stdout?.isTTY);
142
- // Orbit & Lock, then the Q says hello (T.HELLO decodes in beside it); rarely the Dragon's Eye after that.
148
+ // Orbit & Lock, then the Q says hello (T.HELLO decodes in beside it); rarely the dragon's googly-eye greeting after that.
143
149
  const eye = React.useMemo(() => (animating && animateIntro === undefined ? launchEgg({ tty: Boolean(stdout?.isTTY) }) : false), []);
144
150
  const introMs = helloIntroMs({ eye, text: T.HELLO });
145
151
  const introStart = React.useRef(Date.now());
@@ -173,7 +179,7 @@ export function GettingStarted({
173
179
 
174
180
  function offerLines() {
175
181
  const items = [{ kind: 'guide', text: T.OFFER }];
176
- items.push({ kind: 'guide', text: T.OFFER_ASK, bold: true, gap: false }, { kind: 'dim', text: T.ALREADY });
182
+ items.push({ kind: 'ask', text: T.OFFER_ASK }, { kind: 'dim', text: T.ALREADY });
177
183
  push(...items);
178
184
  set({ step: 'offer' });
179
185
  persist();
@@ -184,7 +190,7 @@ export function GettingStarted({
184
190
  if (m.intent === 'empty') return;
185
191
  push({ kind: 'user', text });
186
192
  if (m.intent === 'connect') { push({ kind: 'guide', text: T.CONNECT_INTENT_LINE }); set({ step: 'ask' }); return; }
187
- set({ intent: m.intent, request: text });
193
+ set({ intent: m.intent, request: requestText(text) });
188
194
  push({ kind: 'guide', text: m.intent === 'offscript' ? T.DEFERRAL : T.INTENT_LINES[m.intent] });
189
195
  offerLines();
190
196
  }
@@ -197,7 +203,7 @@ export function GettingStarted({
197
203
  if (conn) {
198
204
  push({ kind: 'user', text: t });
199
205
  if (!conn.ok) { push({ kind: 'dim', tone: 'warn', text: conn.reason === 'url' ? T.connectBadUrl(conn.value) : T.CONNECT_USAGE }); return true; }
200
- push({ kind: 'guide', text: T.connectHandoff(conn.url) });
206
+ push({ kind: 'guide', text: T.connectChecking(conn.url) });
201
207
  exit({ action: 'connect', url: conn.url });
202
208
  return true;
203
209
  }
@@ -233,6 +239,11 @@ export function GettingStarted({
233
239
 
234
240
  async function toCredential() {
235
241
  set({ rows: [] });
242
+ if (S.current.provider === 'skip') { // Z2H-R2B: the owner already chose "Skip for now"; never re-ask on resume (/connect stays open)
243
+ push({ kind: 'guide', tone: 'warn', text: T.CRED_SKIPPED });
244
+ startInstall();
245
+ return;
246
+ }
236
247
  const ollama = await findOllama();
237
248
  if (!live.current || S.current.done) return;
238
249
  const options = credentialOptions({ ollama });
@@ -250,8 +261,10 @@ export function GettingStarted({
250
261
  const held = await requestCredential(opt.id); // THE hook: K1 secure card once it merges
251
262
  if (!live.current || S.current.done) { held?.drop?.(); return; }
252
263
  if (!held) { set({ step: 'credential', input: '' }); return; }
253
- push({ kind: 'user', text: opt.label }, { kind: 'guide', tone: 'ok', text: T.credHeld(opt.label, held.fingerprint) });
264
+ push({ kind: 'user', text: opt.label }, { kind: 'guide', tone: 'ok', text: T.credHeld(opt.label, held.fingerprint) }, ...(needsConsent(opt.id) ? [{ kind: 'guide', text: T.credConsent(opt.label) }] : []));
254
265
  set({ secret: held, input: '' });
266
+ if (needsConsent(opt.id)) { set({ step: 'cloudConsent', cloudConsent: null }); return; } // ask once; the answer is the user's own
267
+
255
268
  } else if (opt.id === 'ollama' && !opt.detected) {
256
269
  set({ step: 'ollamaUrl', input: '' }); // B3: nothing answered locally; ask for the LAN URL
257
270
  return;
@@ -261,6 +274,13 @@ export function GettingStarted({
261
274
  startInstall();
262
275
  }
263
276
 
277
+ function answerCloudConsent(yes) {
278
+ const p = S.current.provider; const who = providerLabel(p);
279
+ push({ kind: 'user', text: yes ? 'yes' : 'no' }, { kind: 'guide', tone: yes ? 'ok' : 'warn', text: yes ? T.credConsentYes(who) : T.credConsentNo(who, p) });
280
+ set({ cloudConsent: yes });
281
+ startInstall();
282
+ }
283
+
264
284
  function submitOllamaUrl() {
265
285
  const url = normaliseOllamaUrl(S.current.input);
266
286
  if (!url) { set({ flash: T.OLLAMA_URL_BAD }); return; }
@@ -300,6 +320,7 @@ export function GettingStarted({
300
320
  if (!live.current) return;
301
321
  if (ev.type === 'log') set(s => ({ logs: [...s.logs, ev.line].slice(-8) }));
302
322
  if (ev.type === 'script') push({ kind: 'long', tone: 'dim', gap: false, text: T.installScript(ev.url, ev.sha256, ev.bytes), lead: T.installScriptLead(ev.url, ev.bytes), value: ev.sha256 });
323
+ if (ev.type === 'stage-label') set(s => ({ install: { ...s.install, stages: s.install.stages.map(x => (x.id === ev.id ? { ...x, body: ev.label } : x)) } }));
303
324
  if (ev.type === 'stage') set(s => ({ install: { ...s.install, stages: s.install.stages.map(x => (x.id === ev.id ? { ...x, status: ev.status, startedAt: ev.status === 'running' ? ev.at : x.startedAt, ms: ev.ms ?? x.ms, earlier: Boolean(ev.earlier) } : x)) } }));
304
325
  }
305
326
  });
@@ -308,9 +329,10 @@ export function GettingStarted({
308
329
  if (res.interrupted || S.current.stopping) {
309
330
  push({ kind: 'dim', text: T.INSTALL_STOPPED }); persist({ step: 'install' }); exit({ action: 'exit' }); return;
310
331
  }
332
+ if (res.logPath) set({ installLogPath: res.logPath });
311
333
  if (res.dashboardUrl) set({ dashboardUrl: res.dashboardUrl });
312
334
  const rows = S.current.install.stages.map(st => ({ ...st, tail: st.earlier ? T.INSTALL_EARLIER : st.ms != null ? fmtS(st.ms) : '' }));
313
- if (!res.ok) { set(s => ({ step: 'installFail', install: { ...s.install, failed: res.failedStage || '?', degraded: Boolean(res.degraded), error: res.error || null } })); persist({ step: 'install' }); return; }
335
+ if (!res.ok) { set(s => ({ step: 'installFail', install: { ...s.install, failed: res.failedStage || '?', degraded: Boolean(res.degraded), error: res.error || null, logPath: res.logPath || null, cause: res.cause?.message || null } })); persist({ step: 'install' }); return; }
314
336
  push({ kind: 'guide', text: T.INSTALL_TITLE + (dry ? ` (${T.INSTALL_DRY})` : ''), bold: true }, { kind: 'rows', rows });
315
337
  if (dry || !finisher) { finale({ dashboardUrl: res.dashboardUrl, dry }); return; }
316
338
  toOwner();
@@ -351,6 +373,7 @@ export function GettingStarted({
351
373
  { id: 'account', name: F.account.name, status: 'running', body: F.account.running },
352
374
  { id: 'signin', name: F.signin.name, status: 'pending', body: F.signin.running },
353
375
  { id: 'vault', name: F.vault.name, status: 'pending', body: S.current.secret ? F.vault.running(opt?.label || 'key') : F.vault.skipped },
376
+ ...(S.current.cloudConsent === true ? [{ id: 'consent', name: F.consent.name, status: 'pending', body: F.consent.running(providerLabel(S.current.provider)) }] : []),
354
377
  { id: 'link', name: F.link.name, status: 'pending', body: F.link.running }
355
378
  ];
356
379
  const row = (id, patch) => set(s => ({ finRows: s.finRows.map(r => (r.id === id ? { ...r, ...patch } : r)) }));
@@ -385,15 +408,27 @@ export function GettingStarted({
385
408
  }
386
409
  set({ vault });
387
410
 
411
+ if (S.current.cloudConsent === true && finisher.grantConsent) { // the owner said yes at the credential step
412
+ row('consent', { status: 'running' });
413
+ const g = await finisher.grantConsent(S.current.provider);
414
+ if (!live.current || S.current.done) return;
415
+ const who = providerLabel(S.current.provider);
416
+ row('consent', g?.ok ? { status: 'ok', body: F.consent.ok(who) } : { status: 'warn', body: F.consent.fail(g?.error || 'unknown error', S.current.provider) });
417
+ }
418
+
388
419
  row('link', { status: 'running' });
389
420
  const link = await finisher.loginLink(S.current.dashboardUrl);
390
421
  if (!live.current || S.current.done) return;
391
422
  row('link', link.ok ? { status: 'ok', body: F.link.ok } : { status: 'warn', body: F.link.fail(link.error) });
392
423
 
424
+ // Z2H-R5 E: only mention adding a host where this install can serve its host installer (unknown = as before).
425
+ let hostsAvailable = true;
426
+ try { const hi = finisher.hostInstaller ? await finisher.hostInstaller() : null; if (hi && hi.ok === false && Number(hi.status) >= 400) hostsAvailable = false; } catch { /* unknown */ }
427
+ if (!live.current || S.current.done) return;
393
428
  push({ kind: 'guide', text: T.FINISH_TITLE, bold: true }, { kind: 'rows', rows: S.current.finRows });
394
429
  if (vault) push({ kind: 'dim', text: vault.ok ? T.KEY_GONE_AFTER : `${T.KEY_GONE_AFTER} ${T.keyNotStored(vault.provider || S.current.provider)}` });
395
430
  set({ finRows: [] });
396
- finale({ dashboardUrl: S.current.dashboardUrl, link: link.ok ? link.url : null, dry: false });
431
+ finale({ dashboardUrl: S.current.dashboardUrl, link: link.ok ? link.url : null, dry: false, hostsAvailable });
397
432
  }
398
433
 
399
434
  function replayText() {
@@ -402,7 +437,7 @@ export function GettingStarted({
402
437
  return request;
403
438
  }
404
439
 
405
- function finale({ dashboardUrl, link = null, dry }) {
440
+ function finale({ dashboardUrl, link = null, dry, hostsAvailable = true }) {
406
441
  const items = [];
407
442
  const intent = S.current.intent && T.TIPS[S.current.intent] ? S.current.intent : 'explore';
408
443
  if (dry) items.push({ kind: 'guide', tone: 'warn', text: T.FINALE_DRY_BANNER });
@@ -414,9 +449,11 @@ export function GettingStarted({
414
449
  else items.push({ kind: 'long', text: T.finaleDashboardPlain(url), lead: T.FINALE_DASHBOARD_LEAD_PLAIN, value: url });
415
450
  items.push({ kind: 'guide', gap: false, text: T.FINALE_PROOF });
416
451
  if (!dry) items.push({ kind: 'dim', text: T.FINALE_CERT });
452
+ if (!dry && S.current.installLogPath) items.push({ kind: 'dim', text: T.INSTALL_LOG_PATH(S.current.installLogPath) });
417
453
  items.push({ kind: 'guide', text: T.FINALE_TIPS_TITLE }, { kind: 'list', items: dry ? T.FINALE_TIPS : T.TIPS[intent] });
418
454
  const noAi = !dry && (S.current.provider === 'skip' || (S.current.vault && !S.current.vault.ok));
419
- if (!dry) items.push({ kind: 'guide', text: noAi ? T.k2NoAi(S.current.provider === 'anthropic' ? 'anthropic' : 'openai') : T.K2[intent] });
455
+ if (noAi && hostsAvailable) items.push({ kind: 'guide', text: T.FINALE_HOSTS_NO_AI });
456
+ if (!dry) items.push({ kind: 'guide', text: noAi ? T.k2NoAi(S.current.provider === 'anthropic' ? 'anthropic' : 'openai') : (!hostsAvailable && intent === 'servers' ? T.K2_SERVERS_NO_INSTALLER : T.K2[intent]) });
420
457
  push(...items);
421
458
  S.current.secret?.drop?.();
422
459
  set({ step: 'finale', secret: null, finaleDry: dry });
@@ -455,15 +492,23 @@ export function GettingStarted({
455
492
  toCredential();
456
493
  }
457
494
  else if (ev.type === 'text' && /^q$/i.test(ev.text)) { push({ kind: 'dim', text: T.BYE }); persist({ step: 'preflight' }); exit({ action: 'exit' }); }
495
+ else if (ev.type === 'text' && /^y$/i.test(ev.text) && !dryRun) { // FIRSTRUN: the guide fixes Docker itself
496
+ const offer = dockerOffer(s.rows);
497
+ if (offer) { push({ kind: 'ask', text: offer.ask }, { kind: 'user', text: 'yes' }); persist({ step: 'preflight' }); exit({ action: 'docker', kind: offer.kind }); }
498
+ }
458
499
  return;
459
500
  case 'credential': {
460
501
  const n = s.options.length;
461
- if (ev.type === 'key' && ev.name === 'up') set({ sel: (s.sel - 1 + n) % n, flash: null });
462
- else if (ev.type === 'key' && (ev.name === 'down' || ev.name === 'tab')) set({ sel: (s.sel + 1) % n, flash: null });
502
+ if (ev.type === 'key' && ev.name === 'up') set({ sel: nextSelectable(s.options, s.sel, -1), flash: null }); // #1288: never land on a disabled row
503
+ else if (ev.type === 'key' && (ev.name === 'down' || ev.name === 'tab')) set({ sel: nextSelectable(s.options, s.sel, 1), flash: null });
463
504
  else if (ev.type === 'key' && ev.name === 'enter') choose(s.options[s.sel]);
464
505
  else if (ev.type === 'text' && /^[1-9]$/.test(ev.text) && Number(ev.text) <= n) { set({ sel: Number(ev.text) - 1 }); choose(s.options[Number(ev.text) - 1]); }
465
506
  return;
466
507
  }
508
+ case 'cloudConsent':
509
+ if (ev.type === 'text' && /^y(es)?$/i.test(ev.text.trim())) answerCloudConsent(true);
510
+ else if ((ev.type === 'text' && /^n(o)?$/i.test(ev.text.trim())) || (ev.type === 'key' && (ev.name === 'enter' || ev.name === 'escape'))) answerCloudConsent(false);
511
+ return;
467
512
  case 'install': if (ev.type === 'text' && /^v$/i.test(ev.text)) set({ showLog: !s.showLog }); return;
468
513
  case 'installStop':
469
514
  if (ev.type === 'text' && /^y/i.test(ev.text)) stopInstall();
@@ -484,7 +529,7 @@ export function GettingStarted({
484
529
  }
485
530
  };
486
531
  function stopInstall() {
487
- push({ kind: 'dim', text: T.INSTALL_STOP_ASK }, { kind: 'user', text: 'yes' });
532
+ push({ kind: 'ask', text: T.INSTALL_STOP_ASK }, { kind: 'user', text: 'yes' });
488
533
  set({ stopping: true, step: 'install' });
489
534
  S.current.abort?.abort?.(); // the runner stops the installer's process group; startInstall then exits
490
535
  }
@@ -549,7 +594,11 @@ export function GettingStarted({
549
594
  case 'preflight': case 'preflightFail':
550
595
  liveParts.push(h(Box, { key: 'pf', marginTop: 1, paddingLeft: 2 }, h(Text, { bold: true, color: c('text') }, T.PREFLIGHT_TITLE)),
551
596
  ...s.rows.map((r) => h(CheckRow, { key: r.id, r, cols })));
552
- if (s.step === 'preflightFail') liveParts.push(h(Box, { key: 'pfn', marginTop: 1, paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.fixNote })), h(Hint, { key: 'h', text: dryRun ? T.PREFLIGHT_RETRY_DRY : T.PREFLIGHT_RETRY, cols }));
597
+ if (s.step === 'preflightFail') {
598
+ const offer = dryRun ? null : dockerOffer(s.rows); // FIRSTRUN: offer, never "go and do this"
599
+ if (offer) liveParts.push(h(Box, { key: 'pfo', marginTop: 1 }, h(AskCard, { text: offer.ask, cols })), h(Hint, { key: 'h', text: T.DOCKER_OFFER_HINT, cols }));
600
+ else liveParts.push(h(Box, { key: 'pfn', marginTop: 1, paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.fixNote })), h(Hint, { key: 'h', text: dryRun ? T.PREFLIGHT_RETRY_DRY : T.PREFLIGHT_RETRY, cols }));
601
+ }
553
602
  break;
554
603
  case 'credential':
555
604
  liveParts.push(h(Box, { key: 'opts', flexDirection: 'column', marginTop: 1 }, ...s.options.map((o, i) => {
@@ -573,6 +622,11 @@ export function GettingStarted({
573
622
  h(Text, { color: c('violet') }, '─'.repeat(cols))));
574
623
  break;
575
624
  }
625
+ case 'cloudConsent':
626
+ liveParts.push(h(Box, { key: 'cc', flexDirection: 'column', marginTop: 1 },
627
+ h(AskCard, { text: T.credConsentAsk(providerLabel(s.provider)), cols }),
628
+ h(Hint, { text: T.CRED_CONSENT_HINT, cols })));
629
+ break;
576
630
  case 'ollamaUrl':
577
631
  liveParts.push(h(Box, { key: 'card', flexDirection: 'column', marginTop: 1 },
578
632
  h(Text, { color: c('violet') }, '─'.repeat(cols)),
@@ -606,10 +660,13 @@ export function GettingStarted({
606
660
  if (s.step === 'installFail') {
607
661
  liveParts.push(h(Box, { key: 'if', paddingLeft: 2, width: cols, marginTop: 1, flexDirection: 'column' },
608
662
  h(Para, { width: cols - 2, color: c('fail'), text: T.INSTALL_FAILED(T.STAGES[s.install.failed] || s.install.failed) }),
663
+ s.install.cause ? h(Para, { width: cols - 2, color: c('warn'), text: s.install.cause }) : null, // FIRSTRUN: why, in one line
609
664
  s.install.error ? h(Para, { width: cols - 2, color: c('dim'), text: s.install.error }) : null,
665
+ s.install.logPath ? h(Para, { width: cols - 2, color: c('dim'), text: T.INSTALL_LOG_PATH(s.install.logPath) }) : null,
666
+ s.install.logPath ? h(Para, { width: cols - 2, color: c('dim'), text: T.INSTALL_RERUN_HINT }) : null,
610
667
  s.install.degraded ? h(Para, { width: cols - 2, color: c('warn'), text: T.INSTALL_DEGRADED }) : null));
611
668
  }
612
- if (s.step === 'installStop') liveParts.push(h(Box, { key: 'stop', paddingLeft: 2, width: cols, marginTop: 1 }, h(Para, { width: cols - 2, color: c('warn'), bold: true, text: T.INSTALL_STOP_ASK })));
669
+ if (s.step === 'installStop') liveParts.push(h(Box, { key: 'stop', width: cols }, h(AskCard, { text: T.INSTALL_STOP_ASK, cols })));
613
670
  liveParts.push(h(Hint, { key: 'h', text: s.step === 'installFail' ? T.INSTALL_FAIL_HINT : s.step === 'installStop' ? 'y to stop · n or Enter to carry on' : T.INSTALL_HINT, cols }));
614
671
  break;
615
672
  }
@@ -0,0 +1,10 @@
1
+ /** The org as the status line shows it: its name, or a short "org …H398" until the name arrives. Never a raw ULID. */
2
+ const ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
3
+ export const isOrgId = (s) => ULID.test(String(s || '').trim());
4
+ export function orgLabel(name, id = null) {
5
+ const n = name == null ? '' : String(name).trim();
6
+ if (n && !isOrgId(n)) return n;
7
+ const raw = isOrgId(n) ? n : (id ? String(id).trim() : '');
8
+ if (!raw) return null;
9
+ return isOrgId(raw) ? `org …${raw.slice(-4)}` : raw; // only a real ULID is masked
10
+ }
@@ -0,0 +1,33 @@
1
+ /** /org helpers (pure). Org list rows, fuzzy match, and the lines the command prints. QUOXON-FW3. */
2
+ const norm = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
3
+
4
+ export const orgRows = (list) => (Array.isArray(list) ? list : []).map((o) => ({ id: o.id, name: o.name || o.slug || o.id, slug: o.slug || null, role: o.role || null }));
5
+
6
+ /**
7
+ * Resolve a typed name or id against the user's orgs. Exact id, exact slug/name, then every word of the
8
+ * query found in the name or slug (so "hot tubs hampshire" finds "Hot Tubs Hampshire Ltd"). Never guesses between ties.
9
+ * -> { org } | { ambiguous: [orgs] } | { none: true }
10
+ */
11
+ export function matchOrg(query, list) {
12
+ const rows = orgRows(list);
13
+ const q = norm(query);
14
+ if (!q) return { none: true };
15
+ // An exact slug wins outright, so a slug never ties with another org's look-alike name (#1141).
16
+ const bySlug = rows.filter((o) => o.slug && o.slug === String(query).trim());
17
+ if (bySlug.length === 1) return { org: bySlug[0] };
18
+ const exact = rows.filter((o) => String(o.id).toLowerCase() === String(query).trim().toLowerCase() || norm(o.slug) === q || norm(o.name) === q);
19
+ if (exact.length === 1) return { org: exact[0] };
20
+ if (exact.length > 1) return { ambiguous: exact };
21
+ const words = q.split(' ');
22
+ const fuzzy = rows.filter((o) => { const hay = `${norm(o.name)} ${norm(o.slug)}`; return words.every((w) => hay.includes(w)); });
23
+ if (fuzzy.length === 1) return { org: fuzzy[0] };
24
+ if (fuzzy.length > 1) return { ambiguous: fuzzy };
25
+ return { none: true };
26
+ }
27
+
28
+ /** One line per org; the current one is marked. */
29
+ export function orgLines(list, currentId) {
30
+ const rows = orgRows(list);
31
+ const w = Math.min(36, Math.max(4, ...rows.map((o) => [...o.name].length)));
32
+ return rows.map((o) => `${o.id === currentId ? '* ' : ' '}${o.name.padEnd(w)} ${o.slug || o.id}${o.role ? ` (${o.role})` : ''}`);
33
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Your own broadcast is not news to you: the fan-out reaches every held slot in the org, including this window's, and
3
+ * the "BROADCAST sent" card already told the sender. Match by the sender's identity, never by text.
4
+ */
5
+ import { parseBroadcastText } from './slash/broadcast.js';
6
+
7
+ const norm = (s) => String(s || '').toLowerCase().replace(/\s*\(.*?\)\s*/g, ' ').replace(/\s+/g, ' ').trim();
8
+
9
+ /**
10
+ * @param {object} m the slot message
11
+ * @param {object} me { label: this window's sender label, display: the signed-in display name, userId }
12
+ */
13
+ export function isOwnBroadcast(m, me = {}) {
14
+ const bc = parseBroadcastText(m?.text);
15
+ if (!bc) return false;
16
+ const uid = me.userId ? String(me.userId) : null;
17
+ if (uid && [m.user_id, m.from_user_id, m.author_user_id, m.author_id].some((x) => x != null && String(x) === uid)) return true;
18
+ const sender = norm(bc.sender), from = norm(m.from);
19
+ const mine = [me.label, me.display].map(norm).filter(Boolean);
20
+ if (!mine.length) return false;
21
+ return mine.some((x) => x === sender || x === from) || (me.label && m.from === me.label) || false;
22
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The semantic palette (QUOXON-FW11). One role per MEANING, so a reader can see where prose stops and a thing
3
+ * they can type starts:
4
+ *
5
+ * prose default text. White on dark, ink on light. Never grey.
6
+ * command anything the user can type: /slash commands, `quox ...`, !bang, flags and args INCLUDED. The whole
7
+ * command is ONE span in ONE colour (never split into differently coloured parts).
8
+ * value ids, names, numbers, paths, URLs (theme role `id`; theme's `value` stays the near-white card text).
9
+ * ok / warn / error state. Carried by a glyph or label ("⚠"), never by tinting the sentence, so commands and
10
+ * values inside keep their own colours.
11
+ * brand Quox purple: chrome only (logo, status-bar labels, headers). Never a command inside prose.
12
+ * quiet secondary text and rules: a tinted, readable shade (never a mid-grey).
13
+ *
14
+ * Both tables: [truecolor hex, ansi256 index]. DARK >= 4.5:1 on #000, LIGHT >= 4.5:1 on #fff (tests).
15
+ */
16
+ export const SEMANTIC_DARK = {
17
+ prose: ['#f1f5f9', 255], command: ['#2dd4bf', 80], value: ['#93c5fd', 117],
18
+ ok: ['#4ade80', 78], warn: ['#f59e0b', 214], error: ['#f87171', 210],
19
+ brand: ['#a855f7', 135], quiet: ['#bcc8e8', 189], rule: ['#7c8fe0', 105]
20
+ };
21
+ export const SEMANTIC_LIGHT = {
22
+ prose: ['#0f172a', 234], command: ['#0f766e', 30], value: ['#1d4ed8', 26],
23
+ ok: ['#15803d', 28], warn: ['#b45309', 130], error: ['#b91c1c', 124],
24
+ brand: ['#7e22ce', 91], quiet: ['#3b4f87', 60], rule: ['#7280c8', 104]
25
+ };
26
+ export const SEMANTIC_ROLES = Object.keys(SEMANTIC_DARK);
27
+
28
+ /** Is this hex a mid-grey? Low saturation in the middle of the lightness range (white and ink are allowed). */
29
+ export function isGrey(hex) {
30
+ const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16) / 255);
31
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), l = (max + min) / 2, d = max - min;
32
+ const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
33
+ return s < 0.25 && l > 0.2 && l < 0.85;
34
+ }
35
+
36
+ // ---- inline markup used by every notice producer --------------------------------------------------
37
+ // ⟦text⟧ = a command (one span, markers dropped on screen). ⟪text⟫ = a value. Helpers build them. (Not backticks:
38
+ // plain backticks already appear literally in notices.)
39
+ export const cmd = (s) => `⟦${s}⟧`;
40
+ export const val = (s) => `⟪${s}⟫`;
41
+ export const MARK = /⟦([^⟧\n]+)⟧|⟪([^⟫\n]+)⟫/g;
42
+ /** The on-screen characters of marked-up text (markers dropped). Use for width and wrapping. */
43
+ export const stripMarks = (s) => String(s ?? '').replace(MARK, (_, a, b) => a ?? b);
@@ -7,6 +7,7 @@
7
7
  import { textGate } from './events.js';
8
8
  import { streamFields, fallbackNote } from './model.js';
9
9
  import { connectOfferFor } from './connect.js';
10
+ import { isConsentError, consentPrintMessage } from './consent.js';
10
11
 
11
12
  export const EXIT = { ok: 0, error: 1, approval: 2, interrupted: 3, auth: 4, unconfirmed: 5 };
12
13
 
@@ -28,7 +29,7 @@ const isAuth = (e) => e?.status === 401 || /valid jwt|service key|unauthor|not a
28
29
  const newTurnId = () => `QXN_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
29
30
 
30
31
  /** Runs one turn. out/err are write(string) sinks. Returns the exit code. */
31
- export async function runPrint({ api, prompt, output = 'text', conversation = null, turnId = newTurnId(), timeoutS = 0, color = false, stopGraceMs = 8000, context = undefined, choice = null, promptProfile = undefined, out, err }) {
32
+ export async function runPrint({ api, prompt, agent = 'quox', output = 'text', conversation = null, turnId = newTurnId(), timeoutS = 0, color = false, stopGraceMs = 8000, context = undefined, choice = null, promptProfile = undefined, profile = null, out, err }) {
32
33
  if (!prompt || !prompt.trim()) { err('quox: empty prompt (pass --print "<prompt>" or pipe it on stdin)\n'); return EXIT.error; }
33
34
  const ctrl = new AbortController();
34
35
  let abandoned = false, graceTimer = null;
@@ -45,10 +46,10 @@ export async function runPrint({ api, prompt, output = 'text', conversation = nu
45
46
  const sum = { tools: 0, approvals: 0, receipts: [] };
46
47
  let convo = conversation, doneSession = null, status = null, text = '', sawApproval = false, errorMsg = null, authFail = false;
47
48
  const seen = new Set();
48
- let offer = null;
49
+ let offer = null, handoff = null;
49
50
  let gate = null;
50
51
  try {
51
- for await (const ev of api.stream({ turnId, message: prompt, agent: 'quox', conversationId: conversation, context, ...streamFields(choice), ...(promptProfile ? { promptProfile } : {}) }, ctrl.signal)) {
52
+ for await (const ev of api.stream({ turnId, message: prompt, agent: agent || 'quox', conversationId: conversation, context, ...streamFields(choice), ...(promptProfile ? { promptProfile } : {}), ...(timeoutS > 0 ? { turnBudgetMs: timeoutS * 1000 } : {}) }, ctrl.signal)) {
52
53
  if (output === 'json') out(JSON.stringify(ev) + '\n');
53
54
  const fb = fallbackNote(choice, ev);
54
55
  if (fb) err(`quox: ${fb}\n`);
@@ -61,8 +62,9 @@ export async function runPrint({ api, prompt, output = 'text', conversation = nu
61
62
  else if (ev.type === 'tool.finished') { if (!seen.has(ev.call_id)) { seen.add(ev.call_id); sum.tools++; } if (ev.receipt?.ward_seq != null) sum.receipts.push(ev.receipt.ward_seq); }
62
63
  else if (ev.type === 'approval.requested') { sawApproval = true; sum.approvals++; }
63
64
  else if (ev.type === 'approval.resolved') sawApproval = false;
65
+ else if (ev.type === 'turn.handoff_suggested' && ev.handoff_suggested?.agent) handoff = ev.handoff_suggested; // QUOXON handoff offer
64
66
  else if (ev.type === 'turn.finished') status = ev.status || 'completed';
65
- else if (ev.type === 'error') errorMsg = ev.error || 'stream error';
67
+ else if (ev.type === 'error') errorMsg = isConsentError(ev) ? consentPrintMessage(ev, profile) : (ev.error || 'stream error');
66
68
  }
67
69
  } catch (e) {
68
70
  if (ctrl.signal.aborted) { status = 'abandoned'; errorMsg = `timed out after ${timeoutS}s; the server was not confirmed stopped`; }
@@ -83,12 +85,14 @@ export async function runPrint({ api, prompt, output = 'text', conversation = nu
83
85
  if (!status) status = authFail ? 'auth' : 'failed';
84
86
 
85
87
  if (output === 'json') {
86
- out(JSON.stringify({ type: 'summary', status, turn_id: turnId, conversation_id: convo, duration_ms: Date.now() - t0, server_stopped: serverStopped, ...sum }) + '\n');
88
+ out(JSON.stringify({ type: 'summary', status, turn_id: turnId, conversation_id: convo, duration_ms: Date.now() - t0, server_stopped: serverStopped, ...sum, ...(handoff ? { handoff_suggested: handoff } : {}) }) + '\n');
87
89
  } else if (text) out(text.endsWith('\n') ? text : text + '\n');
88
90
  if (authFail) err(`Not signed in for chat. Run: quox login -p <profile>\n`);
89
91
  else if (code === EXIT.approval) err('Held for approval: approve it in the dashboard or with `quox` /approvals.\n');
90
92
  else if (code === EXIT.unconfirmed) err('quox: Ended without confirmation from the server\n');
91
93
  else if (errorMsg) err(`quox: ${errorMsg}\n`);
92
94
  if (offer && code !== EXIT.ok) err(`${offer}\n`);
95
+ // QUOXON handoff offer: no y/N here, so name the exact one-turn command (same question, same conversation).
96
+ if (handoff && output !== 'json' && code === EXIT.ok) err(`quox: ${handoff.reason ? `${String(handoff.reason).replace(/\s+$/, '')} ` : ''}${handoff.name || handoff.agent} can answer it: quox --print --agent ${handoff.agent}${convo ? ` --conversation ${convo}` : ''} ${JSON.stringify(prompt)}\n`);
93
97
  return code;
94
98
  }
@@ -0,0 +1,53 @@
1
+ // PROFILE-SWITCH (owner 2026-10-16): switch this window between logged-in profiles with one y, never automatically.
2
+ import { askText } from './ask.js';
3
+
4
+ /**
5
+ * The App's api, swappable in place: every reader (and every controller built at mount) goes through this proxy,
6
+ * so a profile switch replaces ref.current.live and the whole window follows. Methods are bound to the live api.
7
+ */
8
+ export function liveApi(ref) {
9
+ return new Proxy({}, {
10
+ get(_, k) {
11
+ const a = ref.current?.live;
12
+ if (a == null) return undefined;
13
+ const v = a[k];
14
+ return typeof v === 'function' ? v.bind(a) : v;
15
+ },
16
+ has(_, k) { const a = ref.current?.live; return a != null && k in Object(a); },
17
+ });
18
+ }
19
+
20
+ /** The ask card offering to switch an agent-login window to a human login. */
21
+ export const switchAskLine = (profile, human) => askText('Switch?', {
22
+ title: `This window is an agent login (${profile}).`,
23
+ action: `switch this window to your login '${human}' (same conversation, attach, slot and groups)`,
24
+ });
25
+
26
+ /** The ask card for a /slot name that does not exist (the near misses are said on the line above). */
27
+ export const slotCreateAskLine = (nm) => askText('Create it?', { title: `No slot ${nm}.`, action: `create and hold slot ${nm}` });
28
+
29
+ /** A local run request (the shared run card): sign a profile in with the real `quox login`, terminal handed over. */
30
+ export function loginRunRequest(name, why, now = Date.now()) {
31
+ return { id: null, cmd: `quox login -p ${name}`, why, who: 'Quox', runnable: true, refuse: null, danger: null, expiresAt: now + 120000 };
32
+ }
33
+
34
+ /** A safe profile name to put on a command line (letters, digits, . _ -). */
35
+ export const okProfileName = (n) => /^[A-Za-z0-9._-]{1,64}$/.test(String(n || ''));
36
+
37
+ /**
38
+ * /profile chooser rows (PROFILE-CHOOSER): signed-in profiles first (this window's marked), then profiles on this
39
+ * machine that are not signed in, then the current login's orgs (the window's org marked). Each row carries `pick`:
40
+ * { profile } switches this window in place, { login } offers `quox login -p`, { org } goes through /org switch
41
+ * (window-scoped since #1034, confirmed first). Nothing here writes the shared default profile.
42
+ */
43
+ export function profileChooserRows({ current, humanProfiles = [], loginCandidates = [], orgs = [], orgId = null }) {
44
+ const names = [...new Set([current, ...humanProfiles].filter(okProfileName))];
45
+ const later = loginCandidates.filter(n => okProfileName(n) && !names.includes(n));
46
+ const orgList = (Array.isArray(orgs) ? orgs : []).map(o => ({ id: o.id, name: o.name || o.slug || o.id, slug: o.slug || null, role: o.role || null }));
47
+ const w = Math.min(28, Math.max(8, ...[...names, ...later].map(n => n.length + 8), ...orgList.map(o => [...o.name].length + 4)));
48
+ const rows = [];
49
+ for (const n of names) rows.push({ key: `${n === current ? '* ' : ' '}${`profile ${n}`.padEnd(w)}`, text: n === current ? 'this window' : 'signed in: switch this window to it', pick: { profile: n } });
50
+ for (const n of later) rows.push({ key: ` ${`profile ${n}`.padEnd(w)}`, text: 'not signed in: offers quox login', pick: { login: n } });
51
+ for (const o of orgList) rows.push({ key: `${o.id === orgId ? '* ' : ' '}${`org ${o.name}`.padEnd(w)}`, text: `${o.slug || o.id}${o.role ? ` (${o.role})` : ''}${o.id === orgId ? ' this window' : ''}`, pick: { org: o } });
52
+ return rows;
53
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * QUOXON-HISTORY: what the user typed, kept across quox runs so Up recalls it (most recent first), like a shell.
3
+ * Local only (~/.quox/history.jsonl, mode 0600), capped. The caller never offers it anything that looks like a key:
4
+ * keys, PINs and /connect secrets are refused before they reach history.
5
+ */
6
+ import { readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join, dirname } from 'node:path';
9
+
10
+ export const HISTORY_CAP = 500;
11
+ export const defaultHistoryPath = (home = homedir()) => join(home, '.quox', 'history.jsonl');
12
+
13
+ /** -> { load(): string[] oldest first, add(text) } on the given file. Every failure is quiet: history is a convenience. */
14
+ export function fileHistory(path = defaultHistoryPath(), { cap = HISTORY_CAP } = {}) {
15
+ const load = () => {
16
+ try {
17
+ const out = [];
18
+ for (const ln of readFileSync(path, 'utf8').split('\n')) {
19
+ if (!ln) continue;
20
+ try { const t = JSON.parse(ln)?.t; if (typeof t === 'string' && t) out.push(t); } catch { /* a torn line */ }
21
+ }
22
+ return out.slice(-cap);
23
+ } catch { return []; }
24
+ };
25
+ const add = (text) => {
26
+ try {
27
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
28
+ appendFileSync(path, JSON.stringify({ t: text }) + '\n', { mode: 0o600 });
29
+ if (statSync(path).size > cap * 400) { const keep = load(); writeFileSync(path, keep.map(t => JSON.stringify({ t })).join('\n') + '\n', { mode: 0o600 }); }
30
+ } catch { /* read-only home: this run still has it in memory */ }
31
+ };
32
+ return { load, add };
33
+ }
34
+
35
+ /** Push onto the in-memory list, skipping an exact repeat of the last entry (shell ignoredups). -> true when added. */
36
+ export function pushHistory(list, text) {
37
+ if (typeof text !== 'string' || !text.trim()) return false;
38
+ if (list.length && list[list.length - 1] === text) return false;
39
+ list.push(text);
40
+ return true;
41
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Provider availability, one source of truth for /model, /connect, /doctor and /capabilities (R3 audit D5:
3
+ * the Claude subscription used to tell five different stories).
4
+ *
5
+ * The SERVER decides: GET /api/v1/capabilities reports `provider.<id>` from real signals (org vault keys,
6
+ * instance keys, the server's Claude sign-in). The org vault list only adds detail (which credential, a
7
+ * stored-but-not-default key). A provider the server does not report (Ollama) falls back to the vault; when
8
+ * neither answers the state is unknown, never "not connected".
9
+ *
10
+ * Pure: `caps` is { ok, capabilities } from api.capabilities(), `vault` is { ok, rows } from api.connections().
11
+ */
12
+
13
+ export const SUBSCRIPTION_NAME = 'Claude (subscription, dev only)';
14
+ const DISPLAY = { 'provider.claude_subscription': SUBSCRIPTION_NAME };
15
+
16
+ /** The name every surface shows for a capability (the server's own name, except where Quox has a fixed one). */
17
+ export const capabilityName = (c) => DISPLAY[c?.id] || c?.name || c?.id;
18
+
19
+ /** The server could not read this provider's status (not "not available"): shown as ? unknown on every surface. */
20
+ export const statusUnreadable = (c) => c?.availability === 'unavailable' && /^Could not read .* status/i.test(c.reason || '');
21
+
22
+ export const vaultPick = (rows, type) => {
23
+ const of = rows.filter(r => r.integration_type === type);
24
+ return { def: of.find(r => r.is_default === 1 || r.is_default === true) || null, any: of[0] || null };
25
+ };
26
+
27
+ /**
28
+ * -> { state, row?, reason?, setup?, source } for one provider.
29
+ * state: 'connected' (the server can use it) | 'stored' (a key exists but is not the org default) |
30
+ * 'missing' (needs setup) | 'unavailable' (not on this server) | 'unknown' (nothing answered).
31
+ * p: { cap: 'openai' | 'claude_subscription' | ..., vault?: integration type }
32
+ */
33
+ export function providerState(p, caps, vault) {
34
+ const rows = vault?.ok && Array.isArray(vault.rows) ? vault.rows : null;
35
+ const v = rows && p.vault ? vaultPick(rows, p.vault) : { def: null, any: null };
36
+ const c = caps?.ok && Array.isArray(caps.capabilities) ? caps.capabilities.find(x => x?.id === `provider.${p.cap}`) : null;
37
+ if (c && statusUnreadable(c)) return { state: 'unknown', reason: c.reason, source: 'server' };
38
+ if (c) {
39
+ const base = { reason: c.reason || null, setup: c.setup || null, source: 'server' };
40
+ if (c.availability === 'installed') return { ...base, state: 'connected', row: v.def || null };
41
+ if (c.availability === 'requires_setup') return v.any && !v.def ? { ...base, state: 'stored', row: v.any } : { ...base, state: 'missing' };
42
+ return { ...base, state: 'unavailable' };
43
+ }
44
+ if (!rows || !p.vault) return { state: 'unknown', source: 'none' };
45
+ if (v.def) return { state: 'connected', row: v.def, source: 'vault' };
46
+ if (v.any) return { state: 'stored', row: v.any, source: 'vault' };
47
+ return { state: 'missing', source: 'vault' };
48
+ }