quoxcli 0.4.0 → 0.6.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 (199) hide show
  1. package/CHANGELOG.md +110 -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/admin.js +7 -0
  6. package/lib/api/endpoints/agent-attach.js +7 -0
  7. package/lib/api/endpoints/compliance.js +25 -0
  8. package/lib/api/endpoints/governance.js +7 -0
  9. package/lib/api/endpoints/inbox.js +6 -0
  10. package/lib/api/endpoints/inboxq.js +58 -0
  11. package/lib/api/endpoints/index.js +4 -0
  12. package/lib/api/endpoints/lucid.js +17 -0
  13. package/lib/api/endpoints/mcp.js +35 -0
  14. package/lib/api/endpoints/mon.js +122 -0
  15. package/lib/api/endpoints/notesq.js +2 -0
  16. package/lib/api/endpoints/quoxplan.js +51 -0
  17. package/lib/api/endpoints/tools.js +13 -0
  18. package/lib/api/endpoints/v1.js +13 -0
  19. package/lib/commands/actions.js +51 -13
  20. package/lib/commands/admin.js +35 -0
  21. package/lib/commands/agentAttach.js +15 -0
  22. package/lib/commands/approvals.js +193 -3
  23. package/lib/commands/ask.js +1 -1
  24. package/lib/commands/auth.js +64 -11
  25. package/lib/commands/backup.js +11 -4
  26. package/lib/commands/capabilities.js +3 -2
  27. package/lib/commands/chat.js +15 -0
  28. package/lib/commands/claude.js +4 -1
  29. package/lib/commands/compliance.js +109 -0
  30. package/lib/commands/connect.js +69 -0
  31. package/lib/commands/contacts.js +2 -0
  32. package/lib/commands/dev.js +6 -8
  33. package/lib/commands/doctor.js +22 -3
  34. package/lib/commands/evidence.js +2 -1
  35. package/lib/commands/gripe.js +24 -13
  36. package/lib/commands/inbox.js +37 -3
  37. package/lib/commands/inboxq.js +147 -0
  38. package/lib/commands/kuma.js +78 -5
  39. package/lib/commands/licenceLinkView.js +311 -0
  40. package/lib/commands/linkedin.js +19 -0
  41. package/lib/commands/loop.js +30 -3
  42. package/lib/commands/lucid.js +7 -0
  43. package/lib/commands/lucidGoals.js +389 -0
  44. package/lib/commands/mail.js +427 -25
  45. package/lib/commands/mcp.js +190 -0
  46. package/lib/commands/memory.js +68 -4
  47. package/lib/commands/memorySync.js +158 -0
  48. package/lib/commands/mon.js +69 -144
  49. package/lib/commands/monitoring.js +68 -0
  50. package/lib/commands/notes.js +29 -7
  51. package/lib/commands/notesSeal.js +617 -0
  52. package/lib/commands/objective.js +5 -2
  53. package/lib/commands/offload.js +52 -12
  54. package/lib/commands/plan.js +487 -52
  55. package/lib/commands/provider.js +29 -8
  56. package/lib/commands/qlarity.js +32 -1
  57. package/lib/commands/run.js +2 -1
  58. package/lib/commands/server.js +37 -0
  59. package/lib/commands/setup.js +127 -75
  60. package/lib/commands/skills.js +4 -1
  61. package/lib/commands/slot.js +49 -9
  62. package/lib/commands/status.js +92 -44
  63. package/lib/commands/tool.js +30 -0
  64. package/lib/commands/vault.js +6 -3
  65. package/lib/commands/volt.js +6 -1
  66. package/lib/commands/walkthrough.js +14 -2
  67. package/lib/commands/watch.js +11 -2
  68. package/lib/commands/wazuh.js +880 -0
  69. package/lib/commands/wazuhIntake.js +358 -0
  70. package/lib/commands/wazuhReport.js +192 -0
  71. package/lib/commands/workflow.js +47 -4
  72. package/lib/commands/x.js +50 -0
  73. package/lib/core/announce.js +86 -0
  74. package/lib/core/auth.js +20 -2
  75. package/lib/core/callMe.js +118 -0
  76. package/lib/core/client.js +27 -1
  77. package/lib/core/commandRegistrars.js +9 -0
  78. package/lib/core/config.js +42 -3
  79. package/lib/core/confirm.js +68 -7
  80. package/lib/core/crossOrgApprovals.js +69 -0
  81. package/lib/core/errors.js +52 -1
  82. package/lib/core/governedSession.js +135 -5
  83. package/lib/core/inboxResolution.js +3 -2
  84. package/lib/core/loginGuard.js +73 -0
  85. package/lib/core/offload.js +120 -0
  86. package/lib/core/org-override.js +5 -1
  87. package/lib/core/output.js +30 -5
  88. package/lib/core/profileTrust.js +182 -0
  89. package/lib/core/quoxglyph.js +22 -1
  90. package/lib/core/router.js +14 -0
  91. package/lib/core/selfUpdate.js +2 -0
  92. package/lib/core/stepUpPrompt.js +99 -0
  93. package/lib/core/version.js +1 -1
  94. package/lib/core/walkthrough.js +9 -5
  95. package/lib/dragon/select.js +2 -2
  96. package/lib/quoxon/EXTENSIONS.md +142 -19
  97. package/lib/quoxon/api.js +189 -34
  98. package/lib/quoxon/app.js +1277 -188
  99. package/lib/quoxon/approvalBatch.js +117 -0
  100. package/lib/quoxon/approvalFacts.js +56 -2
  101. package/lib/quoxon/approvalSentence.js +8 -4
  102. package/lib/quoxon/approvalsView.js +72 -12
  103. package/lib/quoxon/ask.js +4 -2
  104. package/lib/quoxon/autoOpen.js +94 -0
  105. package/lib/quoxon/backgroundWork.js +59 -0
  106. package/lib/quoxon/bang.js +8 -2
  107. package/lib/quoxon/capabilities.js +7 -2
  108. package/lib/quoxon/commandShort.js +8 -1
  109. package/lib/quoxon/commands.js +42 -12
  110. package/lib/quoxon/connect.js +5 -3
  111. package/lib/quoxon/deckLazy.js +58 -0
  112. package/lib/quoxon/delegationDragon.js +49 -0
  113. package/lib/quoxon/destructive.js +35 -0
  114. package/lib/quoxon/dnd.js +118 -0
  115. package/lib/quoxon/dragonCompanion.js +37 -7
  116. package/lib/quoxon/dragonMoments.js +78 -0
  117. package/lib/quoxon/echo.js +1 -0
  118. package/lib/quoxon/events.js +55 -8
  119. package/lib/quoxon/extensions.js +400 -38
  120. package/lib/quoxon/foreignText.js +49 -0
  121. package/lib/quoxon/gripeQueue.js +135 -0
  122. package/lib/quoxon/inboxView.js +125 -22
  123. package/lib/quoxon/keys.js +2 -2
  124. package/lib/quoxon/launcher.js +2 -1
  125. package/lib/quoxon/loopCard.js +7 -2
  126. package/lib/quoxon/loopCards.js +17 -8
  127. package/lib/quoxon/loopStream.js +20 -9
  128. package/lib/quoxon/mailView.js +71 -0
  129. package/lib/quoxon/main.js +27 -6
  130. package/lib/quoxon/markdown.js +1 -1
  131. package/lib/quoxon/model.js +15 -0
  132. package/lib/quoxon/neverAnswered.js +20 -0
  133. package/lib/quoxon/objects/away.js +43 -5
  134. package/lib/quoxon/onboarding/connectServer.js +162 -0
  135. package/lib/quoxon/onboarding/copy.js +6 -0
  136. package/lib/quoxon/onboarding/dockerOffer.js +3 -1
  137. package/lib/quoxon/onboarding/index.js +5 -43
  138. package/lib/quoxon/onboarding/probe.js +20 -1
  139. package/lib/quoxon/onboarding/view.js +16 -3
  140. package/lib/quoxon/orgSwitch.js +22 -0
  141. package/lib/quoxon/print.js +2 -0
  142. package/lib/quoxon/profileSwitch.js +7 -0
  143. package/lib/quoxon/promptHistory.js +3 -1
  144. package/lib/quoxon/render.js +68 -16
  145. package/lib/quoxon/resend.js +14 -0
  146. package/lib/quoxon/roster.js +10 -5
  147. package/lib/quoxon/rosterSessions.js +44 -0
  148. package/lib/quoxon/rows.js +3 -1
  149. package/lib/quoxon/screenContext.js +1 -1
  150. package/lib/quoxon/secretShape.js +88 -0
  151. package/lib/quoxon/slash/agents.js +2 -1
  152. package/lib/quoxon/slash/argSources.js +9 -2
  153. package/lib/quoxon/slash/args.js +4 -2
  154. package/lib/quoxon/slash/broadcast.js +48 -11
  155. package/lib/quoxon/slash/broadcastRun.js +24 -7
  156. package/lib/quoxon/slash/cliLeaves.json +98 -1
  157. package/lib/quoxon/slash/dashboard.js +2 -0
  158. package/lib/quoxon/slash/hatWalk.js +1 -1
  159. package/lib/quoxon/slash/listView.js +2 -1
  160. package/lib/quoxon/slash/palette.js +63 -1
  161. package/lib/quoxon/slash/paletteController.js +15 -5
  162. package/lib/quoxon/slash/restart.js +9 -4
  163. package/lib/quoxon/slash/stepUp.js +5 -5
  164. package/lib/quoxon/slash/stepUpCmds.js +74 -1
  165. package/lib/quoxon/slashSpinner.js +35 -0
  166. package/lib/quoxon/slotAttach.js +8 -0
  167. package/lib/quoxon/slotChat.js +24 -3
  168. package/lib/quoxon/slotFormat.js +1 -1
  169. package/lib/quoxon/slotOutbox.js +113 -0
  170. package/lib/quoxon/slotRun.js +2 -0
  171. package/lib/quoxon/suggestCommand.js +1 -1
  172. package/lib/quoxon/term.js +25 -1
  173. package/lib/quoxon/transcript.js +2 -1
  174. package/lib/quoxon/webPty.js +15 -0
  175. package/lib/quoxon/welcome.js +1 -1
  176. package/lib/quoxon/whatsnew.js +1 -1
  177. package/lib/quoxon/whatsnew.jsonl +42 -0
  178. package/lib/render/planBrief.js +614 -0
  179. package/lib/server/installFlow.js +168 -0
  180. package/lib/server/status.js +15 -1
  181. package/lib/toolOutputShape.js +61 -3
  182. package/package.json +3 -2
  183. package/vendor/dragonlab/SOURCE +3 -3
  184. package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
  185. package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
  186. package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
  187. package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
  188. package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
  189. package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
  190. package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
  191. package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
  192. package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
  193. package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
  194. package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
  195. package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
  196. package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
  197. package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
  198. package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
  199. package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
