quoxcli 0.3.1 → 0.5.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 (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -18,6 +18,7 @@ import * as T from './copy.js';
18
18
  import { matchIntent, yesNo, parseConnect } from './intent.js';
19
19
  import { loadJourney, saveJourney, clearJourney, resumeStep } from './journey.js';
20
20
  import { pendingRows, runPreflight, preflightPassed, detectOllama, defaultDeps } from './preflight.js';
21
+ import { probeLocalQuox } from './probe.js';
21
22
  import { credentialOptions, KEY_PROVIDERS, normaliseOllamaUrl, nextSelectable } from './credential.js';
22
23
  import { needsConsent, providerLabel } from '../consent.js';
23
24
  import { HelloIntro, helloIntroMs } from '../qmotion.js';
@@ -101,8 +102,8 @@ const Hint = ({ text, cols }) => h(Box, { paddingLeft: 2 }, h(Text, { color: c('
101
102
 
102
103
  // ---- the view ---------------------------------------------------------------------------------
103
104
  export function GettingStarted({
104
- columns, version = '0.3.1', cwd = '', welcome = false, animateIntro, earlyInput = null,
105
- journeyPath, dryRun = false, runner = null, preflightDeps, runChecks = runPreflight, findOllama = detectOllama,
105
+ columns, version = '0.5.0', cwd = '', welcome = false, animateIntro, earlyInput = null,
106
+ journeyPath, dryRun = false, runner = null, preflightDeps, runChecks = runPreflight, findLocal = probeLocalQuox, findOllama = detectOllama,
106
107
  requestCredential, broker = null, finisher = null, onExit = () => {}, now = () => Date.now()
107
108
  }) {
108
109
  const { stdin, setRawMode, isRawModeSupported } = useStdin();
@@ -179,7 +180,7 @@ export function GettingStarted({
179
180
 
180
181
  function offerLines() {
181
182
  const items = [{ kind: 'guide', text: T.OFFER }];
182
- items.push({ kind: 'ask', text: T.OFFER_ASK }, { kind: 'dim', text: T.ALREADY });
183
+ items.push({ kind: 'ask', text: T.OFFER_ASK }, { kind: 'dim', text: T.CHOICES }, { kind: 'dim', text: T.ALREADY });
183
184
  push(...items);
184
185
  set({ step: 'offer' });
185
186
  persist();
@@ -218,6 +219,10 @@ export function GettingStarted({
218
219
  if (text.startsWith('/')) { onSlash(text); return; }
219
220
  if (s.step === 'ask') return onRequest(text);
220
221
  if (s.step === 'offer' || s.step === 'declined') {
222
+ const pick = text.toLowerCase();
223
+ if (pick === 'a') { push({ kind: 'user', text }); startPreflight(); return; } // install here: the same flow as yes
224
+ if (pick === 'b') { push({ kind: 'user', text }, { kind: 'dim', text: T.LOCAL_CHECKING }); useLocal(); return; }
225
+ if (pick === 'c') { push({ kind: 'user', text }, { kind: 'guide', text: T.CONNECT_ELSEWHERE }); return; }
221
226
  const yn = yesNo(text);
222
227
  if (yn === true) { push({ kind: 'user', text: text || 'yes' }); startPreflight(); return; }
223
228
  if (yn === false) { push({ kind: 'user', text }, { kind: 'guide', text: T.DECLINED }, { kind: 'dim', text: T.DECLINED_HINT }); set({ step: 'declined' }); persist(); return; }
@@ -225,6 +230,14 @@ export function GettingStarted({
225
230
  }
226
231
  }
227
232
 
233
+ async function useLocal() {
234
+ const f = await findLocal();
235
+ if (!live.current || S.current.done) return;
236
+ if (!f.found) { push({ kind: 'guide', tone: 'warn', text: T.LOCAL_NONE }); return; }
237
+ push({ kind: 'guide', tone: 'ok', text: T.localFound(f.url) }, { kind: 'guide', text: T.connectChecking(f.url) });
238
+ exit({ action: 'connect', url: f.url });
239
+ }
240
+
228
241
  async function startPreflight() {
229
242
  set({ step: 'preflight', rows: pendingRows(), fixNote: null });
230
243
  persist();
@@ -5,10 +5,32 @@ export const orgRows = (list) => (Array.isArray(list) ? list : []).map((o) => ({
5
5
 
6
6
  /**
7
7
  * Resolve a typed name or id against the user's orgs. Exact id, exact slug/name, then every word of the
8
- * query found in the name or slug (so "hot tubs hampshire" finds "Hot Tubs Hampshire Ltd"). Never guesses between ties.
8
+ * query found in the name or slug (so "acme widgets" finds "Acme Widgets Ltd"). Never guesses between ties.
9
9
  * -> { org } | { ambiguous: [orgs] } | { none: true }
10
10
  */
11
+ const VERBS = new Set(['switch', 'swith', 'swich', 'sw', 'to', 'go', 'use']);
12
+ /** Drop ONE leading verb-ish token ("swith quox dev" -> "quox dev"); a lone verb is kept (it may be the name). */
13
+ export function stripOrgVerb(query) {
14
+ const m = /^(\S+)\s+(\S[\s\S]*)$/.exec(String(query ?? '').trim());
15
+ return m && VERBS.has(m[1].toLowerCase()) ? m[2].trim() : String(query ?? '').trim();
16
+ }
17
+
18
+ /** Orgs for a picker when nothing matched uniquely: those hitting the most query words (verb stripped), at least one. */
19
+ export function orgCandidates(query, list) {
20
+ const words = norm(stripOrgVerb(query)).split(' ').filter(Boolean);
21
+ const scored = orgRows(list).map((o) => { const hay = `${norm(o.name)} ${norm(o.slug)}`; return { o, n: words.filter((w) => hay.includes(w)).length }; });
22
+ const best = Math.max(0, ...scored.map((x) => x.n));
23
+ return best ? scored.filter((x) => x.n === best).map((x) => x.o) : [];
24
+ }
25
+
11
26
  export function matchOrg(query, list) {
27
+ const first = matchOrgExact(query, list);
28
+ if (!first.none) return first;
29
+ const stripped = stripOrgVerb(query);
30
+ return stripped !== String(query ?? '').trim() ? matchOrgExact(stripped, list) : first;
31
+ }
32
+
33
+ function matchOrgExact(query, list) {
12
34
  const rows = orgRows(list);
13
35
  const q = norm(query);
14
36
  if (!q) return { none: true };
@@ -31,3 +53,29 @@ export function orgLines(list, currentId) {
31
53
  const w = Math.min(36, Math.max(4, ...rows.map((o) => [...o.name].length)));
32
54
  return rows.map((o) => `${o.id === currentId ? '* ' : ' '}${o.name.padEnd(w)} ${o.slug || o.id}${o.role ? ` (${o.role})` : ''}`);
33
55
  }
56
+
57
+ /** `quox org switch` with no argument: the same list, numbered from 1, current org marked. */
58
+ export function numberedOrgLines(list, currentId) {
59
+ const rows = orgRows(list);
60
+ const w = Math.min(36, Math.max(4, ...rows.map((o) => [...o.name].length)));
61
+ return rows.map((o, i) => `${o.id === currentId ? '*' : ' '} ${String(i + 1).padStart(String(rows.length).length)}) ${o.name.padEnd(w)} ${o.slug || o.id}${o.role ? ` (${o.role})` : ''}`);
62
+ }
63
+
64
+ /** A typed answer to the numbered list: an exact slug/name/id first (so a numeric slug is never shadowed), then a 1-based number. */
65
+ export function pickOrgByInput(input, list) {
66
+ const rows = orgRows(list);
67
+ const t = String(input ?? '').trim();
68
+ if (!t) return { none: true };
69
+ const m = matchOrg(t, rows);
70
+ const exactly = m.org && (m.org.slug === t || String(m.org.id).toLowerCase() === t.toLowerCase() || norm(m.org.name) === norm(t));
71
+ if (exactly) return m;
72
+ if (/^\d+$/.test(t)) { const n = Number(t); return n >= 1 && n <= rows.length ? { org: rows[n - 1] } : { none: true }; }
73
+ return m;
74
+ }
75
+
76
+ /** After a profile-wide `quox org switch`: how the other windows on that profile pick the change up. */
77
+ export function sharedSwitchHint({ name, pids }) {
78
+ const n = pids.length;
79
+ return `${n} other quox session${n === 1 ? '' : 's'} ${n === 1 ? 'uses' : 'use'} profile "${name}" (pid ${pids.join(', ')}). `
80
+ + 'They pick this org up on their next token refresh; run /restart in a window to pick it up now. /org <name> moves one window only.';
81
+ }
@@ -0,0 +1,51 @@
1
+ // QUOXON plan approval card (owner 2026-10-26 18:05: "i would deny based on the fact it's not even presenting a plan"):
2
+ // an objective's "approve plan" item must list the plan's steps, and show the org the item really lives in.
3
+ // Pure helpers plus one async enrich step; the card renders what this returns and nothing else.
4
+
5
+ export const PLAN_STEPS_MAX = 8;
6
+ /** Verbs that change things. Anything else reads as read-only (inspect, list, audit, report). */
7
+ const CHANGES = /\b(upgrad\w*|updat\w*|restart\w*|redeploy\w*|deploy\w*|writ\w*|delet\w*|remov\w*|install\w*|migrat\w*|patch\w*|rollback|roll back|reboot\w*|creat\w*|apply|applies|rotat\w*|scal\w*|provision\w*|stop|start|kill|drop|reset|modify|modifies|change\w*|configur\w*)\b/i;
8
+ export const changesThings = (title) => CHANGES.test(String(title || ''));
9
+
10
+ /** Does this inbox item ask for approval of an objective's plan? */
11
+ export const isPlanApproval = (it) => it?.payload?.source === 'objective_checkpoint'
12
+ && it?.payload?.checkpoint_type === 'plan_approval' && typeof it?.payload?.objective_id === 'string';
13
+
14
+ /** Steps from an objective read: [{ n, title, agent, writes }]. Agent display name from `names` (id -> name), else the id. */
15
+ export function stepsOf(objective, names = new Map()) {
16
+ const tasks = Array.isArray(objective?.tasks) ? objective.tasks : [];
17
+ return tasks.filter(t => t && t.title).map((t, i) => ({
18
+ n: i + 1, title: String(t.title), agent: t.agent_id ? String(names.get(t.agent_id) || t.agent_id) : null, writes: changesThings(t.title)
19
+ }));
20
+ }
21
+
22
+ /** Rows for the card: numbered lines plus "+N more" past the cap. Each row is a plain string. */
23
+ export function planLines(steps, max = PLAN_STEPS_MAX) {
24
+ const list = Array.isArray(steps) ? steps : [];
25
+ const lines = list.slice(0, max).map(s => `${s.n}. ${s.writes ? '[changes]' : '[read-only]'} ${s.title}${s.agent ? ` · ${s.agent}` : ''}`);
26
+ if (list.length > max) lines.push(`+${list.length - max} more`);
27
+ return lines;
28
+ }
29
+
30
+ /**
31
+ * Attach { steps, orgId, orgName } as item._plan for a plan-approval item. Reads the objective once (cached by objective id
32
+ * in `cache`). Never throws: when the read fails the item is returned as it came and the card says the plan could not be loaded.
33
+ */
34
+ export async function enrichPlanItem(item, { objectiveGet, agents = null, orgs = null, cache = new Map() } = {}) {
35
+ if (!isPlanApproval(item) || typeof objectiveGet !== 'function') return item;
36
+ const id = item.payload.objective_id;
37
+ if (!cache.has(id)) {
38
+ cache.set(id, (async () => {
39
+ try {
40
+ const o = await objectiveGet(id);
41
+ let names = new Map();
42
+ try { for (const a of (agents ? await agents() : []) || []) if (a?.agent_id && a.name) names.set(a.agent_id, a.name); } catch { /* ids stand in */ }
43
+ const orgId = item.org_id || o?.org_id || null;
44
+ let orgName = null;
45
+ try { orgName = orgId && orgs ? ((await orgs()) || []).find(x => x?.id === orgId)?.name || null : null; } catch { /* the short id stands in */ }
46
+ return { steps: stepsOf(o, names), orgId, orgName, loaded: true };
47
+ } catch (e) { cache.delete(id); return { steps: [], orgId: item.org_id || null, orgName: null, loaded: false, error: e?.message || 'failed' }; }
48
+ })());
49
+ }
50
+ return { ...item, _plan: await cache.get(id) };
51
+ }
@@ -4,6 +4,7 @@
4
4
  * Exit codes: 0 completed, 2 held for approval, 3 interrupted, 4 auth, 5 unconfirmed
5
5
  * (stream closed without turn.finished), 1 other.
6
6
  */
7
+ import { eventLine } from '../core/announce.js';
7
8
  import { textGate } from './events.js';
8
9
  import { streamFields, fallbackNote } from './model.js';
9
10
  import { connectOfferFor } from './connect.js';
@@ -46,7 +47,7 @@ export async function runPrint({ api, prompt, agent = 'quox', output = 'text', c
46
47
  const sum = { tools: 0, approvals: 0, receipts: [] };
47
48
  let convo = conversation, doneSession = null, status = null, text = '', sawApproval = false, errorMsg = null, authFail = false;
48
49
  const seen = new Set();
49
- let offer = null, handoff = null;
50
+ let offer = null, handoff = null, objective = null;
50
51
  let gate = null;
51
52
  try {
52
53
  for await (const ev of api.stream({ turnId, message: prompt, agent: agent || 'quox', conversationId: conversation, context, ...streamFields(choice), ...(promptProfile ? { promptProfile } : {}), ...(timeoutS > 0 ? { turnBudgetMs: timeoutS * 1000 } : {}) }, ctrl.signal)) {
@@ -60,8 +61,10 @@ export async function runPrint({ api, prompt, agent = 'quox', output = 'text', c
60
61
  else if (ev.type === 'done' && !text && ev.full_response) text = ev.full_response;
61
62
  else if (ev.type === 'tool.started' && !seen.has(ev.call_id)) { seen.add(ev.call_id); sum.tools++; }
62
63
  else if (ev.type === 'tool.finished') { if (!seen.has(ev.call_id)) { seen.add(ev.call_id); sum.tools++; } if (ev.receipt?.ward_seq != null) sum.receipts.push(ev.receipt.ward_seq); }
64
+ else if (ev.type === 'quox_event') { if (output !== 'json') { const q = eventLine(ev); if (q) err(`${q}\n`); } } // QUOX-ANNOUNCE A2: stdout stays the answer
63
65
  else if (ev.type === 'approval.requested') { sawApproval = true; sum.approvals++; }
64
66
  else if (ev.type === 'approval.resolved') sawApproval = false;
67
+ else if (ev.type === 'turn.objective_suggested' && ev.objective_suggested?.goal) objective = ev.objective_suggested; // QUOXON objective offer
65
68
  else if (ev.type === 'turn.handoff_suggested' && ev.handoff_suggested?.agent) handoff = ev.handoff_suggested; // QUOXON handoff offer
66
69
  else if (ev.type === 'turn.finished') status = ev.status || 'completed';
67
70
  else if (ev.type === 'error') errorMsg = isConsentError(ev) ? consentPrintMessage(ev, profile) : (ev.error || 'stream error');
@@ -85,7 +88,7 @@ export async function runPrint({ api, prompt, agent = 'quox', output = 'text', c
85
88
  if (!status) status = authFail ? 'auth' : 'failed';
86
89
 
87
90
  if (output === 'json') {
88
- out(JSON.stringify({ type: 'summary', status, turn_id: turnId, conversation_id: convo, duration_ms: Date.now() - t0, server_stopped: serverStopped, ...sum, ...(handoff ? { handoff_suggested: handoff } : {}) }) + '\n');
91
+ out(JSON.stringify({ type: 'summary', status, turn_id: turnId, conversation_id: convo, duration_ms: Date.now() - t0, server_stopped: serverStopped, ...sum, ...(handoff ? { handoff_suggested: handoff } : {}), ...(objective ? { objective_suggested: objective } : {}) }) + '\n');
89
92
  } else if (text) out(text.endsWith('\n') ? text : text + '\n');
90
93
  if (authFail) err(`Not signed in for chat. Run: quox login -p <profile>\n`);
91
94
  else if (code === EXIT.approval) err('Held for approval: approve it in the dashboard or with `quox` /approvals.\n');
@@ -93,6 +96,8 @@ export async function runPrint({ api, prompt, agent = 'quox', output = 'text', c
93
96
  else if (errorMsg) err(`quox: ${errorMsg}\n`);
94
97
  if (offer && code !== EXIT.ok) err(`${offer}\n`);
95
98
  // QUOXON handoff offer: no y/N here, so name the exact one-turn command (same question, same conversation).
96
- if (handoff && output !== 'json' && code === EXIT.ok) err(`quox: ${handoff.reason ? `${String(handoff.reason).replace(/\s+$/, '')} ` : ''}${handoff.name || handoff.agent} can answer it: quox --print --agent ${handoff.agent}${convo ? ` --conversation ${convo}` : ''} ${JSON.stringify(prompt)}\n`);
99
+ // QUOXON objective offer: no y/N here, so name the exact command that makes it an objective.
100
+ if (objective && output !== 'json' && code === EXIT.ok) err(`quox: ${objective.reason ? `${String(objective.reason).replace(/\s+$/, '')} ` : ''}Run it until done: quox objective create ${JSON.stringify(objective.goal)} --checkpoint\n`);
101
+ else if (handoff && output !== 'json' && code === EXIT.ok) err(`quox: ${handoff.reason ? `${String(handoff.reason).replace(/\s+$/, '')} ` : ''}${handoff.name || handoff.agent} can answer it: quox --print --agent ${handoff.agent}${convo ? ` --conversation ${convo}` : ''} ${JSON.stringify(prompt)}\n`);
97
102
  return code;
98
103
  }
@@ -23,6 +23,13 @@ export const switchAskLine = (profile, human) => askText('Switch?', {
23
23
  action: `switch this window to your login '${human}' (same conversation, attach, slot and groups)`,
24
24
  });
25
25
 
26
+ /** The startup offer on an agent login (owner 2026-10-28): restart as the human profile; /profile lists the rest. */
27
+ export const startupSwitchAskLine = (profile, human, others = 0) => askText(`Restart as you (${human})?`, {
28
+ title: `This window would run as the agent '${profile}'.`,
29
+ action: `restart this window as '${human}' (same directory, nothing else changes)`,
30
+ reason: others > 0 ? `${others} other signed-in profile${others === 1 ? '' : 's'}: /profile lists them` : '',
31
+ });
32
+
26
33
  /** The ask card for a /slot name that does not exist (the near misses are said on the line above). */
27
34
  export const slotCreateAskLine = (nm) => askText('Create it?', { title: `No slot ${nm}.`, action: `create and hold slot ${nm}` });
28
35
 
@@ -6,6 +6,7 @@
6
6
  import { readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
8
8
  import { join, dirname } from 'node:path';
9
+ import { redactSecrets } from './secretShape.js';
9
10
 
10
11
  export const HISTORY_CAP = 500;
11
12
  export const defaultHistoryPath = (home = homedir()) => join(home, '.quox', 'history.jsonl');
@@ -22,7 +23,8 @@ export function fileHistory(path = defaultHistoryPath(), { cap = HISTORY_CAP } =
22
23
  return out.slice(-cap);
23
24
  } catch { return []; }
24
25
  };
25
- const add = (text) => {
26
+ const add = (text0) => {
27
+ const text = redactSecrets(text0); // SECRET-GUARD: a value never reaches the file
26
28
  try {
27
29
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
28
30
  appendFileSync(path, JSON.stringify({ t: text }) + '\n', { mode: 0o600 });
@@ -37,17 +37,18 @@ export function flyPose(ms) {
37
37
  /** The frame at `ms` since the pass began. still: the one fixed frame (mid-sky, wings raised). mono: shapes only. */
38
38
  /** The sky's size for a terminal `termRows` tall: the mid Q, or the small Q (headroom scaled to match) when the mid sky would
39
39
  * not leave 10 rows for the transcript and composer. Never taller than max(12, termRows - 10). */
40
- export function flySky(termRows) {
41
- const room = Math.max(12, (termRows || 40) - 10);
40
+ export function flySky(termRows, chromeH = null) {
41
+ const room = chromeH == null ? Math.max(12, (termRows || 40) - 10) : (termRows || 40) - chromeH - 2; // measured chrome (app.js): the sky + margin + chrome must stay under the terminal height or Ink wipes the screen
42
42
  const fits = (size, top) => dragonLayout(120, { size, lm: 0, ty: top }).rows <= room;
43
- if (fits(SIZE, TOP)) return { size: SIZE, top: TOP, bob: BOB };
43
+ if (fits(SIZE, TOP)) return { size: SIZE, top: TOP, bob: BOB, fits: true };
44
44
  const k = DSIZES.small / DSIZES[SIZE];
45
- return { size: 'small', top: Math.round(TOP * k), bob: Math.max(1, Math.round(BOB * k)) };
45
+ const top = Math.round(TOP * k);
46
+ return { size: 'small', top, bob: Math.max(1, Math.round(BOB * k)), fits: chromeH == null || dragonLayout(120, { size: 'small', lm: 0, ty: top }).rows <= room };
46
47
  }
47
48
 
48
- export function flyFrame(ms, cols, { still = false, mono = false, theme = 'dark', termRows } = {}) {
49
+ export function flyFrame(ms, cols, { still = false, mono = false, theme = 'dark', termRows, chromeH = null } = {}) {
49
50
  const t = still ? 0 : Math.max(0, Math.min(ms, FLY_PASS_MS));
50
- const { size: SIZE, top: TOP, bob: BOB } = flySky(termRows);
51
+ const { size: SIZE, top: TOP, bob: BOB } = flySky(termRows, chromeH);
51
52
  const probe = dragonLayout(cols, { size: SIZE, lm: 0, ty: TOP });
52
53
  const rows = probe.rows;
53
54
  const xc = still ? cols / 2 : -SPAN + (cols + 2 * SPAN) * kf(t, [[0, 0], [FLY_PASS_MS, 1]]);
@@ -11,6 +11,7 @@ import { helloCells, helloGradient, helloMs } from './qhello.js';
11
11
  import { mono, c, clip, isLight } from './theme.js';
12
12
  import { CLIPS, clipCells, clipCaption, eggCells, EGG_MS } from './qdragonScenes.js';
13
13
  import { reducedMotion } from './motion/clock.js';
14
+ import { flySky } from './qdragonFly.js';
14
15
  import { FrameContext, useFrame } from './motion/frame.js';
15
16
  const isQuiet = () => reducedMotion(); // motion only (NO_COLOR is colour only; QGlyph falls back to the circled Q itself)
16
17
  import { termRows } from './term.js';
@@ -130,3 +131,19 @@ export function DragonLive({ cols, rows }) {
130
131
  ...cells.map((row, k) => h(Text, { key: k, wrap: 'truncate' }, ...cellNodes(row, `d${k}-`))),
131
132
  h(Text, { color: c('dim') }, clip(caption, Math.max(10, cols - 2))));
132
133
  }
134
+
135
+ /** Rows the 15-row dragon clips need besides the chrome: the clip, its caption and the margin above it. */
136
+ export const CLIP_ROWS = 17;
137
+ /** Does an explicit /dragon <clip> fit? { ok } or { ok:false, note } (one line, never silent). Unknown size (null rows) is ok. */
138
+ export function clipRoom(clipName, rows, chromeH = 10) {
139
+ if (!rows) return { ok: true };
140
+ const need = CLIP_ROWS + chromeH + 2;
141
+ return rows >= need ? { ok: true } : { ok: false, note: `${clipName} needs ${need} rows, you have ${rows}. Make the window taller.` };
142
+ }
143
+ /** Does /dragon-fly fit? Same shape as clipRoom; the smallest sky is 12 rows. */
144
+ export function flyRoom(rows, chromeH = 10) {
145
+ if (!rows) return { ok: true };
146
+ if (flySky(rows, chromeH).fits) return { ok: true };
147
+ const need = 12 + 1 + chromeH + 2;
148
+ return { ok: false, note: `fly needs ${need} rows, you have ${rows}. Make the window taller.` };
149
+ }