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
@@ -5,119 +5,22 @@ import { App } from './app.js';
5
5
  import { inkRenderOptions, inkStdout } from './term.js';
6
6
  import { createDemoApi } from './demo.js';
7
7
  import { DEMO_LABEL } from './demo.fixtures.js';
8
- import { PASTE_ON } from './keys.js';
8
+ import { AUTOPLAY_LABEL, autoplayOn } from './demo.autoplay.js';
9
+ import { captureEarlyInput } from './earlyInput.js';
9
10
  import { shouldShowWelcome, markWelcomeSeen } from './welcome.js';
10
11
  import { loadRules, rulesContext } from './rules.js';
11
12
  import { createTranscript } from './transcript.js';
12
- import { parseModelArg } from './model.js';
13
+ import { createScreenRecorder, screenFramesEnabled } from './screenFrames.js';
13
14
  import { applySettings } from './slash/settings.js';
14
- import { headOf, RUNNING_ROOT, execRestart, checkRestart } from './slash/restart.js';
15
+ import { headOf, RUNNING_ROOT, execRestart, restartPlan, checkRestart, startBuildWatch, settleMsFrom, restoreTerminal, RESTART_SLOT_ENV, RESTART_ATTACH_ENV, RESTART_NOTE_ENV, RESTART_MODE_ENV } from './slash/restart.js';
16
+ import { readLastSeen, writeLastSeen } from './away.js';
17
+ import { fileHistory } from './promptHistory.js';
18
+ import { takeLauncherHandoff, writeRestartHandoff, RESTART_EXIT_CODE } from './launcher.js';
15
19
 
16
- const FLAGS = new Set(['-p', '--profile', '--verbose', '--demo', '--print', '--output', '--conversation', '--turn-id', '--timeout', '--color', '--no-clear', '-n', '--name', '--service-key', '--demo-onboarding', '--provider', '--model', '--resume', '-h', '--help', '--']);
17
- const isFlag = (a) => FLAGS.has(a) || /^--(profile|verbose|print|output|conversation|turn-id|timeout|provider|model|resume)=/.test(a);
20
+ export { parseArgs, USAGE, pickProfile, choosePick, chatCredentialProblem, noServerConfigured, printAuthMessage } from './headless.js';
21
+ import { parseArgs, USAGE, noServerConfigured, mainPrint, pickProfile, chatCredentialProblem } from './headless.js';
18
22
 