@@ -0,0 +1,34 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig.mjs';
2
+ export const VULCAN_CLIPS=Object.freeze([{id:'idle',title:'Banked Furnace',duration:6000},{id:'fire',title:'Fire and Brimstone',duration:6000}]);
3
+ export const STORY_DURATION=12000;
4
+ export function poseRig(rig,scene,ms){
5
+ const t=ms%6000,heat=(1-Math.cos(t/6000*Math.PI*2))/2;
6
+ const paths=rig.paths.map(p=>({...p,rgb:p.id.includes('lava')||p.id.includes('heat')||p.id.includes('crack')||p.id==='hot-billet'||p.id==='hammer-seam'?[255,Math.round(100+95*heat),Math.round(25+30*heat)]:p.rgb}));
7
+ // Fixed camera includes the complete mouth-to-forge flame envelope.
8
+ if(scene==='fire'){
9
+ const power=Math.sin(Math.PI*t/6000)**2;
10
+ if(power>.02){
11
+ const flame=(id,rgb,points)=>({id,group:'fire',rgb,stroke:0,erase:false,polygons:[points]});
12
+ // Authored muzzle already faces right/down toward the workpiece.
13
+ const tip=[468+153*power,194+113*power],cx=468+87*power,cy=194+57*power;
14
+ paths.push(flame('fire-outer',[255,85,20],[[458,187],[cx,cy-23*power],[tip[0]-9*power,tip[1]-34*power],tip,[tip[0]-45*power,tip[1]-12*power],[cx-13*power,cy+19*power],[458,201]]));
15
+ paths.push(flame('fire-heart',[255,220,92],[[458,192],[cx,cy-5*power],[tip[0]-8*power,tip[1]-6*power],[cx-5*power,cy+7*power],[458,198]]));
16
+ for(let i=0;i<4;i++){
17
+ const phase=(t/1100+i/4)%1,x=595+(i-1.5)*18+phase*12,y=304-phase*64;
18
+ const s=2+2*power;paths.push(flame('brimstone-'+i,[255,211,67],[[x,y-s],[x+s,y],[x,y+s],[x-s,y]]));
19
+ }
20
+ }
21
+ }
22
+ return {...rig,paths};
23
+ }
24
+ export function renderVulcan(rig,scene='idle',ms=0,options={}){
25
+ if(!VULCAN_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
26
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
27
+ const r=renderSvgRig(poseRig(rig,scene,options.still?3000:ms),options);
28
+ return {...r,scene,clip:scene,caption:scene==='fire'?'Fire and brimstone.':'The furnace waits.'};
29
+ }
30
+ export function renderStory(rig,ms,options={}){
31
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
32
+ const t=options.still?9000:ms%STORY_DURATION;
33
+ return renderVulcan(rig,t<6000?'idle':'fire',t%6000,options);
34
+ }
@@ -0,0 +1,57 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
2
+ <title>Vulcan — basalt dragon at the brimstone forge</title>
3
+ <g id="tail">
4
+ <path id="tail-rock" fill="#706a71" d="M 297 331 C 212 319 191 391 110 380 L 57 345 L 75 398 Q 167 450 290 398 Z"/>
5
+ <path id="tail-lava" fill="none" stroke="#ff801f" stroke-width="9" d="M 283 365 L 228 363 L 191 399 L 120 398 L 79 375"/>
6
+ <path id="tail-spines" fill="#c1a68c" d="M 80 362 L 53 317 L 112 376 L 121 347 L 152 382 L 176 355 L 190 381 L 214 333 L 234 356 Z"/>
7
+ </g>
8
+ <g id="wings">
9
+ <path id="far-wing" fill="#504953" d="M 366 287 L 349 126 L 293 65 L 300 162 L 241 129 L 278 219 L 252 239 Z"/>
10
+ <path id="near-wing" fill="#7e7273" d="M 302 307 L 231 186 L 146 130 L 159 213 L 112 192 L 154 265 L 123 284 L 229 316 Z"/>
11
+ <path id="wing-lava" fill="none" stroke="#ff8b27" stroke-width="9" d="M 302 307 L 231 186 L 146 130 M 231 186 L 190 255 L 123 284 M 366 287 L 349 126 L 293 65"/>
12
+ </g>
13
+ <g id="body">
14
+ <path id="body-rock" fill="#8b7d78" d="M 290 205 L 367 188 L 433 250 L 452 345 L 416 408 L 291 409 L 255 359 L 263 278 Z"/>
15
+ <path id="shoulder-plate" fill="#b4a08a" d="M 291 214 L 332 190 L 361 222 L 346 259 L 301 266 L 276 240 Z"/>
16
+ <path id="belly-core" fill="#ff6820" d="M 352 255 L 403 268 L 420 329 L 394 381 L 346 366 L 321 312 Z"/>
17
+ <path id="belly-plates" fill="none" stroke="#554953" stroke-width="15" d="M 336 280 L 405 295 M 329 310 L 418 326 M 339 341 L 408 352"/>
18
+ <path id="rock-feet" fill="#a28b7a" d="M 292 345 L 329 365 L 321 410 L 341 430 L 266 430 L 252 410 L 261 370 Z M 397 365 L 434 356 L 453 407 L 476 428 L 390 430 L 378 411 Z"/>
19
+ <path id="claws" fill="#f3dfb0" d="M 268 413 L 255 433 L 282 430 L 290 410 L 287 431 L 309 430 L 315 414 L 321 432 L 343 429 M 398 412 L 386 433 L 409 430 L 420 411 L 419 432 L 442 431 L 447 414 L 454 432 L 479 430"/>
20
+ <path id="body-cracks" fill="none" stroke="#ffbc37" stroke-width="7" d="M 281 269 L 298 300 L 279 327 L 297 345 M 421 271 L 435 305 L 420 329 M 282 379 L 296 397 L 286 414"/>
21
+ </g>
22
+ <g id="neck">
23
+ <path id="neck-rock" fill="#99857a" d="M 338 163 L 397 162 L 430 257 L 375 280 L 327 232 Z"/>
24
+ <path id="neck-lava" fill="none" stroke="#ff9c2f" stroke-width="10" d="M 371 187 L 362 222 L 388 246"/>
25
+ </g>
26
+ <g id="head">
27
+ <path id="horn-far" fill="#d3ba91" d="M 330 131 L 311 89 L 326 33 L 341 92 L 357 115 Z"/>
28
+ <path id="horn-near" fill="#e4c798" d="M 387 119 L 401 65 L 439 34 L 423 90 L 420 138 Z"/>
29
+ <path id="head-rock" fill="#a99786" d="M 325 110 L 370 93 L 414 113 L 443 148 L 429 201 L 385 224 L 339 203 L 312 157 Z"/>
30
+ <path id="crown-plates" fill="#6d6063" d="M 328 112 L 347 83 L 365 110 L 382 77 L 400 112 L 418 94 L 422 134 L 382 123 Z"/>
31
+ <path id="face-cracks" fill="none" stroke="#ffbc37" stroke-width="7" d="M 371 109 L 367 131 L 380 143 M 335 172 L 348 192 L 372 202"/>
32
+ <path id="muzzle" fill="#bf9c7f" d="M 406 156 L 448 158 L 469 177 L 461 198 L 423 211 L 394 193 Z"/>
33
+ <path id="furnace-mouth" fill="#080d19" d="M 419 183 L 464 183 L 458 199 L 423 206 L 410 197 Z"/>
34
+ <path id="mouth-heat" fill="#ff8c22" d="M 423 191 L 459 189 L 452 196 L 427 201 Z"/>
35
+ <path id="nose" fill="#080d19" d="M 448 166 L 458 174 L 446 176 Z"/>
36
+ <path id="brow" fill="none" stroke="#e3c49b" stroke-width="8" d="M 330 140 L 354 135 M 374 137 L 406 143"/>
37
+ </g>
38
+ <g id="tools">
39
+ <path id="hammer-handle" fill="none" stroke="#d5aa77" stroke-width="17" d="M 461 296 L 533 140"/>
40
+ <path id="hammer-head" fill="#9b9290" d="M 493 86 L 565 110 L 580 151 L 559 176 L 485 150 L 474 112 Z"/>
41
+ <path id="hammer-face" fill="#dbb885" d="M 565 110 L 580 151 L 559 176 L 549 135 Z"/>
42
+ <path id="hammer-seam" fill="none" stroke="#ffaf2b" stroke-width="8" d="M 495 102 L 518 121 L 509 142 L 545 157"/>
43
+ <path id="arm" fill="#b59b80" d="M 410 247 L 438 241 L 455 267 L 478 253 L 495 264 L 493 288 L 455 309 L 431 293 Z"/>
44
+ <path id="grip" fill="none" stroke="#554953" stroke-width="5" d="M 471 261 L 488 271 M 466 273 L 485 282"/>
45
+ <path id="anvil" fill="#9a98a4" d="M 519 321 L 690 321 L 737 339 L 682 356 L 654 356 L 638 385 L 649 404 L 680 418 L 680 433 L 549 433 L 549 419 L 580 405 L 591 385 L 575 358 L 544 355 Z"/>
46
+ <path id="anvil-top" fill="none" stroke="#f0d8b2" stroke-width="9" d="M 521 322 L 688 322 L 733 338"/>
47
+ <path id="hot-billet" fill="#ffac2d" d="M 567 307 L 638 307 L 648 319 L 563 319 Z"/>
48
+ </g>
49
+ <g id="eyes">
50
+ <path id="far-eye-socket" fill="#080d19" d="M 325 146 Q 342 133 358 148 L 360 175 Q 342 188 326 173 Z"/>
51
+ <path id="far-eye-white" fill="#ffffff" d="M 331 149 Q 343 141 353 151 L 353 169 Q 342 179 332 169 Z"/>
52
+ <path id="far-pupil" fill="#080d19" d="M 343 160 C 343 152 351 152 351 160 C 351 168 343 168 343 160 Z"/>
53
+ <path id="near-eye-socket" fill="#080d19" d="M 369 145 Q 391 132 412 151 L 413 178 Q 389 193 370 177 Z"/>
54
+ <path id="near-eye-white" fill="#ffffff" d="M 376 149 Q 392 141 406 154 L 406 172 Q 391 183 377 172 Z"/>
55
+ <path id="near-pupil" fill="#080d19" d="M 391 162 C 391 152 401 152 401 162 C 401 172 391 172 391 162 Z"/>
56
+ </g>
57
+ </svg>
@@ -0,0 +1,39 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig.mjs';
2
+ import {renderVulcan as previous,poseRig as heatRig,VULCAN_CLIPS as previousClips} from '../v1/character.mjs';
3
+ export const VULCAN_CLIPS=Object.freeze([...previousClips,{id:'forge',title:'The Brimstone Forge',duration:9000}]);
4
+ export const STORY_DURATION=9000;
5
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
6
+ export function hammerAngle(t){
7
+ if(t<3000)return 0;
8
+ if(t<3800)return -.15*ease((t-3000)/800);
9
+ if(t<4200)return -.15+1.23*((t-3800)/400)**2;
10
+ if(t<4480)return 1.08-.22*ease((t-4200)/280);
11
+ if(t<4900)return .86+.09*ease((t-4480)/420);
12
+ if(t<7000)return .95*(1-ease((t-4900)/2100));
13
+ return 0;
14
+ }
15
+ export function poseRig(rig,ms){
16
+ const t=ms%9000;
17
+ const posed=heatRig(rig,t<3000?'fire':'idle',t<3000?t*2:0);
18
+ const a=hammerAngle(t),c=Math.cos(a),s=Math.sin(a);
19
+ const turn=([x,y])=>[478+(x-478)*c-(y-275)*s,275+(x-478)*s+(y-275)*c];
20
+ const moving=new Set(['hammer-handle','hammer-head','hammer-face','hammer-seam','grip']);
21
+ const paths=posed.paths.map(p=>moving.has(p.id)?{...p,polygons:p.polygons.map(poly=>poly.map(turn))}:p);
22
+ const delta=t-4200;
23
+ if(delta>=0&&delta<1000){
24
+ const u=delta/1000;
25
+ for(let i=0;i<9;i++){
26
+ const vx=(i-4)*25,vy=-85-12*(i%3),x=625+vx*u,y=307+vy*u+72*u*u,r=3.5*(1-u)+.5;
27
+ paths.push({id:'strike-spark-'+i,group:'sparks',stroke:0,erase:false,rgb:i%2?[255,233,143]:[255,154,34],polygons:[[[x-r,y],[x,y-r*1.8],[x+r,y],[x,y+r*1.8]]]});
28
+ }
29
+ }
30
+ return {...posed,paths};
31
+ }
32
+ export function renderVulcan(rig,scene='idle',ms=0,options={}){
33
+ if(scene!=='forge')return previous(rig,scene,ms,options);
34
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
35
+ const t=options.still?4350:ms%9000;
36
+ const caption=t<3000?'Heat the iron.':t<4200?'Raise the hammer.':t<4900?'Fire. Iron. Impact.':t<7000?'Let the sparks settle.':'Ready for the next forging.';
37
+ return {...renderSvgRig(poseRig(rig,t),options),clip:scene,scene,caption};
38
+ }
39
+ export const renderStory=(rig,ms,options={})=>renderVulcan(rig,'forge',ms,options);
@@ -0,0 +1,41 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig.mjs';
2
+ import {poseRig as thermalPose} from '../v1/character.mjs';
3
+ import {poseRig as forgePose,renderVulcan as previous,VULCAN_CLIPS as oldClips} from '../v2/character.mjs';
4
+ export const VULCAN_CLIPS=Object.freeze([{id:'attach',title:'The Smith Arrives',duration:3600},...oldClips.map(c=>c.id==='idle'?{...c,duration:12000}:c)]);
5
+ export const STORY_DURATION=15600;
6
+ const pulse=(t,start,end)=>t<=start||t>=end?0:Math.sin(Math.PI*(t-start)/(end-start))**2;
7
+ export function poseRig(rig,scene,ms){
8
+ const t=scene==='forge'?ms%9000:scene==='idle'?ms%12000:ms;
9
+ let base=scene==='forge'?forgePose(rig,t):thermalPose(rig,scene==='fire'?'fire':'idle',scene==='attach'?0:t);
10
+ // Keep mouth and target locked while emitting fire. Move only after it dies down.
11
+ const nod=scene==='attach'?pulse(t,300,1900):scene==='forge'?pulse(t,3800,4900):scene==='idle'?.22*pulse(t,2200,5200):0;
12
+ const follow=scene==='attach'?pulse(t,900,3200):scene==='forge'?pulse(t,4200,6800):scene==='idle'?.35*pulse(t,3100,7800):0;
13
+ const salute=scene==='attach'?-.13*pulse(t,1100,2900):0,c=Math.cos(salute),s=Math.sin(salute);
14
+ const tools=new Set(['hammer-handle','hammer-head','hammer-face','hammer-seam','grip']);
15
+ const paths=base.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
16
+ if(p.group==='head'||p.group==='eyes')return [x+4*nod,y+10*nod];
17
+ if(p.group==='neck'){const w=Math.max(0,Math.min(1,(270-y)/65));return [x+4*nod*w,y+10*nod*w];}
18
+ if(p.group==='wings'){const w=Math.max(0,Math.min(1,(310-y)/170));return [x+7*follow*w,y+5*follow*w];}
19
+ if(p.group==='tail'){const w=Math.max(0,Math.min(1,(270-x)/190));return [x,y-9*follow*w*w];}
20
+ if(tools.has(p.id)&&salute)return [478+(x-478)*c-(y-275)*s,275+(x-478)*s+(y-275)*c];
21
+ return [x,y];
22
+ }))}));
23
+ const byId=new Map(paths.map(p=>[p.id,p]));
24
+ const eyes=rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}));
25
+ return {...base,paths,eyes};
26
+ }
27
+ export function renderVulcan(rig,scene='idle',ms=0,options={}){
28
+ if(!VULCAN_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
29
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
30
+ if(scene==='attach'&&ms>=3600&&!options.still)return renderVulcan(rig,'idle',ms-3600,options);
31
+ const t=options.still?({attach:1400,idle:4000,forge:4350,fire:3000}[scene]):ms;
32
+ const caption=scene==='attach'?'A nod from the forge.':scene==='idle'?'The furnace waits.':previous(rig,scene,t,options).caption;
33
+ return {...renderSvgRig(poseRig(rig,scene,t),options),clip:scene,scene,caption};
34
+ }
35
+ export function renderStory(rig,ms,options={}){
36
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
37
+ if(options.still)return renderVulcan(rig,'forge',4350,options);
38
+ if(ms<3600)return renderVulcan(rig,'attach',ms,options);
39
+ if(ms<12600)return renderVulcan(rig,'forge',ms-3600,options);
40
+ return renderVulcan(rig,'idle',ms-12600,options);
41
+ }
@@ -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 {renderVulcan,VULCAN_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('Vulcan V3 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(VULCAN_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','VULCAN\nThe furnace waits.');
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/vulcan-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=renderVulcan(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:'VULCAN\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 {VULCAN_CLIPS,renderVulcan,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?VULCAN_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?renderVulcan(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'VULCAN · '+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,64 @@
1
+ /** Shared authored SVG → original Q cells, supporting independently owned eyes. */
2
+ import {flattenPath,sampleShape} from '../design/stage3/svg-coverage.js';
3
+ import {makeField,fieldToCells} from '../vendor/quox-dragon/qart.js';
4
+ const attr=(s,k)=>s.match(new RegExp(`(?:^|\\s)${k}="([^"]*)"`))?.[1];
5
+ export function parseSvgRig(svg){
6
+ if(!svg.includes('viewBox="0 0 800 480"'))throw Error('Expected authored 800x480 master');
7
+ const ids=new Set(),groups=new Map(),stack=[];
8
+ for(const [tag]of svg.matchAll(/<g\s[^>]*>|<\/g>|<path\s[^>]*\/>/g)){
9
+ if(tag.startsWith('<g '))stack.push(attr(tag,'id'));else if(tag==='</g>')stack.pop();else groups.set(attr(tag,'id'),stack.at(-1));
10
+ }
11
+ const paths=[...svg.matchAll(/<path\s[^>]*\/>/g)].map(([tag])=>{
12
+ const id=attr(tag,'id'),d=attr(tag,'d'),stroke=attr(tag,'stroke'),fill=attr(tag,'fill');
13
+ if(!id||ids.has(id)||!d)throw Error('Every path requires unique id and geometry');ids.add(id);
14
+ const colour=stroke||fill;if(!/^#[0-9a-f]{6}$/i.test(colour))throw Error('Explicit hex colour required');
15
+ const width=stroke?Number(attr(tag,'stroke-width')):0;
16
+ if(!Number.isFinite(width)||width<0||width>30)throw Error('Invalid stroke width');
17
+ return {id,group:groups.get(id),polygons:flattenPath(d),rgb:[1,3,5].map(i=>parseInt(colour.slice(i,i+2),16)),stroke:width,erase:colour==='#080d19'};
18
+ });
19
+ const eyes=paths.filter(p=>p.id.endsWith('-eye-socket')).map(p=>{
20
+ const name=p.id.slice(0,-11),white=paths.find(p=>p.id===name+'-eye-white'),pupil=paths.find(p=>p.id===name+'-pupil');
21
+ if(!white||!pupil)throw Error('Every eye needs white and pupil');return {name,socket:p,white,pupil};
22
+ });
23
+ if(!paths.length||!eyes.length)throw Error('Missing anatomy or eyes');
24
+ const points=paths.flatMap(p=>p.polygons.flat());
25
+ const x=points.map(p=>p[0]),y=points.map(p=>p[1]);
26
+ return {paths,eyes,bounds:{left:Math.min(...x),top:Math.min(...y),right:Math.max(...x),bottom:Math.max(...y)}};
27
+ }
28
+ export function renderSvgRig(rig,{cols=39,rows=11,mono=false}={}){
29
+ if(!Number.isSafeInteger(cols)||!Number.isSafeInteger(rows)||cols<28||rows<10||cols>200||rows>72)throw RangeError('Art needs 28–200 columns and 10–72 rows');
30
+ if(typeof mono!=='boolean')throw TypeError('mono must be boolean');
31
+ const W=cols*2,H=rows*4,{left,top,right,bottom}=rig.bounds;
32
+ const scale=Math.min((W-4)/(right-left),(H-4)/(bottom-top));
33
+ const geometry={W,H,scale,ox:(W-(right-left)*scale)/2-left*scale,oy:(H-(bottom-top)*scale)/2-top*scale};
34
+ const field=makeField(W,H),labels=new Array(W*H).fill(''),colour=new Array(W*H).fill(null);
35
+ const coverage=p=>{const indices=[];sampleShape(p.polygons,{...geometry,stroke:p.stroke},(x,y)=>indices.push(y*W+x));return indices;};
36
+ const eyeIds=new Set(rig.eyes.flatMap(e=>[e.socket.id,e.white.id,e.pupil.id]));
37
+ function paint(p){for(const i of coverage(p)){field.v[i]=p.erase?0:1;labels[i]=p.id;colour[i]=p.rgb;}}
38
+ for(const p of rig.paths)if(!eyeIds.has(p.id))paint(p);
39
+ // Track eye cells for diagnostics; carve only the curved socket coverage.
40
+ const owned=new Set();
41
+ for(const e of rig.eyes)for(const i of coverage(e.socket))owned.add(Math.floor(i%W/2)+Math.floor(Math.floor(i/W)/4)*cols);
42
+ // Preserve facial dots outside each authored socket. Cell colour still prioritizes eye white.
43
+ for(const e of rig.eyes)paint(e.socket);
44
+ const pupilDots={};
45
+ for(const e of rig.eyes){
46
+ paint(e.white);let dots=coverage(e.pupil);
47
+ if(!dots.length){
48
+ // Optical correction is local to THIS eye; no Lucid-specific coordinates.
49
+ const points=e.pupil.polygons.flat(),cx=points.reduce((n,p)=>n+p[0],0)/points.length,cy=points.reduce((n,p)=>n+p[1],0)/points.length;
50
+ let best=-1,d=Infinity;
51
+ for(let i=0;i<labels.length;i++)if(labels[i]===e.white.id&&field.v[i]){const q=(i%W+.5-(geometry.ox+cx*scale))**2+(Math.floor(i/W)+.5-(geometry.oy+cy*scale))**2;if(q<d){best=i;d=q;}}
52
+ if(best>=0)dots=[best];
53
+ }
54
+ for(const i of dots){field.v[i]=0;labels[i]=e.pupil.id;colour[i]=null;}
55
+ pupilDots[e.name]=dots.length;
56
+ }
57
+ const frame=fieldToCells(field,{color:false,quant:false});let ink=0;
58
+ for(let y=0;y<rows;y++)for(let x=0;x<cols;x++){
59
+ const c=frame[y][x];c.bg=null;c.rgb=null;let chosen=-1,rank=-1;
60
+ for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++){const i=(y*4+dy)*W+x*2+dx;if(!field.v[i])continue;const r=labels[i].endsWith('-eye-white')?2:1;if(r>rank){rank=r;chosen=i;}}
61
+ if(chosen>=0){ink++;if(!mono)c.rgb=[...colour[chosen]];}
62
+ }
63
+ return {ok:true,frame,geometry,metrics:{cols,rows,ink,pupilDots,eyeOwnedCells:owned.size},visualOnly:true,demo:true};
64
+ }
@@ -8,6 +8,8 @@ export const CHARACTERS=Object.freeze({
8
8
  seer:{name:'Seer',version:'V4 flowing eight',review:'../../design/seer/v4/review.mjs',power:'pattern',powerName:'Pattern Sight',powerDuration:5200,arrival:{scene:'swim',duration:5800}},
9
9
  sentinel:{name:'Sentinel',version:'A solid (provisional)',review:'../../design/stage3/companion-review.mjs',power:'scorch',powerName:'Aegis Inferno',powerDuration:3800,arrival:{scene:'greeting',duration:2400}},
10
10
  cipher:{name:'Cipher',version:'V2 network acting',review:'../../design/cipher/v2/review.mjs',power:'trace',powerName:'Trace Route',powerDuration:4800,arrival:{scene:'attach',duration:3200}},
11
+ vulcan:{name:'Vulcan',version:'V3 forge salute',review:'../../design/vulcan/v3/review.mjs',power:'forge',powerName:'Brimstone Forge',powerDuration:9000,arrival:{scene:'attach',duration:3600}},
12
+ quoxperticus:{name:'Quoxperticus',version:'V4 fleet conductor',review:'../../design/quoxperticus/v4/review.mjs',power:'orchestrate',powerName:'Fleet Conductor',powerDuration:10000,arrival:{scene:'attach',duration:3000}},
11
13
  drago:{name:'Drago',version:'V4 scoped inferno',review:'../../design/drago/v4/review.mjs',power:'fire',powerName:'Scoped Inferno',powerDuration:6800,arrival:{scene:'attach',duration:3800}},
12
14
  });
13
15
  const ids=Object.keys(CHARACTERS);
@@ -16,6 +18,10 @@ const SCENES=Object.freeze({
16
18
  sentinel:{curious:'scan',focus:'scan',concerned:'failure',proud:'idle'},
17
19
  // Cipher claims no network facts: 'restored'/'delay'/'broken' routes are simulated, so completion stays on plain listening.
18
20
  cipher:{curious:'inspect',focus:'inspect',standby:'idle',concerned:'broken',proud:'idle'},
21
+ // Vulcan has only idle, fire and forge clips besides the entrance. 'fire' is a decorative flourish, so nothing here claims a build or a fault: every host mood rests on idle and the forge is the working power.
22
+ vulcan:{curious:'idle',focus:'idle',standby:'idle',concerned:'idle',proud:'idle'},
23
+ // Quoxperticus: 'skill' (select a skill) stands for attention; the fleet scene is simulated, so it only appears as the working power.
24
+ quoxperticus:{curious:'skill',focus:'skill',standby:'idle',concerned:'idle',proud:'idle'},
19
25
  drago:{curious:'inspect',focus:'active',concerned:'concerned',proud:'acknowledge'},
20
26
  });
21
27
  const SENTINEL_MOOD=Object.freeze({idle:'cute',greeting:'cute',standby:'cute',scan:'alert',approval:'alert',failure:'alert',warden:'warden',brace:'warden',scorch:'warden'});
@@ -74,6 +80,20 @@ export async function loadCharacter(id){
74
80
  ]);
75
81
  return {...spec,render:(scene,time,options)=>renderCipher(rig,scene,time,options)};
76
82
  }
83
+ if(id==='vulcan'){
84
+ const [rig,{renderVulcan}]=await Promise.all([
85
+ readFile(new URL('../../design/vulcan/v1/vulcan-master.svg',import.meta.url),'utf8').then(parseSvgRig),
86
+ import('../../design/vulcan/v3/character.mjs'),
87
+ ]);
88
+ return {...spec,render:(scene,time,options)=>renderVulcan(rig,scene,time,options)};
89
+ }
90
+ if(id==='quoxperticus'){
91
+ const [rig,{renderQuoxperticus}]=await Promise.all([
92
+ readFile(new URL('../../design/quoxperticus/v2/quoxperticus-master.svg',import.meta.url),'utf8').then(parseSvgRig),
93
+ import('../../design/quoxperticus/v4/character.mjs'),
94
+ ]);
95
+ return {...spec,render:(scene,time,options)=>renderQuoxperticus(rig,scene,time,options)};
96
+ }
77
97
  if(id==='drago'){
78
98
  const [rig,{renderDrago}]=await Promise.all([
79
99
  readFile(new URL('../../design/drago/v1/drago-master.svg',import.meta.url),'utf8').then(parseSvgRig),
@@ -0,0 +1,2 @@
1
+ import {createDragonCompanion} from './companion.mjs';
2
+ export const createCompanion=options=>createDragonCompanion('quoxperticus',options);
@@ -18,7 +18,7 @@ export async function loadDragonView(id,{still=false,arrival=true,mono=false,per
18
18
  // Greeting-only: withdrawn for good once the entrance is over, unless an approval is pending.
19
19
  if(!persist&&!still&&now-state.started>=character(id).arrival.duration&&!state.approvals.length)return {frame:null,caption:null,hidden:true,name:actor.name,scene:null};
20
20
  const slot=artSlot(cols,Math.max(1,rows));
21
- if(id==='seer'||id==='cipher'){slot.cols=Math.min(39,slot.cols);slot.rows=Math.min(11,slot.rows);}
21
+ if(id==='seer'||id==='cipher'||id==='vulcan'||id==='quoxperticus'){slot.cols=Math.min(39,slot.cols);slot.rows=Math.min(11,slot.rows);}
22
22
  const v=activityView(id,state,now,{arrival:arrival&&!still});
23
23
  const result=slot.showArt?actor.render(v.scene,v.time,{...slot,still,mono,motionTime:v.motionTime}):null;
24
24
  if(result&&!result.ok)throw new Error(result.code);
@@ -0,0 +1,2 @@
1
+ import {createDragonCompanion} from './companion.mjs';
2
+ export const createCompanion=options=>createDragonCompanion('vulcan',options);