@rainbow-robotics/muscat-plugin-kit 1.1.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +99 -0
  2. package/cli/README.md +40 -4
  3. package/cli/bin/muscat-pk.mjs +33 -6
  4. package/cli/docker/Dockerfile.cpp +11 -6
  5. package/cli/docker/Dockerfile.dev +12 -0
  6. package/cli/src/build.mjs +6 -6
  7. package/cli/src/cpp.mjs +3 -1
  8. package/cli/src/dev.mjs +2 -1
  9. package/cli/src/devBack.mjs +8 -7
  10. package/cli/src/manifest.mjs +56 -0
  11. package/cli/src/mcp.mjs +382 -43
  12. package/cli/src/new.mjs +3 -0
  13. package/cli/src/ports.mjs +3 -3
  14. package/cli/src/ros2.mjs +3 -1
  15. package/cli/src/templates-embedded.mjs +3 -0
  16. package/cli/src/util.mjs +13 -0
  17. package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
  18. package/cli/storybook-dist/assets/{Color-YHDXOIA2-YYWRJKA2.js → Color-YHDXOIA2-DixbmWSF.js} +1 -1
  19. package/cli/storybook-dist/assets/{Docs-CKJ2w--X.js → Docs-Blx_41TT.js} +1 -1
  20. package/cli/storybook-dist/assets/Docs-C0E3mHmn.js +7 -0
  21. package/cli/storybook-dist/assets/Docs-CMEiBjLz.js +20 -0
  22. package/cli/storybook-dist/assets/{Docs-UmN7NFo1.js → Docs-D6oFYDre.js} +1 -1
  23. package/cli/storybook-dist/assets/{Docs-DIJ7cbFD.js → Docs-DcUa4nGr.js} +1 -1
  24. package/cli/storybook-dist/assets/{Docs-DT9RD87x.js → Docs-Dv-5qjF4.js} +1 -1
  25. package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-CyAy1mnJ.js → DocsRenderer-CFRXHY34-GwWANrWN.js} +1 -1
  26. package/cli/storybook-dist/assets/NxAvatar.stories-CyZ4AE2b.js +13 -0
  27. package/cli/storybook-dist/assets/NxButton.stories-Bx8UtGNd.js +20 -0
  28. package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
  29. package/cli/storybook-dist/assets/{NxNumberInput.stories-zlQJuGFP.js → NxNumberInput.stories-skizAdd5.js} +3 -3
  30. package/cli/storybook-dist/assets/NxPagination.stories-gtIxt2u0.js +11 -0
  31. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-DSHMk3N5.js +14 -0
  32. package/cli/storybook-dist/assets/{NxRangeInput.stories-Cu5ZWU0w.js → NxRangeInput.stories-C1YtrFZ1.js} +3 -3
  33. package/cli/storybook-dist/assets/NxSelectBox.stories-DCySAtBp.js +17 -0
  34. package/cli/storybook-dist/assets/NxSlideRange.stories-gSaMOWo7.js +14 -0
  35. package/cli/storybook-dist/assets/NxSwitch.stories-DF0DBiyG.js +11 -0
  36. package/cli/storybook-dist/assets/NxTextArea.stories-Dill0M6z.js +11 -0
  37. package/cli/storybook-dist/assets/NxTextInput.stories-Bax14r48.js +11 -0
  38. package/cli/storybook-dist/assets/RBAccordion.stories-CCwz_pIV.js +17 -0
  39. package/cli/storybook-dist/assets/RBAccordionGroup.stories-BjDxxdPV.js +33 -0
  40. package/cli/storybook-dist/assets/RBAlert.stories-5z6GasgP.js +17 -0
  41. package/cli/storybook-dist/assets/RBBadge.stories-BwcM7i7w.js +1 -0
  42. package/cli/storybook-dist/assets/RBBattery.stories-DOeQH3rK.js +10 -0
  43. package/cli/storybook-dist/assets/RBCard.stories-DcmiOXAV.js +45 -0
  44. package/cli/storybook-dist/assets/RBCheckBox.stories-BYru87fX.js +1 -0
  45. package/cli/storybook-dist/assets/RBCheckIconButton.stories-CXUjtR1a.js +1 -0
  46. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-BSOQV7Du.js +24 -0
  47. package/cli/storybook-dist/assets/RBChipTab.stories-ByBmeukU.js +21 -0
  48. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-GtJ01RTr.js +1 -0
  49. package/cli/storybook-dist/assets/RBColorIcons.stories-C9saET1u.js +3 -0
  50. package/cli/storybook-dist/assets/{RBCommand.stories-C3bwPq2T.js → RBCommand.stories-CcvnWlWZ.js} +3 -3
  51. package/cli/storybook-dist/assets/RBConfirm.stories-hdBVSdBS.js +20 -0
  52. package/cli/storybook-dist/assets/RBDateInput.stories-BE9JQl-E.js +15 -0
  53. package/cli/storybook-dist/assets/RBDialog.stories-BaWuDgl_.js +38 -0
  54. package/cli/storybook-dist/assets/RBDivider.stories-D3INKm_Z.js +1 -0
  55. package/cli/storybook-dist/assets/RBDrawer.stories-BZ8V0J6-.js +14 -0
  56. package/cli/storybook-dist/assets/{RBDropdownMenu.stories-9fBcV2Ct.js → RBDropdownMenu.stories-DxLi8HRw.js} +2 -2
  57. package/cli/storybook-dist/assets/RBEmptyView.stories-DQcvDt2S.js +12 -0
  58. package/cli/storybook-dist/assets/RBIconButton.stories-DJfuTpQ0.js +1 -0
  59. package/cli/storybook-dist/assets/RBIcons.stories-BSsyb0BK.js +3 -0
  60. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-Di3th--x.js +1 -0
  61. package/cli/storybook-dist/assets/RBJoyStick.stories-DfGazm4n.js +11 -0
  62. package/cli/storybook-dist/assets/RBLabelButton.stories-CLbxzW56.js +1 -0
  63. package/cli/storybook-dist/assets/RBLight.stories-DeiuDm_P.js +1 -0
  64. package/cli/storybook-dist/assets/RBMenubar.stories-BZQTV9e5.js +3 -0
  65. package/cli/storybook-dist/assets/RBMultiRangeInput.stories-DDNmdi_n.js +14 -0
  66. package/cli/storybook-dist/assets/RBPagination.stories-CLBCEyCU.js +17 -0
  67. package/cli/storybook-dist/assets/RBPopover.stories-DRI5Kfa-.js +11 -0
  68. package/cli/storybook-dist/assets/RBPrompt.stories-DIAmypoJ.js +20 -0
  69. package/cli/storybook-dist/assets/RBRadioBox.stories-BS_N4aLf.js +1 -0
  70. package/cli/storybook-dist/assets/RBRadioButton.stories-BTm8cYvm.js +11 -0
  71. package/cli/storybook-dist/assets/RBRangeDateInput.stories-D5hBsSyB.js +15 -0
  72. package/cli/storybook-dist/assets/RBResizable.stories-CxPEkbXQ.js +20 -0
  73. package/cli/storybook-dist/assets/RBSelectBox.stories-FUQ7AcUi.js +6 -0
  74. package/cli/storybook-dist/assets/RBSheet.stories-mhVHH_71.js +39 -0
  75. package/cli/storybook-dist/assets/RBSideNumberInput.stories-5FMkLUim.js +5 -0
  76. package/cli/storybook-dist/assets/RBSlider.stories-Bnhcp9pz.js +5 -0
  77. package/cli/storybook-dist/assets/RBSpinner.stories-Bf4416mP.js +14 -0
  78. package/cli/storybook-dist/assets/{RBSwitch.stories-CBYXvhkd.js → RBSwitch.stories-BNhFjRaV.js} +2 -2
  79. package/cli/storybook-dist/assets/RBTab.stories-Bsr7jlfb.js +1 -0
  80. package/cli/storybook-dist/assets/RBTable.stories-BkA6pKzs.js +62 -0
  81. package/cli/storybook-dist/assets/RBTableRow.stories-D07Z7D5-.js +11 -0
  82. package/cli/storybook-dist/assets/RBTabs.stories-DHnZiV21.js +21 -0
  83. package/cli/storybook-dist/assets/RBTextArea.stories-CtLMLuV_.js +5 -0
  84. package/cli/storybook-dist/assets/RBTextField.stories-Chz0i1-u.js +45 -0
  85. package/cli/storybook-dist/assets/RBTextInput.stories-Dk5cZfUF.js +5 -0
  86. package/cli/storybook-dist/assets/RBToast.stories-tZxbjmr8.js +11 -0
  87. package/cli/storybook-dist/assets/RBToggle.stories-VTe7a4xZ.js +12 -0
  88. package/cli/storybook-dist/assets/RBToggleGroup.stories-WiFnGGYx.js +17 -0
  89. package/cli/storybook-dist/assets/RBTooltip.stories-B_L4W7Mn.js +9 -0
  90. package/cli/storybook-dist/assets/{MapEditor-C6_H9h5t.css → SideNumberInput-SAkhydsn.css} +2 -2
  91. package/cli/storybook-dist/assets/SideNumberInput-os5wKKf7.js +1109 -0
  92. package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
  93. package/cli/storybook-dist/assets/{entry-preview-N9dpIJQA.js → entry-preview-NpzneD8a.js} +1 -1
  94. package/cli/storybook-dist/assets/{iframe-BwCxSq5d.js → iframe-DsvRKHwc.js} +2 -2
  95. package/cli/storybook-dist/assets/{index-CgJh_5EZ.js → index-BzvSJzrT.js} +3 -3
  96. package/cli/storybook-dist/assets/preview-BXXleNPp.css +1 -0
  97. package/cli/storybook-dist/assets/preview-BXxjLQ9M.js +3 -0
  98. package/cli/storybook-dist/assets/{preview-DsYc6ovL.js → preview-yXaYuZ78.js} +2 -2
  99. package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
  100. package/cli/storybook-dist/iframe.html +1 -1
  101. package/cli/storybook-dist/project.json +1 -1
  102. package/cli/templates/common/README.md +352 -24
  103. package/cli/templates/common/manifest.json +1 -0
  104. package/cli/templates/common/py/main.py +24 -1
  105. package/cli/templates/common/requirements.txt +3 -3
  106. package/cli/templates/cpp/cpp/CMakeLists.txt +7 -7
  107. package/cli/templates/cpp/cpp/main.cpp +13 -6
  108. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
  109. package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +2 -0
  110. package/package.json +3 -3
  111. package/cli/storybook-dist/assets/Docs-DLFwGiks.js +0 -7
  112. package/cli/storybook-dist/assets/Docs-dOc9cH1E.js +0 -20
  113. package/cli/storybook-dist/assets/MapEditor-Jj1J2sUz.js +0 -6418
  114. package/cli/storybook-dist/assets/NxAvatar.stories-B-iQoylp.js +0 -13
  115. package/cli/storybook-dist/assets/NxButton.stories-CawQZN07.js +0 -20
  116. package/cli/storybook-dist/assets/NxPagination.stories-VYXLplNl.js +0 -11
  117. package/cli/storybook-dist/assets/NxPointerHoldButton.stories-BpqIkt0X.js +0 -14
  118. package/cli/storybook-dist/assets/NxSelectBox.stories-JECi_1PM.js +0 -17
  119. package/cli/storybook-dist/assets/NxSlideRange.stories-DJe8F2Mv.js +0 -14
  120. package/cli/storybook-dist/assets/NxSwitch.stories-DX6LxxQL.js +0 -11
  121. package/cli/storybook-dist/assets/NxTextArea.stories-Cy42nYOI.js +0 -11
  122. package/cli/storybook-dist/assets/NxTextInput.stories-Bb_q6FuW.js +0 -11
  123. package/cli/storybook-dist/assets/RBAccordion.stories-ChgPXO0_.js +0 -17
  124. package/cli/storybook-dist/assets/RBAccordionGroup.stories-nmbqYYSE.js +0 -33
  125. package/cli/storybook-dist/assets/RBAlert.stories-ChOPuFxI.js +0 -17
  126. package/cli/storybook-dist/assets/RBBadge.stories-Bas7g4jb.js +0 -1
  127. package/cli/storybook-dist/assets/RBBattery.stories-C0MywWvB.js +0 -10
  128. package/cli/storybook-dist/assets/RBCard.stories-Ba7y0u6Q.js +0 -45
  129. package/cli/storybook-dist/assets/RBCheckBox.stories-CsHFsXCd.js +0 -1
  130. package/cli/storybook-dist/assets/RBCheckIconButton.stories-GERClL9R.js +0 -1
  131. package/cli/storybook-dist/assets/RBCheckLabelButton.stories-C-BrmKaW.js +0 -24
  132. package/cli/storybook-dist/assets/RBChipTab.stories-C8JCa6F9.js +0 -21
  133. package/cli/storybook-dist/assets/RBChipsBoxTab.stories-DmUaHBJ3.js +0 -1
  134. package/cli/storybook-dist/assets/RBColorIcons.stories-Bcah6eZe.js +0 -3
  135. package/cli/storybook-dist/assets/RBConfirm.stories-CT0GuE-J.js +0 -20
  136. package/cli/storybook-dist/assets/RBDateInput.stories-CMd2xRCF.js +0 -15
  137. package/cli/storybook-dist/assets/RBDialog.stories-Ui5Ni-P_.js +0 -38
  138. package/cli/storybook-dist/assets/RBDivider.stories-r8TDiO36.js +0 -1
  139. package/cli/storybook-dist/assets/RBDrawer.stories-CymmrHjI.js +0 -14
  140. package/cli/storybook-dist/assets/RBEmptyView.stories-By9vncNC.js +0 -12
  141. package/cli/storybook-dist/assets/RBIconButton.stories-DC29KMxX.js +0 -1
  142. package/cli/storybook-dist/assets/RBIcons.stories-Dpd_V9b9.js +0 -3
  143. package/cli/storybook-dist/assets/RBInlineLabelButton.stories-CfUQdAdt.js +0 -1
  144. package/cli/storybook-dist/assets/RBJoyStick.stories-Dl-uXk9x.js +0 -11
  145. package/cli/storybook-dist/assets/RBLabelButton.stories-Dx_Vn6EM.js +0 -1
  146. package/cli/storybook-dist/assets/RBLight.stories-Cm5UYJ4n.js +0 -1
  147. package/cli/storybook-dist/assets/RBMenubar.stories-CN5z7F6D.js +0 -3
  148. package/cli/storybook-dist/assets/RBMultiRangeInput.stories-DrAMI4mQ.js +0 -14
  149. package/cli/storybook-dist/assets/RBPagination.stories-BVLXuTLw.js +0 -17
  150. package/cli/storybook-dist/assets/RBPopover.stories-VCws5q80.js +0 -11
  151. package/cli/storybook-dist/assets/RBPrompt.stories-Cw5-wssh.js +0 -20
  152. package/cli/storybook-dist/assets/RBRadioBox.stories-ClqGtGA_.js +0 -1
  153. package/cli/storybook-dist/assets/RBRadioButton.stories-SxcJFioj.js +0 -11
  154. package/cli/storybook-dist/assets/RBRangeDateInput.stories-L_wCftDv.js +0 -15
  155. package/cli/storybook-dist/assets/RBResizable.stories-2VytDv6m.js +0 -20
  156. package/cli/storybook-dist/assets/RBSelectBox.stories-BoOKTSL_.js +0 -6
  157. package/cli/storybook-dist/assets/RBSheet.stories-CUClMqKK.js +0 -39
  158. package/cli/storybook-dist/assets/RBSideNumberInput.stories-BTYDDAmk.js +0 -5
  159. package/cli/storybook-dist/assets/RBSlider.stories-Bu6vD9ym.js +0 -5
  160. package/cli/storybook-dist/assets/RBSpinner.stories-XmVcaTVS.js +0 -14
  161. package/cli/storybook-dist/assets/RBTab.stories-DFFYlkCv.js +0 -1
  162. package/cli/storybook-dist/assets/RBTable.stories-CR2j2EQQ.js +0 -62
  163. package/cli/storybook-dist/assets/RBTableRow.stories-CpPIP1au.js +0 -11
  164. package/cli/storybook-dist/assets/RBTabs.stories-Bew5XVDZ.js +0 -21
  165. package/cli/storybook-dist/assets/RBTextArea.stories-JiRn-tk_.js +0 -5
  166. package/cli/storybook-dist/assets/RBTextField.stories-8LWyUBm9.js +0 -45
  167. package/cli/storybook-dist/assets/RBTextInput.stories-PLJ70xm5.js +0 -5
  168. package/cli/storybook-dist/assets/RBToast.stories-BQf_9x0k.js +0 -11
  169. package/cli/storybook-dist/assets/RBToggle.stories-C5dZnmX_.js +0 -12
  170. package/cli/storybook-dist/assets/RBToggleGroup.stories-D7fwXCxA.js +0 -17
  171. package/cli/storybook-dist/assets/RBTooltip.stories-D-sWRxbA.js +0 -9
  172. package/cli/storybook-dist/assets/RainbowUiProvider-CsMXuxM1.js +0 -31
  173. package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
  174. package/cli/storybook-dist/assets/keymapTileIndex.worker-BsNYNbqs.js +0 -1
  175. package/cli/storybook-dist/assets/preview-CzDPTg-k.js +0 -3
  176. package/cli/storybook-dist/assets/preview-DZWUyVBI.css +0 -1
  177. package/cli/storybook-dist/assets/react-resizable-panels.browser-CHDWt8fa.js +0 -72
  178. /package/cli/storybook-dist/assets/{RainbowUiProvider-BaelaOGy.css → react-resizable-panels-BaelaOGy.css} +0 -0