19
- export function parseArgs(argv) {
20
- const o = { profile: null, verbose: 'on', demo: false, help: false, print: false, prompt: null, output: 'text', conversation: null, turnId: null, timeout: 0, color: false, noClear: false, name: null, serviceKey: false, demoOnboarding: false, provider: null, resume: null, verboseSet: false, errors: [] };
21
- let rawProvider = null, rawModel = null;
22
- const value = (flag, i) => { const v = argv[i + 1]; if (v === undefined || (v.startsWith('-') && isFlag(v))) { o.errors.push(`${flag} needs a value`); return undefined; } return v; };
23
- for (let i = 0; i < argv.length; i++) {
24
- const a = argv[i];
25
- if (a === '--') { const rest = argv.slice(i + 1).join(' '); if (rest) o.prompt = rest; break; }
26
- if (a === '-p' || a === '--profile') { const v = value(a, i); if (v !== undefined) { o.profile = v; i++; } }
27
- else if (a.startsWith('--profile=')) o.profile = a.slice(10);
28
- else if (a === '--verbose') { o.verboseSet = true; const v = argv[i + 1]; if (['off', 'on', 'full'].includes(v)) { o.verbose = v; i++; } else o.verbose = 'on'; } // no value means on
29
- else if (a.startsWith('--verbose=')) { o.verboseSet = true; o.verbose = a.slice(10); }
30
- else if (a === '--resume') { const v = argv[i + 1]; if (v !== undefined && !v.startsWith('-')) { o.resume = v; i++; } else o.resume = true; } // alone: the picker
31
- else if (a.startsWith('--resume=')) o.resume = a.slice(9) || true;
32
- else if (a === '-n' || a === '--name') { const v = value(a, i); if (v !== undefined) { o.name = v; i++; } }
33
- else if (a.startsWith('--name=')) o.name = a.slice(7);
34
- else if (a === '--demo') o.demo = true;
35
- else if (a === '--demo-onboarding') o.demoOnboarding = true;
36
- else if (a === '--print') { o.print = true; const n = argv[i + 1]; if (n !== undefined && !isFlag(n)) { o.prompt = n; i++; } }
37
- else if (a.startsWith('--print=')) { o.print = true; o.prompt = a.slice(8); }
38
- else if (a === '--output') { const v = value(a, i); if (v !== undefined) { o.output = v; i++; } }
39
- else if (a.startsWith('--output=')) o.output = a.slice(9);
40
- else if (a === '--conversation') { const v = value(a, i); if (v !== undefined) { o.conversation = v; i++; } }
41
- else if (a === '--turn-id') { const v = value(a, i); if (v !== undefined) { o.turnId = v; i++; } }
42
- else if (a === '--timeout') { const v = value(a, i); if (v !== undefined) { o.timeout = Number(v) || 0; i++; } }
43
- else if (a === '--color') o.color = true;
44
- else if (a === '--no-clear') o.noClear = true;
45
- else if (a === '--service-key') o.serviceKey = true;
46
- else if (a === '--provider' || a === '--model') { const v = value(a, i); if (v !== undefined) { if (a === '--provider') rawProvider = v; else rawModel = v; i++; } }
47
- else if (a.startsWith('--provider=')) rawProvider = a.slice(11);
48
- else if (a.startsWith('--model=')) rawModel = a.slice(8);
49
- else if (a === '--prompt-profile') { const v = value(a, i); if (v !== undefined) { o.promptProfile = v; i++; } }
50
- else if (a.startsWith('--prompt-profile=')) o.promptProfile = a.slice(17);
51
- else if (a === '-h' || a === '--help') o.help = true;
52
- else if (!a.startsWith('-') && o.prompt === null && argv.includes('--print')) o.prompt = a; // `--print` given earlier or later; this is its prompt
53
- else o.errors.push(a.startsWith('-') ? `unknown option ${a}` : `unexpected argument ${a}`);
54
- }
55
- if (rawModel && !rawProvider) o.errors.push('--model needs --provider (e.g. --provider openai --model gpt-5.4)');
56
- else if (rawProvider) {
57
- const m = parseModelArg(rawModel ? `${rawProvider} ${rawModel}` : rawProvider);
58
- if (!m.ok) o.errors.push(m.error); else o.provider = m.choice || 'auto'; // 'auto' = this run ignores the saved /model choice
59
- }
60
- if (o.promptProfile !== undefined && !['compact', 'full'].includes(o.promptProfile)) { o.errors.push(`invalid --prompt-profile "${o.promptProfile}" (use compact or full)`); delete o.promptProfile; }
61
- if (!['off', 'on', 'full'].includes(o.verbose)) o.verbose = 'on';
62
- if (!['text', 'json'].includes(o.output)) { o.errors.push(`invalid --output "${o.output}" (use text or json)`); o.output = 'text'; }
63
- return o;
64
- }
65
-
66
- export const USAGE = 'quox [-p|--profile <name>] [-n|--name <session name>] [--conversation <id>] [--resume [id|name]] [--verbose off|on|full] [--demo] [--no-clear] [--service-key] [--provider openai|anthropic|ollama|openrouter|huggingface|groq|together|lmstudio|vllm|custom|auto [--model <id>]] (quoxon is an alias; the old menu is `quox tui`)\n quox --conversation <id> resume that conversation interactively (its saved turns load into the window)\n quox --resume [id|name] pick up a recent conversation; alone it opens a picker\n quox --print "<prompt>" [--output text|json] [--conversation <id>] [--turn-id <id>] [--timeout <s>] [--provider <p> [--model <id>]] [--prompt-profile compact|full] (prompt from stdin if absent or -; use -- before a prompt that starts with -)\n exit: 0 completed, 1 other, 2 held for approval, 3 interrupted or abandoned, 4 not signed in, 5 ended without server confirmation\n --provider asks the server for that provider this run (default: your /model choice for this profile, else auto); turn.started from the server says what ran\n --service-key lets chat use an ambient QUOX_SERVICE_KEY when the profile has no login (never used silently)\n --demo plays a simulated journey on its own (no server, nothing real runs)\n --demo-onboarding walks Getting started with a dry-run install (nothing is downloaded, built or started)';
67
-
68
- /** Default profile: a resident (agent) login cannot chat, so use the single logged-in human profile. */
69
- export function pickProfile(explicit, cfgMod) {
70
- if (explicit) return { name: explicit, note: null };
71
- const probe = cfgMod.loadConfig();
72
- const active = probe.getActiveProfileName?.();
73
- const residents = new Set((probe.listResidentProfiles?.() || []).map(r => r.name));
74
- return choosePick(active, residents, probe.listProfiles?.() || []);
75
- }
76
- export function choosePick(active, residents, profiles) {
77
- if (!residents.has(active)) return { name: undefined, note: null };
78
- const humans = profiles.filter(p => !residents.has(p.name) && p.loggedIn);
79
- if (humans.length === 1) return { name: humans[0].name, note: null }; // the status line shows the profile; no transcript noise
80
- return { name: undefined, note: `The default profile "${active}" is an agent login and cannot chat. Pass -p <your profile>.` };
81
- }
82
-
83
- /**
84
- * Chat identity gate. A named profile that does not exist is an error (never created on the fly), and an
85
- * ambient QUOX_SERVICE_KEY is only a credential when the user asked for it (--service-key or FORCE).
86
- * -> null when chat may proceed, else { message }.
87
- */
88
- export function chatCredentialProblem({ profileArg, profiles, hasStoredLogin, profileName, env = process.env, serviceKeyFlag = false }) {
89
- if (profileArg && !profiles.some(p => p.name === profileArg)) return { message: `profile ${profileArg} does not exist; create it with: quox login -p ${profileArg}` };
90
- if (hasStoredLogin) return null;
91
- if ((serviceKeyFlag || env.QUOX_FORCE_SERVICE_KEY) && env.QUOX_SERVICE_KEY) return null;
92
- return { message: `Not signed in for chat on profile ${profileName}. Run: quox login -p ${profileName}${env.QUOX_SERVICE_KEY ? ' (an ambient QUOX_SERVICE_KEY is ignored unless you pass --service-key)' : ''}` };
93
- }
94
-
95
- /**
96
- * Is this a machine with no Quox server configured at all? Then bare `quox` is Getting started, and --print
97
- * says how to get there. Reads profiles only (no network).
98
- */
99
- export async function noServerConfigured(opts, cfgMod, env = process.env) {
100
- const { needsGettingStarted } = await import('./onboarding/detect.js');
101
- const { loadJourney } = await import('./onboarding/journey.js');
102
- const probe = cfgMod.loadConfig();
103
- return needsGettingStarted({ profiles: probe.listProfiles?.() || [], residents: probe.listResidentProfiles?.() || [], profileArg: opts.profile, env, serviceKeyFlag: opts.serviceKey, journey: loadJourney() });
104
- }
105
-
106
- /** --print without a usable login: today's message, plus the way in when no server exists yet. */
107
- export function printAuthMessage(problem, gettingStarted, hint) {
108
- return `quox: ${problem.message}\n${gettingStarted ? `quox: ${hint}\n` : ''}`;
109
- }
110
-
111
- /** Keys typed before the app is ready: raw mode now, so nothing is cooked-echoed, buffered for the composer. */
112
- export function captureEarlyInput(stdin = process.stdin, stdout = process.stdout) {
113
- if (!stdin?.isTTY) return null;
114
- const chunks = [];
115
- const fn = (d) => chunks.push(typeof d === 'string' ? d : d.toString('utf8'));
116
- try { stdin.setRawMode?.(true); stdin.resume?.(); } catch { /* not a tty after all */ }
117
- stdin.on('data', fn);
118
- if (stdout?.isTTY) stdout.write(PASTE_ON);
119
- return { chunks, detach: () => stdin.off('data', fn) };
120
- }
23
+ export { captureEarlyInput } from './earlyInput.js';
121
24
 
