@rainbow-robotics/muscat-plugin-kit 1.10.1-dev.1 → 1.10.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.
- package/README.md +282 -282
- package/cli/README.md +274 -274
- package/cli/bin/create-muscat-plugin.mjs +8 -8
- package/cli/bin/muscat-pk.mjs +155 -155
- package/cli/bundle-templates.mjs +31 -31
- package/cli/docker/Dockerfile.cpp +28 -28
- package/cli/docker/Dockerfile.dev +30 -30
- package/cli/docker/Dockerfile.python +21 -21
- package/cli/m5-e2e.mjs +69 -69
- package/cli/prepack.mjs +10 -10
- package/cli/src/ai-rules.mjs +70 -70
- package/cli/src/build.mjs +447 -447
- package/cli/src/check-package.mjs +13 -13
- package/cli/src/completion.mjs +166 -166
- package/cli/src/cpp.mjs +78 -78
- package/cli/src/cppIde.mjs +75 -75
- package/cli/src/dev.mjs +165 -165
- package/cli/src/devBack.mjs +734 -727
- package/cli/src/devUi.mjs +247 -243
- package/cli/src/esbuild.mjs +56 -56
- package/cli/src/host_agent.py +223 -223
- package/cli/src/manifest.mjs +287 -287
- package/cli/src/mcp.mjs +1071 -1071
- package/cli/src/muscatLocal.mjs +209 -209
- package/cli/src/new.mjs +343 -343
- package/cli/src/pluginDevServer.mjs +111 -111
- package/cli/src/ports.mjs +193 -193
- package/cli/src/python.mjs +112 -112
- package/cli/src/ros2.mjs +109 -109
- package/cli/src/sdk.mjs +117 -117
- package/cli/src/storybook.mjs +168 -168
- package/cli/src/type.mjs +141 -141
- package/cli/src/uninstall.mjs +81 -81
- package/cli/src/update.mjs +295 -295
- package/cli/src/util.mjs +441 -441
- package/cli/src/zenohDev.mjs +83 -81
- package/cli/storybook-dist/_design-tokens.css +4957 -4957
- package/cli/storybook-dist/_stories-src/NxAvatar.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/NxPagination.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordion.stories.tsx +46 -46
- package/cli/storybook-dist/_stories-src/RBAccordion/RBAccordionGroup.stories.tsx +73 -73
- package/cli/storybook-dist/_stories-src/RBBadge/RBBadge.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBBattery/RBBattery.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckIconButton.stories.tsx +28 -28
- package/cli/storybook-dist/_stories-src/RBButton/RBCheckLabelButton.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBButton/RBIconButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBButton/RBInlineLabelButton.stories.tsx +41 -41
- package/cli/storybook-dist/_stories-src/RBButton/RBLabelButton.stories.tsx +60 -60
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBDateInput.stories.tsx +44 -44
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +56 -56
- package/cli/storybook-dist/_stories-src/RBDialog/RBAlert.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBDialog/RBConfirm.stories.tsx +55 -55
- package/cli/storybook-dist/_stories-src/RBDialog/RBPrompt.stories.tsx +52 -52
- package/cli/storybook-dist/_stories-src/RBDivider/RBDivider.stories.tsx +93 -93
- package/cli/storybook-dist/_stories-src/RBEmptyView/RBEmptyView.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBIcons/RBColorIcons.stories.tsx +108 -108
- package/cli/storybook-dist/_stories-src/RBIcons/RBIcons.stories.tsx +106 -106
- package/cli/storybook-dist/_stories-src/RBInput/RBCheckBox.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBInput/RBMultiRangeInput.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioBox.stories.tsx +65 -65
- package/cli/storybook-dist/_stories-src/RBInput/RBRadioButton.stories.tsx +68 -68
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +216 -216
- package/cli/storybook-dist/_stories-src/RBInput/RBSideNumberInput.stories.tsx +90 -90
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextArea.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/RBInput/RBText/RBTextInput.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/RBInput/RBTextField.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBJoyStick/RBJoyStick.stories.tsx +31 -31
- package/cli/storybook-dist/_stories-src/RBLight/RBLight.stories.tsx +36 -36
- package/cli/storybook-dist/_stories-src/RBPagination/RBPagination.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCard/RBCard.stories.tsx +89 -89
- package/cli/storybook-dist/_stories-src/RBShadcn/RBCommand/RBCommand.stories.tsx +116 -116
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDialog/RBDialog.stories.tsx +74 -74
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDrawer/RBDrawer.stories.tsx +99 -99
- package/cli/storybook-dist/_stories-src/RBShadcn/RBDropdownMenu/RBDropdownMenu.stories.tsx +123 -123
- package/cli/storybook-dist/_stories-src/RBShadcn/RBMenubar/RBMenubar.stories.tsx +135 -135
- package/cli/storybook-dist/_stories-src/RBShadcn/RBPopover/RBPopover.stories.tsx +69 -69
- package/cli/storybook-dist/_stories-src/RBShadcn/RBResizable/RBResizable.stories.tsx +49 -49
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSheet/RBSheet.stories.tsx +91 -91
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSlider/RBSlider.stories.tsx +38 -38
- package/cli/storybook-dist/_stories-src/RBShadcn/RBSwitch/RBSwitch.stories.tsx +112 -112
- package/cli/storybook-dist/_stories-src/RBShadcn/RBTabs/RBTabs.stories.tsx +75 -75
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggle/RBToggle.stories.tsx +51 -51
- package/cli/storybook-dist/_stories-src/RBShadcn/RBToggleGroup/RBToggleGroup.stories.tsx +84 -84
- package/cli/storybook-dist/_stories-src/RBTab/RBChipTab.stories.tsx +102 -102
- package/cli/storybook-dist/_stories-src/RBTab/RBChipsBoxTab.stories.tsx +78 -78
- package/cli/storybook-dist/_stories-src/RBTab/RBTab.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/RBTable/RBTable.stories.tsx +177 -177
- package/cli/storybook-dist/_stories-src/RBTable/RBTableRow.stories.tsx +64 -64
- package/cli/storybook-dist/_stories-src/RBToast/RBToast.stories.tsx +117 -117
- package/cli/storybook-dist/_stories-src/RBTooltip/RBTooltip.stories.tsx +50 -50
- package/cli/storybook-dist/_stories-src/loading/RBSpinner.stories.tsx +30 -30
- package/cli/storybook-dist/_stories-src/nx-button/NxButton.stories.tsx +54 -54
- package/cli/storybook-dist/_stories-src/nx-button/NxPointerHoldButton.stories.tsx +57 -57
- package/cli/storybook-dist/_stories-src/nx-icons/NxIcons.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxNumberInput.stories.tsx +103 -103
- package/cli/storybook-dist/_stories-src/nx-inputs/NxRangeInput.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSelectBox.stories.tsx +67 -67
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSlideRange.stories.tsx +45 -45
- package/cli/storybook-dist/_stories-src/nx-inputs/NxSwitch.stories.tsx +27 -27
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextArea.stories.tsx +48 -48
- package/cli/storybook-dist/_stories-src/nx-inputs/NxTextInput.stories.tsx +46 -46
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-D6JqZzDJ.js → Color-YHDXOIA2-B4IER17l.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-ESb7JAHg.js → Docs-C8GCHFtK.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-6gD2kUo1.js → Docs-CgdEMPQP.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-B9wd8Ok7.js → Docs-Ck3WHvk6.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CgFbWWXM.js → Docs-D9NHsoeZ.js} +2 -2
- package/cli/storybook-dist/assets/{Docs-DBQup8Bu.js → Docs-DNYQC7nl.js} +3 -3
- package/cli/storybook-dist/assets/{Docs-CMjovnSY.js → Docs-DUfeiM4I.js} +4 -4
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-BOUI1AHi.js → DocsRenderer-CFRXHY34-C0WhyDoR.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-DsYFMUh3.js → NxAvatar.stories-CwnK8AUF.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-CHfNcqsc.js → NxButton.stories-ueYdosfQ.js} +8 -8
- package/cli/storybook-dist/assets/{NxNumberInput.stories-BFw_4vFE.js → NxNumberInput.stories-M5ZCLbrK.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-DVfxyw4M.js → NxPagination.stories-bxxUnHEL.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-BzyxUWoZ.js → NxPointerHoldButton.stories-BHxB2kr5.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-DmRdzBc1.js → NxRangeInput.stories-BKfr-fp4.js} +6 -6
- package/cli/storybook-dist/assets/{NxSelectBox.stories-B1KkNUOf.js → NxSelectBox.stories-CG8FuTBq.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CyOdvfQ6.js → NxSlideRange.stories-BRTFtMTL.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-DrDBw0qp.js → NxSwitch.stories-Bwuyzn-R.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-DTB2aqNS.js → NxTextArea.stories-COq2sGqu.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-DU0rucmM.js → NxTextInput.stories-CjsBLz05.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-DTfOtXtp.js → RBAccordion.stories-BARwuZ0Q.js} +3 -3
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-9y9BjrUe.js → RBAccordionGroup.stories-COrPxETW.js} +10 -10
- package/cli/storybook-dist/assets/{RBAlert.stories-DLTAv9fT.js → RBAlert.stories-BQrkjZPt.js} +3 -3
- package/cli/storybook-dist/assets/{RBBadge.stories-BNMACewA.js → RBBadge.stories-BTsLL-lK.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-B3kXMAt5.js → RBBattery.stories-DAeQdJbc.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-Y-s8R2n7.js → RBCard.stories-HyhOE80R.js} +39 -39
- package/cli/storybook-dist/assets/{RBCheckBox.stories-DBuT9tL4.js → RBCheckBox.stories-KFk0WqX_.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-CWu1TL56.js → RBCheckIconButton.stories-CXbbktF5.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-sCu2MGCX.js → RBCheckLabelButton.stories-DWfYz_9Z.js} +17 -17
- package/cli/storybook-dist/assets/{RBChipTab.stories-BX9qLJkR.js → RBChipTab.stories-JPEgVFAS.js} +5 -5
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-35u1aJ-P.js → RBChipsBoxTab.stories-CTPuWXrQ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-DAOZ5Ies.js → RBColorIcons.stories-DiFRZ5jk.js} +1 -1
- package/cli/storybook-dist/assets/RBCommand.stories-Pf1HAVIR.js +73 -0
- package/cli/storybook-dist/assets/{RBConfirm.stories-iMXcUh7Z.js → RBConfirm.stories-D1cEPUXk.js} +3 -3
- package/cli/storybook-dist/assets/{RBDateInput.stories-hK5rXECf.js → RBDateInput.stories-RPYpCjig.js} +1 -1
- package/cli/storybook-dist/assets/RBDialog.stories-DJH-0XSR.js +38 -0
- package/cli/storybook-dist/assets/{RBDivider.stories-BAK-iZ1Z.js → RBDivider.stories-uEj1vOgK.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-B2nqf2Nh.js → RBDrawer.stories-BiMzElb2.js} +12 -12
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-Da_TBkla.js +86 -0
- package/cli/storybook-dist/assets/{RBEmptyView.stories-nCenrgBh.js → RBEmptyView.stories-BHeKGfPS.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-DRH7O3sp.js → RBIconButton.stories-NEsRFoYp.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-D3PyJXG8.js → RBIcons.stories-CcZ-TTLS.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-modQbt10.js → RBInlineLabelButton.stories-B0wpdQyR.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BY14qVgR.js → RBJoyStick.stories-DEgBou3K.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-DzF7vGwm.js → RBLabelButton.stories-DT8drIsF.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-lF4FRmVh.js → RBLight.stories-Dh-aoeyG.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-XhrI7bsV.js → RBMenubar.stories-D-H7UqVh.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DprllQDl.js → RBMultiRangeInput.stories-hlclv6jI.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-2v2gBJfC.js → RBPagination.stories-DWCeoZ_S.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-C8iLIZpv.js → RBPopover.stories-C514M09_.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-Th_HrWHI.js → RBPrompt.stories-BSItvfiT.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-CoHuLaN3.js → RBRadioBox.stories-BBb4YbFU.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-DULxE_ff.js → RBRadioButton.stories-CXyOGJCv.js} +1 -1
- package/cli/storybook-dist/assets/{RBRangeDateInput.stories-D8A9KBSS.js → RBRangeDateInput.stories-qvhubu0e.js} +1 -1
- package/cli/storybook-dist/assets/{RBResizable.stories-C41QfrQ3.js → RBResizable.stories-CZKeyGjH.js} +14 -14
- package/cli/storybook-dist/assets/{RBSelectBox.stories-C1gNmwzw.js → RBSelectBox.stories-CRKP3ybf.js} +1 -1
- package/cli/storybook-dist/assets/RBSheet.stories-sCM3ZJUa.js +39 -0
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-9LlgQ8nh.js → RBSideNumberInput.stories-Bb7gw8RF.js} +3 -3
- package/cli/storybook-dist/assets/{RBSlider.stories-oUgo5K9d.js → RBSlider.stories-23OAQU5z.js} +3 -3
- package/cli/storybook-dist/assets/{RBSpinner.stories-DDpohbY2.js → RBSpinner.stories-2xcQdcmt.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-gW-GN7QO.js → RBSwitch.stories-BACUaJm_.js} +7 -7
- package/cli/storybook-dist/assets/{RBTab.stories-CN-PV3HH.js → RBTab.stories-CDHE3sVs.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-BAZLpxtM.js → RBTable.stories-C0Cfhfl7.js} +29 -29
- package/cli/storybook-dist/assets/{RBTableRow.stories-CwjVrszB.js → RBTableRow.stories-BF8kIkpp.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-JuEo2sUL.js → RBTabs.stories-BdJWRq1o.js} +7 -7
- package/cli/storybook-dist/assets/{RBTextArea.stories-D0_Pv4qZ.js → RBTextArea.stories-CGy2xeYH.js} +3 -3
- package/cli/storybook-dist/assets/{RBTextField.stories-CCsOvZqV.js → RBTextField.stories-IfUOzxCM.js} +15 -15
- package/cli/storybook-dist/assets/{RBTextInput.stories-Cccn3VNm.js → RBTextInput.stories-VjXNFo4R.js} +3 -3
- package/cli/storybook-dist/assets/{RBToast.stories-I5617t0K.js → RBToast.stories-BzcobblV.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-DsgKjGsJ.js → RBToggle.stories-DuLE5mKv.js} +4 -4
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-BZEzSDuz.js → RBToggleGroup.stories-CuR0N0GJ.js} +5 -5
- package/cli/storybook-dist/assets/{RBTooltip.stories-4oSyAWsA.js → RBTooltip.stories--aAgNC8_.js} +1 -1
- package/cli/storybook-dist/assets/{SideNumberInput-DSKbd0bG.js → SideNumberInput-DyWQxYUj.js} +743 -743
- package/cli/storybook-dist/assets/{entry-preview-B35XREGs.js → entry-preview-D7IHsza_.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-BB797lmL.js → iframe-CbaIp5I-.js} +2 -2
- package/cli/storybook-dist/assets/{index-BU2567EP.js → index-C6CBgMuG.js} +3 -3
- package/cli/storybook-dist/assets/{preview-C7DaSWyc.js → preview-B-iYLDuW.js} +2 -2
- package/cli/storybook-dist/assets/{preview-CbuRHit4.js → preview-CRWqc9DH.js} +1 -1
- package/cli/storybook-dist/assets/{react-resizable-panels.browser-ChIvlwo7.js → react-resizable-panels.browser-8hC8dr-6.js} +8 -8
- package/cli/storybook-dist/iframe.html +2 -2
- package/cli/storybook-dist/index.html +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/.mcp.json +8 -8
- package/cli/templates/common/README.md +1231 -1231
- package/cli/templates/common/assets/icon.svg +45 -45
- package/cli/templates/common/manifest.json +26 -26
- package/cli/templates/common/package.json +22 -22
- package/cli/templates/common/py/main.py +125 -125
- package/cli/templates/common/tsconfig.json +13 -13
- package/cli/templates/common/ui/content.tsx +216 -216
- package/cli/templates/common/ui/style.css +6 -6
- package/cli/templates/common/vite.config.ts +10 -10
- package/cli/templates/cpp/cpp/main.cpp +73 -73
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/__PLUGIN_SLUG__/node.py +25 -25
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/package.xml +10 -10
- package/cli/templates/ros2/ros2_ws/src/__PLUGIN_SLUG__/setup.py +18 -18
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/package.xml +11 -11
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +23 -23
- package/cli/vite.d.mts +25 -25
- package/cli/vite.mjs +292 -292
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBCommand.stories-BSqXDkKw.js +0 -73
- package/cli/storybook-dist/assets/RBDialog.stories-BAktz-ku.js +0 -38
- package/cli/storybook-dist/assets/RBDropdownMenu.stories-DbW2fWAj.js +0 -86
- package/cli/storybook-dist/assets/RBSheet.stories-_6VH-RUm.js +0 -39
package/cli/src/mcp.mjs
CHANGED
|
@@ -1,1071 +1,1071 @@
|
|
|
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
|
-
// Add to an MCP client config, e.g. Claude Code's .mcp.json:
|
|
22
|
-
// { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
|
|
23
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
-
import { z } from "zod";
|
|
26
|
-
import ts from "typescript";
|
|
27
|
-
import {
|
|
28
|
-
appendFileSync,
|
|
29
|
-
existsSync,
|
|
30
|
-
mkdirSync,
|
|
31
|
-
readdirSync,
|
|
32
|
-
readFileSync,
|
|
33
|
-
renameSync,
|
|
34
|
-
statSync,
|
|
35
|
-
writeFileSync,
|
|
36
|
-
} from "node:fs";
|
|
37
|
-
import { spawnSync } from "node:child_process";
|
|
38
|
-
import { homedir } from "node:os";
|
|
39
|
-
import { join, resolve } from "node:path";
|
|
40
|
-
import { fileURLToPath } from "node:url";
|
|
41
|
-
import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
|
|
42
|
-
import { PYTHON_DEV_IMAGE } from "./python.mjs";
|
|
43
|
-
import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
|
|
44
|
-
import { CPP_DEV_IMAGE } from "./cpp.mjs";
|
|
45
|
-
import { NATIVE_TO_RB, SERVER_INSTRUCTIONS } from "./ai-rules.mjs";
|
|
46
|
-
|
|
47
|
-
const TEMPLATE_READMES = { python: "common/README.md", cpp: "readme-cpp/README.md" };
|
|
48
|
-
// Design tokens: prefer the shipped copy, fall back to the monorepo source.
|
|
49
|
-
const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
|
|
50
|
-
? SHIPPED_DESIGN_TOKENS
|
|
51
|
-
: resolve(kitRoot, "../..", "packages/rb-styles/css/output/rb-token.css");
|
|
52
|
-
|
|
53
|
-
/* -------------------------------------------------------------------------- */
|
|
54
|
-
/* react.d.ts introspection — component/hook list + prop types */
|
|
55
|
-
/* -------------------------------------------------------------------------- */
|
|
56
|
-
|
|
57
|
-
// plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components types
|
|
58
|
-
// are inlined at build — see plugin-ui/build-dist.mjs), so this one file is
|
|
59
|
-
// enough even in an external install.
|
|
60
|
-
async function reactDtsPath() {
|
|
61
|
-
const entryUrl = await import.meta.resolve("@rainbow-robotics/plugin-ui/react");
|
|
62
|
-
const dtsPath = fileURLToPath(entryUrl).replace(/\.js$/, ".d.ts");
|
|
63
|
-
if (!existsSync(dtsPath)) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`plugin-ui type declarations not found at ${dtsPath} — reinstall or rebuild @rainbow-robotics/plugin-ui.`,
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return dtsPath;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function loadReactTypes(dtsPath) {
|
|
72
|
-
// Without moduleResolution the `import ... from "react"` inside react.d.ts
|
|
73
|
-
// never resolves and every React-derived type collapses to `any`.
|
|
74
|
-
const program = ts.createProgram([dtsPath], {
|
|
75
|
-
target: ts.ScriptTarget.ES2022,
|
|
76
|
-
module: ts.ModuleKind.ESNext,
|
|
77
|
-
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
78
|
-
skipLibCheck: true,
|
|
79
|
-
});
|
|
80
|
-
const sourceFile = program.getSourceFile(dtsPath);
|
|
81
|
-
const checker = program.getTypeChecker();
|
|
82
|
-
const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
|
|
83
|
-
const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
|
|
84
|
-
return { checker, sourceFile, exports };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// The barrel's trailing `export { A, B, ... }` produces Alias symbols whose
|
|
88
|
-
// OWN flags don't reflect value-vs-type — resolve to the real declaration first.
|
|
89
|
-
function resolveExport(checker, symbol) {
|
|
90
|
-
return symbol.flags & ts.SymbolFlags.Alias
|
|
91
|
-
? checker.getAliasedSymbol(symbol)
|
|
92
|
-
: symbol;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function exportKind(checker, symbol) {
|
|
96
|
-
const resolved = resolveExport(checker, symbol);
|
|
97
|
-
return resolved.flags & ts.SymbolFlags.Value ? "value" : "type";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// typeToString alone often prints an opaque alias (`ForwardRefExoticComponent<PropsType$s>`).
|
|
101
|
-
// The first call signature works for plain components and forwardRef alike; its
|
|
102
|
-
// first parameter is the props object.
|
|
103
|
-
function ownProps(checker, type, sourceFile) {
|
|
104
|
-
const [signature] = type.getCallSignatures?.() ?? [];
|
|
105
|
-
const param = signature?.getParameters?.()[0];
|
|
106
|
-
if (!param) return [];
|
|
107
|
-
const declaration = param.valueDeclaration ?? param.declarations?.[0];
|
|
108
|
-
if (!declaration) return [];
|
|
109
|
-
const propsType = checker.getTypeOfSymbolAtLocation(param, declaration);
|
|
110
|
-
|
|
111
|
-
return checker
|
|
112
|
-
.getPropertiesOfType(propsType)
|
|
113
|
-
// Without this, ~220 inherited React DOM attributes bury the component's own API.
|
|
114
|
-
// Own = declared in react.d.ts itself (rb-components types are inlined there). Not a
|
|
115
|
-
// "node_modules" path test: in a plugin project react.d.ts itself lives in node_modules.
|
|
116
|
-
.filter((prop) => (prop.declarations ?? []).some((d) => d.getSourceFile() === sourceFile))
|
|
117
|
-
.map((prop) => {
|
|
118
|
-
const d = prop.valueDeclaration ?? prop.declarations[0];
|
|
119
|
-
const optional = prop.flags & ts.SymbolFlags.Optional ? "?" : "";
|
|
120
|
-
return `${prop.getName()}${optional}: ${checker.typeToString(
|
|
121
|
-
checker.getTypeOfSymbolAtLocation(prop, d),
|
|
122
|
-
)}`;
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Kept out of the tool handler so every export can be precomputed and cached.
|
|
127
|
-
function propsText(checker, sourceFile, name, symbol) {
|
|
128
|
-
const resolved = resolveExport(checker, symbol);
|
|
129
|
-
const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
|
|
130
|
-
const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
|
|
131
|
-
const signature = checker.typeToString(
|
|
132
|
-
type,
|
|
133
|
-
undefined,
|
|
134
|
-
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const props = ownProps(checker, type, sourceFile);
|
|
138
|
-
if (props.length) {
|
|
139
|
-
// Keep the signature line: RBSlider's value/min/max come from Radix and the
|
|
140
|
-
// own-props filter drops them.
|
|
141
|
-
return `# ${name} — own props\n${props.sort().join("\n")}\n\nfull type: ${signature}`;
|
|
142
|
-
}
|
|
143
|
-
// No own props means either a Radix wrapper or unresolved React types. The two
|
|
144
|
-
// look identical, so name the second case explicitly.
|
|
145
|
-
if (/<any>/.test(signature)) {
|
|
146
|
-
return (
|
|
147
|
-
`# ${name} — prop types could not be resolved\n${signature}\n\n` +
|
|
148
|
-
"React's types didn't resolve, so this collapsed to `any`. Install `@types/react` " +
|
|
149
|
-
"in this project (`npm i -D @types/react`) and retry for the real props."
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
return (
|
|
153
|
-
`# ${name} — full type (no plugin-ui-specific props; it forwards a third-party ` +
|
|
154
|
-
`component's props)\n${signature}`
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Rendering every export up front is what makes the result cacheable: once the
|
|
159
|
-
// text exists no TypeChecker is needed.
|
|
160
|
-
function buildUiIndex(dtsPath) {
|
|
161
|
-
const { checker, sourceFile, exports } = loadReactTypes(dtsPath);
|
|
162
|
-
const props = {};
|
|
163
|
-
for (const symbol of exports) {
|
|
164
|
-
props[symbol.getName()] = propsText(checker, sourceFile, symbol.getName(), symbol);
|
|
165
|
-
}
|
|
166
|
-
return {
|
|
167
|
-
components: exports
|
|
168
|
-
.map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
|
|
169
|
-
.sort()
|
|
170
|
-
.join("\n"),
|
|
171
|
-
props,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ts.createProgram pulls in ~1100 source files (~1.5s) and an MCP server is a fresh
|
|
176
|
-
// process per session. The key covers the .d.ts identity and the kit version.
|
|
177
|
-
const UI_INDEX_CACHE_DIR = join(homedir(), ".cache", "muscat-pk");
|
|
178
|
-
const UI_INDEX_CACHE = join(UI_INDEX_CACHE_DIR, "ui-index.json");
|
|
179
|
-
const UI_INDEX_FORMAT = 2;
|
|
180
|
-
|
|
181
|
-
let uiIndexCache = null;
|
|
182
|
-
|
|
183
|
-
async function loadUiIndex() {
|
|
184
|
-
if (uiIndexCache) return uiIndexCache;
|
|
185
|
-
const dtsPath = await reactDtsPath();
|
|
186
|
-
const { mtimeMs, size } = statSync(dtsPath);
|
|
187
|
-
// Bump UI_INDEX_FORMAT when the rendered text changes, so dev builds (same version) rebuild too.
|
|
188
|
-
const key = `${UI_INDEX_FORMAT}|${dtsPath}|${mtimeMs}|${size}|${kitVersion()}`;
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const cached = JSON.parse(readFileSync(UI_INDEX_CACHE, "utf8"));
|
|
192
|
-
// A half-written file still parses as JSON, so check the shape too.
|
|
193
|
-
if (
|
|
194
|
-
cached.key === key &&
|
|
195
|
-
typeof cached.components === "string" &&
|
|
196
|
-
cached.props &&
|
|
197
|
-
typeof cached.props === "object"
|
|
198
|
-
) {
|
|
199
|
-
return (uiIndexCache = cached);
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
/* absent, unreadable or corrupt — rebuild below */
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
uiIndexCache = { key, ...buildUiIndex(dtsPath) };
|
|
206
|
-
try {
|
|
207
|
-
mkdirSync(UI_INDEX_CACHE_DIR, { recursive: true });
|
|
208
|
-
// rename is atomic, so a concurrent server never reads a half-written file.
|
|
209
|
-
const tmp = `${UI_INDEX_CACHE}.${process.pid}.tmp`;
|
|
210
|
-
writeFileSync(tmp, JSON.stringify(uiIndexCache));
|
|
211
|
-
renameSync(tmp, UI_INDEX_CACHE);
|
|
212
|
-
} catch {
|
|
213
|
-
/* read-only fs / sandbox: the cache is an optimisation, never a requirement */
|
|
214
|
-
}
|
|
215
|
-
return uiIndexCache;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/* -------------------------------------------------------------------------- */
|
|
219
|
-
/* Storybook story source lookup */
|
|
220
|
-
/* -------------------------------------------------------------------------- */
|
|
221
|
-
|
|
222
|
-
// Case-insensitive recursive search for `${name}.stories.tsx` under the
|
|
223
|
-
// staged story sources (see storybook.mjs's buildStorybook()).
|
|
224
|
-
function findStorySource(name) {
|
|
225
|
-
if (!existsSync(SHIPPED_STORIES_SRC)) return null;
|
|
226
|
-
const target = `${name.toLowerCase()}.stories.tsx`;
|
|
227
|
-
const stack = [SHIPPED_STORIES_SRC];
|
|
228
|
-
while (stack.length) {
|
|
229
|
-
const dir = stack.pop();
|
|
230
|
-
for (const entry of readdirSync(dir)) {
|
|
231
|
-
const full = join(dir, entry);
|
|
232
|
-
if (statSync(full).isDirectory()) {
|
|
233
|
-
stack.push(full);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (entry.toLowerCase() === target) return readFileSync(full, "utf8");
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Stories are written against the monorepo (@repo/rb-components…). Point those imports at the
|
|
243
|
-
// package a plugin actually has, and flag names plugin-ui does not re-export.
|
|
244
|
-
function rewriteStoryImports(source, exportNames) {
|
|
245
|
-
const exported = new Set(exportNames);
|
|
246
|
-
const missing = new Set();
|
|
247
|
-
const text = source.replace(
|
|
248
|
-
/import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']@repo\/rb-components[^"']*["'];?/g,
|
|
249
|
-
(_, typeOnly = "", names) => {
|
|
250
|
-
const list = names.split(",").map((n) => n.trim()).filter(Boolean);
|
|
251
|
-
for (const n of list) if (!exported.has(n.replace(/^type\s+/, "").split(/\s+as\s+/)[0])) missing.add(n);
|
|
252
|
-
return `import ${typeOnly}{ ${list.join(", ")} } from "@rainbow-robotics/plugin-ui/react";`;
|
|
253
|
-
},
|
|
254
|
-
);
|
|
255
|
-
const notes = [
|
|
256
|
-
"// Storybook story source. In a plugin, import components from \"@rainbow-robotics/plugin-ui/react\"",
|
|
257
|
-
"// (rewritten below); @storybook/* imports, Meta/StoryObj and play() are Storybook-only.",
|
|
258
|
-
...(missing.size ? [`// Not exported by plugin-ui, do not use in a plugin: ${[...missing].join(", ")}`] : []),
|
|
259
|
-
];
|
|
260
|
-
return `${notes.join("\n")}\n\n${text}`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const editDistance = (a, b) => {
|
|
264
|
-
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
265
|
-
for (let i = 1; i <= a.length; i++) {
|
|
266
|
-
let prev = row[0]++;
|
|
267
|
-
for (let j = 1; j <= b.length; j++) {
|
|
268
|
-
const cur = row[j];
|
|
269
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
270
|
-
prev = cur;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return row[b.length];
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
// "RBSelect" → RBSelectBox, RBSelectOption, …; "select" (a native element) → its plugin-ui replacement.
|
|
277
|
-
function suggestExports(name, exportNames) {
|
|
278
|
-
const q = name.toLowerCase().replace(/[<>]/g, "");
|
|
279
|
-
const native = NATIVE_TO_RB.filter(([el]) => el.split(/[\s/=]+/).includes(q)).map(([, rb]) => rb);
|
|
280
|
-
const bare = q.replace(/^rb/, "");
|
|
281
|
-
const close = exportNames
|
|
282
|
-
.map((n) => {
|
|
283
|
-
const lower = n.toLowerCase();
|
|
284
|
-
const score = lower.includes(q) || (bare.length > 2 && lower.includes(bare)) ? 0 : editDistance(q, lower);
|
|
285
|
-
return { n, score };
|
|
286
|
-
})
|
|
287
|
-
.filter((x) => x.score <= 3)
|
|
288
|
-
.sort((a, b) => a.score - b.score || a.n.length - b.n.length)
|
|
289
|
-
.slice(0, 6)
|
|
290
|
-
.map((x) => x.n);
|
|
291
|
-
return [...native, ...close];
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function unknownExportText(name, exportNames, what) {
|
|
295
|
-
const hints = suggestExports(name, exportNames);
|
|
296
|
-
return (
|
|
297
|
-
`No ${what} named "${name}" in @rainbow-robotics/plugin-ui/react.` +
|
|
298
|
-
(hints.length ? ` Did you mean: ${hints.join(", ")}?` : "") +
|
|
299
|
-
" Call list_components for valid names. Do not fall back to a native HTML element."
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/* -------------------------------------------------------------------------- */
|
|
304
|
-
/* README search */
|
|
305
|
-
/* -------------------------------------------------------------------------- */
|
|
306
|
-
|
|
307
|
-
// Split the README on headings and keyword-score each section.
|
|
308
|
-
// Headings inside fenced code blocks (```…```) don't count — a column-0
|
|
309
|
-
// Python comment would otherwise start a bogus section.
|
|
310
|
-
function splitSections(text) {
|
|
311
|
-
const sections = [];
|
|
312
|
-
let current = [];
|
|
313
|
-
let inFence = false;
|
|
314
|
-
for (const line of text.split("\n")) {
|
|
315
|
-
if (/^\s*```/.test(line)) inFence = !inFence;
|
|
316
|
-
if (!inFence && /^#{1,4} /.test(line) && current.length) {
|
|
317
|
-
sections.push(current.join("\n"));
|
|
318
|
-
current = [];
|
|
319
|
-
}
|
|
320
|
-
current.push(line);
|
|
321
|
-
}
|
|
322
|
-
if (current.length) sections.push(current.join("\n"));
|
|
323
|
-
return sections;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Standalone binaries have no templates/ on disk.
|
|
327
|
-
async function readTemplateReadme(rel) {
|
|
328
|
-
const onDisk = join(kitRoot, "cli/templates", rel);
|
|
329
|
-
if (existsSync(onDisk)) return readFileSync(onDisk, "utf8");
|
|
330
|
-
try {
|
|
331
|
-
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
332
|
-
return TEMPLATES[rel] ? Buffer.from(TEMPLATES[rel], "base64").toString("utf8") : "";
|
|
333
|
-
} catch {
|
|
334
|
-
return "";
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function searchReadme(query, lang) {
|
|
339
|
-
const langs = lang ? [lang] : ["python", "cpp"];
|
|
340
|
-
const seen = new Set();
|
|
341
|
-
const sections = [];
|
|
342
|
-
for (const l of langs) {
|
|
343
|
-
for (const section of splitSections(await readTemplateReadme(TEMPLATE_READMES[l]))) {
|
|
344
|
-
if (seen.has(section)) continue; // UI/manifest sections are shared by both READMEs
|
|
345
|
-
seen.add(section);
|
|
346
|
-
sections.push(langs.length > 1 && l === "cpp" ? `[C++ backend]\n${section}` : section);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
350
|
-
|
|
351
|
-
return sections
|
|
352
|
-
.map((section) => {
|
|
353
|
-
const lower = section.toLowerCase();
|
|
354
|
-
const score = terms.reduce((n, term) => n + (lower.includes(term) ? 1 : 0), 0);
|
|
355
|
-
return { section: section.trim(), score };
|
|
356
|
-
})
|
|
357
|
-
.filter((x) => x.score > 0)
|
|
358
|
-
.sort((a, b) => b.score - a.score)
|
|
359
|
-
.slice(0, 5)
|
|
360
|
-
.map((x) => x.section);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/* -------------------------------------------------------------------------- */
|
|
364
|
-
/* Design tokens — parsed from rb-styles' generated rb-token.css */
|
|
365
|
-
/* -------------------------------------------------------------------------- */
|
|
366
|
-
|
|
367
|
-
// A color var `--color-<name>` becomes Tailwind utilities `bg-<name>`,
|
|
368
|
-
// `text-<name>`, `border-<name>`, `fill-<name>`. The design system's naming
|
|
369
|
-
// already encodes intent (bg-/fg-/border-/fill-), so we surface the idiomatic
|
|
370
|
-
// class per group.
|
|
371
|
-
const COLOR_CLASS_HINT = {
|
|
372
|
-
bg: (n) => `bg-${n}`, // --color-bg-default → bg-bg-default
|
|
373
|
-
fg: (n) => `text-${n}`, // --color-fg-subtle → text-fg-subtle
|
|
374
|
-
border: (n) => `border-${n}`,
|
|
375
|
-
fill: (n) => `fill-${n}`,
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
let tokensCache = null;
|
|
379
|
-
|
|
380
|
-
function loadDesignTokens() {
|
|
381
|
-
if (tokensCache) return tokensCache;
|
|
382
|
-
if (!existsSync(DESIGN_TOKENS_CSS)) {
|
|
383
|
-
throw new Error(
|
|
384
|
-
`design token CSS not found at ${DESIGN_TOKENS_CSS} — rebuild via "muscat-pk storybook --rebuild" (monorepo) or reinstall.`,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
const css = readFileSync(DESIGN_TOKENS_CSS, "utf8");
|
|
388
|
-
|
|
389
|
-
// @theme { --name: value; ... } → { group, name, value, class? }
|
|
390
|
-
const vars = [];
|
|
391
|
-
for (const [, rawName, value] of css.matchAll(
|
|
392
|
-
/^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);/gim,
|
|
393
|
-
)) {
|
|
394
|
-
const name = rawName.replace(/^--/, "");
|
|
395
|
-
const m = name.match(/^color-(bg|fg|border|fill)-(.+)$/);
|
|
396
|
-
const token = { name, value: value.trim() };
|
|
397
|
-
if (m) {
|
|
398
|
-
token.group = `color-${m[1]}`;
|
|
399
|
-
token.class = COLOR_CLASS_HINT[m[1]](`${m[1]}-${m[2]}`);
|
|
400
|
-
} else {
|
|
401
|
-
token.group = name.split("-").slice(0, 2).join("-");
|
|
402
|
-
}
|
|
403
|
-
vars.push(token);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// @utility typo-… → composite typography classes used directly in className.
|
|
407
|
-
const typo = [
|
|
408
|
-
...new Set(
|
|
409
|
-
[...css.matchAll(/@utility\s+(typo-[a-z0-9-]+)\s*\{/g)].map((m) => m[1]),
|
|
410
|
-
),
|
|
411
|
-
].sort();
|
|
412
|
-
|
|
413
|
-
tokensCache = { vars, typo };
|
|
414
|
-
return tokensCache;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/* -------------------------------------------------------------------------- */
|
|
418
|
-
/* rainbow-rb-sdk (Python) introspection */
|
|
419
|
-
/* -------------------------------------------------------------------------- */
|
|
420
|
-
|
|
421
|
-
function pythonCmd() {
|
|
422
|
-
// Prefer the current plugin's own .venv — that's where `muscat-pk new` /
|
|
423
|
-
// ensureIdeVenv() installs rainbow-rb-sdk from requirements.txt. The AI
|
|
424
|
-
// agent's cwd is usually the plugin dir mcp was launched from.
|
|
425
|
-
try {
|
|
426
|
-
const py = venvPython(resolve(findPluginRoot(), ".venv"));
|
|
427
|
-
if (existsSync(py)) return py;
|
|
428
|
-
} catch {
|
|
429
|
-
/* not inside a plugin project — fall through to PATH lookup */
|
|
430
|
-
}
|
|
431
|
-
for (const c of ["python3", "python"]) {
|
|
432
|
-
const r = spawnSync(c, ["--version"], { encoding: "utf8" });
|
|
433
|
-
if (r.status === 0) return c;
|
|
434
|
-
}
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Run an introspection script against the installed rainbow.rb_sdk (a plugin's
|
|
439
|
-
// backend dependency). Returns a clear hint if it isn't importable.
|
|
440
|
-
function introspectRbSdk(script) {
|
|
441
|
-
const py = pythonCmd();
|
|
442
|
-
const probe = `
|
|
443
|
-
import json, importlib
|
|
444
|
-
try:
|
|
445
|
-
importlib.import_module("rainbow.rb_sdk")
|
|
446
|
-
except Exception as e:
|
|
447
|
-
print(json.dumps({"__notinstalled__": str(e)})); raise SystemExit(0)
|
|
448
|
-
${script}
|
|
449
|
-
`;
|
|
450
|
-
if (!py) {
|
|
451
|
-
return introspectInDocker(probe) ?? { error: "Python not found on PATH. Install Python 3 to use rb-sdk introspection." };
|
|
452
|
-
}
|
|
453
|
-
const r = spawnSync(py, ["-c", probe], { encoding: "utf8", maxBuffer: 8 << 20 });
|
|
454
|
-
if (r.status !== 0) {
|
|
455
|
-
return { error: `python introspection failed: ${(r.stderr || "").trim().slice(0, 500)}` };
|
|
456
|
-
}
|
|
457
|
-
const local = parseProbe(r);
|
|
458
|
-
return local.error && local.notInstalled ? introspectInDocker(probe) ?? local : local;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// No macOS wheels: import the plugin's pinned SDK inside the python dev image (cached).
|
|
462
|
-
function introspectInDocker(probe) {
|
|
463
|
-
if (spawnSync("docker", ["image", "inspect", PYTHON_DEV_IMAGE], { encoding: "utf8" }).status !== 0) return null;
|
|
464
|
-
let ver = "";
|
|
465
|
-
try {
|
|
466
|
-
const req = readFileSync(join(findPluginRoot(), "requirements.txt"), "utf8");
|
|
467
|
-
ver = req.match(/^rainbow-rb-sdk==(\S+)/m)?.[1] ?? "";
|
|
468
|
-
} catch {
|
|
469
|
-
/* not a python plugin — latest */
|
|
470
|
-
}
|
|
471
|
-
// ponytail: "latest" cache is never refreshed; delete ~/.cache/... rb-sdk-py/latest to update.
|
|
472
|
-
const cache = join(UI_INDEX_CACHE_DIR, "rb-sdk-py", ver || "latest");
|
|
473
|
-
mkdirSync(cache, { recursive: true });
|
|
474
|
-
const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
|
|
475
|
-
const script =
|
|
476
|
-
`[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
|
|
477
|
-
`PYTHONPATH=/c python3 -c "$PROBE"`;
|
|
478
|
-
const r = spawnSync("docker", ["run", "--rm", "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
|
|
479
|
-
encoding: "utf8",
|
|
480
|
-
maxBuffer: 8 << 20,
|
|
481
|
-
env: { ...process.env, PROBE: probe },
|
|
482
|
-
});
|
|
483
|
-
if (r.status !== 0) return { error: `python introspection (docker) failed: ${(r.stderr || "").trim().slice(-500)}` };
|
|
484
|
-
return parseProbe(r);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function parseProbe(r) {
|
|
488
|
-
// Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
|
|
489
|
-
// Zenoh client logs its connection endpoints), ahead of our own final
|
|
490
|
-
// print(json.dumps(...)) — so parse the last line, not the whole output.
|
|
491
|
-
const lines = r.stdout.trim().split("\n");
|
|
492
|
-
let data;
|
|
493
|
-
try {
|
|
494
|
-
data = JSON.parse(lines[lines.length - 1]);
|
|
495
|
-
} catch {
|
|
496
|
-
return { error: `unexpected introspection output: ${r.stdout.slice(0, 300)}` };
|
|
497
|
-
}
|
|
498
|
-
if (data && data.__notinstalled__) {
|
|
499
|
-
return {
|
|
500
|
-
notInstalled: true,
|
|
501
|
-
error:
|
|
502
|
-
"rainbow-rb-sdk is not installed in this environment. Install it with `pip install rainbow-rb-sdk` " +
|
|
503
|
-
"(it's already in a plugin's requirements.txt), then retry.",
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
return { data };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/* -------------------------------------------------------------------------- */
|
|
510
|
-
/* rainbow-rb-sdk-dev (C++) headers — copied out of the cpp dev image */
|
|
511
|
-
/* -------------------------------------------------------------------------- */
|
|
512
|
-
|
|
513
|
-
const CPP_INCLUDE_CACHE = join(UI_INDEX_CACHE_DIR, "rb-sdk-include");
|
|
514
|
-
// Headers we don't want to surface: third-party vendored code, pybind glue,
|
|
515
|
-
// flatbuffers-generated schemas, and the industrial comm stacks.
|
|
516
|
-
const CPP_SKIP = /^(vendor|binding|rb_comm_source)\/|_generated\.h$/;
|
|
517
|
-
|
|
518
|
-
// The apt package is only installed inside the muscat-pk-cpp-dev image, so
|
|
519
|
-
// copy /usr/include/rainbow/rb_sdk out once per image build (cache key =
|
|
520
|
-
// image id) with `docker cp` — no host tar needed.
|
|
521
|
-
function cppIncludeDir() {
|
|
522
|
-
const insp = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
523
|
-
if (insp.error) return { error: "Docker not found — the C++ SDK headers live in the muscat-pk cpp dev image." };
|
|
524
|
-
if (insp.status !== 0) {
|
|
525
|
-
return {
|
|
526
|
-
error:
|
|
527
|
-
`Docker image ${CPP_DEV_IMAGE} not built yet. Run \`muscat-pk dev:back\` (or \`muscat-pk build\`) once ` +
|
|
528
|
-
"in a cpp / rclcpp plugin to build it, then retry.",
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
const dir = join(CPP_INCLUDE_CACHE, insp.stdout.trim().replace(/^sha256:/, "").slice(0, 12));
|
|
532
|
-
const out = join(dir, "rb_sdk");
|
|
533
|
-
if (existsSync(out)) return { dir: out };
|
|
534
|
-
mkdirSync(dir, { recursive: true });
|
|
535
|
-
const ctr = spawnSync("docker", ["create", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
536
|
-
if (ctr.status !== 0) return { error: `docker create failed: ${(ctr.stderr || "").trim().slice(0, 300)}` };
|
|
537
|
-
const id = ctr.stdout.trim();
|
|
538
|
-
const cp = spawnSync("docker", ["cp", `${id}:/usr/include/rainbow/rb_sdk`, dir], { encoding: "utf8" });
|
|
539
|
-
spawnSync("docker", ["rm", "-f", id], { encoding: "utf8" });
|
|
540
|
-
if (cp.status !== 0 || !existsSync(out)) {
|
|
541
|
-
return { error: `docker cp failed: ${(cp.stderr || "").trim().slice(0, 300)}` };
|
|
542
|
-
}
|
|
543
|
-
return { dir: out };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function cppHeaders(root) {
|
|
547
|
-
const files = [];
|
|
548
|
-
const walk = (d, rel) => {
|
|
549
|
-
for (const name of readdirSync(d)) {
|
|
550
|
-
const r = rel ? `${rel}/${name}` : name;
|
|
551
|
-
if (CPP_SKIP.test(r)) continue;
|
|
552
|
-
const full = join(d, name);
|
|
553
|
-
if (statSync(full).isDirectory()) walk(full, r);
|
|
554
|
-
else if (/\.hpp$/.test(name)) files.push(r);
|
|
555
|
-
}
|
|
556
|
-
};
|
|
557
|
-
walk(root, "");
|
|
558
|
-
return files.sort();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// "class Foo {" / "class Foo : public Bar {" — not forward declarations.
|
|
562
|
-
const CLASS_RE = /^\s*(?:class|struct)\s+(?:[A-Z][A-Z0-9_]*_API\s+)?(\w+)\b(?![^\n]*;\s*$)/; // skip export macros (RB_SDK_API)
|
|
563
|
-
|
|
564
|
-
function cppClassesIn(text) {
|
|
565
|
-
return [...new Set(text.split("\n").map((l) => l.match(CLASS_RE)?.[1]).filter(Boolean))];
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Return the class body (brace-matched) plus the file's leading comment block,
|
|
569
|
-
// which in these headers usually carries a full usage example.
|
|
570
|
-
function cppExtract(text, className, method) {
|
|
571
|
-
const lines = text.split("\n");
|
|
572
|
-
const start = lines.findIndex((l) => CLASS_RE.test(l) && l.match(CLASS_RE)[1] === className);
|
|
573
|
-
if (start < 0) return null;
|
|
574
|
-
let depth = 0, end = start, seen = false;
|
|
575
|
-
for (let i = start; i < lines.length; i++) {
|
|
576
|
-
for (const ch of lines[i]) {
|
|
577
|
-
if (ch === "{") { depth++; seen = true; }
|
|
578
|
-
else if (ch === "}") depth--;
|
|
579
|
-
}
|
|
580
|
-
if (seen && depth === 0) { end = i; break; }
|
|
581
|
-
}
|
|
582
|
-
const body = lines.slice(start, end + 1);
|
|
583
|
-
if (method) {
|
|
584
|
-
// Declarations sit at class-member indent (<= 2 spaces) and the name is
|
|
585
|
-
// not preceded by `.`/`->`/`::` — that skips call sites inside bodies.
|
|
586
|
-
const re = new RegExp(`^ {0,2}[^(]*(?<![\\w.>:])${method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
|
|
587
|
-
const hits = [];
|
|
588
|
-
for (let i = 0; i < body.length; i++) {
|
|
589
|
-
if (!re.test(body[i]) || /^\s*\/\//.test(body[i])) continue;
|
|
590
|
-
let a = i;
|
|
591
|
-
while (a > 0 && /^\s*\/\//.test(body[a - 1])) a--;
|
|
592
|
-
let b = i;
|
|
593
|
-
while (b < body.length - 1 && !/[{;]/.test(body[b])) b++;
|
|
594
|
-
hits.push(body.slice(a, b + 1).join("\n"));
|
|
595
|
-
}
|
|
596
|
-
return hits.length ? hits.join("\n\n") : null;
|
|
597
|
-
}
|
|
598
|
-
const head = [];
|
|
599
|
-
for (const l of lines) {
|
|
600
|
-
if (/^\s*\/\//.test(l)) head.push(l);
|
|
601
|
-
else if (l.trim() === "" || /^#pragma/.test(l)) continue;
|
|
602
|
-
else break;
|
|
603
|
-
}
|
|
604
|
-
return (head.length > 3 ? head.join("\n") + "\n\n" : "") + body.join("\n");
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function cppFind(className) {
|
|
608
|
-
const { dir, error } = cppIncludeDir();
|
|
609
|
-
if (error) return { error };
|
|
610
|
-
for (const rel of cppHeaders(dir)) {
|
|
611
|
-
const text = readFileSync(join(dir, rel), "utf8");
|
|
612
|
-
if (cppClassesIn(text).includes(className)) return { rel, text };
|
|
613
|
-
}
|
|
614
|
-
return { error: `No class "${className}" in the C++ SDK headers. Call list_rb_sdk with lang "cpp" for valid names.` };
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/* -------------------------------------------------------------------------- */
|
|
618
|
-
/* client registration — `muscat-pk mcp --install <client>` */
|
|
619
|
-
/* -------------------------------------------------------------------------- */
|
|
620
|
-
|
|
621
|
-
// Claude Code needs no install step: `muscat-pk new` scaffolds a project-level
|
|
622
|
-
// .mcp.json. Codex only reads the global ~/.codex/config.toml, so we append
|
|
623
|
-
// the server block there (idempotent).
|
|
624
|
-
export function installMcp(target) {
|
|
625
|
-
if (target !== "codex") {
|
|
626
|
-
throw new Error(
|
|
627
|
-
`Unknown --install target ${JSON.stringify(target ?? "")} — supported: "codex". ` +
|
|
628
|
-
"(Claude Code works out of the box via the scaffolded .mcp.json.)",
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
const cfgDir = join(homedir(), ".codex");
|
|
632
|
-
const cfgPath = join(cfgDir, "config.toml");
|
|
633
|
-
const existing = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : "";
|
|
634
|
-
if (/^\s*\[mcp_servers\.muscat\]/m.test(existing)) {
|
|
635
|
-
console.log(`Codex already has the muscat MCP server registered (${cfgPath}) — nothing to do.`);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
// ponytail: Windows Codex spawns without a shell, so it needs the .cmd shim.
|
|
639
|
-
const command = process.platform === "win32" ? "muscat-pk.cmd" : "muscat-pk";
|
|
640
|
-
mkdirSync(cfgDir, { recursive: true });
|
|
641
|
-
const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
642
|
-
appendFileSync(
|
|
643
|
-
cfgPath,
|
|
644
|
-
`${sep}[mcp_servers.muscat]\ncommand = "${command}"\nargs = ["mcp"]\n`,
|
|
645
|
-
);
|
|
646
|
-
console.log(`Registered the muscat MCP server for Codex → ${cfgPath}`);
|
|
647
|
-
console.log("Restart Codex (or start a new session) to pick it up.");
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/* -------------------------------------------------------------------------- */
|
|
651
|
-
/* server */
|
|
652
|
-
/* -------------------------------------------------------------------------- */
|
|
653
|
-
|
|
654
|
-
export async function runMcp() {
|
|
655
|
-
const server = new McpServer(
|
|
656
|
-
{ name: "muscat-plugin-kit", version: kitVersion() },
|
|
657
|
-
{ instructions: SERVER_INSTRUCTIONS },
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
server.registerTool(
|
|
661
|
-
"list_components",
|
|
662
|
-
{
|
|
663
|
-
title: "List Muscat plugin UI components",
|
|
664
|
-
description:
|
|
665
|
-
"List every component, hook, and type exported from @rainbow-robotics/plugin-ui/react — " +
|
|
666
|
-
"the full UI surface available to Muscat plugin authors. Call this first before guessing a " +
|
|
667
|
-
"component name; plugin UI uses these instead of native <select>/<input>/<button> elements.",
|
|
668
|
-
inputSchema: {},
|
|
669
|
-
},
|
|
670
|
-
async () => {
|
|
671
|
-
const { components } = await loadUiIndex();
|
|
672
|
-
return { content: [{ type: "text", text: components }] };
|
|
673
|
-
},
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
server.registerTool(
|
|
677
|
-
"get_component_props",
|
|
678
|
-
{
|
|
679
|
-
title: "Get a component or hook's real type signature",
|
|
680
|
-
description:
|
|
681
|
-
"Get the full TypeScript type for a named export of @rainbow-robotics/plugin-ui/react " +
|
|
682
|
-
"(e.g. RBSlider, RBLabelButton, useRb, useRbFormData). Use list_components to find valid names.",
|
|
683
|
-
inputSchema: { name: z.string().describe('Exported name, e.g. "RBSlider"') },
|
|
684
|
-
},
|
|
685
|
-
async ({ name }) => {
|
|
686
|
-
const { props } = await loadUiIndex();
|
|
687
|
-
if (!Object.hasOwn(props, name)) {
|
|
688
|
-
return { isError: true, content: [{ type: "text", text: unknownExportText(name, Object.keys(props), "export") }] };
|
|
689
|
-
}
|
|
690
|
-
return { content: [{ type: "text", text: props[name] }] };
|
|
691
|
-
},
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
server.registerTool(
|
|
695
|
-
"get_component_example",
|
|
696
|
-
{
|
|
697
|
-
title: "Get a real Storybook usage example",
|
|
698
|
-
description:
|
|
699
|
-
"Get the real Storybook story source for a component (e.g. RBSlider) — actual working " +
|
|
700
|
-
"props, imports, and layout, not a guessed shape. Prefer this over get_component_props when " +
|
|
701
|
-
"you just need to know how to use a component, not its full type.",
|
|
702
|
-
// `component` accepted as an alias — agents often guess it over `name`.
|
|
703
|
-
inputSchema: {
|
|
704
|
-
name: z.string().optional().describe('Component name, e.g. "RBSlider"'),
|
|
705
|
-
component: z.string().optional().describe("Alias for `name`."),
|
|
706
|
-
},
|
|
707
|
-
},
|
|
708
|
-
async ({ name, component }) => {
|
|
709
|
-
const target = name ?? component;
|
|
710
|
-
if (!target) {
|
|
711
|
-
return {
|
|
712
|
-
isError: true,
|
|
713
|
-
content: [{ type: "text", text: 'Missing required argument: "name" (component name, e.g. "RBSlider").' }],
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
const exportNames = Object.keys((await loadUiIndex()).props);
|
|
717
|
-
const source = findStorySource(target);
|
|
718
|
-
if (!source) {
|
|
719
|
-
// An existing export without a story still has a type; an unknown name gets suggestions.
|
|
720
|
-
const text = exportNames.includes(target)
|
|
721
|
-
? `No Storybook example for "${target}". Call get_component_props for its type, or run "muscat-pk storybook" to browse components live.`
|
|
722
|
-
: unknownExportText(target, exportNames, "component");
|
|
723
|
-
return { content: [{ type: "text", text }] };
|
|
724
|
-
}
|
|
725
|
-
return { content: [{ type: "text", text: rewriteStoryImports(source, exportNames) }] };
|
|
726
|
-
},
|
|
727
|
-
);
|
|
728
|
-
|
|
729
|
-
server.registerTool(
|
|
730
|
-
"search_rb_sdk",
|
|
731
|
-
{
|
|
732
|
-
title: "Search the Muscat plugin-author docs",
|
|
733
|
-
description:
|
|
734
|
-
"Search the Muscat plugin-author README for how to use useRb(), rb.pub/sub/action/serve, " +
|
|
735
|
-
"plugin.router (external endpoints: router.on/subscribe/emit exposed over REST/Socket.IO/TCP/" +
|
|
736
|
-
"MQTT/WebRTC at common/plugin/{plugin_id}/{path}, PluginRouteError, meta.transport/principal), " +
|
|
737
|
-
"rb.toast/alert/confirm/prompt, useRbFormData/useRbSave (save/restore form data through the " +
|
|
738
|
-
"host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
|
|
739
|
-
"names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
|
|
740
|
-
"set_variable/executor_state/check_stop), video streaming (MediaMTX: backend publishes RTSP to " +
|
|
741
|
-
"127.0.0.1:8554, UI plays WebRTC from :8889), ROS 2 topics (plugin namespace, rb.rosPeers to allow " +
|
|
742
|
-
"an external PC), contribution points, and backend runtimes " +
|
|
743
|
-
"(Python/C++/ROS2). Covers BOTH backends: the Python README (py/main.py, rainbow.rb_sdk) and the " +
|
|
744
|
-
"C++ README (cpp/main.cpp or an rclcpp node: rb_plugin::RBPluginCall serve/subscribe/publish/" +
|
|
745
|
-
"router(), rb_plugin::PluginProgramContext, RBManipulateSDK, CMake rb_sdk::rb_sdk). Pass lang " +
|
|
746
|
-
"\"python\" or \"cpp\" to match the plugin's manifest runtime (cpp or ros2+rclcpp → \"cpp\"); " +
|
|
747
|
-
"omit it to search both. IMPORTANT: " +
|
|
748
|
-
"the backend pattern depends on the contribution point — search 'which backend pattern' before " +
|
|
749
|
-
"writing the backend. Only a program.panel command uses PluginProgramContext; a toolbar/move.panel " +
|
|
750
|
-
"plugin controls the robot with the SDK (RBManipulateSDK/RBAmrSDK) directly.",
|
|
751
|
-
inputSchema: {
|
|
752
|
-
query: z.string().describe('Keywords, e.g. "save form data" or "rb.action"'),
|
|
753
|
-
lang: z.enum(["python", "cpp"]).optional().describe("Backend language; omit to search both"),
|
|
754
|
-
},
|
|
755
|
-
},
|
|
756
|
-
async ({ query, lang }) => {
|
|
757
|
-
const sections = await searchReadme(query, lang);
|
|
758
|
-
if (sections.length === 0) {
|
|
759
|
-
return { content: [{ type: "text", text: `No matching section found for "${query}".` }] };
|
|
760
|
-
}
|
|
761
|
-
return { content: [{ type: "text", text: sections.join("\n\n---\n\n") }] };
|
|
762
|
-
},
|
|
763
|
-
);
|
|
764
|
-
|
|
765
|
-
/* --- design tokens --- */
|
|
766
|
-
|
|
767
|
-
server.registerTool(
|
|
768
|
-
"list_design_tokens",
|
|
769
|
-
{
|
|
770
|
-
title: "List Muscat design tokens (Tailwind classes)",
|
|
771
|
-
description:
|
|
772
|
-
"List the design system's tokens as the Tailwind classes to use in a plugin's className: " +
|
|
773
|
-
"semantic colors (bg-bg-*, text-fg-*, border-border-*, fill-fill-*), typography (typo-*), " +
|
|
774
|
-
"shadows, radius, spacing. Use these instead of raw hex/px so the plugin matches the host theme " +
|
|
775
|
-
"(and light/dark). Optionally filter by category.",
|
|
776
|
-
inputSchema: {
|
|
777
|
-
category: z
|
|
778
|
-
.enum(["bg", "fg", "border", "fill", "typography", "shadow", "radius", "spacing", "all"])
|
|
779
|
-
.optional()
|
|
780
|
-
.describe('Filter, e.g. "bg", "fg", "typography". Omit for a grouped overview.'),
|
|
781
|
-
},
|
|
782
|
-
},
|
|
783
|
-
async ({ category }) => {
|
|
784
|
-
const { vars, typo } = loadDesignTokens();
|
|
785
|
-
const out = [];
|
|
786
|
-
|
|
787
|
-
const colorGroups = { bg: [], fg: [], border: [], fill: [] };
|
|
788
|
-
const other = { shadow: [], radius: [], spacing: [] };
|
|
789
|
-
for (const t of vars) {
|
|
790
|
-
const m = t.group?.match(/^color-(bg|fg|border|fill)$/);
|
|
791
|
-
// Class name only: the value is just an indirection to another CSS var
|
|
792
|
-
// (var(--color-scale-zinc-200)), and each class is declared twice (light/dark)
|
|
793
|
-
// with different values — dropping the value lets the Set dedup collapse them.
|
|
794
|
-
if (m && t.class) colorGroups[m[1]].push(t.class);
|
|
795
|
-
else if (t.name.startsWith("shadow-")) other.shadow.push(`shadow-${t.name.slice(7)}`);
|
|
796
|
-
else if (t.name.startsWith("radius-")) other.radius.push(`rounded-${t.name.slice(7)}`);
|
|
797
|
-
else if (t.name.startsWith("spacing-")) other.spacing.push(t.name);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
const want = (c) => !category || category === "all" || category === c;
|
|
801
|
-
const push = (title, items) => {
|
|
802
|
-
if (items.length) out.push(`## ${title}\n${[...new Set(items)].sort().join("\n")}`);
|
|
803
|
-
};
|
|
804
|
-
if (want("bg")) push("Background — bg-bg-*", colorGroups.bg);
|
|
805
|
-
if (want("fg")) push("Text/foreground — text-fg-*", colorGroups.fg);
|
|
806
|
-
if (want("border")) push("Border — border-border-*", colorGroups.border);
|
|
807
|
-
if (want("fill")) push("SVG fill — fill-fill-*", colorGroups.fill);
|
|
808
|
-
if (want("typography")) push("Typography — typo-* (use directly)", typo);
|
|
809
|
-
if (want("shadow")) push("Shadow", other.shadow);
|
|
810
|
-
if (want("radius")) push("Radius", other.radius);
|
|
811
|
-
if (want("spacing")) push("Spacing scale (p-/m-/gap-/w-/h-)", other.spacing);
|
|
812
|
-
|
|
813
|
-
return {
|
|
814
|
-
content: [
|
|
815
|
-
{
|
|
816
|
-
type: "text",
|
|
817
|
-
text: out.join("\n\n") || `No tokens for category "${category}".`,
|
|
818
|
-
},
|
|
819
|
-
],
|
|
820
|
-
};
|
|
821
|
-
},
|
|
822
|
-
);
|
|
823
|
-
|
|
824
|
-
server.registerTool(
|
|
825
|
-
"search_design_tokens",
|
|
826
|
-
{
|
|
827
|
-
title: "Search design tokens by name or value",
|
|
828
|
-
description:
|
|
829
|
-
"Find a design token by keyword — e.g. 'danger' (error colors), 'elevated', 'brand', or a hex " +
|
|
830
|
-
"like '#fb2c36'. Returns the matching Tailwind class + value.",
|
|
831
|
-
inputSchema: { query: z.string().describe('e.g. "danger", "elevated", "#fb2c36"') },
|
|
832
|
-
},
|
|
833
|
-
async ({ query }) => {
|
|
834
|
-
const { vars, typo } = loadDesignTokens();
|
|
835
|
-
const q = query.toLowerCase();
|
|
836
|
-
const hits = vars
|
|
837
|
-
.filter((t) => t.name.toLowerCase().includes(q) || t.value.toLowerCase().includes(q))
|
|
838
|
-
.map((t) => `${t.class ?? t.name} (${t.value})`);
|
|
839
|
-
const typoHits = typo.filter((c) => c.toLowerCase().includes(q));
|
|
840
|
-
const all = [...typoHits, ...hits];
|
|
841
|
-
return {
|
|
842
|
-
content: [
|
|
843
|
-
{
|
|
844
|
-
type: "text",
|
|
845
|
-
text: all.length ? [...new Set(all)].slice(0, 60).join("\n") : `No token matches "${query}".`,
|
|
846
|
-
},
|
|
847
|
-
],
|
|
848
|
-
};
|
|
849
|
-
},
|
|
850
|
-
);
|
|
851
|
-
|
|
852
|
-
/* --- rainbow-rb-sdk (Python backend) --- */
|
|
853
|
-
|
|
854
|
-
server.registerTool(
|
|
855
|
-
"list_rb_sdk",
|
|
856
|
-
{
|
|
857
|
-
title: "List rainbow-rb-sdk classes and functions (Python or C++)",
|
|
858
|
-
description:
|
|
859
|
-
"List the public classes and functions of the plugin BACKEND SDK. lang \"python\" (default): the " +
|
|
860
|
-
"installed rainbow.rb_sdk package (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, " +
|
|
861
|
-
"...) — call before writing py/main.py; requires rainbow-rb-sdk to be pip-installed. lang \"cpp\": " +
|
|
862
|
-
"the C++ SDK headers (apt rainbow-rb-sdk-dev: RBManipulateSDK, rb_plugin::PluginProgramContext, " +
|
|
863
|
-
"...) grouped by header path — call before writing cpp/main.cpp or an rclcpp node; requires the " +
|
|
864
|
-
"muscat-pk-cpp-dev Docker image (built by `muscat-pk dev:back`). RBPluginCall (UI pub/sub/serve, " +
|
|
865
|
-
"router) exists in both: Python rainbow.rb_sdk.RBPluginCall, C++ rb_plugin::RBPluginCall " +
|
|
866
|
-
"(plugin/rb_plugin.hpp).",
|
|
867
|
-
inputSchema: { lang: z.enum(["python", "cpp"]).optional().describe('Default "python"') },
|
|
868
|
-
},
|
|
869
|
-
async ({ lang }) => {
|
|
870
|
-
if (lang === "cpp") {
|
|
871
|
-
const { dir, error } = cppIncludeDir();
|
|
872
|
-
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
873
|
-
const out = [];
|
|
874
|
-
for (const rel of cppHeaders(dir)) {
|
|
875
|
-
const classes = cppClassesIn(readFileSync(join(dir, rel), "utf8"));
|
|
876
|
-
if (classes.length) out.push(`${rel}: ${classes.join(", ")}`);
|
|
877
|
-
}
|
|
878
|
-
return { content: [{ type: "text", text: `#include <rainbow/rb_sdk/...>\n\n${out.join("\n")}` }] };
|
|
879
|
-
}
|
|
880
|
-
const { data, error } = introspectRbSdk(`
|
|
881
|
-
import inspect, json
|
|
882
|
-
mod = importlib.import_module("rainbow.rb_sdk")
|
|
883
|
-
out = []
|
|
884
|
-
for name in sorted(getattr(mod, "__all__", None) or dir(mod)):
|
|
885
|
-
if name.startswith("_"): continue
|
|
886
|
-
obj = getattr(mod, name)
|
|
887
|
-
if inspect.isclass(obj): out.append(name + " (class)")
|
|
888
|
-
elif inspect.isfunction(obj) or inspect.isbuiltin(obj): out.append(name + " (function)")
|
|
889
|
-
print(json.dumps(out))
|
|
890
|
-
`);
|
|
891
|
-
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
892
|
-
return { content: [{ type: "text", text: data.join("\n") }] };
|
|
893
|
-
},
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
server.registerTool(
|
|
897
|
-
"get_rb_sdk_api",
|
|
898
|
-
{
|
|
899
|
-
title: "Get a rainbow-rb-sdk class/function signature + docs (Python or C++)",
|
|
900
|
-
description:
|
|
901
|
-
"Get the signature(s) and docstring for a rainbow.rb_sdk class or function (e.g. " +
|
|
902
|
-
"'PluginProgramContext', 'RBManipulateSDK'). For a class, lists its public methods with " +
|
|
903
|
-
"signatures. To drill into ONE method of a class (e.g. RBManipulateSDK.call_move_j), pass " +
|
|
904
|
-
"`method` alongside `name` — do not put both in `name`. lang \"python\" (default) introspects the " +
|
|
905
|
-
"pip-installed package. lang \"cpp\" returns the class declaration from the C++ SDK header " +
|
|
906
|
-
"(rainbow-rb-sdk-dev) with its header comment — usually a full usage example — and the " +
|
|
907
|
-
"#include path; with `method`, just that method's declaration and comment.",
|
|
908
|
-
inputSchema: {
|
|
909
|
-
name: z.string().describe('Class or function name, e.g. "RBManipulateSDK"'),
|
|
910
|
-
method: z.string().optional().describe('Optional: a specific method on that class, e.g. "call_move_j"'),
|
|
911
|
-
lang: z.enum(["python", "cpp"]).optional().describe('Default "python"'),
|
|
912
|
-
},
|
|
913
|
-
},
|
|
914
|
-
async ({ name, method, lang }) => {
|
|
915
|
-
// Tolerate a combined "ClassName method_name" in `name` (a common agent
|
|
916
|
-
// mistake) by splitting it ourselves instead of failing validation.
|
|
917
|
-
let className = name;
|
|
918
|
-
let methodName = method;
|
|
919
|
-
if (!methodName && /\s/.test(name.trim())) {
|
|
920
|
-
const parts = name.trim().split(/\s+/);
|
|
921
|
-
if (parts.length === 2) [className, methodName] = parts;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (lang === "cpp") {
|
|
925
|
-
const cls = className.replace(/^.*::/, "");
|
|
926
|
-
let { rel, text, error } = cppFind(cls);
|
|
927
|
-
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
928
|
-
let body = cppExtract(text, cls, methodName);
|
|
929
|
-
let owner = cls;
|
|
930
|
-
if (!body && methodName) {
|
|
931
|
-
// Like the Python path: most calls live on sub-SDK members
|
|
932
|
-
// (RBManipulateMoveSDK move; -> sdk.move.call_move_j) or on the base
|
|
933
|
-
// class — look there before giving up.
|
|
934
|
-
const decl = cppExtract(text, cls);
|
|
935
|
-
const cands = [
|
|
936
|
-
...[...decl.matchAll(/^\s*(\w+SDK)\s+(\w+);/gm)].map((m) => [m[1], `${cls}.${m[2]}`]),
|
|
937
|
-
...[...decl.matchAll(/:\s*public\s+(\w+)/g)].map((m) => [m[1], `${cls} (base ${m[1]})`]),
|
|
938
|
-
];
|
|
939
|
-
for (const [c, label] of cands) {
|
|
940
|
-
const f = cppFind(c);
|
|
941
|
-
if (f.error) continue;
|
|
942
|
-
const b = cppExtract(f.text, c, methodName);
|
|
943
|
-
if (b) { ({ rel, text } = f); body = b; owner = label; break; }
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
if (!body) {
|
|
947
|
-
return {
|
|
948
|
-
isError: true,
|
|
949
|
-
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.` }],
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
const ns = text.match(/^namespace\s+(\w+)\s*\{/m)?.[1];
|
|
953
|
-
const header =
|
|
954
|
-
`// #include <rainbow/rb_sdk/${rel}>${ns ? ` (namespace ${ns})` : ""}\n` +
|
|
955
|
-
`// link: find_package(rb_sdk) + rb_sdk::rb_sdk\n` +
|
|
956
|
-
(methodName ? `// ${owner}.${methodName}\n` : "") + "\n";
|
|
957
|
-
return { content: [{ type: "text", text: header + body.slice(0, 60_000) }] };
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const { data, error } = introspectRbSdk(`
|
|
961
|
-
import inspect, json
|
|
962
|
-
mod = importlib.import_module("rainbow.rb_sdk")
|
|
963
|
-
name = ${JSON.stringify(className)}
|
|
964
|
-
method = ${methodName ? JSON.stringify(methodName) : "None"}
|
|
965
|
-
obj = getattr(mod, name, None)
|
|
966
|
-
if obj is None:
|
|
967
|
-
print(json.dumps({"__missing__": True})); raise SystemExit(0)
|
|
968
|
-
def sig(o):
|
|
969
|
-
try: return str(inspect.signature(o))
|
|
970
|
-
except Exception: return "(...)"
|
|
971
|
-
|
|
972
|
-
if method:
|
|
973
|
-
# Direct method on the class itself.
|
|
974
|
-
fn = getattr(obj, method, None) if inspect.isclass(obj) else None
|
|
975
|
-
# rb_sdk classes often group calls under a nested namespace set in
|
|
976
|
-
# __init__ (e.g. RBManipulateSDK().move.call_move_j) — that only exists on
|
|
977
|
-
# an INSTANCE, which we won't construct (side effects, needs a robot
|
|
978
|
-
# connection). Best-effort: search the class's own __init__ source text
|
|
979
|
-
# for self.<attr> = <NestedClass>(...) and check each nested class too.
|
|
980
|
-
owner = name
|
|
981
|
-
if fn is None and inspect.isclass(obj):
|
|
982
|
-
try:
|
|
983
|
-
src = inspect.getsource(obj.__init__)
|
|
984
|
-
except Exception:
|
|
985
|
-
src = ""
|
|
986
|
-
import re, sys as _sys
|
|
987
|
-
# The nested sub-SDK classes (e.g. RBManipulateMoveSDK) are imported in
|
|
988
|
-
# the module that DEFINES obj (rainbow.rb_sdk.manipulate), not in the
|
|
989
|
-
# rainbow.rb_sdk top-level namespace — resolve names against both.
|
|
990
|
-
_defmod = _sys.modules.get(getattr(obj, "__module__", ""))
|
|
991
|
-
for attr, nested_name in re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", src):
|
|
992
|
-
nested = (
|
|
993
|
-
(getattr(_defmod, nested_name, None) if _defmod else None)
|
|
994
|
-
or mod.__dict__.get(nested_name)
|
|
995
|
-
or globals().get(nested_name)
|
|
996
|
-
)
|
|
997
|
-
cand = getattr(nested, method, None) if nested else None
|
|
998
|
-
if cand:
|
|
999
|
-
fn = cand
|
|
1000
|
-
owner = f"{name}.{attr} ({nested_name})"
|
|
1001
|
-
break
|
|
1002
|
-
if fn is None:
|
|
1003
|
-
print(json.dumps({"__missing__": True, "__method__": True})); raise SystemExit(0)
|
|
1004
|
-
print(json.dumps({
|
|
1005
|
-
"name": name, "kind": "method", "owner": owner,
|
|
1006
|
-
"signature": method + sig(fn), "doc": inspect.getdoc(fn) or "",
|
|
1007
|
-
}))
|
|
1008
|
-
raise SystemExit(0)
|
|
1009
|
-
|
|
1010
|
-
res = {"name": name, "doc": inspect.getdoc(obj) or ""}
|
|
1011
|
-
if inspect.isclass(obj):
|
|
1012
|
-
res["kind"] = "class"
|
|
1013
|
-
res["init"] = name + sig(obj.__init__ if "__init__" in dir(obj) else obj)
|
|
1014
|
-
def _methods_of(cls, prefix=""):
|
|
1015
|
-
out = []
|
|
1016
|
-
for m, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
1017
|
-
if m.startswith("_"): continue
|
|
1018
|
-
out.append({"name": prefix + m, "sig": prefix + m + sig(fn),
|
|
1019
|
-
"doc": (inspect.getdoc(fn) or "").split("\\n")[0]})
|
|
1020
|
-
return out
|
|
1021
|
-
methods = _methods_of(obj)
|
|
1022
|
-
# Most rb_sdk calls live on nested sub-SDKs assigned in __init__
|
|
1023
|
-
# (e.g. self.move = RBManipulateMoveSDK() -> obj.move.call_move_j). List
|
|
1024
|
-
# those too, name-prefixed, so the API is discoverable without an instance.
|
|
1025
|
-
import re as _re, sys as _sys
|
|
1026
|
-
_defmod = _sys.modules.get(getattr(obj, "__module__", ""))
|
|
1027
|
-
try:
|
|
1028
|
-
_src = inspect.getsource(obj.__init__)
|
|
1029
|
-
except Exception:
|
|
1030
|
-
_src = ""
|
|
1031
|
-
for _attr, _nested_name in _re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", _src):
|
|
1032
|
-
_nested = (getattr(_defmod, _nested_name, None) if _defmod else None) or mod.__dict__.get(_nested_name)
|
|
1033
|
-
if inspect.isclass(_nested):
|
|
1034
|
-
methods.extend(_methods_of(_nested, prefix=_attr + "."))
|
|
1035
|
-
res["methods"] = methods
|
|
1036
|
-
else:
|
|
1037
|
-
res["kind"] = "function"
|
|
1038
|
-
res["signature"] = name + sig(obj)
|
|
1039
|
-
print(json.dumps(res))
|
|
1040
|
-
`);
|
|
1041
|
-
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
1042
|
-
if (data.__missing__) {
|
|
1043
|
-
const text = data.__method__
|
|
1044
|
-
? `No method "${methodName}" found directly on "${className}" (nor in an obvious nested ` +
|
|
1045
|
-
`namespace set in __init__). Call get_rb_sdk_api with just \`name: "${className}"\` to see ` +
|
|
1046
|
-
`its full method list and constructor first.`
|
|
1047
|
-
: `No "${className}" in rainbow.rb_sdk. Call list_rb_sdk for valid names.`;
|
|
1048
|
-
return { isError: true, content: [{ type: "text", text }] };
|
|
1049
|
-
}
|
|
1050
|
-
if (data.kind === "method") {
|
|
1051
|
-
const lines = [`# ${data.owner}.${data.signature.split("(")[0]}`, data.signature];
|
|
1052
|
-
if (data.doc) lines.push("", data.doc);
|
|
1053
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1054
|
-
}
|
|
1055
|
-
const lines = [`# ${data.name} (${data.kind})`];
|
|
1056
|
-
if (data.doc) lines.push(data.doc, "");
|
|
1057
|
-
if (data.kind === "class") {
|
|
1058
|
-
lines.push(`constructor: ${data.init}`, "", "methods:");
|
|
1059
|
-
for (const m of data.methods) {
|
|
1060
|
-
lines.push(` ${m.sig}${m.doc ? ` — ${m.doc}` : ""}`);
|
|
1061
|
-
}
|
|
1062
|
-
} else {
|
|
1063
|
-
lines.push(data.signature);
|
|
1064
|
-
}
|
|
1065
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1066
|
-
},
|
|
1067
|
-
);
|
|
1068
|
-
|
|
1069
|
-
const transport = new StdioServerTransport();
|
|
1070
|
-
await server.connect(transport);
|
|
1071
|
-
}
|
|
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
|
+
// Add to an MCP client config, e.g. Claude Code's .mcp.json:
|
|
22
|
+
// { "mcpServers": { "muscat": { "command": "muscat-pk", "args": ["mcp"] } } }
|
|
23
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import ts from "typescript";
|
|
27
|
+
import {
|
|
28
|
+
appendFileSync,
|
|
29
|
+
existsSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
readdirSync,
|
|
32
|
+
readFileSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
statSync,
|
|
35
|
+
writeFileSync,
|
|
36
|
+
} from "node:fs";
|
|
37
|
+
import { spawnSync } from "node:child_process";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { join, resolve } from "node:path";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
|
|
42
|
+
import { PYTHON_DEV_IMAGE } from "./python.mjs";
|
|
43
|
+
import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
|
|
44
|
+
import { CPP_DEV_IMAGE } from "./cpp.mjs";
|
|
45
|
+
import { NATIVE_TO_RB, SERVER_INSTRUCTIONS } from "./ai-rules.mjs";
|
|
46
|
+
|
|
47
|
+
const TEMPLATE_READMES = { python: "common/README.md", cpp: "readme-cpp/README.md" };
|
|
48
|
+
// Design tokens: prefer the shipped copy, fall back to the monorepo source.
|
|
49
|
+
const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
|
|
50
|
+
? SHIPPED_DESIGN_TOKENS
|
|
51
|
+
: resolve(kitRoot, "../..", "packages/rb-styles/css/output/rb-token.css");
|
|
52
|
+
|
|
53
|
+
/* -------------------------------------------------------------------------- */
|
|
54
|
+
/* react.d.ts introspection — component/hook list + prop types */
|
|
55
|
+
/* -------------------------------------------------------------------------- */
|
|
56
|
+
|
|
57
|
+
// plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components types
|
|
58
|
+
// are inlined at build — see plugin-ui/build-dist.mjs), so this one file is
|
|
59
|
+
// enough even in an external install.
|
|
60
|
+
async function reactDtsPath() {
|
|
61
|
+
const entryUrl = await import.meta.resolve("@rainbow-robotics/plugin-ui/react");
|
|
62
|
+
const dtsPath = fileURLToPath(entryUrl).replace(/\.js$/, ".d.ts");
|
|
63
|
+
if (!existsSync(dtsPath)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`plugin-ui type declarations not found at ${dtsPath} — reinstall or rebuild @rainbow-robotics/plugin-ui.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return dtsPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadReactTypes(dtsPath) {
|
|
72
|
+
// Without moduleResolution the `import ... from "react"` inside react.d.ts
|
|
73
|
+
// never resolves and every React-derived type collapses to `any`.
|
|
74
|
+
const program = ts.createProgram([dtsPath], {
|
|
75
|
+
target: ts.ScriptTarget.ES2022,
|
|
76
|
+
module: ts.ModuleKind.ESNext,
|
|
77
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
78
|
+
skipLibCheck: true,
|
|
79
|
+
});
|
|
80
|
+
const sourceFile = program.getSourceFile(dtsPath);
|
|
81
|
+
const checker = program.getTypeChecker();
|
|
82
|
+
const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
|
|
83
|
+
const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
|
|
84
|
+
return { checker, sourceFile, exports };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The barrel's trailing `export { A, B, ... }` produces Alias symbols whose
|
|
88
|
+
// OWN flags don't reflect value-vs-type — resolve to the real declaration first.
|
|
89
|
+
function resolveExport(checker, symbol) {
|
|
90
|
+
return symbol.flags & ts.SymbolFlags.Alias
|
|
91
|
+
? checker.getAliasedSymbol(symbol)
|
|
92
|
+
: symbol;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function exportKind(checker, symbol) {
|
|
96
|
+
const resolved = resolveExport(checker, symbol);
|
|
97
|
+
return resolved.flags & ts.SymbolFlags.Value ? "value" : "type";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// typeToString alone often prints an opaque alias (`ForwardRefExoticComponent<PropsType$s>`).
|
|
101
|
+
// The first call signature works for plain components and forwardRef alike; its
|
|
102
|
+
// first parameter is the props object.
|
|
103
|
+
function ownProps(checker, type, sourceFile) {
|
|
104
|
+
const [signature] = type.getCallSignatures?.() ?? [];
|
|
105
|
+
const param = signature?.getParameters?.()[0];
|
|
106
|
+
if (!param) return [];
|
|
107
|
+
const declaration = param.valueDeclaration ?? param.declarations?.[0];
|
|
108
|
+
if (!declaration) return [];
|
|
109
|
+
const propsType = checker.getTypeOfSymbolAtLocation(param, declaration);
|
|
110
|
+
|
|
111
|
+
return checker
|
|
112
|
+
.getPropertiesOfType(propsType)
|
|
113
|
+
// Without this, ~220 inherited React DOM attributes bury the component's own API.
|
|
114
|
+
// Own = declared in react.d.ts itself (rb-components types are inlined there). Not a
|
|
115
|
+
// "node_modules" path test: in a plugin project react.d.ts itself lives in node_modules.
|
|
116
|
+
.filter((prop) => (prop.declarations ?? []).some((d) => d.getSourceFile() === sourceFile))
|
|
117
|
+
.map((prop) => {
|
|
118
|
+
const d = prop.valueDeclaration ?? prop.declarations[0];
|
|
119
|
+
const optional = prop.flags & ts.SymbolFlags.Optional ? "?" : "";
|
|
120
|
+
return `${prop.getName()}${optional}: ${checker.typeToString(
|
|
121
|
+
checker.getTypeOfSymbolAtLocation(prop, d),
|
|
122
|
+
)}`;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Kept out of the tool handler so every export can be precomputed and cached.
|
|
127
|
+
function propsText(checker, sourceFile, name, symbol) {
|
|
128
|
+
const resolved = resolveExport(checker, symbol);
|
|
129
|
+
const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
|
|
130
|
+
const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
|
|
131
|
+
const signature = checker.typeToString(
|
|
132
|
+
type,
|
|
133
|
+
undefined,
|
|
134
|
+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const props = ownProps(checker, type, sourceFile);
|
|
138
|
+
if (props.length) {
|
|
139
|
+
// Keep the signature line: RBSlider's value/min/max come from Radix and the
|
|
140
|
+
// own-props filter drops them.
|
|
141
|
+
return `# ${name} — own props\n${props.sort().join("\n")}\n\nfull type: ${signature}`;
|
|
142
|
+
}
|
|
143
|
+
// No own props means either a Radix wrapper or unresolved React types. The two
|
|
144
|
+
// look identical, so name the second case explicitly.
|
|
145
|
+
if (/<any>/.test(signature)) {
|
|
146
|
+
return (
|
|
147
|
+
`# ${name} — prop types could not be resolved\n${signature}\n\n` +
|
|
148
|
+
"React's types didn't resolve, so this collapsed to `any`. Install `@types/react` " +
|
|
149
|
+
"in this project (`npm i -D @types/react`) and retry for the real props."
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
`# ${name} — full type (no plugin-ui-specific props; it forwards a third-party ` +
|
|
154
|
+
`component's props)\n${signature}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Rendering every export up front is what makes the result cacheable: once the
|
|
159
|
+
// text exists no TypeChecker is needed.
|
|
160
|
+
function buildUiIndex(dtsPath) {
|
|
161
|
+
const { checker, sourceFile, exports } = loadReactTypes(dtsPath);
|
|
162
|
+
const props = {};
|
|
163
|
+
for (const symbol of exports) {
|
|
164
|
+
props[symbol.getName()] = propsText(checker, sourceFile, symbol.getName(), symbol);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
components: exports
|
|
168
|
+
.map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
|
|
169
|
+
.sort()
|
|
170
|
+
.join("\n"),
|
|
171
|
+
props,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ts.createProgram pulls in ~1100 source files (~1.5s) and an MCP server is a fresh
|
|
176
|
+
// process per session. The key covers the .d.ts identity and the kit version.
|
|
177
|
+
const UI_INDEX_CACHE_DIR = join(homedir(), ".cache", "muscat-pk");
|
|
178
|
+
const UI_INDEX_CACHE = join(UI_INDEX_CACHE_DIR, "ui-index.json");
|
|
179
|
+
const UI_INDEX_FORMAT = 2;
|
|
180
|
+
|
|
181
|
+
let uiIndexCache = null;
|
|
182
|
+
|
|
183
|
+
async function loadUiIndex() {
|
|
184
|
+
if (uiIndexCache) return uiIndexCache;
|
|
185
|
+
const dtsPath = await reactDtsPath();
|
|
186
|
+
const { mtimeMs, size } = statSync(dtsPath);
|
|
187
|
+
// Bump UI_INDEX_FORMAT when the rendered text changes, so dev builds (same version) rebuild too.
|
|
188
|
+
const key = `${UI_INDEX_FORMAT}|${dtsPath}|${mtimeMs}|${size}|${kitVersion()}`;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const cached = JSON.parse(readFileSync(UI_INDEX_CACHE, "utf8"));
|
|
192
|
+
// A half-written file still parses as JSON, so check the shape too.
|
|
193
|
+
if (
|
|
194
|
+
cached.key === key &&
|
|
195
|
+
typeof cached.components === "string" &&
|
|
196
|
+
cached.props &&
|
|
197
|
+
typeof cached.props === "object"
|
|
198
|
+
) {
|
|
199
|
+
return (uiIndexCache = cached);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
/* absent, unreadable or corrupt — rebuild below */
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
uiIndexCache = { key, ...buildUiIndex(dtsPath) };
|
|
206
|
+
try {
|
|
207
|
+
mkdirSync(UI_INDEX_CACHE_DIR, { recursive: true });
|
|
208
|
+
// rename is atomic, so a concurrent server never reads a half-written file.
|
|
209
|
+
const tmp = `${UI_INDEX_CACHE}.${process.pid}.tmp`;
|
|
210
|
+
writeFileSync(tmp, JSON.stringify(uiIndexCache));
|
|
211
|
+
renameSync(tmp, UI_INDEX_CACHE);
|
|
212
|
+
} catch {
|
|
213
|
+
/* read-only fs / sandbox: the cache is an optimisation, never a requirement */
|
|
214
|
+
}
|
|
215
|
+
return uiIndexCache;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* -------------------------------------------------------------------------- */
|
|
219
|
+
/* Storybook story source lookup */
|
|
220
|
+
/* -------------------------------------------------------------------------- */
|
|
221
|
+
|
|
222
|
+
// Case-insensitive recursive search for `${name}.stories.tsx` under the
|
|
223
|
+
// staged story sources (see storybook.mjs's buildStorybook()).
|
|
224
|
+
function findStorySource(name) {
|
|
225
|
+
if (!existsSync(SHIPPED_STORIES_SRC)) return null;
|
|
226
|
+
const target = `${name.toLowerCase()}.stories.tsx`;
|
|
227
|
+
const stack = [SHIPPED_STORIES_SRC];
|
|
228
|
+
while (stack.length) {
|
|
229
|
+
const dir = stack.pop();
|
|
230
|
+
for (const entry of readdirSync(dir)) {
|
|
231
|
+
const full = join(dir, entry);
|
|
232
|
+
if (statSync(full).isDirectory()) {
|
|
233
|
+
stack.push(full);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (entry.toLowerCase() === target) return readFileSync(full, "utf8");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Stories are written against the monorepo (@repo/rb-components…). Point those imports at the
|
|
243
|
+
// package a plugin actually has, and flag names plugin-ui does not re-export.
|
|
244
|
+
function rewriteStoryImports(source, exportNames) {
|
|
245
|
+
const exported = new Set(exportNames);
|
|
246
|
+
const missing = new Set();
|
|
247
|
+
const text = source.replace(
|
|
248
|
+
/import\s+(type\s+)?\{([^}]*)\}\s*from\s*["']@repo\/rb-components[^"']*["'];?/g,
|
|
249
|
+
(_, typeOnly = "", names) => {
|
|
250
|
+
const list = names.split(",").map((n) => n.trim()).filter(Boolean);
|
|
251
|
+
for (const n of list) if (!exported.has(n.replace(/^type\s+/, "").split(/\s+as\s+/)[0])) missing.add(n);
|
|
252
|
+
return `import ${typeOnly}{ ${list.join(", ")} } from "@rainbow-robotics/plugin-ui/react";`;
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
const notes = [
|
|
256
|
+
"// Storybook story source. In a plugin, import components from \"@rainbow-robotics/plugin-ui/react\"",
|
|
257
|
+
"// (rewritten below); @storybook/* imports, Meta/StoryObj and play() are Storybook-only.",
|
|
258
|
+
...(missing.size ? [`// Not exported by plugin-ui, do not use in a plugin: ${[...missing].join(", ")}`] : []),
|
|
259
|
+
];
|
|
260
|
+
return `${notes.join("\n")}\n\n${text}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const editDistance = (a, b) => {
|
|
264
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
265
|
+
for (let i = 1; i <= a.length; i++) {
|
|
266
|
+
let prev = row[0]++;
|
|
267
|
+
for (let j = 1; j <= b.length; j++) {
|
|
268
|
+
const cur = row[j];
|
|
269
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
270
|
+
prev = cur;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return row[b.length];
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// "RBSelect" → RBSelectBox, RBSelectOption, …; "select" (a native element) → its plugin-ui replacement.
|
|
277
|
+
function suggestExports(name, exportNames) {
|
|
278
|
+
const q = name.toLowerCase().replace(/[<>]/g, "");
|
|
279
|
+
const native = NATIVE_TO_RB.filter(([el]) => el.split(/[\s/=]+/).includes(q)).map(([, rb]) => rb);
|
|
280
|
+
const bare = q.replace(/^rb/, "");
|
|
281
|
+
const close = exportNames
|
|
282
|
+
.map((n) => {
|
|
283
|
+
const lower = n.toLowerCase();
|
|
284
|
+
const score = lower.includes(q) || (bare.length > 2 && lower.includes(bare)) ? 0 : editDistance(q, lower);
|
|
285
|
+
return { n, score };
|
|
286
|
+
})
|
|
287
|
+
.filter((x) => x.score <= 3)
|
|
288
|
+
.sort((a, b) => a.score - b.score || a.n.length - b.n.length)
|
|
289
|
+
.slice(0, 6)
|
|
290
|
+
.map((x) => x.n);
|
|
291
|
+
return [...native, ...close];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function unknownExportText(name, exportNames, what) {
|
|
295
|
+
const hints = suggestExports(name, exportNames);
|
|
296
|
+
return (
|
|
297
|
+
`No ${what} named "${name}" in @rainbow-robotics/plugin-ui/react.` +
|
|
298
|
+
(hints.length ? ` Did you mean: ${hints.join(", ")}?` : "") +
|
|
299
|
+
" Call list_components for valid names. Do not fall back to a native HTML element."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* -------------------------------------------------------------------------- */
|
|
304
|
+
/* README search */
|
|
305
|
+
/* -------------------------------------------------------------------------- */
|
|
306
|
+
|
|
307
|
+
// Split the README on headings and keyword-score each section.
|
|
308
|
+
// Headings inside fenced code blocks (```…```) don't count — a column-0
|
|
309
|
+
// Python comment would otherwise start a bogus section.
|
|
310
|
+
function splitSections(text) {
|
|
311
|
+
const sections = [];
|
|
312
|
+
let current = [];
|
|
313
|
+
let inFence = false;
|
|
314
|
+
for (const line of text.split("\n")) {
|
|
315
|
+
if (/^\s*```/.test(line)) inFence = !inFence;
|
|
316
|
+
if (!inFence && /^#{1,4} /.test(line) && current.length) {
|
|
317
|
+
sections.push(current.join("\n"));
|
|
318
|
+
current = [];
|
|
319
|
+
}
|
|
320
|
+
current.push(line);
|
|
321
|
+
}
|
|
322
|
+
if (current.length) sections.push(current.join("\n"));
|
|
323
|
+
return sections;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Standalone binaries have no templates/ on disk.
|
|
327
|
+
async function readTemplateReadme(rel) {
|
|
328
|
+
const onDisk = join(kitRoot, "cli/templates", rel);
|
|
329
|
+
if (existsSync(onDisk)) return readFileSync(onDisk, "utf8");
|
|
330
|
+
try {
|
|
331
|
+
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
332
|
+
return TEMPLATES[rel] ? Buffer.from(TEMPLATES[rel], "base64").toString("utf8") : "";
|
|
333
|
+
} catch {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function searchReadme(query, lang) {
|
|
339
|
+
const langs = lang ? [lang] : ["python", "cpp"];
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
const sections = [];
|
|
342
|
+
for (const l of langs) {
|
|
343
|
+
for (const section of splitSections(await readTemplateReadme(TEMPLATE_READMES[l]))) {
|
|
344
|
+
if (seen.has(section)) continue; // UI/manifest sections are shared by both READMEs
|
|
345
|
+
seen.add(section);
|
|
346
|
+
sections.push(langs.length > 1 && l === "cpp" ? `[C++ backend]\n${section}` : section);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
350
|
+
|
|
351
|
+
return sections
|
|
352
|
+
.map((section) => {
|
|
353
|
+
const lower = section.toLowerCase();
|
|
354
|
+
const score = terms.reduce((n, term) => n + (lower.includes(term) ? 1 : 0), 0);
|
|
355
|
+
return { section: section.trim(), score };
|
|
356
|
+
})
|
|
357
|
+
.filter((x) => x.score > 0)
|
|
358
|
+
.sort((a, b) => b.score - a.score)
|
|
359
|
+
.slice(0, 5)
|
|
360
|
+
.map((x) => x.section);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* -------------------------------------------------------------------------- */
|
|
364
|
+
/* Design tokens — parsed from rb-styles' generated rb-token.css */
|
|
365
|
+
/* -------------------------------------------------------------------------- */
|
|
366
|
+
|
|
367
|
+
// A color var `--color-<name>` becomes Tailwind utilities `bg-<name>`,
|
|
368
|
+
// `text-<name>`, `border-<name>`, `fill-<name>`. The design system's naming
|
|
369
|
+
// already encodes intent (bg-/fg-/border-/fill-), so we surface the idiomatic
|
|
370
|
+
// class per group.
|
|
371
|
+
const COLOR_CLASS_HINT = {
|
|
372
|
+
bg: (n) => `bg-${n}`, // --color-bg-default → bg-bg-default
|
|
373
|
+
fg: (n) => `text-${n}`, // --color-fg-subtle → text-fg-subtle
|
|
374
|
+
border: (n) => `border-${n}`,
|
|
375
|
+
fill: (n) => `fill-${n}`,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
let tokensCache = null;
|
|
379
|
+
|
|
380
|
+
function loadDesignTokens() {
|
|
381
|
+
if (tokensCache) return tokensCache;
|
|
382
|
+
if (!existsSync(DESIGN_TOKENS_CSS)) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`design token CSS not found at ${DESIGN_TOKENS_CSS} — rebuild via "muscat-pk storybook --rebuild" (monorepo) or reinstall.`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const css = readFileSync(DESIGN_TOKENS_CSS, "utf8");
|
|
388
|
+
|
|
389
|
+
// @theme { --name: value; ... } → { group, name, value, class? }
|
|
390
|
+
const vars = [];
|
|
391
|
+
for (const [, rawName, value] of css.matchAll(
|
|
392
|
+
/^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);/gim,
|
|
393
|
+
)) {
|
|
394
|
+
const name = rawName.replace(/^--/, "");
|
|
395
|
+
const m = name.match(/^color-(bg|fg|border|fill)-(.+)$/);
|
|
396
|
+
const token = { name, value: value.trim() };
|
|
397
|
+
if (m) {
|
|
398
|
+
token.group = `color-${m[1]}`;
|
|
399
|
+
token.class = COLOR_CLASS_HINT[m[1]](`${m[1]}-${m[2]}`);
|
|
400
|
+
} else {
|
|
401
|
+
token.group = name.split("-").slice(0, 2).join("-");
|
|
402
|
+
}
|
|
403
|
+
vars.push(token);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// @utility typo-… → composite typography classes used directly in className.
|
|
407
|
+
const typo = [
|
|
408
|
+
...new Set(
|
|
409
|
+
[...css.matchAll(/@utility\s+(typo-[a-z0-9-]+)\s*\{/g)].map((m) => m[1]),
|
|
410
|
+
),
|
|
411
|
+
].sort();
|
|
412
|
+
|
|
413
|
+
tokensCache = { vars, typo };
|
|
414
|
+
return tokensCache;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* -------------------------------------------------------------------------- */
|
|
418
|
+
/* rainbow-rb-sdk (Python) introspection */
|
|
419
|
+
/* -------------------------------------------------------------------------- */
|
|
420
|
+
|
|
421
|
+
function pythonCmd() {
|
|
422
|
+
// Prefer the current plugin's own .venv — that's where `muscat-pk new` /
|
|
423
|
+
// ensureIdeVenv() installs rainbow-rb-sdk from requirements.txt. The AI
|
|
424
|
+
// agent's cwd is usually the plugin dir mcp was launched from.
|
|
425
|
+
try {
|
|
426
|
+
const py = venvPython(resolve(findPluginRoot(), ".venv"));
|
|
427
|
+
if (existsSync(py)) return py;
|
|
428
|
+
} catch {
|
|
429
|
+
/* not inside a plugin project — fall through to PATH lookup */
|
|
430
|
+
}
|
|
431
|
+
for (const c of ["python3", "python"]) {
|
|
432
|
+
const r = spawnSync(c, ["--version"], { encoding: "utf8" });
|
|
433
|
+
if (r.status === 0) return c;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Run an introspection script against the installed rainbow.rb_sdk (a plugin's
|
|
439
|
+
// backend dependency). Returns a clear hint if it isn't importable.
|
|
440
|
+
function introspectRbSdk(script) {
|
|
441
|
+
const py = pythonCmd();
|
|
442
|
+
const probe = `
|
|
443
|
+
import json, importlib
|
|
444
|
+
try:
|
|
445
|
+
importlib.import_module("rainbow.rb_sdk")
|
|
446
|
+
except Exception as e:
|
|
447
|
+
print(json.dumps({"__notinstalled__": str(e)})); raise SystemExit(0)
|
|
448
|
+
${script}
|
|
449
|
+
`;
|
|
450
|
+
if (!py) {
|
|
451
|
+
return introspectInDocker(probe) ?? { error: "Python not found on PATH. Install Python 3 to use rb-sdk introspection." };
|
|
452
|
+
}
|
|
453
|
+
const r = spawnSync(py, ["-c", probe], { encoding: "utf8", maxBuffer: 8 << 20 });
|
|
454
|
+
if (r.status !== 0) {
|
|
455
|
+
return { error: `python introspection failed: ${(r.stderr || "").trim().slice(0, 500)}` };
|
|
456
|
+
}
|
|
457
|
+
const local = parseProbe(r);
|
|
458
|
+
return local.error && local.notInstalled ? introspectInDocker(probe) ?? local : local;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// No macOS wheels: import the plugin's pinned SDK inside the python dev image (cached).
|
|
462
|
+
function introspectInDocker(probe) {
|
|
463
|
+
if (spawnSync("docker", ["image", "inspect", PYTHON_DEV_IMAGE], { encoding: "utf8" }).status !== 0) return null;
|
|
464
|
+
let ver = "";
|
|
465
|
+
try {
|
|
466
|
+
const req = readFileSync(join(findPluginRoot(), "requirements.txt"), "utf8");
|
|
467
|
+
ver = req.match(/^rainbow-rb-sdk==(\S+)/m)?.[1] ?? "";
|
|
468
|
+
} catch {
|
|
469
|
+
/* not a python plugin — latest */
|
|
470
|
+
}
|
|
471
|
+
// ponytail: "latest" cache is never refreshed; delete ~/.cache/... rb-sdk-py/latest to update.
|
|
472
|
+
const cache = join(UI_INDEX_CACHE_DIR, "rb-sdk-py", ver || "latest");
|
|
473
|
+
mkdirSync(cache, { recursive: true });
|
|
474
|
+
const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
|
|
475
|
+
const script =
|
|
476
|
+
`[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
|
|
477
|
+
`PYTHONPATH=/c python3 -c "$PROBE"`;
|
|
478
|
+
const r = spawnSync("docker", ["run", "--rm", "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
|
|
479
|
+
encoding: "utf8",
|
|
480
|
+
maxBuffer: 8 << 20,
|
|
481
|
+
env: { ...process.env, PROBE: probe },
|
|
482
|
+
});
|
|
483
|
+
if (r.status !== 0) return { error: `python introspection (docker) failed: ${(r.stderr || "").trim().slice(-500)}` };
|
|
484
|
+
return parseProbe(r);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function parseProbe(r) {
|
|
488
|
+
// Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
|
|
489
|
+
// Zenoh client logs its connection endpoints), ahead of our own final
|
|
490
|
+
// print(json.dumps(...)) — so parse the last line, not the whole output.
|
|
491
|
+
const lines = r.stdout.trim().split("\n");
|
|
492
|
+
let data;
|
|
493
|
+
try {
|
|
494
|
+
data = JSON.parse(lines[lines.length - 1]);
|
|
495
|
+
} catch {
|
|
496
|
+
return { error: `unexpected introspection output: ${r.stdout.slice(0, 300)}` };
|
|
497
|
+
}
|
|
498
|
+
if (data && data.__notinstalled__) {
|
|
499
|
+
return {
|
|
500
|
+
notInstalled: true,
|
|
501
|
+
error:
|
|
502
|
+
"rainbow-rb-sdk is not installed in this environment. Install it with `pip install rainbow-rb-sdk` " +
|
|
503
|
+
"(it's already in a plugin's requirements.txt), then retry.",
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return { data };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* -------------------------------------------------------------------------- */
|
|
510
|
+
/* rainbow-rb-sdk-dev (C++) headers — copied out of the cpp dev image */
|
|
511
|
+
/* -------------------------------------------------------------------------- */
|
|
512
|
+
|
|
513
|
+
const CPP_INCLUDE_CACHE = join(UI_INDEX_CACHE_DIR, "rb-sdk-include");
|
|
514
|
+
// Headers we don't want to surface: third-party vendored code, pybind glue,
|
|
515
|
+
// flatbuffers-generated schemas, and the industrial comm stacks.
|
|
516
|
+
const CPP_SKIP = /^(vendor|binding|rb_comm_source)\/|_generated\.h$/;
|
|
517
|
+
|
|
518
|
+
// The apt package is only installed inside the muscat-pk-cpp-dev image, so
|
|
519
|
+
// copy /usr/include/rainbow/rb_sdk out once per image build (cache key =
|
|
520
|
+
// image id) with `docker cp` — no host tar needed.
|
|
521
|
+
function cppIncludeDir() {
|
|
522
|
+
const insp = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
523
|
+
if (insp.error) return { error: "Docker not found — the C++ SDK headers live in the muscat-pk cpp dev image." };
|
|
524
|
+
if (insp.status !== 0) {
|
|
525
|
+
return {
|
|
526
|
+
error:
|
|
527
|
+
`Docker image ${CPP_DEV_IMAGE} not built yet. Run \`muscat-pk dev:back\` (or \`muscat-pk build\`) once ` +
|
|
528
|
+
"in a cpp / rclcpp plugin to build it, then retry.",
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const dir = join(CPP_INCLUDE_CACHE, insp.stdout.trim().replace(/^sha256:/, "").slice(0, 12));
|
|
532
|
+
const out = join(dir, "rb_sdk");
|
|
533
|
+
if (existsSync(out)) return { dir: out };
|
|
534
|
+
mkdirSync(dir, { recursive: true });
|
|
535
|
+
const ctr = spawnSync("docker", ["create", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
536
|
+
if (ctr.status !== 0) return { error: `docker create failed: ${(ctr.stderr || "").trim().slice(0, 300)}` };
|
|
537
|
+
const id = ctr.stdout.trim();
|
|
538
|
+
const cp = spawnSync("docker", ["cp", `${id}:/usr/include/rainbow/rb_sdk`, dir], { encoding: "utf8" });
|
|
539
|
+
spawnSync("docker", ["rm", "-f", id], { encoding: "utf8" });
|
|
540
|
+
if (cp.status !== 0 || !existsSync(out)) {
|
|
541
|
+
return { error: `docker cp failed: ${(cp.stderr || "").trim().slice(0, 300)}` };
|
|
542
|
+
}
|
|
543
|
+
return { dir: out };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function cppHeaders(root) {
|
|
547
|
+
const files = [];
|
|
548
|
+
const walk = (d, rel) => {
|
|
549
|
+
for (const name of readdirSync(d)) {
|
|
550
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
551
|
+
if (CPP_SKIP.test(r)) continue;
|
|
552
|
+
const full = join(d, name);
|
|
553
|
+
if (statSync(full).isDirectory()) walk(full, r);
|
|
554
|
+
else if (/\.hpp$/.test(name)) files.push(r);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
walk(root, "");
|
|
558
|
+
return files.sort();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// "class Foo {" / "class Foo : public Bar {" — not forward declarations.
|
|
562
|
+
const CLASS_RE = /^\s*(?:class|struct)\s+(?:[A-Z][A-Z0-9_]*_API\s+)?(\w+)\b(?![^\n]*;\s*$)/; // skip export macros (RB_SDK_API)
|
|
563
|
+
|
|
564
|
+
function cppClassesIn(text) {
|
|
565
|
+
return [...new Set(text.split("\n").map((l) => l.match(CLASS_RE)?.[1]).filter(Boolean))];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Return the class body (brace-matched) plus the file's leading comment block,
|
|
569
|
+
// which in these headers usually carries a full usage example.
|
|
570
|
+
function cppExtract(text, className, method) {
|
|
571
|
+
const lines = text.split("\n");
|
|
572
|
+
const start = lines.findIndex((l) => CLASS_RE.test(l) && l.match(CLASS_RE)[1] === className);
|
|
573
|
+
if (start < 0) return null;
|
|
574
|
+
let depth = 0, end = start, seen = false;
|
|
575
|
+
for (let i = start; i < lines.length; i++) {
|
|
576
|
+
for (const ch of lines[i]) {
|
|
577
|
+
if (ch === "{") { depth++; seen = true; }
|
|
578
|
+
else if (ch === "}") depth--;
|
|
579
|
+
}
|
|
580
|
+
if (seen && depth === 0) { end = i; break; }
|
|
581
|
+
}
|
|
582
|
+
const body = lines.slice(start, end + 1);
|
|
583
|
+
if (method) {
|
|
584
|
+
// Declarations sit at class-member indent (<= 2 spaces) and the name is
|
|
585
|
+
// not preceded by `.`/`->`/`::` — that skips call sites inside bodies.
|
|
586
|
+
const re = new RegExp(`^ {0,2}[^(]*(?<![\\w.>:])${method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
|
|
587
|
+
const hits = [];
|
|
588
|
+
for (let i = 0; i < body.length; i++) {
|
|
589
|
+
if (!re.test(body[i]) || /^\s*\/\//.test(body[i])) continue;
|
|
590
|
+
let a = i;
|
|
591
|
+
while (a > 0 && /^\s*\/\//.test(body[a - 1])) a--;
|
|
592
|
+
let b = i;
|
|
593
|
+
while (b < body.length - 1 && !/[{;]/.test(body[b])) b++;
|
|
594
|
+
hits.push(body.slice(a, b + 1).join("\n"));
|
|
595
|
+
}
|
|
596
|
+
return hits.length ? hits.join("\n\n") : null;
|
|
597
|
+
}
|
|
598
|
+
const head = [];
|
|
599
|
+
for (const l of lines) {
|
|
600
|
+
if (/^\s*\/\//.test(l)) head.push(l);
|
|
601
|
+
else if (l.trim() === "" || /^#pragma/.test(l)) continue;
|
|
602
|
+
else break;
|
|
603
|
+
}
|
|
604
|
+
return (head.length > 3 ? head.join("\n") + "\n\n" : "") + body.join("\n");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function cppFind(className) {
|
|
608
|
+
const { dir, error } = cppIncludeDir();
|
|
609
|
+
if (error) return { error };
|
|
610
|
+
for (const rel of cppHeaders(dir)) {
|
|
611
|
+
const text = readFileSync(join(dir, rel), "utf8");
|
|
612
|
+
if (cppClassesIn(text).includes(className)) return { rel, text };
|
|
613
|
+
}
|
|
614
|
+
return { error: `No class "${className}" in the C++ SDK headers. Call list_rb_sdk with lang "cpp" for valid names.` };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* -------------------------------------------------------------------------- */
|
|
618
|
+
/* client registration — `muscat-pk mcp --install <client>` */
|
|
619
|
+
/* -------------------------------------------------------------------------- */
|
|
620
|
+
|
|
621
|
+
// Claude Code needs no install step: `muscat-pk new` scaffolds a project-level
|
|
622
|
+
// .mcp.json. Codex only reads the global ~/.codex/config.toml, so we append
|
|
623
|
+
// the server block there (idempotent).
|
|
624
|
+
export function installMcp(target) {
|
|
625
|
+
if (target !== "codex") {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Unknown --install target ${JSON.stringify(target ?? "")} — supported: "codex". ` +
|
|
628
|
+
"(Claude Code works out of the box via the scaffolded .mcp.json.)",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const cfgDir = join(homedir(), ".codex");
|
|
632
|
+
const cfgPath = join(cfgDir, "config.toml");
|
|
633
|
+
const existing = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : "";
|
|
634
|
+
if (/^\s*\[mcp_servers\.muscat\]/m.test(existing)) {
|
|
635
|
+
console.log(`Codex already has the muscat MCP server registered (${cfgPath}) — nothing to do.`);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
// ponytail: Windows Codex spawns without a shell, so it needs the .cmd shim.
|
|
639
|
+
const command = process.platform === "win32" ? "muscat-pk.cmd" : "muscat-pk";
|
|
640
|
+
mkdirSync(cfgDir, { recursive: true });
|
|
641
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
642
|
+
appendFileSync(
|
|
643
|
+
cfgPath,
|
|
644
|
+
`${sep}[mcp_servers.muscat]\ncommand = "${command}"\nargs = ["mcp"]\n`,
|
|
645
|
+
);
|
|
646
|
+
console.log(`Registered the muscat MCP server for Codex → ${cfgPath}`);
|
|
647
|
+
console.log("Restart Codex (or start a new session) to pick it up.");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* -------------------------------------------------------------------------- */
|
|
651
|
+
/* server */
|
|
652
|
+
/* -------------------------------------------------------------------------- */
|
|
653
|
+
|
|
654
|
+
export async function runMcp() {
|
|
655
|
+
const server = new McpServer(
|
|
656
|
+
{ name: "muscat-plugin-kit", version: kitVersion() },
|
|
657
|
+
{ instructions: SERVER_INSTRUCTIONS },
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
server.registerTool(
|
|
661
|
+
"list_components",
|
|
662
|
+
{
|
|
663
|
+
title: "List Muscat plugin UI components",
|
|
664
|
+
description:
|
|
665
|
+
"List every component, hook, and type exported from @rainbow-robotics/plugin-ui/react — " +
|
|
666
|
+
"the full UI surface available to Muscat plugin authors. Call this first before guessing a " +
|
|
667
|
+
"component name; plugin UI uses these instead of native <select>/<input>/<button> elements.",
|
|
668
|
+
inputSchema: {},
|
|
669
|
+
},
|
|
670
|
+
async () => {
|
|
671
|
+
const { components } = await loadUiIndex();
|
|
672
|
+
return { content: [{ type: "text", text: components }] };
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
server.registerTool(
|
|
677
|
+
"get_component_props",
|
|
678
|
+
{
|
|
679
|
+
title: "Get a component or hook's real type signature",
|
|
680
|
+
description:
|
|
681
|
+
"Get the full TypeScript type for a named export of @rainbow-robotics/plugin-ui/react " +
|
|
682
|
+
"(e.g. RBSlider, RBLabelButton, useRb, useRbFormData). Use list_components to find valid names.",
|
|
683
|
+
inputSchema: { name: z.string().describe('Exported name, e.g. "RBSlider"') },
|
|
684
|
+
},
|
|
685
|
+
async ({ name }) => {
|
|
686
|
+
const { props } = await loadUiIndex();
|
|
687
|
+
if (!Object.hasOwn(props, name)) {
|
|
688
|
+
return { isError: true, content: [{ type: "text", text: unknownExportText(name, Object.keys(props), "export") }] };
|
|
689
|
+
}
|
|
690
|
+
return { content: [{ type: "text", text: props[name] }] };
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
server.registerTool(
|
|
695
|
+
"get_component_example",
|
|
696
|
+
{
|
|
697
|
+
title: "Get a real Storybook usage example",
|
|
698
|
+
description:
|
|
699
|
+
"Get the real Storybook story source for a component (e.g. RBSlider) — actual working " +
|
|
700
|
+
"props, imports, and layout, not a guessed shape. Prefer this over get_component_props when " +
|
|
701
|
+
"you just need to know how to use a component, not its full type.",
|
|
702
|
+
// `component` accepted as an alias — agents often guess it over `name`.
|
|
703
|
+
inputSchema: {
|
|
704
|
+
name: z.string().optional().describe('Component name, e.g. "RBSlider"'),
|
|
705
|
+
component: z.string().optional().describe("Alias for `name`."),
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
async ({ name, component }) => {
|
|
709
|
+
const target = name ?? component;
|
|
710
|
+
if (!target) {
|
|
711
|
+
return {
|
|
712
|
+
isError: true,
|
|
713
|
+
content: [{ type: "text", text: 'Missing required argument: "name" (component name, e.g. "RBSlider").' }],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
const exportNames = Object.keys((await loadUiIndex()).props);
|
|
717
|
+
const source = findStorySource(target);
|
|
718
|
+
if (!source) {
|
|
719
|
+
// An existing export without a story still has a type; an unknown name gets suggestions.
|
|
720
|
+
const text = exportNames.includes(target)
|
|
721
|
+
? `No Storybook example for "${target}". Call get_component_props for its type, or run "muscat-pk storybook" to browse components live.`
|
|
722
|
+
: unknownExportText(target, exportNames, "component");
|
|
723
|
+
return { content: [{ type: "text", text }] };
|
|
724
|
+
}
|
|
725
|
+
return { content: [{ type: "text", text: rewriteStoryImports(source, exportNames) }] };
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
server.registerTool(
|
|
730
|
+
"search_rb_sdk",
|
|
731
|
+
{
|
|
732
|
+
title: "Search the Muscat plugin-author docs",
|
|
733
|
+
description:
|
|
734
|
+
"Search the Muscat plugin-author README for how to use useRb(), rb.pub/sub/action/serve, " +
|
|
735
|
+
"plugin.router (external endpoints: router.on/subscribe/emit exposed over REST/Socket.IO/TCP/" +
|
|
736
|
+
"MQTT/WebRTC at common/plugin/{plugin_id}/{path}, PluginRouteError, meta.transport/principal), " +
|
|
737
|
+
"rb.toast/alert/confirm/prompt, useRbFormData/useRbSave (save/restore form data through the " +
|
|
738
|
+
"host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
|
|
739
|
+
"names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
|
|
740
|
+
"set_variable/executor_state/check_stop), video streaming (MediaMTX: backend publishes RTSP to " +
|
|
741
|
+
"127.0.0.1:8554, UI plays WebRTC from :8889), ROS 2 topics (plugin namespace, rb.rosPeers to allow " +
|
|
742
|
+
"an external PC), contribution points, and backend runtimes " +
|
|
743
|
+
"(Python/C++/ROS2). Covers BOTH backends: the Python README (py/main.py, rainbow.rb_sdk) and the " +
|
|
744
|
+
"C++ README (cpp/main.cpp or an rclcpp node: rb_plugin::RBPluginCall serve/subscribe/publish/" +
|
|
745
|
+
"router(), rb_plugin::PluginProgramContext, RBManipulateSDK, CMake rb_sdk::rb_sdk). Pass lang " +
|
|
746
|
+
"\"python\" or \"cpp\" to match the plugin's manifest runtime (cpp or ros2+rclcpp → \"cpp\"); " +
|
|
747
|
+
"omit it to search both. IMPORTANT: " +
|
|
748
|
+
"the backend pattern depends on the contribution point — search 'which backend pattern' before " +
|
|
749
|
+
"writing the backend. Only a program.panel command uses PluginProgramContext; a toolbar/move.panel " +
|
|
750
|
+
"plugin controls the robot with the SDK (RBManipulateSDK/RBAmrSDK) directly.",
|
|
751
|
+
inputSchema: {
|
|
752
|
+
query: z.string().describe('Keywords, e.g. "save form data" or "rb.action"'),
|
|
753
|
+
lang: z.enum(["python", "cpp"]).optional().describe("Backend language; omit to search both"),
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
async ({ query, lang }) => {
|
|
757
|
+
const sections = await searchReadme(query, lang);
|
|
758
|
+
if (sections.length === 0) {
|
|
759
|
+
return { content: [{ type: "text", text: `No matching section found for "${query}".` }] };
|
|
760
|
+
}
|
|
761
|
+
return { content: [{ type: "text", text: sections.join("\n\n---\n\n") }] };
|
|
762
|
+
},
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
/* --- design tokens --- */
|
|
766
|
+
|
|
767
|
+
server.registerTool(
|
|
768
|
+
"list_design_tokens",
|
|
769
|
+
{
|
|
770
|
+
title: "List Muscat design tokens (Tailwind classes)",
|
|
771
|
+
description:
|
|
772
|
+
"List the design system's tokens as the Tailwind classes to use in a plugin's className: " +
|
|
773
|
+
"semantic colors (bg-bg-*, text-fg-*, border-border-*, fill-fill-*), typography (typo-*), " +
|
|
774
|
+
"shadows, radius, spacing. Use these instead of raw hex/px so the plugin matches the host theme " +
|
|
775
|
+
"(and light/dark). Optionally filter by category.",
|
|
776
|
+
inputSchema: {
|
|
777
|
+
category: z
|
|
778
|
+
.enum(["bg", "fg", "border", "fill", "typography", "shadow", "radius", "spacing", "all"])
|
|
779
|
+
.optional()
|
|
780
|
+
.describe('Filter, e.g. "bg", "fg", "typography". Omit for a grouped overview.'),
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
async ({ category }) => {
|
|
784
|
+
const { vars, typo } = loadDesignTokens();
|
|
785
|
+
const out = [];
|
|
786
|
+
|
|
787
|
+
const colorGroups = { bg: [], fg: [], border: [], fill: [] };
|
|
788
|
+
const other = { shadow: [], radius: [], spacing: [] };
|
|
789
|
+
for (const t of vars) {
|
|
790
|
+
const m = t.group?.match(/^color-(bg|fg|border|fill)$/);
|
|
791
|
+
// Class name only: the value is just an indirection to another CSS var
|
|
792
|
+
// (var(--color-scale-zinc-200)), and each class is declared twice (light/dark)
|
|
793
|
+
// with different values — dropping the value lets the Set dedup collapse them.
|
|
794
|
+
if (m && t.class) colorGroups[m[1]].push(t.class);
|
|
795
|
+
else if (t.name.startsWith("shadow-")) other.shadow.push(`shadow-${t.name.slice(7)}`);
|
|
796
|
+
else if (t.name.startsWith("radius-")) other.radius.push(`rounded-${t.name.slice(7)}`);
|
|
797
|
+
else if (t.name.startsWith("spacing-")) other.spacing.push(t.name);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const want = (c) => !category || category === "all" || category === c;
|
|
801
|
+
const push = (title, items) => {
|
|
802
|
+
if (items.length) out.push(`## ${title}\n${[...new Set(items)].sort().join("\n")}`);
|
|
803
|
+
};
|
|
804
|
+
if (want("bg")) push("Background — bg-bg-*", colorGroups.bg);
|
|
805
|
+
if (want("fg")) push("Text/foreground — text-fg-*", colorGroups.fg);
|
|
806
|
+
if (want("border")) push("Border — border-border-*", colorGroups.border);
|
|
807
|
+
if (want("fill")) push("SVG fill — fill-fill-*", colorGroups.fill);
|
|
808
|
+
if (want("typography")) push("Typography — typo-* (use directly)", typo);
|
|
809
|
+
if (want("shadow")) push("Shadow", other.shadow);
|
|
810
|
+
if (want("radius")) push("Radius", other.radius);
|
|
811
|
+
if (want("spacing")) push("Spacing scale (p-/m-/gap-/w-/h-)", other.spacing);
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
content: [
|
|
815
|
+
{
|
|
816
|
+
type: "text",
|
|
817
|
+
text: out.join("\n\n") || `No tokens for category "${category}".`,
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
server.registerTool(
|
|
825
|
+
"search_design_tokens",
|
|
826
|
+
{
|
|
827
|
+
title: "Search design tokens by name or value",
|
|
828
|
+
description:
|
|
829
|
+
"Find a design token by keyword — e.g. 'danger' (error colors), 'elevated', 'brand', or a hex " +
|
|
830
|
+
"like '#fb2c36'. Returns the matching Tailwind class + value.",
|
|
831
|
+
inputSchema: { query: z.string().describe('e.g. "danger", "elevated", "#fb2c36"') },
|
|
832
|
+
},
|
|
833
|
+
async ({ query }) => {
|
|
834
|
+
const { vars, typo } = loadDesignTokens();
|
|
835
|
+
const q = query.toLowerCase();
|
|
836
|
+
const hits = vars
|
|
837
|
+
.filter((t) => t.name.toLowerCase().includes(q) || t.value.toLowerCase().includes(q))
|
|
838
|
+
.map((t) => `${t.class ?? t.name} (${t.value})`);
|
|
839
|
+
const typoHits = typo.filter((c) => c.toLowerCase().includes(q));
|
|
840
|
+
const all = [...typoHits, ...hits];
|
|
841
|
+
return {
|
|
842
|
+
content: [
|
|
843
|
+
{
|
|
844
|
+
type: "text",
|
|
845
|
+
text: all.length ? [...new Set(all)].slice(0, 60).join("\n") : `No token matches "${query}".`,
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
/* --- rainbow-rb-sdk (Python backend) --- */
|
|
853
|
+
|
|
854
|
+
server.registerTool(
|
|
855
|
+
"list_rb_sdk",
|
|
856
|
+
{
|
|
857
|
+
title: "List rainbow-rb-sdk classes and functions (Python or C++)",
|
|
858
|
+
description:
|
|
859
|
+
"List the public classes and functions of the plugin BACKEND SDK. lang \"python\" (default): the " +
|
|
860
|
+
"installed rainbow.rb_sdk package (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, " +
|
|
861
|
+
"...) — call before writing py/main.py; requires rainbow-rb-sdk to be pip-installed. lang \"cpp\": " +
|
|
862
|
+
"the C++ SDK headers (apt rainbow-rb-sdk-dev: RBManipulateSDK, rb_plugin::PluginProgramContext, " +
|
|
863
|
+
"...) grouped by header path — call before writing cpp/main.cpp or an rclcpp node; requires the " +
|
|
864
|
+
"muscat-pk-cpp-dev Docker image (built by `muscat-pk dev:back`). RBPluginCall (UI pub/sub/serve, " +
|
|
865
|
+
"router) exists in both: Python rainbow.rb_sdk.RBPluginCall, C++ rb_plugin::RBPluginCall " +
|
|
866
|
+
"(plugin/rb_plugin.hpp).",
|
|
867
|
+
inputSchema: { lang: z.enum(["python", "cpp"]).optional().describe('Default "python"') },
|
|
868
|
+
},
|
|
869
|
+
async ({ lang }) => {
|
|
870
|
+
if (lang === "cpp") {
|
|
871
|
+
const { dir, error } = cppIncludeDir();
|
|
872
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
873
|
+
const out = [];
|
|
874
|
+
for (const rel of cppHeaders(dir)) {
|
|
875
|
+
const classes = cppClassesIn(readFileSync(join(dir, rel), "utf8"));
|
|
876
|
+
if (classes.length) out.push(`${rel}: ${classes.join(", ")}`);
|
|
877
|
+
}
|
|
878
|
+
return { content: [{ type: "text", text: `#include <rainbow/rb_sdk/...>\n\n${out.join("\n")}` }] };
|
|
879
|
+
}
|
|
880
|
+
const { data, error } = introspectRbSdk(`
|
|
881
|
+
import inspect, json
|
|
882
|
+
mod = importlib.import_module("rainbow.rb_sdk")
|
|
883
|
+
out = []
|
|
884
|
+
for name in sorted(getattr(mod, "__all__", None) or dir(mod)):
|
|
885
|
+
if name.startswith("_"): continue
|
|
886
|
+
obj = getattr(mod, name)
|
|
887
|
+
if inspect.isclass(obj): out.append(name + " (class)")
|
|
888
|
+
elif inspect.isfunction(obj) or inspect.isbuiltin(obj): out.append(name + " (function)")
|
|
889
|
+
print(json.dumps(out))
|
|
890
|
+
`);
|
|
891
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
892
|
+
return { content: [{ type: "text", text: data.join("\n") }] };
|
|
893
|
+
},
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
server.registerTool(
|
|
897
|
+
"get_rb_sdk_api",
|
|
898
|
+
{
|
|
899
|
+
title: "Get a rainbow-rb-sdk class/function signature + docs (Python or C++)",
|
|
900
|
+
description:
|
|
901
|
+
"Get the signature(s) and docstring for a rainbow.rb_sdk class or function (e.g. " +
|
|
902
|
+
"'PluginProgramContext', 'RBManipulateSDK'). For a class, lists its public methods with " +
|
|
903
|
+
"signatures. To drill into ONE method of a class (e.g. RBManipulateSDK.call_move_j), pass " +
|
|
904
|
+
"`method` alongside `name` — do not put both in `name`. lang \"python\" (default) introspects the " +
|
|
905
|
+
"pip-installed package. lang \"cpp\" returns the class declaration from the C++ SDK header " +
|
|
906
|
+
"(rainbow-rb-sdk-dev) with its header comment — usually a full usage example — and the " +
|
|
907
|
+
"#include path; with `method`, just that method's declaration and comment.",
|
|
908
|
+
inputSchema: {
|
|
909
|
+
name: z.string().describe('Class or function name, e.g. "RBManipulateSDK"'),
|
|
910
|
+
method: z.string().optional().describe('Optional: a specific method on that class, e.g. "call_move_j"'),
|
|
911
|
+
lang: z.enum(["python", "cpp"]).optional().describe('Default "python"'),
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
async ({ name, method, lang }) => {
|
|
915
|
+
// Tolerate a combined "ClassName method_name" in `name` (a common agent
|
|
916
|
+
// mistake) by splitting it ourselves instead of failing validation.
|
|
917
|
+
let className = name;
|
|
918
|
+
let methodName = method;
|
|
919
|
+
if (!methodName && /\s/.test(name.trim())) {
|
|
920
|
+
const parts = name.trim().split(/\s+/);
|
|
921
|
+
if (parts.length === 2) [className, methodName] = parts;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (lang === "cpp") {
|
|
925
|
+
const cls = className.replace(/^.*::/, "");
|
|
926
|
+
let { rel, text, error } = cppFind(cls);
|
|
927
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
928
|
+
let body = cppExtract(text, cls, methodName);
|
|
929
|
+
let owner = cls;
|
|
930
|
+
if (!body && methodName) {
|
|
931
|
+
// Like the Python path: most calls live on sub-SDK members
|
|
932
|
+
// (RBManipulateMoveSDK move; -> sdk.move.call_move_j) or on the base
|
|
933
|
+
// class — look there before giving up.
|
|
934
|
+
const decl = cppExtract(text, cls);
|
|
935
|
+
const cands = [
|
|
936
|
+
...[...decl.matchAll(/^\s*(\w+SDK)\s+(\w+);/gm)].map((m) => [m[1], `${cls}.${m[2]}`]),
|
|
937
|
+
...[...decl.matchAll(/:\s*public\s+(\w+)/g)].map((m) => [m[1], `${cls} (base ${m[1]})`]),
|
|
938
|
+
];
|
|
939
|
+
for (const [c, label] of cands) {
|
|
940
|
+
const f = cppFind(c);
|
|
941
|
+
if (f.error) continue;
|
|
942
|
+
const b = cppExtract(f.text, c, methodName);
|
|
943
|
+
if (b) { ({ rel, text } = f); body = b; owner = label; break; }
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!body) {
|
|
947
|
+
return {
|
|
948
|
+
isError: true,
|
|
949
|
+
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.` }],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const ns = text.match(/^namespace\s+(\w+)\s*\{/m)?.[1];
|
|
953
|
+
const header =
|
|
954
|
+
`// #include <rainbow/rb_sdk/${rel}>${ns ? ` (namespace ${ns})` : ""}\n` +
|
|
955
|
+
`// link: find_package(rb_sdk) + rb_sdk::rb_sdk\n` +
|
|
956
|
+
(methodName ? `// ${owner}.${methodName}\n` : "") + "\n";
|
|
957
|
+
return { content: [{ type: "text", text: header + body.slice(0, 60_000) }] };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const { data, error } = introspectRbSdk(`
|
|
961
|
+
import inspect, json
|
|
962
|
+
mod = importlib.import_module("rainbow.rb_sdk")
|
|
963
|
+
name = ${JSON.stringify(className)}
|
|
964
|
+
method = ${methodName ? JSON.stringify(methodName) : "None"}
|
|
965
|
+
obj = getattr(mod, name, None)
|
|
966
|
+
if obj is None:
|
|
967
|
+
print(json.dumps({"__missing__": True})); raise SystemExit(0)
|
|
968
|
+
def sig(o):
|
|
969
|
+
try: return str(inspect.signature(o))
|
|
970
|
+
except Exception: return "(...)"
|
|
971
|
+
|
|
972
|
+
if method:
|
|
973
|
+
# Direct method on the class itself.
|
|
974
|
+
fn = getattr(obj, method, None) if inspect.isclass(obj) else None
|
|
975
|
+
# rb_sdk classes often group calls under a nested namespace set in
|
|
976
|
+
# __init__ (e.g. RBManipulateSDK().move.call_move_j) — that only exists on
|
|
977
|
+
# an INSTANCE, which we won't construct (side effects, needs a robot
|
|
978
|
+
# connection). Best-effort: search the class's own __init__ source text
|
|
979
|
+
# for self.<attr> = <NestedClass>(...) and check each nested class too.
|
|
980
|
+
owner = name
|
|
981
|
+
if fn is None and inspect.isclass(obj):
|
|
982
|
+
try:
|
|
983
|
+
src = inspect.getsource(obj.__init__)
|
|
984
|
+
except Exception:
|
|
985
|
+
src = ""
|
|
986
|
+
import re, sys as _sys
|
|
987
|
+
# The nested sub-SDK classes (e.g. RBManipulateMoveSDK) are imported in
|
|
988
|
+
# the module that DEFINES obj (rainbow.rb_sdk.manipulate), not in the
|
|
989
|
+
# rainbow.rb_sdk top-level namespace — resolve names against both.
|
|
990
|
+
_defmod = _sys.modules.get(getattr(obj, "__module__", ""))
|
|
991
|
+
for attr, nested_name in re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", src):
|
|
992
|
+
nested = (
|
|
993
|
+
(getattr(_defmod, nested_name, None) if _defmod else None)
|
|
994
|
+
or mod.__dict__.get(nested_name)
|
|
995
|
+
or globals().get(nested_name)
|
|
996
|
+
)
|
|
997
|
+
cand = getattr(nested, method, None) if nested else None
|
|
998
|
+
if cand:
|
|
999
|
+
fn = cand
|
|
1000
|
+
owner = f"{name}.{attr} ({nested_name})"
|
|
1001
|
+
break
|
|
1002
|
+
if fn is None:
|
|
1003
|
+
print(json.dumps({"__missing__": True, "__method__": True})); raise SystemExit(0)
|
|
1004
|
+
print(json.dumps({
|
|
1005
|
+
"name": name, "kind": "method", "owner": owner,
|
|
1006
|
+
"signature": method + sig(fn), "doc": inspect.getdoc(fn) or "",
|
|
1007
|
+
}))
|
|
1008
|
+
raise SystemExit(0)
|
|
1009
|
+
|
|
1010
|
+
res = {"name": name, "doc": inspect.getdoc(obj) or ""}
|
|
1011
|
+
if inspect.isclass(obj):
|
|
1012
|
+
res["kind"] = "class"
|
|
1013
|
+
res["init"] = name + sig(obj.__init__ if "__init__" in dir(obj) else obj)
|
|
1014
|
+
def _methods_of(cls, prefix=""):
|
|
1015
|
+
out = []
|
|
1016
|
+
for m, fn in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
1017
|
+
if m.startswith("_"): continue
|
|
1018
|
+
out.append({"name": prefix + m, "sig": prefix + m + sig(fn),
|
|
1019
|
+
"doc": (inspect.getdoc(fn) or "").split("\\n")[0]})
|
|
1020
|
+
return out
|
|
1021
|
+
methods = _methods_of(obj)
|
|
1022
|
+
# Most rb_sdk calls live on nested sub-SDKs assigned in __init__
|
|
1023
|
+
# (e.g. self.move = RBManipulateMoveSDK() -> obj.move.call_move_j). List
|
|
1024
|
+
# those too, name-prefixed, so the API is discoverable without an instance.
|
|
1025
|
+
import re as _re, sys as _sys
|
|
1026
|
+
_defmod = _sys.modules.get(getattr(obj, "__module__", ""))
|
|
1027
|
+
try:
|
|
1028
|
+
_src = inspect.getsource(obj.__init__)
|
|
1029
|
+
except Exception:
|
|
1030
|
+
_src = ""
|
|
1031
|
+
for _attr, _nested_name in _re.findall(r"self\\.(\\w+)\\s*=\\s*(\\w+)\\(", _src):
|
|
1032
|
+
_nested = (getattr(_defmod, _nested_name, None) if _defmod else None) or mod.__dict__.get(_nested_name)
|
|
1033
|
+
if inspect.isclass(_nested):
|
|
1034
|
+
methods.extend(_methods_of(_nested, prefix=_attr + "."))
|
|
1035
|
+
res["methods"] = methods
|
|
1036
|
+
else:
|
|
1037
|
+
res["kind"] = "function"
|
|
1038
|
+
res["signature"] = name + sig(obj)
|
|
1039
|
+
print(json.dumps(res))
|
|
1040
|
+
`);
|
|
1041
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
1042
|
+
if (data.__missing__) {
|
|
1043
|
+
const text = data.__method__
|
|
1044
|
+
? `No method "${methodName}" found directly on "${className}" (nor in an obvious nested ` +
|
|
1045
|
+
`namespace set in __init__). Call get_rb_sdk_api with just \`name: "${className}"\` to see ` +
|
|
1046
|
+
`its full method list and constructor first.`
|
|
1047
|
+
: `No "${className}" in rainbow.rb_sdk. Call list_rb_sdk for valid names.`;
|
|
1048
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
1049
|
+
}
|
|
1050
|
+
if (data.kind === "method") {
|
|
1051
|
+
const lines = [`# ${data.owner}.${data.signature.split("(")[0]}`, data.signature];
|
|
1052
|
+
if (data.doc) lines.push("", data.doc);
|
|
1053
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1054
|
+
}
|
|
1055
|
+
const lines = [`# ${data.name} (${data.kind})`];
|
|
1056
|
+
if (data.doc) lines.push(data.doc, "");
|
|
1057
|
+
if (data.kind === "class") {
|
|
1058
|
+
lines.push(`constructor: ${data.init}`, "", "methods:");
|
|
1059
|
+
for (const m of data.methods) {
|
|
1060
|
+
lines.push(` ${m.sig}${m.doc ? ` — ${m.doc}` : ""}`);
|
|
1061
|
+
}
|
|
1062
|
+
} else {
|
|
1063
|
+
lines.push(data.signature);
|
|
1064
|
+
}
|
|
1065
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
const transport = new StdioServerTransport();
|
|
1070
|
+
await server.connect(transport);
|
|
1071
|
+
}
|