package/cli/src/mcp.mjs CHANGED
@@ -13,7 +13,9 @@
13
13
  //
14
14
  // Plugin SDK docs + Python backend:
15
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.
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).
17
19
  // - get_rb_sdk_api signature + docstring for an rb_sdk class/function.
18
20
  //
19
21
  // Add to an MCP client config, e.g. Claude Code's .mcp.json:
@@ -22,12 +24,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
25
  import { z } from "zod";
24
26
  import ts from "typescript";
25
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
27
+ import {
28
+ appendFileSync,
29
+ existsSync,
30
+ mkdirSync,
31
+ readdirSync,
32
+ readFileSync,
33
+ renameSync,
34
+ statSync,
35
+ writeFileSync,
36
+ } from "node:fs";
26
37
  import { spawnSync } from "node:child_process";
38
+ import { homedir } from "node:os";
27
39
  import { join, resolve } from "node:path";
28
40
  import { fileURLToPath } from "node:url";
29
41
  import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
30
42
  import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
43
+ import { CPP_DEV_IMAGE } from "./cpp.mjs";
31
44
 
32
45
  const TEMPLATE_README = join(kitRoot, "cli/templates/common/README.md");
33
46
  // Design tokens: prefer the shipped copy, fall back to the monorepo source.
@@ -39,14 +52,10 @@ const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
39
52
  /* react.d.ts introspection — component/hook list + prop types */
