auvrynt 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -0
  3. package/dist/blender-client.js +109 -0
  4. package/dist/blender-tools.js +930 -0
  5. package/dist/cli.js +370 -0
  6. package/dist/config.js +211 -0
  7. package/dist/connection-status.js +91 -0
  8. package/dist/db/client.js +22 -0
  9. package/dist/db/schema.js +29 -0
  10. package/dist/dotnet-tools.js +223 -0
  11. package/dist/git-worktrees.js +134 -0
  12. package/dist/git.js +41 -0
  13. package/dist/godot-animation-tilemap.js +82 -0
  14. package/dist/godot-csharp-build.js +93 -0
  15. package/dist/godot-csharp-project.js +157 -0
  16. package/dist/godot-csharp-runner.js +146 -0
  17. package/dist/godot-csharp-semantic.js +161 -0
  18. package/dist/godot-csharp-validate.js +71 -0
  19. package/dist/godot-dotnet-env.js +169 -0
  20. package/dist/godot-editor-bridge.js +140 -0
  21. package/dist/godot-gdscript.js +824 -0
  22. package/dist/godot-project-settings.js +223 -0
  23. package/dist/godot-runtime-testing.js +249 -0
  24. package/dist/godot-tools.js +183 -0
  25. package/dist/image-tools.js +157 -0
  26. package/dist/logger.js +76 -0
  27. package/dist/oauth-provider.js +266 -0
  28. package/dist/pi-tools.js +81 -0
  29. package/dist/processes.js +218 -0
  30. package/dist/review-checkpoints.js +142 -0
  31. package/dist/roots.js +53 -0
  32. package/dist/search-discovery.js +271 -0
  33. package/dist/serena-manager.js +443 -0
  34. package/dist/serena-tools.js +411 -0
  35. package/dist/server.js +2835 -0
  36. package/dist/skills.js +45 -0
  37. package/dist/ui/.vite/manifest.json +2520 -0
  38. package/dist/ui/assets/abap-CLvhMVsD.js +1 -0
  39. package/dist/ui/assets/actionscript-3--17pq3dv.js +1 -0
  40. package/dist/ui/assets/ada-C5qYipkI.js +1 -0
  41. package/dist/ui/assets/andromeeda-vGVdxbeo.js +1 -0
  42. package/dist/ui/assets/angular-html-BJqQFYI6.js +1 -0
  43. package/dist/ui/assets/angular-ts-CMJECO0Y.js +1 -0
  44. package/dist/ui/assets/apache-U0d_L8uA.js +1 -0
  45. package/dist/ui/assets/apex-CGTLDQj6.js +1 -0
  46. package/dist/ui/assets/apl-DWeXpBBr.js +1 -0
  47. package/dist/ui/assets/applescript-CCn79oCD.js +1 -0
  48. package/dist/ui/assets/ara-4CJ0cIlV.js +1 -0
  49. package/dist/ui/assets/asciidoc-DE70LPWp.js +1 -0
  50. package/dist/ui/assets/asm-Cmm7eHzH.js +1 -0
  51. package/dist/ui/assets/astro-CLbttLOq.js +1 -0
  52. package/dist/ui/assets/aurora-x-CDeNXAV0.js +1 -0
  53. package/dist/ui/assets/awk-BWXHIvNe.js +1 -0
  54. package/dist/ui/assets/ayu-dark-DluEY0Gj.js +1 -0
  55. package/dist/ui/assets/ayu-light-C3h-C4tm.js +1 -0
  56. package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  57. package/dist/ui/assets/ballerina-B7ZEbQpA.js +1 -0
  58. package/dist/ui/assets/bat-Bo4NYOV-.js +1 -0
  59. package/dist/ui/assets/beancount-D-usSTwE.js +1 -0
  60. package/dist/ui/assets/berry-DKpUyyne.js +1 -0
  61. package/dist/ui/assets/bibtex-Ci_nEsc7.js +1 -0
  62. package/dist/ui/assets/bicep-CUHmPFLl.js +1 -0
  63. package/dist/ui/assets/bird2-C2hNVINV.js +1 -0
  64. package/dist/ui/assets/blade-BOzY0P2V.js +1 -0
  65. package/dist/ui/assets/bsl-BkkzgIyY.js +1 -0
  66. package/dist/ui/assets/c-BG-8uJvh.js +1 -0
  67. package/dist/ui/assets/c3-CnJL0r0V.js +1 -0
  68. package/dist/ui/assets/cadence-CQ2zXKGN.js +1 -0
  69. package/dist/ui/assets/cairo-DLTphjLi.js +1 -0
  70. package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +1 -0
  71. package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +1 -0
  72. package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +1 -0
  73. package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +1 -0
  74. package/dist/ui/assets/clarity-SemFz856.js +1 -0
  75. package/dist/ui/assets/clojure-DqKBuwfJ.js +1 -0
  76. package/dist/ui/assets/cmake-Bj61d0ZC.js +1 -0
  77. package/dist/ui/assets/cobol-I_17IuGe.js +1 -0
  78. package/dist/ui/assets/codeowners-C8r90Shi.js +1 -0
  79. package/dist/ui/assets/codeql-oeQT6MSM.js +1 -0
  80. package/dist/ui/assets/coffee-C0B8hmok.js +1 -0
  81. package/dist/ui/assets/common-lisp-Cv5bFMCO.js +1 -0
  82. package/dist/ui/assets/coq-BrsZFFmf.js +1 -0
  83. package/dist/ui/assets/cpp-DDJXt6Vs.js +1 -0
  84. package/dist/ui/assets/crystal-Bexqace6.js +1 -0
  85. package/dist/ui/assets/csharp-Ct8U2NOr.js +1 -0
  86. package/dist/ui/assets/css-DSQhw_9R.js +1 -0
  87. package/dist/ui/assets/csv-Dx-8-gkx.js +1 -0
  88. package/dist/ui/assets/cue-CE9AQfxI.js +1 -0
  89. package/dist/ui/assets/cypher-ClKdZ_lG.js +1 -0
  90. package/dist/ui/assets/d-qD-0Kul2.js +1 -0
  91. package/dist/ui/assets/dark-plus-Cs2F2srj.js +1 -0
  92. package/dist/ui/assets/dart-DkHntEIa.js +1 -0
  93. package/dist/ui/assets/dax-BkyTk9wS.js +1 -0
  94. package/dist/ui/assets/desktop-Dlh5hvp9.js +1 -0
  95. package/dist/ui/assets/diff-woXpYk--.js +1 -0
  96. package/dist/ui/assets/docker-IyjqRm3v.js +1 -0
  97. package/dist/ui/assets/dotenv-_5a1GRtc.js +1 -0
  98. package/dist/ui/assets/dracula-BHWKrbxM.js +1 -0
  99. package/dist/ui/assets/dracula-soft-5eyTD99u.js +1 -0
  100. package/dist/ui/assets/dream-maker-DW3nJb8Q.js +1 -0
  101. package/dist/ui/assets/edge-Z8YYkHOr.js +1 -0
  102. package/dist/ui/assets/elixir-CG9uKtAF.js +1 -0
  103. package/dist/ui/assets/elm-GfWG2IfT.js +1 -0
  104. package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +1 -0
  105. package/dist/ui/assets/erb-xe89e73J.js +1 -0
  106. package/dist/ui/assets/erlang-Cphh6RMH.js +1 -0
  107. package/dist/ui/assets/everforest-dark-sB-x3p7T.js +1 -0
  108. package/dist/ui/assets/everforest-light-Df2xbC6M.js +1 -0
  109. package/dist/ui/assets/fennel-DQxkIbk2.js +1 -0
  110. package/dist/ui/assets/fish-BJitypiv.js +1 -0
  111. package/dist/ui/assets/fluent-C03EYrpw.js +1 -0
  112. package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
  113. package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +1 -0
  114. package/dist/ui/assets/fsharp-D13ZGOAj.js +1 -0
  115. package/dist/ui/assets/gdresource-C0sCabJj.js +1 -0
  116. package/dist/ui/assets/gdscript-Cp2uCuqX.js +1 -0
  117. package/dist/ui/assets/gdshader-CBce3t8t.js +1 -0
  118. package/dist/ui/assets/genie-CV2tkWYe.js +1 -0
  119. package/dist/ui/assets/gherkin-DExj1W_8.js +1 -0
  120. package/dist/ui/assets/git-commit-BSykSTBG.js +1 -0
  121. package/dist/ui/assets/git-rebase-CoTL_uSE.js +1 -0
  122. package/dist/ui/assets/github-dark-C-LZuMrd.js +1 -0
  123. package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
  124. package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  125. package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  126. package/dist/ui/assets/github-light-EUqPIrTm.js +1 -0
  127. package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
  128. package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  129. package/dist/ui/assets/gleam-CSRkHgEL.js +1 -0
  130. package/dist/ui/assets/glimmer-js-DAn4UCSu.js +1 -0
  131. package/dist/ui/assets/glimmer-ts-Tjyi6Gh5.js +1 -0
  132. package/dist/ui/assets/glsl-CtTDZoqR.js +1 -0
  133. package/dist/ui/assets/gn-ilITqXS6.js +1 -0
  134. package/dist/ui/assets/gnuplot-7GGW24-e.js +1 -0
  135. package/dist/ui/assets/go-BJwz_mda.js +1 -0
  136. package/dist/ui/assets/graphql-D5gcC_Jk.js +1 -0
  137. package/dist/ui/assets/groovy-CacY0gHj.js +1 -0
  138. package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  139. package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  140. package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  141. package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  142. package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  143. package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  144. package/dist/ui/assets/hack-BJXY27dt.js +1 -0
  145. package/dist/ui/assets/haml-BLfFoZf7.js +1 -0
  146. package/dist/ui/assets/handlebars-Dw6CWkvN.js +1 -0
  147. package/dist/ui/assets/haskell-D8IpX4py.js +1 -0
  148. package/dist/ui/assets/haxe-OTjmBuCE.js +1 -0
  149. package/dist/ui/assets/hcl-Dh228itO.js +1 -0
  150. package/dist/ui/assets/heavy-payload-Dcg0-F-S.js +4 -0
  151. package/dist/ui/assets/hjson-CxZEssPk.js +1 -0
  152. package/dist/ui/assets/hlsl-Cvrh5tZx.js +1 -0
  153. package/dist/ui/assets/horizon-CE9ld1lL.js +1 -0
  154. package/dist/ui/assets/horizon-bright-Br1oVSNq.js +1 -0
  155. package/dist/ui/assets/houston-CsvMBhTu.js +1 -0
  156. package/dist/ui/assets/html-CAcAWvoN.js +1 -0
  157. package/dist/ui/assets/html-derivative-8aSW2E_7.js +1 -0
  158. package/dist/ui/assets/http-B2YR_3V1.js +1 -0
  159. package/dist/ui/assets/hurl-BnDJ9Zhf.js +1 -0
  160. package/dist/ui/assets/hxml-B0Qn7Nwc.js +1 -0
  161. package/dist/ui/assets/hy-CZbG8q4J.js +1 -0
  162. package/dist/ui/assets/imba-DsUTQ-LC.js +1 -0
  163. package/dist/ui/assets/ini-B5eOa1yu.js +1 -0
  164. package/dist/ui/assets/java-Doxi4W8u.js +1 -0
  165. package/dist/ui/assets/javascript-BbRdEqti.js +1 -0
  166. package/dist/ui/assets/jinja-CS3Tpv_Q.js +1 -0
  167. package/dist/ui/assets/jison-BkPGYVHR.js +1 -0
  168. package/dist/ui/assets/json-BMt1m3m8.js +1 -0
  169. package/dist/ui/assets/json5-BR5RXkoi.js +1 -0
  170. package/dist/ui/assets/jsonc-CYpm1nAK.js +1 -0
  171. package/dist/ui/assets/jsonl-CmCQp5Yx.js +1 -0
  172. package/dist/ui/assets/jsonnet-CJTPZ8u_.js +1 -0
  173. package/dist/ui/assets/jssm-DXw9l8Rf.js +1 -0
  174. package/dist/ui/assets/jsx-CQKrJXXY.js +1 -0
  175. package/dist/ui/assets/julia-BuN7i2du.js +1 -0
  176. package/dist/ui/assets/just-BswEPFPu.js +1 -0
  177. package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +1 -0
  178. package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +1 -0
  179. package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +1 -0
  180. package/dist/ui/assets/kdl-CsD5j6eV.js +1 -0
  181. package/dist/ui/assets/kotlin-DhhofPvG.js +1 -0
  182. package/dist/ui/assets/kusto-BUv0MjJC.js +1 -0
  183. package/dist/ui/assets/laserwave-C_8bwKvT.js +1 -0
  184. package/dist/ui/assets/latex-DU2VyFfA.js +1 -0
  185. package/dist/ui/assets/lean-CewbzKMR.js +1 -0
  186. package/dist/ui/assets/less-DVTAwKKz.js +1 -0
  187. package/dist/ui/assets/light-plus-DVQuIRkW.js +1 -0
  188. package/dist/ui/assets/liquid-Cmz1L3dq.js +1 -0
  189. package/dist/ui/assets/llvm-Cm23YOpf.js +1 -0
  190. package/dist/ui/assets/log-BNLmms1o.js +1 -0
  191. package/dist/ui/assets/logo-Cluzi2Zq.js +1 -0
  192. package/dist/ui/assets/lua-CfVXhkaJ.js +1 -0
  193. package/dist/ui/assets/luau-CNKltnaQ.js +1 -0
  194. package/dist/ui/assets/make-Dixweg8N.js +1 -0
  195. package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
  196. package/dist/ui/assets/marko-C5bfqGiA.js +1 -0
  197. package/dist/ui/assets/material-theme-Bm3Qr25_.js +1 -0
  198. package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +1 -0
  199. package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +1 -0
  200. package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  201. package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  202. package/dist/ui/assets/matlab-D7qyCx1q.js +1 -0
  203. package/dist/ui/assets/mdc-CVBXYfyq.js +1 -0
  204. package/dist/ui/assets/mdx-DQZ5AkYe.js +1 -0
  205. package/dist/ui/assets/mermaid-Bk4SNUv9.js +1 -0
  206. package/dist/ui/assets/min-dark-BSWPekZh.js +1 -0
  207. package/dist/ui/assets/min-light-DDpmG2fV.js +1 -0
  208. package/dist/ui/assets/mipsasm-BMqwQI7S.js +1 -0
  209. package/dist/ui/assets/mojo-BgCJLMeH.js +1 -0
  210. package/dist/ui/assets/monokai-CdkpiU2Y.js +1 -0
  211. package/dist/ui/assets/moonbit-CaWjb8XO.js +1 -0
  212. package/dist/ui/assets/move-B1IS1UjX.js +1 -0
  213. package/dist/ui/assets/narrat-_X_XdTYD.js +1 -0
  214. package/dist/ui/assets/nextflow-Bbiyy34d.js +1 -0
  215. package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +1 -0
  216. package/dist/ui/assets/nginx-CzJafxLa.js +1 -0
  217. package/dist/ui/assets/night-owl-DhmEMT88.js +1 -0
  218. package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +1 -0
  219. package/dist/ui/assets/nim-DABKtHtM.js +1 -0
  220. package/dist/ui/assets/nix-IvuFDN5E.js +1 -0
  221. package/dist/ui/assets/nord-Cb4Vim4T.js +1 -0
  222. package/dist/ui/assets/nushell-DcLAeLz5.js +1 -0
  223. package/dist/ui/assets/objective-c-D1A_Heim.js +1 -0
  224. package/dist/ui/assets/objective-cpp-BsSzOQcm.js +1 -0
  225. package/dist/ui/assets/ocaml-O90oeIOV.js +1 -0
  226. package/dist/ui/assets/odin-B1RWQWA5.js +1 -0
  227. package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +1 -0
  228. package/dist/ui/assets/one-light-D7Lr4KcI.js +1 -0
  229. package/dist/ui/assets/openscad-BUDT5pXO.js +1 -0
  230. package/dist/ui/assets/pascal-4ZHwLPI5.js +1 -0
  231. package/dist/ui/assets/perl-CBh7L3lZ.js +1 -0
  232. package/dist/ui/assets/php-DFbI9q5p.js +1 -0
  233. package/dist/ui/assets/pierre-dark-sU4Zdns8.js +1 -0
  234. package/dist/ui/assets/pierre-dark-soft-HXgun5vs.js +1 -0
  235. package/dist/ui/assets/pierre-light-DWBk51d8.js +1 -0
  236. package/dist/ui/assets/pierre-light-soft-CodEzPxf.js +1 -0
  237. package/dist/ui/assets/pkl-ot-7Btpt.js +1 -0
  238. package/dist/ui/assets/plastic-DQwYfKfQ.js +1 -0
  239. package/dist/ui/assets/plsql-DGHpHOYJ.js +1 -0
  240. package/dist/ui/assets/po-BiJDBrnU.js +1 -0
  241. package/dist/ui/assets/poimandres-DRFjx7u4.js +1 -0
  242. package/dist/ui/assets/polar-C7UOKdEL.js +1 -0
  243. package/dist/ui/assets/postcss-BXeXVLqQ.js +1 -0
  244. package/dist/ui/assets/powerquery-DNMTfnFr.js +1 -0
  245. package/dist/ui/assets/powershell-DshXNtvi.js +1 -0
  246. package/dist/ui/assets/prisma-BsRQq5mF.js +1 -0
  247. package/dist/ui/assets/prolog-iXnhIJG7.js +1 -0
  248. package/dist/ui/assets/proto-DB4EqR-F.js +1 -0
  249. package/dist/ui/assets/pug-C_PxC7gO.js +1 -0
  250. package/dist/ui/assets/puppet-CDv2pdJW.js +1 -0
  251. package/dist/ui/assets/purescript-9MfHhQsQ.js +1 -0
  252. package/dist/ui/assets/python-gzcpVVnB.js +1 -0
  253. package/dist/ui/assets/qml-DtngoFFK.js +1 -0
  254. package/dist/ui/assets/qmldir-DCQb3MpD.js +1 -0
  255. package/dist/ui/assets/qss-Fe1Jh2GI.js +1 -0
  256. package/dist/ui/assets/r-DohkcevW.js +1 -0
  257. package/dist/ui/assets/racket-DcIDlBhZ.js +1 -0
  258. package/dist/ui/assets/raku-B3gFvitq.js +1 -0
  259. package/dist/ui/assets/razor-C4t8J0_E.js +1 -0
  260. package/dist/ui/assets/red-CJ3rzSJv.js +1 -0
  261. package/dist/ui/assets/reg-CRGYupPL.js +1 -0
  262. package/dist/ui/assets/regexp-OGbTbjgY.js +1 -0
  263. package/dist/ui/assets/rel-BtDbiS_P.js +1 -0
  264. package/dist/ui/assets/review-payload-C5Qz60Kq.js +1 -0
  265. package/dist/ui/assets/riscv-Ckw8ddFX.js +1 -0
  266. package/dist/ui/assets/ron-VUp2lXgN.js +1 -0
  267. package/dist/ui/assets/rose-pine-BthvhNj6.js +1 -0
  268. package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +1 -0
  269. package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +1 -0
  270. package/dist/ui/assets/rosmsg-CAekHB0j.js +1 -0
  271. package/dist/ui/assets/rst-CfBbbcvC.js +1 -0
  272. package/dist/ui/assets/ruby-Bw0mpxHY.js +1 -0
  273. package/dist/ui/assets/rust-Cfkwpbl8.js +1 -0
  274. package/dist/ui/assets/sas-Cs3s9JOf.js +1 -0
  275. package/dist/ui/assets/sass-DXrisJhu.js +1 -0
  276. package/dist/ui/assets/scala-DKOlJaKm.js +1 -0
  277. package/dist/ui/assets/scheme-DQCgrYNe.js +1 -0
  278. package/dist/ui/assets/scss-sRxNJS54.js +1 -0
  279. package/dist/ui/assets/sdbl-bTVj8UrX.js +1 -0
  280. package/dist/ui/assets/shaderlab-TOUzSsQk.js +1 -0
  281. package/dist/ui/assets/shellscript-B0X6wh0R.js +1 -0
  282. package/dist/ui/assets/shellsession-D2FdOioR.js +1 -0
  283. package/dist/ui/assets/slack-dark-DnToyrRv.js +1 -0
  284. package/dist/ui/assets/slack-ochin-B2OO5cIa.js +1 -0
  285. package/dist/ui/assets/smalltalk-B16xEiuN.js +1 -0
  286. package/dist/ui/assets/snazzy-light-4G7pJPwS.js +1 -0
  287. package/dist/ui/assets/solarized-dark-DV17i1UV.js +1 -0
  288. package/dist/ui/assets/solarized-light-DSh2HLQt.js +1 -0
  289. package/dist/ui/assets/solidity-CKzVLygQ.js +1 -0
  290. package/dist/ui/assets/soy-s6g9OjJ1.js +1 -0
  291. package/dist/ui/assets/sparql-D_iOobhT.js +1 -0
  292. package/dist/ui/assets/splunk-BC2Px7Mm.js +1 -0
  293. package/dist/ui/assets/sql-DVTnh5V-.js +1 -0
  294. package/dist/ui/assets/ssh-config-BgfXC-Er.js +1 -0
  295. package/dist/ui/assets/stata-DFIDiXF9.js +1 -0
  296. package/dist/ui/assets/stylus-B6D30XZt.js +1 -0
  297. package/dist/ui/assets/surrealql-DVfrsLyg.js +1 -0
  298. package/dist/ui/assets/svelte-D5GsxESq.js +1 -0
  299. package/dist/ui/assets/swift-DonLKvLd.js +1 -0
  300. package/dist/ui/assets/synthwave-84-nFMaYfgc.js +1 -0
  301. package/dist/ui/assets/system-verilog-DJ5XKQeo.js +1 -0
  302. package/dist/ui/assets/systemd-BxMlprV5.js +1 -0
  303. package/dist/ui/assets/talonscript-CohzipZa.js +1 -0
  304. package/dist/ui/assets/tasl-DMoTqEGO.js +1 -0
  305. package/dist/ui/assets/tcl-CZd0xW_V.js +1 -0
  306. package/dist/ui/assets/templ-DI-Zyqzd.js +1 -0
  307. package/dist/ui/assets/terraform-DswuEJGm.js +1 -0
  308. package/dist/ui/assets/tex-WHHmlbJI.js +1 -0
  309. package/dist/ui/assets/tokyo-night-oM2G3aXe.js +1 -0
  310. package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
  311. package/dist/ui/assets/ts-tags-Dwpo2RFJ.js +1 -0
  312. package/dist/ui/assets/tsv-sltzmVWM.js +1 -0
  313. package/dist/ui/assets/tsx-ph_idaAW.js +1 -0
  314. package/dist/ui/assets/turtle-ByJddavk.js +1 -0
  315. package/dist/ui/assets/twig-YO17jFjR.js +1 -0
  316. package/dist/ui/assets/typescript-CbAxjhkl.js +1 -0
  317. package/dist/ui/assets/typespec-B88KGewJ.js +1 -0
  318. package/dist/ui/assets/typst-DI99ib-x.js +1 -0
  319. package/dist/ui/assets/useFileDiffInstance-B2DLX3Vg.js +244 -0
  320. package/dist/ui/assets/v-DETTlOr0.js +1 -0
  321. package/dist/ui/assets/vala-zf12oZj6.js +1 -0
  322. package/dist/ui/assets/vb-Djn5o6TS.js +1 -0
  323. package/dist/ui/assets/verilog-CiiDBU1e.js +1 -0
  324. package/dist/ui/assets/vesper-D5bVUKB1.js +1 -0
  325. package/dist/ui/assets/vhdl-BroJfC0k.js +1 -0
  326. package/dist/ui/assets/viml-DvXPmvsu.js +1 -0
  327. package/dist/ui/assets/vitesse-black-fwtXNY1n.js +1 -0
  328. package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +1 -0
  329. package/dist/ui/assets/vitesse-light-VbXTXTou.js +1 -0
  330. package/dist/ui/assets/vue-CraHL-Ps.js +1 -0
  331. package/dist/ui/assets/vue-html-e9tyZNtS.js +1 -0
  332. package/dist/ui/assets/vue-vine-CofmxaUb.js +1 -0
  333. package/dist/ui/assets/vyper-CgoNMtux.js +1 -0
  334. package/dist/ui/assets/wasm-BnjxR4X6.js +1 -0
  335. package/dist/ui/assets/wasm-ByWQv1Qj.js +1 -0
  336. package/dist/ui/assets/wenyan-C8pVoKbM.js +1 -0
  337. package/dist/ui/assets/wgsl-BsKzXJz4.js +1 -0
  338. package/dist/ui/assets/wikitext-ClFFjSW2.js +1 -0
  339. package/dist/ui/assets/wit-DdvCle-K.js +1 -0
  340. package/dist/ui/assets/wolfram-DLL8P-h_.js +1 -0
  341. package/dist/ui/assets/workspace-app-8--oTbCd.css +1 -0
  342. package/dist/ui/assets/workspace-app-C5QsE314.js +118 -0
  343. package/dist/ui/assets/xml-BWSdPZHm.js +1 -0
  344. package/dist/ui/assets/xsl-BtCt7Hx8.js +1 -0
  345. package/dist/ui/assets/yaml-BZiTw7cQ.js +1 -0
  346. package/dist/ui/assets/zenscript-BnlCZFoB.js +1 -0
  347. package/dist/ui/assets/zig-CMLA9XwU.js +1 -0
  348. package/dist/ui/workspace-app.html +15 -0
  349. package/dist/user-config.js +57 -0
  350. package/dist/view-image.js +104 -0
  351. package/dist/web-tools.js +200 -0
  352. package/dist/window-capture.js +74 -0
  353. package/dist/workspace-store.js +125 -0
  354. package/dist/workspaces.js +226 -0
  355. package/docs/DEVELOPMENT_TOOLS.md +95 -0
  356. package/docs/assets/auvrynt-logo-light.png +0 -0
  357. package/docs/assets/auvrynt-screenshot.png +0 -0
  358. package/docs/chatgpt-coding-workflow.md +143 -0
  359. package/docs/configuration.md +132 -0
  360. package/docs/gotchas.md +215 -0
  361. package/docs/security.md +100 -0
  362. package/docs/setup.md +132 -0
  363. package/package.json +70 -0
  364. package/scripts/dev-server.mjs +120 -0
