quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,244 @@
1
+ import React from 'react';
2
+ import { render } from 'ink';
3
+ import { realpathSync } from 'node:fs';
4
+ import { App } from './app.js';
5
+ import { inkRenderOptions, inkStdout } from './term.js';
6
+ import { createDemoApi } from './demo.js';
7
+ import { DEMO_LABEL } from './demo.fixtures.js';
8
+ import { PASTE_ON } from './keys.js';
9
+ import { shouldShowWelcome, markWelcomeSeen } from './welcome.js';
10
+ import { loadRules, rulesContext } from './rules.js';
11
+ import { createTranscript } from './transcript.js';
12
+ import { parseModelArg } from './model.js';
13
+ import { applySettings } from './slash/settings.js';
14
+ import { headOf, RUNNING_ROOT, execRestart, checkRestart } from './slash/restart.js';
15
+
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);
18
+
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
+ }
121
+
122
+ /**
123
+ * Clear the screen before the chat client? Never right after Getting started: its finale (the one-time
124
+ * dashboard link, a password shown once) is on screen and in scrollback, and ESC[3J would erase both.
125
+ */
126
+ export function shouldClearBeforeChat({ isTTY, noClear, fromGettingStarted }) {
127
+ return Boolean(isTTY) && !noClear && !fromGettingStarted;
128
+ }
129
+
130
+ export async function main(argv = process.argv.slice(2)) {
131
+ 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
+ // Raw mode FIRST: the awaits below (module loads, config, health) must not leave the tty cooked.
135
+ const early = !opts.print ? captureEarlyInput() : null;
136
+ const cwd = process.cwd().replace(process.env.HOME || '\0', '~');
137
+ const { resolveCliVersion } = await import('../core/version.js');
138
+ let version = '0.2.0';
139
+ try { version = resolveCliVersion?.() || version; } catch { /* keep default */ }
140
+ if (opts.print) return mainPrint(opts);
141
+ let replay = null;
142
+ let fromGettingStarted = false;
143
+ if (opts.demoOnboarding || (!opts.demo && await noServerConfigured(opts, await import('../core/config.js')))) {
144
+ // Getting started (FIRST_RUN_JOURNEY.md C2a): a separate view, never the chat client talking.
145
+ if (process.stdout.isTTY && !opts.noClear) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
146
+ const welcome = shouldShowWelcome({ print: false });
147
+ if (welcome && !opts.demoOnboarding) markWelcomeSeen();
148
+ const { runGettingStarted } = await import('./onboarding/index.js');
149
+ const r = await runGettingStarted({ version, cwd, welcome, earlyInput: early, demo: opts.demoOnboarding });
150
+ if (r?.action !== 'connected') return;
151
+ // signed in (an existing server, or the one just installed): carry on into the conversation,
152
+ // and a fresh install replays the original request as the first real turn
153
+ replay = r.replay || null;
154
+ fromGettingStarted = true;
155
+ }
156
+ let props;
157
+ let restart = null; // set by /restart: run the installed quox here once the app has let go of the terminal
158
+ if (opts.demo) {
159
+ // /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 };
161
+ } else {
162
+ const cfgMod = await import('../core/config.js');
163
+ const { createAuth } = await import('../core/auth.js');
164
+ const { createClient } = await import('../core/client.js');
165
+ const { createApi } = await import('./api.js');
166
+ const probe = cfgMod.loadConfig();
167
+ const profiles = probe.listProfiles?.() || [];
168
+ const missing = opts.profile && !profiles.some(p => p.name === opts.profile);
169
+ const pick = pickProfile(opts.profile, cfgMod);
170
+ 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 });
174
+ const pname = missing ? opts.profile : (config.getActiveProfileName?.() || 'default');
175
+ const problem = chatCredentialProblem({ profileArg: opts.profile, profiles, hasStoredLogin: missing ? false : Boolean(config.getAuth?.()?.access_token), profileName: pname, serviceKeyFlag: opts.serviceKey });
176
+ // The health probe runs inside the app (background); the UI is up and typing works immediately.
177
+ const startup = async () => {
178
+ if (problem) return problem.message;
179
+ if (pick.note) return pick.note;
180
+ const hl = await api.health(8000);
181
+ 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
+ };
183
+ const saved = api.settings();
184
+ 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 };
187
+ }
188
+ // Interactive start: a clean screen under the intro (never when piped; NO_COLOR still clears),
189
+ // except straight after Getting started, whose finale must stay readable.
190
+ if (shouldClearBeforeChat({ isTTY: process.stdout.isTTY, noClear: opts.noClear, fromGettingStarted })) process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
191
+ const welcome = shouldShowWelcome({ print: opts.print });
192
+ if (welcome) markWelcomeSeen();
193
+ const transcript = opts.demo ? null : createTranscript({ meta: { version, cols: process.stdout.columns, rows: process.stdout.rows, profile: props.profile, org: props.org, cwd } });
194
+ const onResize = () => transcript?.resize(process.stdout.columns, process.stdout.rows);
195
+ 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); }
200
+ if (!restart) return;
201
+ // 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 });
203
+ 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
+ process.exitCode = r.code;
205
+ }
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
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Terminal markdown for QUOXON. `marked` supplies the LEXER only; every token is
3
+ * rendered by our own Ink components in the Quox style (no marked-terminal).
4
+ *
5
+ * Streaming contract:
6
+ * splitSettled(text) -> { settled, rest } settled = complete top-level blocks
7
+ * (safe to move to <Static>), rest = the
8
+ * unfinished tail (re-rendered live).
9
+ * healInline(text) hides half-typed syntax in the live tail
10
+ * (`**bo`, `` `co ``, `[txt](ht`).
11
+ * An unclosed ``` fence is a single code token to the end of text, so it renders
12
+ * as code while streaming and never as raw backticks.
13
+ */
14
+ import React from 'react';
15
+ import { Box, Text, Transform } from 'ink';
16
+ import { Lexer } from 'marked';
17
+ import { graphemes } from './textBuffer.js';
18
+ import { c, mono, clip, gradient, bulletRole } from './theme.js';
19
+
20
+ const h = React.createElement;
21
+
22
+ export const lex = (src) => new Lexer({ gfm: true, breaks: false }).lex(src);
23
+
24
+ export function splitSettled(text) {
25
+ const tokens = lex(text);
26
+ let last = -1;
27
+ for (let i = tokens.length - 1; i >= 0; i--) if (tokens[i].type !== 'space') { last = i; break; }
28
+ if (last <= 0) return { settled: '', rest: text };
29
+ let off = 0;
30
+ for (let i = 0; i < last; i++) off += tokens[i].raw.length;
31
+ return { settled: text.slice(0, off), rest: text.slice(off) };
32
+ }
33
+
34
+ export function healInline(text) {
35
+ let t = text;
36
+ const fence = (t.match(/^ {0,3}(```|~~~)/gm) || []).length;
37
+ if (fence % 2 === 1) return t; // inside an open fence: leave verbatim, it renders as code
38
+ const cut = (i) => { t = t.slice(0, i); };
39
+ const ticks = (t.match(/`/g) || []).length;
40
+ if (ticks % 2 === 1) cut(t.lastIndexOf('`'));
41
+ const bold = t.split('**').length - 1;
42
+ if (bold % 2 === 1) cut(t.lastIndexOf('**'));
43
+ const tilde = t.split('~~').length - 1;
44
+ if (tilde % 2 === 1) cut(t.lastIndexOf('~~'));
45
+ const lb = t.lastIndexOf('[');
46
+ if (lb >= 0 && !/\]\([^)]*\)/.test(t.slice(lb)) && /^\[[^\]]*(\]\(?[^)\s]*)?$/.test(t.slice(lb))) cut(lb);
47
+ return t;
48
+ }
49
+
50
+ // ---- wrapped text ------------------------------------------------------------------------------
51
+ /**
52
+ * Ink wraps with wrap-ansi `trim: false`: when a row fills the width exactly, the space before the next
53
+ * word starts the continuation row (" AI agents."). Drop that one space on wrapped rows (index > 0),
54
+ * after any leading colour codes. Width is unchanged, so layout and row counts are unaffected.
55
+ */
56
+ export const dropLeadSpace = (line, i) => (i > 0 ? line.replace(/^((?:\x1b\[[0-9;]*m)*) /, '$1') : line);
57
+ /** <Text> whose wrapped continuation rows never start with a stray space. */
58
+ export const WText = (props) => h(Transform, { transform: dropLeadSpace }, h(Text, props));
59
+
60
+ // ---- hyperlinks (OSC 8) --------------------------------------------------------------------------
61
+ export function supportsHyperlinks(env = process.env, isTTY = Boolean(process.stdout?.isTTY)) {
62
+ if (!isTTY || env.NO_COLOR || env.QUOX_HYPERLINKS === '0') return false;
63
+ if (env.QUOX_HYPERLINKS === '1') return true;
64
+ if (env.KITTY_WINDOW_ID || env.WT_SESSION) return true;
65
+ if (['iTerm.app', 'WezTerm', 'vscode', 'ghostty'].includes(env.TERM_PROGRAM)) return true;
66
+ return Number(env.VTE_VERSION) >= 5000;
67
+ }
68
+ const osc8 = (url, text) => `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
69
+
70
+ // ---- inline ----------------------------------------------------------------------------------
71
+ export function inlineText(tokens = []) {
72
+ return tokens.map(t => {
73
+ if (t.tokens) return inlineText(t.tokens);
74
+ if (t.type === 'br') return ' ';
75
+ return t.text ?? t.raw ?? '';
76
+ }).join('').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
77
+ }
78
+
79
+ /** A heading word painted sky -> indigo -> violet, bold (plain bold under NO_COLOR). */
80
+ export const Heading = ({ text, underline = false }) => h(Text, { bold: true, underline },
81
+ ...(mono() ? [String(text)] : gradient(text).map((g, i) => h(Text, { key: i, color: g.color }, g.ch))));
82
+
83
+ /** Prose with numbers and quantities picked out in the number role. Characters unchanged. */
84
+ const NUM = /(?<![\w.#/-])\d+(?:[.,:-]\d+)*(?:%|ms|s|k|m|h|d|x|KB|MB|GB)?(?!\w)/g;
85
+ export function numberRuns(text, key = 'n') {
86
+ const s = String(text);
87
+ if (mono()) return [s];
88
+ const out = [];
89
+ let i = 0;
90
+ for (const m of s.matchAll(NUM)) {
91
+ if (m.index > i) out.push(s.slice(i, m.index));
92
+ out.push(h(Text, { key: `${key}#${m.index}`, color: c('number') }, m[0]));
93
+ i = m.index + m[0].length;
94
+ }
95
+ if (i < s.length) out.push(s.slice(i));
96
+ return out;
97
+ }
98
+
99
+ const unesc = (s) => String(s).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
100
+
101
+ function inline(tokens = [], style = {}, key = 'i') {
102
+ return tokens.map((t, i) => {
103
+ const k = `${key}.${i}`;
104
+ switch (t.type) {
105
+ case 'strong': return h(Text, { key: k, bold: true, color: c('value') }, ...inline(t.tokens, style, k));
106
+ case 'em': return h(Text, { key: k, italic: true }, ...inline(t.tokens, style, k));
107
+ case 'del': return h(Text, { key: k, strikethrough: true }, ...inline(t.tokens, style, k));
108
+ case 'codespan': return mono()
109
+ ? h(Text, { key: k }, '`' + unesc(t.text) + '`')
110
+ : h(Text, { key: k, color: c('code'), backgroundColor: c('codeBg') }, ' ' + unesc(t.text) + ' ');
111
+ case 'br': return h(Text, { key: k }, '\n');
112
+ case 'link': {
113
+ const label = inlineText(t.tokens) || t.href;
114
+ const shown = supportsHyperlinks() ? osc8(t.href, label) : label;
115
+ return h(Text, { key: k }, h(Text, { color: c('sky'), underline: true }, shown),
116
+ label !== t.href ? h(Text, { color: c('muted') }, ` (${t.href})`) : null);
117
+ }
118
+ case 'image': return h(Text, { key: k, color: c('dim') }, `[image: ${t.text || t.href}]`);
119
+ case 'escape': return h(Text, { key: k }, t.text);
120
+ case 'html': return h(Text, { key: k, color: c('dim') }, t.text);
121
+ case 'text': return t.tokens ? h(Text, { key: k }, ...inline(t.tokens, style, k)) : h(Text, { key: k }, ...numberRuns(unesc(t.text), k));
122
+ default: return h(Text, { key: k }, unesc(t.text ?? t.raw ?? ''));
123
+ }
124
+ });
125
+ }
126
+
127
+ // ---- tables ----------------------------------------------------------------------------------
128
+ const w = (s) => graphemes(s).length;
129
+ const padTo = (s, n) => s + ' '.repeat(Math.max(0, n - w(s)));
130
+
131
+ /** Plain-text table layout: returns { mode: 'grid'|'vertical', lines: [{ text, head? }] }. Pure, for tests. */
132
+ export function layoutTable(header, rows, cols) {
133
+ const n = header.length;
134
+ const widths = header.map((hd, i) => Math.max(w(hd), ...rows.map(r => w(r[i] ?? '')), 1));
135
+ const total = widths.reduce((a, b) => a + b, 0) + 3 * (n - 1);
136
+ if (total <= cols) {
137
+ const line = (r) => r.map((cell, i) => padTo(cell ?? '', widths[i])).join(' │ ').replace(/\s+$/, '');
138
+ const rule = widths.map(x => '─'.repeat(x)).join('─┼─');
139
+ return { mode: 'grid', lines: [{ text: line(header), head: true }, { text: rule, rule: true }, ...rows.map(r => ({ text: line(r) }))] };
140
+ }
141
+ // too wide: vertical key: value, one block per row
142
+ const lines = [];
143
+ const keyW = Math.min(Math.max(...header.map(w)), Math.floor(cols / 3));
144
+ rows.forEach((r, ri) => {
145
+ if (ri) lines.push({ text: '' });
146
+ header.forEach((hd, i) => lines.push({ text: `${padTo(clip(hd, keyW), keyW)} ${r[i] ?? ''}`, key: keyW }));
147
+ });
148
+ return { mode: 'vertical', lines };
149
+ }
150
+
151
+ function Table({ t, cols }) {
152
+ const header = t.header.map(x => inlineText(x.tokens));
153
+ const rows = t.rows.map(r => r.map(x => inlineText(x.tokens)));
154
+ const lay = layoutTable(header, rows, cols);
155
+ return h(Box, { flexDirection: 'column' }, ...lay.lines.map((l, i) => h(Text, {
156
+ key: i, bold: l.head, color: l.rule ? c('muted') : (l.head ? c('label') : undefined), wrap: 'truncate-end'
157
+ }, l.text || ' ')));
158
+ }
159
+
160
+ // ---- blocks ----------------------------------------------------------------------------------
161
+ const rule = (cols, label) => {
162
+ if (!label) return '─'.repeat(cols);
163
+ const lead = `── ${label} `;
164
+ return lead + '─'.repeat(Math.max(0, cols - w(lead)));
165
+ };
166
+
167
+ /** Fenced code top rule: dim teal line, the language label in the icon role. Same characters as rule(). */
168
+ function codeRule(cols, label) {
169
+ const line = rule(cols, label);
170
+ if (!label) return h(Text, { color: c('codeRule') }, line);
171
+ return h(Text, null, h(Text, { color: c('codeRule') }, '── '), h(Text, { color: c('icon') }, label), h(Text, { color: c('codeRule') }, line.slice(3 + label.length)));
172
+ }
173
+
174
+ function List({ t, cols, depth }) {
175
+ const start = typeof t.start === 'number' ? t.start : 1;
176
+ return h(Box, { flexDirection: 'column' }, ...t.items.map((it, i) => {
177
+ const marker = t.ordered ? `${start + i}.` : (depth % 2 ? '◦' : '•');
178
+ const mw = w(marker) + 1;
179
+ return h(Box, { key: i },
180
+ h(Box, { width: mw, flexShrink: 0 }, h(Text, { color: c(bulletRole(depth)) }, marker)),
181
+ h(Box, { flexDirection: 'column', flexGrow: 1, flexShrink: 1 }, ...blocks(it.tokens, cols - mw, depth + 1, `li${i}`)));
182
+ }));
183
+ }
184
+
185
+ function blocks(tokens = [], cols, depth = 0, key = 'b') {
186
+ const out = [];
187
+ tokens.forEach((t, i) => {
188
+ const k = `${key}.${i}`;
189
+ switch (t.type) {
190
+ case 'space': return;
191
+ case 'heading': {
192
+ out.push(h(Box, { key: k, marginTop: depth ? 0 : 1 }, h(Transform, { transform: dropLeadSpace }, h(Heading, { text: inlineStr(t.tokens), underline: t.depth === 1 && mono() }))));
193
+ return;
194
+ }
195
+ case 'paragraph': case 'text':
196
+ out.push(h(Box, { key: k }, h(WText, null, ...inline(t.tokens || [{ type: 'text', text: t.text }], {}, k))));
197
+ return;
198
+ case 'code': {
199
+ const lines = String(t.text).split('\n');
200
+ out.push(h(Box, { key: k, flexDirection: 'column', width: cols },
201
+ codeRule(cols, t.lang ? String(t.lang).split(/\s+/)[0] : ''),
202
+ ...lines.map((ln, j) => h(Text, { key: j, color: c('text') }, ln || ' ')),
203
+ h(Text, { color: c('codeRule') }, rule(cols))));
204
+ return;
205
+ }
206
+ case 'list': out.push(h(List, { key: k, t, cols, depth })); return;
207
+ case 'blockquote':
208
+ out.push(h(Box, { key: k },
209
+ h(Box, { width: 2, flexShrink: 0 }, h(Text, { color: c('label') }, '▎ ')),
210
+ h(Box, { flexDirection: 'column', flexGrow: 1, flexShrink: 1 }, ...blocks(t.tokens, cols - 2, depth + 1, k))));
211
+ return;
212
+ case 'hr': out.push(h(Text, { key: k, color: c('dim') }, '─'.repeat(cols))); return;
213
+ case 'table': out.push(h(Table, { key: k, t, cols })); return;
214
+ case 'html': out.push(h(Text, { key: k, color: c('dim') }, t.text.trim())); return;
215
+ case 'def': return;
216
+ default: out.push(h(Text, { key: k }, unesc(t.raw || '')));
217
+ }
218
+ });
219
+ return out;
220
+ }
221
+
222
+ /** Render markdown text. live = unfinished tail of a streaming answer. */
223
+ export function Markdown({ text, cols, live = false }) {
224
+ const src = live ? healInline(text) : text;
225
+ if (!src.trim()) return null;
226
+ const kids = blocks(lex(src), cols).map((el, i) => h(Box, { key: el.key ?? i, marginTop: i ? 1 : 0, flexDirection: 'column' }, el));
227
+ return h(Box, { flexDirection: 'column', width: cols }, ...kids);
228
+ }
229
+
230
+ // ---- height (sizes the live region; must match what Ink lays out) ---------------------------------
231
+ /** Rows one line takes in Ink's Text wrap (wrap-ansi 9, trim: false, hard: true), plain text only. */
232
+ function wrapLineRows(line, cols) {
233
+ if (cols <= 0) return 1;
234
+ const words = line.split(' ');
235
+ const rows = [0];
236
+ words.forEach((word, i) => {
237
+ let len = rows[rows.length - 1];
238
+ const wl = w(word);
239
+ if (i !== 0) {
240
+ if (len >= cols) { rows.push(0); len = 0; }
241
+ rows[rows.length - 1] = ++len;
242
+ }
243
+ if (wl > cols) { // hard-break a word longer than the line
244
+ const remaining = cols - len;
245
+ const startHere = 1 + Math.floor((wl - remaining - 1) / cols);
246
+ const startNext = Math.floor((wl - 1) / cols);
247
+ if (startNext < startHere) rows.push(0);
248
+ let vis = rows[rows.length - 1];
249
+ const chars = graphemes(word);
250
+ chars.forEach((_, k) => {
251
+ if (vis + 1 <= cols) rows[rows.length - 1] = ++vis;
252
+ else { rows.push(1); vis = 1; }
253
+ if (vis === cols && k < chars.length - 1) { rows.push(0); vis = 0; }
254
+ });
255
+ if (!vis && rows.length > 1 && rows[rows.length - 1] === 0) rows.pop();
256
+ return;
257
+ }
258
+ if (len + wl > cols && len > 0 && wl > 0) { rows.push(0); len = 0; }
259
+ rows[rows.length - 1] = len + wl;
260
+ });
261
+ return rows.length;
262
+ }
263
+
264
+ /** Rows a plain string takes when Ink wraps it at `cols`. */
265
+ export function wrapRows(text, cols) {
266
+ return String(text).replace(/\r\n/g, '\n').split('\n').reduce((n, l) => n + wrapLineRows(l, cols), 0);
267
+ }
268
+
269
+ function inlineStr(tokens = []) {
270
+ return tokens.map(t => {
271
+ switch (t.type) {
272
+ case 'strong': case 'em': case 'del': return inlineStr(t.tokens);
273
+ case 'codespan': return mono() ? '`' + unesc(t.text) + '`' : ' ' + unesc(t.text) + ' ';
274
+ case 'br': return '\n';
275
+ case 'link': { const label = inlineText(t.tokens) || t.href; return label + (label !== t.href ? ` (${t.href})` : ''); }
276
+ case 'image': return `[image: ${t.text || t.href}]`;
277
+ case 'escape': case 'html': return t.text;
278
+ case 'text': return t.tokens ? inlineStr(t.tokens) : unesc(t.text);
279
+ default: return unesc(t.text ?? t.raw ?? '');
280
+ }
281
+ }).join('');
282
+ }
283
+
284
+ /** Heights of the elements blocks() would produce, in order (space/def produce none). */
285
+ function blockRows(tokens = [], cols, depth = 0) {
286
+ const out = [];
287
+ for (const t of tokens) {
288
+ switch (t.type) {
289
+ case 'space': case 'def': break;
290
+ case 'heading': out.push(wrapRows(inlineStr(t.tokens), cols) + (depth ? 0 : 1)); break;
291
+ case 'paragraph': case 'text': out.push(wrapRows(inlineStr(t.tokens || [{ type: 'text', text: t.text }]), cols)); break;
292
+ case 'code': out.push(2 + String(t.text).split('\n').reduce((n, ln) => n + wrapRows(ln || ' ', cols), 0)); break;
293
+ case 'list': {
294
+ out.push(t.items.reduce((n, it, i) => {
295
+ const marker = t.ordered ? `${(typeof t.start === 'number' ? t.start : 1) + i}.` : '•';
296
+ const inner = blockRows(it.tokens, cols - (w(marker) + 1), depth + 1).reduce((a, b) => a + b, 0);
297
+ return n + Math.max(1, inner);
298
+ }, 0));
299
+ break;
300
+ }
301
+ case 'blockquote': out.push(Math.max(1, blockRows(t.tokens, cols - 2, depth + 1).reduce((a, b) => a + b, 0))); break;
302
+ case 'hr': out.push(1); break;
303
+ case 'table': out.push(layoutTable(t.header.map(x => inlineText(x.tokens)), t.rows.map(r => r.map(x => inlineText(x.tokens))), cols).lines.length); break;
304
+ case 'html': out.push(wrapRows(t.text.trim(), cols)); break;
305
+ default: out.push(wrapRows(unesc(t.raw || ''), cols));
306
+ }
307
+ }
308
+ return out;
309
+ }
310
+
311
+ /** Rows <Markdown text cols live/> occupies. Pure; mirrors Markdown() above. */
312
+ export function markdownRows(text, cols, live = false) {
313
+ const src = live ? healInline(text) : text;
314
+ if (!src.trim()) return 0;
315
+ const rows = blockRows(lex(src), cols);
316
+ return rows.reduce((a, b) => a + b, 0) + Math.max(0, rows.length - 1);
317
+ }