122
25
  /**
123
26
  * Clear the screen before the chat client? Never right after Getting started: its finale (the one-time
@@ -129,13 +32,14 @@ export function shouldClearBeforeChat({ isTTY, noClear, fromGettingStarted }) {
129
32
 
130
33
  export async function main(argv = process.argv.slice(2)) {
131
34
  const opts = parseArgs(argv);
132
- if (opts.help) { console.log(USAGE); return; }
133
- if (opts.errors.length) { process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
134
35
  // Raw mode FIRST: the awaits below (module loads, config, health) must not leave the tty cooked.
36
+ // bin/quox.js already turned it on before loading anything heavy; this returns that same capture.
135
37
  const early = !opts.print ? captureEarlyInput() : null;
38
+ if (opts.help) { early?.release(); console.log(USAGE); return; }
39
+ if (opts.errors.length) { early?.release(); process.stderr.write(`quox: ${opts.errors.join('; ')}\n${USAGE}\n`); process.exitCode = 1; return; }
136
40
  const cwd = process.cwd().replace(process.env.HOME || '\0', '~');
137
41
  const { resolveCliVersion } = await import('../core/version.js');
138
- let version = '0.2.0';
42
+ let version = '0.3.1';
139
43
  try { version = resolveCliVersion?.() || version; } catch { /* keep default */ }
140
44
  if (opts.print) return mainPrint(opts);
141
45
  let replay = null;
