quoxcli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) 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/actions.js +15 -0
  5. package/lib/api/endpoints/admin.js +46 -0
  6. package/lib/api/endpoints/agent-schedules.js +26 -0
  7. package/lib/api/endpoints/agents.js +7 -0
  8. package/lib/api/endpoints/assistants.js +12 -0
  9. package/lib/api/endpoints/away.js +15 -0
  10. package/lib/api/endpoints/blog.js +16 -0
  11. package/lib/api/endpoints/chat.js +14 -0
  12. package/lib/api/endpoints/contactsq.js +6 -0
  13. package/lib/api/endpoints/conversations.js +14 -0
  14. package/lib/api/endpoints/devServerRegistry.js +25 -0
  15. package/lib/api/endpoints/engagement.js +26 -0
  16. package/lib/api/endpoints/evidence.js +20 -0
  17. package/lib/api/endpoints/executions.js +13 -0
  18. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  19. package/lib/api/endpoints/files.js +14 -0
  20. package/lib/api/endpoints/hat.js +44 -0
  21. package/lib/api/endpoints/inbox.js +20 -0
  22. package/lib/api/endpoints/index.js +26 -0
  23. package/lib/api/endpoints/loops.js +26 -0
  24. package/lib/api/endpoints/memory.js +19 -0
  25. package/lib/api/endpoints/mind.js +13 -0
  26. package/lib/api/endpoints/mon.js +179 -0
  27. package/lib/api/endpoints/objectives.js +12 -0
  28. package/lib/api/endpoints/plugins.js +18 -0
  29. package/lib/api/endpoints/presence.js +33 -0
  30. package/lib/api/endpoints/quoxplan.js +7 -0
  31. package/lib/api/endpoints/quoxseo.js +12 -0
  32. package/lib/api/endpoints/router.js +17 -0
  33. package/lib/api/endpoints/safety.js +7 -0
  34. package/lib/api/endpoints/server.js +21 -0
  35. package/lib/api/endpoints/setup.js +6 -0
  36. package/lib/api/endpoints/skills.js +20 -0
  37. package/lib/api/endpoints/stepup.js +20 -0
  38. package/lib/api/endpoints/teams.js +2 -2
  39. package/lib/api/endpoints/triggers.js +26 -0
  40. package/lib/api/endpoints/v1.js +120 -0
  41. package/lib/api/endpoints/ward.js +7 -0
  42. package/lib/api/endpoints/workflows.js +7 -0
  43. package/lib/api/endpoints/x.js +8 -0
  44. package/lib/commands/actions.js +131 -0
  45. package/lib/commands/admin.js +16 -0
  46. package/lib/commands/agent.js +170 -27
  47. package/lib/commands/agentAttach.js +140 -13
  48. package/lib/commands/approvals.js +78 -1
  49. package/lib/commands/aside.js +24 -0
  50. package/lib/commands/ask.js +146 -0
  51. package/lib/commands/assistant.js +59 -4
  52. package/lib/commands/auth.js +8 -2
  53. package/lib/commands/backup.js +8 -4
  54. package/lib/commands/blog.js +110 -0
  55. package/lib/commands/brain.js +105 -14
  56. package/lib/commands/build.js +19 -5
  57. package/lib/commands/chat.js +180 -26
  58. package/lib/commands/chatCompanion.js +27 -0
  59. package/lib/commands/chatState.js +45 -0
  60. package/lib/commands/claude.js +35 -38
  61. package/lib/commands/contacts.js +134 -4
  62. package/lib/commands/contactsSendAt.js +21 -0
  63. package/lib/commands/conversation.js +35 -4
  64. package/lib/commands/dashboard.js +43 -0
  65. package/lib/commands/department.js +28 -7
  66. package/lib/commands/devResident.js +15 -3
  67. package/lib/commands/devSupervisor.js +13 -1
  68. package/lib/commands/discord.js +75 -39
  69. package/lib/commands/done.js +98 -28
  70. package/lib/commands/engagement.js +107 -6
  71. package/lib/commands/evidence.js +154 -0
  72. package/lib/commands/ext.js +23 -0
  73. package/lib/commands/fetchAllowlist.js +71 -0
  74. package/lib/commands/file.js +38 -1
  75. package/lib/commands/fleet.js +336 -4
  76. package/lib/commands/governance.js +12 -3
  77. package/lib/commands/gripe.js +44 -13
  78. package/lib/commands/hat.js +254 -9
  79. package/lib/commands/helpers.js +18 -1
  80. package/lib/commands/hyperion.js +58 -0
  81. package/lib/commands/image.js +68 -0
  82. package/lib/commands/inbox.js +74 -23
  83. package/lib/commands/kuma.js +1050 -0
  84. package/lib/commands/kumaSla.js +174 -0
  85. package/lib/commands/licence.js +52 -155
  86. package/lib/commands/loop.js +291 -0
  87. package/lib/commands/lucid.js +10 -4
  88. package/lib/commands/memory.js +85 -8
  89. package/lib/commands/mind.js +71 -0
  90. package/lib/commands/mon.js +69 -2
  91. package/lib/commands/monitoring.js +1 -1
  92. package/lib/commands/motion.js +2 -2
  93. package/lib/commands/nucleus.js +48 -17
  94. package/lib/commands/objective.js +59 -1
  95. package/lib/commands/offload.js +89 -0
  96. package/lib/commands/onboard.js +2 -1
  97. package/lib/commands/org.js +30 -3
  98. package/lib/commands/peek.js +28 -0
  99. package/lib/commands/plan.js +38 -0
  100. package/lib/commands/plugin.js +110 -0
  101. package/lib/commands/presence.js +89 -0
  102. package/lib/commands/provider.js +217 -0
  103. package/lib/commands/qlarity.js +19 -3
  104. package/lib/commands/quoxchat.js +13 -4
  105. package/lib/commands/quoxseo.js +69 -0
  106. package/lib/commands/reach.js +2 -1
  107. package/lib/commands/research.js +79 -0
  108. package/lib/commands/router.js +77 -0
  109. package/lib/commands/rows.js +367 -0
  110. package/lib/commands/run.js +82 -10
  111. package/lib/commands/scope.js +2 -2
  112. package/lib/commands/server.js +405 -0
  113. package/lib/commands/setup.js +33 -7
  114. package/lib/commands/shortcut.js +41 -0
  115. package/lib/commands/skills.js +68 -9
  116. package/lib/commands/slot.js +260 -7
  117. package/lib/commands/status.js +1 -1
  118. package/lib/commands/team.js +3 -1
  119. package/lib/commands/tips.js +23 -0
  120. package/lib/commands/tool.js +133 -45
  121. package/lib/commands/trace.js +12 -7
  122. package/lib/commands/trigger.js +95 -0
  123. package/lib/commands/update.js +46 -0
  124. package/lib/commands/vault.js +31 -8
  125. package/lib/commands/vm.js +237 -0
  126. package/lib/commands/voice.js +34 -3
  127. package/lib/commands/volt.js +45 -3
  128. package/lib/commands/walkthrough.js +32 -0
  129. package/lib/commands/watch.js +73 -21
  130. package/lib/commands/welcome.js +14 -0
  131. package/lib/commands/workflow.js +189 -32
  132. package/lib/commands/x.js +13 -4
  133. package/lib/commands/youtube.js +172 -0
  134. package/lib/core/actingAs.js +2 -2
  135. package/lib/core/actionItems.js +115 -0
  136. package/lib/core/activeSummary.js +186 -0
  137. package/lib/core/agentAlias.js +2 -0
  138. package/lib/core/agentFamilies.js +7 -0
  139. package/lib/core/aside.js +54 -0
  140. package/lib/core/askDecision.js +60 -0
  141. package/lib/core/auth.js +164 -13
  142. package/lib/core/callerSource.js +1 -1
  143. package/lib/core/cliBuild.js +62 -0
  144. package/lib/core/client.js +152 -35
  145. package/lib/core/commandRegistrars.js +130 -0
  146. package/lib/core/config.js +143 -25
  147. package/lib/core/customHats.js +168 -0
  148. package/lib/core/doneEngineFetch.js +8 -0
  149. package/lib/core/errors.js +329 -20
  150. package/lib/core/findingReconcile.js +90 -0
  151. package/lib/core/global-flags.js +105 -5
  152. package/lib/core/governedSession.js +1 -3
  153. package/lib/core/hyperion.js +102 -0
  154. package/lib/core/inboxResolution.js +65 -0
  155. package/lib/core/lazyCommands.js +4 -1
  156. package/lib/core/liveSessionsReport.js +1 -1
  157. package/lib/core/loopGlyph.js +97 -0
  158. package/lib/core/offload.js +533 -0
  159. package/lib/core/org-override.js +1 -1
  160. package/lib/core/orgResolve.js +70 -0
  161. package/lib/core/output.js +11 -11
  162. package/lib/core/peek.js +126 -0
  163. package/lib/core/profileUsers.js +51 -0
  164. package/lib/core/promptCancel.js +18 -0
  165. package/lib/core/repoPath.js +11 -0
  166. package/lib/core/residentSeed.js +3 -1
  167. package/lib/core/rootOptions.js +39 -0
  168. package/lib/core/router.js +168 -0
  169. package/lib/core/selfUpdate.js +188 -0
  170. package/lib/core/sessionHeartbeat.js +12 -8
  171. package/lib/core/sessionsOverview.js +64 -0
  172. package/lib/core/shortcut.js +83 -0
  173. package/lib/core/slotKeeper.js +248 -3
  174. package/lib/core/slotWake.js +120 -0
  175. package/lib/core/supervisorEngine.js +71 -2
  176. package/lib/core/version.js +1 -1
  177. package/lib/core/vm.js +366 -0
  178. package/lib/core/vmTui.js +86 -0
  179. package/lib/core/walkthrough.js +35 -0
  180. package/lib/dragon/companionView.js +34 -0
  181. package/lib/dragon/frames.js +118 -0
  182. package/lib/dragon/jobs.js +123 -0
  183. package/lib/dragon/renderWorker.js +15 -0
  184. package/lib/dragon/select.js +72 -0
  185. package/lib/evidence/bundleExport.js +47 -0
  186. package/lib/evidence/bundleVerify.js +77 -0
  187. package/lib/hyperion/deck.js +29 -0
  188. package/lib/lucid/LucidCard.jsx +68 -0
  189. package/lib/lucid/LucidIntro.jsx +45 -58
  190. package/lib/lucid/cards.js +86 -0
  191. package/lib/lucid/markdown.js +81 -0
  192. package/lib/lucid/motion.js +56 -0
  193. package/lib/quoxon/EXTENSIONS.md +23 -0
  194. package/lib/quoxon/activity.js +14 -2
  195. package/lib/quoxon/aocl.js +117 -0
  196. package/lib/quoxon/api.js +690 -25
  197. package/lib/quoxon/app.js +3586 -250
  198. package/lib/quoxon/approvalFacts.js +96 -0
  199. package/lib/quoxon/approvalSentence.js +44 -0
  200. package/lib/quoxon/approvalsView.js +384 -0
  201. package/lib/quoxon/asideView.js +40 -0
  202. package/lib/quoxon/ask.js +121 -0
  203. package/lib/quoxon/away.js +52 -0
  204. package/lib/quoxon/bang.js +224 -0
  205. package/lib/quoxon/bulkRisk.js +17 -0
  206. package/lib/quoxon/capabilities.js +13 -6
  207. package/lib/quoxon/cells.js +53 -0
  208. package/lib/quoxon/commandShort.js +124 -0
  209. package/lib/quoxon/commands.js +429 -39
  210. package/lib/quoxon/connect.js +210 -23
  211. package/lib/quoxon/connectCard.js +19 -2
  212. package/lib/quoxon/consent.js +86 -0
  213. package/lib/quoxon/copy.js +4 -3
  214. package/lib/quoxon/countdownLine.js +20 -0
  215. package/lib/quoxon/deck.js +30 -0
  216. package/lib/quoxon/demo.autoplay.js +29 -0
  217. package/lib/quoxon/demo.fixtures.js +1 -1
  218. package/lib/quoxon/demo.js +46 -3
  219. package/lib/quoxon/devObserver.js +20 -0
  220. package/lib/quoxon/dividerAocl.js +192 -0
  221. package/lib/quoxon/dividerRule.js +57 -0
  222. package/lib/quoxon/dragonCompanion.js +255 -0
  223. package/lib/quoxon/dragonModes.js +151 -0
  224. package/lib/quoxon/dragonMoments.js +82 -0
  225. package/lib/quoxon/dragonReads.js +68 -0
  226. package/lib/quoxon/dragonView.js +41 -0
  227. package/lib/quoxon/earlyInput.js +40 -0
  228. package/lib/quoxon/echo.js +15 -0
  229. package/lib/quoxon/egg.js +2 -2
  230. package/lib/quoxon/events.js +266 -26
  231. package/lib/quoxon/extensions.js +276 -0
  232. package/lib/quoxon/feedback.js +50 -0
  233. package/lib/quoxon/gallery.js +21 -7
  234. package/lib/quoxon/handoffOffer.js +78 -0
  235. package/lib/quoxon/hatSwitch.js +37 -0
  236. package/lib/quoxon/headless.js +161 -0
  237. package/lib/quoxon/hostUrls.js +41 -0
  238. package/lib/quoxon/inboxView.js +79 -0
  239. package/lib/quoxon/indicator.js +6 -2
  240. package/lib/quoxon/installCli.js +79 -0
  241. package/lib/quoxon/intent.js +12 -1
  242. package/lib/quoxon/keys.js +51 -8
  243. package/lib/quoxon/launcher.js +103 -0
  244. package/lib/quoxon/loopCard.js +456 -0
  245. package/lib/quoxon/loopCards.js +180 -0
  246. package/lib/quoxon/loopStream.js +87 -0
  247. package/lib/quoxon/main.js +101 -160
  248. package/lib/quoxon/markdown.js +45 -21
  249. package/lib/quoxon/markdownSettle.js +19 -0
  250. package/lib/quoxon/matrixDomains.json +46 -0
  251. package/lib/quoxon/mentions.js +90 -0
  252. package/lib/quoxon/mind.js +103 -0
  253. package/lib/quoxon/mode.js +46 -0
  254. package/lib/quoxon/model.js +132 -26
  255. package/lib/quoxon/modelPrefs.js +345 -0
  256. package/lib/quoxon/motion/clock.js +3 -2
  257. package/lib/quoxon/motion/level.js +91 -0
  258. package/lib/quoxon/mouse.js +222 -0
  259. package/lib/quoxon/noai.js +57 -0
  260. package/lib/quoxon/objectiveOffer.js +82 -0
  261. package/lib/quoxon/objects/away.js +72 -0
  262. package/lib/quoxon/objects/fetch.js +17 -5
  263. package/lib/quoxon/objects/fleet.js +149 -0
  264. package/lib/quoxon/objects/health.js +186 -0
  265. package/lib/quoxon/objects/index.js +46 -7
  266. package/lib/quoxon/objects/mirror.js +137 -0
  267. package/lib/quoxon/objects/model.js +33 -0
  268. package/lib/quoxon/objects/peek.js +17 -0
  269. package/lib/quoxon/objects/proof.js +12 -4
  270. package/lib/quoxon/objects/prove.js +130 -0
  271. package/lib/quoxon/objects/replay.js +125 -0
  272. package/lib/quoxon/objects/sitrep.js +58 -0
  273. package/lib/quoxon/objects/view.js +11 -0
  274. package/lib/quoxon/onboarding/copy.js +32 -4
  275. package/lib/quoxon/onboarding/credential.js +17 -4
  276. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  277. package/lib/quoxon/onboarding/failureCause.js +33 -0
  278. package/lib/quoxon/onboarding/finish.js +15 -0
  279. package/lib/quoxon/onboarding/index.js +34 -7
  280. package/lib/quoxon/onboarding/installRunner.js +64 -5
  281. package/lib/quoxon/onboarding/preflight.js +17 -3
  282. package/lib/quoxon/onboarding/probe.js +65 -0
  283. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  284. package/lib/quoxon/onboarding/view.js +76 -19
  285. package/lib/quoxon/orgLabel.js +10 -0
  286. package/lib/quoxon/orgSwitch.js +59 -0
  287. package/lib/quoxon/ownBroadcast.js +22 -0
  288. package/lib/quoxon/palette.js +43 -0
  289. package/lib/quoxon/planSteps.js +51 -0
  290. package/lib/quoxon/print.js +12 -5
  291. package/lib/quoxon/profileSwitch.js +53 -0
  292. package/lib/quoxon/promptHistory.js +41 -0
  293. package/lib/quoxon/providers.js +48 -0
  294. package/lib/quoxon/qart.js +20 -51
  295. package/lib/quoxon/qdragon.js +410 -0
  296. package/lib/quoxon/qdragonFly.js +62 -0
  297. package/lib/quoxon/qdragonScenes.js +282 -0
  298. package/lib/quoxon/qeyes.js +213 -0
  299. package/lib/quoxon/qmotion.js +75 -37
  300. package/lib/quoxon/render.js +740 -124
  301. package/lib/quoxon/resend.js +72 -0
  302. package/lib/quoxon/roster.js +117 -17
  303. package/lib/quoxon/route.js +6 -5
  304. package/lib/quoxon/rows.js +954 -0
  305. package/lib/quoxon/rowsStore.js +80 -0
  306. package/lib/quoxon/screenContext.js +150 -0
  307. package/lib/quoxon/screenFrames.js +254 -0
  308. package/lib/quoxon/serverChip.js +42 -0
  309. package/lib/quoxon/session.js +182 -2
  310. package/lib/quoxon/sessionDead.js +17 -0
  311. package/lib/quoxon/signin.js +125 -0
  312. package/lib/quoxon/slash/agents.js +112 -0
  313. package/lib/quoxon/slash/argSources.js +65 -0
  314. package/lib/quoxon/slash/args.js +65 -0
  315. package/lib/quoxon/slash/automate.js +89 -0
  316. package/lib/quoxon/slash/broadcast.js +140 -0
  317. package/lib/quoxon/slash/broadcastRun.js +108 -0
  318. package/lib/quoxon/slash/card.js +30 -0
  319. package/lib/quoxon/slash/challenge.js +114 -0
  320. package/lib/quoxon/slash/cliCommand.js +94 -0
  321. package/lib/quoxon/slash/cliLeaves.json +1023 -0
  322. package/lib/quoxon/slash/compact.js +1 -1
  323. package/lib/quoxon/slash/confirm.js +30 -0
  324. package/lib/quoxon/slash/crystallize.js +69 -0
  325. package/lib/quoxon/slash/dashboard.js +136 -0
  326. package/lib/quoxon/slash/draft.js +54 -0
  327. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  328. package/lib/quoxon/slash/hatWalk.js +206 -0
  329. package/lib/quoxon/slash/inverses.js +77 -0
  330. package/lib/quoxon/slash/killer.js +24 -0
  331. package/lib/quoxon/slash/killerApi.js +16 -0
  332. package/lib/quoxon/slash/ledger.js +13 -8
  333. package/lib/quoxon/slash/listView.js +3 -1
  334. package/lib/quoxon/slash/modelCmd.js +261 -0
  335. package/lib/quoxon/slash/palette.js +22 -8
  336. package/lib/quoxon/slash/paletteController.js +117 -7
  337. package/lib/quoxon/slash/panels.js +21 -6
  338. package/lib/quoxon/slash/picker.js +20 -2
  339. package/lib/quoxon/slash/qpeCmd.js +35 -0
  340. package/lib/quoxon/slash/restart.js +95 -13
  341. package/lib/quoxon/slash/resume.js +18 -1
  342. package/lib/quoxon/slash/rowCmd.js +157 -0
  343. package/lib/quoxon/slash/rowWalk.js +175 -0
  344. package/lib/quoxon/slash/settings.js +16 -6
  345. package/lib/quoxon/slash/spawn.js +177 -0
  346. package/lib/quoxon/slash/stepUp.js +274 -0
  347. package/lib/quoxon/slash/stepUpCard.js +60 -0
  348. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  349. package/lib/quoxon/slash/todoCmd.js +122 -0
  350. package/lib/quoxon/slash/undoApi.js +38 -0
  351. package/lib/quoxon/slash/undoCmd.js +49 -0
  352. package/lib/quoxon/slash/undoWish.js +15 -0
  353. package/lib/quoxon/slash/watch.js +164 -0
  354. package/lib/quoxon/slash/wish.js +23 -0
  355. package/lib/quoxon/slash/wishCmd.js +32 -0
  356. package/lib/quoxon/slotChat.js +300 -0
  357. package/lib/quoxon/slotFormat.js +195 -0
  358. package/lib/quoxon/slotRun.js +131 -0
  359. package/lib/quoxon/sound/mapping.js +2 -1
  360. package/lib/quoxon/stray.js +22 -0
  361. package/lib/quoxon/suggestCommand.js +90 -0
  362. package/lib/quoxon/term.js +46 -11
  363. package/lib/quoxon/textBuffer.js +176 -2
  364. package/lib/quoxon/theme.js +58 -25
  365. package/lib/quoxon/tips.js +433 -0
  366. package/lib/quoxon/toolCards.js +30 -0
  367. package/lib/quoxon/toolError.js +41 -0
  368. package/lib/quoxon/transcript.js +42 -4
  369. package/lib/quoxon/web/qface.js +18 -10
  370. package/lib/quoxon/welcome.js +1 -1
  371. package/lib/quoxon/whatsnew.js +136 -0
  372. package/lib/quoxon/whatsnew.jsonl +134 -0
  373. package/lib/quoxon/windowOrg.js +123 -0
  374. package/lib/server/devServers.js +137 -0
  375. package/lib/server/diag.js +129 -0
  376. package/lib/server/doctor.js +71 -0
  377. package/lib/server/install.js +149 -0
  378. package/lib/server/pluginEnable.js +161 -0
  379. package/lib/server/skew.js +56 -0
  380. package/lib/server/snapshot.js +131 -0
  381. package/lib/server/status.js +128 -0
  382. package/lib/server/sys.js +65 -0
  383. package/lib/server/uninstall.js +63 -0
  384. package/lib/server/update.js +234 -0
  385. package/lib/toolOutputShape.js +315 -0
  386. package/lib/tui/views/HyperionDeck.jsx +79 -0
  387. package/lib/tui/views/LucidDeck.jsx +209 -217
  388. package/package.json +5 -1
  389. package/vendor/dragonlab/SOURCE +4 -0
  390. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  391. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  392. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  393. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  394. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  395. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  396. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  397. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  398. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  399. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  400. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  401. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  402. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  403. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  404. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  405. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  406. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  407. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  408. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  409. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  410. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  411. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  412. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  413. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  414. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  415. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  416. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  417. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  418. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  419. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  420. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  421. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  422. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  423. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  424. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  425. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  426. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  427. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  428. package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
  429. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  430. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  431. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  432. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  433. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  434. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  435. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  436. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  437. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  438. package/vendor/dragonlab/lib/power-effects.js +294 -0
  439. package/vendor/dragonlab/lib/power-registry.js +74 -0
  440. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  441. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
  442. package/lib/core/websiteSession.js +0 -148
