quoxcli 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -2
  3. package/bin/quox.js +19 -1
  4. package/bin/quoxon.js +6 -1
  5. package/lib/api/endpoints/actions.js +15 -0
  6. package/lib/api/endpoints/admin.js +27 -0
  7. package/lib/api/endpoints/agent-attach.js +7 -0
  8. package/lib/api/endpoints/blog.js +16 -0
  9. package/lib/api/endpoints/chat.js +7 -0
  10. package/lib/api/endpoints/compliance.js +25 -0
  11. package/lib/api/endpoints/contactsq.js +4 -0
  12. package/lib/api/endpoints/devServerRegistry.js +25 -0
  13. package/lib/api/endpoints/engagement.js +26 -0
  14. package/lib/api/endpoints/evidence.js +20 -0
  15. package/lib/api/endpoints/hat.js +14 -0
  16. package/lib/api/endpoints/inbox.js +6 -0
  17. package/lib/api/endpoints/inboxq.js +19 -0
  18. package/lib/api/endpoints/index.js +14 -0
  19. package/lib/api/endpoints/loops.js +26 -0
  20. package/lib/api/endpoints/mon.js +295 -0
  21. package/lib/api/endpoints/objectives.js +6 -0
  22. package/lib/api/endpoints/quoxseo.js +6 -0
  23. package/lib/api/endpoints/server.js +21 -0
  24. package/lib/api/endpoints/setup.js +6 -0
  25. package/lib/api/endpoints/tools.js +13 -0
  26. package/lib/api/endpoints/v1.js +47 -0
  27. package/lib/commands/actions.js +81 -11
  28. package/lib/commands/admin.js +35 -0
  29. package/lib/commands/agent.js +11 -4
  30. package/lib/commands/agentAttach.js +61 -7
  31. package/lib/commands/approvals.js +198 -3
  32. package/lib/commands/ask.js +4 -2
  33. package/lib/commands/auth.js +64 -11
  34. package/lib/commands/backup.js +11 -4
  35. package/lib/commands/blog.js +110 -0
  36. package/lib/commands/capabilities.js +3 -2
  37. package/lib/commands/chat.js +142 -28
  38. package/lib/commands/chatCompanion.js +27 -0
  39. package/lib/commands/chatState.js +45 -0
  40. package/lib/commands/claude.js +7 -33
  41. package/lib/commands/compliance.js +109 -0
  42. package/lib/commands/connect.js +69 -0
  43. package/lib/commands/contacts.js +85 -4
  44. package/lib/commands/contactsSendAt.js +21 -0
  45. package/lib/commands/dev.js +6 -8
  46. package/lib/commands/doctor.js +22 -3
  47. package/lib/commands/engagement.js +90 -0
  48. package/lib/commands/evidence.js +155 -0
  49. package/lib/commands/ext.js +23 -0
  50. package/lib/commands/fleet.js +25 -0
  51. package/lib/commands/gripe.js +24 -13
  52. package/lib/commands/hat.js +76 -0
  53. package/lib/commands/image.js +68 -0
  54. package/lib/commands/inbox.js +37 -3
  55. package/lib/commands/inboxq.js +147 -0
  56. package/lib/commands/kuma.js +1123 -0
  57. package/lib/commands/kumaSla.js +174 -0
  58. package/lib/commands/licence.js +39 -1
  59. package/lib/commands/licenceLinkView.js +311 -0
  60. package/lib/commands/linkedin.js +19 -0
  61. package/lib/commands/loop.js +318 -0
  62. package/lib/commands/lucid.js +1 -0
  63. package/lib/commands/mail.js +161 -20
  64. package/lib/commands/memory.js +68 -4
  65. package/lib/commands/memorySync.js +158 -0
  66. package/lib/commands/mon.js +55 -144
  67. package/lib/commands/notes.js +29 -7
  68. package/lib/commands/notesSeal.js +259 -0
  69. package/lib/commands/objective.js +53 -3
  70. package/lib/commands/offload.js +2 -3
  71. package/lib/commands/org.js +21 -4
  72. package/lib/commands/plan.js +17 -0
  73. package/lib/commands/provider.js +87 -8
  74. package/lib/commands/qlarity.js +32 -1
  75. package/lib/commands/quoxseo.js +31 -0
  76. package/lib/commands/research.js +79 -0
  77. package/lib/commands/rows.js +9 -0
  78. package/lib/commands/run.js +20 -2
  79. package/lib/commands/scope.js +2 -2
  80. package/lib/commands/server.js +150 -0
  81. package/lib/commands/setup.js +127 -75
  82. package/lib/commands/shortcut.js +41 -0
  83. package/lib/commands/skills.js +24 -5
  84. package/lib/commands/slot.js +49 -9
  85. package/lib/commands/status.js +92 -44
  86. package/lib/commands/tool.js +30 -0
  87. package/lib/commands/vault.js +6 -3
  88. package/lib/commands/vm.js +2 -2
  89. package/lib/commands/walkthrough.js +14 -2
  90. package/lib/commands/watch.js +11 -2
  91. package/lib/commands/wazuh.js +824 -0
  92. package/lib/commands/wazuhIntake.js +317 -0
  93. package/lib/commands/wazuhReport.js +192 -0
  94. package/lib/commands/workflow.js +47 -4
  95. package/lib/commands/x.js +50 -0
  96. package/lib/commands/youtube.js +172 -0
  97. package/lib/core/actionItems.js +115 -0
  98. package/lib/core/agentAlias.js +2 -0
  99. package/lib/core/agentFamilies.js +7 -0
  100. package/lib/core/announce.js +86 -0
  101. package/lib/core/auth.js +20 -2
  102. package/lib/core/callMe.js +118 -0
  103. package/lib/core/client.js +37 -3
  104. package/lib/core/commandRegistrars.js +17 -0
  105. package/lib/core/config.js +29 -3
  106. package/lib/core/confirm.js +68 -7
  107. package/lib/core/crossOrgApprovals.js +69 -0
  108. package/lib/core/errors.js +52 -1
  109. package/lib/core/findingReconcile.js +90 -0
  110. package/lib/core/global-flags.js +1 -1
  111. package/lib/core/governedSession.js +136 -8
  112. package/lib/core/inboxResolution.js +3 -2
  113. package/lib/core/liveSessionsReport.js +1 -1
  114. package/lib/core/loginGuard.js +73 -0
  115. package/lib/core/loopGlyph.js +97 -0
  116. package/lib/core/offload.js +73 -7
  117. package/lib/core/org-override.js +5 -1
  118. package/lib/core/output.js +8 -8
  119. package/lib/core/profileTrust.js +182 -0
  120. package/lib/core/quoxglyph.js +22 -1
  121. package/lib/core/router.js +14 -0
  122. package/lib/core/sessionHeartbeat.js +12 -8
  123. package/lib/core/shortcut.js +83 -0
  124. package/lib/core/stepUpPrompt.js +99 -0
  125. package/lib/core/version.js +1 -1
  126. package/lib/core/vm.js +5 -0
  127. package/lib/core/walkthrough.js +9 -5
  128. package/lib/dragon/companionView.js +34 -0
  129. package/lib/dragon/frames.js +118 -0
  130. package/lib/dragon/jobs.js +123 -0
  131. package/lib/dragon/renderWorker.js +15 -0
  132. package/lib/dragon/select.js +72 -0
  133. package/lib/evidence/bundleExport.js +47 -0
  134. package/lib/evidence/bundleVerify.js +77 -0
  135. package/lib/quoxon/EXTENSIONS.md +146 -0
  136. package/lib/quoxon/api.js +364 -27
  137. package/lib/quoxon/app.js +1629 -183
  138. package/lib/quoxon/approvalBatch.js +117 -0
  139. package/lib/quoxon/approvalFacts.js +64 -8
  140. package/lib/quoxon/approvalSentence.js +48 -0
  141. package/lib/quoxon/approvalsView.js +124 -7
  142. package/lib/quoxon/autoOpen.js +94 -0
  143. package/lib/quoxon/backgroundWork.js +59 -0
  144. package/lib/quoxon/bang.js +1 -1
  145. package/lib/quoxon/capabilities.js +7 -2
  146. package/lib/quoxon/commandShort.js +22 -3
  147. package/lib/quoxon/commands.js +110 -15
  148. package/lib/quoxon/connect.js +5 -3
  149. package/lib/quoxon/countdownLine.js +10 -1
  150. package/lib/quoxon/deck.js +30 -0
  151. package/lib/quoxon/deckLazy.js +58 -0
  152. package/lib/quoxon/demo.autoplay.js +1 -1
  153. package/lib/quoxon/demo.js +3 -1
  154. package/lib/quoxon/destructive.js +35 -0
  155. package/lib/quoxon/dividerAocl.js +4 -3
  156. package/lib/quoxon/dividerRule.js +3 -3
  157. package/lib/quoxon/dnd.js +71 -0
  158. package/lib/quoxon/dragonCompanion.js +285 -0
  159. package/lib/quoxon/dragonModes.js +1 -0
  160. package/lib/quoxon/dragonMoments.js +160 -0
  161. package/lib/quoxon/dragonReads.js +68 -0
  162. package/lib/quoxon/dragonView.js +2 -2
  163. package/lib/quoxon/echo.js +1 -0
  164. package/lib/quoxon/events.js +67 -10
  165. package/lib/quoxon/extensions.js +636 -0
  166. package/lib/quoxon/gripeQueue.js +135 -0
  167. package/lib/quoxon/inboxView.js +182 -0
  168. package/lib/quoxon/installCli.js +79 -0
  169. package/lib/quoxon/keys.js +4 -4
  170. package/lib/quoxon/loopCard.js +460 -0
  171. package/lib/quoxon/loopCards.js +189 -0
  172. package/lib/quoxon/loopStream.js +98 -0
  173. package/lib/quoxon/mailView.js +71 -0
  174. package/lib/quoxon/main.js +29 -5
  175. package/lib/quoxon/markdown.js +1 -1
  176. package/lib/quoxon/model.js +15 -0
  177. package/lib/quoxon/mouse.js +12 -1
  178. package/lib/quoxon/neverAnswered.js +20 -0
  179. package/lib/quoxon/objectiveOffer.js +82 -0
  180. package/lib/quoxon/objects/away.js +43 -5
  181. package/lib/quoxon/objects/index.js +22 -4
  182. package/lib/quoxon/objects/model.js +33 -0
  183. package/lib/quoxon/objects/proof.js +12 -4
  184. package/lib/quoxon/onboarding/connectServer.js +162 -0
  185. package/lib/quoxon/onboarding/copy.js +6 -0
  186. package/lib/quoxon/onboarding/index.js +5 -43
  187. package/lib/quoxon/onboarding/probe.js +20 -1
  188. package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
  189. package/lib/quoxon/onboarding/view.js +16 -3
  190. package/lib/quoxon/orgSwitch.js +49 -1
  191. package/lib/quoxon/planSteps.js +51 -0
  192. package/lib/quoxon/print.js +8 -3
  193. package/lib/quoxon/profileSwitch.js +7 -0
  194. package/lib/quoxon/promptHistory.js +3 -1
  195. package/lib/quoxon/qdragonFly.js +7 -6
  196. package/lib/quoxon/qmotion.js +17 -0
  197. package/lib/quoxon/render.js +187 -41
  198. package/lib/quoxon/roster.js +36 -17
  199. package/lib/quoxon/rosterSessions.js +44 -0
  200. package/lib/quoxon/rows.js +244 -1
  201. package/lib/quoxon/screenContext.js +1 -1
  202. package/lib/quoxon/secretShape.js +86 -0
  203. package/lib/quoxon/serverChip.js +42 -0
  204. package/lib/quoxon/session.js +10 -0
  205. package/lib/quoxon/slash/agents.js +2 -1
  206. package/lib/quoxon/slash/argSources.js +9 -2
  207. package/lib/quoxon/slash/args.js +4 -2
  208. package/lib/quoxon/slash/broadcast.js +18 -5
  209. package/lib/quoxon/slash/broadcastRun.js +13 -2
  210. package/lib/quoxon/slash/cliCommand.js +42 -1
  211. package/lib/quoxon/slash/cliLeaves.json +139 -2
  212. package/lib/quoxon/slash/evidenceCmd.js +34 -0
  213. package/lib/quoxon/slash/hatWalk.js +1 -1
  214. package/lib/quoxon/slash/palette.js +23 -2
  215. package/lib/quoxon/slash/paletteController.js +10 -4
  216. package/lib/quoxon/slash/qpeCmd.js +35 -0
  217. package/lib/quoxon/slash/restart.js +31 -4
  218. package/lib/quoxon/slash/resume.js +18 -1
  219. package/lib/quoxon/slash/stepUp.js +5 -5
  220. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  221. package/lib/quoxon/slash/todoCmd.js +122 -0
  222. package/lib/quoxon/slashSpinner.js +35 -0
  223. package/lib/quoxon/slotAttach.js +8 -0
  224. package/lib/quoxon/slotChat.js +17 -0
  225. package/lib/quoxon/slotFormat.js +1 -1
  226. package/lib/quoxon/slotOutbox.js +113 -0
  227. package/lib/quoxon/suggestCommand.js +34 -3
  228. package/lib/quoxon/term.js +25 -1
  229. package/lib/quoxon/toolCards.js +30 -0
  230. package/lib/quoxon/transcript.js +2 -1
  231. package/lib/quoxon/welcome.js +1 -1
  232. package/lib/quoxon/whatsnew.js +1 -1
  233. package/lib/quoxon/whatsnew.jsonl +81 -0
  234. package/lib/quoxon/windowOrg.js +11 -1
  235. package/lib/server/devServers.js +137 -0
  236. package/lib/server/installFlow.js +168 -0
  237. package/lib/server/status.js +15 -1
  238. package/lib/toolOutputShape.js +55 -0
  239. package/package.json +4 -2
  240. package/vendor/dragonlab/SOURCE +4 -0
  241. package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
  242. package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
  243. package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
  244. package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
  245. package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
  246. package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
  247. package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
  248. package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
  249. package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
  250. package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
  251. package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
  252. package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
  253. package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
  254. package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
  255. package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
  256. package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
  257. package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
  258. package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
  259. package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
  260. package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
  261. package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
  262. package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
  263. package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
  264. package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
  265. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  266. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  267. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  268. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  269. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  270. package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
  271. package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
  272. package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
  273. package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
  274. package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
  275. package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
  276. package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
  277. package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
  278. package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
  279. package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
  280. package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
  281. package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
  282. package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
  283. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  284. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  285. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  286. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  287. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  288. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  289. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  290. package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
  291. package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
  292. package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
  293. package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
  294. package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
  295. package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
  296. package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
  297. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  298. package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
  299. package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
  300. package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
  301. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
  302. package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
  303. package/vendor/dragonlab/lib/power-effects.js +294 -0
  304. package/vendor/dragonlab/lib/power-registry.js +74 -0
  305. package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
  306. package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
