@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.9 → 0.10.1-dev.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 (276) hide show
  1. package/README.ko.md +1188 -0
  2. package/README.md +1139 -57
  3. package/cli/README.md +239 -7
  4. package/cli/bin/create-muscat-plugin.mjs +1 -1
  5. package/cli/bin/muscat-pk.mjs +123 -27
  6. package/cli/bundle-templates.mjs +31 -0
  7. package/cli/docker/Dockerfile.cpp +28 -0
  8. package/cli/docker/Dockerfile.dev +17 -0
  9. package/cli/docker/Dockerfile.python +21 -0
  10. package/cli/prepack.mjs +12 -0
  11. package/cli/program-commands.json +2079 -0
  12. package/cli/src/__pycache__/host_agent.cpython-37.pyc +0 -0
  13. package/cli/src/ai-rules.mjs +108 -0
  14. package/cli/src/build.mjs +333 -93
  15. package/cli/src/check-package.mjs +8 -23
  16. package/cli/src/completion.mjs +168 -0
  17. package/cli/src/cpp.mjs +78 -0
  18. package/cli/src/cppIde.mjs +76 -0
  19. package/cli/src/dev.mjs +195 -6
  20. package/cli/src/devBack.mjs +671 -60
  21. package/cli/src/devId.mjs +54 -0
  22. package/cli/src/devSession.mjs +67 -0
  23. package/cli/src/devUi.mjs +218 -25
  24. package/cli/src/esbuild.mjs +27 -10
  25. package/cli/src/host_agent.py +223 -0
  26. package/cli/src/manifest.mjs +288 -0
  27. package/cli/src/mcp.mjs +1232 -0
  28. package/cli/src/mcpProxy.mjs +110 -0
  29. package/cli/src/muscatLocal.mjs +292 -0
  30. package/cli/src/new.mjs +205 -68
  31. package/cli/src/pluginDevServer.mjs +111 -0
  32. package/cli/src/ports.mjs +193 -0
  33. package/cli/src/programCommands.mjs +327 -0
  34. package/cli/src/projectCheck.mjs +102 -0
  35. package/cli/src/python.mjs +112 -0
  36. package/cli/src/ros2.mjs +62 -18
  37. package/cli/src/sdk.mjs +74 -24
  38. package/cli/src/sim.mjs +177 -0
  39. package/cli/src/storybook.mjs +150 -0
  40. package/cli/src/templates-embedded.mjs +4 -0
  41. package/cli/src/type.mjs +93 -34
  42. package/cli/src/uninstall.mjs +81 -0
  43. package/cli/src/update.mjs +364 -35
  44. package/cli/src/util.mjs +341 -11
  45. package/cli/src/zenohDev.mjs +83 -0
  46. package/cli/src/zip.mjs +121 -0
  47. package/cli/storybook-dist/_design-tokens.css +4957 -0
  48. package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -0
  49. package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -0
  50. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -0
  51. package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -0
  52. package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -0
  53. package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -0
  54. package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -0
  55. package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -0
  56. package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -0
  57. package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -0
  58. package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -0
  59. package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -0
  60. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -0
  61. package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -0
  62. package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -0
  63. package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -0
  64. package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -0
  65. package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -0
  66. package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -0
  67. package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -0
  68. package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -0
  69. package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -0
  70. package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -0
  71. package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -0
  72. package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -0
  73. package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -0
  74. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -0
  75. package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -0
  76. package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -0
  77. package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -0
  78. package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -0
  79. package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -0
  80. package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -0
  81. package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -0
  82. package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -0
  83. package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -0
  84. package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -0
  85. package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -0
  86. package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -0
  87. package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -0
  88. package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -0
  89. package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -0
  90. package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -0
  91. package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -0
  92. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -0
  93. package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -0
  94. package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -0
  95. package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -0
  96. package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -0
  97. package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -0
  98. package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -0
  99. package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -0
  100. package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -0
  101. package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -0
  102. package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -0
  103. package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -0
  104. package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -0
  105. package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -0
  106. package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -0
  107. package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -0
  108. package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -0
  109. package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -0
  110. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -0
  111. package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -0
  112. package/cli/storybook-dist/assets/Color-YHDXOIA2-pCKrjUal.js +1 -0
  113. package/cli/storybook-dist/assets/Docs-B-Y7I1GA.js +7 -0
  114. package/cli/storybook-dist/assets/Docs-BFJcE_VS.js +8 -0
  115. package/cli/storybook-dist/assets/Docs-B_QaTtz8.js +8 -0
  116. package/cli/storybook-dist/assets/Docs-CryDGmfh.js +20 -0
  117. package/cli/storybook-dist/assets/Docs-Cw2AVNKx.js +7 -0
  118. package/cli/storybook-dist/assets/Docs-Q6r1fBeL.js +12 -0
  119. package/cli/storybook-dist/assets/DocsRenderer-CFRXHY34-_OmZBCn8.js +2 -0
  120. package/cli/storybook-dist/assets/NxAvatar.stories-Ctf5G2FQ.js +13 -0
  121. package/cli/storybook-dist/assets/NxButton.stories-DeLObVSj.js +20 -0
  122. package/cli/storybook-dist/assets/NxIcons.stories-DeqrVzVs.js +3 -0
  123. package/cli/storybook-dist/assets/NxNumberInput.stories-LqoY0ed1.js +26 -0
  124. package/cli/storybook-dist/assets/NxPagination.stories-DI6bXuJM.js +11 -0
  125. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-3HSV1XmN.js +14 -0
  126. package/cli/storybook-dist/assets/NxRangeInput.stories-1pjgQVaG.js +17 -0
  127. package/cli/storybook-dist/assets/NxSelectBox.stories-bcQPZEuF.js +17 -0
  128. package/cli/storybook-dist/assets/NxSlideRange.stories-BosX4axk.js +14 -0
  129. package/cli/storybook-dist/assets/NxSwitch.stories-DxV6dS33.js +11 -0
  130. package/cli/storybook-dist/assets/NxTextArea.stories-CO545-LP.js +11 -0
  131. package/cli/storybook-dist/assets/NxTextInput.stories-BP0npai1.js +11 -0
  132. package/cli/storybook-dist/assets/PretendardVariable-CJuje-Rk.woff2 +0 -0
  133. package/cli/storybook-dist/assets/RBAccordion.stories-fVR4Va2a.js +17 -0
  134. package/cli/storybook-dist/assets/RBAccordionGroup.stories-Du5bsckD.js +33 -0
  135. package/cli/storybook-dist/assets/RBAlert.stories-DNkMXUu6.js +17 -0
  136. package/cli/storybook-dist/assets/RBBadge.stories-C44pXt8D.js +1 -0
  137. package/cli/storybook-dist/assets/RBBattery.stories-DE2JHvfL.js +10 -0
  138. package/cli/storybook-dist/assets/RBCard.stories-CuIda7NI.js +45 -0
  139. package/cli/storybook-dist/assets/RBCheckBox.stories-D1qM1ayr.js +1 -0
  140. package/cli/storybook-dist/assets/RBCheckIconButton.stories-e4gkliBO.js +1 -0
  141. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-CDzCtsMI.js +24 -0
  142. package/cli/storybook-dist/assets/RBChipTab.stories-DBrc_3Mp.js +21 -0
  143. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-hlrsHjKY.js +1 -0
  144. package/cli/storybook-dist/assets/RBColorIcons.stories-BHUvpkdu.js +3 -0
  145. package/cli/storybook-dist/assets/RBCommand.stories-azAF41NP.js +73 -0
  146. package/cli/storybook-dist/assets/RBConfirm.stories-BMNjwbmT.js +20 -0
  147. package/cli/storybook-dist/assets/RBDateInput.stories-D0VezDzv.js +15 -0
  148. package/cli/storybook-dist/assets/RBDialog.stories-AOJ975Fy.js +38 -0
  149. package/cli/storybook-dist/assets/RBDivider.stories-NSvnj43Z.js +1 -0
  150. package/cli/storybook-dist/assets/RBDrawer.stories-Du0oTvD1.js +14 -0
  151. package/cli/storybook-dist/assets/RBDropdownMenu.stories-Hsxve4PW.js +86 -0
  152. package/cli/storybook-dist/assets/RBEmptyView.stories-BlwHx3zp.js +12 -0
  153. package/cli/storybook-dist/assets/RBIconButton.stories-DAPDY63x.js +1 -0
  154. package/cli/storybook-dist/assets/RBIcons.stories-W5ZUV2L1.js +3 -0
  155. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-C99j0Y3g.js +1 -0
  156. package/cli/storybook-dist/assets/RBJoyStick.stories-Du0H1iAS.js +11 -0
  157. package/cli/storybook-dist/assets/RBLabelButton.stories-BlvHaU_Z.js +1 -0
  158. package/cli/storybook-dist/assets/RBLight.stories-BY6DKrre.js +1 -0
  159. package/cli/storybook-dist/assets/RBMenubar.stories-D2PEGpM9.js +3 -0
  160. package/cli/storybook-dist/assets/RBMultiRangeInput.stories-Bm2qSmjq.js +14 -0
  161. package/cli/storybook-dist/assets/RBPagination.stories-DDleJa5N.js +17 -0
  162. package/cli/storybook-dist/assets/RBPopover.stories-27YxPSFG.js +11 -0
  163. package/cli/storybook-dist/assets/RBPrompt.stories-BFaIOctr.js +20 -0
  164. package/cli/storybook-dist/assets/RBRadioBox.stories-BroDar0t.js +1 -0
  165. package/cli/storybook-dist/assets/RBRadioButton.stories-M3O-wX7V.js +11 -0
  166. package/cli/storybook-dist/assets/RBRangeDateInput.stories-CWlM_D5X.js +15 -0
  167. package/cli/storybook-dist/assets/RBResizable.stories-ui8X1qnc.js +20 -0
  168. package/cli/storybook-dist/assets/RBSelectBox.stories-C8T6cfA0.js +74 -0
  169. package/cli/storybook-dist/assets/RBSheet.stories-BCOo_Zcc.js +39 -0
  170. package/cli/storybook-dist/assets/RBSideNumberInput.stories-CU2_MVlJ.js +5 -0
  171. package/cli/storybook-dist/assets/RBSlider.stories-DgOp1TFc.js +5 -0
  172. package/cli/storybook-dist/assets/RBSpinner.stories-qLScY3zZ.js +14 -0
  173. package/cli/storybook-dist/assets/RBSwitch.stories-SXSY1QMb.js +65 -0
  174. package/cli/storybook-dist/assets/RBTab.stories-BzXXTsXP.js +1 -0
  175. package/cli/storybook-dist/assets/RBTable.stories-DHTeRNVk.js +62 -0
  176. package/cli/storybook-dist/assets/RBTableRow.stories-DRNRsd15.js +11 -0
  177. package/cli/storybook-dist/assets/RBTabs.stories-DP0noBLp.js +21 -0
  178. package/cli/storybook-dist/assets/RBTextArea.stories-TkaejfxJ.js +5 -0
  179. package/cli/storybook-dist/assets/RBTextField.stories-B6VpCGyt.js +45 -0
  180. package/cli/storybook-dist/assets/RBTextInput.stories-D3hZ08e-.js +5 -0
  181. package/cli/storybook-dist/assets/RBToast.stories-BQ2wS2U0.js +11 -0
  182. package/cli/storybook-dist/assets/RBToggle.stories-GVOjGqo3.js +12 -0
  183. package/cli/storybook-dist/assets/RBToggleGroup.stories-pzx0btOD.js +17 -0
  184. package/cli/storybook-dist/assets/RBTooltip.stories-jufB6fa1.js +9 -0
  185. package/cli/storybook-dist/assets/SideNumberInput-BYlmNHEw.js +1109 -0
  186. package/cli/storybook-dist/assets/SideNumberInput-SAkhydsn.css +21 -0
  187. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  188. package/cli/storybook-dist/assets/chunk-XP5HYGXS-BHXC9YeQ.js +1 -0
  189. package/cli/storybook-dist/assets/client-DTf7SId2.js +1 -0
  190. package/cli/storybook-dist/assets/entry-preview-CAkU-e0D.js +2 -0
  191. package/cli/storybook-dist/assets/entry-preview-docs-b8bpVBXy.js +46 -0
  192. package/cli/storybook-dist/assets/iframe-B-BmTsxV.js +211 -0
  193. package/cli/storybook-dist/assets/index-B4FGfHer.js +1 -0
  194. package/cli/storybook-dist/assets/index-BRq5TIsn.js +24 -0
  195. package/cli/storybook-dist/assets/index-CT3U1fyk.js +1 -0
  196. package/cli/storybook-dist/assets/index-ChsGqxH_.js +9 -0
  197. package/cli/storybook-dist/assets/index-CkB0GfaI.js +575 -0
  198. package/cli/storybook-dist/assets/index-ClAc6FBW.js +240 -0
  199. package/cli/storybook-dist/assets/index-DKFBREX_.js +1 -0
  200. package/cli/storybook-dist/assets/index-DgH-xKnr.js +11 -0
  201. package/cli/storybook-dist/assets/index-Dmy2JGbm.js +1 -0
  202. package/cli/storybook-dist/assets/index-DrFu-skq.js +6 -0
  203. package/cli/storybook-dist/assets/jsx-runtime-BTJTZTIL.js +9 -0
  204. package/cli/storybook-dist/assets/preview-B8lJiyuQ.js +34 -0
  205. package/cli/storybook-dist/assets/preview-BBWR9nbA.js +1 -0
  206. package/cli/storybook-dist/assets/preview-BVJwlGUF.js +1 -0
  207. package/cli/storybook-dist/assets/preview-BWzBA1C2.js +396 -0
  208. package/cli/storybook-dist/assets/preview-CVgpLj3b.js +1 -0
  209. package/cli/storybook-dist/assets/preview-CvbIS5ZJ.js +1 -0
  210. package/cli/storybook-dist/assets/preview-CznQwwbo.js +3 -0
  211. package/cli/storybook-dist/assets/preview-DD_OYowb.js +1 -0
  212. package/cli/storybook-dist/assets/preview-DGUiP6tS.js +7 -0
  213. package/cli/storybook-dist/assets/preview-DHQbi4pV.js +1 -0
  214. package/cli/storybook-dist/assets/preview-M77JRg1A.js +2 -0
  215. package/cli/storybook-dist/assets/preview-kTZPgP6D.css +1 -0
  216. package/cli/storybook-dist/assets/react-18-hB3smqWJ.js +1 -0
  217. package/cli/storybook-dist/assets/react-resizable-panels-HsFbxPbn.css +1 -0
  218. package/cli/storybook-dist/assets/react-resizable-panels.browser-DuxpZLkF.js +120 -0
  219. package/cli/storybook-dist/assets/test-utils-BQudk2xt.js +9 -0
  220. package/cli/storybook-dist/favicon.svg +1 -0
  221. package/cli/storybook-dist/iframe.html +666 -0
  222. package/cli/storybook-dist/index.html +190 -0
  223. package/cli/storybook-dist/index.json +1 -0
  224. package/cli/storybook-dist/nunito-sans-bold-italic.woff2 +0 -0
  225. package/cli/storybook-dist/nunito-sans-bold.woff2 +0 -0
  226. package/cli/storybook-dist/nunito-sans-italic.woff2 +0 -0
  227. package/cli/storybook-dist/nunito-sans-regular.woff2 +0 -0
  228. package/cli/storybook-dist/project.json +1 -0
  229. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js +331 -0
  230. package/cli/storybook-dist/sb-addons/chromatic-com-storybook-10/manager-bundle.js.LEGAL.txt +51 -0
  231. package/cli/storybook-dist/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
  232. package/cli/storybook-dist/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
  233. package/cli/storybook-dist/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
  234. package/cli/storybook-dist/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
  235. package/cli/storybook-dist/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
  236. package/cli/storybook-dist/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
  237. package/cli/storybook-dist/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
  238. package/cli/storybook-dist/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
  239. package/cli/storybook-dist/sb-addons/links-1/manager-bundle.js +3 -0
  240. package/cli/storybook-dist/sb-addons/storybook-13/manager-bundle.js +3 -0
  241. package/cli/storybook-dist/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
  242. package/cli/storybook-dist/sb-addons/storybook-experimental-addon-test-12/manager-bundle.js +223 -0
  243. package/cli/storybook-dist/sb-addons/themes-11/manager-bundle.js +3 -0
  244. package/cli/storybook-dist/sb-common-assets/favicon.svg +1 -0
  245. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  246. package/cli/storybook-dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  247. package/cli/storybook-dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  248. package/cli/storybook-dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  249. package/cli/storybook-dist/sb-manager/globals-module-info.js +1052 -0
  250. package/cli/storybook-dist/sb-manager/globals-runtime.js +41775 -0
  251. package/cli/storybook-dist/sb-manager/globals.js +48 -0
  252. package/cli/storybook-dist/sb-manager/runtime.js +12048 -0
  253. package/cli/templates/common/README.md +1070 -83
  254. package/cli/templates/common/assets/icon.svg +44 -2
  255. package/cli/templates/common/manifest.json +14 -13
  256. package/cli/templates/common/package.json +11 -4
  257. package/cli/templates/common/py/main.py +125 -0
  258. package/cli/templates/common/requirements.txt +4 -3
  259. package/cli/templates/common/tsconfig.json +1 -1
  260. package/cli/templates/common/ui/content.tsx +216 -0
  261. package/cli/templates/common/ui/style.css +6 -0
  262. package/cli/templates/common/vite.config.ts +6 -25
  263. package/cli/templates/cpp/cpp/CMakeLists.txt +14 -0
  264. package/cli/templates/cpp/cpp/main.cpp +73 -0
  265. package/cli/templates/readme-cpp/README.md +1012 -0
  266. package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +12 -1
  267. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +22 -0
  268. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -0
  269. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +41 -0
  270. package/cli/vite.d.mts +25 -0
  271. package/cli/vite.mjs +292 -0
  272. package/package.json +34 -7
  273. package/cli/src/pack.mjs +0 -61
  274. package/cli/src/preview.mjs +0 -91
  275. package/cli/templates/common/main.py +0 -114
  276. package/cli/templates/common/src/content.tsx +0 -33