@@ -0,0 +1,180 @@
1
+ /**
2
+ * QLOOP-LIVE: the loop cards in the chat stream. One card per loop, updated in place; observer points and the finish line
3
+ * append as ordinary chat lines beneath it, so the history reads as the loop's story.
4
+ *
5
+ * Glue only (the logic is in loopCard.js and loopStream.js): it owns the watchers, the redraw throttle (<= 8 per second),
6
+ * the keys of a focused card and the finish moment. The host (app.js) supplies say / addItem / bump / dragon, which keeps this
7
+ * testable without Ink.
8
+ */
9
+ import { loopPinned, initLoop, applySnapshot, applyProgress, reduceLoop, setConn, observerLine, finishLines, startLine, createThrottle, REDRAW_MIN_MS, REDRAW_UNFOCUSED_MS, TERMINAL } from './loopCard.js';
10
+ import { watchLoop } from './loopStream.js';
11
+ import { objectiveMoment } from './dragonMoments.js';
12
+
13
+ const TICK_MS = 250; // a focused card's spinner and clock
14
+ const TICK_UNFOCUSED_EVERY = 4; // nobody on the card: every 4th tick (1 s), so idle cards cost little
15
+ const REFRESH_MIN_MS = 2000; // the runner's own turn is read from the snapshot at most this often per card
16
+ const REFRESH_CAP_MS = 1500; // and the finish never waits longer than this for the last read
17
+
18
+ /**
19
+ * The roster sees a loop finish (its row turns done) up to a minute BEFORE the stream's loop.finished reaches the card (the server
20
+ * drains receipts first), and its dragon then lands on a card that is still running and overflows the screen. A loop with a live card
21
+ * summons its dragon from the card, once, at its finish: the roster's watch skips those rows.
22
+ */
23
+ export const rowsWithoutLoopCards = (rows, has) => (rows || []).filter((r) => !(r?.kind === 'loop' && has(r.id)));
24
+
25
+ export function createLoopCards({ api, say, addItem, bump, dragon = null, now = Date.now, setTick = (f, ms) => { const t = setInterval(f, ms); t.unref?.(); return () => clearInterval(t); }, throttleTimer, watch = watchLoop, sleep } = {}) {
26
+ const cards = new Map(); // id -> ctrl
27
+ let focusId = null, cancelTick = null;
28
+ const throttle = createThrottle(() => bump(), { minMs: () => (focusId ? REDRAW_MIN_MS : REDRAW_UNFOCUSED_MS), now, ...(throttleTimer ? { timer: throttleTimer } : {}) });
29
+
30
+ const running = () => [...cards.values()].some((c) => !c.state.done);
31
+ /**
32
+ * A runner loop's stream `turn` counts model messages; its own turn is the snapshot's `iteration`. Read it again when a turn moves
33
+ * (at most every REFRESH_MIN_MS; a request inside the window is made by the next tick). -> a promise that settles when the read is over.
34
+ */
35
+ function refresh(c, force = false) {
36
+ if (!api.loopSnapshot || c.refreshing) return c.refreshing || Promise.resolve();
37
+ if (!force && now() - (c.lastSnapAt ?? -Infinity) < REFRESH_MIN_MS) { c.refreshDue = true; return Promise.resolve(); }
38
+ c.lastSnapAt = now(); c.refreshDue = false;
39
+ c.refreshing = Promise.resolve().then(() => api.loopSnapshot(c.id)).then((snap) => { c.state = applyProgress(c.state, snap); throttle.poke(); }, () => {}).finally(() => { c.refreshing = null; });
40
+ return c.refreshing;
41
+ }
42
+ let tickN = 0;
43
+ function tick() { // elapsed time and the spinner move once per tick, never faster than the throttle allows
44
+ if (!running()) { cancelTick?.(); cancelTick = null; return; }
45
+ if (!focusId && ++tickN % TICK_UNFOCUSED_EVERY) return;
46
+ for (const c of cards.values()) if (!c.state.done) { c.frame = (c.frame + 1) % 4; if (c.refreshDue) refresh(c); }
47
+ throttle.poke();
48
+ }
49
+ const ensureTick = () => { if (!cancelTick && running()) cancelTick = setTick(tick, TICK_MS); };
50
+
51
+ function finish(c, ev) {
52
+ if (!c.finishedSaid) { c.finishedSaid = true; for (const l of finishLines(c.state)) say(l.text, l.tone); }
53
+ c.confirm = false; // a focused card keeps its focus: Enter then expands the final output and Esc lets the frozen card go to scrollback
54
+ // The moment engine fires the dragon. Same call and same gates as a finished roster row (mode on, not typing, cooldown).
55
+ const st = c.state;
56
+ const m = !c.quiet && dragon?.summon && ['completed', 'completed_with_errors', 'failed'].includes(st.state)
57
+ ? objectiveMoment({ kind: 'loop', state: st.state === 'failed' ? 'failed' : 'done', name: st.title || st.id, activity: st.reason || '' }) : null;
58
+ if (m) Promise.resolve(dragon.summon(st.agent && typeof st.agent === 'string' ? st.agent : m.dragon, m.caption)).then((shown) => { if (shown) bump(); }, () => {});
59
+ return ev;
60
+ }
61
+
62
+ function onEvent(c, ev) {
63
+ const before = c.state;
64
+ c.state = reduceLoop(before, ev);
65
+ if (c.state === before) return;
66
+ const line = c.quiet ? null : observerLine(ev, c.state); // replaying a loop that was already over: the card tells it, the chat does not re-read it
67
+ if (line) { c.state.observed = [...c.state.observed, line.text].slice(-50); say(line.text, line.tone); }
68
+ if (ev.type === 'loop.turn.started' || ev.type === 'loop.turn.completed' || ev.type === 'loop.observe') refresh(c);
69
+ if (ev.type === 'loop.finished') { // the last read of the runner's turn first: "completed, N turns" must be the loop's count
70
+ c.finishing = true;
71
+ let t; const cap = new Promise((res) => { t = setTimeout(res, REFRESH_CAP_MS); t.unref?.(); });
72
+ Promise.race([refresh(c, true), cap]).then(() => { clearTimeout(t); finish(c, ev); throttle.poke(); });
73
+ }
74
+ throttle.poke();
75
+ }
76
+
77
+ async function run(c) {
78
+ const ctl = c.abort = new AbortController();
79
+ try {
80
+ const out = await watch({
81
+ api, id: c.id, signal: ctl.signal, sleep,
82
+ onSnapshot: (snap) => { c.state = applySnapshot(c.state, snap); if (c.state.done) c.quiet = true; throttle.poke(); }, // the finish line waits for the replay: it needs the receipts
83
+ onEvent: (ev) => onEvent(c, ev),
84
+ onConn: ({ conn, attempt, note }) => { if (!c.state.done) { c.state = setConn(c.state, conn, { attempt, note }); throttle.poke(); } },
85
+ });
86
+ if (out?.lost && !c.state.done) { c.state = setConn(c.state, 'lost', { attempt: 0, note: out.reason }); }
87
+ if (c.state.done && !c.finishedSaid && !c.finishing) { c.finishedSaid = true; for (const l of finishLines(c.state)) say(l.text, l.tone); } // finished before we looked (or the replay ended without a finish event)
88
+ } catch (e) {
89
+ c.state = setConn(c.state, 'lost', { note: e?.message || 'failed' });
90
+ } finally { throttle.poke(); }
91
+ }
92
+
93
+ return {
94
+ /** Put a live card in the chat stream for this loop and start following it. -> the card, or the existing one. */
95
+ watch(id, { title = '', agent = null, announce = true } = {}) {
96
+ if (!id) return null;
97
+ if (cards.has(id)) return cards.get(id);
98
+ const c = { id, state: initLoop({ id, title, agent }), expanded: false, focused: false, confirm: false, frame: 0, abort: null, finishedSaid: false };
99
+ cards.set(id, c);
100
+ if (announce) { const l = startLine(c.state); say(l.text, l.tone); } // the start line first, then the card, then the story beneath
101
+ addItem({ id: `loopcard-${id}`, kind: 'loopcard', card: c });
102
+ ensureTick();
103
+ run(c).then(() => bump());
104
+ return c;
105
+ },
106
+ /**
107
+ * A loop the chat just made (its `objective` tool answered `Loop "<title>" created ...`): the tool result carries no id, so look
108
+ * the loop up in the org's list (newest first) and put a card in this conversation for the live, unwatched one that matches
109
+ * the title. One retry, because the list can lag the tool's answer. -> ids watched.
110
+ */
111
+ async discover({ title = '' } = {}) {
112
+ if (!api.loopList) return [];
113
+ const want = String(title || '').replace(/…$/, '').trim().toLowerCase();
114
+ for (let attempt = 0; attempt < 2; attempt++) {
115
+ let rows = [];
116
+ try { const d = await api.loopList(); rows = Array.isArray(d) ? d : (d?.loops || []); } catch { rows = []; }
117
+ const hit = rows.filter((r) => {
118
+ const id = r.objective_id || r.id; if (!id || cards.has(id) || TERMINAL.has(r.state)) return false;
119
+ const age = now() - (Date.parse(String(r.created_at || '').replace(' ', 'T') + (/[zZ]|[+-]\d\d:?\d\d$/.test(String(r.created_at || '')) ? '' : 'Z')) || 0);
120
+ return age < 20 * 60 * 1000 && (!want || String(r.title || '').toLowerCase().startsWith(want));
121
+ });
122
+ if (hit.length) { const r = hit[0]; const id = r.objective_id || r.id; this.watch(id, { title: r.title || title }); return [id]; }
123
+ if (attempt === 0) await (sleep || ((ms) => new Promise((res) => setTimeout(res, ms))))(1500);
124
+ }
125
+ return [];
126
+ },
127
+ /** Read the runner's turn now (tests, and a card that was just focused). */
128
+ refresh: (id) => (cards.has(id) ? refresh(cards.get(id), true) : Promise.resolve()),
129
+ has: (id) => cards.has(id),
130
+ get: (id) => cards.get(id) || null,
131
+ all: () => [...cards.values()],
132
+ /** True while this history item must stay in the live region (commitLimit): its loop has not finished. */
133
+ held: (item) => Boolean(item?.kind === 'loopcard' && item.card && (!item.card.state.done || item.card.focused)), // a focused card stays live even when finished, so Enter / Esc work on it
134
+ anyRunning: running,
135
+ /** The cards drawn above the composer: running ones, and a finished one you are still on. */
136
+ pinned: () => [...cards.values()].filter(loopPinned),
137
+ /** The card the keys act on, or null. */
138
+ focused: () => (focusId && cards.get(focusId)) || null,
139
+ /** Focus the newest card that is still running (or the named one). -> the card or null. */
140
+ focus(id = null) {
141
+ const pick = id ? cards.get(id) : [...cards.values()].reverse().find((c) => !c.state.done) || [...cards.values()].pop();
142
+ if (!pick) return null;
143
+ const old = this.focused(); if (old) { old.focused = false; old.confirm = false; }
144
+ pick.focused = true; focusId = pick.id; throttle.poke(); bump(); return pick;
145
+ },
146
+ blur() { const c = this.focused(); if (c) { c.focused = false; c.confirm = false; } focusId = null; bump(); },
147
+ /**
148
+ * Keys while a card is focused. Enter toggles the expanded spool, s asks to halt (y confirms, anything else cancels),
149
+ * Esc collapses and then leaves. -> true when the key was consumed.
150
+ */
151
+ key(name, text) {
152
+ const c = this.focused(); if (!c) return false;
153
+ if (c.confirm) {
154
+ c.confirm = false;
155
+ if (text === 'y') this.halt(c.id).then(() => bump(), () => bump()); else say('Halt cancelled.');
156
+ bump(); return true;
157
+ }
158
+ if (name === 'enter') { c.expanded = !c.expanded; bump(); return true; }
159
+ if (name === 'escape') { if (c.expanded) c.expanded = false; else this.blur(); bump(); return true; }
160
+ if (text === 's' && !c.state.done) { c.confirm = true; bump(); return true; }
161
+ if (name === 'up' || name === 'down' || name === 'tab') { // cycle between cards when there are several
162
+ const ids = [...cards.keys()]; if (ids.length > 1) { const i = ids.indexOf(c.id); this.focus(ids[(i + (name === 'up' ? ids.length - 1 : 1)) % ids.length]); }
163
+ return true;
164
+ }
165
+ if (text) { this.blur(); return false; } // ordinary typing goes to the draft
166
+ return true;
167
+ },
168
+ /** The halt goes through the same governed route as the roster's s: after an explicit y from the human, never silently. */
169
+ async halt(id) {
170
+ const c = cards.get(id); if (!c) return { ok: false, message: `no loop card for ${id}` };
171
+ if (!api.stopRow) { say('Halt is not available on this server.', 'warn'); return { ok: false }; }
172
+ const r = await api.stopRow({ source: 'objectives', kind: 'loop', id, name: c.state.title || id });
173
+ say(r.message || (r.ok ? 'Halt sent.' : 'Halt refused.'), r.ok ? undefined : 'warn');
174
+ return r;
175
+ },
176
+ dispose() { for (const c of cards.values()) c.abort?.abort(); cards.clear(); cancelTick?.(); cancelTick = null; throttle.dispose(); },
177
+ };
178
+ }
179
+
180
+ export { TERMINAL };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * QLOOP-LIVE: the client for a running loop. Reads GET /api/v1/loops/:id (snapshot) and
3
+ * GET /api/v1/loops/:id/events (SSE; ?since=<event_seq> resumes; a heartbeat comment every 15 s; ends after loop.finished).
4
+ * Contract: ~/quoxon-work/qloop-live-design.md section 1. Every event carries a monotonic event_seq.
5
+ *
6
+ * Reconnect is the part that must not lie. A drop is counted, backed off (500 ms doubling to 10 s, reset by any event) and
7
+ * reported to the card as 'reconnecting'; after MAX_FAILS drops in a row with no event between, or at once on 401/403/404,
8
+ * the watcher stops and says 'lost' with the reason. It never reports a lost stream as a finished loop.
9
+ */
10
+ export const BACKOFF_START_MS = 500;
11
+ export const BACKOFF_CAP_MS = 10000;
12
+ export const MAX_FAILS = 8;
13
+ const FATAL = new Set([401, 403, 404]);
14
+ const TERMINAL_STATES = new Set(['completed', 'completed_with_errors', 'failed', 'interrupted', 'halted', 'cancelled', 'aborted', 'stopped']);
15
+ const isTerminal = (snap) => Boolean(snap && (snap.finished === true || TERMINAL_STATES.has(snap.state)));
16
+
17
+ const sleepFor = (ms, signal) => new Promise((resolve) => {
18
+ if (signal?.aborted) return resolve();
19
+ const t = setTimeout(done, ms); t.unref?.();
20
+ function done() { signal?.removeEventListener?.('abort', done); clearTimeout(t); resolve(); }
21
+ signal?.addEventListener?.('abort', done, { once: true });
22
+ });
23
+
24
+ /**
25
+ * Follow one loop until loop.finished, abort, or a fatal failure.
26
+ *
27
+ * api: { loopSnapshot(id) -> object, loopEvents(id, { since }, signal) -> async iterable of events }
28
+ * handlers: { onSnapshot(snap), onEvent(ev), onConn({ conn: 'live'|'reconnecting'|'lost', attempt, note }) }
29
+ * -> { finished: boolean, lost: boolean, lastSeq, reason? }
30
+ */
31
+ export async function watchLoop({ api, id, signal, since = 0, onSnapshot = () => {}, onEvent = () => {}, onConn = () => {}, sleep = sleepFor, backoffStart = BACKOFF_START_MS, backoffCap = BACKOFF_CAP_MS, maxFails = MAX_FAILS }) {
32
+ let lastSeq = Number(since) || 0, fails = 0, delay = backoffStart, finished = false;
33
+ try { onSnapshot(await api.loopSnapshot(id)); }
34
+ catch (e) { // the snapshot is a head start, not a gate: the replayed events carry the same facts. A fatal status still stops.
35
+ if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: 0, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
36
+ }
37
+ while (!signal?.aborted && !finished) {
38
+ let sawEvent = false;
39
+ try {
40
+ for await (const ev of api.loopEvents(id, { since: lastSeq || undefined }, signal)) {
41
+ if (signal?.aborted) break;
42
+ if (!ev || typeof ev.type !== 'string') continue;
43
+ const seq = Number(ev.event_seq);
44
+ if (Number.isFinite(seq) && seq <= lastSeq) continue; // the boundary event of a resume
45
+ if (Number.isFinite(seq)) lastSeq = seq;
46
+ if (!sawEvent) { sawEvent = true; fails = 0; delay = backoffStart; onConn({ conn: 'live', attempt: 0, note: null }); }
47
+ onEvent(ev);
48
+ if (ev.type === 'loop.finished') { finished = true; break; }
49
+ }
50
+ if (!finished && !signal?.aborted && !sawEvent) {
51
+ // A clean close with no events is not a failure: a loop's silent final turn, or a loop that already finished, ends this way.
52
+ // Ask the snapshot. Terminal: finish quietly with the final state. Still running: reconnect with ?since, failures reset.
53
+ // Only a snapshot read that itself fails counts (a real error).
54
+ let snap = null;
55
+ try { snap = await api.loopSnapshot(id); }
56
+ catch (e) {
57
+ if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
58
+ fails += 1;
59
+ if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
60
+ onConn({ conn: 'reconnecting', attempt: fails, note: describe(e) });
61
+ await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
62
+ continue;
63
+ }
64
+ if (isTerminal(snap)) { onSnapshot(snap); return { finished: true, lost: false, lastSeq, final: snap }; }
65
+ fails = 0; // still running: quiet, not a failure
66
+ await sleep(backoffStart, signal);
67
+ continue;
68
+ }
69
+ else if (!finished && sawEvent) fails = 0;
70
+ } catch (e) {
71
+ if (signal?.aborted) break;
72
+ if (FATAL.has(e?.status)) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
73
+ fails += 1;
74
+ if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: describe(e) }); return { finished: false, lost: true, lastSeq, reason: describe(e) }; }
75
+ onConn({ conn: 'reconnecting', attempt: fails, note: describe(e) });
76
+ await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
77
+ continue;
78
+ }
79
+ if (finished || signal?.aborted) break;
80
+ if (fails >= maxFails) { onConn({ conn: 'lost', attempt: fails, note: 'the stream keeps closing' }); return { finished: false, lost: true, lastSeq, reason: 'the stream keeps closing' }; }
81
+ onConn({ conn: 'reconnecting', attempt: Math.max(1, fails), note: null });
82
+ await sleep(delay, signal); delay = Math.min(delay * 2, backoffCap);
83
+ }
84
+ return { finished, lost: false, lastSeq };
85
+ }
86
+
87
+ const describe = (e) => `${e?.status ? `HTTP ${e.status} ` : ''}${e?.message || 'failed'}`.trim();
@@ -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, minGapMsFrom, 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.4.0';
139
43
  try { version = resolveCliVersion?.() || version; } catch { /* keep default */ }
