quoxcli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (516) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +144 -2
  3. package/bin/quox.js +265 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +151 -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/agent-schedules.js +26 -0
  9. package/lib/api/endpoints/agents.js +63 -0
  10. package/lib/api/endpoints/assistants.js +62 -0
  11. package/lib/api/endpoints/audit.js +14 -0
  12. package/lib/api/endpoints/auth.js +50 -0
  13. package/lib/api/endpoints/awareness.js +34 -0
  14. package/lib/api/endpoints/away.js +15 -0
  15. package/lib/api/endpoints/backup-schedules.js +20 -0
  16. package/lib/api/endpoints/backups.js +86 -0
  17. package/lib/api/endpoints/brain.js +140 -0
  18. package/lib/api/endpoints/budgets.js +62 -0
  19. package/lib/api/endpoints/calendarq.js +71 -0
  20. package/lib/api/endpoints/catalog.js +14 -0
  21. package/lib/api/endpoints/chat.js +61 -0
  22. package/lib/api/endpoints/consent.js +26 -0
  23. package/lib/api/endpoints/contactsq.js +68 -0
  24. package/lib/api/endpoints/conversations.js +46 -0
  25. package/lib/api/endpoints/custom-roles.js +92 -0
  26. package/lib/api/endpoints/divisions.js +56 -0
  27. package/lib/api/endpoints/domains.js +38 -0
  28. package/lib/api/endpoints/engagement.js +231 -0
  29. package/lib/api/endpoints/engine.js +11 -0
  30. package/lib/api/endpoints/entity.js +50 -0
  31. package/lib/api/endpoints/executions.js +45 -0
  32. package/lib/api/endpoints/fetchAllowlist.js +27 -0
  33. package/lib/api/endpoints/file.js +20 -0
  34. package/lib/api/endpoints/files.js +40 -0
  35. package/lib/api/endpoints/github.js +21 -0
  36. package/lib/api/endpoints/governance.js +43 -0
  37. package/lib/api/endpoints/hat.js +66 -0
  38. package/lib/api/endpoints/inbox.js +52 -0
  39. package/lib/api/endpoints/inboxq.js +120 -0
  40. package/lib/api/endpoints/index.js +242 -0
  41. package/lib/api/endpoints/invites.js +19 -0
  42. package/lib/api/endpoints/llm-policy.js +43 -0
  43. package/lib/api/endpoints/lucid.js +89 -0
  44. package/lib/api/endpoints/members.js +32 -0
  45. package/lib/api/endpoints/memory.js +94 -0
  46. package/lib/api/endpoints/mfa.js +32 -0
  47. package/lib/api/endpoints/mind.js +13 -0
  48. package/lib/api/endpoints/mon.js +184 -0
  49. package/lib/api/endpoints/notesq.js +43 -0
  50. package/lib/api/endpoints/objectives.js +68 -0
  51. package/lib/api/endpoints/orgs.js +38 -0
  52. package/lib/api/endpoints/plugins.js +152 -0
  53. package/lib/api/endpoints/powers.js +75 -0
  54. package/lib/api/endpoints/presence.js +33 -0
  55. package/lib/api/endpoints/prompts.js +69 -0
  56. package/lib/api/endpoints/qlarity.js +122 -0
  57. package/lib/api/endpoints/quoxbox.js +55 -0
  58. package/lib/api/endpoints/quoxchat.js +54 -0
  59. package/lib/api/endpoints/quoxflow.js +32 -0
  60. package/lib/api/endpoints/quoxplan.js +121 -0
  61. package/lib/api/endpoints/quoxseo.js +114 -0
  62. package/lib/api/endpoints/reach.js +47 -0
  63. package/lib/api/endpoints/router.js +17 -0
  64. package/lib/api/endpoints/runs.js +38 -0
  65. package/lib/api/endpoints/safety.js +53 -0
  66. package/lib/api/endpoints/scim.js +46 -0
  67. package/lib/api/endpoints/scopes.js +14 -0
  68. package/lib/api/endpoints/security.js +69 -0
  69. package/lib/api/endpoints/setup.js +90 -0
  70. package/lib/api/endpoints/skills.js +200 -0
  71. package/lib/api/endpoints/sso.js +67 -0
  72. package/lib/api/endpoints/stepup.js +20 -0
  73. package/lib/api/endpoints/tags.js +32 -0
  74. package/lib/api/endpoints/teams.js +56 -0
  75. package/lib/api/endpoints/telephony.js +46 -0
  76. package/lib/api/endpoints/tg.js +56 -0
  77. package/lib/api/endpoints/tools.js +50 -0
  78. package/lib/api/endpoints/traces.js +157 -0
  79. package/lib/api/endpoints/triggers.js +96 -0
  80. package/lib/api/endpoints/user-library.js +41 -0
  81. package/lib/api/endpoints/users.js +20 -0
  82. package/lib/api/endpoints/v1.js +585 -0
  83. package/lib/api/endpoints/vault.js +192 -0
  84. package/lib/api/endpoints/voice.js +22 -0
  85. package/lib/api/endpoints/volt.js +84 -0
  86. package/lib/api/endpoints/ward.js +47 -0
  87. package/lib/api/endpoints/webhooks.js +46 -0
  88. package/lib/api/endpoints/workflows.js +169 -0
  89. package/lib/api/endpoints/x.js +32 -0
  90. package/lib/api/endpoints.js +15 -0
  91. package/lib/commands/actions.js +61 -0
  92. package/lib/commands/admin.js +375 -0
  93. package/lib/commands/aee.js +99 -0
  94. package/lib/commands/agent.js +337 -0
  95. package/lib/commands/agentAttach.js +1528 -0
  96. package/lib/commands/approvals.js +119 -0
  97. package/lib/commands/aside.js +24 -0
  98. package/lib/commands/ask.js +144 -0
  99. package/lib/commands/assistant.js +169 -0
  100. package/lib/commands/auth.js +464 -0
  101. package/lib/commands/awareness.js +88 -0
  102. package/lib/commands/backup.js +377 -0
  103. package/lib/commands/brain.js +1085 -0
  104. package/lib/commands/budget.js +241 -0
  105. package/lib/commands/build.js +170 -0
  106. package/lib/commands/calendar.js +216 -0
  107. package/lib/commands/capabilities.js +34 -0
  108. package/lib/commands/chat.js +295 -0
  109. package/lib/commands/claude.js +380 -0
  110. package/lib/commands/config-cmd.js +106 -0
  111. package/lib/commands/consent.js +84 -0
  112. package/lib/commands/contacts.js +1016 -0
  113. package/lib/commands/contactsSelftest.js +132 -0
  114. package/lib/commands/conversation.js +105 -0
  115. package/lib/commands/dashboard.js +43 -0
  116. package/lib/commands/department.js +96 -0
  117. package/lib/commands/dev.js +620 -0
  118. package/lib/commands/devResident.js +326 -0
  119. package/lib/commands/devSupervisor.js +1062 -0
  120. package/lib/commands/discord.js +132 -0
  121. package/lib/commands/doctor.js +181 -0
  122. package/lib/commands/domain.js +114 -0
  123. package/lib/commands/done.js +347 -0
  124. package/lib/commands/engagement.js +597 -0
  125. package/lib/commands/entity.js +119 -0
  126. package/lib/commands/facebook.js +212 -0
  127. package/lib/commands/fetchAllowlist.js +71 -0
  128. package/lib/commands/file.js +165 -0
  129. package/lib/commands/fleet.js +1189 -0
  130. package/lib/commands/governance.js +647 -0
  131. package/lib/commands/gripe.js +355 -0
  132. package/lib/commands/hat.js +374 -0
  133. package/lib/commands/helpers.js +96 -0
  134. package/lib/commands/hyperion.js +58 -0
  135. package/lib/commands/inbox.js +173 -0
  136. package/lib/commands/instagram.js +264 -0
  137. package/lib/commands/integration.js +100 -0
  138. package/lib/commands/inventory.js +115 -0
  139. package/lib/commands/invites.js +45 -0
  140. package/lib/commands/licence.js +139 -0
  141. package/lib/commands/linkedin.js +178 -0
  142. package/lib/commands/llm-policy.js +117 -0
  143. package/lib/commands/llm.js +45 -0
  144. package/lib/commands/lucid.js +597 -0
  145. package/lib/commands/mail.js +531 -0
  146. package/lib/commands/memory.js +361 -0
  147. package/lib/commands/mind.js +71 -0
  148. package/lib/commands/mon.js +581 -0
  149. package/lib/commands/monitoring.js +86 -0
  150. package/lib/commands/motion.js +22 -0
  151. package/lib/commands/notes.js +1246 -0
  152. package/lib/commands/notesBatch.js +160 -0
  153. package/lib/commands/notesBridges.js +56 -0
  154. package/lib/commands/notesLinks.js +60 -0
  155. package/lib/commands/notification.js +57 -0
  156. package/lib/commands/nucleus.js +123 -0
  157. package/lib/commands/objective.js +240 -0
  158. package/lib/commands/offload.js +89 -0
  159. package/lib/commands/onboard.js +329 -0
  160. package/lib/commands/open.js +56 -0
  161. package/lib/commands/org.js +276 -0
  162. package/lib/commands/peek.js +28 -0
  163. package/lib/commands/plan.js +944 -0
  164. package/lib/commands/plugin-doctor.js +1091 -0
  165. package/lib/commands/plugin.js +649 -0
  166. package/lib/commands/policy.js +78 -0
  167. package/lib/commands/powers.js +296 -0
  168. package/lib/commands/presence.js +89 -0
  169. package/lib/commands/prompt.js +350 -0
  170. package/lib/commands/provider.js +159 -0
  171. package/lib/commands/qlarity.js +706 -0
  172. package/lib/commands/quoxbox.js +270 -0
  173. package/lib/commands/quoxchat.js +275 -0
  174. package/lib/commands/quoxseo.js +378 -0
  175. package/lib/commands/reach.js +159 -0
  176. package/lib/commands/role.js +286 -0
  177. package/lib/commands/router.js +77 -0
  178. package/lib/commands/rows.js +358 -0
  179. package/lib/commands/run.js +187 -0
  180. package/lib/commands/safety.js +107 -0
  181. package/lib/commands/scim.js +88 -0
  182. package/lib/commands/scope.js +58 -0
  183. package/lib/commands/security.js +157 -0
  184. package/lib/commands/server.js +292 -0
  185. package/lib/commands/service.js +89 -0
  186. package/lib/commands/settings.js +99 -0
  187. package/lib/commands/setup.js +569 -0
  188. package/lib/commands/skillManifest.js +113 -0
  189. package/lib/commands/skillRightsFormat.js +38 -0
  190. package/lib/commands/skills.js +687 -0
  191. package/lib/commands/slot.js +1451 -0
  192. package/lib/commands/sso.js +229 -0
  193. package/lib/commands/status.js +137 -0
  194. package/lib/commands/tag.js +91 -0
  195. package/lib/commands/team.js +142 -0
  196. package/lib/commands/tg.js +240 -0
  197. package/lib/commands/tiktok.js +218 -0
  198. package/lib/commands/tips.js +23 -0
  199. package/lib/commands/tool.js +304 -0
  200. package/lib/commands/trace.js +530 -0
  201. package/lib/commands/trigger.js +223 -0
  202. package/lib/commands/update.js +46 -0
  203. package/lib/commands/vault.js +1447 -0
  204. package/lib/commands/vm.js +237 -0
  205. package/lib/commands/voice.js +213 -0
  206. package/lib/commands/volt.js +570 -0
  207. package/lib/commands/walkthrough.js +32 -0
  208. package/lib/commands/watch.js +167 -0
  209. package/lib/commands/webhook.js +137 -0
  210. package/lib/commands/welcome.js +14 -0
  211. package/lib/commands/workflow.js +660 -0
  212. package/lib/commands/x.js +448 -0
  213. package/lib/core/actingAs.js +136 -0
  214. package/lib/core/activeSummary.js +186 -0
  215. package/lib/core/aee.js +53 -0
  216. package/lib/core/agentAlias.js +163 -0
  217. package/lib/core/approvalContext.js +61 -0
  218. package/lib/core/aside.js +54 -0
  219. package/lib/core/askDecision.js +60 -0
  220. package/lib/core/auth.js +501 -0
  221. package/lib/core/banner.js +143 -0
  222. package/lib/core/brainPrime.js +90 -0
  223. package/lib/core/callerSource.js +129 -0
  224. package/lib/core/claudeLs.js +94 -0
  225. package/lib/core/cliBuild.js +62 -0
  226. package/lib/core/client.js +693 -0
  227. package/lib/core/commandRegistrars.js +121 -0
  228. package/lib/core/completions.js +163 -0
  229. package/lib/core/config.js +669 -0
  230. package/lib/core/confirm.js +37 -0
  231. package/lib/core/customHats.js +168 -0
  232. package/lib/core/deeplinks.js +76 -0
  233. package/lib/core/doneEngineFetch.js +227 -0
  234. package/lib/core/errors.js +441 -0
  235. package/lib/core/global-flags.js +190 -0
  236. package/lib/core/governedSession.js +402 -0
  237. package/lib/core/hyperion.js +102 -0
  238. package/lib/core/inboxResolution.js +65 -0
  239. package/lib/core/lazyCommands.js +122 -0
  240. package/lib/core/liveSessionsReport.js +112 -0
  241. package/lib/core/offload.js +504 -0
  242. package/lib/core/org-override.js +52 -0
  243. package/lib/core/orgResolve.js +70 -0
  244. package/lib/core/output.js +239 -0
  245. package/lib/core/peek.js +126 -0
  246. package/lib/core/prodEnv.js +7 -0
  247. package/lib/core/profileUsers.js +51 -0
  248. package/lib/core/promptCancel.js +18 -0
  249. package/lib/core/quoxglyph.js +158 -0
  250. package/lib/core/repoPath.js +11 -0
  251. package/lib/core/residentAlarm.js +282 -0
  252. package/lib/core/residentAuth.js +243 -0
  253. package/lib/core/residentPump.js +229 -0
  254. package/lib/core/residentSeed.js +106 -0
  255. package/lib/core/rootOptions.js +39 -0
  256. package/lib/core/router.js +168 -0
  257. package/lib/core/selfUpdate.js +188 -0
  258. package/lib/core/sessionHeartbeat.js +192 -0
  259. package/lib/core/sessionsOverview.js +224 -0
  260. package/lib/core/slotKeeper.js +527 -0
  261. package/lib/core/slotWake.js +120 -0
  262. package/lib/core/supervisorEngine.js +933 -0
  263. package/lib/core/systemdInstaller.js +256 -0
  264. package/lib/core/version.js +87 -0
  265. package/lib/core/vm.js +361 -0
  266. package/lib/core/vmTui.js +86 -0
  267. package/lib/core/walkthrough.js +35 -0
  268. package/lib/hyperion/deck.js +29 -0
  269. package/lib/lucid/LucidCard.jsx +68 -0
  270. package/lib/lucid/LucidIntro.jsx +92 -0
  271. package/lib/lucid/blind.js +71 -0
  272. package/lib/lucid/cards.js +86 -0
  273. package/lib/lucid/deck.js +32 -0
  274. package/lib/lucid/fmt.js +139 -0
  275. package/lib/lucid/markdown.js +81 -0
  276. package/lib/lucid/motion.js +56 -0
  277. package/lib/lucid/sitrep.js +173 -0
  278. package/lib/plugins/loader.js +93 -0
  279. package/lib/quoxon/activity.js +116 -0
  280. package/lib/quoxon/aocl.js +117 -0
  281. package/lib/quoxon/api.js +952 -0
  282. package/lib/quoxon/app.js +4032 -0
  283. package/lib/quoxon/approvalFacts.js +94 -0
  284. package/lib/quoxon/approvalsView.js +325 -0
  285. package/lib/quoxon/asideView.js +40 -0
  286. package/lib/quoxon/ask.js +121 -0
  287. package/lib/quoxon/away.js +52 -0
  288. package/lib/quoxon/bang.js +224 -0
  289. package/lib/quoxon/bulkRisk.js +17 -0
  290. package/lib/quoxon/capabilities.js +82 -0
  291. package/lib/quoxon/cells.js +53 -0
  292. package/lib/quoxon/commandShort.js +112 -0
  293. package/lib/quoxon/commands.js +479 -0
  294. package/lib/quoxon/connect.js +654 -0
  295. package/lib/quoxon/connectCard.js +45 -0
  296. package/lib/quoxon/consent.js +86 -0
  297. package/lib/quoxon/context.js +68 -0
  298. package/lib/quoxon/copy.js +34 -0
  299. package/lib/quoxon/countdownLine.js +11 -0
  300. package/lib/quoxon/demo.autoplay.js +29 -0
  301. package/lib/quoxon/demo.fixtures.js +24 -0
  302. package/lib/quoxon/demo.js +76 -0
  303. package/lib/quoxon/devObserver.js +20 -0
  304. package/lib/quoxon/dividerAocl.js +191 -0
  305. package/lib/quoxon/dividerRule.js +57 -0
  306. package/lib/quoxon/dragonModes.js +150 -0
  307. package/lib/quoxon/dragonView.js +41 -0
  308. package/lib/quoxon/earlyInput.js +40 -0
  309. package/lib/quoxon/echo.js +15 -0
  310. package/lib/quoxon/egg.js +36 -0
  311. package/lib/quoxon/events.js +677 -0
  312. package/lib/quoxon/feedback.js +50 -0
  313. package/lib/quoxon/gallery.js +237 -0
  314. package/lib/quoxon/handoffOffer.js +78 -0
  315. package/lib/quoxon/hatSwitch.js +37 -0
  316. package/lib/quoxon/headless.js +161 -0
  317. package/lib/quoxon/hostUrls.js +41 -0
  318. package/lib/quoxon/indicator.js +197 -0
  319. package/lib/quoxon/intent.js +52 -0
  320. package/lib/quoxon/keys.js +164 -0
  321. package/lib/quoxon/launcher.js +103 -0
  322. package/lib/quoxon/main.js +176 -0
  323. package/lib/quoxon/markdown.js +341 -0
  324. package/lib/quoxon/markdownSettle.js +19 -0
  325. package/lib/quoxon/matrixDomains.json +46 -0
  326. package/lib/quoxon/mentions.js +90 -0
  327. package/lib/quoxon/mind.js +103 -0
  328. package/lib/quoxon/mode.js +46 -0
  329. package/lib/quoxon/model.js +311 -0
  330. package/lib/quoxon/modelPrefs.js +345 -0
  331. package/lib/quoxon/motion/clock.js +70 -0
  332. package/lib/quoxon/motion/color.js +35 -0
  333. package/lib/quoxon/motion/frame.js +14 -0
  334. package/lib/quoxon/motion/level.js +91 -0
  335. package/lib/quoxon/motion/reveal.js +54 -0
  336. package/lib/quoxon/motion/ring.js +79 -0
  337. package/lib/quoxon/mouse.js +211 -0
  338. package/lib/quoxon/noai.js +57 -0
  339. package/lib/quoxon/objects/agent.js +61 -0
  340. package/lib/quoxon/objects/away.js +72 -0
  341. package/lib/quoxon/objects/crystal.js +100 -0
  342. package/lib/quoxon/objects/fetch.js +182 -0
  343. package/lib/quoxon/objects/fleet.js +149 -0
  344. package/lib/quoxon/objects/health.js +186 -0
  345. package/lib/quoxon/objects/index.js +106 -0
  346. package/lib/quoxon/objects/ladder.js +71 -0
  347. package/lib/quoxon/objects/memory.js +44 -0
  348. package/lib/quoxon/objects/mirror.js +137 -0
  349. package/lib/quoxon/objects/model.js +118 -0
  350. package/lib/quoxon/objects/peek.js +17 -0
  351. package/lib/quoxon/objects/proof.js +110 -0
  352. package/lib/quoxon/objects/prove.js +130 -0
  353. package/lib/quoxon/objects/replay.js +125 -0
  354. package/lib/quoxon/objects/sitrep.js +58 -0
  355. package/lib/quoxon/objects/skill.js +105 -0
  356. package/lib/quoxon/objects/view.js +87 -0
  357. package/lib/quoxon/objects/workflow.js +119 -0
  358. package/lib/quoxon/observers.js +42 -0
  359. package/lib/quoxon/onboarding/copy.js +263 -0
  360. package/lib/quoxon/onboarding/credential.js +108 -0
  361. package/lib/quoxon/onboarding/detect.js +22 -0
  362. package/lib/quoxon/onboarding/dockerOffer.js +45 -0
  363. package/lib/quoxon/onboarding/failureCause.js +33 -0
  364. package/lib/quoxon/onboarding/finish.js +134 -0
  365. package/lib/quoxon/onboarding/index.js +94 -0
  366. package/lib/quoxon/onboarding/installRunner.js +284 -0
  367. package/lib/quoxon/onboarding/intent.js +53 -0
  368. package/lib/quoxon/onboarding/journey.js +73 -0
  369. package/lib/quoxon/onboarding/preflight.js +156 -0
  370. package/lib/quoxon/onboarding/probe.js +65 -0
  371. package/lib/quoxon/onboarding/runner.js +58 -0
  372. package/lib/quoxon/onboarding/view.js +682 -0
  373. package/lib/quoxon/orgLabel.js +10 -0
  374. package/lib/quoxon/orgSwitch.js +33 -0
  375. package/lib/quoxon/ownBroadcast.js +22 -0
  376. package/lib/quoxon/palette.js +43 -0
  377. package/lib/quoxon/print.js +98 -0
  378. package/lib/quoxon/profileSwitch.js +53 -0
  379. package/lib/quoxon/promptHistory.js +41 -0
  380. package/lib/quoxon/providers.js +48 -0
  381. package/lib/quoxon/qart.js +348 -0
  382. package/lib/quoxon/qdragon.js +410 -0
  383. package/lib/quoxon/qdragonFly.js +61 -0
  384. package/lib/quoxon/qdragonScenes.js +282 -0
  385. package/lib/quoxon/qeyes.js +213 -0
  386. package/lib/quoxon/qglyph.js +143 -0
  387. package/lib/quoxon/qhello.js +58 -0
  388. package/lib/quoxon/qmotion.js +132 -0
  389. package/lib/quoxon/render.js +1167 -0
  390. package/lib/quoxon/resend.js +72 -0
  391. package/lib/quoxon/roster.js +307 -0
  392. package/lib/quoxon/route.js +39 -0
  393. package/lib/quoxon/rows.js +711 -0
  394. package/lib/quoxon/rowsStore.js +80 -0
  395. package/lib/quoxon/rules.js +130 -0
  396. package/lib/quoxon/screenContext.js +150 -0
  397. package/lib/quoxon/screenFrames.js +254 -0
  398. package/lib/quoxon/session.js +238 -0
  399. package/lib/quoxon/sessionDead.js +17 -0
  400. package/lib/quoxon/signin.js +125 -0
  401. package/lib/quoxon/skills.js +30 -0
  402. package/lib/quoxon/slash/agents.js +112 -0
  403. package/lib/quoxon/slash/argSources.js +65 -0
  404. package/lib/quoxon/slash/args.js +65 -0
  405. package/lib/quoxon/slash/automate.js +89 -0
  406. package/lib/quoxon/slash/broadcast.js +140 -0
  407. package/lib/quoxon/slash/broadcastRun.js +108 -0
  408. package/lib/quoxon/slash/card.js +30 -0
  409. package/lib/quoxon/slash/challenge.js +114 -0
  410. package/lib/quoxon/slash/cliCommand.js +53 -0
  411. package/lib/quoxon/slash/cliLeaves.json +948 -0
  412. package/lib/quoxon/slash/compact.js +90 -0
  413. package/lib/quoxon/slash/confirm.js +30 -0
  414. package/lib/quoxon/slash/crystallize.js +69 -0
  415. package/lib/quoxon/slash/dashboard.js +136 -0
  416. package/lib/quoxon/slash/draft.js +54 -0
  417. package/lib/quoxon/slash/hatWalk.js +206 -0
  418. package/lib/quoxon/slash/inverses.js +77 -0
  419. package/lib/quoxon/slash/killer.js +24 -0
  420. package/lib/quoxon/slash/killerApi.js +16 -0
  421. package/lib/quoxon/slash/ledger.js +88 -0
  422. package/lib/quoxon/slash/listView.js +30 -0
  423. package/lib/quoxon/slash/modelCmd.js +261 -0
  424. package/lib/quoxon/slash/palette.js +118 -0
  425. package/lib/quoxon/slash/paletteController.js +169 -0
  426. package/lib/quoxon/slash/panels.js +116 -0
  427. package/lib/quoxon/slash/picker.js +56 -0
  428. package/lib/quoxon/slash/restart.js +202 -0
  429. package/lib/quoxon/slash/resume.js +59 -0
  430. package/lib/quoxon/slash/rowCmd.js +157 -0
  431. package/lib/quoxon/slash/rowWalk.js +175 -0
  432. package/lib/quoxon/slash/settings.js +60 -0
  433. package/lib/quoxon/slash/spawn.js +177 -0
  434. package/lib/quoxon/slash/stepUp.js +274 -0
  435. package/lib/quoxon/slash/stepUpCard.js +60 -0
  436. package/lib/quoxon/slash/stepUpCmds.js +78 -0
  437. package/lib/quoxon/slash/undoApi.js +38 -0
  438. package/lib/quoxon/slash/undoCmd.js +49 -0
  439. package/lib/quoxon/slash/undoWish.js +15 -0
  440. package/lib/quoxon/slash/watch.js +164 -0
  441. package/lib/quoxon/slash/wish.js +23 -0
  442. package/lib/quoxon/slash/wishCmd.js +32 -0
  443. package/lib/quoxon/slotChat.js +300 -0
  444. package/lib/quoxon/slotFormat.js +195 -0
  445. package/lib/quoxon/slotRun.js +131 -0
  446. package/lib/quoxon/sound/index.js +186 -0
  447. package/lib/quoxon/sound/mapping.js +120 -0
  448. package/lib/quoxon/sound/output.js +52 -0
  449. package/lib/quoxon/sound/synth.js +122 -0
  450. package/lib/quoxon/stray.js +22 -0
  451. package/lib/quoxon/suggestCommand.js +59 -0
  452. package/lib/quoxon/term.js +141 -0
  453. package/lib/quoxon/textBuffer.js +416 -0
  454. package/lib/quoxon/theme.js +286 -0
  455. package/lib/quoxon/tips.js +433 -0
  456. package/lib/quoxon/toolError.js +41 -0
  457. package/lib/quoxon/transcript.js +188 -0
  458. package/lib/quoxon/web/qface.js +89 -0
  459. package/lib/quoxon/welcome.js +52 -0
  460. package/lib/quoxon/whatsnew.js +136 -0
  461. package/lib/quoxon/whatsnew.jsonl +89 -0
  462. package/lib/quoxon/windowOrg.js +113 -0
  463. package/lib/server/diag.js +129 -0
  464. package/lib/server/doctor.js +71 -0
  465. package/lib/server/install.js +149 -0
  466. package/lib/server/pluginEnable.js +161 -0
  467. package/lib/server/skew.js +56 -0
  468. package/lib/server/snapshot.js +131 -0
  469. package/lib/server/status.js +128 -0
  470. package/lib/server/sys.js +65 -0
  471. package/lib/server/uninstall.js +63 -0
  472. package/lib/server/update.js +234 -0
  473. package/lib/toolOutputShape.js +315 -0
  474. package/lib/tui/app.jsx +289 -0
  475. package/lib/tui/components/Breadcrumb.jsx +42 -0
  476. package/lib/tui/components/ChatInput.jsx +68 -0
  477. package/lib/tui/components/Confirm.jsx +32 -0
  478. package/lib/tui/components/DataTable.jsx +270 -0
  479. package/lib/tui/components/DetailPanel.jsx +241 -0
  480. package/lib/tui/components/HelpOverlay.jsx +67 -0
  481. package/lib/tui/components/Layout.jsx +129 -0
  482. package/lib/tui/components/Palette.jsx +141 -0
  483. package/lib/tui/components/SearchInput.jsx +125 -0
  484. package/lib/tui/components/Sparkline.jsx +55 -0
  485. package/lib/tui/components/StatusBar.jsx +133 -0
  486. package/lib/tui/components/Toast.jsx +52 -0
  487. package/lib/tui/hooks/useApi.js +44 -0
  488. package/lib/tui/hooks/useHealth.js +90 -0
  489. package/lib/tui/hooks/usePolledApi.js +86 -0
  490. package/lib/tui/hooks/useTerminalSize.js +46 -0
  491. package/lib/tui/index.js +56 -0
  492. package/lib/tui/intro/Intro.jsx +332 -0
  493. package/lib/tui/jsx-loader.js +32 -0
  494. package/lib/tui/keys.js +87 -0
  495. package/lib/tui/registry.js +49 -0
  496. package/lib/tui/theme.js +103 -0
  497. package/lib/tui/views/AdminView.jsx +398 -0
  498. package/lib/tui/views/AgentView.jsx +204 -0
  499. package/lib/tui/views/BackupView.jsx +225 -0
  500. package/lib/tui/views/BrainView.jsx +223 -0
  501. package/lib/tui/views/ChatView.jsx +468 -0
  502. package/lib/tui/views/FilesView.jsx +218 -0
  503. package/lib/tui/views/FleetView.jsx +234 -0
  504. package/lib/tui/views/GovernanceView.jsx +453 -0
  505. package/lib/tui/views/HealthView.jsx +216 -0
  506. package/lib/tui/views/HyperionDeck.jsx +79 -0
  507. package/lib/tui/views/InboxView.jsx +228 -0
  508. package/lib/tui/views/LucidDeck.jsx +719 -0
  509. package/lib/tui/views/MainMenu.jsx +123 -0
  510. package/lib/tui/views/MemoryView.jsx +377 -0
  511. package/lib/tui/views/ObjectivesView.jsx +230 -0
  512. package/lib/tui/views/OrgView.jsx +229 -0
  513. package/lib/tui/views/ToolView.jsx +221 -0
  514. package/lib/tui/views/VaultView.jsx +422 -0
  515. package/lib/tui/views/WorkflowView.jsx +329 -0
  516. package/package.json +75 -4