@@ -0,0 +1,1232 @@
1
+ // muscat-pk mcp — an MCP (Model Context Protocol) server exposing the Muscat
2
+ // plugin UI/SDK surface to an AI coding agent (Claude Code, Codex, ...) so it
3
+ // looks things up instead of guessing. Tools:
4
+ //
5
+ // Frontend UI (@rainbow-robotics/plugin-ui/react, via dist/react.d.ts):
6
+ // - list_components every exported component/hook/type.
7
+ // - get_component_props the real TypeScript type for one export.
8
+ // - get_component_example a real Storybook story (working code, not a guess).
9
+ //
10
+ // Design system (rb-styles rb-token.css):
11
+ // - list_design_tokens Tailwind classes to use (bg-bg-*, text-fg-*, typo-*, ...).
12
+ // - search_design_tokens find a token by name or hex value.
13
+ //
14
+ // Plugin SDK docs + Python backend:
15
+ // - search_rb_sdk search the plugin-author README (useRb(), rb.*, manifest, runtimes).
16
+ // - list_rb_sdk classes/functions of the installed rainbow.rb_sdk
17
+ // (lang: "python" | "cpp" — cpp reads the headers of the
18
+ // rainbow-rb-sdk-dev apt package out of the cpp dev image).
19
+ // - get_rb_sdk_api signature + docstring for an rb_sdk class/function.
20
+ //
21
+ // Program page (nexus program.c.tsx + common OpenAPI):
22
+ // - list_program_commands built-in commands, default args, nesting rules.
23
+ // - get_program_api rb.program / REST guide + live endpoint schemas.
24
+ //
25
+ // Add to an MCP client config, e.g. Claude Code's .mcp.json:
26
+ // { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
27
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
29
+ import { z } from "zod";
30
+ import ts from "typescript";
31
+ import {
32
+ appendFileSync,
33
+ existsSync,
34
+ mkdirSync,
35
+ readdirSync,
36
+ readFileSync,
37
+ renameSync,
38
+ rmSync,
39
+ statSync,
40
+ writeFileSync,
41
+ } from "node:fs";
42
+ import { spawnSync } from "node:child_process";
43
+ import { homedir } from "node:os";
44
+ import { join, resolve } from "node:path";
45
+ import { fileURLToPath } from "node:url";
46
+ import { dockerUserFlags, kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
47
+ import { PYTHON_DEV_IMAGE } from "./python.mjs";
48
+ import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
49
+ import { CPP_DEV_IMAGE } from "./cpp.mjs";
50
+ import { NATIVE_TO_RB, SERVER_INSTRUCTIONS } from "./ai-rules.mjs";
51
+ import { checkProject, formatProjectCheck } from "./projectCheck.mjs";
52
+ import { describeProgramApi, loadProgramCommands, PROGRAM_API_GUIDE } from "./programCommands.mjs";
53
+
54
+ const TEMPLATE_READMES = { python: "common/README.md", cpp: "readme-cpp/README.md" };
55
+ // Design tokens: prefer the shipped copy, fall back to the monorepo source.
56
+ const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
57
+ ? SHIPPED_DESIGN_TOKENS
58
+ : resolve(kitRoot, "../..", "packages/rb-styles/css/output/rb-token.css");
59
+
60
+ /* -------------------------------------------------------------------------- */
61
+ /* react.d.ts introspection — component/hook list + prop types */
62
+ /* -------------------------------------------------------------------------- */
63
+
64
+ // plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components types
65
+ // are inlined at build — see plugin-ui/build-dist.mjs), so this one file is
66
+ // enough even in an external install.
67
+ async function reactDtsPath() {
68
+ const entryUrl = await import.meta.resolve("@rainbow-robotics/plugin-ui/react");
69
+ const dtsPath = fileURLToPath(entryUrl).replace(/\.js$/, ".d.ts");
70
+ if (!existsSync(dtsPath)) {
71
+ throw new Error(
72
+ `plugin-ui type declarations not found at ${dtsPath} — reinstall or rebuild @rainbow-robotics/plugin-ui.`,
73
+ );
74
+ }
75
+ return dtsPath;
76
+ }
77
+
78
+ function loadReactTypes(dtsPath) {
79
+ // Without moduleResolution the `import ... from "react"` inside react.d.ts
80
+ // never resolves and every React-derived type collapses to `any`.
81
+ const program = ts.createProgram([dtsPath], {
82
+ target: ts.ScriptTarget.ES2022,
83
+ module: ts.ModuleKind.ESNext,
84
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
85
+ skipLibCheck: true,
86
+ });
87
+ const sourceFile = program.getSourceFile(dtsPath);
88
+ const checker = program.getTypeChecker();
89
+ const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
90
+ const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
91
+ return { checker, sourceFile, exports };
92
+ }
93
+
94
+ // The barrel's trailing `export { A, B, ... }` produces Alias symbols whose
95
+ // OWN flags don't reflect value-vs-type — resolve to the real declaration first.
96
+ function resolveExport(checker, symbol) {
97
+ return symbol.flags & ts.SymbolFlags.Alias
98
+ ? checker.getAliasedSymbol(symbol)
99
+ : symbol;
100
+ }
101
+
102
+ function exportKind(checker, symbol) {
103
+ const resolved = resolveExport(checker, symbol);
104
+ return resolved.flags & ts.SymbolFlags.Value ? "value" : "type";
105
+ }
106
+
107
+ // typeToString alone often prints an opaque alias (`ForwardRefExoticComponent<PropsType$s>`).
108
+ // The first call signature works for plain components and forwardRef alike; its
109
+ // first parameter is the props object.
110
+ function ownProps(checker, type, sourceFile) {
111
+ const [signature] = type.getCallSignatures?.() ?? [];
112
+ const param = signature?.getParameters?.()[0];
113
+ if (!param) return [];
114
+ const declaration = param.valueDeclaration ?? param.declarations?.[0];
115
+ if (!declaration) return [];
116
+ const propsType = checker.getTypeOfSymbolAtLocation(param, declaration);
117
+
118
+ return checker
119
+ .getPropertiesOfType(propsType)
120
+ // Without this, ~220 inherited React DOM attributes bury the component's own API.
121
+ // Own = declared in react.d.ts itself (rb-components types are inlined there). Not a
122
+ // "node_modules" path test: in a plugin project react.d.ts itself lives in node_modules.
123
+ .filter((prop) => (prop.declarations ?? []).some((d) => d.getSourceFile() === sourceFile))
124
+ .map((prop) => {
125
+ const d = prop.valueDeclaration ?? prop.declarations[0];
126
+ const optional = prop.flags & ts.SymbolFlags.Optional ? "?" : "";
127
+ return `${prop.getName()}${optional}: ${checker.typeToString(
128
+ checker.getTypeOfSymbolAtLocation(prop, d),
129
+ )}`;
130
+ });
131
+ }
132
+
133
+ // Kept out of the tool handler so every export can be precomputed and cached.
134
+ function propsText(checker, sourceFile, name, symbol) {
135
+ const resolved = resolveExport(checker, symbol);
136
+ const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
137
+ const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
138
+ const signature = checker.typeToString(
139
+ type,
140
+ undefined,
141
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
142
+ );
143
+
144
+ const props = ownProps(checker, type, sourceFile);
145
+ if (props.length) {
146
+ // Keep the signature line: RBSlider's value/min/max come from Radix and the
147
+ // own-props filter drops them.
148
+ return `# ${name} — own props\n${props.sort().join("\n")}\n\nfull type: ${signature}`;
149
+ }
150
+ // No own props means either a Radix wrapper or unresolved React types. The two
151
+ // look identical, so name the second case explicitly.
152
+ if (/<any>/.test(signature)) {
153
+ return (
154
+ `# ${name} — prop types could not be resolved\n${signature}\n\n` +
155
+ "React's types didn't resolve, so this collapsed to `any`. Install `@types/react` " +
156
+ "in this project (`npm i -D @types/react`) and retry for the real props."
157
+ );
158
+ }
159
+ return (
160
+ `# ${name} — full type (no plugin-ui-specific props; it forwards a third-party ` +
161
+ `component's props)\n${signature}`
162
+ );
163
+ }
164
+
165
+ // Rendering every export up front is what makes the result cacheable: once the
166
+ // text exists no TypeChecker is needed.
167
+ function buildUiIndex(dtsPath) {
168
+ const { checker, sourceFile, exports } = loadReactTypes(dtsPath);
169
+ const props = {};
170
+ for (const symbol of exports) {
171
+ props[symbol.getName()] = propsText(checker, sourceFile, symbol.getName(), symbol);
172
+ }
173
+ return {
174
+ components: exports
175
+ .map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
176
+ .sort()
177
+ .join("\n"),
178
+ props,
179
+ };
180
+ }
181
+
182
+ // ts.createProgram pulls in ~1100 source files (~1.5s) and an MCP server is a fresh
183
+ // process per session. The key covers the .d.ts identity and the kit version.
184
+ const UI_INDEX_CACHE_DIR = join(homedir(), ".cache", "muscat-pk");
185
+ const UI_INDEX_CACHE = join(UI_INDEX_CACHE_DIR, "ui-index.json");
186
+ const UI_INDEX_FORMAT = 2;
187
+
188
+ let uiIndexCache = null;
189
+
190
+ async function loadUiIndex() {
191
+ if (uiIndexCache) return uiIndexCache;
192
+ const dtsPath = await reactDtsPath();
193
+ const { mtimeMs, size } = statSync(dtsPath);
194
+ // Bump UI_INDEX_FORMAT when the rendered text changes, so dev builds (same version) rebuild too.
195
+ const key = `${UI_INDEX_FORMAT}|${dtsPath}|${mtimeMs}|${size}|${kitVersion()}`;
196
+
197
+ try {
198
+ const cached = JSON.parse(readFileSync(UI_INDEX_CACHE, "utf8"));
199
+ // A half-written file still parses as JSON, so check the shape too.
200
+ if (
201
+ cached.key === key &&
202
+ typeof cached.components === "string" &&
203
+ cached.props &&
204
+ typeof cached.props === "object"
205
+ ) {
206
+ return (uiIndexCache = cached);
207
+ }
208
+ } catch {
209
+ /* absent, unreadable or corrupt — rebuild below */
210
+ }
211
+
212
+ uiIndexCache = { key, ...buildUiIndex(dtsPath) };
213
+ try {
214
+ mkdirSync(UI_INDEX_CACHE_DIR, { recursive: true });
215
+ // rename is atomic, so a concurrent server never reads a half-written file.
216
+ const tmp = `${UI_INDEX_CACHE}.${process.pid}.tmp`;
217
+ writeFileSync(tmp, JSON.stringify(uiIndexCache));
218
+ renameSync(tmp, UI_INDEX_CACHE);
219
+ } catch {
220
+ /* read-only fs / sandbox: the cache is an optimisation, never a requirement */
221
+ }
222
+ return uiIndexCache;
223
+ }
224
+
225
+ /* -------------------------------------------------------------------------- */
226
+ /* Storybook story source lookup */
227
+ /* -------------------------------------------------------------------------- */
228
+
229
+ // Case-insensitive recursive search for `${name}.stories.tsx` under the
230
+ // staged story sources (see storybook.mjs's buildStorybook()).
231
+ function findStorySource(name) {
232
+ if (!existsSync(SHIPPED_STORIES_SRC)) return null;
233
+ const target = `${name.toLowerCase()}.stories.tsx`;
234
+ const stack = [SHIPPED_STORIES_SRC];
235
+ while (stack.length) {
236
+ const dir = stack.pop();
237
+ for (const entry of readdirSync(dir)) {
238
+ const full = join(dir, entry);
239
+ if (statSync(full).isDirectory()) {
240
+ stack.push(full);
241
+ continue;
242
+ }
243
+ if (entry.toLowerCase() === target) return readFileSync(full, "utf8");
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+
249
+ // Stories are written against the monorepo (@repo/rb-components…). Point those imports at the
250
+ // package a plugin actually has, and flag names plugin-ui does not re-export.
251
+ function rewriteStoryImports(source, exportNames) {
252
+ const exported = new Set(exportNames);
253
+ const missing = new Set();
254
+ const text = source.replace(
255
+ /import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']@repo\/rb-components[^"']*["'];?/g,
256
+ (_, typeOnly = "", names) => {
257
+ const list = names.split(",").map((n) => n.trim()).filter(Boolean);
258
+ for (const n of list) if (!exported.has(n.replace(/^type\s+/, "").split(/\s+as\s+/)[0])) missing.add(n);
259
+ return `import ${typeOnly}{ ${list.join(", ")} } from "@rainbow-robotics/plugin-ui/react";`;
260
+ },
261
+ );
262
+ const notes = [
263
+ "// Storybook story source. In a plugin, import components from \"@rainbow-robotics/plugin-ui/react\"",
264
+ "// (rewritten below); @storybook/* imports, Meta/StoryObj and play() are Storybook-only.",
265
+ ...(missing.size ? [`// Not exported by plugin-ui, do not use in a plugin: ${[...missing].join(", ")}`] : []),
266
+ ];
267
+ return `${notes.join("\n")}\n\n${text}`;
268
+ }
269
+
270
+ const editDistance = (a, b) => {
271
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
272
+ for (let i = 1; i <= a.length; i++) {
273
+ let prev = row[0]++;
274
+ for (let j = 1; j <= b.length; j++) {
275
+ const cur = row[j];
276
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
277
+ prev = cur;
278
+ }
279
+ }
280
+ return row[b.length];
281
+ };
282
+
283
+ // "RBSelect" → RBSelectBox, RBSelectOption, …; "select" (a native element) → its plugin-ui replacement.
284
+ function suggestExports(name, exportNames) {
285
+ const q = name.toLowerCase().replace(/[<>]/g, "");
286
+ const native = NATIVE_TO_RB.filter(([el]) => el.split(/[\s/=]+/).includes(q)).map(([, rb]) => rb);
287
+ const bare = q.replace(/^rb/, "");
288
+ const close = exportNames
289
+ .map((n) => {
290
+ const lower = n.toLowerCase();
291
+ const score = lower.includes(q) || (bare.length > 2 && lower.includes(bare)) ? 0 : editDistance(q, lower);
292
+ return { n, score };
293
+ })
294
+ .filter((x) => x.score <= 3)
295
+ .sort((a, b) => a.score - b.score || a.n.length - b.n.length)
296
+ .slice(0, 6)
297
+ .map((x) => x.n);
298
+ return [...native, ...close];
299
+ }
300
+
301
+ function unknownExportText(name, exportNames, what) {
302
+ const hints = suggestExports(name, exportNames);
303
+ return (
304
+ `No ${what} named "${name}" in @rainbow-robotics/plugin-ui/react.` +
305
+ (hints.length ? ` Did you mean: ${hints.join(", ")}?` : "") +
306
+ " Call list_components for valid names. Do not fall back to a native HTML element."
307
+ );
308
+ }
309
+
310
+ /* -------------------------------------------------------------------------- */
311
+ /* README search */
312
+ /* -------------------------------------------------------------------------- */
313
+
314
+ // Split the README on headings and keyword-score each section.
315
+ // Headings inside fenced code blocks (```…```) don't count — a column-0
316
+ // Python comment would otherwise start a bogus section.
317
+ function splitSections(text) {
318
+ const sections = [];
319
+ let current = [];
320
+ let inFence = false;
321
+ for (const line of text.split("\n")) {
322
+ if (/^\s*```/.test(line)) inFence = !inFence;
323
+ if (!inFence && /^#{1,4} /.test(line) && current.length) {
324
+ sections.push(current.join("\n"));
325
+ current = [];
326
+ }
327
+ current.push(line);
328
+ }
329
+ if (current.length) sections.push(current.join("\n"));
330
+ return sections;
331
+ }
332
+
333
+ // Standalone binaries have no templates/ on disk.
334
+ async function readTemplateReadme(rel) {
335
+ const onDisk = join(kitRoot, "cli/templates", rel);
336
+ if (existsSync(onDisk)) return readFileSync(onDisk, "utf8");
337
+ try {
338
+ const { TEMPLATES } = await import("./templates-embedded.mjs");
339
+ return TEMPLATES[rel] ? Buffer.from(TEMPLATES[rel], "base64").toString("utf8") : "";
340
+ } catch {
341
+ return "";
342
+ }
343
+ }
344
+
345
+ async function searchReadme(query, lang) {
346
+ const langs = lang ? [lang] : ["python", "cpp"];
347
+ const seen = new Set();
348
+ const sections = [];
349
+ for (const l of langs) {
350
+ for (const section of splitSections(await readTemplateReadme(TEMPLATE_READMES[l]))) {
351
+ if (seen.has(section)) continue; // UI/manifest sections are shared by both READMEs
352
+ seen.add(section);
353
+ sections.push(langs.length > 1 && l === "cpp" ? `[C++ backend]\n${section}` : section);
354
+ }
355
+ }
356
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
357
+
358
+ return sections
359
+ .map((section) => {
360
+ const lower = section.toLowerCase();
361
+ const score = terms.reduce((n, term) => n + (lower.includes(term) ? 1 : 0), 0);
362
+ return { section: section.trim(), score };
363
+ })
364
+ .filter((x) => x.score > 0)
365
+ .sort((a, b) => b.score - a.score)
366
+ .slice(0, 5)
367
+ .map((x) => x.section);
368
+ }
369
+
370
+ /* -------------------------------------------------------------------------- */
371
+ /* Design tokens — parsed from rb-styles' generated rb-token.css */
372
+ /* -------------------------------------------------------------------------- */
373
+
374
+ // A color var `--color-<name>` becomes Tailwind utilities `bg-<name>`,
375
+ // `text-<name>`, `border-<name>`, `fill-<name>`. The design system's naming
376
+ // already encodes intent (bg-/fg-/border-/fill-), so we surface the idiomatic
377
+ // class per group.
378
+ const COLOR_CLASS_HINT = {
379
+ bg: (n) => `bg-${n}`, // --color-bg-default → bg-bg-default
380
+ fg: (n) => `text-${n}`, // --color-fg-subtle → text-fg-subtle
381
+ border: (n) => `border-${n}`,
382
+ fill: (n) => `fill-${n}`,
383
+ };
384
+
385
+ let tokensCache = null;
386
+
387
+ function loadDesignTokens() {
388
+ if (tokensCache) return tokensCache;
389
+ if (!existsSync(DESIGN_TOKENS_CSS)) {
390
+ throw new Error(
391
+ `design token CSS not found at ${DESIGN_TOKENS_CSS} — rebuild via "muscat-pk storybook --rebuild" (monorepo) or reinstall.`,
392
+ );
393
+ }
394
+ const css = readFileSync(DESIGN_TOKENS_CSS, "utf8");
395
+
396
+ // @theme { --name: value; ... } → { group, name, value, class? }
397
+ const vars = [];
398
+ for (const [, rawName, value] of css.matchAll(
399
+ /^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);/gim,
400
+ )) {
401
+ const name = rawName.replace(/^--/, "");
402
+ const m = name.match(/^color-(bg|fg|border|fill)-(.+)$/);
403
+ const token = { name, value: value.trim() };
404
+ if (m) {
405
+ token.group = `color-${m[1]}`;
406
+ token.class = COLOR_CLASS_HINT[m[1]](`${m[1]}-${m[2]}`);
407
+ } else {
408
+ token.group = name.split("-").slice(0, 2).join("-");
409
+ }
410
+ vars.push(token);
411
+ }
412
+
413
+ // @utility typo-… → composite typography classes used directly in className.
414
+ const typo = [
415
+ ...new Set(
416
+ [...css.matchAll(/@utility\s+(typo-[a-z0-9-]+)\s*\{/g)].map((m) => m[1]),
417
+ ),
418
+ ].sort();
419
+
420
+ tokensCache = { vars, typo };
421
+ return tokensCache;
422
+ }
423
+
424
+ /* -------------------------------------------------------------------------- */
425
+ /* rainbow-rb-sdk (Python) introspection */
426
+ /* -------------------------------------------------------------------------- */
427
+
428
+ function pythonCmd() {
429
+ // Prefer the current plugin's own .venv — that's where `muscat-pk new` /
430
+ // ensureIdeVenv() installs rainbow-rb-sdk from requirements.txt. The AI
431
+ // agent's cwd is usually the plugin dir mcp was launched from.
432
+ try {
433
+ const py = venvPython(resolve(findPluginRoot(), ".venv"));
434
+ if (existsSync(py)) return py;
435
+ } catch {
436
+ /* not inside a plugin project — fall through to PATH lookup */
437
+ }
438
+ for (const c of ["python3", "python"]) {
439
+ const r = spawnSync(c, ["--version"], { encoding: "utf8" });
440
+ if (r.status === 0) return c;
441
+ }
442
+ return null;
443
+ }
444
+
445
+ // Run an introspection script against the installed rainbow.rb_sdk (a plugin's
446
+ // backend dependency). Returns a clear hint if it isn't importable.
447
+ function introspectRbSdk(script) {
448
+ const py = pythonCmd();
449
+ const probe = `
450
+ import json, importlib
451
+ try:
452
+ importlib.import_module("rainbow.rb_sdk")
453
+ except Exception as e:
454
+ print(json.dumps({"__notinstalled__": str(e)})); raise SystemExit(0)
455
+ ${script}
456
+ `;
457
+ if (!py) {
458
+ return introspectInDocker(probe) ?? NO_DEV_IMAGE;
459
+ }
460
+ const r = spawnSync(py, ["-c", probe], { encoding: "utf8", maxBuffer: 8 << 20 });
461
+ if (r.status !== 0) {
462
+ return { error: `python introspection failed: ${(r.stderr || "").trim().slice(0, 500)}` };
463
+ }
464
+ const local = parseProbe(r);
465
+ return local.error && local.notInstalled ? introspectInDocker(probe) ?? NO_DEV_IMAGE : local;
466
+ }
467
+
468
+ // The SDK isn't importable here (macOS has no wheels) and the image to read it in isn't built yet.
469
+ const NO_DEV_IMAGE = {
470
+ error:
471
+ "rainbow-rb-sdk can't be read on this machine yet: it isn't importable from the plugin's .venv " +
472
+ `(macOS has no SDK wheels) and the ${PYTHON_DEV_IMAGE} Docker image isn't built. Run \`muscat-pk dev:back\` ` +
473
+ "once in a python plugin (it builds the image), then retry.",
474
+ };
475
+
476
+ // No macOS wheels: import the plugin's pinned SDK inside the python dev image (cached).
477
+ function introspectInDocker(probe) {
478
+ if (spawnSync("docker", ["image", "inspect", PYTHON_DEV_IMAGE], { encoding: "utf8" }).status !== 0) return null;
479
+ let ver = "";
480
+ try {
481
+ const req = readFileSync(join(findPluginRoot(), "requirements.txt"), "utf8");
482
+ ver = req.match(/^rainbow-rb-sdk==(\S+)/m)?.[1] ?? "";
483
+ } catch {
484
+ /* not a python plugin — latest */
485
+ }
486
+ // ponytail: "latest" cache is never refreshed; delete ~/.cache/... rb-sdk-py/latest to update.
487
+ const cacheRoot = join(UI_INDEX_CACHE_DIR, "rb-sdk-py");
488
+ const cache = join(cacheRoot, ver || "latest");
489
+ // Only the SDK version in use is kept (~165MB each) — other versions don't pile up.
490
+ for (const old of existsSync(cacheRoot) ? readdirSync(cacheRoot) : []) {
491
+ if (join(cacheRoot, old) !== cache) removeCache(join(cacheRoot, old));
492
+ }
493
+ mkdirSync(cache, { recursive: true });
494
+ const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
495
+ const script =
496
+ `[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
497
+ `PYTHONPATH=/c python3 -c "$PROBE"`;
498
+ const r = spawnSync("docker", ["run", "--rm", ...dockerUserFlags(), "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
499
+ encoding: "utf8",
500
+ maxBuffer: 8 << 20,
501
+ env: { ...process.env, PROBE: probe },
502
+ });
503
+ if (r.status !== 0) return { error: `python introspection (docker) failed: ${(r.stderr || "").trim().slice(-500)}` };
504
+ return parseProbe(r);
505
+ }
506
+
507
+ // Caches written by older kits as root (Linux) can't be unlinked by the user — clear them as root in Docker.
508
+ function removeCache(dir) {
509
+ try {
510
+ rmSync(dir, { recursive: true, force: true });
511
+ } catch {
512
+ spawnSync("docker", ["run", "--rm", "-v", `${dir}:/c`, PYTHON_DEV_IMAGE, "find", "/c", "-mindepth", "1", "-delete"], { stdio: "ignore" });
513
+ try {
514
+ rmSync(dir, { recursive: true, force: true });
515
+ } catch {
516
+ /* left for a later run — must not block reading the SDK */
517
+ }
518
+ }
519
+ }
520
+
521
+ function parseProbe(r) {
522
+ // Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
523
+ // Zenoh client logs its connection endpoints), ahead of our own final
524
+ // print(json.dumps(...)) — so parse the last line, not the whole output.
525
+ const lines = r.stdout.trim().split("\n");
526
+ let data;
527
+ try {
528
+ data = JSON.parse(lines[lines.length - 1]);
529
+ } catch {
530
+ return { error: `unexpected introspection output: ${r.stdout.slice(0, 300)}` };
531
+ }
532
+ if (data && data.__notinstalled__) {
533
+ return {
534
+ notInstalled: true,
535
+ error:
536
+ "rainbow-rb-sdk is not installed in this environment. Install it with `pip install rainbow-rb-sdk` " +
537
+ "(it's already in a plugin's requirements.txt), then retry.",
538
+ };
539
+ }
540
+ return { data };
541
+ }
542
+
543
+ /* -------------------------------------------------------------------------- */
544
+ /* rainbow-rb-sdk-dev (C++) headers — copied out of the cpp dev image */
545
+ /* -------------------------------------------------------------------------- */
546
+
547
+ const CPP_INCLUDE_CACHE = join(UI_INDEX_CACHE_DIR, "rb-sdk-include");
548
+ // Headers we don't want to surface: third-party vendored code, pybind glue,
549
+ // flatbuffers-generated schemas, and the industrial comm stacks.
550
+ const CPP_SKIP = /^(vendor|binding|rb_comm_source)\/|_generated\.h$/;
551
+
552
+ // The apt package is only installed inside the muscat-pk-cpp-dev image, so
553
+ // copy /usr/include/rainbow/rb_sdk out once per image build (cache key =
554
+ // image id) with `docker cp` — no host tar needed.
555
+ function cppIncludeDir() {
556
+ const insp = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", CPP_DEV_IMAGE], { encoding: "utf8" });
557
+ if (insp.error) return { error: "Docker not found — the C++ SDK headers live in the muscat-pk cpp dev image." };
558
+ if (insp.status !== 0) {
559
+ return {
560
+ error:
561
+ `Docker image ${CPP_DEV_IMAGE} not built yet. Run \`muscat-pk dev:back\` (or \`muscat-pk build\`) once ` +
562
+ "in a cpp / rclcpp plugin to build it, then retry.",
563
+ };
564
+ }
565
+ const dir = join(CPP_INCLUDE_CACHE, insp.stdout.trim().replace(/^sha256:/, "").slice(0, 12));
566
+ const out = join(dir, "rb_sdk");
567
+ if (existsSync(out)) return { dir: out };
568
+ mkdirSync(dir, { recursive: true });
569
+ const ctr = spawnSync("docker", ["create", CPP_DEV_IMAGE], { encoding: "utf8" });
570
+ if (ctr.status !== 0) return { error: `docker create failed: ${(ctr.stderr || "").trim().slice(0, 300)}` };
571
+ const id = ctr.stdout.trim();
572
+ const cp = spawnSync("docker", ["cp", `${id}:/usr/include/rainbow/rb_sdk`, dir], { encoding: "utf8" });
573
+ spawnSync("docker", ["rm", "-f", id], { encoding: "utf8" });
574
+ if (cp.status !== 0 || !existsSync(out)) {
575
+ return { error: `docker cp failed: ${(cp.stderr || "").trim().slice(0, 300)}` };
576
+ }
577
+ return { dir: out };
578
+ }
579
+
580
+ function cppHeaders(root) {
581
+ const files = [];
582
+ const walk = (d, rel) => {
583
+ for (const name of readdirSync(d)) {
584
+ const r = rel ? `${rel}/${name}` : name;
585
+ if (CPP_SKIP.test(r)) continue;
586
+ const full = join(d, name);
587
+ if (statSync(full).isDirectory()) walk(full, r);
588
+ else if (/\.hpp$/.test(name)) files.push(r);
589
+ }
590
+ };
591
+ walk(root, "");
592
+ return files.sort();
593
+ }
594
+
595
+ // "class Foo {" / "class Foo : public Bar {" — not forward declarations.
596
+ const CLASS_RE = /^\s*(?:class|struct)\s+(?:[A-Z][A-Z0-9_]*_API\s+)?(\w+)\b(?![^\n]*;\s*$)/; // skip export macros (RB_SDK_API)
597
+
598
+ function cppClassesIn(text) {
599
+ return [...new Set(text.split("\n").map((l) => l.match(CLASS_RE)?.[1]).filter(Boolean))];
600
+ }
601
+
602
+ // Return the class body (brace-matched) plus the file's leading comment block,
603
+ // which in these headers usually carries a full usage example.
604
+ function cppExtract(text, className, method) {
605
+ const lines = text.split("\n");
606
+ const start = lines.findIndex((l) => CLASS_RE.test(l) && l.match(CLASS_RE)[1] === className);
607
+ if (start < 0) return null;
608
+ let depth = 0, end = start, seen = false;
609
+ for (let i = start; i < lines.length; i++) {
610
+ for (const ch of lines[i]) {
611
+ if (ch === "{") { depth++; seen = true; }
612
+ else if (ch === "}") depth--;
613
+ }
614
+ if (seen && depth === 0) { end = i; break; }
615
+ }
616
+ const body = lines.slice(start, end + 1);
617
+ if (method) {
618
+ // Declarations sit at class-member indent (<= 2 spaces) and the name is
619
+ // not preceded by `.`/`->`/`::` — that skips call sites inside bodies.
620
+ const re = new RegExp(`^ {0,2}[^(]*(?<![\\w.>:])${method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
621
+ const hits = [];
622
+ for (let i = 0; i < body.length; i++) {
623
+ if (!re.test(body[i]) || /^\s*\/\//.test(body[i])) continue;
624
+ let a = i;
625
+ while (a > 0 && /^\s*\/\//.test(body[a - 1])) a--;
626
+ let b = i;
627
+ while (b < body.length - 1 && !/[{;]/.test(body[b])) b++;
628
+ hits.push(body.slice(a, b + 1).join("\n"));
629
+ }
630
+ return hits.length ? hits.join("\n\n") : null;
631
+ }
632
+ const head = [];
633
+ for (const l of lines) {
634
+ if (/^\s*\/\//.test(l)) head.push(l);
635
+ else if (l.trim() === "" || /^#pragma/.test(l)) continue;
636
+ else break;
637
+ }
638
+ return (head.length > 3 ? head.join("\n") + "\n\n" : "") + body.join("\n");
639
+ }
640
+
641
+ function cppFind(className) {
642
+ const { dir, error } = cppIncludeDir();
643
+ if (error) return { error };
644
+ for (const rel of cppHeaders(dir)) {
645
+ const text = readFileSync(join(dir, rel), "utf8");
646
+ if (cppClassesIn(text).includes(className)) return { rel, text };
647
+ }
648
+ return { error: `No class "${className}" in the C++ SDK headers. Call list_rb_sdk with lang "cpp" for valid names.` };
649
+ }
650
+
651
+ /* -------------------------------------------------------------------------- */
652
+ /* client registration — `muscat-pk mcp --install <client>` */
653
+ /* -------------------------------------------------------------------------- */
654
+
655
+ // Claude Code needs no install step: `muscat-pk new` scaffolds a project-level
656
+ // .mcp.json. Codex only reads the global ~/.codex/config.toml, so we append
657
+ // the server block there (idempotent).
658
+ export function installMcp(target) {
659
+ if (target !== "codex") {
660
+ throw new Error(
661
+ `Unknown --install target ${JSON.stringify(target ?? "")} — supported: "codex". ` +
662
+ "(Claude Code works out of the box via the scaffolded .mcp.json.)",
663
+ );
664
+ }
665
+ const cfgDir = join(homedir(), ".codex");
666
+ const cfgPath = join(cfgDir, "config.toml");
667
+ const existing = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : "";
668
+ if (/^\s*\[mcp_servers\.muscat\]/m.test(existing)) {
669
+ console.log(`Codex already has the muscat MCP server registered (${cfgPath}) — nothing to do.`);
670
+ return;
671
+ }
672
+ // ponytail: Windows Codex spawns without a shell, so it needs the .cmd shim.
673
+ const command = process.platform === "win32" ? "muscat-pk.cmd" : "muscat-pk";
674
+ mkdirSync(cfgDir, { recursive: true });
675
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
676
+ appendFileSync(
677
+ cfgPath,
678
+ `${sep}[mcp_servers.muscat]\ncommand = "${command}"\nargs = ["mcp"]\n`,
679
+ );
680
+ console.log(`Registered the muscat MCP server for Codex → ${cfgPath}`);
681
+ console.log("Restart Codex (or start a new session) to pick it up.");
682
+ }
683
+
684
+ /* -------------------------------------------------------------------------- */
685
+ /* server */
686
+ /* -------------------------------------------------------------------------- */
687
+
688
+ export async function runMcp() {
689
+ const server = new McpServer(
690
+ { name: "muscat-plugin-kit", version: kitVersion() },
691
+ { instructions: SERVER_INSTRUCTIONS },
692
+ );
693
+
694
+ server.registerTool(
695
+ "list_components",
696
+ {
697
+ title: "List Muscat plugin UI components",
698
+ description:
699
+ "List every component, hook, and type exported from @rainbow-robotics/plugin-ui/react — " +
700
+ "the full UI surface available to Muscat plugin authors. Call this first before guessing a " +
701
+ "component name; plugin UI uses these instead of native <select>/<input>/<button> elements.",
702
+ inputSchema: {},
703
+ },
704
+ async () => {
705
+ const { components } = await loadUiIndex();
706
+ return { content: [{ type: "text", text: components }] };
707
+ },
708
+ );
709
+
710
+ server.registerTool(
711
+ "get_component_props",
712
+ {
713
+ title: "Get a component or hook's real type signature",
714
+ description:
715
+ "Get the full TypeScript type for a named export of @rainbow-robotics/plugin-ui/react " +
716
+ "(e.g. RBSlider, RBLabelButton, useRb, useRbFormData). Use list_components to find valid names.",
717
+ inputSchema: { name: z.string().describe('Exported name, e.g. "RBSlider"') },
718
+ },
719
+ async ({ name }) => {
720
+ const { props } = await loadUiIndex();
721
+ if (!Object.hasOwn(props, name)) {
722
+ return { isError: true, content: [{ type: "text", text: unknownExportText(name, Object.keys(props), "export") }] };
723
+ }
724
+ return { content: [{ type: "text", text: props[name] }] };
725
+ },
726
+ );
727
+
728
+ server.registerTool(
729
+ "get_component_example",
730
+ {
731
+ title: "Get a real Storybook usage example",
732
+ description:
733
+ "Get the real Storybook story source for a component (e.g. RBSlider) — actual working " +
734
+ "props, imports, and layout, not a guessed shape. Prefer this over get_component_props when " +
735
+ "you just need to know how to use a component, not its full type.",
736
+ // `component` accepted as an alias — agents often guess it over `name`.
737
+ inputSchema: {
738
+ name: z.string().optional().describe('Component name, e.g. "RBSlider"'),
739
+ component: z.string().optional().describe("Alias for `name`."),
740
+ },
741
+ },
742
+ async ({ name, component }) => {
743
+ const target = name ?? component;
744
+ if (!target) {
745
+ return {
746
+ isError: true,
747
+ content: [{ type: "text", text: 'Missing required argument: "name" (component name, e.g. "RBSlider").' }],
748
+ };
749
+ }
750
+ const exportNames = Object.keys((await loadUiIndex()).props);
751
+ const source = findStorySource(target);
752
+ if (!source) {
753
+ // An existing export without a story still has a type; an unknown name gets suggestions.
754
+ const text = exportNames.includes(target)
755
+ ? `No Storybook example for "${target}". Call get_component_props for its type, or run "muscat-pk storybook" to browse components live.`
756
+ : unknownExportText(target, exportNames, "component");
757
+ return { content: [{ type: "text", text }] };
758
+ }
759
+ return { content: [{ type: "text", text: rewriteStoryImports(source, exportNames) }] };
760
+ },
761
+ );
762
+
763
+ server.registerTool(
764
+ "search_rb_sdk",
765
+ {
766
+ title: "Search the Muscat plugin-author docs",
767
+ description:
768
+ "Search the Muscat plugin-author README for how to use useRb(), rb.pub/sub/action/serve, " +
769
+ "plugin.router (external endpoints: router.on/subscribe/emit exposed over REST/Socket.IO/TCP/" +
770
+ "MQTT/WebRTC at common/plugin/{plugin_id}/{path}, PluginRouteError, meta.transport/principal), " +
771
+ "rb.toast/alert/confirm/prompt, useRbFormData/useRbSave (save/restore form data through the " +
772
+ "host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
773
+ "names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
774
+ "set_variable/executor_state/check_stop), video streaming (MediaMTX: backend publishes RTSP to " +
775
+ "127.0.0.1:8554, UI plays WebRTC from :8889), ROS 2 topics (plugin namespace, rb.rosPeers to allow " +
776
+ "an external PC), contribution points, and backend runtimes " +
777
+ "(Python/C++/ROS2). Covers BOTH backends: the Python README (py/main.py, rainbow.rb_sdk) and the " +
778
+ "C++ README (cpp/main.cpp or an rclcpp node: rb_plugin::RBPluginCall serve/subscribe/publish/" +
779
+ "router(), rb_plugin::PluginProgramContext, RBManipulateSDK, CMake rb_sdk::rb_sdk). Pass lang " +
780
+ "\"python\" or \"cpp\" to match the plugin's manifest runtime (cpp or ros2+rclcpp → \"cpp\"); " +
781
+ "omit it to search both. IMPORTANT: " +
782
+ "the backend pattern depends on the contribution point — search 'which backend pattern' before " +
783
+ "writing the backend. Only a program.panel command uses PluginProgramContext; a toolbar/move.panel " +
784
+ "plugin controls the robot with the SDK (RBManipulateSDK/RBAmrSDK) directly.",
785
+ inputSchema: {
786
+ query: z.string().describe('Keywords, e.g. "save form data" or "rb.action"'),
787
+ lang: z.enum(["python", "cpp"]).optional().describe("Backend language; omit to search both"),
788
+ },
789
+ },
790
+ async ({ query, lang }) => {
791
+ const sections = await searchReadme(query, lang);
792
+ if (sections.length === 0) {
793
+ return { content: [{ type: "text", text: `No matching section found for "${query}".` }] };
794
+ }
795
+ return { content: [{ type: "text", text: sections.join("\n\n---\n\n") }] };
796
+ },
797
+ );
798
+
799
+ /* --- program commands / program API --- */
800
+
801
+ server.registerTool(
802
+ "list_program_commands",
803
+ {
804
+ title: "List built-in Muscat program commands",
805
+ description:
806
+ "List the built-in commands of the Muscat Program page (from nexus program.c.tsx): Wait, Repeat, " +
807
+ "Condition, Switch/Case, Thread, Script, MoveJ/MoveL + Point, Gripper, AMR Goal, ... with their " +
808
+ "category (common | manipulate | amr | rby1), funcName, default args and nesting rules " +
809
+ "(allowChildren/allowParents/notAllowChildren/requireChildren/requireParent). Pass `command` for one " +
810
+ "command's full defaults. Use it to build steps for rb.program.setSteps or POST /common/program/steps/upsert " +
811
+ "(see get_program_api), or to fill a program.panel commandInfo's allowParents/allowChildren with real names. " +
812
+ "Values in <angle brackets> are computed when the step is added (live robot state, ids).",
813
+ inputSchema: {
814
+ command: z.string().optional().describe('Command name, e.g. "Point" or "Repeat"'),
815
+ category: z.enum(["common", "manipulate", "amr", "rby1"]).optional().describe("Filter by robot category"),
816
+ },
817
+ },
818
+ async ({ command, category }) => {
819
+ const all = loadProgramCommands();
820
+ if (command) {
821
+ const found = all.find((c) => c.command.toLowerCase() === command.toLowerCase());
822
+ if (!found) {
823
+ const hints = suggestExports(command, all.map((c) => c.command));
824
+ const text = `No program command "${command}".${hints.length ? ` Did you mean: ${hints.join(", ")}?` : ""} Call list_program_commands without command for the list.`;
825
+ return { isError: true, content: [{ type: "text", text }] };
826
+ }
827
+ return { content: [{ type: "text", text: JSON.stringify(found, null, 2) }] };
828
+ }
829
+ const lines = all
830
+ .filter((c) => !category || c.category === category)
831
+ .map((c) => {
832
+ const r = c.rules;
833
+ const notes = [
834
+ c.funcName && `func ${c.funcName}${c.postFuncName ? ` → ${c.postFuncName}` : ""}`,
835
+ r.allowChildren && `children: ${r.allowChildren.join(",")}`,
836
+ r.allowParents && `parents: ${r.allowParents.join(",")}`,
837
+ r.requireChildren && "needs children",
838
+ r.requireParent && "needs parent",
839
+ r.notAllowChildren && `no children: ${r.notAllowChildren.join(",")}`,
840
+ r.disabled === true && "disabled",
841
+ c.computedDefaults && "computed defaults",
842
+ c.asksOnInsert && "asks on insert",
843
+ c.defaultArgs && `args: ${Object.keys(c.defaultArgs).join(",")}`,
844
+ ].filter(Boolean);
845
+ return `${c.command} [${c.category}] ${c.label}${notes.length ? ` — ${notes.join("; ")}` : ""}`;
846
+ });
847
+ return { content: [{ type: "text", text: `${lines.join("\n")}\n\nCall again with command for full default args.` }] };
848
+ },
849
+ );
850
+
851
+ server.registerTool(
852
+ "get_program_api",
853
+ {
854
+ title: "How to build and run a Muscat program from a plugin",
855
+ description:
856
+ "How a plugin creates, edits, saves and runs Muscat programs: rb.program.getCatalog/getSteps/setSteps/" +
857
+ "getCommandDefaults from plugin UI, and the common REST API (/common/program/create, /program/steps/upsert " +
858
+ "step fields and ordering, load/start/stop, export/import). Without `path` returns the guide plus the live " +
859
+ "/program endpoint list; with `path` (e.g. \"/program/steps/upsert\") returns that endpoint's request/response " +
860
+ "schema from the running Muscat's OpenAPI (default http://localhost:3000; pass `url` for a robot).",
861
+ inputSchema: {
862
+ path: z.string().optional().describe('Endpoint path, e.g. "/program/steps/upsert"'),
863
+ url: z.string().optional().describe('Muscat base URL, default "http://localhost:3000"'),
864
+ },
865
+ },
866
+ async ({ path, url }) => {
867
+ const live = await describeProgramApi(path, url);
868
+ const text = path ? live : `${PROGRAM_API_GUIDE}\n## Live /program endpoints\n\n${live}`;
869
+ return { content: [{ type: "text", text }] };
870
+ },
871
+ );
872
+
873
+ /* --- design tokens --- */
874
+
875
+ server.registerTool(
876
+ "list_design_tokens",
877
+ {
878
+ title: "List Muscat design tokens (Tailwind classes)",
879
+ description:
880
+ "List the design system's tokens as the Tailwind classes to use in a plugin's className: " +
881
+ "semantic colors (bg-bg-*, text-fg-*, border-border-*, fill-fill-*), typography (typo-*), " +
882
+ "shadows, radius, spacing. Use these instead of raw hex/px so the plugin matches the host theme " +
883
+ "(and light/dark). Optionally filter by category.",
884
+ inputSchema: {
885
+ category: z
886
+ .enum(["bg", "fg", "border", "fill", "typography", "shadow", "radius", "spacing", "all"])
887
+ .optional()
888
+ .describe('Filter, e.g. "bg", "fg", "typography". Omit for a grouped overview.'),
889
+ },
890
+ },
891
+ async ({ category }) => {
892
+ const { vars, typo } = loadDesignTokens();
893
+ const out = [];
894
+
895
+ const colorGroups = { bg: [], fg: [], border: [], fill: [] };
896
+ const other = { shadow: [], radius: [], spacing: [] };
897
+ for (const t of vars) {
898
+ const m = t.group?.match(/^color-(bg|fg|border|fill)$/);
899
+ // Class name only: the value is just an indirection to another CSS var
900
+ // (var(--color-scale-zinc-200)), and each class is declared twice (light/dark)
901
+ // with different values — dropping the value lets the Set dedup collapse them.
902
+ if (m && t.class) colorGroups[m[1]].push(t.class);
903
+ else if (t.name.startsWith("shadow-")) other.shadow.push(`shadow-${t.name.slice(7)}`);
904
+ else if (t.name.startsWith("radius-")) other.radius.push(`rounded-${t.name.slice(7)}`);
905
+ else if (t.name.startsWith("spacing-")) other.spacing.push(t.name);
906
+ }
907
+
908
+ const want = (c) => !category || category === "all" || category === c;
909
+ const push = (title, items) => {
910
+ if (items.length) out.push(`## ${title}\n${[...new Set(items)].sort().join("\n")}`);
911
+ };
912
+ if (want("bg")) push("Background — bg-bg-*", colorGroups.bg);
913
+ if (want("fg")) push("Text/foreground — text-fg-*", colorGroups.fg);
914
+ if (want("border")) push("Border — border-border-*", colorGroups.border);
915
+ if (want("fill")) push("SVG fill — fill-fill-*", colorGroups.fill);
916
+ if (want("typography")) push("Typography — typo-* (use directly)", typo);
917
+ if (want("shadow")) push("Shadow", other.shadow);
918
+ if (want("radius")) push("Radius", other.radius);
919
+ if (want("spacing")) push("Spacing scale (p-/m-/gap-/w-/h-)", other.spacing);
920
+
921
+ return {
922
+ content: [
923
+ {
924
+ type: "text",
925
+ text: out.join("\n\n") || `No tokens for category "${category}".`,
926
+ },
927
+ ],
928
+ };
929
+ },
930
+ );
931
+
932
+ server.registerTool(
933
+ "search_design_tokens",
934
+ {
935
+ title: "Search design tokens by name or value",
936
+ description:
937
+ "Find a design token by keyword — e.g. 'danger' (error colors), 'elevated', 'brand', or a hex " +
938
+ "like '#fb2c36'. Returns the matching Tailwind class + value.",
939
+ inputSchema: { query: z.string().describe('e.g. "danger", "elevated", "#fb2c36"') },
940
+ },
941
+ async ({ query }) => {
942
+ const { vars, typo } = loadDesignTokens();
943
+ const q = query.toLowerCase();
944
+ const hits = vars
945
+ .filter((t) => t.name.toLowerCase().includes(q) || t.value.toLowerCase().includes(q))
946
+ .map((t) => `${t.class ?? t.name} (${t.value})`);
947
+ const typoHits = typo.filter((c) => c.toLowerCase().includes(q));
948
+ const all = [...typoHits, ...hits];
949
+ return {
950
+ content: [
951
+ {
952
+ type: "text",
953
+ text: all.length ? [...new Set(all)].slice(0, 60).join("\n") : `No token matches "${query}".`,
954
+ },
955
+ ],
956
+ };
957
+ },
958
+ );
959
+
960
+ /* --- rainbow-rb-sdk (Python backend) --- */
961
+
962
+ server.registerTool(
963
+ "list_rb_sdk",
964
+ {
965
+ title: "List rainbow-rb-sdk classes and functions (Python or C++)",
966
+ description:
967
+ "List the public classes and functions of the plugin BACKEND SDK. lang \"python\" (default): the " +
968
+ "installed rainbow.rb_sdk package (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, " +
969
+ "...) — call before writing py/main.py; requires rainbow-rb-sdk to be pip-installed. lang \"cpp\": " +
970
+ "the C++ SDK headers (apt rainbow-rb-sdk-dev: RBManipulateSDK, rb_plugin::PluginProgramContext, " +
971
+ "...) grouped by header path — call before writing cpp/main.cpp or an rclcpp node; requires the " +
972
+ "muscat-pk-cpp-dev Docker image (built by `muscat-pk dev:back`). RBPluginCall (UI pub/sub/serve, " +
973
+ "router) exists in both: Python rainbow.rb_sdk.RBPluginCall, C++ rb_plugin::RBPluginCall " +
974
+ "(plugin/rb_plugin.hpp).",
975
+ inputSchema: { lang: z.enum(["python", "cpp"]).optional().describe('Default "python"') },
976
+ },
977
+ async ({ lang }) => {
978
+ if (lang === "cpp") {
979
+ const { dir, error } = cppIncludeDir();
980
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
981
+ const out = [];
982
+ for (const rel of cppHeaders(dir)) {
983
+ const classes = cppClassesIn(readFileSync(join(dir, rel), "utf8"));
984
+ if (classes.length) out.push(`${rel}: ${classes.join(", ")}`);
985
+ }
986
+ return { content: [{ type: "text", text: `#include <rainbow/rb_sdk/...>\n\n${out.join("\n")}` }] };
987
+ }
988
+ const { data, error } = introspectRbSdk(`
989
+ import inspect, json
990
+ mod = importlib.import_module("rainbow.rb_sdk")
991
+ out = []
992
+ for name in sorted(getattr(mod, "__all__", None) or dir(mod)):
993
+ if name.startswith("_"): continue
994
+ obj = getattr(mod, name)
995
+ if inspect.isclass(obj): out.append(name + " (class)")
996
+ elif inspect.isfunction(obj) or inspect.isbuiltin(obj): out.append(name + " (function)")
997
+ print(json.dumps(out))
998
+ `);
999
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
1000
+ return { content: [{ type: "text", text: data.join("\n") }] };
1001
+ },
1002
+ );
1003
+
1004
+ server.registerTool(
1005
+ "get_rb_sdk_api",
1006
+ {
1007
+ title: "Get a rainbow-rb-sdk class/function signature + docs (Python or C++)",
1008
+ description:
1009
+ "Get the signature(s) and docstring for a rainbow.rb_sdk class or function (e.g. " +
1010
+ "'PluginProgramContext', 'RBManipulateSDK'). For a class, lists its public methods with " +
1011
+ "signatures. To drill into ONE method of a class (e.g. RBManipulateSDK.call_move_j), pass " +
1012
+ "`method` alongside `name` — do not put both in `name`. lang \"python\" (default) introspects the " +
1013
+ "pip-installed package. lang \"cpp\" returns the class declaration from the C++ SDK header " +
1014
+ "(rainbow-rb-sdk-dev) with its header comment — usually a full usage example — and the " +
1015
+ "#include path; with `method`, just that method's declaration and comment.",
1016
+ inputSchema: {
1017
+ name: z.string().describe('Class or function name, e.g. "RBManipulateSDK"'),
1018
+ method: z.string().optional().describe('Optional: a specific method on that class, e.g. "call_move_j"'),
1019
+ lang: z.enum(["python", "cpp"]).optional().describe('Default "python"'),
1020
+ },
1021
+ },
1022
+ async ({ name, method, lang }) => {
1023
+ // Tolerate a combined "ClassName method_name" in `name` (a common agent
1024
+ // mistake) by splitting it ourselves instead of failing validation.
1025
+ let className = name;
1026
+ let methodName = method;
1027
+ if (!methodName && /\s/.test(name.trim())) {
1028
+ const parts = name.trim().split(/\s+/);
1029
+ if (parts.length === 2) [className, methodName] = parts;
1030
+ }
1031
+
1032
+ if (lang === "cpp") {
1033
+ const cls = className.replace(/^.*::/, "");
1034
+ let { rel, text, error } = cppFind(cls);
1035
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
1036
+ let body = cppExtract(text, cls, methodName);
1037
+ let owner = cls;
1038
+ if (!body && methodName) {
1039
+ // Like the Python path: most calls live on sub-SDK members
1040
+ // (RBManipulateMoveSDK move; -> sdk.move.call_move_j) or on the base
1041
+ // class — look there before giving up.
1042
+ const decl = cppExtract(text, cls);
1043
+ const cands = [
1044
+ ...[...decl.matchAll(/^\s*(\w+SDK)\s+(\w+);/gm)].map((m) => [m[1], `${cls}.${m[2]}`]),
1045
+ ...[...decl.matchAll(/:\s*public\s+(\w+)/g)].map((m) => [m[1], `${cls} (base ${m[1]})`]),
1046
+ ];
1047
+ for (const [c, label] of cands) {
1048
+ const f = cppFind(c);
1049
+ if (f.error) continue;
1050
+ const b = cppExtract(f.text, c, methodName);
1051
+ if (b) { ({ rel, text } = f); body = b; owner = label; break; }
1052
+ }
1053
+ }
1054
+ if (!body) {
1055
+ return {
1056
+ isError: true,
1057
+ content: [{ type: "text", text: `No method "${methodName}" declared in class "${cls}" (${rel}) or its sub-SDK members / base class. Call get_rb_sdk_api with just \`name\` to see the class.` }],
1058
+ };
1059
+ }
1060
+ const ns = text.match(/^namespace\s+(\w+)\s*\{/m)?.[1];
1061
+ const header =
1062
+ `// #include <rainbow/rb_sdk/${rel}>${ns ? ` (namespace ${ns})` : ""}\n` +
1063
+ `// link: find_package(rb_sdk) + rb_sdk::rb_sdk\n` +
1064
+ (methodName ? `// ${owner}.${methodName}\n` : "") + "\n";
1065
+ return { content: [{ type: "text", text: header + body.slice(0, 60_000) }] };
1066
+ }
1067
+
1068
+ const { data, error } = introspectRbSdk(`
1069
+ import inspect, json, re, sys as _sys
1070
+ mod = importlib.import_module("rainbow.rb_sdk")
1071
+ try:
1072
+ _cpp = importlib.import_module("rainbow.rb_sdk.rb_sdk_cpp") # C++ SDK (>= 0.10): sub-SDK classes live here
1073
+ except Exception:
1074
+ _cpp = None
1075
+ name = ${JSON.stringify(className)}
1076
+ method = ${methodName ? JSON.stringify(methodName) : "None"}
1077
+
1078
+ def _lookup(n, near=None):
1079
+ for m in (_sys.modules.get(getattr(near, "__module__", "")) if near else None, mod, _cpp):
1080
+ o = getattr(m, n, None) if m else None
1081
+ if o is not None:
1082
+ return o
1083
+ return None
1084
+
1085
+ obj = _lookup(name)
1086
+ if obj is None:
1087
+ print(json.dumps({"__missing__": True})); raise SystemExit(0)
1088
+
1089
+ def sig(o, n=""):
1090
+ try:
1091
+ return str(inspect.signature(o))
1092
+ except Exception:
1093
+ pass
1094
+ # pybind11 (C++ SDK): the signature is the docstring's first line.
1095
+ first = (getattr(o, "__doc__", None) or "").strip().split("\\n")[0]
1096
+ m = re.match(r"^\\w*(\\(.*\\))(\\s*->\\s*.+)?$", first)
1097
+ if not m:
1098
+ return "(...)"
1099
+ return re.sub(r"self(: [\\w.]+)?(, )?", "", m.group(1), count=1) + (m.group(2) or "")
1100
+
1101
+ def summary(o):
1102
+ lines = [l.strip() for l in (inspect.getdoc(o) or "").split("\\n") if l.strip()]
1103
+ if lines and re.match(r"^\\w*\\(.*\\)", lines[0]): # pybind signature line
1104
+ lines = lines[1:]
1105
+ return lines[0] if lines else ""
1106
+
1107
+ def routines(cls):
1108
+ for m in dir(cls):
1109
+ if m.startswith("_"):
1110
+ continue
1111
+ if isinstance(inspect.getattr_static(cls, m, None), property):
1112
+ continue
1113
+ fn = getattr(cls, m, None)
1114
+ if inspect.isroutine(fn):
1115
+ yield m, fn
1116
+
1117
+ def sub_sdks(cls):
1118
+ """(attr, class) for sub-SDKs: C++ properties (\`move\` -> RBManipulateMoveSDK)
1119
+ and pure-Python \`self.<attr> = <Class>(...)\` in __init__."""
1120
+ out = []
1121
+ for attr in dir(cls):
1122
+ p = inspect.getattr_static(cls, attr, None)
1123
+ if attr.startswith("_") or not isinstance(p, property) or p.fget is None:
1124
+ continue
1125
+ ret = (getattr(p.fget, "__annotations__", {}) or {}).get("return")
1126
+ tname = ret.__name__ if inspect.isclass(ret) else None
1127
+ if not tname:
1128
+ m = re.search(r"->\\s*([\\w.]+)", p.fget.__doc__ or "")
1129
+ tname = m and m.group(1).split(".")[-1]
1130
+ sub = _lookup(tname, cls) if tname else None
1131
+ if inspect.isclass(sub):
1132
+ out.append((attr, sub))
1133
+ try:
1134
+ src = inspect.getsource(cls.__init__)
1135
+ except Exception:
1136
+ src = ""
1137
+ for attr, nested_name in re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", src):
1138
+ sub = _lookup(nested_name, cls)
1139
+ if inspect.isclass(sub):
1140
+ out.append((attr, sub))
1141
+ return out
1142
+
1143
+ if method:
1144
+ fn = getattr(obj, method, None) if inspect.isclass(obj) else None
1145
+ owner = name
1146
+ if fn is None and inspect.isclass(obj):
1147
+ for attr, sub in sub_sdks(obj):
1148
+ cand = getattr(sub, method, None)
1149
+ if cand:
1150
+ fn, owner = cand, f"{name}.{attr} ({sub.__name__})"
1151
+ break
1152
+ if fn is None:
1153
+ print(json.dumps({"__missing__": True, "__method__": True})); raise SystemExit(0)
1154
+ print(json.dumps({
1155
+ "name": name, "kind": "method", "owner": owner,
1156
+ "signature": method + sig(fn), "doc": inspect.getdoc(fn) or "",
1157
+ }))
1158
+ raise SystemExit(0)
1159
+
1160
+ res = {"name": name, "doc": summary(obj) if inspect.isclass(obj) else (inspect.getdoc(obj) or "")}
1161
+ if inspect.isclass(obj):
1162
+ res["kind"] = "class"
1163
+ res["init"] = name + sig(obj.__init__ if "__init__" in dir(obj) else obj)
1164
+ methods = [{"name": m, "sig": m + sig(fn), "doc": summary(fn)} for m, fn in routines(obj)]
1165
+ # Most calls live on sub-SDKs (sdk.move.call_move_j) — list them name-prefixed.
1166
+ for attr, sub in sub_sdks(obj):
1167
+ methods += [{"name": f"{attr}.{m}", "sig": f"{attr}.{m}" + sig(fn), "doc": summary(fn)} for m, fn in routines(sub)]
1168
+ res["methods"] = methods
1169
+ else:
1170
+ res["kind"] = "function"
1171
+ res["signature"] = name + sig(obj)
1172
+ print(json.dumps(res))
1173
+ `);
1174
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
1175
+ if (data.__missing__) {
1176
+ const text = data.__method__
1177
+ ? `No method "${methodName}" found on "${className}" or its sub-SDKs (e.g. .move, .state). Call get_rb_sdk_api with just \`name: "${className}"\` to see ` +
1178
+ `its full method list and constructor first.`
1179
+ : `No "${className}" in rainbow.rb_sdk (or rb_sdk_cpp). Call list_rb_sdk for valid names.`;
1180
+ return { isError: true, content: [{ type: "text", text }] };
1181
+ }
1182
+ if (data.kind === "method") {
1183
+ const lines = [`# ${data.owner}.${data.signature.split("(")[0]}`, data.signature];
1184
+ if (data.doc) lines.push("", data.doc);
1185
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1186
+ }
1187
+ const lines = [`# ${data.name} (${data.kind})`];
1188
+ if (data.doc) lines.push(data.doc, "");
1189
+ if (data.kind === "class") {
1190
+ lines.push(`constructor: ${data.init}`, "", "methods:");
1191
+ for (const m of data.methods) {
1192
+ lines.push(` ${m.sig}${m.doc ? ` — ${m.doc}` : ""}`);
1193
+ }
1194
+ } else {
1195
+ lines.push(data.signature);
1196
+ }
1197
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1198
+ },
1199
+ );
1200
+
1201
+ server.registerTool(
1202
+ "check_project",
1203
+ {
1204
+ title: "Check the plugin project for changes the installed plugin-ui needs",
1205
+ description:
1206
+ "List what the plugin project must change to work with the plugin-ui it has installed: old APIs " +
1207
+ "(connect(App), Slider/Text/Switch…, rb.toast(message, type), a vite.config without the muscat() " +
1208
+ "preset, entries without a default export) and `tsc --noEmit` errors, each as file:line. Same check " +
1209
+ "as `muscat-pk update`. Call it after an update or when the UI fails to build, fix each item " +
1210
+ "(get_component_example for replacement components), and call it again until it reports no issues.",
1211
+ inputSchema: {
1212
+ path: z.string().optional().describe("Plugin project directory (default: the server's working directory)"),
1213
+ },
1214
+ },
1215
+ async ({ path }) => {
1216
+ let root;
1217
+ try {
1218
+ root = findPluginRoot(path ? resolve(path) : process.cwd());
1219
+ } catch (err) {
1220
+ return { isError: true, content: [{ type: "text", text: err.message }] };
1221
+ }
1222
+ const lines = formatProjectCheck(checkProject(root));
1223
+ const text = lines.length
1224
+ ? `${root}: ${lines.filter((l) => /:\d+ {2}/.test(l)).length} item(s) to fix\n${lines.join("\n")}`
1225
+ : `${root}: no issues — the project matches the installed plugin-ui.`;
1226
+ return { content: [{ type: "text", text }] };
1227
+ },
1228
+ );
1229
+
1230
+ const transport = new StdioServerTransport();
1231
+ await server.connect(transport);
1232
+ }