140
44
  if (opts.print) return mainPrint(opts);
141
45
  let replay = null;
@@ -152,12 +56,23 @@ export async function main(argv = process.argv.slice(2)) {
152
56
  // and a fresh install replays the original request as the first real turn
153
57
  replay = r.replay || null;
154
58
  fromGettingStarted = true;
59
+ // QUOXON-Q: ONE offer for a short `q` command, asked once (answer kept in the CLI config)
60
+ if (process.stdin.isTTY && process.stdout.isTTY && !opts.demoOnboarding) {
61
+ try {
62
+ const { offerShortcut } = await import('./onboarding/shortcutOffer.js');
63
+ const { createInterface } = await import('node:readline/promises');
64
+ const config = (await import('../core/config.js')).loadConfig();
65
+ await offerShortcut({ config, ask: async (line) => { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { return await rl.question(`\n${line} `); } finally { rl.close(); } } });
66
+ } catch { /* an offer must never block the first conversation */ }
67
+ }
155
68
  }
156
69
  let props;
157
70
  let restart = null; // set by /restart: run the installed quox here once the app has let go of the terminal
158
71
  if (opts.demo) {
72
+ 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
73
  // /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 };
74
+ // QUOX_DEMO_DECISIONS: demo-only pty test seam for the walkthrough proofs (a decisions state file; never a real server)
75
+ 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
76
  } else {
162
77
  const cfgMod = await import('../core/config.js');
163
78
  const { createAuth } = await import('../core/auth.js');
@@ -168,77 +83,103 @@ export async function main(argv = process.argv.slice(2)) {
168
83
  const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
169
84
  const pick = pickProfile(opts.profile, cfgMod);
170
85
  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 });