@@ -0,0 +1,59 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
2
+ <title>Quoxperticus — the white, crimson and obsidian skill keeper</title>
3
+ <g id="wings">
4
+ <path id="left-wing" fill="#a92339" d="M 322 298 Q 252 207 137 92 L 161 209 L 114 182 L 148 279 L 112 291 L 243 337 Z"/>
5
+ <path id="right-wing" fill="#c12b42" d="M 444 296 Q 510 133 621 78 L 608 192 L 666 170 L 634 269 L 677 277 L 533 338 Z"/>
6
+ <path id="wing-ribs" fill="none" stroke="#666777" stroke-width="12" d="M 322 298 L 137 92 L 161 209 L 148 279 M 322 298 L 161 209 M 444 296 L 621 78 L 608 192 L 634 269 M 444 296 L 608 192"/>
7
+ <path id="wing-tips" fill="#f6f0f2" d="M 126 105 L 137 76 L 165 103 L 145 98 Z M 600 91 L 629 66 L 642 99 L 622 86 Z"/>
8
+ </g>
9
+ <g id="tail">
10
+ <path id="tail-coil" fill="#555763" d="M 447 353 C 521 330 611 352 631 391 C 655 437 576 454 521 432 C 578 438 610 413 593 395 Q 566 368 461 387 Z"/>
11
+ <path id="tail-edge" fill="none" stroke="#eeecef" stroke-width="10" d="M 468 367 C 537 346 611 376 619 404 Q 617 428 563 433"/>
12
+ <path id="tail-blade" fill="#e8374d" d="M 523 431 L 494 406 L 504 443 L 543 447 Z"/>
13
+ </g>
14
+ <g id="body">
15
+ <path id="obsidian-body" fill="#55545f" d="M 315 237 Q 393 210 448 257 L 479 347 Q 475 403 431 418 L 311 413 Q 272 378 292 312 Z"/>
16
+ <path id="chest-armour" fill="#e9e4ea" d="M 340 231 L 407 237 L 436 304 L 415 369 L 367 395 L 324 350 L 319 287 Z"/>
17
+ <path id="chest-joints" fill="none" stroke="#8d8696" stroke-width="7" d="M 325 275 L 417 280 M 324 305 L 430 312 M 336 339 L 419 344 M 351 367 L 399 374"/>
18
+ <path id="legs" fill="#666370" d="M 301 341 L 340 367 L 325 407 L 350 435 L 271 435 L 261 414 Z M 425 360 L 465 345 L 483 410 L 500 434 L 420 434 L 405 413 Z"/>
19
+ <path id="feet-armour" fill="#f1edf1" d="M 273 409 L 319 407 L 342 429 L 271 429 Z M 424 408 L 475 408 L 493 428 L 420 429 Z"/>
20
+ <path id="talons" fill="#161923" d="M 282 416 L 278 432 L 290 427 L 300 415 L 300 432 L 312 428 L 318 417 L 325 433 L 337 428 M 433 416 L 430 432 L 442 428 L 451 415 L 453 432 L 465 428 L 472 417 L 480 432 L 490 428"/>
21
+ </g>
22
+ <g id="neck">
23
+ <path id="neck-black" fill="#53515d" d="M 334 166 L 400 160 Q 437 211 433 267 L 389 292 L 335 261 L 348 214 Z"/>
24
+ <path id="neck-white" fill="#f5eef2" d="M 344 186 L 371 187 Q 384 227 409 261 L 386 281 L 353 252 L 360 216 Z"/>
25
+ <path id="neck-spines" fill="#ede6ef" d="M 399 178 L 442 177 L 423 196 L 450 211 L 428 221 L 457 241 L 432 247 L 449 268 L 416 265 Z"/>
26
+ </g>
27
+ <g id="head">
28
+ <path id="horn-far" fill="#faf4f4" d="M 324 117 L 297 76 L 300 26 L 315 65 L 346 105 Z"/>
29
+ <path id="horn-near" fill="#ffffff" d="M 380 108 L 401 59 L 446 22 L 425 71 L 411 124 Z"/>
30
+ <path id="horn-bands" fill="none" stroke="#292b35" stroke-width="9" d="M 300 65 L 321 69 M 312 91 L 334 89 M 408 66 L 430 61 M 397 90 L 417 91"/>
31
+ <path id="face-armour" fill="#e9e5ed" d="M 304 128 L 339 99 L 384 100 L 419 127 L 409 173 L 381 201 L 326 193 L 293 165 Z"/>
32
+ <path id="crown" fill="#e8334e" d="M 337 113 L 341 73 L 358 96 L 376 60 L 379 101 L 397 85 L 392 125 L 365 116 Z"/>
33
+ <path id="cheek-fins" fill="#faf5f7" d="M 393 143 L 455 114 L 427 151 L 457 148 L 426 174 L 438 186 L 399 190 Z"/>
34
+ <path id="muzzle" fill="#ffffff" d="M 313 163 Q 286 153 271 170 L 280 192 L 323 204 L 358 188 L 352 174 Z"/>
35
+ <path id="nostril" fill="#20212b" d="M 282 172 L 293 168 L 290 180 Z"/>
36
+ <path id="mouth" fill="none" stroke="#6a5265" stroke-width="5" d="M 281 187 Q 314 204 347 184"/>
37
+ </g>
38
+ <g id="panel">
39
+ <path id="panel-shell" fill="#6d626f" d="M 186 309 L 416 309 L 442 367 L 211 367 Z"/>
40
+ <path id="panel-screen" fill="#242333" d="M 199 317 L 406 317 L 425 354 L 218 354 Z"/>
41
+ <path id="panel-rim" fill="none" stroke="#ff6475" stroke-width="7" d="M 186 309 L 416 309 L 442 367 L 211 367 Z"/>
42
+ <path id="skill-cards" fill="#f0e9ee" d="M 224 326 L 258 326 L 264 344 L 230 344 Z M 281 326 L 315 326 L 321 344 L 287 344 Z"/>
43
+ <path id="selected-skill" fill="#ff344f" d="M 341 326 L 379 326 L 385 344 L 347 344 Z"/>
44
+ </g>
45
+ <g id="arms">
46
+ <path id="far-arm" fill="#e5dfe8" d="M 319 248 Q 288 235 267 263 L 251 290 L 228 294 L 223 312 L 272 315 L 287 284 L 325 277 Z"/>
47
+ <path id="near-arm" fill="#f7eff2" d="M 412 250 Q 448 247 451 276 L 417 304 L 382 311 L 369 299 L 389 282 L 411 277 Z"/>
48
+ <path id="finger-claws" fill="#20212b" d="M 232 297 L 225 316 L 240 308 L 249 298 L 245 317 L 260 309 M 377 296 L 367 313 L 385 308 L 393 295 L 390 312 L 407 303"/>
49
+ <path id="arm-scales" fill="none" stroke="#a699ac" stroke-width="6" d="M 281 259 L 298 275 M 421 261 L 438 277"/>
50
+ </g>
51
+ <g id="eyes">
52
+ <path id="far-eye-socket" fill="#080d19" d="M 304 147 C 304 137.0588745 312.0588745 129 322 129 C 331.9411255 129 340 137.0588745 340 147 C 340 156.9411255 331.9411255 165 322 165 C 312.0588745 165 304 156.9411255 304 147 Z"/>
53
+ <path id="far-eye-white" fill="#ffffff" d="M 310 147 C 310 140.372583 315.372583 135 322 135 C 328.627417 135 334 140.372583 334 147 C 334 153.627417 328.627417 159 322 159 C 315.372583 159 310 153.627417 310 147 Z"/>
54
+ <path id="far-pupil" fill="#080d19" d="M 318 145 C 318 138 326 138 326 145 C 326 153 318 153 318 145 Z"/>
55
+ <path id="near-eye-socket" fill="#080d19" d="M 349 147 C 349 134.8497355 358.8497355 125 371 125 C 383.1502645 125 393 134.8497355 393 147 C 393 159.1502645 383.1502645 169 371 169 C 358.8497355 169 349 159.1502645 349 147 Z"/>
56
+ <path id="near-eye-white" fill="#ffffff" d="M 356 147 C 356 138.71572875 362.71572875 132 371 132 C 379.28427125 132 386 138.71572875 386 147 C 386 155.28427125 379.28427125 162 371 162 C 362.71572875 162 356 155.28427125 356 147 Z"/>
57
+ <path id="near-pupil" fill="#080d19" d="M 364 145 C 364 137 373 137 373 145 C 373 154 364 154 364 145 Z"/>
58
+ </g>
59
+ </svg>
@@ -0,0 +1,43 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ export const QUOXPERTICUS_CLIPS=Object.freeze([{id:'idle',title:'Quiet Expertise',duration:12000},{id:'attach',title:'At Your Command',duration:3000},{id:'skill',title:'Select a Skill',duration:6000}]);
3
+ export const STORY_DURATION=12000;
4
+ const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
5
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
6
+ export function poseRig(rig,scene,ms){
7
+ const t=scene==='attach'?ms:ms%(scene==='skill'?6000:12000);
8
+ const look=scene==='skill'?pulse(t,0,3000):scene==='attach'?pulse(t,200,1900):.25*pulse(t,2000,5500);
9
+ const tap=scene==='skill'?(t<1800?ease((t-800)/1000):1-ease((t-2200)/1000)):0;
10
+ const follow=scene==='attach'?pulse(t,700,2800):scene==='idle'?.4*pulse(t,3000,8500):.25*pulse(t,3000,5800);
11
+ const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
12
+ if(p.group==='head'||p.group==='eyes')return [x-2*look,y+7*look];
13
+ if(p.group==='neck'){const w=Math.max(0,Math.min(1,(285-y)/75));return [x-2*look*w,y+7*look*w];}
14
+ if(p.id==='near-arm'||p.id==='finger-claws'&&x>350||p.id==='arm-scales'&&x>350){const w=Math.max(0,Math.min(1,(y-250)/45));return [x-12*tap*w,y+20*tap*w];}
15
+ if(p.group==='wings'){const w=Math.max(0,Math.min(1,(330-y)/240));return [x+(x<380?4:-4)*follow*w,y+5*follow*w];}
16
+ if(p.group==='tail'){const w=Math.max(0,Math.min(1,(x-460)/150));return [x,y-6*follow*w];}
17
+ return [x,y];
18
+ }))}));
19
+ const active=scene==='skill'&&t>=1800&&t<5000;
20
+ for(const p of paths)if(p.id==='selected-skill')p.rgb=active?[255,Math.round(70+100*pulse(t,1800,5000)),105]:[145,35,55];
21
+ if(active){
22
+ // Trace along the panel's own lower edge, clear of face and claws.
23
+ const u=Math.min(1,(t-1800)/1800),x=222+204*u,y=359;
24
+ paths.push({id:'skill-pulse',group:'effect',rgb:[255,230,240],stroke:0,erase:false,polygons:[[[x-4,y],[x,y-4],[x+4,y],[x,y+4]]]});
25
+ }
26
+ const map=new Map(paths.map(p=>[p.id,p]));
27
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
28
+ }
29
+ export function renderQuoxperticus(rig,scene='idle',ms=0,options={}){
30
+ if(!QUOXPERTICUS_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
31
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
32
+ if(scene==='attach'&&ms>=3000&&!options.still)return renderQuoxperticus(rig,'idle',ms-3000,options);
33
+ const t=options.still?({idle:4000,attach:1200,skill:2200}[scene]):ms;
34
+ const caption=scene==='attach'?'Skills at your command.':scene==='skill'?'Select. Illuminate. Connect.':'Ready when you are.';
35
+ return {...renderSvgRig(poseRig(rig,scene,t),options),clip:scene,scene,caption};
36
+ }
37
+ export function renderStory(rig,ms,options={}){
38
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
39
+ if(options.still)return renderQuoxperticus(rig,'skill',2200,options);
40
+ if(ms<3000)return renderQuoxperticus(rig,'attach',ms,options);
41
+ if(ms<9000)return renderQuoxperticus(rig,'skill',ms-3000,options);
42
+ return renderQuoxperticus(rig,'idle',ms-9000,options);
43
+ }
@@ -0,0 +1,57 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as previousPose,renderQuoxperticus as previous,QUOXPERTICUS_CLIPS as clips} from '../v3/character.mjs';
3
+ export const QUOXPERTICUS_CLIPS=Object.freeze([...clips,{id:'orchestrate',title:'Fleet Conductor',duration:10000}]);
4
+ export const STORY_DURATION=13000;
5
+ const clamp=x=>Math.max(0,Math.min(1,x));
6
+ const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
7
+ const fit=rig=>({...rig,bounds:{left:12,top:0,right:790,bottom:460}});
8
+ export function poseRig(rig,ms,{deck=false}={}){
9
+ const t=ms%10000,energy=ease(t/1400)*(1-ease((t-7800)/2200));
10
+ const base=previousPose(fit(rig),'idle',t);
11
+ const beat=energy*(.75+.25*Math.sin(t/380));
12
+ const paths=base.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
13
+ if(p.id==='near-arm'||p.id==='finger-claws'&&x>350||p.id==='arm-scales'&&x>350){const w=clamp((450-x)/70);return [x+14*energy*w,y-65*beat*w];}
14
+ return [x,y];
15
+ }))}));
16
+ const back=[],front=[];
17
+ const shape=(list,id,rgb,points,stroke=0)=>list.push({id,group:'orchestration',rgb,polygons:[points],stroke,erase:false});
18
+ const light=(v)=>[Math.round(160+95*v),Math.round(36+170*v),Math.round(65+170*v)];
19
+ const anchor=[390,288-50*beat];
20
+ if(energy>.01){
21
+ const count=deck?6:3;
22
+ for(let i=0;i<count;i++){
23
+ const left=i<3,x=left?55:742,y=110+(i%3)*100;
24
+ const phase=((t-1400-i*230)/2200%1+1)%1;
25
+ const bright=energy*(phase>.78?1:.3);
26
+ shape(back,'rack-'+i,light(bright),[[x-28,y-20],[x+28,y-20],[x+28,y+20],[x-28,y+20],[x-28,y-20]],4);
27
+ shape(back,'rack-divider-'+i,light(bright),[[x-26,y],[x+26,y]],3);
28
+ for(let j=0;j<2;j++)shape(back,'rack-led-'+i+'-'+j,[255,Math.round(130*energy),Math.round(150*energy)],[[x-17,y-12+j*20],[x-11,y-12+j*20],[x-11,y-7+j*20],[x-17,y-7+j*20]]);
29
+ // Curved command threads arch through the space above the wings.
30
+ const control=[left?170:600,25+i%3*22];
31
+ const point=u=>[(1-u)**2*anchor[0]+2*(1-u)*u*control[0]+u*u*x,(1-u)**2*anchor[1]+2*(1-u)*u*control[1]+u*u*y];
32
+ const thread=Array.from({length:31},(_,j)=>point(j/30));
33
+ shape(back,'thread-'+i,[Math.round(180*energy),Math.round(38*energy),Math.round(65*energy)],thread,3);
34
+ if(t>1400+i*230&&t<8500){
35
+ const [wx,wy]=point(phase),r=deck?6:7;
36
+ shape(front,'worker-'+i,light(energy),[[wx-r,wy],[wx,wy-r],[wx+r,wy],[wx,wy+r]]);
37
+ // Short comet tail makes outbound travel readable.
38
+ shape(front,'worker-tail-'+i,[240,65,105],[point(Math.max(0,phase-.055)),[wx,wy]],3);
39
+ }
40
+ }
41
+ // A ring at the conducting claw keeps the origin visible.
42
+ shape(front,'command-halo',light(energy),Array.from({length:33},(_,i)=>[anchor[0]+17*Math.cos(i*Math.PI/16),anchor[1]+9*Math.sin(i*Math.PI/16)]),3);
43
+ }
44
+ return {...base,paths:[...back,...paths,...front]};
45
+ }
46
+ export function renderQuoxperticus(rig,scene='idle',ms=0,options={}){
47
+ if(scene!=='orchestrate')return previous(fit(rig),scene,ms,options);
48
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
49
+ const t=options.still?4600:ms%10000;
50
+ return {...renderSvgRig(poseRig(rig,t,options),options),clip:scene,scene,caption:t<1400?'Raise the command.':t<7800?'Dispatch workers. Conduct the fleet.':'Workers settle. The mission continues.',visualOnly:true,demo:true};
51
+ }
52
+ export function renderStory(rig,ms,options={}){
53
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
54
+ if(options.still)return renderQuoxperticus(rig,'orchestrate',4600,options);
55
+ if(ms<3000)return renderQuoxperticus(rig,'attach',ms,options);
56
+ return renderQuoxperticus(rig,'orchestrate',ms-3000,options);
57
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import {parseSvgRig} from '../../../factory/svg-rig.mjs';
4
+ import {renderQuoxperticus,QUOXPERTICUS_CLIPS} from './character.mjs';
5
+ import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
6
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
7
+ const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
8
+ for(let i=0;i<args.length;i++){
9
+ if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
10
+ else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
11
+ }
12
+ const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
13
+ if(args.includes('--help')){console.log('Quoxperticus V4 forge studio. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
14
+ if(args.includes('--list')){console.log(QUOXPERTICUS_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
15
+ const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
16
+ if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
17
+ const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
18
+ const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
19
+ const message=value('--message','QUOXPERTICUS\nSkills at your command.');
20
+ if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
21
+ else{
22
+ if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
23
+ const rig=parseSvgRig(await fs.readFile(new URL('../v2/quoxperticus-master.svg',import.meta.url),'utf8'));
24
+ const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
25
+ const scene=value('--scene',args.includes('--attach')?'attach':'idle');
26
+ const r=renderQuoxperticus(rig,scene,Number(value('--at',0)),opts);
27
+ if(!r.ok)throw Error(r.code);
28
+ if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
29
+ const {watch}=await import('./watch.mjs');
30
+ await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
31
+ }else{
32
+ const shownMessage=args.includes('--message')?message:'QUOXPERTICUS\n'+r.caption;
33
+ console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,shownMessage,{terminalCols:w})]:composeMessage(r.frame,shownMessage,{terminalCols:w})).join('\n'));
34
+ }
35
+ }
@@ -0,0 +1,56 @@
1
+ import {performance} from 'node:perf_hooks';
2
+ import {QUOXPERTICUS_CLIPS,renderQuoxperticus,renderStory,STORY_DURATION} from './character.mjs';
3
+ import {composeMessage} from '../../lucid/recovery/layout.mjs';
4
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
5
+
6
+ /** One bounded performance. No alternate screen or full-screen clearing. */
7
+ export async function watch(paths,options,config){
8
+ if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
9
+ const {terminalCols,terminalRows,scene,large,message}=config;
10
+ const clip=scene?QUOXPERTICUS_CLIPS.find(c=>c.id===scene):null;
11
+ const duration=(clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
12
+ const realCols=process.stdout.columns,realRows=process.stdout.rows;
13
+ const start=performance.now();
14
+ const render=t=>{
15
+ const r=scene?renderQuoxperticus(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'QUOXPERTICUS · '+r.clip+'\n'+r.caption;
18
+ return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
19
+ };
20
+ // Reserve enough lines for captions too; keep within the real terminal.
21
+ const height=options.rows+3;
22
+ if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
23
+ let ended=false,drawn=false,timer,resolveDone,rejectDone;
24
+ const wasRaw=Boolean(process.stdin.isRaw);
25
+ const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
26
+ const cleanup=(error)=>{
27
+ if(ended)return;ended=true;clearInterval(timer);
28
+ process.stdin.off('data',key);process.stdout.off('resize',resized);
29
+ process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
30
+ process.stdin.setRawMode(wasRaw);process.stdin.pause();
31
+ if(drawn)process.stdout.write(`\x1b[${height}B\r`);
32
+ process.stdout.write('\x1b[0m\x1b[?25h');
33
+ error?rejectDone(error):resolveDone();
34
+ };
35
+ const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
36
+ const sigint=()=>cleanup(),sigterm=()=>cleanup();
37
+ const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
38
+ try{
39
+ process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
40
+ process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
41
+ process.stdout.write('\x1b[?25l');
42
+ // Allocate the block once; each frame replaces only these lines.
43
+ process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
44
+ const tick=()=>{
45
+ try{
46
+ if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
47
+ const t=Math.min(performance.now()-start,duration-1),lines=render(t);
48
+ if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
49
+ process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
50
+ if(performance.now()-start>=duration)cleanup();
51
+ }catch(e){cleanup(e);}
52
+ };
53
+ tick();if(!ended)timer=setInterval(tick,1000/6);
54
+ await done;
55
+ }catch(e){cleanup(e);await done;}
56
+ }
@@ -0,0 +1,53 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
2
+ <title>Seer — jade coil, golden belly and red mane</title>
3
+ <g id="far-feet">
4
+ <path id="far-front-leg" fill="#287c50" d="M562 253 Q605 244 616 267 L614 284 L631 287 Q641 297 630 302 L603 298 Q593 291 597 274 L575 279 Z"/>
5
+ <path id="far-front-claws" fill="#ffe6a0" d="M613 286 L621 282 L624 296 L616 296 Z M624 289 L632 288 L636 300 L628 300 Z"/>
6
+ <path id="far-back-leg" fill="#287c50" d="M257 303 Q230 279 205 291 L186 310 L173 307 Q161 310 168 320 L188 330 L218 310 L246 324 Z"/>
7
+ <path id="far-back-claws" fill="#ffe6a0" d="M168 309 L181 307 L177 319 L168 319 Z M183 314 L194 312 L188 326 L180 323 Z"/>
8
+ </g>
9
+ <g id="body">
10
+ <path id="spine-fins" fill="#dd5946" d="M488 197 L461 203 L490 220 L470 232 L510 243 L493 258 L537 268 L530 285 L562 280 L559 252 L532 229 Z M393 289 L373 265 L360 282 L336 259 L328 278 L300 258 L293 278 L268 263 L265 291 Z M188 318 L162 316 L169 340 L146 344 L160 367 L140 380 L170 394 L165 410 L194 416 Z"/>
11
+ <path id="serpentine-body" fill="#42b66c" d="M480 177 C470 215 504 252 540 266 C568 280 559 302 528 306 C472 321 379 258 270 277 C177 293 125 350 169 403 C212 456 347 444 438 416 C506 394 544 351 589 353 C621 355 640 375 625 388 C610 399 591 388 603 377 C573 378 577 410 605 418 C654 431 693 396 662 365 C634 333 592 320 550 328 C486 339 444 391 355 408 C283 425 202 414 203 378 C201 344 252 320 293 322 C372 323 455 363 539 344 C597 331 620 299 594 264 C565 226 523 221 533 185 Z"/>
12
+ <path id="back-highlight" fill="#80d77e" d="M487 194 C486 219 514 244 544 257 C590 279 577 309 542 316 C465 339 387 280 292 289 C218 295 166 337 170 370 C155 337 204 290 271 280 C374 260 466 326 530 308 C561 302 573 284 542 269 C505 255 477 222 480 195 Z"/>
13
+ <path id="belly-ribbon" fill="#efc75b" d="M514 194 C515 231 552 243 575 270 C600 300 572 324 533 332 C455 349 369 308 291 309 C235 310 181 342 186 378 C188 423 274 435 360 418 C453 400 497 352 552 339 C594 329 625 343 647 365 C626 349 607 346 589 350 C540 347 502 393 435 415 C344 442 217 450 177 401 C141 355 192 305 266 289 C365 270 456 334 530 318 C565 312 579 292 557 276 C524 253 501 229 500 203 Z"/>
14
+ <path id="belly-bands" stroke="#b49c42" stroke-width="4" fill="none" d="M510 222 L527 218 M522 245 L542 237 M546 265 L561 254 M567 285 L586 282 M563 306 L576 319 M537 318 L542 331 M500 321 L500 336 M460 315 L456 332 M420 305 L412 323 M378 295 L371 314 M336 290 L333 309 M292 294 L292 309 M249 302 L255 316 M212 322 L225 334 M184 352 L199 357 M185 383 L201 380 M205 410 L215 397 M244 426 L248 411 M284 431 L284 419 M326 427 L325 416 M370 417 L365 408 M409 404 L403 395 M450 385 L443 377 M489 361 L480 354 M529 345 L522 335"/>
15
+ </g>
16
+ <g id="tail">
17
+ <path id="tail-tuft" fill="#d9503e" d="M598 379 Q590 366 607 350 L607 363 L626 352 L623 368 L641 365 Q633 384 615 389 Z"/>
18
+ <path id="tail-tuft-light" fill="#f3924b" d="M603 379 L616 364 L613 378 L628 371 L616 385 Z"/>
19
+ </g>
20
+ <g id="near-feet">
21
+ <path id="near-back-leg" fill="#4ebc70" d="M243 315 Q271 331 264 356 L247 371 L263 385 Q268 397 253 398 L227 384 Q220 376 229 363 L240 351 L222 337 Z"/>
22
+ <path id="near-back-claws" fill="#ffe6a0" d="M244 382 L254 377 L258 391 L249 393 Z M230 378 L238 374 L242 388 L233 387 Z"/>
23
+ <path id="near-front-leg" fill="#56c779" d="M527 275 Q503 283 491 306 Q483 322 496 331 L514 331 L514 345 Q522 355 532 342 L534 322 Q531 313 516 311 L538 293 Z"/>
24
+ <path id="near-front-claws" fill="#ffe6a0" d="M516 331 L522 328 L525 343 L517 345 Z M526 328 L534 325 L534 341 L527 344 Z"/>
25
+ </g>
26
+ <g id="head">
27
+ <path id="mane" fill="#d9503e" d="M480 104 Q462 85 439 87 L450 107 L421 105 L437 128 L412 136 L433 151 L410 166 L438 178 L429 199 L458 195 L455 216 L487 203 L514 184 Z"/>
28
+ <path id="mane-light" fill="#f38848" d="M455 117 L439 116 L449 139 L431 141 L448 155 L433 171 L455 175 L449 191 L474 184 Z"/>
29
+ <path id="far-horn" fill="#ebc76b" d="M551 108 Q546 82 562 61 L558 44 L568 38 L576 61 L566 82 L582 73 L591 81 L565 100 L565 116 Z"/>
30
+ <path id="near-horn" fill="#ffdb7e" d="M480 112 Q465 91 477 67 L460 54 L464 44 L489 62 L494 40 L505 41 L500 73 Q485 93 496 111 Z"/>
31
+ <path id="ear" fill="#59bb6c" d="M463 126 Q429 101 411 116 Q411 145 453 155 Z"/>
32
+ <path id="ear-inner" fill="#e5bc58" d="M451 134 L423 119 Q427 139 451 146 Z"/>
33
+ <path id="face" fill="#59c879" d="M460 116 C478 91 528 91 554 108 C581 112 595 135 593 156 L622 164 Q657 174 640 197 Q627 216 589 213 C574 235 525 240 489 218 C458 207 440 155 460 116 Z"/>
34
+ <path id="forehead-light" fill="#9ce18b" d="M465 126 Q489 99 526 108 Q493 113 477 142 Z"/>
35
+ <path id="muzzle" fill="#c4dd84" d="M558 169 Q591 153 626 174 Q648 185 629 199 Q600 212 563 199 Z"/>
36
+ <path id="chin" fill="#f3d478" d="M496 201 Q523 218 555 205 Q571 214 591 211 Q569 233 534 225 Q509 219 496 201 Z"/>
37
+ <path id="nostril" fill="#23664b" d="M621 175 Q631 171 631 179 Q627 186 621 181 Z"/>
38
+ <path id="mouth" stroke="#317b4f" stroke-width="5" fill="none" d="M558 198 Q588 215 614 202"/>
39
+ <path id="cheek" fill="#e5c263" d="M470 180 Q483 174 492 185 Q489 199 477 194 Z"/>
40
+ </g>
41
+ <g id="whiskers">
42
+ <path id="far-whisker" stroke="#f7d786" stroke-width="6" fill="none" d="M619 187 C653 174 669 146 694 150 Q708 154 700 166"/>
43
+ <path id="near-whisker" stroke="#ffe29b" stroke-width="7" fill="none" d="M592 200 C614 229 656 243 679 223 Q691 212 681 202"/>
44
+ </g>
45
+ <g id="eyes">
46
+ <path id="far-eye-socket" fill="#080d19" d="M548 131 C552 113 572 113 580 130 C588 149 580 170 566 171 C550 169 544 149 548 131 Z"/>
47
+ <path id="far-eye-white" fill="#ffffff" d="M552 132 C555 119 570 119 576 132 C582 147 577 165 566 166 C554 164 549 147 552 132 Z"/>
48
+ <path id="far-pupil" fill="#080d19" d="M561 142 C561 132 572 131 575 141 C578 151 567 157 563 150 Z"/>
49
+ <path id="near-eye-socket" fill="#080d19" d="M475 130 C482 110 509 109 521 128 C534 151 522 180 502 182 C479 181 466 153 475 130 Z"/>
50
+ <path id="near-eye-white" fill="#ffffff" d="M480 132 C486 116 506 115 516 131 C527 151 517 175 502 176 C483 176 473 151 480 132 Z"/>
51
+ <path id="near-pupil" fill="#080d19" d="M495 143 C495 132 508 128 514 140 C519 151 510 162 501 157 C497 155 495 149 495 143 Z"/>
52
+ </g>
53
+ </svg>
@@ -0,0 +1,88 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig.mjs';
2
+ const clamp=x=>Math.max(0,Math.min(1,x));
3
+ const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
4
+ const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
5
+ export const SEER_CLIPS=[
6
+ {id:'idle',title:'Quiet Watch',duration:14000},
7
+ {id:'attach',title:'A Closer Look',duration:3400},
8
+ {id:'curious',title:'Following a Detail',duration:3000},
9
+ {id:'focus',title:'Reading the Pattern',duration:3000},
10
+ {id:'concerned',title:'Something Does Not Fit',duration:3000},
11
+ {id:'proud',title:'There It Is',duration:2800},
12
+ {id:'standby',title:'Patient Company',duration:4000},
13
+ {id:'pattern',title:'Pattern Sight',duration:5200,power:true},
14
+ ];
15
+ export const STORY=[['attach',3400],['idle',2200],['curious',3000],['focus',2400],['pattern',5200],['proud',2800],['standby',3000]];
16
+ export const STORY_DURATION=STORY.reduce((s,b)=>s+b[1],0);
17
+ const captions={idle:'Quietly watching.',attach:'Let me look closer.',curious:'Something caught my eye.',focus:'Following the details.',concerned:'This needs another look.',proud:'There it is · expression study',standby:'Take your time.',pattern:'Pattern Sight · visual demonstration'};
18
+ export function bodyPose(ms){
19
+ const t=ms%14000;
20
+ return {turn:.018*pulse(t,800,4300)-.012*pulse(t,6500,9500),nod:2*pulse(t,1300,4600),coil:pulse(t,3200,8800),whisker:pulse(t,1600,5000)-.6*pulse(t,7300,10300)};
21
+ }
22
+ export function poseRig(rig,p,face){
23
+ const paths=rig.paths.map(path=>({...path,polygons:path.polygons.map(poly=>poly.map(([sx,sy])=>{
24
+ let x=sx,y=sy;
25
+ if(path.id.endsWith('-eye-white')||path.id.endsWith('-pupil')){
26
+ const cy=path.id.startsWith('near-')?146:143;
27
+ y=cy+(y-cy)*face.open;
28
+ if(path.id.endsWith('-pupil'))x+=face.gaze*4;
29
+ }
30
+ // The same spatial warp moves all anatomy, so neck, fins and feet stay joined.
31
+ const upper=ease((310-sy)/110),a=p.turn*upper;
32
+ const dx=x-520,dy=y-270;
33
+ x=520+dx*Math.cos(a)-dy*Math.sin(a);y=270+dx*Math.sin(a)+dy*Math.cos(a)+p.nod*upper;
34
+ // Compress inward instead of enlarging the envelope; leave the planted bottom still.
35
+ const coil=ease((sy-275)/55)*(1-ease((sy-385)/40));
36
+ x+=(395-sx)*.009*p.coil*coil;
37
+ y-=2*p.coil*coil;
38
+ if(path.group==='whiskers'){
39
+ const root=path.id==='far-whisker'?619:592;
40
+ const w=ease((sx-root)/90);y+=3*p.whisker*w;x-=1.5*Math.abs(p.whisker)*w;
41
+ }
42
+ return [x,y];
43
+ }))}));
44
+ const byId=new Map(paths.map(p=>[p.id,p]));
45
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
46
+ }
47
+ function expression(scene,t){
48
+ const blink=pulse(t%7000,5400,5620);
49
+ switch(scene){
50
+ case 'curious':return {gaze:.7*ease(t/700),open:1.07};
51
+ case 'focus':return {gaze:.35,open:.72};
52
+ case 'concerned':return {gaze:-.35,open:.88};
53
+ case 'proud':return {gaze:0,open:1.1};
54
+ case 'standby':return {gaze:0,open:.85};
55
+ case 'pattern':return {gaze:-.65*ease(t/900),open:1};
56
+ default:return {gaze:.25*Math.sin(t/2300),open:1-.65*blink};
57
+ }
58
+ }
59
+ function pattern(frame,t,mono){
60
+ const h=frame.length,w=frame[0].length,nodes=[[.14,.28],[.24,.18],[.34,.29],[.40,.40]];
61
+ const progress=ease(t/3400),fade=1-ease((t-4300)/900);let count=0;
62
+ const put=(x,y,bright)=>{x=Math.round(x*w);y=Math.round(y*h);if(x<1||x>=w-1||y<1||y>=h-1||fade<.05||frame[y][x].ch!==' ')return;frame[y][x]={ch:bright?'⠿':'⠂',bg:null,rgb:mono?null:(bright?[255,235,156]:[229,186,83]).map(n=>Math.round(n*fade))};count++;};
63
+ for(let i=0;i<nodes.length;i++)if(progress>(i+1)*.09)put(...nodes[i],true);
64
+ for(let i=0;i<3;i++)for(let j=1;j<8;j++)if(progress>.42+(i+j/8)*.18){const a=nodes[i],b=nodes[i+1];put(a[0]+(b[0]-a[0])*j/8,a[1]+(b[1]-a[1])*j/8,false);}
65
+ return count;
66
+ }
67
+ export function renderSeer(rig,scene='idle',ms=0,options={}){
68
+ const clip=SEER_CLIPS.find(c=>c.id===scene);
69
+ if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
70
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
71
+ const t=options.still?(clip.power?3300:0):ms;
72
+ let face=expression(scene,t),pose=bodyPose(options.motionTime??t);
73
+ if(scene==='attach'){
74
+ const duration=options.deck?4400:3400;
75
+ if(t>=duration){pose=bodyPose(t-duration);face=expression('idle',t-duration);}
76
+ else{const u=t/duration;pose={turn:-.02*pulse(u,.1,.8),nod:3*pulse(u,.25,.9),coil:.8*pulse(u,.35,1),whisker:pulse(u,.3,.95)};face={gaze:-.5*pulse(u,0,.85),open:1};}
77
+ }
78
+ if(scene==='proud')pose={...pose,nod:pose.nod-1.5*pulse(t,0,2800)};
79
+ if(options.still)pose={turn:0,nod:0,coil:0,whisker:0};
80
+ const r=renderSvgRig(poseRig(rig,pose,face),options);
81
+ const effectCells=clip.power?pattern(r.frame,t,options.mono):0;
82
+ return {...r,scene,clip:clip.title,caption:captions[scene],pose,face,effectCells};
83
+ }
84
+ export function renderStory(rig,ms,options={}){
85
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
86
+ let t=options.still?0:Math.min(ms,STORY_DURATION-1);
87
+ for(const [scene,duration] of STORY){if(t<duration)return renderSeer(rig,scene,t,{...options,deck:false,motionTime:Math.max(0,ms-3400)});t-=duration;}
88
+ }
@@ -0,0 +1,33 @@
1
+ import {renderSeer as renderV2,SEER_CLIPS as V2_CLIPS} from '../v2/character.mjs';
2
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
3
+ export const SEER_CLIPS=[...V2_CLIPS,{id:'eight',title:'Eight · HI!',duration:10000}];
4
+ export const STORY_DURATION=10000;
5
+ import {flattenPath} from '../../stage3/svg-coverage.js';
6
+ // Authored rest and entrance spines; transfer existing artwork along their arc length.
7
+ const rest=flattenPath('M506 185 C500 225 565 245 576 280 C590 315 555 330 528 325 C460 345 370 285 286 300 C220 310 170 343 184 380 C200 430 280 439 357 420 C440 400 490 360 554 340 C600 324 635 349 650 376 C660 400 617 420 596 399 C586 389 593 381 603 377')[0];
8
+ const target=flattenPath('M506 185 C490 215 410 235 350 270 C280 305 250 320 280 355 C320 405 414 405 450 355 C490 310 405 285 350 250 C285 215 245 195 275 155 C310 115 390 115 423 154 C453 187 420 220 385 222 C365 223 347 210 354 197')[0];
9
+ function spine(points){let total=0;const segments=[];for(let i=1;i<points.length;i++){const a=points[i-1],b=points[i],dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy);if(!len)continue;segments.push({a,dx,dy,len,start:total});total+=len;}return {segments,total};}
10
+ const original=spine(rest),destination=spine(target),cache=new WeakMap();
11
+ function mapPoint(x,y){
12
+ let nearest,dist=Infinity,along=0,offset=0;
13
+ for(const s of original.segments){const u=Math.max(0,Math.min(1,((x-s.a[0])*s.dx+(y-s.a[1])*s.dy)/(s.len*s.len)));const px=s.a[0]+u*s.dx,py=s.a[1]+u*s.dy,d=(x-px)**2+(y-py)**2;if(d<dist){dist=d;nearest=s;along=(s.start+u*s.len)/original.total;offset=((x-px)*-s.dy+(y-py)*s.dx)/s.len;}}
14
+ const at=along*destination.total,s=destination.segments.find(s=>at<=s.start+s.len)||destination.segments.at(-1),u=(at-s.start)/s.len;
15
+ return [s.a[0]+u*s.dx-offset*s.dy/s.len,s.a[1]+u*s.dy+offset*s.dx/s.len];
16
+ }
17
+ export function figureEight(rig,amount){
18
+ if(!cache.has(rig))cache.set(rig,rig.paths.map(p=>p.polygons.map(poly=>poly.map(([x,y])=>['head','eyes','whiskers'].includes(p.group)?[x,y]:mapPoint(x,y)))));
19
+ const mapped=cache.get(rig);
20
+ const paths=rig.paths.map((p,i)=>({...p,polygons:p.polygons.map((poly,j)=>poly.map(([x,y],k)=>[x+(mapped[i][j][k][0]-x)*amount,y+(mapped[i][j][k][1]-y)*amount]))}));
21
+ const byId=new Map(paths.map(p=>[p.id,p]));
22
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
23
+ }
24
+ export function renderSeer(rig,scene='eight',ms=0,options={}){
25
+ if(scene!=='eight')return renderV2(rig,scene,ms,options);
26
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
27
+ const t=options.still?3000:ms;
28
+ if(t>=7000)return renderV2(rig,'idle',t-7000,options);
29
+ const amount=ease(t/2200)*(1-ease((t-4500)/2500));
30
+ const r=renderV2(figureEight(rig,amount),'idle',0,{...options,motionTime:0});
31
+ return {...r,scene:'eight',clip:'Eight · HI!',caption:t>=2200&&t<4500?'HI!':'A little twist…',amount};
32
+ }
33
+ export function renderStory(rig,ms,options={}){return renderSeer(rig,'eight',ms,options);}
@@ -0,0 +1,66 @@
1
+ import {renderSeer as previous,SEER_CLIPS as previousClips} from '../v3/character.mjs';
2
+ import {flattenPath} from '../../stage3/svg-coverage.js';
3
+ import {renderSvgRig} from '../../../factory/svg-rig.mjs';
4
+ const TAU=2*Math.PI;
5
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
6
+ export const SEER_CLIPS=[...previousClips,{id:'swim',title:'Swimming Eight · HI!',duration:11000}];
7
+ export const STORY_DURATION=11000;
8
+ export const arrivalDuration=deck=>deck?8500:5800;
9
+ const rest=flattenPath('M506 185 C500 225 565 245 576 280 C590 315 555 330 528 325 C460 345 370 285 286 300 C220 310 170 343 184 380 C200 430 280 439 357 420 C440 400 490 360 554 340 C600 324 635 349 650 376 C660 400 617 420 596 399 C586 389 593 381 603 377')[0];
10
+ let length=0;
11
+ const segments=rest.slice(1).map((b,i)=>{const a=rest[i],dx=b[0]-a[0],dy=b[1]-a[1],n=Math.hypot(dx,dy),s={a,dx,dy,n,start:length};length+=n;return s;}).filter(s=>s.n);
12
+ const cache=new WeakMap();
13
+ function bind(x,y){
14
+ let distance=Infinity,binding;
15
+ for(const s of segments){const u=Math.max(0,Math.min(1,((x-s.a[0])*s.dx+(y-s.a[1])*s.dy)/s.n**2));
16
+ const px=s.a[0]+u*s.dx,py=s.a[1]+u*s.dy,d=(x-px)**2+(y-py)**2;
17
+ if(d<distance){distance=d;binding={along:(s.start+u*s.n)/length,offset:((x-px)*-s.dy+(y-py)*s.dx)/s.n};}}
18
+ return binding;
19
+ }
20
+ // Upright eight. Arc-length lookup makes skin sections travel at equal speed.
21
+ const track=Array.from({length:721},(_,i)=>{const a=i/720*TAU;return [390+100*Math.sin(2*a),277-81*Math.cos(a)];});
22
+ const lengths=[0];for(let i=1;i<track.length;i++)lengths.push(lengths.at(-1)+Math.hypot(track[i][0]-track[i-1][0],track[i][1]-track[i-1][1]));
23
+ function sample(u){
24
+ const d=((u%1)+1)%1*lengths.at(-1);let lo=0,hi=720;
25
+ while(hi-lo>1){const m=(lo+hi)>>1;if(lengths[m]<d)lo=m;else hi=m;}
26
+ const f=(d-lengths[lo])/(lengths[hi]-lengths[lo]),a=track[lo],b=track[hi],n=Math.hypot(b[0]-a[0],b[1]-a[1]);
27
+ return {x:a[0]+f*(b[0]-a[0]),y:a[1]+f*(b[1]-a[1]),nx:-(b[1]-a[1])/n,ny:(b[0]-a[0])/n};
28
+ }
29
+ export function swimRig(rig,ms,{deck=false,still=false}={}){
30
+ const duration=arrivalDuration(deck),t=still?duration*.69:Math.min(ms,duration),u=t/duration;
31
+ // Slow into a greeting at the top; settle with zero endpoint velocity.
32
+ const amount=ease(u/.18)*(1-ease((u-.78)/.22));
33
+ const phase=1-ease((u-.13)/.54),head=sample(phase);
34
+ const angle=.13*Math.sin(TAU*phase),c=Math.cos(angle),s=Math.sin(angle);
35
+ if(!cache.has(rig))cache.set(rig,rig.paths.map(p=>p.polygons.map(poly=>poly.map(([x,y])=>bind(x,y)))));
36
+ const bindings=cache.get(rig);
37
+ const paths=rig.paths.map((p,i)=>({...p,polygons:p.polygons.map((poly,j)=>poly.map(([x,y],k)=>{
38
+ const b=bindings[i][j][k];let tx,ty;
39
+ if(['head','eyes','whiskers'].includes(p.group)){
40
+ tx=head.x+(x-506)*c-(y-185)*s;ty=head.y+(x-506)*s+(y-185)*c;
41
+ if(p.group==='whiskers'){
42
+ const root=p.id==='far-whisker'?619:592,w=ease((x-root)/85);
43
+ ty+=7*w*Math.sin(TAU*phase-.8)*Math.sin(Math.PI*ease(u/.78));
44
+ }
45
+ }else{
46
+ const q=sample(phase+b.along*.9);
47
+ tx=q.x+q.nx*b.offset*.85;ty=q.y+q.ny*b.offset*.85;
48
+ // Blend the neck root into the same rigid face transform.
49
+ const root=1-ease(b.along/.055);
50
+ tx=tx*(1-root)+(head.x+(x-506)*c-(y-185)*s)*root;
51
+ ty=ty*(1-root)+(head.y+(x-506)*s+(y-185)*c)*root;
52
+ }
53
+ return [x+(tx-x)*amount,y+(ty-y)*amount];
54
+ }))}));
55
+ const byId=new Map(paths.map(p=>[p.id,p]));
56
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)})),flight:{amount,phase,head,u}};
57
+ }
58
+ export function renderSeer(rig,scene='swim',ms=0,options={}){
59
+ if(scene!=='swim')return previous(rig,scene,ms,options);
60
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
61
+ const end=arrivalDuration(options.deck);
62
+ if(!options.still&&ms>=end)return previous(rig,'idle',ms-end,options);
63
+ const posed=swimRig(rig,ms,options),r=renderSvgRig(posed,options),u=posed.flight.u;
64
+ return {...r,scene:'swim',clip:'Swimming Eight · HI!',caption:u>=.67&&u<.83?'HI!':u<.67?'Following my own trail…':'Right here with you.',flight:posed.flight};
65
+ }
66
+ export function renderStory(rig,ms,options={}){return renderSeer(rig,'swim',ms,options);}
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import {parseSvgRig} from '../../../factory/svg-rig.mjs';
4
+ import {renderSeer,SEER_CLIPS} from './character.mjs';
5
+ import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
6
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
7
+ const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
8
+ for(let i=0;i<args.length;i++){
9
+ if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
10
+ else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
11
+ }
12
+ const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
13
+ if(args.includes('--help')){console.log('Seer V4 flowing figure-eight entrance. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
14
+ if(args.includes('--list')){console.log(SEER_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
15
+ const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
16
+ if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
17
+ const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
18
+ const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
19
+ const message=value('--message','SEER\nLet me look closer.');
20
+ if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
21
+ else{
22
+ if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
23
+ const rig=parseSvgRig(await fs.readFile(new URL('../v1/seer-master.svg',import.meta.url),'utf8'));
24
+ const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
25
+ const scene=value('--scene','swim');
26
+ const r=renderSeer(rig,scene,Number(value('--at',0)),opts);
27
+ if(!r.ok)throw Error(r.code);
28
+ if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
29
+ const {watch}=await import('./watch.mjs');
30
+ await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach')&&scene==='attach',message:args.includes('--message')?message:null});
31
+ }else
32
+ console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
33
+ }
@@ -0,0 +1,56 @@
1
+ import {performance} from 'node:perf_hooks';
2
+ import {SEER_CLIPS,renderSeer,renderStory,STORY_DURATION} from './character.mjs';
3
+ import {composeMessage} from '../../lucid/recovery/layout.mjs';
4
+ import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
5
+
6
+ /** One bounded performance. No alternate screen or full-screen clearing. */
7
+ export async function watch(paths,options,config){
8
+ if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
9
+ const {terminalCols,terminalRows,scene,large,message}=config;
10
+ const clip=scene?SEER_CLIPS.find(c=>c.id===scene):null;
11
+ const duration=(config.arrival&&options.deck?4400:clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
12
+ const realCols=process.stdout.columns,realRows=process.stdout.rows;
13
+ const start=performance.now();
14
+ const render=t=>{
15
+ const r=scene?renderSeer(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'SEER · '+r.clip+'\n'+r.caption;
18
+ return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
19
+ };
20
+ // Reserve enough lines for captions too; keep within the real terminal.
21
+ const height=options.rows+3;
22
+ if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
23
+ let ended=false,drawn=false,timer,resolveDone,rejectDone;
24
+ const wasRaw=Boolean(process.stdin.isRaw);
25
+ const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
26
+ const cleanup=(error)=>{
27
+ if(ended)return;ended=true;clearInterval(timer);
28
+ process.stdin.off('data',key);process.stdout.off('resize',resized);
29
+ process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
30
+ process.stdin.setRawMode(wasRaw);process.stdin.pause();
31
+ if(drawn)process.stdout.write(`\x1b[${height}B\r`);
32
+ process.stdout.write('\x1b[0m\x1b[?25h');
33
+ error?rejectDone(error):resolveDone();
34
+ };
35
+ const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
36
+ const sigint=()=>cleanup(),sigterm=()=>cleanup();
37
+ const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
38
+ try{
39
+ process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
40
+ process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
41
+ process.stdout.write('\x1b[?25l');
42
+ // Allocate the block once; each frame replaces only these lines.
43
+ process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
44
+ const tick=()=>{
45
+ try{
46
+ if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
47
+ const t=Math.min(performance.now()-start,duration-1),lines=render(t);
48
+ if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
49
+ process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
50
+ if(performance.now()-start>=duration)cleanup();
51
+ }catch(e){cleanup(e);}
52
+ };
53
+ tick();if(!ended)timer=setInterval(tick,1000/12);
54
+ await done;
55
+ }catch(e){cleanup(e);await done;}
56
+ }