@@ -156,8 +60,10 @@ export async function main(argv = process.argv.slice(2)) {
156
60
  let props;
157
61
  let restart = null; // set by /restart: run the installed quox here once the app has let go of the terminal
158
62
  if (opts.demo) {
63
+ const auto = autoplayOn(opts.autoplay) ? { exitAfterMs: opts.autoplayExit != null ? opts.autoplayExit * 1000 : (process.env.QUOX_DEMO_AUTOPLAY_EXIT ? Number(process.env.QUOX_DEMO_AUTOPLAY_EXIT) * 1000 : null) } : null;
159
64
  // /restart works in the demo too (a fresh demo, nothing to resume): the same hand-over, testable with no server
160
- props = { api: createDemoApi(), profile: 'demo', org: 'demo-org', demoLabel: DEMO_LABEL, autoplay: 'Show me what you can do (demo)', onRestart: (r) => { restart = { ...r, conversationId: null }; }, restartCheck: checkRestart };
65
+ // QUOX_DEMO_DECISIONS: demo-only pty test seam for the walkthrough proofs (a decisions state file; never a real server)
66
+ props = { api: createDemoApi({ objects: true, decisionsFile: process.env.QUOX_DEMO_DECISIONS || null }), profile: 'demo', org: 'demo-org', demoLabel: auto ? AUTOPLAY_LABEL : DEMO_LABEL, autoplay: auto ? null : 'Show me what you can do (demo)', demoAuto: auto, onRestart: (r) => { restart = { ...r, conversationId: null }; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart };
161
67
  } else {
162
68
  const cfgMod = await import('../core/config.js');
163
69
  const { createAuth } = await import('../core/auth.js');
@@ -168,77 +74,103 @@ export async function main(argv = process.argv.slice(2)) {
168
74
  const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
169
75
  const pick = pickProfile(opts.profile, cfgMod);
170
76
  const config = missing ? probe : cfgMod.loadConfig(pick.name);
171
- const auth = createAuth(config);
172
- const client = createClient(config, auth);
173
- const api = createApi({ client, config, auth });
77
+ const { createWindowClient, carryOrg, RESTART_ORG_ENV } = await import('./windowOrg.js');
78
+ const win = createWindowClient({ config, auth: createAuth(config), createClient }); // #1034: /org switch is window-scoped
79
+ const api = createApi({ ...win, config });
174
80
  const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
175
81
  const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
82
+ // Z2H: a dead session (refresh rejected) or a cleared login on a human profile is ONE signed-out state, answered
83
+ // by a single card and /login, not scattered digest/approvals/chat errors. Transient failures read as alive.
84
+ let signedOut = null;
85
+ if (!missing && !opts.serviceKey) {
86
+ if (problem && !pick.note) signedOut = { reason: null };
87
+ else if (!problem) {
88
+ const pr = await Promise.race([api.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
89
+ if (!pr.alive) signedOut = { reason: pr.reason || null };
90
+ }
91
+ }
176
92
  // The health probe runs inside the app (background); the UI is up and typing works immediately.
177
93
  const startup = async () => {
94
+ if (signedOut) return null;
178
95
  if (problem) return problem.message;
179
96
  if (pick.note) return pick.note;
180
97
  const hl = await api.health(8000);
181
98
  return hl.ok ? null : hl.absent ? `No QuoxCORE running at ${hl.url}` : `QuoxCORE at ${hl.url} is not answering (${hl.status}); messages may be slow`;
182
99
  };
100
+ // /restart hand-over: re-enter the predecessor window's org in this window only (the profile is untouched).
101
+ const carried = !missing && !signedOut && !problem ? await carryOrg(api, process.env) : (delete process.env.QUOX_RESTART_ORG, null);
102
+ const startupCarry = async () => (carried?.error ? `Could not re-enter org ${carried.id} after the restart (${carried.error}). This window is in the profile's org.` : startup());
183
103
  const saved = api.settings();
184
104
  applySettings(saved);
185
- if (!opts.verboseSet && ['off', 'on', 'full'].includes(saved.verbose)) opts.verbose = saved.verbose;
186
- props = { api, modelChoice: opts.provider, resume: opts.resume ?? null, onRestart: (r) => { restart = r; }, restartCheck: checkRestart, startSha: headOf(RUNNING_ROOT), rules: loadRules(), profile: pname, org: missing ? null : (config.getAuth?.()?.org_name || null), startup, blocked: problem ? problem.message : null, autoplay: replay };
105
+ if (!opts.verboseSet) opts.verbose = ['off', 'on', 'full'].includes(saved.verbose) ? saved.verbose : 'off'; // protocol lines are opt-in: /verbose on
106
+ // PROFILE-SWITCH: build a signed-in profile's api for an in-place window switch (the App only asks after a y or
107
+ // /profile). The window org rides along; the profile file's default is never moved (loadConfig(name) is a lens).
108
+ const residentNames = () => (cfgMod.loadConfig().listResidentProfiles?.() || []).map(r => r.name);
109
+ const switchProfile = async (name, { org: winOrg = null } = {}) => {
110
+ const row = (cfgMod.loadConfig().listProfiles?.() || []).find(p => p.name === name);
111
+ if (!row || !row.loggedIn) return { ok: false, reason: 'not_signed_in' };
112
+ const c2 = cfgMod.loadConfig(name);
113
+ const a2 = createApi({ ...createWindowClient({ config: c2, auth: createAuth(c2), createClient }), config: c2 });
114
+ const pr = await Promise.race([a2.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
115
+ if (!pr.alive) return { ok: false, reason: 'not_signed_in' };
116
+ const c = winOrg ? await carryOrg(a2, { [RESTART_ORG_ENV]: winOrg }) : null;
117
+ const note = c?.error ? `Could not re-enter org ${winOrg} (${c.error}); this window is in ${name}'s org.` : null;
118
+ return { ok: true, api: a2, agentLogin: residentNames().includes(name), orgName: (c && !c.error && c.name) || c2.getAuth?.()?.org_name || null, note };
119
+ };
120
+ const loginCandidates = profiles.filter(p => !p.loggedIn && !residentNames().includes(p.name)).map(p => p.name);
121
+ props = { api, switchProfile, loginCandidates, away: { read: readLastSeen, write: writeLastSeen }, modelChoice: opts.provider, resume: opts.resume ?? null, onRestart: (r) => { restart = r; }, onDemo: () => { restart = { demo: true }; }, restartCheck: checkRestart, buildWatch: (o) => startBuildWatch({ ...o, settleMs: settleMsFrom() }), startSha: headOf(RUNNING_ROOT), rules: loadRules(), profile: pname, agentLogin: (probe.listResidentProfiles?.() || []).some(r => r.name === pname), humanProfiles: profiles.filter(p => p.loggedIn && !(probe.listResidentProfiles?.() || []).some(r => r.name === p.name)).map(p => p.name), org: missing ? null : ((carried && !carried.error && carried.name) || config.getAuth?.()?.org_name || null), startup: startupCarry, healthRecheck: () => api.health(8000), blocked: problem && !signedOut ? problem.message : null, signedOut, autoplay: replay };
187
122
  }
188
123
  // Interactive start: a clean screen under the intro (never when piped; NO_COLOR still clears),
189
124
  // except straight after Getting started, whose finale must stay readable.
190
125
  if (shouldClearBeforeChat({ isTTY: process.stdout.isTTY, noClear: opts.noClear, fromGettingStarted })) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
126
+ // Z2H-R5 C: do not advertise /slot where the licence check says no (the server's own capabilities answer, short wait).
127
+ let slotsAvailable = true;
128
+ if (!opts.demo && process.stdout.isTTY && props.api?.capabilities && !props.signedOut && !props.blocked) {
129
+ try {
130
+ const caps = await Promise.race([props.api.capabilities(), new Promise(res => setTimeout(() => res(null), 2000))]);
131
+ const c = caps?.ok ? caps.capabilities.find(x => x.id === 'slots') : null;
132
+ if (c && c.availability === 'unavailable' && /not in your plan/i.test(String(c.reason || ''))) slotsAvailable = false;
133
+ } catch { /* unknown: leave the invite as it was */ }
134
+ }
191
135
  const welcome = shouldShowWelcome({ print: opts.print });
192
136
  if (welcome) markWelcomeSeen();
193
137
  const transcript = opts.demo ? null : createTranscript({ meta: { version, cols: process.stdout.columns, rows: process.stdout.rows, profile: props.profile, org: props.org, cwd } });
194
138
  const onResize = () => transcript?.resize(process.stdout.columns, process.stdout.rows);
195
139
  if (transcript) process.stdout.on('resize', onResize);
196
- const app = render(React.createElement(App, { ...props, welcome, version, cwd, verbose: opts.verbose, earlyInput: early, sessionName: opts.name, resumeConversation: opts.conversation || null, invite: Boolean(process.stdout.isTTY) && !opts.name, transcript }), inkRenderOptions(process.stdout));
197
- try { await app.waitUntilExit(); transcript?.end(restart ? 'restart' : 'exit'); }
198
- catch (e) { transcript?.end(`crash: ${e?.message || e}`); throw e; }
199
- finally { process.stdout.off('resize', onResize); }
140
+ // SCREEN-FRAMES: what the screen showed, settled, into the same local transcript (QUOX_SCREEN_FRAMES=0 turns it off).
141
+ const screenRec = transcript && process.stdout.isTTY && screenFramesEnabled(process.env)
142
+ ? createScreenRecorder({ write: (rec) => transcript.screen(rec), cols: () => process.stdout.columns, rows: () => process.stdout.rows })
143
+ : null;
144
+ const restartSlot = opts.demo ? null : (process.env[RESTART_SLOT_ENV] || null); delete process.env[RESTART_SLOT_ENV]; // one hand-over only
145
+ const restartAttach = opts.demo ? null : (process.env[RESTART_ATTACH_ENV] || null); delete process.env[RESTART_ATTACH_ENV];
146
+ const restartMode = opts.demo ? null : (process.env[RESTART_MODE_ENV] === 'plan' ? 'plan' : null); delete process.env[RESTART_MODE_ENV]; // PLAN MODE: window-scoped, carried once
147
+ const restartNote = opts.demo ? null : (process.env[RESTART_NOTE_ENV] || null); delete process.env[RESTART_NOTE_ENV]; // e.g. a failed /update, said once in the restarted client
148
+ if (restartNote && !props.introNote) props.introNote = restartNote;
149
+ const app = render(React.createElement(App, { ...props, restartSlot, restartAttach, ...(restartMode ? { mode: restartMode } : {}), welcome, tipStartup: !fromGettingStarted && !opts.demo && Boolean(process.stdout.isTTY), whatsNewStartup: !opts.demo && Boolean(process.stdout.isTTY), version, cwd, verbose: opts.verbose, earlyInput: early, sessionName: opts.name, resumeConversation: opts.conversation || null, invite: Boolean(process.stdout.isTTY) && !opts.name, slotsAvailable, transcript, historyStore: opts.demo ? null : fileHistory() }), inkRenderOptions(process.stdout, { tap: screenRec?.tap }));
150
+ try { await app.waitUntilExit(); screenRec?.stop(); transcript?.end(restart ? 'restart' : 'exit'); }
151
+ catch (e) { screenRec?.stop(); transcript?.end(`crash: ${e?.message || e}`); throw e; }
152
+ finally { screenRec?.stop(); process.stdout.off('resize', onResize); }
200
153
  if (!restart) return;
154
+ if (restart.update) { // /update said y: the update owns the terminal (npm prompts, sudo), then the same restart either way
155
+ const { runUpdate } = await import('../core/selfUpdate.js');
156
+ const plan = restart.update;
157
+ restoreTerminal();
158
+ process.stdout.write(`\nUpdating quox: ${plan.command.display}\n`);
159
+ const u = runUpdate(plan);
160
+ if (u.ok) process.stdout.write(`\u2714 Update finished (exit 0). Restarting into this conversation\u2026\n`);
161
+ else {
162
+ const note = `Update failed: ${plan.command.display} ${u.error ? `could not start (${u.error})` : `exited ${u.code}`}. Restarting on the build that is installed now. Run it yourself to see why: ${plan.manual}`;
163
+ process.stdout.write(`\u2716 ${note}\n`);
164
+ process.env[RESTART_NOTE_ENV] = note; // the restarted client (same build) says it again in the conversation
165
+ }
166
+ }
201
167
  // The app preflighted the installed quox before letting go, so a failure here is rare (it changed in between).
202
- const r = execRestart({ argv, conversationId: restart.conversationId, profile: opts.demo ? null : props.profile, name: restart.name || opts.name || null, bin: restart.bin || undefined });
168
+ const how = { argv: restart.demo ? ['--demo', '--autoplay'] : argv, conversationId: restart.demo ? null : restart.conversationId, profile: opts.demo ? null : (restart.profile || props.profile), name: restart.name || opts.name || null, slot: restart.demo ? null : (restart.slot || null), attach: restart.demo ? null : (restart.attach || null), org: restart.demo ? null : (restart.org || null), mode: restart.demo ? null : (restart.mode || null), bin: restart.bin || undefined };
169
+ // QUOXON-RESTART-LEAK: under the launcher this process EXITS and the launcher runs the successor, so exactly one
170
+ // client stays alive per terminal. Without one (QUOX_NO_LAUNCHER=1) it falls back to waiting on a nested child.
171
+ const launched = takeLauncherHandoff();
172
+ if (launched && writeRestartHandoff(launched, restartPlan(how))) { restoreTerminal(); process.exitCode = RESTART_EXIT_CODE; return; }
173
+ const r = execRestart(how);
203
174
  if (r.error) { process.stderr.write(`quox: could not restart (${r.bin}: ${r.error})${restart.conversationId ? `; resume with: quox --resume ${restart.conversationId}` : ''}\n`); process.exitCode = 1; return; }
204
175
  process.exitCode = r.code;
205
176
  }
206
-
207
- async function mainPrint(opts) {
208
- const { runPrint, resolvePrompt, EXIT } = await import('./print.js');
209
- const out = (s) => process.stdout.write(s);
210
- const err = (s) => process.stderr.write(s);
211
- const prompt = await resolvePrompt(opts.prompt, process.stdin);
212
- let api;
213
- if (opts.demo) api = createDemoApi({ speed: 20 });
214
- else {
215
- const cfgMod = await import('../core/config.js');
216
- const { createAuth } = await import('../core/auth.js');
217
- const { createClient } = await import('../core/client.js');
218
- const { createApi } = await import('./api.js');
219
- const probe = cfgMod.loadConfig();
220
- const profiles = probe.listProfiles?.() || [];
221
- const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
222
- const pick = pickProfile(opts.profile, cfgMod);
223
- if (pick.note) err(pick.note + '\n');
224
- const config = missing ? probe : cfgMod.loadConfig(pick.name);
225
- const auth = createAuth(config);
226
- const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
227
- const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
228
- if (problem) {
229
- const { NO_SERVER_PRINT } = await import('./onboarding/copy.js');
230
- err(printAuthMessage(problem, await noServerConfigured(opts, cfgMod), NO_SERVER_PRINT));
231
- process.exitCode = EXIT.auth; return;
232
- }
233
- api = createApi({ client: createClient(config, auth), config, auth });
234
- if (opts.conversation && api.conversationExists) {
235
- const c = await api.conversationExists(opts.conversation);
236
- if (c.exists === false) { err(`quox: conversation not found: ${opts.conversation}\n`); process.exitCode = EXIT.error; return; }
237
- if (c.exists === null) err(`quox: could not verify conversation ${opts.conversation} (${c.reason}); continuing\n`);
238
- }
239
- }
240
- const rules = opts.demo ? null : loadRules();
241
- for (const s of rules?.skipped || []) err(`quox: ignoring ${s.path}: ${s.reason}\n`);
242
- const choice = opts.provider === 'auto' ? null : opts.provider || api.modelChoice?.() || null;
243
- process.exitCode = await runPrint({ api, choice, promptProfile: opts.promptProfile, context: rulesContext(rules), prompt, output: opts.output, conversation: opts.conversation, turnId: opts.turnId || undefined, timeoutS: opts.timeout, color: opts.color, out, err });
244
- }
@@ -13,23 +13,14 @@
13
13
  */
14
14
  import React from 'react';
15
15
  import { Box, Text, Transform } from 'ink';
16
- import { Lexer } from 'marked';
17
16
  import { graphemes } from './textBuffer.js';
18
- import { c, mono, clip, gradient, bulletRole } from './theme.js';
17
+ import { c, mono, gradient, bulletRole } from './theme.js';
19
18
 
20
19
  const h = React.createElement;
21
20
 
22
- export const lex = (src) => new Lexer({ gfm: true, breaks: false }).lex(src);
21
+ import { lex, splitSettled } from './markdownSettle.js';
23
22
 
24
- export function splitSettled(text) {
25
- const tokens = lex(text);
26
- let last = -1;
27
- for (let i = tokens.length - 1; i >= 0; i--) if (tokens[i].type !== 'space') { last = i; break; }
28
- if (last <= 0) return { settled: '', rest: text };
29
- let off = 0;
30
- for (let i = 0; i < last; i++) off += tokens[i].raw.length;
31
- return { settled: text.slice(0, off), rest: text.slice(off) };
32
- }
23
+ export { lex, splitSettled }; // QUOXON-LATENCY: defined in markdownSettle.js so --print never loads Ink
33
24
 
34
25
  export function healInline(text) {
35
26
  let t = text;
@@ -128,22 +119,55 @@ function inline(tokens = [], style = {}, key = 'i') {
128
119
  const w = (s) => graphemes(s).length;
129
120
  const padTo = (s, n) => s + ' '.repeat(Math.max(0, n - w(s)));
130
121
 
131
- /** Plain-text table layout: returns { mode: 'grid'|'vertical', lines: [{ text, head? }] }. Pure, for tests. */
122
+ /** Hard-wrap plain text to `width` columns at word boundaries (long words split). Never drops text. Always >= 1 line. */
123
+ export function wrapCell(text, width) {
124
+ const width_ = Math.max(1, width);
125
+ const out = [];
126
+ for (const para of String(text ?? '').split('\n')) {
127
+ let cur = '';
128
+ for (const word of para.split(/\s+/).filter(Boolean)) {
129
+ let g = graphemes(word).map(x => x.text);
130
+ if (cur && w(cur) + 1 + g.length <= width_) { cur += ' ' + word; continue; }
131
+ if (cur) { out.push(cur); cur = ''; }
132
+ while (g.length > width_) { out.push(g.slice(0, width_).join('')); g = g.slice(width_); }
133
+ cur = g.join('');
134
+ }
135
+ out.push(cur);
136
+ }
137
+ return out;
138
+ }
139
+
140
+ /**
141
+ * Plain-text table layout: { mode: 'grid'|'vertical', lines: [{ text, head?, rule? }] }. Pure, for tests.
142
+ * Grid when it fits, else vertical key/value blocks whose values wrap under a hanging indent. No cell is ever truncated
143
+ * (TUI-UX #3: the grounding warning named an id that sat in a clipped cell).
144
+ */
132
145
  export function layoutTable(header, rows, cols) {
133
146
  const n = header.length;
134
147
  const widths = header.map((hd, i) => Math.max(w(hd), ...rows.map(r => w(r[i] ?? '')), 1));
135
148
  const total = widths.reduce((a, b) => a + b, 0) + 3 * (n - 1);
136
- if (total <= cols) {
137
- const line = (r) => r.map((cell, i) => padTo(cell ?? '', widths[i])).join(' │ ').replace(/\s+$/, '');
138
- const rule = widths.map(x => '─'.repeat(x)).join('─┼─');
139
- return { mode: 'grid', lines: [{ text: line(header), head: true }, { text: rule, rule: true }, ...rows.map(r => ({ text: line(r) }))] };
140
- }
141
- // too wide: vertical key: value, one block per row
149
+ const gridLines = (ws) => {
150
+ const cellLines = (r) => {
151
+ const cells = r.map((cell, i) => wrapCell(cell ?? '', ws[i]));
152
+ const h_ = Math.max(...cells.map(x => x.length));
153
+ return Array.from({ length: h_ }, (_, k) => cells.map((x, i) => padTo(x[k] ?? '', ws[i])).join(' │ ').replace(/\s+$/, ''));
154
+ };
155
+ const rule = ws.map(x => '─'.repeat(x)).join('─┼─');
156
+ return [...cellLines(header).map(text => ({ text, head: true })), { text: rule, rule: true }, ...rows.flatMap(r => cellLines(r).map(text => ({ text })))];
157
+ };
158
+ if (total <= cols) return { mode: 'grid', lines: gridLines(widths) };
159
+ // vertical key: value, one block per row; values wrap under a hanging indent
142
160
  const lines = [];
143
161
  const keyW = Math.min(Math.max(...header.map(w)), Math.floor(cols / 3));
162
+ const valW = Math.max(8, cols - keyW - 2);
144
163
  rows.forEach((r, ri) => {
145
164
  if (ri) lines.push({ text: '' });
146
- header.forEach((hd, i) => lines.push({ text: `${padTo(clip(hd, keyW), keyW)} ${r[i] ?? ''}`, key: keyW }));
165
+ header.forEach((hd, i) => {
166
+ const keyLines = wrapCell(hd, keyW);
167
+ const val = wrapCell(r[i] ?? '', valW);
168
+ const count = Math.max(keyLines.length, val.length);
169
+ for (let k = 0; k < count; k++) lines.push({ text: `${padTo(keyLines[k] ?? '', keyW)} ${val[k] ?? ''}`.replace(/\s+$/, ''), key: k === 0 ? keyW : undefined });
170
+ });
147
171
  });
148
172
  return { mode: 'vertical', lines };
149
173
  }
@@ -153,7 +177,7 @@ function Table({ t, cols }) {
153
177
  const rows = t.rows.map(r => r.map(x => inlineText(x.tokens)));
154
178
  const lay = layoutTable(header, rows, cols);
155
179
  return h(Box, { flexDirection: 'column' }, ...lay.lines.map((l, i) => h(Text, {
156
- key: i, bold: l.head, color: l.rule ? c('muted') : (l.head ? c('label') : undefined), wrap: 'truncate-end'
180
+ key: i, bold: l.head, color: l.rule ? c('muted') : (l.head ? c('label') : undefined), wrap: 'wrap'
157
181
  }, l.text || ' ')));
158
182
  }
159
183
 
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The streaming split, without the renderer. events.js (and so `quox --print`) needs only
3
+ * splitSettled; importing it from markdown.js loaded React + Ink (~2 s cold) on every
4
+ * headless turn before the request was even sent (QUOXON-LATENCY). markdown.js re-exports
5
+ * both names, so the interactive client is unchanged.
6
+ */
7
+ import { Lexer } from 'marked';
8
+
9
+ export const lex = (src) => new Lexer({ gfm: true, breaks: false }).lex(src);
10
+
11
+ export function splitSettled(text) {
12
+ const tokens = lex(text);
13
+ let last = -1;
14
+ for (let i = tokens.length - 1; i >= 0; i--) if (tokens[i].type !== 'space') { last = i; break; }
15
+ if (last <= 0) return { settled: '', rest: text };
16
+ let off = 0;
17
+ for (let i = 0; i < last; i++) off += tokens[i].raw.length;
18
+ return { settled: text.slice(0, off), rest: text.slice(off) };
19
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "_comment": "Vendored from ~/cli.quox.ai/matrix/domains.json (the proof matrix, 40 domain ids). Source: https://github.com/quoxai/cli.quox.ai matrix/domains.json. Tips name a domain from this list so the matrix can ingest them. When the matrix becomes canonical this file is replaced by the matrix export.",
3
+ "domains": [
4
+ "chat",
5
+ "providers",
6
+ "approvals",
7
+ "receipts",
8
+ "safety",
9
+ "hats",
10
+ "fleet",
11
+ "proxmox",
12
+ "monitoring",
13
+ "lucid",
14
+ "memory",
15
+ "qortex",
16
+ "vault",
17
+ "agents",
18
+ "workflows",
19
+ "skills",
20
+ "objectives",
21
+ "mail",
22
+ "calendar",
23
+ "contacts",
24
+ "notes",
25
+ "actions",
26
+ "voice",
27
+ "quoxchat",
28
+ "seo",
29
+ "plan",
30
+ "signal",
31
+ "browser",
32
+ "lens",
33
+ "social",
34
+ "comms",
35
+ "sessions",
36
+ "code",
37
+ "security",
38
+ "spend",
39
+ "files",
40
+ "plugins",
41
+ "identity",
42
+ "install",
43
+ "compliance",
44
+ "mind"
45
+ ]
46
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @mention completion in the composer (MENTIONS). Typing `@` at the start of a word suggests agents, fleet hosts,
3
+ * tasks (objectives) and receipts (this session's own tool-call envelopes). The remote lists are fetched ONCE per
4
+ * session on the first `@`, then every keystroke filters the cache: no request per key. A failed fetch is quiet
5
+ * (that kind just has no rows) and is retried at most every RETRY_MS. Tab or Enter accepts, Esc closes.
6
+ */
7
+ export const RETRY_MS = 30000;
8
+ export const KIND_ORDER = ['agent', 'host', 'task', 'receipt'];
9
+
10
+ /** The @word being typed at the end of the draft -> { start (index after '@'), partial } or null. */
11
+ export function mentionContext(draft) {
12
+ const t = String(draft ?? '');
13
+ const m = t.match(/(^|\s)@([^\s@]*)$/);
14
+ if (!m) return null;
15
+ return { start: t.length - m[2].length, partial: m[2] };
16
+ }
17
+
18
+ const clean = (s) => String(s ?? '').replace(/\s+/g, ' ').trim();
19
+
20
+ /** api -> [{ value, kind, label }] from the session's remote sources, never throwing; failures named per kind. */
21
+ export async function loadMentions(api) {
22
+ const kinds = {
23
+ agent: async () => (await api.agents()).map(a => ({ value: String(a.agent_id), kind: 'agent', label: a.name && a.name !== a.agent_id ? clean(a.name) : '' })),
24
+ host: async () => {
25
+ const none = { status: 'absent', value: [] };
26
+ const [inv, beat] = await Promise.all([api.mentionHosts, api.objects?.fleetNames].map((fn, i) => (fn ? Promise.allSettled([i ? api.objects.fleetNames() : api.mentionHosts()]).then(r => r[0]) : none)));
27
+ const tried = [inv, beat].filter(r => r.status !== 'absent');
28
+ if (tried.length && tried.every(r => r.status === 'rejected')) throw tried[0].reason;
29
+ const out = (inv.status === 'fulfilled' ? inv.value : []).map(h => ({ value: String(h.id), kind: 'host', label: clean([h.group, h.health].filter(Boolean).join(' · ')) }));
30
+ for (const h of beat.status === 'fulfilled' ? beat.value || [] : []) if (!out.some(o => o.value === String(h.value))) out.push({ value: String(h.value), kind: 'host', label: clean(h.desc) });
31
+ return out;
32
+ },
33
+ task: async () => ((await api.mentionTasks?.()) || []).map(o => ({ value: String(o.id), kind: 'task', label: clean(`${o.title || ''}${o.status ? ` (${o.status})` : ''}`) })),
34
+ };
35
+ const out = []; const failed = [];
36
+ await Promise.all(Object.entries(kinds).map(async ([k, fn]) => {
37
+ try { out.push(...(await fn()).filter(x => x.value && /^[\w.:-]+$/.test(x.value))); } catch { failed.push(k); }
38
+ }));
39
+ return { items: out, failed };
40
+ }
41
+
42
+ /** This session's tool calls that carry an envelope id: their receipts, newest first. Local, no request. */
43
+ export function receiptMentions(items) {
44
+ const seen = new Set(); const out = [];
45
+ for (const b of [...(items || [])].reverse()) {
46
+ if (!b || b.kind !== 'tool' || !b.envelopeId || seen.has(b.envelopeId)) continue;
47
+ seen.add(b.envelopeId);
48
+ out.push({ value: String(b.envelopeId), kind: 'receipt', label: clean(`${b.tool || 'tool call'}${b.status ? ` ${b.status}` : ''}`) });
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /** Prefix on the value first, then a substring of the value or its label; agents, hosts, tasks, receipts in that order. */
54
+ export function rankMentions(items, partial, max = 50) {
55
+ const q = String(partial || '').toLowerCase();
56
+ const seen = new Set();
57
+ const uniq = (items || []).filter(i => i?.value && !seen.has(`${i.kind}:${i.value}`) && seen.add(`${i.kind}:${i.value}`));
58
+ const order = (a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind);
59
+ if (!q) return uniq.sort(order).slice(0, max);
60
+ const pre = uniq.filter(i => i.value.toLowerCase().startsWith(q)).sort(order);
61
+ const sub = uniq.filter(i => !i.value.toLowerCase().startsWith(q) && (i.value.toLowerCase().includes(q) || i.label.toLowerCase().includes(q))).sort(order);
62
+ const out = [...pre, ...sub];
63
+ return out.length === 1 && out[0].value.toLowerCase() === q ? [] : out.slice(0, max);
64
+ }
65
+
66
+ /** Accepting a mention: replace the partial word, add one space. */
67
+ export const applyMention = (draft, cx, value) => String(draft).slice(0, cx.start) + value + ' ';
68
+
69
+ /**
70
+ * One cache for the session. items() returns what is loaded (never blocks) and starts the single load the first
71
+ * time it is asked; a failed kind is retried no sooner than RETRY_MS. loads() counts fetches (tests, /status).
72
+ */
73
+ export function createMentionCache({ load, refresh = () => {}, now = Date.now, retryMs = RETRY_MS }) {
74
+ let items = []; let state = 'idle'; let failedAt = 0; let loads = 0;
75
+ const start = () => {
76
+ state = 'loading'; loads++;
77
+ Promise.resolve().then(load).then(
78
+ (r) => { items = r?.items || []; state = r?.failed?.length ? 'partial' : 'ready'; if (state === 'partial') failedAt = now(); },
79
+ () => { state = 'failed'; failedAt = now(); }
80
+ ).then(() => refresh());
81
+ };
82
+ return {
83
+ items() {
84
+ if (state === 'idle' || ((state === 'failed' || state === 'partial') && now() - failedAt >= retryMs)) start();
85
+ return items;
86
+ },
87
+ state: () => state,
88
+ loads: () => loads,
89
+ };
90
+ }