86
+ const { createWindowClient, carryOrg, RESTART_ORG_ENV } = await import('./windowOrg.js');
87
+ const win = createWindowClient({ config, auth: createAuth(config), createClient }); // #1034: /org switch is window-scoped
88
+ const api = createApi({ ...win, config });
174
89
  const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
175
90
  const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
91
+ // Z2H: a dead session (refresh rejected) or a cleared login on a human profile is ONE signed-out state, answered
92
+ // by a single card and /login, not scattered digest/approvals/chat errors. Transient failures read as alive.
93
+ let signedOut = null;
94
+ if (!missing && !opts.serviceKey) {
95
+ if (problem && !pick.note) signedOut = { reason: null };
96
+ else if (!problem) {
97
+ const pr = await Promise.race([api.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
98
+ if (!pr.alive) signedOut = { reason: pr.reason || null };
99
+ }
100
+ }
176
101
  // The health probe runs inside the app (background); the UI is up and typing works immediately.
177
102
  const startup = async () => {
103
+ if (signedOut) return null;
178
104
  if (problem) return problem.message;
179
105
  if (pick.note) return pick.note;
180
106
  const hl = await api.health(8000);
181
107
  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
108
  };
109
+ // /restart hand-over: re-enter the predecessor window's org in this window only (the profile is untouched).
110
+ const carried = !missing && !signedOut && !problem ? await carryOrg(api, process.env) : (delete process.env.QUOX_RESTART_ORG, null);
111
+ 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
112
  const saved = api.settings();
184
113
  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 };
114
+ if (!opts.verboseSet) opts.verbose = ['off', 'on', 'full'].includes(saved.verbose) ? saved.verbose : 'off'; // protocol lines are opt-in: /verbose on
115
+ // PROFILE-SWITCH: build a signed-in profile's api for an in-place window switch (the App only asks after a y or
116
+ // /profile). The window org rides along; the profile file's default is never moved (loadConfig(name) is a lens).
117
+ const residentNames = () => (cfgMod.loadConfig().listResidentProfiles?.() || []).map(r => r.name);
118
+ const switchProfile = async (name, { org: winOrg = null } = {}) => {
119
+ const row = (cfgMod.loadConfig().listProfiles?.() || []).find(p => p.name === name);
120
+ if (!row || !row.loggedIn) return { ok: false, reason: 'not_signed_in' };
121
+ const c2 = cfgMod.loadConfig(name);
122
+ const a2 = createApi({ ...createWindowClient({ config: c2, auth: createAuth(c2), createClient }), config: c2 });
123
+ const pr = await Promise.race([a2.probeSession(), new Promise(r => setTimeout(() => r({ alive: true }), 4000))]).catch(() => ({ alive: true }));
124
+ if (!pr.alive) return { ok: false, reason: 'not_signed_in' };
125
+ const c = winOrg ? await carryOrg(a2, { [RESTART_ORG_ENV]: winOrg }) : null;
126
+ const note = c?.error ? `Could not re-enter org ${winOrg} (${c.error}); this window is in ${name}'s org.` : null;
127
+ return { ok: true, api: a2, agentLogin: residentNames().includes(name), orgName: (c && !c.error && c.name) || c2.getAuth?.()?.org_name || null, note };
128
+ };
129
+ const loginCandidates = profiles.filter(p => !p.loggedIn && !residentNames().includes(p.name)).map(p => p.name);
130
+ 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(), minGapMs: minGapMsFrom() }), 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
131
  }