@@ -0,0 +1,711 @@
1
+ /**
2
+ * QUOXON-FW17: custom rows. User-defined lines rendered just above the status line.
3
+ *
4
+ * A row is { id, label?, source: { type: 'url', url, every } | { type: 'cmd', cmd, every }, template, colour? }.
5
+ * The template pulls values out of the fetched JSON (or the command's stdout, as {{text}}) with a tiny
6
+ * SAFE evaluator: {{a.b[0].c}}, {{count(list)}}, {{count(list[field=value])}}, list[field~text] (contains, any
7
+ * case, a plain includes), list[*] (every item) and {a,b} (only these fields). Nothing is eval'd.
8
+ *
9
+ * Sync split (a security line, enforced here and tested): url rows sync per user (preference namespace
10
+ * quoxonRows); cmd rows are LOCAL ONLY. normRows() on anything that arrives from the server drops every cmd
11
+ * row, so a synced row can never make another machine run a shell command.
12
+ *
13
+ * ROWS (2026-10-17): the status-row contract. A row with NO template reads the public JSON shape
14
+ * { text, label?, value?, tone?: ok|warn|error|info|neutral, trend?: up|down|flat|<number>, link? }
15
+ * (docs/STATUS_ROWS.md). Plain text output counts as { text }. Two more keyless, sync-safe sources:
16
+ * quox: { type: 'quox', service: auth|collector, path: '/api/...{org}...' } your own Quox, with your session
17
+ * plugin: { type: 'plugin', plugin: '<id>', path: '<relative>' } GET /api/v1/plugins/<id>/<path>
18
+ * Everything a row prints passes cleanText(): terminal escapes and control characters never reach the screen.
19
+ */
20
+ import { execFile } from 'node:child_process';
21
+ import { homedir } from 'node:os';
22
+ import { join, dirname } from 'node:path';
23
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
24
+
25
+ export const MAX_ROWS = 3;
26
+ export const FETCH_TIMEOUT_MS = 5000;
27
+ /** url rows fetch from busy boxes (the proof board is ~400 KB): 15 s. cmd rows keep FETCH_TIMEOUT_MS. */
28
+ export const URL_TIMEOUT_MS = 15000;
29
+ /** A failed refresh keeps showing the last good value (with an age marker) for this long, then warns. */
30
+ export const STALE_MAX_MS = 30 * 60 * 1000;
31
+ export const BACKOFF_CAP_MS = 10 * 60 * 1000;
32
+ export const OUTPUT_CAP = 64 * 1024;
33
+ /** A JSON body is parsed whole or not at all: cutting it leaves nothing to read (2026-10-14: the proof board grew to 347 KB and the row showed only dashes). */
34
+ export const JSON_CAP = 2 * 1024 * 1024;
35
+ export const ROLES = ['prose', 'command', 'value', 'ok', 'warn', 'error', 'brand', 'quiet'];
36
+ const ROLE_TO_THEME = { value: 'id' }; // palette role -> theme.js name
37
+ export const themeRole = (r) => ROLE_TO_THEME[r] || r || 'prose';
38
+
39
+ const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
40
+
41
+ /* ---- what a row may print ------------------------------------------------------------------ */
42
+
43
+ // CSI, OSC (title, hyperlink, clipboard), DCS/SOS/PM/APC strings and lone 2-byte escapes.
44
+ const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[PX^_][^\x1b]*(?:\x1b\\)?|\x1b[@-Z\\-_]?/g;
45
+ // C0, DEL, C1, zero-width space, LRM/RLM, bidi embeddings, overrides and isolates, BOM. ZWJ stays (emoji).
46
+ const CTRL_RE = /[\u0000-\u001f\u007f-\u009f\u200b\u200e\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g;
47
+
48
+ /**
49
+ * Text from a URL, a command or a plugin -> one safe line. A remote endpoint must never be able to move the
50
+ * cursor, retitle the terminal, write the clipboard (OSC 52) or reorder text with bidi overrides.
51
+ */
52
+ export function cleanText(v, max = 500) {
53
+ return String(v ?? '').replace(ANSI_RE, '').replace(/[\t\n\r\v\f]+/g, ' ').replace(CTRL_RE, '').replace(/\s+/g, ' ').trim().slice(0, max);
54
+ }
55
+
56
+ /* ---- durations --------------------------------------------------------------------------- */
57
+
58
+ /** '60s' | '5m' | '1h' -> ms, or null. Floor 5 s, ceiling 1 day. */
59
+ export function parseEvery(s) {
60
+ const m = /^(\d+)\s*(s|m|h)$/i.exec(String(s ?? '').trim());
61
+ if (!m) return null;
62
+ const ms = Number(m[1]) * { s: 1000, m: 60000, h: 3600000 }[m[2].toLowerCase()];
63
+ return ms >= 5000 && ms <= 86400000 ? ms : null;
64
+ }
65
+
66
+ /* ---- the template evaluator ------------------------------------------------------------- */
67
+
68
+ /** A path -> steps, or null when malformed. */
69
+ export function parsePath(src) {
70
+ const s = String(src ?? '').trim();
71
+ if (!s) return null;
72
+ const steps = [];
73
+ let i = 0;
74
+ while (i < s.length) {
75
+ if (s[i] === '.') { i++; continue; }
76
+ if (s[i] === '[') {
77
+ const end = s.indexOf(']', i);
78
+ if (end < 0) return null;
79
+ const inner = s.slice(i + 1, end).trim();
80
+ i = end + 1;
81
+ if (/^\d+$/.test(inner)) steps.push({ index: Number(inner) });
82
+ else if (inner === '*') steps.push({ wildcard: true }); // list[*]: every item
83
+ else {
84
+ const eq = /^([^=~\s\[\]]+)\s*=\s*(.*)$/.exec(inner);
85
+ if (eq) steps.push({ field: eq[1], value: eq[2].trim().replace(/^(["'])(.*)\1$/, '$2') });
86
+ else {
87
+ // list[field~text]: case-insensitive contains, a plain includes (never a regex from input).
88
+ const ct = /^([^=~\s\[\]]+)\s*~\s*(.*)$/.exec(inner);
89
+ if (!ct) return null;
90
+ const text = ct[2].trim().replace(/^(["'])(.*)\1$/, '$2');
91
+ if (!text) return null;
92
+ steps.push({ field: ct[1], contains: text.toLowerCase() });
93
+ }
94
+ }
95
+ continue;
96
+ }
97
+ if (s[i] === '{') {
98
+ // #1044: {a,b,c} keeps only these fields (of every item when on a list).
99
+ const end = s.indexOf('}', i);
100
+ if (end < 0) return null;
101
+ const fields = s.slice(i + 1, end).split(',').map((f) => f.trim());
102
+ if (!fields.length || fields.some((f) => !/^[A-Za-z0-9_-]+$/.test(f))) return null;
103
+ steps.push({ fields });
104
+ i = end + 1;
105
+ continue;
106
+ }
107
+ let j = i;
108
+ while (j < s.length && s[j] !== '.' && s[j] !== '[') j++;
109
+ const key = s.slice(i, j).trim();
110
+ if (!key || /[\s(){}=,;+*\/]/.test(key)) return null;
111
+ steps.push({ key });
112
+ i = j;
113
+ }
114
+ return steps.length ? steps : null;
115
+ }
116
+
117
+ const own = (o, k) => o !== null && typeof o === 'object' && !Array.isArray(o) && !BLOCKED_KEYS.has(k) && Object.hasOwn(o, k);
118
+
119
+ function walk(data, steps) {
120
+ let cur = data;
121
+ for (const st of steps) {
122
+ if (cur === undefined || cur === null) return undefined;
123
+ if ('key' in st) { if (!own(cur, st.key)) return undefined; cur = cur[st.key]; }
124
+ else if ('index' in st) { if (!Array.isArray(cur)) return undefined; cur = cur[st.index]; }
125
+ else if (st.wildcard) { if (!Array.isArray(cur)) return undefined; }
126
+ else if (st.fields) {
127
+ const pick = (o) => { const r = {}; for (const f of st.fields) if (own(o, f)) r[f] = o[f]; return r; };
128
+ cur = Array.isArray(cur) ? cur.map(pick) : pick(cur);
129
+ }
130
+ else {
131
+ if (!Array.isArray(cur)) return undefined;
132
+ const sub = parsePath(st.field);
133
+ cur = cur.filter((it) => {
134
+ const v = sub ? walk(it, sub) : undefined;
135
+ if (v === undefined || v === null || typeof v === 'object') return false;
136
+ return 'contains' in st ? String(v).toLowerCase().includes(st.contains) : String(v) === st.value;
137
+ });
138
+ }
139
+ }
140
+ return cur;
141
+ }
142
+
143
+ /** 1234 -> 1.2k, 12345 -> 12k, 1500000 -> 1.5M. Small numbers stay as they are. */
144
+ export function fmtNum(n) {
145
+ if (!Number.isFinite(n)) return String(n);
146
+ const a = Math.abs(n);
147
+ if (a < 1000) return String(Number.isInteger(n) ? n : Number(n.toFixed(1)));
148
+ for (const [u, sfx] of [[1e12, 'T'], [1e9, 'B'], [1e6, 'M'], [1e3, 'k']]) {
149
+ if (a >= u) {
150
+ const v = n / u;
151
+ return `${Math.abs(v) < 10 ? v.toFixed(1).replace(/\.0$/, '') : Math.round(v)}${sfx}`;
152
+ }
153
+ }
154
+ return String(n);
155
+ }
156
+
157
+ const show = (v) => {
158
+ if (v === undefined || v === null) return '–';
159
+ if (typeof v === 'number') return fmtNum(v);
160
+ if (typeof v === 'string') return cleanText(v);
161
+ if (typeof v === 'boolean') return String(v);
162
+ if (Array.isArray(v)) return String(v.length);
163
+ return '{…}';
164
+ };
165
+
166
+ /** One {{ expr }} body -> { ok, value } | { ok: false, error }. */
167
+ function parseExpr(body) {
168
+ const e = String(body ?? '').trim();
169
+ const fn = /^([A-Za-z_]+)\s*\(([\s\S]*)\)$/.exec(e);
170
+ if (fn) {
171
+ if (fn[1] !== 'count') return { error: `unknown function ${fn[1]}() (only count)` };
172
+ const steps = parsePath(fn[2]);
173
+ return steps ? { fn: 'count', steps } : { error: 'count() needs a path' };
174
+ }
175
+ const steps = parsePath(e);
176
+ return steps ? { steps } : { error: e ? `bad path "${e}"` : 'empty {{ }}' };
177
+ }
178
+
179
+ const countOf = (v) => (Array.isArray(v) ? v.length : v && typeof v === 'object' ? Object.keys(v).length : undefined);
180
+
181
+ /** Syntax check for a template at add time -> an error string or null. */
182
+ export function checkTemplate(tpl) {
183
+ const t = String(tpl ?? '');
184
+ if (!t.trim()) return 'The template is empty.';
185
+ if ((t.match(/\{\{/g) || []).length !== (t.match(/\}\}/g) || []).length) return 'Unbalanced {{ }} in the template.';
186
+ for (const m of t.matchAll(/\{\{([\s\S]*?)\}\}/g)) { const p = parseExpr(m[1]); if (p.error) return p.error; }
187
+ return null;
188
+ }
189
+
190
+ /** Fill a template from parsed data. Unknown paths show as "–". Never throws. */
191
+ export function evalTemplate(tpl, data) {
192
+ return String(tpl ?? '').replace(/\{\{([\s\S]*?)\}\}/g, (_, body) => {
193
+ const p = parseExpr(body);
194
+ if (p.error) return '–';
195
+ const v = walk(data, p.steps);
196
+ return show(p.fn === 'count' ? countOf(v) : v);
197
+ });
198
+ }
199
+
200
+ /** Raw text (a response body or stdout) -> the object the template reads: parsed JSON, else { text }. */
201
+ export function dataFrom(raw) {
202
+ const text = String(raw ?? '');
203
+ const t = text.trim();
204
+ if (t.startsWith('{') || t.startsWith('[')) { try { const j = JSON.parse(t); if (j && typeof j === 'object') return j; } catch { /* plain text */ } }
205
+ return { text: t };
206
+ }
207
+
208
+ /* ---- the status-row contract (docs/STATUS_ROWS.md) ---------------------------------------- */
209
+
210
+ export const STATUS_ROW_TONES = ['ok', 'warn', 'error', 'info', 'neutral'];
211
+ const TONE_ROLE = { ok: 'ok', warn: 'warn', error: 'error', info: 'command', neutral: 'prose' };
212
+ const TONE_GLYPH = { ok: '✔', warn: '⚠', error: '✖', info: '◆' };
213
+ export const toneRole = (t) => TONE_ROLE[t] || null;
214
+
215
+ const trendText = (t) => {
216
+ if (t === 'up') return '▲';
217
+ if (t === 'down') return '▼';
218
+ if (t === 'flat') return '▬';
219
+ if (typeof t === 'number' && Number.isFinite(t)) return t > 0 ? `▲${fmtNum(t)}` : t < 0 ? `▼${fmtNum(-t)}` : '▬';
220
+ return '';
221
+ };
222
+
223
+ /**
224
+ * Parsed data -> { seg, warnings } | { error }. Lenient on read (an unknown tone shows neutral and is named
225
+ * in warnings, so `quox rows test` can say so); strict on what it prints (everything passes cleanText).
226
+ */
227
+ export function parseStatusRow(d) {
228
+ if (!d || typeof d !== 'object' || Array.isArray(d)) return { error: 'A status row is a JSON object: {"text": "..."}.' };
229
+ const warnings = [];
230
+ const seg = {};
231
+ const text = typeof d.text === 'string' ? cleanText(d.text, 160) : typeof d.text === 'number' ? fmtNum(d.text) : '';
232
+ if (text) seg.text = text;
233
+ if (typeof d.value === 'number' && Number.isFinite(d.value)) seg.value = fmtNum(d.value);
234
+ else if (typeof d.value === 'string' && cleanText(d.value, 24)) seg.value = cleanText(d.value, 24);
235
+ else if (d.value !== undefined && d.value !== null) warnings.push('"value" is a number or a short string; ignored.');
236
+ if (!seg.text && !seg.value) return { error: 'Not a status row: it needs "text" (see quox rows contract).' };
237
+ if (d.tone !== undefined && d.tone !== null) {
238
+ if (STATUS_ROW_TONES.includes(d.tone)) { if (d.tone !== 'neutral') seg.tone = d.tone; }
239
+ else warnings.push(`"tone" is one of ${STATUS_ROW_TONES.join(', ')}; "${cleanText(d.tone, 16)}" shows as neutral.`);
240
+ }
241
+ if (d.trend !== undefined && d.trend !== null) {
242
+ const tt = trendText(d.trend);
243
+ if (tt) seg.trend = tt; else warnings.push('"trend" is up, down, flat or a number; ignored.');
244
+ }
245
+ if (typeof d.label === 'string' && cleanText(d.label, 16)) seg.label = cleanText(d.label, 16);
246
+ if (d.link !== undefined && d.link !== null) {
247
+ let ok = false;
248
+ try { const u = new URL(String(d.link)); ok = (u.protocol === 'http:' || u.protocol === 'https:') && !u.username && !u.password && String(d.link).length <= 500; } catch { ok = false; }
249
+ if (ok) seg.link = new URL(String(d.link)).href; else warnings.push('"link" must be an http(s) URL with no credentials; ignored.');
250
+ }
251
+ return { seg, warnings };
252
+ }
253
+
254
+ /**
255
+ * Data that is not a status row -> the paths a template could use (scalars, one level deep), for a helpful
256
+ * "try --template" hint. -> ['open', 'connection.connected', ...] (at most `max`).
257
+ */
258
+ export function fieldHints(data, max = 12) {
259
+ const out = [];
260
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return out;
261
+ for (const [k, v] of Object.entries(data)) {
262
+ if (out.length >= max) break;
263
+ if (!/^[A-Za-z0-9_-]+$/.test(k) || BLOCKED_KEYS.has(k)) continue;
264
+ if (v !== null && typeof v !== 'object') out.push(k);
265
+ else if (Array.isArray(v)) out.push(`count(${k})`);
266
+ else if (v && typeof v === 'object') for (const [k2, v2] of Object.entries(v)) {
267
+ if (out.length >= max) break;
268
+ if (/^[A-Za-z0-9_-]+$/.test(k2) && !BLOCKED_KEYS.has(k2) && v2 !== null && typeof v2 !== 'object') out.push(`${k}.${k2}`);
269
+ }
270
+ }
271
+ return out;
272
+ }
273
+
274
+ /** seg -> the one-line text (screen context, /row list, tests): "value text trend". */
275
+ export const segText = (seg) => [seg.value, seg.text, seg.trend].filter(Boolean).join(' ');
276
+
277
+ /**
278
+ * A row + the raw text its source returned -> { ok, text, seg, warnings } | { ok: false, error }.
279
+ * With a template: the template fills from the data. Without one: the status-row contract.
280
+ */
281
+ export function rowValue(row, raw) {
282
+ // Empty output is not a value (owner frame 2026-10-17: "QPE │" drew blank). The engine keeps the last good value.
283
+ if (!cleanText(raw)) return { ok: false, error: 'no output' };
284
+ const data = dataFrom(raw);
285
+ if (row.template) {
286
+ const text = evalTemplate(row.template, data);
287
+ return text.trim() ? { ok: true, text, seg: null, warnings: [] } : { ok: false, error: 'no output' };
288
+ }
289
+ const p = parseStatusRow(data);
290
+ if (p.error) return { ok: false, error: p.error };
291
+ return { ok: true, text: segText(p.seg), seg: p.seg, warnings: p.warnings };
292
+ }
293
+
294
+ /* ---- row records -------------------------------------------------------------------------- */
295
+
296
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,23}$/;
297
+ export const validId = (s) => ID_RE.test(String(s ?? ''));
298
+ export const SOURCE_TYPES = ['url', 'cmd', 'quox', 'plugin'];
299
+ export const QUOX_SERVICES = ['auth', 'collector'];
300
+ const PLUGIN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
301
+
302
+ /** A quox or plugin path -> an error string or null. GET only, no traversal, no other host. */
303
+ function badPath(path, { absolute }) {
304
+ if (!path) return 'The path is empty.';
305
+ if (path.length > 300) return 'That path is too long (300 characters).';
306
+ if (/[\s#\\]/.test(path) || /[\u0000-\u001f\u007f]/.test(path)) return 'A path has no spaces, # or backslashes.';
307
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith('//')) return 'A path, not a URL: it is read from your own Quox.';
308
+ if (path.split(/[/?]/).some((seg) => seg === '..' || seg === '.' || /%2e%2e|%2f/i.test(seg))) return 'A path cannot step out with .. or encoded slashes.';
309
+ if (absolute && !path.startsWith('/api/')) return 'A quox path starts with /api/.';
310
+ if (!/^[A-Za-z0-9._~\-/?=&%{}:,+]+$/.test(path)) return 'That path has characters a row path cannot use.';
311
+ return null;
312
+ }
313
+
314
+ /** True when a row reads data that is safe to sync and share (no shell). */
315
+ export const syncable = (r) => r?.source?.type !== 'cmd';
316
+
317
+ // Names a secret-bearing query parameter, or a value that looks like a token.
318
+ const SECRET_KEY_RE = /^(.*[_-])?(token|access[_-]?token|api[_-]?key|apikey|key|secret|password|passwd|pwd|sig|signature|auth|authorization|session|sessionid|jwt|bearer|credential|creds)$/i;
319
+ const SECRET_VAL_RE = /^(eyJ[A-Za-z0-9_-]{8,}\.|(sk|pk|ghp|gho|xox[abp]|qmcp)[-_][A-Za-z0-9]|[A-Fa-f0-9]{32,}$|[A-Za-z0-9+/_-]{40,}={0,2}$)/;
320
+
321
+ /**
322
+ * Never put a secret in a row definition (it syncs to your account and can be shared). -> a reason or null.
323
+ * Checked when a row is ADDED, EDITED, IMPORTED or SHARED; a stored row is never silently dropped for it.
324
+ */
325
+ export function secretInRow(v) {
326
+ const src = v?.source || {};
327
+ const target = src.type === 'url' ? src.url : (src.type === 'quox' || src.type === 'plugin') ? src.path : null;
328
+ if (typeof target === 'string') {
329
+ let q = '';
330
+ try { q = src.type === 'url' ? new URL(target).search : (target.split('?')[1] || ''); } catch { q = ''; }
331
+ for (const [k, val] of new URLSearchParams(q)) {
332
+ if (SECRET_KEY_RE.test(k)) return `The "${k}" parameter looks like a secret. Rows never carry secrets: use a ${src.type === 'url' ? 'quox or plugin row (your session is used)' : 'path without it'} or a local command that reads the secret itself.`;
333
+ if (SECRET_VAL_RE.test(val)) return `The value of "${k}" looks like a token. Rows never carry secrets.`;
334
+ }
335
+ }
336
+ if (src.type === 'cmd' && typeof src.cmd === 'string' && /(authorization:\s*bearer\s+\S{8,}|\b(sk|ghp|xox[abp]|qmcp)[-_][A-Za-z0-9]{8,}|\beyJ[A-Za-z0-9_-]{8,}\.)/i.test(src.cmd))
337
+ return 'That command has a token written into it. Read the secret inside your script (from a file or the vault), never in the row.';
338
+ return null;
339
+ }
340
+
341
+ /** Validate one row -> { row } | { error }. Strict: used for typed input and for everything stored. */
342
+ export function checkRow(v) {
343
+ const id = typeof v?.id === 'string' ? v.id.trim() : '';
344
+ if (!validId(id)) return { error: 'Row ids are letters, digits, - and _ (24 at most).' };
345
+ const src = v?.source;
346
+ const every = src?.every === undefined ? '60s' : String(src.every).trim();
347
+ if (parseEvery(every) === null) return { error: `"${every}" is not an interval. Use 30s, 5m or 1h (5s to 1 day).` };
348
+ let source;
349
+ if (src?.type === 'url') {
350
+ let u;
351
+ try { u = new URL(String(src.url)); } catch { return { error: 'That is not a URL.' }; }
352
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return { error: 'Only http and https URLs.' };
353
+ if (u.username || u.password) return { error: 'Put no credentials in the URL; none are sent.' };
354
+ if (String(src.url).length > 500) return { error: 'That URL is too long.' };
355
+ source = { type: 'url', url: u.href, every };
356
+ } else if (src?.type === 'cmd') {
357
+ const cmd = typeof src.cmd === 'string' ? src.cmd.trim() : '';
358
+ if (!cmd || cmd.length > 500) return { error: 'The command must be 1 to 500 characters.' };
359
+ source = { type: 'cmd', cmd, every };
360
+ } else if (src?.type === 'quox') {
361
+ const service = src.service === undefined ? 'auth' : String(src.service);
362
+ if (!QUOX_SERVICES.includes(service)) return { error: `A quox row reads the ${QUOX_SERVICES.join(' or ')} service.` };
363
+ const path = typeof src.path === 'string' ? src.path.trim() : '';
364
+ const bad = badPath(path, { absolute: true });
365
+ if (bad) return { error: bad };
366
+ source = { type: 'quox', service, path, every };
367
+ } else if (src?.type === 'plugin') {
368
+ const plugin = typeof src.plugin === 'string' ? src.plugin.trim() : '';
369
+ if (!PLUGIN_ID_RE.test(plugin)) return { error: 'A plugin id is letters, digits, dots, - and _ (64 at most).' };
370
+ const path = typeof src.path === 'string' ? src.path.trim().replace(/^\/+/, '') : '';
371
+ const bad = badPath(path, { absolute: false });
372
+ if (bad) return { error: bad };
373
+ source = { type: 'plugin', plugin, path, every };
374
+ } else return { error: `A row source is one of ${SOURCE_TYPES.join(', ')}.` };
375
+ // No template = the status-row contract ({ text, value?, tone?, trend?, link? }, or plain text).
376
+ const template = typeof v.template === 'string' ? v.template.trim() : '';
377
+ if (template.length > 300) return { error: 'The template is too long (300 characters).' };
378
+ if (template) { const bad = checkTemplate(template); if (bad) return { error: bad }; }
379
+ const row = { id, source };
380
+ if (template) row.template = template;
381
+ const label = typeof v.label === 'string' ? v.label.trim().slice(0, 24) : '';
382
+ if (label) row.label = label;
383
+ if (v.colour !== undefined && v.colour !== null && v.colour !== '') {
384
+ if (!ROLES.includes(v.colour)) return { error: `Colour is one of ${ROLES.join(', ')}.` };
385
+ if (v.colour !== 'prose') row.colour = v.colour;
386
+ }
387
+ return { row };
388
+ }
389
+
390
+ /**
391
+ * Anything stored or received -> clean rows (bad ones dropped, duplicate ids dropped, capped).
392
+ * cmd rows survive ONLY with { cmd: true }; the server copy is always read without it.
393
+ */
394
+ export function normRows(v, { cmd = false } = {}) {
395
+ const out = [];
396
+ for (const x of Array.isArray(v) ? v : Array.isArray(v?.rows) ? v.rows : []) {
397
+ if (x?.source?.type === 'cmd' && !cmd) continue;
398
+ const r = checkRow(x);
399
+ if (r.row && !out.some((o) => o.id === r.row.id) && out.length < MAX_ROWS) out.push(r.row);
400
+ }
401
+ return out;
402
+ }
403
+
404
+ /** url = every row that syncs (url, quox, plugin); cmd = this machine only. */
405
+ export const splitRows = (rows) => ({
406
+ url: rows.filter(syncable),
407
+ cmd: rows.filter((r) => !syncable(r))
408
+ });
409
+ export const rowLabel = (r) => r.label || r.id;
410
+
411
+ /* ---- /row argument parsing ---------------------------------------------------------------- */
412
+
413
+ /** Quote-aware split: "double" and 'single' quotes group, \" escapes. -> tokens, or null on an open quote. */
414
+ export function tokenize(s) {
415
+ const out = [];
416
+ let i = 0;
417
+ const str = String(s ?? '');
418
+ while (i < str.length) {
419
+ if (/\s/.test(str[i])) { i++; continue; }
420
+ let tok = '';
421
+ if (str[i] === '"' || str[i] === "'") {
422
+ const q = str[i++];
423
+ let closed = false;
424
+ while (i < str.length) {
425
+ if (str[i] === '\\' && i + 1 < str.length && (str[i + 1] === q || str[i + 1] === '\\')) { tok += str[i + 1]; i += 2; continue; }
426
+ if (str[i] === q) { closed = true; i++; break; }
427
+ tok += str[i++];
428
+ }
429
+ if (!closed) return null;
430
+ } else while (i < str.length && !/\s/.test(str[i])) tok += str[i++];
431
+ out.push(tok);
432
+ }
433
+ return out;
434
+ }
435
+
436
+ /**
437
+ * The words after /row -> { kind, ... }. kinds: list add rm on off test error.
438
+ * add: /row add <id> url <url> "<template>" [every 60s] [label X] [colour role]
439
+ * /row add <id> cmd "<shell cmd>" "<template>" [every 60s] ...
440
+ */
441
+ export function parseRowCmd(arg) {
442
+ const toks = tokenize(arg);
443
+ if (!toks) return { kind: 'error', text: 'A quote is not closed.' };
444
+ const sub = (toks[0] || 'list').toLowerCase();
445
+ if (sub === 'list' || sub === 'ls') return { kind: 'list' };
446
+ if (sub === 'on' || sub === 'off') return { kind: sub };
447
+ if (['rm', 'remove', 'del', 'delete'].includes(sub))
448
+ return toks[1] ? { kind: 'rm', id: toks[1] } : { kind: 'error', text: 'Usage: /row rm <id>' };
449
+ if (sub === 'test') return toks[1] ? { kind: 'test', id: toks[1] } : { kind: 'error', text: 'Usage: /row test <id>' };
450
+ if (sub === 'new') return { kind: 'new' };
451
+ if (sub === 'reload') return { kind: 'reload' };
452
+ if (sub === 'edit') return toks[1] ? { kind: 'edit', id: toks[1] } : { kind: 'error', text: 'Usage: /row edit <id>' };
453
+ if (sub === 'add') {
454
+ const USAGE = 'Usage: /row add <id> url <url> "<template>" [every 60s] or /row add <id> cmd "<shell cmd>" "<template>"';
455
+ let [, id, type, target, template, ...rest] = toks;
456
+ // No template (nothing after the target, or straight into an option) = the status-row contract.
457
+ if (template !== undefined && ['every', 'label', 'colour', 'color'].includes(template.toLowerCase()) && rest.length % 2 === 1) { rest = [template, ...rest]; template = ''; }
458
+ if (template === undefined && target !== undefined) template = '';
459
+ if (!id || !target || (type !== 'url' && type !== 'cmd')) return { kind: 'error', text: USAGE };
460
+ const opts = {};
461
+ for (let i = 0; i < rest.length; i += 2) {
462
+ const k = (rest[i] || '').toLowerCase();
463
+ if (!['every', 'label', 'colour', 'color'].includes(k) || rest[i + 1] === undefined)
464
+ return { kind: 'error', text: `Unexpected "${rest[i]}". Options: every 60s, label <text>, colour <role>.` };
465
+ opts[k === 'color' ? 'colour' : k] = rest[i + 1];
466
+ }
467
+ const row = { id, source: { type, ...(type === 'url' ? { url: target } : { cmd: target }), ...(opts.every ? { every: opts.every } : {}) } };
468
+ if (template) row.template = template;
469
+ if (opts.label) row.label = opts.label;
470
+ if (opts.colour) row.colour = opts.colour;
471
+ return { kind: 'add', row };
472
+ }
473
+ return { kind: 'error', text: 'Usage: /row [new | add | list | edit <id> | rm <id> | test <id> | reload | on | off]' };
474
+ }
475
+
476
+ /* ---- fetching and running ----------------------------------------------------------------- */
477
+
478
+ /** Plain fetch of an http(s) URL, no credentials, 15 s timeout, body capped. -> text. Throws a short reason. */
479
+ export async function plainFetch(url, { timeoutMs = URL_TIMEOUT_MS, fetchImpl = globalThis.fetch } = {}) {
480
+ let u;
481
+ try { u = new URL(url); } catch { throw new Error('bad URL'); }
482
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('only http(s)');
483
+ const ctl = new AbortController();
484
+ const t = setTimeout(() => ctl.abort(), timeoutMs);
485
+ try {
486
+ const r = await fetchImpl(u.href, { signal: ctl.signal, redirect: 'follow', credentials: 'omit', headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.5' } });
487
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
488
+ const text = await r.text();
489
+ const t0 = text.trimStart();
490
+ if (t0.startsWith('{') || t0.startsWith('[')) {
491
+ if (text.length > JSON_CAP) throw new Error(`response too large (${Math.round(text.length / 1024)} KB)`);
492
+ return text;
493
+ }
494
+ return text.length > OUTPUT_CAP * 4 ? text.slice(0, OUTPUT_CAP * 4) : text;
495
+ } catch (e) {
496
+ if (ctl.signal.aborted) throw new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`);
497
+ if (/^HTTP \d+|^response too large/.test(e?.message)) throw e;
498
+ const code = e?.cause?.code || e?.code || '';
499
+ throw new Error(code ? String(code) : 'unreachable');
500
+ } finally { clearTimeout(t); }
501
+ }
502
+
503
+ /** Run a shell command via the user's shell: timeout, output cap. -> stdout. Throws a short reason. */
504
+ export function runShell(cmd, { timeoutMs = FETCH_TIMEOUT_MS, shell = process.env.SHELL || '/bin/sh', exec = execFile } = {}) {
505
+ return new Promise((resolve, reject) => {
506
+ exec(shell, ['-c', cmd], { timeout: timeoutMs, maxBuffer: OUTPUT_CAP, killSignal: 'SIGKILL', windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }, (err, stdout) => {
507
+ if (!err) return resolve(String(stdout ?? ''));
508
+ if (err.killed || err.signal === 'SIGKILL') return reject(new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`));
509
+ if (/maxBuffer/i.test(err.message || '') || err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') return reject(new Error('output too large'));
510
+ reject(new Error(typeof err.code === 'number' ? `exit ${err.code}` : 'could not run'));
511
+ });
512
+ });
513
+ }
514
+
515
+ /* ---- the engine --------------------------------------------------------------------------- */
516
+
517
+ /** Next retry delay after `fails` consecutive failures: 5 s doubling, capped at 10 minutes. */
518
+ export const backoffMs = (fails) => Math.min(BACKOFF_CAP_MS, 5000 * 2 ** Math.max(0, fails - 1));
519
+
520
+ const sameRow = (a, b) => JSON.stringify(a) === JSON.stringify(b);
521
+
522
+ /**
523
+ * Owns the polling. Injected: fetchUrl(url) -> text, runCmd(cmd) -> text, onChange(), timer fns (tests).
524
+ * Never blocks input: every poll is async and a failure is state, not an exception.
525
+ */
526
+ export function createRowEngine({ fetchUrl, runCmd, fetchQuox = () => Promise.reject(new Error('not available here')), onChange = () => {}, now = Date.now, setT = setTimeout, clearT = clearTimeout }) {
527
+ let rows = [];
528
+ let enabled = true;
529
+ let stopped = false;
530
+ const st = new Map(); // id -> { row, status, text, error, fails, timer, running }
531
+
532
+ const note = () => { try { onChange(); } catch { /* a render hiccup never kills polling */ } };
533
+
534
+ async function probe(row) {
535
+ let raw;
536
+ try {
537
+ const t = row.source.type;
538
+ raw = t === 'url' ? await fetchUrl(row.source.url) : t === 'cmd' ? await runCmd(row.source.cmd) : await fetchQuox(row.source);
539
+ } catch (e) { return { ok: false, error: cleanText(e?.message || 'failed', 60) || 'failed' }; }
540
+ const v = rowValue(row, raw);
541
+ return v.ok ? { ok: true, raw: String(raw ?? ''), text: v.text, seg: v.seg, warnings: v.warnings } : { ok: false, raw: String(raw ?? ''), error: v.error.slice(0, 60) };
542
+ }
543
+
544
+ function arm(s, ms) {
545
+ clearT(s.timer);
546
+ if (stopped || !enabled) { s.timer = null; return; }
547
+ s.timer = setT(() => poll(s), ms);
548
+ s.timer?.unref?.();
549
+ }
550
+
551
+ async function poll(s) {
552
+ if (stopped || !enabled || s.running || st.get(s.row.id) !== s) return;
553
+ s.running = true;
554
+ const r = await probe(s.row);
555
+ s.running = false;
556
+ if (stopped || st.get(s.row.id) !== s) return; // removed or replaced while in flight
557
+ if (r.ok) { s.status = 'ok'; s.text = r.text; s.seg = r.seg || null; s.error = null; s.fails = 0; s.at = now(); arm(s, parseEvery(s.row.source.every) ?? 60000); }
558
+ else { // s.text and s.at stay: the last good value is still shown
559
+ s.status = 'error'; s.error = r.error; s.fails += 1; arm(s, backoffMs(s.fails)); }
560
+ note();
561
+ }
562
+
563
+ return {
564
+ /** Replace the row set. Unchanged rows keep their state; new or edited ones start at once. */
565
+ setRows(next) {
566
+ rows = next.slice(0, MAX_ROWS);
567
+ const ids = new Set(rows.map((r) => r.id));
568
+ for (const [id, s] of st) if (!ids.has(id)) { clearT(s.timer); st.delete(id); }
569
+ for (const row of rows) {
570
+ const cur = st.get(row.id);
571
+ if (cur && sameRow(cur.row, row)) continue;
572
+ if (cur) clearT(cur.timer);
573
+ const s = { row, status: 'loading', text: '', seg: null, error: null, fails: 0, timer: null, running: false };
574
+ st.set(row.id, s);
575
+ if (enabled && !stopped) setT(() => poll(s), 0)?.unref?.();
576
+ }
577
+ note();
578
+ },
579
+ setEnabled(on) {
580
+ enabled = Boolean(on);
581
+ for (const s of st.values()) {
582
+ if (!enabled) { clearT(s.timer); s.timer = null; }
583
+ else if (!s.running) { s.status = s.status === 'ok' ? 'ok' : 'loading'; setT(() => poll(s), 0)?.unref?.(); }
584
+ }
585
+ note();
586
+ },
587
+ isEnabled: () => enabled,
588
+ /** The rows to draw, in order: [{ row, status, text, error }]. Empty when hidden. */
589
+ view() { return enabled ? rows.map((r) => st.get(r.id)).filter(Boolean) : []; },
590
+ state: (id) => st.get(id) || null,
591
+ probe,
592
+ stop() { stopped = true; for (const s of st.values()) clearT(s.timer); }
593
+ };
594
+ }
595
+
596
+ const ageMark = (ms) => { const s = Math.max(0, Math.floor(ms / 1000)); return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}`; };
597
+
598
+ /** One row's display -> { text, role }. Pure; the caller clips to the width. */
599
+ export function rowDisplay({ row, status, text, error, at }, now = Date.now()) {
600
+ if (status === 'error' && text && at != null && now - at <= STALE_MAX_MS)
601
+ return { text: `${text} · ${ageMark(now - at)} old`, role: row.colour || 'prose' };
602
+ if (status === 'error') return { text: `⚠ ${rowLabel(row)}: ${String(error || 'failed').slice(0, 40)}`, role: 'error' };
603
+ if (status === 'loading' || !text) return { text: `${rowLabel(row)} …`, role: row.colour || 'prose' };
604
+ return { text, role: row.colour || 'prose' };
605
+ }
606
+
607
+ /** The width a label column takes for these rows (labels align across rows), 0 when none has a label. */
608
+ export const labelWidth = (states) => Math.min(16, Math.max(0, ...(states || []).map((st) => [...String(st?.row?.label || st?.seg?.label || '')].length)));
609
+
610
+ /**
611
+ * One row as coloured segments -> [[text, role, bold?]]. A row without a label or contract fields draws exactly
612
+ * as rowDisplay always has (one run); a labelled or contract row reads `✔ Label │ value text ▲3`. Roles are
613
+ * palette roles (never a grey): label for the name and separators, value for the number, the tone for the text.
614
+ */
615
+ export function rowParts(st, { now = Date.now(), labelW = 0 } = {}) {
616
+ // Never a bare label: an ok state with nothing to show draws as still loading ("QPE …").
617
+ if (st.status === 'ok' && !String(st.text ?? '').trim() && !st.seg?.value && !st.seg?.text) st = { ...st, status: 'loading' };
618
+ const d = rowDisplay(st, now);
619
+ const { row, seg } = st;
620
+ const live = st.status === 'ok' || (st.status === 'error' && d.role !== 'error');
621
+ if (!live || st.status === 'loading') return [[d.text, d.role]];
622
+ const label = row.label || seg?.label || '';
623
+ const toneR = seg?.tone ? toneRole(seg.tone) : null;
624
+ if (!label && !seg?.tone && !seg?.value && !seg?.trend) return [[d.text, d.role]];
625
+ const out = [];
626
+ if (seg?.tone && TONE_GLYPH[seg.tone]) out.push([TONE_GLYPH[seg.tone] + ' ', toneR]);
627
+ if (label) out.push([label.padEnd(Math.max(labelW, [...label].length)), 'label', true], [' │ ', 'label']);
628
+ const role = toneR || row.colour || 'prose';
629
+ if (seg) {
630
+ if (seg.value) out.push([seg.value, 'value', true]);
631
+ if (seg.text) out.push([(seg.value ? ' ' : '') + seg.text, role]);
632
+ if (seg.trend) out.push([' ' + seg.trend, 'label']);
633
+ } else out.push([st.text, role]);
634
+ if (st.status === 'error') out.push([` · ${ageMark(now - st.at)} old`, 'warn']);
635
+ return out;
636
+ }
637
+
638
+ /** Fit segments into `width` columns (code points), ending in … when cut. */
639
+ export function clipParts(parts, width) {
640
+ const out = []; let used = 0;
641
+ const w = Math.max(1, width);
642
+ for (const part of parts) {
643
+ const [t, role, bold] = part;
644
+ const cs = [...String(t)];
645
+ if (used + cs.length <= w) { out.push(part); used += cs.length; continue; }
646
+ const room = w - used - 1;
647
+ if (room > 0) out.push([cs.slice(0, room).join(''), role, bold]);
648
+ out.push(['…', role, bold]);
649
+ return out;
650
+ }
651
+ return out;
652
+ }
653
+
654
+ /* ---- local-only store for cmd rows -------------------------------------------------------- */
655
+
656
+ export const localRowsFile = () => join(homedir(), '.config', 'quox', 'quoxon-rows.local.json');
657
+
658
+ /** The local cmd rows. Never throws. */
659
+ export function loadLocalCmdRows(file = localRowsFile()) {
660
+ try { return normRows(JSON.parse(readFileSync(file, 'utf8')), { cmd: true }).filter((r) => r.source.type === 'cmd'); }
661
+ catch { return []; }
662
+ }
663
+ /** -> true when written. Only cmd rows are ever written here. */
664
+ export function saveLocalCmdRows(rows, file = localRowsFile()) {
665
+ try {
666
+ mkdirSync(dirname(file), { recursive: true });
667
+ writeFileSync(file, JSON.stringify({ rows: rows.filter((r) => r.source.type === 'cmd') }, null, 2), { mode: 0o600 });
668
+ return true;
669
+ } catch { return false; }
670
+ }
671
+
672
+
673
+ /* ---- share and import --------------------------------------------------------------------- */
674
+
675
+ export const BUNDLE_KIND = 'quox.status-rows';
676
+
677
+ /**
678
+ * Rows -> a share bundle. ONLY syncable rows go in: a command row runs a shell on the machine that holds it and is
679
+ * never shared (it is listed in `skipped`). A row that names a secret is refused (in `refused`).
680
+ */
681
+ export function shareBundle(rows) {
682
+ const out = []; const skipped = []; const refused = [];
683
+ for (const r of rows || []) {
684
+ if (!syncable(r)) { skipped.push(r.id); continue; }
685
+ const why = secretInRow(r);
686
+ if (why) { refused.push({ id: r.id, why }); continue; }
687
+ out.push(r);
688
+ }
689
+ return { bundle: { kind: BUNDLE_KIND, version: 1, rows: out }, skipped, refused };
690
+ }
691
+
692
+ /**
693
+ * Untrusted JSON -> { rows, commands, errors }. rows are syncable and valid; commands are cmd rows, NEVER added by
694
+ * an import on their own (the caller shows each command and needs a person to say yes to it, one by one).
695
+ */
696
+ export function readBundle(input) {
697
+ let v = input;
698
+ if (typeof v === 'string') { try { v = JSON.parse(v); } catch { return { rows: [], commands: [], errors: ['That is not JSON.'] }; } }
699
+ const list = Array.isArray(v) ? v : v && typeof v === 'object' && (v.kind === undefined || v.kind === BUNDLE_KIND) && Array.isArray(v.rows) ? v.rows : null;
700
+ if (!list) return { rows: [], commands: [], errors: [`Not a status-row bundle (expected {"kind":"${BUNDLE_KIND}","rows":[...]}).`] };
701
+ const rows = []; const commands = []; const errors = [];
702
+ for (const x of list.slice(0, 20)) {
703
+ const c = checkRow(x);
704
+ const id = cleanText(x?.id, 24) || '?';
705
+ if (c.error) { errors.push(`${id}: ${c.error}`); continue; }
706
+ const why = secretInRow(c.row);
707
+ if (why) { errors.push(`${id}: ${why}`); continue; }
708
+ (syncable(c.row) ? rows : commands).push(c.row);
709
+ }
710
+ return { rows, commands, errors };
711
+ }