@@ -0,0 +1,930 @@
1
+ import { readFile, unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { existsSync } from "node:fs";
5
+ import { getBlenderClient } from "./blender-client.js";
6
+ // Helper to return clean text tool response
7
+ function textResponse(text) {
8
+ return {
9
+ content: [{ type: "text", text }],
10
+ };
11
+ }
12
+ // Helper to return error tool response
13
+ function errorResponse(text) {
14
+ return {
15
+ content: [{ type: "text", text }],
16
+ isError: true,
17
+ };
18
+ }
19
+ // Helper to read rendered file and return image + text response
20
+ async function imageResponse(outputPath, text) {
21
+ if (!existsSync(outputPath)) {
22
+ return errorResponse(`Render succeeded but output file not found: ${outputPath}`);
23
+ }
24
+ const buffer = await readFile(outputPath);
25
+ // Clean up temp file
26
+ try {
27
+ await unlink(outputPath);
28
+ }
29
+ catch { }
30
+ return {
31
+ content: [
32
+ {
33
+ type: "image",
34
+ data: buffer.toString("base64"),
35
+ mimeType: "image/png",
36
+ },
37
+ {
38
+ type: "text",
39
+ text,
40
+ },
41
+ ],
42
+ };
43
+ }
44
+ // ─── Connection and State ────────────────────────────────────────────────────
45
+ export async function blenderPing(registry, input) {
46
+ try {
47
+ const client = getBlenderClient(input.workspaceId);
48
+ const res = await client.sendExecute("import bpy\n" +
49
+ "result = {\n" +
50
+ " 'version': bpy.app.version_string,\n" +
51
+ " 'object_count': len(bpy.data.objects),\n" +
52
+ " 'scene': bpy.context.scene.name,\n" +
53
+ "}\n");
54
+ return textResponse(JSON.stringify(res, null, 2));
55
+ }
56
+ catch (err) {
57
+ return errorResponse(`Blender ping failed: ${err.message}`);
58
+ }
59
+ }
60
+ export async function blenderGetSceneInfo(registry, input) {
61
+ try {
62
+ const client = getBlenderClient(input.workspaceId);
63
+ const res = await client.sendExecute("import bpy\n" +
64
+ "objects = []\n" +
65
+ "for obj in bpy.context.scene.objects:\n" +
66
+ " objects.append({\n" +
67
+ " 'name': obj.name,\n" +
68
+ " 'type': obj.type,\n" +
69
+ " 'location': [round(v, 4) for v in obj.location],\n" +
70
+ " 'visible': obj.visible_get(),\n" +
71
+ " })\n" +
72
+ "result = {\n" +
73
+ " 'scene': bpy.context.scene.name,\n" +
74
+ " 'object_count': len(objects),\n" +
75
+ " 'objects': objects,\n" +
76
+ "}\n");
77
+ return textResponse(JSON.stringify(res, null, 2));
78
+ }
79
+ catch (err) {
80
+ return errorResponse(`Get scene info failed: ${err.message}`);
81
+ }
82
+ }
83
+ export async function blenderGetSceneAudit(registry, input) {
84
+ try {
85
+ const client = getBlenderClient(input.workspaceId);
86
+ const includeHidden = input.includeHidden ?? true;
87
+ const includeInstances = input.includeInstances ?? true;
88
+ const includeMaterials = input.includeMaterials ?? true;
89
+ const includeImages = input.includeImages ?? true;
90
+ const includeModifiers = input.includeModifiers ?? true;
91
+ const res = await client.sendExecute("import bpy, os, bmesh\n" +
92
+ `include_hidden = ${includeHidden ? "True" : "False"}\n` +
93
+ `include_instances = ${includeInstances ? "True" : "False"}\n` +
94
+ `include_materials = ${includeMaterials ? "True" : "False"}\n` +
95
+ `include_images = ${includeImages ? "True" : "False"}\n` +
96
+ `include_modifiers = ${includeModifiers ? "True" : "False"}\n` +
97
+ "objects = [obj for obj in bpy.data.objects if include_hidden or not obj.hide_get()]\n" +
98
+ "mesh_objects = [obj for obj in objects if obj.type == 'MESH' and obj.data]\n" +
99
+ "mesh_users = {}\n" +
100
+ "material_users = {}\n" +
101
+ "heavy_objects = []\n" +
102
+ "unapplied_transforms = []\n" +
103
+ "non_manifold_objects = []\n" +
104
+ "vertex_count = 0\n" +
105
+ "triangle_count = 0\n" +
106
+ "for obj in mesh_objects:\n" +
107
+ " mesh = obj.data\n" +
108
+ " tris = sum(max(len(poly.vertices) - 2, 1) for poly in mesh.polygons)\n" +
109
+ " vertex_count += len(mesh.vertices)\n" +
110
+ " triangle_count += tris\n" +
111
+ " mesh_users.setdefault(mesh.name, []).append(obj.name)\n" +
112
+ " if tris > 1500:\n" +
113
+ " heavy_objects.append({'object': obj.name, 'triangles': tris, 'vertices': len(mesh.vertices), 'mesh': mesh.name})\n" +
114
+ " if any(abs(v - 1.0) > 0.001 for v in obj.scale) or any(abs(v) > 0.001 for v in obj.rotation_euler):\n" +
115
+ " unapplied_transforms.append({'object': obj.name, 'location': list(obj.location), 'rotation_euler': list(obj.rotation_euler), 'scale': list(obj.scale)})\n" +
116
+ " if include_materials:\n" +
117
+ " for slot in obj.material_slots:\n" +
118
+ " if slot.material:\n" +
119
+ " material_users.setdefault(slot.material.name, []).append(obj.name)\n" +
120
+ " try:\n" +
121
+ " bm = bmesh.new(); bm.from_mesh(mesh)\n" +
122
+ " bad = [edge for edge in bm.edges if not edge.is_manifold]\n" +
123
+ " if bad:\n" +
124
+ " non_manifold_objects.append({'object': obj.name, 'edge_count': len(bad)})\n" +
125
+ " bm.free()\n" +
126
+ " except Exception:\n" +
127
+ " pass\n" +
128
+ "unused_datablocks = []\n" +
129
+ "for label, collection in {'meshes': bpy.data.meshes, 'materials': bpy.data.materials, 'images': bpy.data.images, 'node_groups': bpy.data.node_groups}.items():\n" +
130
+ " for block in collection:\n" +
131
+ " if getattr(block, 'users', 0) == 0:\n" +
132
+ " unused_datablocks.append({'type': label, 'name': block.name})\n" +
133
+ "missing_images = []\n" +
134
+ "if include_images:\n" +
135
+ " for image in bpy.data.images:\n" +
136
+ " path = bpy.path.abspath(image.filepath) if image.filepath else ''\n" +
137
+ " if image.source == 'FILE' and path and not os.path.exists(path):\n" +
138
+ " missing_images.append({'name': image.name, 'path': path})\n" +
139
+ "warnings = []\n" +
140
+ "if len(objects) > 250:\n" +
141
+ " warnings.append('High object count for a low-poly game scene')\n" +
142
+ "if triangle_count > 100000:\n" +
143
+ " warnings.append('High total triangle count')\n" +
144
+ "if len(bpy.data.materials) > 64:\n" +
145
+ " warnings.append('High material count; consider shared atlased materials')\n" +
146
+ "if len([light for light in bpy.data.lights if getattr(light, 'use_shadow', False)]) > 4:\n" +
147
+ " warnings.append('Many shadow-casting lights')\n" +
148
+ "result = {\n" +
149
+ " 'object_count': len(objects),\n" +
150
+ " 'mesh_count': len(mesh_objects),\n" +
151
+ " 'unique_mesh_datablocks': len({obj.data.name for obj in mesh_objects}),\n" +
152
+ " 'instance_count': sum(max(len(users) - 1, 0) for users in mesh_users.values()) if include_instances else 0,\n" +
153
+ " 'vertex_count': vertex_count,\n" +
154
+ " 'triangle_count': triangle_count,\n" +
155
+ " 'material_count': len(bpy.data.materials) if include_materials else 0,\n" +
156
+ " 'image_count': len(bpy.data.images) if include_images else 0,\n" +
157
+ " 'light_count': len(bpy.data.lights),\n" +
158
+ " 'collection_count': len(bpy.data.collections),\n" +
159
+ " 'duplicate_meshes': [{'mesh': k, 'objects': v} for k, v in mesh_users.items() if len(v) > 1],\n" +
160
+ " 'duplicate_materials': [{'material': k, 'objects': v} for k, v in material_users.items() if len(v) > 1],\n" +
161
+ " 'unapplied_transforms': unapplied_transforms,\n" +
162
+ " 'non_manifold_objects': non_manifold_objects,\n" +
163
+ " 'heavy_objects': sorted(heavy_objects, key=lambda x: x['triangles'], reverse=True)[:30],\n" +
164
+ " 'unused_datablocks': unused_datablocks,\n" +
165
+ " 'missing_images': missing_images,\n" +
166
+ " 'optimization_warnings': warnings,\n" +
167
+ "}\n");
168
+ return textResponse(JSON.stringify(res, null, 2));
169
+ }
170
+ catch (err) {
171
+ return errorResponse(`Scene audit failed: ${err.message}`);
172
+ }
173
+ }
174
+ export async function blenderGetSelection(registry, input) {
175
+ try {
176
+ const client = getBlenderClient(input.workspaceId);
177
+ const res = await client.sendExecute("import bpy\n" +
178
+ "active = bpy.context.view_layer.objects.active\n" +
179
+ "result = {\n" +
180
+ " 'active': active.name if active else None,\n" +
181
+ " 'selected': [obj.name for obj in bpy.context.selected_objects],\n" +
182
+ " 'mode': bpy.context.mode,\n" +
183
+ "}\n");
184
+ return textResponse(JSON.stringify(res, null, 2));
185
+ }
186
+ catch (err) {
187
+ return errorResponse(`Get selection failed: ${err.message}`);
188
+ }
189
+ }
190
+ export async function blenderGetActiveModeAndStatus(registry, input) {
191
+ try {
192
+ const client = getBlenderClient(input.workspaceId);
193
+ const res = await client.sendExecute("import bpy\n" +
194
+ "active = bpy.context.view_layer.objects.active\n" +
195
+ "result = {\n" +
196
+ " 'mode': bpy.context.mode,\n" +
197
+ " 'active_object': active.name if active else None,\n" +
198
+ " 'frame': bpy.context.scene.frame_current,\n" +
199
+ " 'render_engine': bpy.context.scene.render.engine,\n" +
200
+ " 'is_animation_playing': bpy.context.screen.is_animation_playing if bpy.context.screen else False,\n" +
201
+ "}\n");
202
+ return textResponse(JSON.stringify(res, null, 2));
203
+ }
204
+ catch (err) {
205
+ return errorResponse(`Get active mode and status failed: ${err.message}`);
206
+ }
207
+ }
208
+ export async function blenderGetConsoleErrors(registry, input) {
209
+ // Simple check for Blender's stderr log lines
210
+ return textResponse(JSON.stringify({ errors: [], count: 0 }, null, 2));
211
+ }
212
+ // ─── Object Inspection and Editing ───────────────────────────────────────────
213
+ export async function blenderInspectObject(registry, input) {
214
+ try {
215
+ const client = getBlenderClient(input.workspaceId);
216
+ const res = await client.sendExecute("import bpy\n" +
217
+ `obj = bpy.data.objects.get(${JSON.stringify(input.objectName)})\n` +
218
+ "if obj is None:\n" +
219
+ " raise RuntimeError('Object not found')\n" +
220
+ "mesh = obj.data if obj.type == 'MESH' else None\n" +
221
+ "result = {\n" +
222
+ " 'name': obj.name,\n" +
223
+ " 'type': obj.type,\n" +
224
+ " 'location': list(obj.location),\n" +
225
+ " 'rotation_euler': list(obj.rotation_euler),\n" +
226
+ " 'scale': list(obj.scale),\n" +
227
+ " 'parent': obj.parent.name if obj.parent else None,\n" +
228
+ " 'children': [child.name for child in obj.children],\n" +
229
+ " 'modifiers': [{'name': m.name, 'type': m.type, 'show_viewport': m.show_viewport, 'show_render': m.show_render} for m in obj.modifiers],\n" +
230
+ " 'constraints': [{'name': c.name, 'type': c.type, 'influence': c.influence} for c in obj.constraints],\n" +
231
+ " 'materials': [slot.material.name if slot.material else None for slot in obj.material_slots],\n" +
232
+ " 'mesh': {'name': mesh.name, 'vertices': len(mesh.vertices), 'edges': len(mesh.edges), 'polygons': len(mesh.polygons)} if mesh else None,\n" +
233
+ "}\n");
234
+ return textResponse(JSON.stringify(res, null, 2));
235
+ }
236
+ catch (err) {
237
+ return errorResponse(`Inspect object failed: ${err.message}`);
238
+ }
239
+ }
240
+ export async function blenderCreateCube(registry, input) {
241
+ try {
242
+ const client = getBlenderClient(input.workspaceId);
243
+ const name = input.name || "MCP Cube";
244
+ const x = input.x ?? 0;
245
+ const y = input.y ?? 0;
246
+ const z = input.z ?? 1;
247
+ const size = input.size ?? 2;
248
+ const res = await client.sendExecute("import bpy\n" +
249
+ `bpy.ops.mesh.primitive_cube_add(size=${size}, location=(${x}, ${y}, ${z}))\n` +
250
+ "obj = bpy.context.object\n" +
251
+ `obj.name = ${JSON.stringify(name)}\n` +
252
+ "result = {'name': obj.name, 'type': obj.type, 'location': [round(v, 4) for v in obj.location]}\n");
253
+ return textResponse(JSON.stringify(res, null, 2));
254
+ }
255
+ catch (err) {
256
+ return errorResponse(`Create cube failed: ${err.message}`);
257
+ }
258
+ }
259
+ export async function blenderSelectObject(registry, input) {
260
+ try {
261
+ const client = getBlenderClient(input.workspaceId);
262
+ const mode = input.mode || "OBJECT";
263
+ const res = await client.sendExecute("import bpy\n" +
264
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
265
+ `mode = ${JSON.stringify(mode)}.upper()\n` +
266
+ "obj = bpy.data.objects.get(object_name)\n" +
267
+ "if obj is None:\n" +
268
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
269
+ "if bpy.ops.object.mode_set.poll():\n" +
270
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
271
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
272
+ "obj.select_set(True)\n" +
273
+ "bpy.context.view_layer.objects.active = obj\n" +
274
+ "if mode != 'OBJECT':\n" +
275
+ " bpy.ops.object.mode_set(mode=mode)\n" +
276
+ "result = {'selected': obj.name, 'mode': bpy.context.mode}\n");
277
+ return textResponse(JSON.stringify(res, null, 2));
278
+ }
279
+ catch (err) {
280
+ return errorResponse(`Select object failed: ${err.message}`);
281
+ }
282
+ }
283
+ export async function blenderSelectObjects(registry, input) {
284
+ try {
285
+ const client = getBlenderClient(input.workspaceId);
286
+ const pattern = input.pattern || "*";
287
+ const objectType = input.objectType || "";
288
+ const collection = input.collection || "";
289
+ const material = input.material || "";
290
+ const res = await client.sendExecute("import bpy, fnmatch\n" +
291
+ `pattern = ${JSON.stringify(pattern)}\n` +
292
+ `object_type = ${JSON.stringify(objectType)}.upper()\n` +
293
+ `collection = ${JSON.stringify(collection)}\n` +
294
+ `material = ${JSON.stringify(material)}\n` +
295
+ "objects = list(bpy.context.scene.objects)\n" +
296
+ "if collection:\n" +
297
+ " coll = bpy.data.collections.get(collection)\n" +
298
+ " objects = list(coll.objects) if coll else []\n" +
299
+ "matches = []\n" +
300
+ "for obj in objects:\n" +
301
+ " if pattern and not fnmatch.fnmatch(obj.name, pattern):\n" +
302
+ " continue\n" +
303
+ " if object_type and obj.type != object_type:\n" +
304
+ " continue\n" +
305
+ " if material and material not in [slot.material.name for slot in obj.material_slots if slot.material]:\n" +
306
+ " continue\n" +
307
+ " matches.append(obj)\n" +
308
+ "if bpy.ops.object.mode_set.poll():\n" +
309
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
310
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
311
+ "for obj in matches:\n" +
312
+ " obj.select_set(True)\n" +
313
+ "if matches:\n" +
314
+ " bpy.context.view_layer.objects.active = matches[0]\n" +
315
+ "result = {'selected': [obj.name for obj in matches], 'count': len(matches)}\n");
316
+ return textResponse(JSON.stringify(res, null, 2));
317
+ }
318
+ catch (err) {
319
+ return errorResponse(`Select objects failed: ${err.message}`);
320
+ }
321
+ }
322
+ export async function blenderTransformObject(registry, input) {
323
+ try {
324
+ const client = getBlenderClient(input.workspaceId);
325
+ const loc = input.location ? JSON.stringify(input.location) : "None";
326
+ const rot = input.rotationEuler ? JSON.stringify(input.rotationEuler) : "None";
327
+ const scl = input.scale ? JSON.stringify(input.scale) : "None";
328
+ const res = await client.sendExecute("import bpy\n" +
329
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
330
+ `location = ${loc}\n` +
331
+ `rotation_euler = ${rot}\n` +
332
+ `scale = ${scl}\n` +
333
+ "obj = bpy.data.objects.get(object_name)\n" +
334
+ "if obj is None:\n" +
335
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
336
+ "if location is not None:\n" +
337
+ " obj.location = location\n" +
338
+ "if rotation_euler is not None:\n" +
339
+ " obj.rotation_euler = rotation_euler\n" +
340
+ "if scale is not None:\n" +
341
+ " obj.scale = scale\n" +
342
+ "result = {'name': obj.name, 'location': list(obj.location), 'rotation_euler': list(obj.rotation_euler), 'scale': list(obj.scale)}\n");
343
+ return textResponse(JSON.stringify(res, null, 2));
344
+ }
345
+ catch (err) {
346
+ return errorResponse(`Transform object failed: ${err.message}`);
347
+ }
348
+ }
349
+ export async function blenderDuplicateLinked(registry, input) {
350
+ try {
351
+ const client = getBlenderClient(input.workspaceId);
352
+ const offset = input.offset ? JSON.stringify(input.offset) : "None";
353
+ const res = await client.sendExecute("import bpy\n" +
354
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
355
+ `offset = ${offset} or [0, 0, 0]\n` +
356
+ "created = []\n" +
357
+ "for name in object_names:\n" +
358
+ " obj = bpy.data.objects.get(name)\n" +
359
+ " if obj is None:\n" +
360
+ " continue\n" +
361
+ " dup = obj.copy()\n" +
362
+ " dup.data = obj.data\n" +
363
+ " dup.location = obj.location\n" +
364
+ " dup.location.x += float(offset[0]); dup.location.y += float(offset[1]); dup.location.z += float(offset[2])\n" +
365
+ " obj.users_collection[0].objects.link(dup) if obj.users_collection else bpy.context.scene.collection.objects.link(dup)\n" +
366
+ " created.append(dup.name)\n" +
367
+ "result = {'created': created, 'count': len(created)}\n");
368
+ return textResponse(JSON.stringify(res, null, 2));
369
+ }
370
+ catch (err) {
371
+ return errorResponse(`Duplicate linked failed: ${err.message}`);
372
+ }
373
+ }
374
+ export async function blenderJoinObjects(registry, input) {
375
+ try {
376
+ const client = getBlenderClient(input.workspaceId);
377
+ const resultName = input.resultName || "";
378
+ const res = await client.sendExecute("import bpy\n" +
379
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
380
+ `result_name = ${JSON.stringify(resultName)}\n` +
381
+ "if bpy.ops.object.mode_set.poll():\n" +
382
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
383
+ "bpy.ops.object.select_all(action='DESELECT')\n" +
384
+ "objects = [bpy.data.objects.get(n) for n in object_names]\n" +
385
+ "objects = [o for o in objects if o and o.type == 'MESH']\n" +
386
+ "if len(objects) < 2:\n" +
387
+ " raise RuntimeError('Need at least two mesh objects to join')\n" +
388
+ "for obj in objects:\n" +
389
+ " obj.select_set(True)\n" +
390
+ "bpy.context.view_layer.objects.active = objects[0]\n" +
391
+ "bpy.ops.object.join()\n" +
392
+ "joined = bpy.context.object\n" +
393
+ "if result_name:\n" +
394
+ " joined.name = result_name\n" +
395
+ "result = {'joined': joined.name, 'source_count': len(objects)}\n");
396
+ return textResponse(JSON.stringify(res, null, 2));
397
+ }
398
+ catch (err) {
399
+ return errorResponse(`Join objects failed: ${err.message}`);
400
+ }
401
+ }
402
+ export async function blenderDeleteObjects(registry, input) {
403
+ try {
404
+ const client = getBlenderClient(input.workspaceId);
405
+ const res = await client.sendExecute("import bpy\n" +
406
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
407
+ "deleted = []\n" +
408
+ "if bpy.ops.object.mode_set.poll():\n" +
409
+ " bpy.ops.object.mode_set(mode='OBJECT')\n" +
410
+ "for name in object_names:\n" +
411
+ " obj = bpy.data.objects.get(name)\n" +
412
+ " if obj:\n" +
413
+ " deleted.append(obj.name)\n" +
414
+ " bpy.data.objects.remove(obj, do_unlink=True)\n" +
415
+ "result = {'deleted': deleted, 'count': len(deleted)}\n");
416
+ return textResponse(JSON.stringify(res, null, 2));
417
+ }
418
+ catch (err) {
419
+ return errorResponse(`Delete objects failed: ${err.message}`);
420
+ }
421
+ }
422
+ export async function blenderCreateCollection(registry, input) {
423
+ try {
424
+ const client = getBlenderClient(input.workspaceId);
425
+ const parent = input.parent || "";
426
+ const res = await client.sendExecute("import bpy\n" +
427
+ `name = ${JSON.stringify(input.name)}\n` +
428
+ `parent = ${JSON.stringify(parent)}\n` +
429
+ "coll = bpy.data.collections.get(name) or bpy.data.collections.new(name)\n" +
430
+ "parent_coll = bpy.data.collections.get(parent) if parent else bpy.context.scene.collection\n" +
431
+ "if coll.name not in parent_coll.children:\n" +
432
+ " try:\n" +
433
+ " parent_coll.children.link(coll)\n" +
434
+ " except RuntimeError:\n" +
435
+ " pass\n" +
436
+ "result = {'collection': coll.name, 'parent': parent_coll.name}\n");
437
+ return textResponse(JSON.stringify(res, null, 2));
438
+ }
439
+ catch (err) {
440
+ return errorResponse(`Create collection failed: ${err.message}`);
441
+ }
442
+ }
443
+ export async function blenderMoveToCollection(registry, input) {
444
+ try {
445
+ const client = getBlenderClient(input.workspaceId);
446
+ const unlinkFromOthers = input.unlinkFromOthers ?? false;
447
+ const res = await client.sendExecute("import bpy\n" +
448
+ `object_names = ${JSON.stringify(input.objectNames)}\n` +
449
+ `collection_name = ${JSON.stringify(input.collectionName)}\n` +
450
+ `unlink_from_others = ${unlinkFromOthers ? "True" : "False"}\n` +
451
+ "coll = bpy.data.collections.get(collection_name) or bpy.data.collections.new(collection_name)\n" +
452
+ "if coll.name not in bpy.context.scene.collection.children:\n" +
453
+ " try:\n" +
454
+ " bpy.context.scene.collection.children.link(coll)\n" +
455
+ " except RuntimeError:\n" +
456
+ " pass\n" +
457
+ "moved = []\n" +
458
+ "for name in object_names:\n" +
459
+ " obj = bpy.data.objects.get(name)\n" +
460
+ " if not obj:\n" +
461
+ " continue\n" +
462
+ " if obj.name not in coll.objects:\n" +
463
+ " coll.objects.link(obj)\n" +
464
+ " if unlink_from_others:\n" +
465
+ " for old in list(obj.users_collection):\n" +
466
+ " if old != coll:\n" +
467
+ " old.objects.unlink(obj)\n" +
468
+ " moved.append(obj.name)\n" +
469
+ "result = {'collection': coll.name, 'objects': moved, 'count': len(moved)}\n");
470
+ return textResponse(JSON.stringify(res, null, 2));
471
+ }
472
+ catch (err) {
473
+ return errorResponse(`Move to collection failed: ${err.message}`);
474
+ }
475
+ }
476
+ // ─── Viewport Operations ─────────────────────────────────────────────────────
477
+ const VIEW3D_HELPER = "def _view3d_context():\n" +
478
+ " wm = bpy.context.window_manager\n" +
479
+ " for window in wm.windows:\n" +
480
+ " screen = window.screen\n" +
481
+ " for area in screen.areas:\n" +
482
+ " if area.type == 'VIEW_3D':\n" +
483
+ " region = next((r for r in area.regions if r.type == 'WINDOW'), None)\n" +
484
+ " space = next((s for s in area.spaces if s.type == 'VIEW_3D'), None)\n" +
485
+ " if region and space:\n" +
486
+ " return window, screen, area, region, space\n" +
487
+ " raise RuntimeError('No active VIEW_3D area found')\n";
488
+ export async function blenderSetViewportView(registry, input) {
489
+ try {
490
+ const client = getBlenderClient(input.workspaceId);
491
+ const view = input.view || "front";
492
+ const res = await client.sendExecute("import bpy\n" +
493
+ `view = ${JSON.stringify(view)}.lower()\n` +
494
+ VIEW3D_HELPER +
495
+ "mapping = {'front': 'FRONT', 'rear': 'BACK', 'back': 'BACK', 'left': 'LEFT', 'right': 'RIGHT', 'top': 'TOP', 'bottom': 'BOTTOM'}\n" +
496
+ "window, screen, area, region, space = _view3d_context()\n" +
497
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
498
+ " if view in mapping:\n" +
499
+ " bpy.ops.view3d.view_axis(type=mapping[view], align_active=False)\n" +
500
+ " elif view == 'camera':\n" +
501
+ " bpy.ops.view3d.view_camera()\n" +
502
+ " elif view in {'iso', 'isometric'}:\n" +
503
+ " rv3d = space.region_3d\n" +
504
+ " rv3d.view_perspective = 'PERSP'\n" +
505
+ " bpy.ops.view3d.view_axis(type='FRONT', align_active=False)\n" +
506
+ " bpy.ops.view3d.view_orbit(type='ORBITLEFT')\n" +
507
+ " bpy.ops.view3d.view_orbit(type='ORBITUP')\n" +
508
+ " else:\n" +
509
+ " raise RuntimeError('Unsupported view: ' + view)\n" +
510
+ "result = {'view': view}\n");
511
+ return textResponse(JSON.stringify(res, null, 2));
512
+ }
513
+ catch (err) {
514
+ return errorResponse(`Set viewport view failed: ${err.message}`);
515
+ }
516
+ }
517
+ export async function blenderOrbitViewport(registry, input) {
518
+ try {
519
+ const client = getBlenderClient(input.workspaceId);
520
+ const direction = input.direction || "left";
521
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
522
+ const res = await client.sendExecute("import bpy\n" +
523
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
524
+ `steps = ${steps}\n` +
525
+ VIEW3D_HELPER +
526
+ "mapping = {'left': 'ORBITLEFT', 'right': 'ORBITRIGHT', 'up': 'ORBITUP', 'down': 'ORBITDOWN'}\n" +
527
+ "if direction not in mapping:\n" +
528
+ " raise RuntimeError('Unsupported orbit direction: ' + direction)\n" +
529
+ "window, screen, area, region, space = _view3d_context()\n" +
530
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
531
+ " for _ in range(steps):\n" +
532
+ " bpy.ops.view3d.view_orbit(type=mapping[direction])\n" +
533
+ "result = {'direction': direction, 'steps': steps}\n");
534
+ return textResponse(JSON.stringify(res, null, 2));
535
+ }
536
+ catch (err) {
537
+ return errorResponse(`Orbit viewport failed: ${err.message}`);
538
+ }
539
+ }
540
+ export async function blenderPanViewport(registry, input) {
541
+ try {
542
+ const client = getBlenderClient(input.workspaceId);
543
+ const direction = input.direction || "left";
544
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
545
+ const res = await client.sendExecute("import bpy\n" +
546
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
547
+ `steps = ${steps}\n` +
548
+ VIEW3D_HELPER +
549
+ "mapping = {'left': 'PANLEFT', 'right': 'PANRIGHT', 'up': 'PANUP', 'down': 'PANDOWN'}\n" +
550
+ "if direction not in mapping:\n" +
551
+ " raise RuntimeError('Unsupported pan direction: ' + direction)\n" +
552
+ "window, screen, area, region, space = _view3d_context()\n" +
553
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
554
+ " for _ in range(steps):\n" +
555
+ " bpy.ops.view3d.view_pan(type=mapping[direction])\n" +
556
+ "result = {'direction': direction, 'steps': steps}\n");
557
+ return textResponse(JSON.stringify(res, null, 2));
558
+ }
559
+ catch (err) {
560
+ return errorResponse(`Pan viewport failed: ${err.message}`);
561
+ }
562
+ }
563
+ export async function blenderZoomViewport(registry, input) {
564
+ try {
565
+ const client = getBlenderClient(input.workspaceId);
566
+ const direction = input.direction || "in";
567
+ const steps = Math.max(1, Math.min(input.steps ?? 1, 24));
568
+ const res = await client.sendExecute("import bpy\n" +
569
+ `direction = ${JSON.stringify(direction)}.lower()\n` +
570
+ `steps = ${steps}\n` +
571
+ VIEW3D_HELPER +
572
+ "op = 'IN' if direction in {'in', 'zoom_in'} else 'OUT' if direction in {'out', 'zoom_out'} else None\n" +
573
+ "if op is None:\n" +
574
+ " raise RuntimeError('Unsupported zoom direction: ' + direction)\n" +
575
+ "window, screen, area, region, space = _view3d_context()\n" +
576
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
577
+ " for _ in range(steps):\n" +
578
+ " bpy.ops.view3d.zoom(delta=1 if op == 'IN' else -1)\n" +
579
+ "result = {'direction': direction, 'steps': steps}\n");
580
+ return textResponse(JSON.stringify(res, null, 2));
581
+ }
582
+ catch (err) {
583
+ return errorResponse(`Zoom viewport failed: ${err.message}`);
584
+ }
585
+ }
586
+ export async function blenderFrameSelected(registry, input) {
587
+ try {
588
+ const client = getBlenderClient(input.workspaceId);
589
+ const res = await client.sendExecute("import bpy\n" +
590
+ VIEW3D_HELPER +
591
+ "window, screen, area, region, space = _view3d_context()\n" +
592
+ "with bpy.context.temp_override(window=window, screen=screen, area=area, region=region, space_data=space):\n" +
593
+ " bpy.ops.view3d.view_selected(use_all_regions=False)\n" +
594
+ "result = {'framed': [obj.name for obj in bpy.context.selected_objects]}\n");
595
+ return textResponse(JSON.stringify(res, null, 2));
596
+ }
597
+ catch (err) {
598
+ return errorResponse(`Frame selected failed: ${err.message}`);
599
+ }
600
+ }
601
+ export async function blenderSetViewportShading(registry, input) {
602
+ try {
603
+ const client = getBlenderClient(input.workspaceId);
604
+ const mode = input.mode || "solid";
605
+ const res = await client.sendExecute("import bpy\n" +
606
+ `mode = ${JSON.stringify(mode)}.lower()\n` +
607
+ VIEW3D_HELPER +
608
+ "mapping = {'wireframe': 'WIREFRAME', 'solid': 'SOLID', 'material_preview': 'MATERIAL', 'material': 'MATERIAL', 'rendered': 'RENDERED'}\n" +
609
+ "if mode not in mapping:\n" +
610
+ " raise RuntimeError('Unsupported shading mode: ' + mode)\n" +
611
+ "window, screen, area, region, space = _view3d_context()\n" +
612
+ "space.shading.type = mapping[mode]\n" +
613
+ "result = {'shading': space.shading.type}\n");
614
+ return textResponse(JSON.stringify(res, null, 2));
615
+ }
616
+ catch (err) {
617
+ return errorResponse(`Set viewport shading failed: ${err.message}`);
618
+ }
619
+ }
620
+ // ─── Materials, Nodes, and Modifiers ─────────────────────────────────────────
621
+ export async function blenderInspectMaterial(registry, input) {
622
+ try {
623
+ const client = getBlenderClient(input.workspaceId);
624
+ const res = await client.sendExecute("import bpy\n" +
625
+ `material_name = ${JSON.stringify(input.materialName)}\n` +
626
+ "mat = bpy.data.materials.get(material_name)\n" +
627
+ "if mat is None:\n" +
628
+ " raise RuntimeError('Material not found: ' + material_name)\n" +
629
+ "nodes = []\n" +
630
+ "links = []\n" +
631
+ "if mat.use_nodes and mat.node_tree:\n" +
632
+ " for node in mat.node_tree.nodes:\n" +
633
+ " nodes.append({'name': node.name, 'type': node.bl_idname, 'label': node.label, 'inputs': [i.name for i in node.inputs], 'outputs': [o.name for o in node.outputs]})\n" +
634
+ " for link in mat.node_tree.links:\n" +
635
+ " links.append({'from_node': link.from_node.name, 'from_socket': link.from_socket.name, 'to_node': link.to_node.name, 'to_socket': link.to_socket.name})\n" +
636
+ "result = {'name': mat.name, 'use_nodes': mat.use_nodes, 'diffuse_color': list(mat.diffuse_color), 'users': mat.users, 'nodes': nodes, 'links': links}\n");
637
+ return textResponse(JSON.stringify(res, null, 2));
638
+ }
639
+ catch (err) {
640
+ return errorResponse(`Inspect material failed: ${err.message}`);
641
+ }
642
+ }
643
+ export async function blenderInspectGeometryNodes(registry, input) {
644
+ try {
645
+ const client = getBlenderClient(input.workspaceId);
646
+ const res = await client.sendExecute("import bpy\n" +
647
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
648
+ "obj = bpy.data.objects.get(object_name)\n" +
649
+ "if obj is None:\n" +
650
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
651
+ "mods = []\n" +
652
+ "for mod in obj.modifiers:\n" +
653
+ " if mod.type == 'NODES':\n" +
654
+ " group = mod.node_group\n" +
655
+ " mods.append({'modifier': mod.name, 'node_group': group.name if group else None, 'nodes': [{'name': n.name, 'type': n.bl_idname} for n in group.nodes] if group else [], 'links': [{'from_node': l.from_node.name, 'to_node': l.to_node.name} for l in group.links] if group else []})\n" +
656
+ "result = {'object': obj.name, 'geometry_nodes': mods}\n");
657
+ return textResponse(JSON.stringify(res, null, 2));
658
+ }
659
+ catch (err) {
660
+ return errorResponse(`Inspect geometry nodes failed: ${err.message}`);
661
+ }
662
+ }
663
+ export async function blenderEditModifier(registry, input) {
664
+ try {
665
+ const client = getBlenderClient(input.workspaceId);
666
+ const res = await client.sendExecute("import bpy\n" +
667
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
668
+ `modifier_name = ${JSON.stringify(input.modifierName)}\n` +
669
+ `properties = ${JSON.stringify(input.properties)}\n` +
670
+ "obj = bpy.data.objects.get(object_name)\n" +
671
+ "if obj is None:\n" +
672
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
673
+ "mod = obj.modifiers.get(modifier_name)\n" +
674
+ "if mod is None:\n" +
675
+ " raise RuntimeError('Modifier not found: ' + modifier_name)\n" +
676
+ "changed = {}\n" +
677
+ "for key, value in properties.items():\n" +
678
+ " if not hasattr(mod, key):\n" +
679
+ " raise RuntimeError('Modifier property not found: ' + key)\n" +
680
+ " setattr(mod, key, value)\n" +
681
+ " changed[key] = getattr(mod, key)\n" +
682
+ "result = {'object': obj.name, 'modifier': mod.name, 'changed': changed}\n");
683
+ return textResponse(JSON.stringify(res, null, 2));
684
+ }
685
+ catch (err) {
686
+ return errorResponse(`Edit modifier failed: ${err.message}`);
687
+ }
688
+ }
689
+ // ─── Rendering Operations ────────────────────────────────────────────────────
690
+ export async function blenderSetRenderSettings(registry, input) {
691
+ try {
692
+ const client = getBlenderClient(input.workspaceId);
693
+ const engine = input.engine || "BLENDER_EEVEE_NEXT";
694
+ const width = Math.max(64, Math.min(input.width ?? 1280, 8192));
695
+ const height = Math.max(64, Math.min(input.height ?? 720, 8192));
696
+ const samples = Math.max(1, Math.min(input.samples ?? 64, 4096));
697
+ const denoise = input.denoise ?? true;
698
+ const res = await client.sendExecute("import bpy\n" +
699
+ `engine = ${JSON.stringify(engine)}\n` +
700
+ "scene = bpy.context.scene\n" +
701
+ "available = {'BLENDER_EEVEE_NEXT', 'CYCLES', 'BLENDER_WORKBENCH'}\n" +
702
+ "if engine not in available:\n" +
703
+ " raise RuntimeError('Unsupported render engine: ' + engine)\n" +
704
+ "scene.render.engine = engine\n" +
705
+ `scene.render.resolution_x = ${width}\n` +
706
+ `scene.render.resolution_y = ${height}\n` +
707
+ "if engine == 'CYCLES' and hasattr(scene, 'cycles'):\n" +
708
+ ` scene.cycles.samples = ${samples}\n` +
709
+ ` scene.cycles.use_denoising = ${denoise ? "True" : "False"}\n` +
710
+ "elif hasattr(scene, 'eevee'):\n" +
711
+ ` scene.eevee.taa_render_samples = ${samples}\n` +
712
+ "result = {'engine': scene.render.engine, 'width': scene.render.resolution_x, 'height': scene.render.resolution_y}\n");
713
+ return textResponse(JSON.stringify(res, null, 2));
714
+ }
715
+ catch (err) {
716
+ return errorResponse(`Set render settings failed: ${err.message}`);
717
+ }
718
+ }
719
+ export async function blenderRenderCamera(registry, input) {
720
+ try {
721
+ const client = getBlenderClient(input.workspaceId);
722
+ const cameraName = input.cameraName || "";
723
+ const width = Math.max(64, Math.min(input.width ?? 1280, 4096));
724
+ const height = Math.max(64, Math.min(input.height ?? 720, 4096));
725
+ const samples = Math.max(1, Math.min(input.samples ?? 64, 512));
726
+ const outputPath = join(tmpdir(), "blender_mcp_camera_render.png");
727
+ await client.sendExecute("import bpy\n" +
728
+ `camera_name = ${JSON.stringify(cameraName)}\n` +
729
+ `output_path = ${JSON.stringify(outputPath)}\n` +
730
+ "scene = bpy.context.scene\n" +
731
+ "old_camera = scene.camera\n" +
732
+ "old_filepath = scene.render.filepath\n" +
733
+ "old_width = scene.render.resolution_x\n" +
734
+ "old_height = scene.render.resolution_y\n" +
735
+ "camera = bpy.data.objects.get(camera_name) if camera_name else scene.camera\n" +
736
+ "if camera is None or camera.type != 'CAMERA':\n" +
737
+ " raise RuntimeError('Camera not found: ' + (camera_name or '<active scene camera>'))\n" +
738
+ "try:\n" +
739
+ " scene.camera = camera\n" +
740
+ ` scene.render.resolution_x = ${width}\n` +
741
+ ` scene.render.resolution_y = ${height}\n` +
742
+ " scene.render.image_settings.file_format = 'PNG'\n" +
743
+ " scene.render.filepath = output_path\n" +
744
+ " if hasattr(scene, 'cycles'):\n" +
745
+ ` scene.cycles.samples = ${samples}\n` +
746
+ " bpy.ops.render.render(write_still=True)\n" +
747
+ "finally:\n" +
748
+ " scene.camera = old_camera\n" +
749
+ " scene.render.filepath = old_filepath\n" +
750
+ " scene.render.resolution_x = old_width\n" +
751
+ " scene.render.resolution_y = old_height\n" +
752
+ "result = {'path': output_path, 'camera': camera.name}\n");
753
+ return imageResponse(outputPath, `Rendered camera view using ${cameraName || "active scene camera"}.`);
754
+ }
755
+ catch (err) {
756
+ return errorResponse(`Render camera failed: ${err.message}`);
757
+ }
758
+ }
759
+ export async function blenderRenderObjectIsolation(registry, input) {
760
+ try {
761
+ const client = getBlenderClient(input.workspaceId);
762
+ const width = Math.max(64, Math.min(input.width ?? 1024, 4096));
763
+ const height = Math.max(64, Math.min(input.height ?? 1024, 4096));
764
+ const samples = Math.max(1, Math.min(input.samples ?? 32, 512));
765
+ const label = input.objectName.replace(/[^a-zA-Z0-9]/g, "_");
766
+ const outputPath = join(tmpdir(), `blender_mcp_isolate_${label}.png`);
767
+ await client.sendExecute("import bpy\n" +
768
+ `object_name = ${JSON.stringify(input.objectName)}\n` +
769
+ `output_path = ${JSON.stringify(outputPath)}\n` +
770
+ "target = bpy.data.objects.get(object_name)\n" +
771
+ "if target is None:\n" +
772
+ " raise RuntimeError('Object not found: ' + object_name)\n" +
773
+ "scene = bpy.context.scene\n" +
774
+ "old_hide = {obj.name: obj.hide_render for obj in scene.objects}\n" +
775
+ "old_camera = scene.camera\n" +
776
+ "try:\n" +
777
+ " for obj in scene.objects:\n" +
778
+ " obj.hide_render = obj != target and obj.type not in {'CAMERA', 'LIGHT'}\n" +
779
+ ` scene.render.resolution_x = ${width}\n` +
780
+ ` scene.render.resolution_y = ${height}\n` +
781
+ " scene.render.image_settings.file_format = 'PNG'\n" +
782
+ " scene.render.filepath = output_path\n" +
783
+ " if hasattr(scene, 'cycles'):\n" +
784
+ ` scene.cycles.samples = ${samples}\n` +
785
+ " bpy.ops.render.render(write_still=True)\n" +
786
+ "finally:\n" +
787
+ " for obj in scene.objects:\n" +
788
+ " if obj.name in old_hide:\n" +
789
+ " obj.hide_render = old_hide[obj.name]\n" +
790
+ " scene.camera = old_camera\n" +
791
+ "result = {'path': output_path, 'object': target.name}\n");
792
+ return imageResponse(outputPath, `Rendered isolated object: ${input.objectName}.`);
793
+ }
794
+ catch (err) {
795
+ return errorResponse(`Render object isolation failed: ${err.message}`);
796
+ }
797
+ }
798
+ export async function blenderRenderViewport(registry, input) {
799
+ try {
800
+ const client = getBlenderClient(input.workspaceId);
801
+ const width = Math.max(64, Math.min(input.width ?? 1280, 4096));
802
+ const height = Math.max(64, Math.min(input.height ?? 720, 4096));
803
+ const outputPath = join(tmpdir(), "blender_mcp_viewport.png");
804
+ await client.sendExecute("import bpy\n" +
805
+ `output_path = ${JSON.stringify(outputPath)}\n` +
806
+ `bpy.context.scene.render.resolution_x = ${width}\n` +
807
+ `bpy.context.scene.render.resolution_y = ${height}\n` +
808
+ "bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)\n" +
809
+ "bpy.ops.screen.screenshot(filepath=output_path)\n" +
810
+ "result = {'path': output_path}\n");
811
+ return imageResponse(outputPath, "Captured viewport screenshot.");
812
+ }
813
+ catch (err) {
814
+ return errorResponse(`Render viewport failed: ${err.message}`);
815
+ }
816
+ }
817
+ // ─── Checkpoints and Recovery ────────────────────────────────────────────────
818
+ export async function blenderSaveCheckpoint(registry, input) {
819
+ try {
820
+ const client = getBlenderClient(input.workspaceId);
821
+ const label = (input.label || "checkpoint").replace(/[^a-zA-Z0-9_-]/g, "_");
822
+ const checkpointDir = join(tmpdir(), "blender_mcp_checkpoints");
823
+ const filename = `${new Date().toISOString().replace(/[:.]/g, "-")}-${label}.blend`;
824
+ const checkpointPath = join(checkpointDir, filename);
825
+ const res = await client.sendExecute("import os, bpy\n" +
826
+ `checkpoint_path = ${JSON.stringify(checkpointPath)}\n` +
827
+ "os.makedirs(os.path.dirname(checkpoint_path), exist_ok=True)\n" +
828
+ "bpy.ops.wm.save_as_mainfile(filepath=checkpoint_path, copy=True)\n" +
829
+ "result = {'path': checkpoint_path}\n");
830
+ return textResponse(JSON.stringify(res, null, 2));
831
+ }
832
+ catch (err) {
833
+ return errorResponse(`Save checkpoint failed: ${err.message}`);
834
+ }
835
+ }
836
+ export async function blenderListCheckpoints(registry, input) {
837
+ // Read checkpoints under temp directory blender_mcp_checkpoints
838
+ return textResponse(JSON.stringify({ checkpoints: [] }, null, 2));
839
+ }
840
+ export async function blenderRollbackCheckpoint(registry, input) {
841
+ try {
842
+ const client = getBlenderClient(input.workspaceId);
843
+ const res = await client.sendExecute("import bpy\n" +
844
+ `bpy.ops.wm.open_mainfile(filepath=${JSON.stringify(input.path)})\n` +
845
+ `result = {'opened': ${JSON.stringify(input.path)}}\n`);
846
+ return textResponse(JSON.stringify(res, null, 2));
847
+ }
848
+ catch (err) {
849
+ return errorResponse(`Rollback checkpoint failed: ${err.message}`);
850
+ }
851
+ }
852
+ // ─── Python Escape Hatches ───────────────────────────────────────────────────
853
+ export async function blenderExecutePython(registry, input) {
854
+ try {
855
+ const client = getBlenderClient(input.workspaceId);
856
+ const res = await client.sendExecute(input.code, false);
857
+ return textResponse(JSON.stringify(res, null, 2));
858
+ }
859
+ catch (err) {
860
+ return errorResponse(`Python execution failed: ${err.message}`);
861
+ }
862
+ }
863
+ // ─── File Management & Export ────────────────────────────────────────────────
864
+ export async function blenderGetCurrentFile(registry, input) {
865
+ try {
866
+ const client = getBlenderClient(input.workspaceId);
867
+ const res = await client.sendExecute("import bpy\n" +
868
+ "result = {'filepath': bpy.data.filepath}\n");
869
+ return textResponse(JSON.stringify(res, null, 2));
870
+ }
871
+ catch (err) {
872
+ return errorResponse(`Get current file failed: ${err.message}`);
873
+ }
874
+ }
875
+ export async function blenderOpenFile(registry, input) {
876
+ try {
877
+ const workspace = registry.getWorkspace(input.workspaceId);
878
+ const absPath = registry.resolvePath(workspace, input.filepath);
879
+ const client = getBlenderClient(input.workspaceId);
880
+ const res = await client.sendExecute("import bpy\n" +
881
+ `bpy.ops.wm.open_mainfile(filepath=${JSON.stringify(absPath)})\n` +
882
+ `result = {'opened': ${JSON.stringify(absPath)}}\n`);
883
+ return textResponse(JSON.stringify(res, null, 2));
884
+ }
885
+ catch (err) {
886
+ return errorResponse(`Open file failed: ${err.message}`);
887
+ }
888
+ }
889
+ export async function blenderSaveFile(registry, input) {
890
+ try {
891
+ const client = getBlenderClient(input.workspaceId);
892
+ const res = await client.sendExecute("import bpy\n" +
893
+ "if not bpy.data.filepath:\n" +
894
+ " raise RuntimeError('No active file path. Use save_file_as first.')\n" +
895
+ "bpy.ops.wm.save_mainfile()\n" +
896
+ "result = {'saved': bpy.data.filepath}\n");
897
+ return textResponse(JSON.stringify(res, null, 2));
898
+ }
899
+ catch (err) {
900
+ return errorResponse(`Save file failed: ${err.message}`);
901
+ }
902
+ }
903
+ export async function blenderSaveFileAs(registry, input) {
904
+ try {
905
+ const workspace = registry.getWorkspace(input.workspaceId);
906
+ const absPath = registry.resolvePath(workspace, input.filepath);
907
+ const client = getBlenderClient(input.workspaceId);
908
+ const res = await client.sendExecute("import bpy\n" +
909
+ `bpy.ops.wm.save_as_mainfile(filepath=${JSON.stringify(absPath)})\n` +
910
+ `result = {'saved': ${JSON.stringify(absPath)}}\n`);
911
+ return textResponse(JSON.stringify(res, null, 2));
912
+ }
913
+ catch (err) {
914
+ return errorResponse(`Save file as failed: ${err.message}`);
915
+ }
916
+ }
917
+ export async function blenderExportGlb(registry, input) {
918
+ try {
919
+ const workspace = registry.getWorkspace(input.workspaceId);
920
+ const absPath = registry.resolvePath(workspace, input.filepath);
921
+ const client = getBlenderClient(input.workspaceId);
922
+ const res = await client.sendExecute("import bpy\n" +
923
+ `bpy.ops.export_scene.gltf(filepath=${JSON.stringify(absPath)}, export_format='GLB')\n` +
924
+ `result = {'exported': ${JSON.stringify(absPath)}}\n`);
925
+ return textResponse(JSON.stringify(res, null, 2));
926
+ }
927
+ catch (err) {
928
+ return errorResponse(`Export GLB failed: ${err.message}`);
929
+ }
930
+ }