188
132
  // Interactive start: a clean screen under the intro (never when piped; NO_COLOR still clears),
189
133
  // except straight after Getting started, whose finale must stay readable.
190
134
  if (shouldClearBeforeChat({ isTTY: process.stdout.isTTY, noClear: opts.noClear, fromGettingStarted })) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
135
+ // Z2H-R5 C: do not advertise /slot where the licence check says no (the server's own capabilities answer, short wait).
136
+ let slotsAvailable = true;
137
+ if (!opts.demo && process.stdout.isTTY && props.api?.capabilities && !props.signedOut && !props.blocked) {
138
+ try {
139
+ const caps = await Promise.race([props.api.capabilities(), new Promise(res => setTimeout(() => res(null), 2000))]);
140
+ const c = caps?.ok ? caps.capabilities.find(x => x.id === 'slots') : null;
141
+ if (c && c.availability === 'unavailable' && /not in your plan/i.test(String(c.reason || ''))) slotsAvailable = false;
142
+ } catch { /* unknown: leave the invite as it was */ }
143
+ }
191
144
  const welcome = shouldShowWelcome({ print: opts.print });
192
145
  if (welcome) markWelcomeSeen();
193
146
  const transcript = opts.demo ? null : createTranscript({ meta: { version, cols: process.stdout.columns, rows: process.stdout.rows, profile: props.profile, org: props.org, cwd } });