40
53
  /* -------------------------------------------------------------------------- */
41
54
 
42
- let typesCache = null;
43
-
44
- // Parse plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components
45
- // types are inlined at build — see plugin-ui/build-dist.mjs), so this one file
46
- // is enough even in an external install.
47
- async function loadReactTypes() {
48
- if (typesCache) return typesCache;
49
-
55
+ // plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components types
56
+ // are inlined at build — see plugin-ui/build-dist.mjs), so this one file is
57
+ // enough even in an external install.
58
+ async function reactDtsPath() {
50
59
  const entryUrl = await import.meta.resolve("@rainbow-robotics/plugin-ui/react");
51
60
  const dtsPath = fileURLToPath(entryUrl).replace(/\.js$/, ".d.ts");
52
61
  if (!existsSync(dtsPath)) {
@@ -54,19 +63,23 @@ async function loadReactTypes() {
54
63
  `plugin-ui type declarations not found at ${dtsPath} — reinstall or rebuild @rainbow-robotics/plugin-ui.`,
55
64
  );
56
65
  }
66
+ return dtsPath;
67
+ }
57
68
 
69
+ function loadReactTypes(dtsPath) {
70
+ // Without moduleResolution the `import ... from "react"` inside react.d.ts
71
+ // never resolves and every React-derived type collapses to `any`.
58
72
  const program = ts.createProgram([dtsPath], {
59
73
  target: ts.ScriptTarget.ES2022,
60
74
  module: ts.ModuleKind.ESNext,
75
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
61
76
  skipLibCheck: true,
62
77
  });
63
78
  const sourceFile = program.getSourceFile(dtsPath);
64
79
  const checker = program.getTypeChecker();
65
80
  const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
66
81
  const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
67
-
68
- typesCache = { checker, sourceFile, exports };
69
- return typesCache;
82
+ return { checker, sourceFile, exports };
70
83
  }
71
84
 
72
85
  // The barrel's trailing `export { A, B, ... }` produces Alias symbols whose
@@ -82,6 +95,124 @@ function exportKind(checker, symbol) {
82
95
  return resolved.flags & ts.SymbolFlags.Value ? "value" : "type";
83
96
  }
84
97
 
98
+ // typeToString alone often prints an opaque alias (`ForwardRefExoticComponent<PropsType$s>`).
99
+ // The first call signature works for plain components and forwardRef alike; its
100
+ // first parameter is the props object.
101
+ function ownProps(checker, type) {
102
+ const [signature] = type.getCallSignatures?.() ?? [];
103
+ const param = signature?.getParameters?.()[0];
104
+ if (!param) return [];
105
+ const declaration = param.valueDeclaration ?? param.declarations?.[0];
106
+ if (!declaration) return [];
107
+ const propsType = checker.getTypeOfSymbolAtLocation(param, declaration);
108
+
109
+ return checker
110
+ .getPropertiesOfType(propsType)
111
+ // Without this, ~220 inherited React DOM attributes bury the component's own API.
112
+ .filter((prop) =>
113
+ (prop.declarations ?? []).some(
114
+ (d) => !d.getSourceFile().fileName.includes("node_modules"),
115
+ ),
116
+ )
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);
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
+
180
+ let uiIndexCache = null;
181
+
182
+ async function loadUiIndex() {
183
+ if (uiIndexCache) return uiIndexCache;
184
+ const dtsPath = await reactDtsPath();
185
+ const { mtimeMs, size } = statSync(dtsPath);
186
+ const key = `${dtsPath}|${mtimeMs}|${size}|${kitVersion()}`;
187
+
188
+ try {
189
+ const cached = JSON.parse(readFileSync(UI_INDEX_CACHE, "utf8"));
190
+ // A half-written file still parses as JSON, so check the shape too.
191
+ if (
192
+ cached.key === key &&
193
+ typeof cached.components === "string" &&
194
+ cached.props &&
195
+ typeof cached.props === "object"
196
+ ) {
197
+ return (uiIndexCache = cached);
198
+ }
199
+ } catch {
200
+ /* absent, unreadable or corrupt — rebuild below */
201
+ }
202
+
203
+ uiIndexCache = { key, ...buildUiIndex(dtsPath) };
204
+ try {
205
+ mkdirSync(UI_INDEX_CACHE_DIR, { recursive: true });
206
+ // rename is atomic, so a concurrent server never reads a half-written file.
207
+ const tmp = `${UI_INDEX_CACHE}.${process.pid}.tmp`;
208
+ writeFileSync(tmp, JSON.stringify(uiIndexCache));
209
+ renameSync(tmp, UI_INDEX_CACHE);
210
+ } catch {
211
+ /* read-only fs / sandbox: the cache is an optimisation, never a requirement */
212
+ }
213
+ return uiIndexCache;
214
+ }
215
+
85
216
  /* -------------------------------------------------------------------------- */
86
217
  /* Storybook story source lookup */
87
218
  /* -------------------------------------------------------------------------- */
@@ -111,10 +242,28 @@ function findStorySource(name) {
111
242
  /* -------------------------------------------------------------------------- */
112
243
 
113
244
  // Split the README on headings and keyword-score each section.
245
+ // Headings inside fenced code blocks (```…```) don't count — a column-0
246
+ // Python comment would otherwise start a bogus section.
247
+ function splitSections(text) {
248
+ const sections = [];
249
+ let current = [];
250
+ let inFence = false;
251
+ for (const line of text.split("\n")) {
252
+ if (/^\s*```/.test(line)) inFence = !inFence;
253
+ if (!inFence && /^#{1,4} /.test(line) && current.length) {
254
+ sections.push(current.join("\n"));
255
+ current = [];
256
+ }
257
+ current.push(line);
258
+ }
259
+ if (current.length) sections.push(current.join("\n"));
260
+ return sections;
261
+ }
262
+
114
263
  function searchReadme(query) {
115
264
  if (!existsSync(TEMPLATE_README)) return [];
116
265
  const text = readFileSync(TEMPLATE_README, "utf8");
117
- const sections = text.split(/\n(?=#{1,4} )/);
266
+ const sections = splitSections(text);
118
267
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
119
268
 
120
269
  return sections
@@ -243,6 +392,147 @@ ${script}
243
392
  return { data };
244
393
  }
245
394
 
395
+ /* -------------------------------------------------------------------------- */
396
+ /* rainbow-rb-sdk-dev (C++) headers — copied out of the cpp dev image */
397
+ /* -------------------------------------------------------------------------- */
398
+
399
+ const CPP_INCLUDE_CACHE = join(UI_INDEX_CACHE_DIR, "rb-sdk-include");
400
+ // Headers we don't want to surface: third-party vendored code, pybind glue,
401
+ // flatbuffers-generated schemas, and the industrial comm stacks.
402
+ const CPP_SKIP = /^(vendor|binding|rb_comm_source)\/|_generated\.h$/;
403
+
404
+ // The apt package is only installed inside the muscat-pk-cpp-dev image, so
405
+ // copy /usr/include/rainbow/rb_sdk out once per image build (cache key =
406
+ // image id) with `docker cp` — no host tar needed.
407
+ function cppIncludeDir() {
408
+ const insp = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", CPP_DEV_IMAGE], { encoding: "utf8" });
409
+ if (insp.error) return { error: "Docker not found — the C++ SDK headers live in the muscat-pk cpp dev image." };
410
+ if (insp.status !== 0) {
411
+ return {
412
+ error:
413
+ `Docker image ${CPP_DEV_IMAGE} not built yet. Run \`muscat-pk dev:back\` (or \`muscat-pk build\`) once ` +
414
+ "in a cpp / rclcpp plugin to build it, then retry.",
415
+ };
416
+ }
417
+ const dir = join(CPP_INCLUDE_CACHE, insp.stdout.trim().replace(/^sha256:/, "").slice(0, 12));
418
+ const out = join(dir, "rb_sdk");
419
+ if (existsSync(out)) return { dir: out };
420
+ mkdirSync(dir, { recursive: true });
421
+ const ctr = spawnSync("docker", ["create", CPP_DEV_IMAGE], { encoding: "utf8" });
422
+ if (ctr.status !== 0) return { error: `docker create failed: ${(ctr.stderr || "").trim().slice(0, 300)}` };
423
+ const id = ctr.stdout.trim();
424
+ const cp = spawnSync("docker", ["cp", `${id}:/usr/include/rainbow/rb_sdk`, dir], { encoding: "utf8" });
425
+ spawnSync("docker", ["rm", "-f", id], { encoding: "utf8" });
426
+ if (cp.status !== 0 || !existsSync(out)) {
427
+ return { error: `docker cp failed: ${(cp.stderr || "").trim().slice(0, 300)}` };
428
+ }
429
+ return { dir: out };
430
+ }
431
+
432
+ function cppHeaders(root) {
433
+ const files = [];
434
+ const walk = (d, rel) => {
435
+ for (const name of readdirSync(d)) {
436
+ const r = rel ? `${rel}/${name}` : name;
437
+ if (CPP_SKIP.test(r)) continue;
438
+ const full = join(d, name);
439
+ if (statSync(full).isDirectory()) walk(full, r);
440
+ else if (/\.hpp$/.test(name)) files.push(r);
441
+ }
442
+ };
443
+ walk(root, "");
444
+ return files.sort();
445
+ }
446
+
447
+ // "class Foo {" / "class Foo : public Bar {" — not forward declarations.
448
+ const CLASS_RE = /^\s*(?:class|struct)\s+(\w+)\b(?![^\n]*;\s*$)/;
449
+
450
+ function cppClassesIn(text) {
451
+ return [...new Set(text.split("\n").map((l) => l.match(CLASS_RE)?.[1]).filter(Boolean))];
452
+ }
453
+
454
+ // Return the class body (brace-matched) plus the file's leading comment block,
455
+ // which in these headers usually carries a full usage example.
456
+ function cppExtract(text, className, method) {
457
+ const lines = text.split("\n");
458
+ const start = lines.findIndex((l) => CLASS_RE.test(l) && l.match(CLASS_RE)[1] === className);
459
+ if (start < 0) return null;
460
+ let depth = 0, end = start, seen = false;
461
+ for (let i = start; i < lines.length; i++) {
462
+ for (const ch of lines[i]) {
463
+ if (ch === "{") { depth++; seen = true; }
464
+ else if (ch === "}") depth--;
465
+ }
466
+ if (seen && depth === 0) { end = i; break; }
467
+ }
468
+ const body = lines.slice(start, end + 1);
469
+ if (method) {
470
+ // Declarations sit at class-member indent (<= 2 spaces) and the name is
471
+ // not preceded by `.`/`->`/`::` — that skips call sites inside bodies.
472
+ const re = new RegExp(`^ {0,2}[^(]*(?<![\\w.>:])${method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
473
+ const hits = [];
474
+ for (let i = 0; i < body.length; i++) {
475
+ if (!re.test(body[i]) || /^\s*\/\//.test(body[i])) continue;
476
+ let a = i;
477
+ while (a > 0 && /^\s*\/\//.test(body[a - 1])) a--;
478
+ let b = i;
479
+ while (b < body.length - 1 && !/[{;]/.test(body[b])) b++;
480
+ hits.push(body.slice(a, b + 1).join("\n"));
481
+ }
482
+ return hits.length ? hits.join("\n\n") : null;
483
+ }
484
+ const head = [];
485
+ for (const l of lines) {
486
+ if (/^\s*\/\//.test(l)) head.push(l);
487
+ else if (l.trim() === "" || /^#pragma/.test(l)) continue;
488
+ else break;
489
+ }
490
+ return (head.length > 3 ? head.join("\n") + "\n\n" : "") + body.join("\n");
491
+ }
492
+
493
+ function cppFind(className) {
494
+ const { dir, error } = cppIncludeDir();
495
+ if (error) return { error };
496
+ for (const rel of cppHeaders(dir)) {
497
+ const text = readFileSync(join(dir, rel), "utf8");
498
+ if (cppClassesIn(text).includes(className)) return { rel, text };
499
+ }
500
+ return { error: `No class "${className}" in the C++ SDK headers. Call list_rb_sdk with lang "cpp" for valid names.` };
501
+ }
502
+
503
+ /* -------------------------------------------------------------------------- */
504
+ /* client registration — `muscat-pk mcp --install <client>` */
505
+ /* -------------------------------------------------------------------------- */
506
+
507
+ // Claude Code needs no install step: `muscat-pk new` scaffolds a project-level
508
+ // .mcp.json. Codex only reads the global ~/.codex/config.toml, so we append
509
+ // the server block there (idempotent).
510
+ export function installMcp(target) {
511
+ if (target !== "codex") {
512
+ throw new Error(
513
+ `Unknown --install target ${JSON.stringify(target ?? "")} — supported: "codex". ` +
514
+ "(Claude Code works out of the box via the scaffolded .mcp.json.)",
515
+ );
516
+ }
517
+ const cfgDir = join(homedir(), ".codex");
518
+ const cfgPath = join(cfgDir, "config.toml");
519
+ const existing = existsSync(cfgPath) ? readFileSync(cfgPath, "utf8") : "";
520
+ if (/^\s*\[mcp_servers\.muscat\]/m.test(existing)) {
521
+ console.log(`Codex already has the muscat MCP server registered (${cfgPath}) — nothing to do.`);
522
+ return;
523
+ }
524
+ // ponytail: Windows Codex spawns without a shell, so it needs the .cmd shim.
525
+ const command = process.platform === "win32" ? "muscat-pk.cmd" : "muscat-pk";
526
+ mkdirSync(cfgDir, { recursive: true });
527
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
528
+ appendFileSync(
529
+ cfgPath,
530
+ `${sep}[mcp_servers.muscat]\ncommand = "${command}"\nargs = ["mcp"]\n`,
531
+ );
532
+ console.log(`Registered the muscat MCP server for Codex → ${cfgPath}`);
533
+ console.log("Restart Codex (or start a new session) to pick it up.");
534
+ }
535
+
246
536
  /* -------------------------------------------------------------------------- */
247
537
  /* server */
248
538
  /* -------------------------------------------------------------------------- */
@@ -261,11 +551,8 @@ export async function runMcp() {
261
551
  inputSchema: {},
262
552
  },
263
553
  async () => {
264
- const { checker, exports } = await loadReactTypes();
265
- const lines = exports
266
- .map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
267
- .sort();
268
- return { content: [{ type: "text", text: lines.join("\n") }] };
554
+ const { components } = await loadUiIndex();
555
+ return { content: [{ type: "text", text: components }] };
269
556
  },
270
557
  );
271
558
 
@@ -279,9 +566,8 @@ export async function runMcp() {
279
566
  inputSchema: { name: z.string().describe('Exported name, e.g. "RBSlider"') },
280
567
  },
281
568
  async ({ name }) => {
282
- const { checker, sourceFile, exports } = await loadReactTypes();
283
- const symbol = exports.find((sym) => sym.getName() === name);
284
- if (!symbol) {
569
+ const { props } = await loadUiIndex();
570
+ if (!Object.hasOwn(props, name)) {
285
571
  return {
286
572
  isError: true,
287
573
  content: [
@@ -292,15 +578,7 @@ export async function runMcp() {
292
578
  ],
293
579
  };
294
580
  }
295
- const resolved = resolveExport(checker, symbol);
296
- const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
297
- const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
298
- const text = checker.typeToString(
299
- type,
300
- undefined,
301
- ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
302
- );
303
- return { content: [{ type: "text", text }] };
581
+ return { content: [{ type: "text", text: props[name] }] };
304
582
  },
305
583
  );
306
584
 
@@ -347,8 +625,13 @@ export async function runMcp() {
347
625
  title: "Search the Muscat plugin-author docs",
348
626
  description:
349
627
  "Search the Muscat plugin-author README for how to use useRb(), rb.pub/sub/action/serve, " +
628
+ "plugin.router (external endpoints: router.on/subscribe/emit exposed over REST/Socket.IO/TCP/" +
629
+ "MQTT/WebRTC at common/plugin/{plugin_id}/{path}, PluginRouteError, meta.transport/principal), " +
350
630
  "rb.toast/alert/confirm/prompt, useRbFormData/useRbSave (save/restore form data through the " +
351
- "host), manifest.json, contribution points, and backend runtimes (Python/C++/ROS2). IMPORTANT: " +
631
+ "host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
632
+ "names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
633
+ "set_variable/executor_state/check_stop), contribution points, and backend runtimes " +
634
+ "(Python/C++/ROS2). IMPORTANT: " +
352
635
  "the backend pattern depends on the contribution point — search 'which backend pattern' before " +
353
636
  "writing py/main.py. Only a program.panel command uses PluginProgramContext; a toolbar/move.panel " +
354
637
  "plugin controls the robot with the SDK (RBManipulateSDK/RBAmrSDK) directly.",
@@ -391,7 +674,10 @@ export async function runMcp() {
391
674
  const other = { shadow: [], radius: [], spacing: [] };
392
675
  for (const t of vars) {
393
676
  const m = t.group?.match(/^color-(bg|fg|border|fill)$/);
394
- if (m && t.class) colorGroups[m[1]].push(`${t.class} (${t.value})`);
677
+ // Class name only: the value is just an indirection to another CSS var
678
+ // (var(--color-scale-zinc-200)), and each class is declared twice (light/dark)
679
+ // with different values — dropping the value lets the Set dedup collapse them.
680
+ if (m && t.class) colorGroups[m[1]].push(t.class);
395
681
  else if (t.name.startsWith("shadow-")) other.shadow.push(`shadow-${t.name.slice(7)}`);
396
682
  else if (t.name.startsWith("radius-")) other.radius.push(`rounded-${t.name.slice(7)}`);
397
683
  else if (t.name.startsWith("spacing-")) other.spacing.push(t.name);
@@ -454,14 +740,28 @@ export async function runMcp() {
454
740
  server.registerTool(
455
741
  "list_rb_sdk",
456
742
  {
457
- title: "List rainbow-rb-sdk (Python) classes and functions",
743
+ title: "List rainbow-rb-sdk classes and functions (Python or C++)",
458
744
  description:
459
- "List the public classes and functions of the installed rainbow.rb_sdk Python package — the " +
460
- "plugin BACKEND SDK (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, ...). Call " +
461
- "before writing py/main.py. Requires rainbow-rb-sdk to be pip-installed.",
462
- inputSchema: {},
745
+ "List the public classes and functions of the plugin BACKEND SDK. lang \"python\" (default): the " +
746
+ "installed rainbow.rb_sdk package (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, " +
747
+ "...) — call before writing py/main.py; requires rainbow-rb-sdk to be pip-installed. lang \"cpp\": " +
748
+ "the C++ SDK headers (apt rainbow-rb-sdk-dev: RBManipulateSDK, rb_plugin::PluginProgramContext, " +
749
+ "...) grouped by header path — call before writing cpp/main.cpp or an rclcpp node; requires the " +
750
+ "muscat-pk-cpp-dev Docker image (built by `muscat-pk dev:back`). Note: RBPluginCall (UI pub/sub/" +
751
+ "serve) exists in Python only.",
752
+ inputSchema: { lang: z.enum(["python", "cpp"]).optional().describe('Default "python"') },
463
753
  },
464
- async () => {
754
+ async ({ lang }) => {
755
+ if (lang === "cpp") {
756
+ const { dir, error } = cppIncludeDir();
757
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
758
+ const out = [];
759
+ for (const rel of cppHeaders(dir)) {
760
+ const classes = cppClassesIn(readFileSync(join(dir, rel), "utf8"));
761
+ if (classes.length) out.push(`${rel}: ${classes.join(", ")}`);
762
+ }
763
+ return { content: [{ type: "text", text: `#include <rainbow/rb_sdk/...>\n\n${out.join("\n")}` }] };
764
+ }
465
765
  const { data, error } = introspectRbSdk(`
466
766
  import inspect, json
467
767
  mod = importlib.import_module("rainbow.rb_sdk")
@@ -481,19 +781,22 @@ print(json.dumps(out))
481
781
  server.registerTool(
482
782
  "get_rb_sdk_api",
483
783
  {
484
- title: "Get a rainbow-rb-sdk class/function signature + docs",
784
+ title: "Get a rainbow-rb-sdk class/function signature + docs (Python or C++)",
485
785
  description:
486
786
  "Get the signature(s) and docstring for a rainbow.rb_sdk class or function (e.g. " +
487
787
  "'PluginProgramContext', 'RBManipulateSDK'). For a class, lists its public methods with " +
488
788
  "signatures. To drill into ONE method of a class (e.g. RBManipulateSDK.call_move_j), pass " +
489
- "`method` alongside `name` — do not put both in `name`. Requires rainbow-rb-sdk to be " +
490
- "pip-installed.",
789
+ "`method` alongside `name` — do not put both in `name`. lang \"python\" (default) introspects the " +
790
+ "pip-installed package. lang \"cpp\" returns the class declaration from the C++ SDK header " +
791
+ "(rainbow-rb-sdk-dev) with its header comment — usually a full usage example — and the " +
792
+ "#include path; with `method`, just that method's declaration and comment.",
491
793
  inputSchema: {
492
794
  name: z.string().describe('Class or function name, e.g. "RBManipulateSDK"'),
493
795
  method: z.string().optional().describe('Optional: a specific method on that class, e.g. "call_move_j"'),
796
+ lang: z.enum(["python", "cpp"]).optional().describe('Default "python"'),
494
797
  },
495
798
  },
496
- async ({ name, method }) => {
799
+ async ({ name, method, lang }) => {
497
800
  // Tolerate a combined "ClassName method_name" in `name` (a common agent
498
801
  // mistake) by splitting it ourselves instead of failing validation.
499
802
  let className = name;
@@ -503,6 +806,42 @@ print(json.dumps(out))
503
806
  if (parts.length === 2) [className, methodName] = parts;
504
807
  }
505
808
 
809
+ if (lang === "cpp") {
810
+ const cls = className.replace(/^.*::/, "");
811
+ let { rel, text, error } = cppFind(cls);
812
+ if (error) return { isError: true, content: [{ type: "text", text: error }] };
813
+ let body = cppExtract(text, cls, methodName);
814
+ let owner = cls;
815
+ if (!body && methodName) {
816
+ // Like the Python path: most calls live on sub-SDK members
817
+ // (RBManipulateMoveSDK move; -> sdk.move.call_move_j) or on the base
818
+ // class — look there before giving up.
819
+ const decl = cppExtract(text, cls);
820
+ const cands = [
821
+ ...[...decl.matchAll(/^\s*(\w+SDK)\s+(\w+);/gm)].map((m) => [m[1], `${cls}.${m[2]}`]),
822
+ ...[...decl.matchAll(/:\s*public\s+(\w+)/g)].map((m) => [m[1], `${cls} (base ${m[1]})`]),
823
+ ];
824
+ for (const [c, label] of cands) {
825
+ const f = cppFind(c);
826
+ if (f.error) continue;
827
+ const b = cppExtract(f.text, c, methodName);
828
+ if (b) { ({ rel, text } = f); body = b; owner = label; break; }
829
+ }
830
+ }
831
+ if (!body) {
832
+ return {
833
+ isError: true,
834
+ 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.` }],
835
+ };
836
+ }
837
+ const ns = text.match(/^namespace\s+(\w+)\s*\{/m)?.[1];
838
+ const header =
839
+ `// #include <rainbow/rb_sdk/${rel}>${ns ? ` (namespace ${ns})` : ""}\n` +
840
+ `// link: find_package(rb_sdk) + rb_sdk::rb_sdk\n` +
841
+ (methodName ? `// ${owner}.${methodName}\n` : "") + "\n";
842
+ return { content: [{ type: "text", text: header + body.slice(0, 60_000) }] };
843
+ }
844
+
506
845
  const { data, error } = introspectRbSdk(`
507
846
  import inspect, json
508
847
  mod = importlib.import_module("rainbow.rb_sdk")
package/cli/src/new.mjs CHANGED
@@ -65,6 +65,9 @@ function printNewSuccess({ targetName, type, pm }) {
65
65
  log.info(` ${color.cyan("muscat-pk dev")} ${color.dim("# preview in mini-Muscat + back end")}`);
66
66
  log.info(` ${color.cyan("muscat-pk build")} ${color.dim("# build + distributable ZIP")}`);
67
67
  log.info("");
68
+ log.info(color.dim("AI assistant? Claude Code reads the scaffolded .mcp.json;"));
69
+ log.info(color.dim(`Codex users run ${color.cyan("muscat-pk mcp --install codex")} once.`));
70
+ log.info("");
68
71
  }
69
72
 
70
73
  // Build the manifest runtime object for a given type choice + plugin slug.
package/cli/src/ports.mjs CHANGED
@@ -28,9 +28,9 @@ function reservedPortReason(port) {
28
28
  return `${range.name} (${range.from}-${range.to})`;
29
29
  }
30
30
 
31
- function isPluginManagedPort(port) {
32
- return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
33
- }
31
+ // function isPluginManagedPort(port) {
32
+ // return reservedPortReason(port) || (port >= PLUGIN_PORT_MIN && port <= PLUGIN_PORT_MAX);
33
+ // }
34
34
 
35
35
  function isInt(value) {
36
36
  return Number.isInteger(value);
package/cli/src/ros2.mjs CHANGED
@@ -37,9 +37,11 @@ export async function ensureImage(image = ROS2_DEV_IMAGE, { platform = null } =
37
37
  const dockerfile = await resolveDockerfile("Dockerfile.dev");
38
38
  log.step(`docker build -t ${image}${platform ? ` (${platform})` : ""} (first run is slow, then cached)…`);
39
39
  const platformFlags = platform ? ["--platform", platform] : [];
40
+ const sdkVer = process.env.MUSCAT_PK_RB_SDK_VERSION;
41
+ const buildArgs = sdkVer ? ["--build-arg", `RB_SDK_VERSION=${sdkVer}`] : [];
40
42
  const r = spawnSync(
41
43
  "docker",
42
- ["build", ...platformFlags, "-t", image, "-f", dockerfile, dirname(dockerfile)],
44
+ ["build", ...platformFlags, ...buildArgs, "-t", image, "-f", dockerfile, dirname(dockerfile)],
43
45
  { stdio: "inherit" },
44
46
  );
45
47
  if (r.status !== 0) throw new Error("docker build failed.");