194
147
  const onResize = () => transcript?.resize(process.stdout.columns, process.stdout.rows);
195
148
  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); }
149
+ // SCREEN-FRAMES: what the screen showed, settled, into the same local transcript (QUOX_SCREEN_FRAMES=0 turns it off).
150
+ const screenRec = transcript && process.stdout.isTTY && screenFramesEnabled(process.env)
151
+ ? createScreenRecorder({ write: (rec) => transcript.screen(rec), cols: () => process.stdout.columns, rows: () => process.stdout.rows })
152
+ : null;
153
+ const restartSlot = opts.demo ? null : (process.env[RESTART_SLOT_ENV] || null); delete process.env[RESTART_SLOT_ENV]; // one hand-over only
154
+ const restartAttach = opts.demo ? null : (process.env[RESTART_ATTACH_ENV] || null); delete process.env[RESTART_ATTACH_ENV];
155
+ 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
156
+ 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
157
+ if (restartNote && !props.introNote) props.introNote = restartNote;
158
+ 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 }));
159
+ try { await app.waitUntilExit(); screenRec?.stop(); transcript?.end(restart ? 'restart' : 'exit'); }
160
+ catch (e) { screenRec?.stop(); transcript?.end(`crash: ${e?.message || e}`); throw e; }
161
+ finally { screenRec?.stop(); process.stdout.off('resize', onResize); }
200
162
  if (!restart) return;
163
+ if (restart.update) { // /update said y: the update owns the terminal (npm prompts, sudo), then the same restart either way
164
+ const { runUpdate } = await import('../core/selfUpdate.js');
165
+ const plan = restart.update;
166
+ restoreTerminal();
167
+ process.stdout.write(`\nUpdating quox: ${plan.command.display}\n`);
168
+ const u = runUpdate(plan);
169
+ if (u.ok) process.stdout.write(`\u2714 Update finished (exit 0). Restarting into this conversation\u2026\n`);
170
+ else {
171
+ 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}`;
172
+ process.stdout.write(`\u2716 ${note}\n`);
173
+ process.env[RESTART_NOTE_ENV] = note; // the restarted client (same build) says it again in the conversation
174
+ }
175
+ }
201
176
  // 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 });
177
+ 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 };
178
+ // QUOXON-RESTART-LEAK: under the launcher this process EXITS and the launcher runs the successor, so exactly one
179
+ // client stays alive per terminal. Without one (QUOX_NO_LAUNCHER=1) it falls back to waiting on a nested child.
180
+ const launched = takeLauncherHandoff();
181
+ if (launched && writeRestartHandoff(launched, restartPlan(how))) { restoreTerminal(); process.exitCode = RESTART_EXIT_CODE; return; }
182
+ const r = execRestart(how);
203
183
  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
184
  process.exitCode = r.code;
205
185
  }
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
- }