@rainbow-robotics/muscat-plugin-kit 1.3.6-dev.8 → 1.3.7
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/cli/README.md +8 -3
- package/cli/bin/muscat-pk.mjs +5 -0
- package/cli/bundle-templates.mjs +6 -1
- package/cli/docker/Dockerfile.cpp +28 -6
- package/cli/docker/Dockerfile.dev +32 -0
- package/cli/docker/Dockerfile.python +19 -0
- package/cli/src/bridge.mjs +45 -12
- package/cli/src/build.mjs +48 -79
- package/cli/src/cpp.mjs +11 -5
- package/cli/src/dev.mjs +7 -2
- package/cli/src/devBack.mjs +97 -40
- package/cli/src/host_agent.py +143 -83
- package/cli/src/manifest.mjs +11 -0
- package/cli/src/mcp.mjs +398 -54
- package/cli/src/new.mjs +13 -3
- package/cli/src/ports.mjs +21 -7
- package/cli/src/python.mjs +16 -20
- package/cli/src/ros2.mjs +16 -8
- package/cli/src/sdk.mjs +23 -21
- package/cli/src/type.mjs +44 -10
- package/cli/src/uninstall.mjs +3 -20
- package/cli/src/update.mjs +14 -8
- package/cli/src/util.mjs +70 -9
- package/cli/storybook-dist/_stories-src/RBDatePicker/RBRangeDateInput.stories.tsx +13 -1
- package/cli/storybook-dist/_stories-src/RBInput/RBSelectBox.stories.tsx +98 -1
- package/cli/storybook-dist/assets/{Color-YHDXOIA2-Du8dJ-nM.js → Color-YHDXOIA2-CdTgUqrV.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Sy5RDbtH.js → Docs-C8wx5zsC.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-guGflRzZ.js → Docs-CbRKwHpQ.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-Cb72AJnj.js → Docs-Cf0s04QW.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-pwGqGTV_.js → Docs-CsxMprX5.js} +1 -1
- package/cli/storybook-dist/assets/{Docs-LdjYwuG4.js → Docs-FyPFrbUS.js} +1 -1
- package/cli/storybook-dist/assets/{Docs--Fm8069p.js → Docs-YtqU-aVu.js} +1 -1
- package/cli/storybook-dist/assets/{DocsRenderer-CFRXHY34-YGBLQ4BD.js → DocsRenderer-CFRXHY34-VSENNRoY.js} +1 -1
- package/cli/storybook-dist/assets/{NxAvatar.stories-C1d4n8cq.js → NxAvatar.stories-oCcYtLwq.js} +1 -1
- package/cli/storybook-dist/assets/{NxButton.stories-gCBoZdMq.js → NxButton.stories-C5jUScIj.js} +1 -1
- package/cli/storybook-dist/assets/{NxIcons.stories-Dg42riT5.js → NxIcons.stories-DeqrVzVs.js} +1 -1
- package/cli/storybook-dist/assets/{NxNumberInput.stories-DB-vLM2J.js → NxNumberInput.stories-gjsEulIN.js} +1 -1
- package/cli/storybook-dist/assets/{NxPagination.stories-CS8iMIak.js → NxPagination.stories-BK6uSWIa.js} +1 -1
- package/cli/storybook-dist/assets/{NxPointerHoldButton.stories-D66BopeS.js → NxPointerHoldButton.stories-BDD5CvSZ.js} +1 -1
- package/cli/storybook-dist/assets/{NxRangeInput.stories-CZxddsJ2.js → NxRangeInput.stories-C_nW26_x.js} +3 -3
- package/cli/storybook-dist/assets/{NxSelectBox.stories-CmezGGrm.js → NxSelectBox.stories-DqLfKU9H.js} +1 -1
- package/cli/storybook-dist/assets/{NxSlideRange.stories-CGAFMITi.js → NxSlideRange.stories-CQjHA8yv.js} +1 -1
- package/cli/storybook-dist/assets/{NxSwitch.stories-B3QInkXX.js → NxSwitch.stories-BMHCiPoL.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextArea.stories-BgJcgZNe.js → NxTextArea.stories-k7r1WVQh.js} +1 -1
- package/cli/storybook-dist/assets/{NxTextInput.stories-CwMQwI9L.js → NxTextInput.stories-Cqfe1I32.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordion.stories-BuBVNzJT.js → RBAccordion.stories-CJ3Ttuja.js} +1 -1
- package/cli/storybook-dist/assets/{RBAccordionGroup.stories-Crdb6kFF.js → RBAccordionGroup.stories-BThv7tM0.js} +1 -1
- package/cli/storybook-dist/assets/{RBAlert.stories-CqXgUvA2.js → RBAlert.stories-CSMP8Vun.js} +1 -1
- package/cli/storybook-dist/assets/{RBBadge.stories-BCw_ksML.js → RBBadge.stories-cBMA2R6F.js} +1 -1
- package/cli/storybook-dist/assets/{RBBattery.stories-0id4XYoc.js → RBBattery.stories-BKVOskqs.js} +1 -1
- package/cli/storybook-dist/assets/{RBCard.stories-CgpG-7Lk.js → RBCard.stories-BTOfK0HC.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckBox.stories-BG02BoGX.js → RBCheckBox.stories-DyE912wG.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckIconButton.stories-vrjD4GQ4.js → RBCheckIconButton.stories-0sLFU0kM.js} +1 -1
- package/cli/storybook-dist/assets/{RBCheckLabelButton.stories-POba8lU-.js → RBCheckLabelButton.stories-c8VecClE.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipTab.stories-XWJbFrHF.js → RBChipTab.stories-CQ9Uav5_.js} +1 -1
- package/cli/storybook-dist/assets/{RBChipsBoxTab.stories-BdU0Dzdp.js → RBChipsBoxTab.stories-Ap4NlhAZ.js} +1 -1
- package/cli/storybook-dist/assets/{RBColorIcons.stories-C_0dCNXG.js → RBColorIcons.stories-BPPXjif2.js} +1 -1
- package/cli/storybook-dist/assets/{RBCommand.stories-Dvrko5No.js → RBCommand.stories-PDpYOKlm.js} +1 -1
- package/cli/storybook-dist/assets/{RBConfirm.stories-CkTn0sHa.js → RBConfirm.stories-BEWvMAYP.js} +1 -1
- package/cli/storybook-dist/assets/{RBDateInput.stories-BH3D08cs.js → RBDateInput.stories-DLdyNnVF.js} +1 -1
- package/cli/storybook-dist/assets/{RBDialog.stories-C2nNX9fl.js → RBDialog.stories-HsawMNOb.js} +1 -1
- package/cli/storybook-dist/assets/{RBDivider.stories-D-GrJ0QZ.js → RBDivider.stories-dlWwOxik.js} +1 -1
- package/cli/storybook-dist/assets/{RBDrawer.stories-XLOnLOnh.js → RBDrawer.stories-C87iqGk0.js} +1 -1
- package/cli/storybook-dist/assets/{RBDropdownMenu.stories-BjBSeoV2.js → RBDropdownMenu.stories-B8EQhA0z.js} +1 -1
- package/cli/storybook-dist/assets/{RBEmptyView.stories-C6U2zKNE.js → RBEmptyView.stories-SzpIhNDw.js} +1 -1
- package/cli/storybook-dist/assets/{RBIconButton.stories-MCJq5Hta.js → RBIconButton.stories-BQ0nM1gb.js} +1 -1
- package/cli/storybook-dist/assets/{RBIcons.stories-DxOCiCe0.js → RBIcons.stories-C19g5M5z.js} +1 -1
- package/cli/storybook-dist/assets/{RBInlineLabelButton.stories-BVZi1aEs.js → RBInlineLabelButton.stories-DbgI4-1P.js} +1 -1
- package/cli/storybook-dist/assets/{RBJoyStick.stories-BDThbEek.js → RBJoyStick.stories-smF-vpGL.js} +1 -1
- package/cli/storybook-dist/assets/{RBLabelButton.stories-CaPEKUS6.js → RBLabelButton.stories-s53klwDv.js} +1 -1
- package/cli/storybook-dist/assets/{RBLight.stories-CihQzyEL.js → RBLight.stories-BnzkoR51.js} +1 -1
- package/cli/storybook-dist/assets/{RBMenubar.stories-J852XVFh.js → RBMenubar.stories-CW014klu.js} +1 -1
- package/cli/storybook-dist/assets/{RBMultiRangeInput.stories-DXyX0cOZ.js → RBMultiRangeInput.stories-qa4tH_4Y.js} +1 -1
- package/cli/storybook-dist/assets/{RBPagination.stories-C9_49ymO.js → RBPagination.stories-YoQNUpQH.js} +1 -1
- package/cli/storybook-dist/assets/{RBPopover.stories-DxLE912-.js → RBPopover.stories-Bhpi0OaH.js} +1 -1
- package/cli/storybook-dist/assets/{RBPrompt.stories-DlsfGqWb.js → RBPrompt.stories-iQM5iAka.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioBox.stories-hb5cNWW1.js → RBRadioBox.stories-BGyq4rzx.js} +1 -1
- package/cli/storybook-dist/assets/{RBRadioButton.stories-CSuOgeN-.js → RBRadioButton.stories-DF6BcUcJ.js} +1 -1
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-COOQ_HNF.js +15 -0
- package/cli/storybook-dist/assets/{RBResizable.stories-BetjF9vG.js → RBResizable.stories-DSVHvwjp.js} +1 -1
- package/cli/storybook-dist/assets/RBSelectBox.stories-BookV8Sm.js +74 -0
- package/cli/storybook-dist/assets/{RBSheet.stories-D3QbcMKq.js → RBSheet.stories-CAK9hb-p.js} +1 -1
- package/cli/storybook-dist/assets/{RBSideNumberInput.stories-DcTh0IjM.js → RBSideNumberInput.stories-DUymnH-9.js} +1 -1
- package/cli/storybook-dist/assets/{RBSlider.stories-B59We7ei.js → RBSlider.stories-BwlDh9--.js} +1 -1
- package/cli/storybook-dist/assets/{RBSpinner.stories-9u3_ETbi.js → RBSpinner.stories-VOlkuP6t.js} +1 -1
- package/cli/storybook-dist/assets/{RBSwitch.stories-DrVm1WGX.js → RBSwitch.stories-BiW2dQnT.js} +1 -1
- package/cli/storybook-dist/assets/{RBTab.stories-D8Dm7cRW.js → RBTab.stories-DFKVsy0C.js} +1 -1
- package/cli/storybook-dist/assets/{RBTable.stories-dRdZ8g1j.js → RBTable.stories-BWG0ZSYI.js} +1 -1
- package/cli/storybook-dist/assets/{RBTableRow.stories-rZeoGwHZ.js → RBTableRow.stories-JVTD83Mh.js} +1 -1
- package/cli/storybook-dist/assets/{RBTabs.stories-STRV2lgO.js → RBTabs.stories-D-aN385u.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextArea.stories-CfgUbrbJ.js → RBTextArea.stories-Dx5xkmZS.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextField.stories-B5t8-Y97.js → RBTextField.stories-DmVciExd.js} +1 -1
- package/cli/storybook-dist/assets/{RBTextInput.stories-Bf1edN6l.js → RBTextInput.stories-CCf0VGEv.js} +1 -1
- package/cli/storybook-dist/assets/{RBToast.stories-Cnm8JwQ6.js → RBToast.stories-DT0DGn05.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggle.stories-B0OCo6zd.js → RBToggle.stories-Ce_q3EtP.js} +1 -1
- package/cli/storybook-dist/assets/{RBToggleGroup.stories-GdYO2Fxg.js → RBToggleGroup.stories-CN40YHsR.js} +1 -1
- package/cli/storybook-dist/assets/{RBTooltip.stories-DXJTCScO.js → RBTooltip.stories-L7btDEpA.js} +1 -1
- package/cli/storybook-dist/assets/SideNumberInput-B5_xLK8z.js +1109 -0
- package/cli/storybook-dist/assets/browserUtils-CQM9Za7e.js +1 -0
- package/cli/storybook-dist/assets/{entry-preview-DSRwIaVJ.js → entry-preview-raBQdKjS.js} +1 -1
- package/cli/storybook-dist/assets/{iframe-Cj53Wn3d.js → iframe-BmIue__l.js} +2 -2
- package/cli/storybook-dist/assets/{index-CkHtLEe8.js → index-CSIe-YDr.js} +3 -3
- package/cli/storybook-dist/assets/{index-C1OzuK6-.js → index-ClAc6FBW.js} +1 -1
- package/cli/storybook-dist/assets/{preview-CILRoSnB.js → preview-BVJwlGUF.js} +1 -1
- package/cli/storybook-dist/assets/{preview-D_2um8Sd.js → preview-CUgthyJT.js} +1 -1
- package/cli/storybook-dist/assets/{preview-CgOtgtCD.js → preview-DD51RTKv.js} +2 -2
- package/cli/storybook-dist/assets/preview-zuAvMksL.css +1 -0
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CYcsrUgg.js +120 -0
- package/cli/storybook-dist/iframe.html +1 -1
- package/cli/storybook-dist/index.json +1 -1
- package/cli/storybook-dist/project.json +1 -1
- package/cli/templates/common/README.md +211 -7
- package/cli/templates/common/py/main.py +29 -2
- package/cli/templates/cpp/cpp/CMakeLists.txt +6 -7
- package/cli/templates/cpp/cpp/main.cpp +57 -10
- package/cli/templates/readme-cpp/README.md +993 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/CMakeLists.txt +4 -0
- package/cli/templates/ros2-cpp/ros2_ws/src/__PLUGIN_SLUG__/src/node.cpp +5 -4
- package/package.json +3 -3
- package/cli/storybook-dist/assets/RBRangeDateInput.stories-KRg2glX8.js +0 -15
- package/cli/storybook-dist/assets/RBSelectBox.stories-ILLQQ8M8.js +0 -6
- package/cli/storybook-dist/assets/SideNumberInput-CrF-a1Nr.js +0 -1114
- package/cli/storybook-dist/assets/browserUtils-CxJ5gea3.js +0 -1
- package/cli/storybook-dist/assets/preview-CEfMaO1t.css +0 -1
- package/cli/storybook-dist/assets/react-resizable-panels.browser-CtORL3tG.js +0 -102
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,15 +24,26 @@ 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 {
|
|
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";
|
|
27
38
|
import { homedir } from "node:os";
|
|
28
39
|
import { join, resolve } from "node:path";
|
|
29
40
|
import { fileURLToPath } from "node:url";
|
|
30
41
|
import { kitRoot, kitVersion, findPluginRoot, venvPython } from "./util.mjs";
|
|
42
|
+
import { PYTHON_DEV_IMAGE } from "./python.mjs";
|
|
31
43
|
import { SHIPPED_STORIES_SRC, SHIPPED_DESIGN_TOKENS } from "./storybook.mjs";
|
|
44
|
+
import { CPP_DEV_IMAGE } from "./cpp.mjs";
|
|
32
45
|
|
|
33
|
-
const
|
|
46
|
+
const TEMPLATE_READMES = { python: "common/README.md", cpp: "readme-cpp/README.md" };
|
|
34
47
|
// Design tokens: prefer the shipped copy, fall back to the monorepo source.
|
|
35
48
|
const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
|
|
36
49
|
? SHIPPED_DESIGN_TOKENS
|
|
@@ -40,14 +53,10 @@ const DESIGN_TOKENS_CSS = existsSync(SHIPPED_DESIGN_TOKENS)
|
|
|
40
53
|
/* react.d.ts introspection — component/hook list + prop types */
|
|
41
54
|
/* -------------------------------------------------------------------------- */
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
// is enough even in an external install.
|
|
48
|
-
async function loadReactTypes() {
|
|
49
|
-
if (typesCache) return typesCache;
|
|
50
|
-
|
|
56
|
+
// plugin-ui/react's dist/react.d.ts. It's self-contained (rb-components types
|
|
57
|
+
// are inlined at build — see plugin-ui/build-dist.mjs), so this one file is
|
|
58
|
+
// enough even in an external install.
|
|
59
|
+
async function reactDtsPath() {
|
|
51
60
|
const entryUrl = await import.meta.resolve("@rainbow-robotics/plugin-ui/react");
|
|
52
61
|
const dtsPath = fileURLToPath(entryUrl).replace(/\.js$/, ".d.ts");
|
|
53
62
|
if (!existsSync(dtsPath)) {
|
|
@@ -55,19 +64,23 @@ async function loadReactTypes() {
|
|
|
55
64
|
`plugin-ui type declarations not found at ${dtsPath} — reinstall or rebuild @rainbow-robotics/plugin-ui.`,
|
|
56
65
|
);
|
|
57
66
|
}
|
|
67
|
+
return dtsPath;
|
|
68
|
+
}
|
|
58
69
|
|
|
70
|
+
function loadReactTypes(dtsPath) {
|
|
71
|
+
// Without moduleResolution the `import ... from "react"` inside react.d.ts
|
|
72
|
+
// never resolves and every React-derived type collapses to `any`.
|
|
59
73
|
const program = ts.createProgram([dtsPath], {
|
|
60
74
|
target: ts.ScriptTarget.ES2022,
|
|
61
75
|
module: ts.ModuleKind.ESNext,
|
|
76
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
62
77
|
skipLibCheck: true,
|
|
63
78
|
});
|
|
64
79
|
const sourceFile = program.getSourceFile(dtsPath);
|
|
65
80
|
const checker = program.getTypeChecker();
|
|
66
81
|
const moduleSymbol = sourceFile && checker.getSymbolAtLocation(sourceFile);
|
|
67
82
|
const exports = moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : [];
|
|
68
|
-
|
|
69
|
-
typesCache = { checker, sourceFile, exports };
|
|
70
|
-
return typesCache;
|
|
83
|
+
return { checker, sourceFile, exports };
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
// The barrel's trailing `export { A, B, ... }` produces Alias symbols whose
|
|
@@ -83,6 +96,124 @@ function exportKind(checker, symbol) {
|
|
|
83
96
|
return resolved.flags & ts.SymbolFlags.Value ? "value" : "type";
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
// typeToString alone often prints an opaque alias (`ForwardRefExoticComponent<PropsType$s>`).
|
|
100
|
+
// The first call signature works for plain components and forwardRef alike; its
|
|
101
|
+
// first parameter is the props object.
|
|
102
|
+
function ownProps(checker, type) {
|
|
103
|
+
const [signature] = type.getCallSignatures?.() ?? [];
|
|
104
|
+
const param = signature?.getParameters?.()[0];
|
|
105
|
+
if (!param) return [];
|
|
106
|
+
const declaration = param.valueDeclaration ?? param.declarations?.[0];
|
|
107
|
+
if (!declaration) return [];
|
|
108
|
+
const propsType = checker.getTypeOfSymbolAtLocation(param, declaration);
|
|
109
|
+
|
|
110
|
+
return checker
|
|
111
|
+
.getPropertiesOfType(propsType)
|
|
112
|
+
// Without this, ~220 inherited React DOM attributes bury the component's own API.
|
|
113
|
+
.filter((prop) =>
|
|
114
|
+
(prop.declarations ?? []).some(
|
|
115
|
+
(d) => !d.getSourceFile().fileName.includes("node_modules"),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
.map((prop) => {
|
|
119
|
+
const d = prop.valueDeclaration ?? prop.declarations[0];
|
|
120
|
+
const optional = prop.flags & ts.SymbolFlags.Optional ? "?" : "";
|
|
121
|
+
return `${prop.getName()}${optional}: ${checker.typeToString(
|
|
122
|
+
checker.getTypeOfSymbolAtLocation(prop, d),
|
|
123
|
+
)}`;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Kept out of the tool handler so every export can be precomputed and cached.
|
|
128
|
+
function propsText(checker, sourceFile, name, symbol) {
|
|
129
|
+
const resolved = resolveExport(checker, symbol);
|
|
130
|
+
const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
|
|
131
|
+
const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
|
|
132
|
+
const signature = checker.typeToString(
|
|
133
|
+
type,
|
|
134
|
+
undefined,
|
|
135
|
+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const props = ownProps(checker, type);
|
|
139
|
+
if (props.length) {
|
|
140
|
+
// Keep the signature line: RBSlider's value/min/max come from Radix and the
|
|
141
|
+
// own-props filter drops them.
|
|
142
|
+
return `# ${name} — own props\n${props.sort().join("\n")}\n\nfull type: ${signature}`;
|
|
143
|
+
}
|
|
144
|
+
// No own props means either a Radix wrapper or unresolved React types. The two
|
|
145
|
+
// look identical, so name the second case explicitly.
|
|
146
|
+
if (/<any>/.test(signature)) {
|
|
147
|
+
return (
|
|
148
|
+
`# ${name} — prop types could not be resolved\n${signature}\n\n` +
|
|
149
|
+
"React's types didn't resolve, so this collapsed to `any`. Install `@types/react` " +
|
|
150
|
+
"in this project (`npm i -D @types/react`) and retry for the real props."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return (
|
|
154
|
+
`# ${name} — full type (no plugin-ui-specific props; it forwards a third-party ` +
|
|
155
|
+
`component's props)\n${signature}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Rendering every export up front is what makes the result cacheable: once the
|
|
160
|
+
// text exists no TypeChecker is needed.
|
|
161
|
+
function buildUiIndex(dtsPath) {
|
|
162
|
+
const { checker, sourceFile, exports } = loadReactTypes(dtsPath);
|
|
163
|
+
const props = {};
|
|
164
|
+
for (const symbol of exports) {
|
|
165
|
+
props[symbol.getName()] = propsText(checker, sourceFile, symbol.getName(), symbol);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
components: exports
|
|
169
|
+
.map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
|
|
170
|
+
.sort()
|
|
171
|
+
.join("\n"),
|
|
172
|
+
props,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ts.createProgram pulls in ~1100 source files (~1.5s) and an MCP server is a fresh
|
|
177
|
+
// process per session. The key covers the .d.ts identity and the kit version.
|
|
178
|
+
const UI_INDEX_CACHE_DIR = join(homedir(), ".cache", "muscat-pk");
|
|
179
|
+
const UI_INDEX_CACHE = join(UI_INDEX_CACHE_DIR, "ui-index.json");
|
|
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
|
+
const key = `${dtsPath}|${mtimeMs}|${size}|${kitVersion()}`;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const cached = JSON.parse(readFileSync(UI_INDEX_CACHE, "utf8"));
|
|
191
|
+
// A half-written file still parses as JSON, so check the shape too.
|
|
192
|
+
if (
|
|
193
|
+
cached.key === key &&
|
|
194
|
+
typeof cached.components === "string" &&
|
|
195
|
+
cached.props &&
|
|
196
|
+
typeof cached.props === "object"
|
|
197
|
+
) {
|
|
198
|
+
return (uiIndexCache = cached);
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
/* absent, unreadable or corrupt — rebuild below */
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
uiIndexCache = { key, ...buildUiIndex(dtsPath) };
|
|
205
|
+
try {
|
|
206
|
+
mkdirSync(UI_INDEX_CACHE_DIR, { recursive: true });
|
|
207
|
+
// rename is atomic, so a concurrent server never reads a half-written file.
|
|
208
|
+
const tmp = `${UI_INDEX_CACHE}.${process.pid}.tmp`;
|
|
209
|
+
writeFileSync(tmp, JSON.stringify(uiIndexCache));
|
|
210
|
+
renameSync(tmp, UI_INDEX_CACHE);
|
|
211
|
+
} catch {
|
|
212
|
+
/* read-only fs / sandbox: the cache is an optimisation, never a requirement */
|
|
213
|
+
}
|
|
214
|
+
return uiIndexCache;
|
|
215
|
+
}
|
|
216
|
+
|
|
86
217
|
/* -------------------------------------------------------------------------- */
|
|
87
218
|
/* Storybook story source lookup */
|
|
88
219
|
/* -------------------------------------------------------------------------- */
|
|
@@ -130,10 +261,29 @@ function splitSections(text) {
|
|
|
130
261
|
return sections;
|
|
131
262
|
}
|
|
132
263
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
264
|
+
// Standalone binaries have no templates/ on disk.
|
|
265
|
+
async function readTemplateReadme(rel) {
|
|
266
|
+
const onDisk = join(kitRoot, "cli/templates", rel);
|
|
267
|
+
if (existsSync(onDisk)) return readFileSync(onDisk, "utf8");
|
|
268
|
+
try {
|
|
269
|
+
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
270
|
+
return TEMPLATES[rel] ? Buffer.from(TEMPLATES[rel], "base64").toString("utf8") : "";
|
|
271
|
+
} catch {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function searchReadme(query, lang) {
|
|
277
|
+
const langs = lang ? [lang] : ["python", "cpp"];
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
const sections = [];
|
|
280
|
+
for (const l of langs) {
|
|
281
|
+
for (const section of splitSections(await readTemplateReadme(TEMPLATE_READMES[l]))) {
|
|
282
|
+
if (seen.has(section)) continue; // UI/manifest sections are shared by both READMEs
|
|
283
|
+
seen.add(section);
|
|
284
|
+
sections.push(langs.length > 1 && l === "cpp" ? `[C++ backend]\n${section}` : section);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
137
287
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
138
288
|
|
|
139
289
|
return sections
|
|
@@ -227,9 +377,6 @@ function pythonCmd() {
|
|
|
227
377
|
// backend dependency). Returns a clear hint if it isn't importable.
|
|
228
378
|
function introspectRbSdk(script) {
|
|
229
379
|
const py = pythonCmd();
|
|
230
|
-
if (!py) {
|
|
231
|
-
return { error: "Python not found on PATH. Install Python 3 to use rb-sdk introspection." };
|
|
232
|
-
}
|
|
233
380
|
const probe = `
|
|
234
381
|
import json, importlib
|
|
235
382
|
try:
|
|
@@ -238,10 +385,44 @@ except Exception as e:
|
|
|
238
385
|
print(json.dumps({"__notinstalled__": str(e)})); raise SystemExit(0)
|
|
239
386
|
${script}
|
|
240
387
|
`;
|
|
388
|
+
if (!py) {
|
|
389
|
+
return introspectInDocker(probe) ?? { error: "Python not found on PATH. Install Python 3 to use rb-sdk introspection." };
|
|
390
|
+
}
|
|
241
391
|
const r = spawnSync(py, ["-c", probe], { encoding: "utf8", maxBuffer: 8 << 20 });
|
|
242
392
|
if (r.status !== 0) {
|
|
243
393
|
return { error: `python introspection failed: ${(r.stderr || "").trim().slice(0, 500)}` };
|
|
244
394
|
}
|
|
395
|
+
const local = parseProbe(r);
|
|
396
|
+
return local.error && local.notInstalled ? introspectInDocker(probe) ?? local : local;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// No macOS wheels: import the plugin's pinned SDK inside the python dev image (cached).
|
|
400
|
+
function introspectInDocker(probe) {
|
|
401
|
+
if (spawnSync("docker", ["image", "inspect", PYTHON_DEV_IMAGE], { encoding: "utf8" }).status !== 0) return null;
|
|
402
|
+
let ver = "";
|
|
403
|
+
try {
|
|
404
|
+
const req = readFileSync(join(findPluginRoot(), "requirements.txt"), "utf8");
|
|
405
|
+
ver = req.match(/^rainbow-rb-sdk==(\S+)/m)?.[1] ?? "";
|
|
406
|
+
} catch {
|
|
407
|
+
/* not a python plugin — latest */
|
|
408
|
+
}
|
|
409
|
+
// ponytail: "latest" cache is never refreshed; delete ~/.cache/... rb-sdk-py/latest to update.
|
|
410
|
+
const cache = join(UI_INDEX_CACHE_DIR, "rb-sdk-py", ver || "latest");
|
|
411
|
+
mkdirSync(cache, { recursive: true });
|
|
412
|
+
const pkg = ver ? `rainbow-rb-sdk==${ver}` : "rainbow-rb-sdk";
|
|
413
|
+
const script =
|
|
414
|
+
`[ -f /c/.ok ] || { pip3 install -q --break-system-packages --target /c '${pkg}' >&2 && touch /c/.ok; } && ` +
|
|
415
|
+
`PYTHONPATH=/c python3 -c "$PROBE"`;
|
|
416
|
+
const r = spawnSync("docker", ["run", "--rm", "-v", `${cache}:/c`, "-e", "PROBE", PYTHON_DEV_IMAGE, "bash", "-c", script], {
|
|
417
|
+
encoding: "utf8",
|
|
418
|
+
maxBuffer: 8 << 20,
|
|
419
|
+
env: { ...process.env, PROBE: probe },
|
|
420
|
+
});
|
|
421
|
+
if (r.status !== 0) return { error: `python introspection (docker) failed: ${(r.stderr || "").trim().slice(-500)}` };
|
|
422
|
+
return parseProbe(r);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseProbe(r) {
|
|
245
426
|
// Importing rainbow.rb_sdk has side effects that print to stdout (e.g. the
|
|
246
427
|
// Zenoh client logs its connection endpoints), ahead of our own final
|
|
247
428
|
// print(json.dumps(...)) — so parse the last line, not the whole output.
|
|
@@ -254,6 +435,7 @@ ${script}
|
|
|
254
435
|
}
|
|
255
436
|
if (data && data.__notinstalled__) {
|
|
256
437
|
return {
|
|
438
|
+
notInstalled: true,
|
|
257
439
|
error:
|
|
258
440
|
"rainbow-rb-sdk is not installed in this environment. Install it with `pip install rainbow-rb-sdk` " +
|
|
259
441
|
"(it's already in a plugin's requirements.txt), then retry.",
|
|
@@ -262,6 +444,114 @@ ${script}
|
|
|
262
444
|
return { data };
|
|
263
445
|
}
|
|
264
446
|
|
|
447
|
+
/* -------------------------------------------------------------------------- */
|
|
448
|
+
/* rainbow-rb-sdk-dev (C++) headers — copied out of the cpp dev image */
|
|
449
|
+
/* -------------------------------------------------------------------------- */
|
|
450
|
+
|
|
451
|
+
const CPP_INCLUDE_CACHE = join(UI_INDEX_CACHE_DIR, "rb-sdk-include");
|
|
452
|
+
// Headers we don't want to surface: third-party vendored code, pybind glue,
|
|
453
|
+
// flatbuffers-generated schemas, and the industrial comm stacks.
|
|
454
|
+
const CPP_SKIP = /^(vendor|binding|rb_comm_source)\/|_generated\.h$/;
|
|
455
|
+
|
|
456
|
+
// The apt package is only installed inside the muscat-pk-cpp-dev image, so
|
|
457
|
+
// copy /usr/include/rainbow/rb_sdk out once per image build (cache key =
|
|
458
|
+
// image id) with `docker cp` — no host tar needed.
|
|
459
|
+
function cppIncludeDir() {
|
|
460
|
+
const insp = spawnSync("docker", ["image", "inspect", "-f", "{{.Id}}", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
461
|
+
if (insp.error) return { error: "Docker not found — the C++ SDK headers live in the muscat-pk cpp dev image." };
|
|
462
|
+
if (insp.status !== 0) {
|
|
463
|
+
return {
|
|
464
|
+
error:
|
|
465
|
+
`Docker image ${CPP_DEV_IMAGE} not built yet. Run \`muscat-pk dev:back\` (or \`muscat-pk build\`) once ` +
|
|
466
|
+
"in a cpp / rclcpp plugin to build it, then retry.",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const dir = join(CPP_INCLUDE_CACHE, insp.stdout.trim().replace(/^sha256:/, "").slice(0, 12));
|
|
470
|
+
const out = join(dir, "rb_sdk");
|
|
471
|
+
if (existsSync(out)) return { dir: out };
|
|
472
|
+
mkdirSync(dir, { recursive: true });
|
|
473
|
+
const ctr = spawnSync("docker", ["create", CPP_DEV_IMAGE], { encoding: "utf8" });
|
|
474
|
+
if (ctr.status !== 0) return { error: `docker create failed: ${(ctr.stderr || "").trim().slice(0, 300)}` };
|
|
475
|
+
const id = ctr.stdout.trim();
|
|
476
|
+
const cp = spawnSync("docker", ["cp", `${id}:/usr/include/rainbow/rb_sdk`, dir], { encoding: "utf8" });
|
|
477
|
+
spawnSync("docker", ["rm", "-f", id], { encoding: "utf8" });
|
|
478
|
+
if (cp.status !== 0 || !existsSync(out)) {
|
|
479
|
+
return { error: `docker cp failed: ${(cp.stderr || "").trim().slice(0, 300)}` };
|
|
480
|
+
}
|
|
481
|
+
return { dir: out };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function cppHeaders(root) {
|
|
485
|
+
const files = [];
|
|
486
|
+
const walk = (d, rel) => {
|
|
487
|
+
for (const name of readdirSync(d)) {
|
|
488
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
489
|
+
if (CPP_SKIP.test(r)) continue;
|
|
490
|
+
const full = join(d, name);
|
|
491
|
+
if (statSync(full).isDirectory()) walk(full, r);
|
|
492
|
+
else if (/\.hpp$/.test(name)) files.push(r);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
walk(root, "");
|
|
496
|
+
return files.sort();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// "class Foo {" / "class Foo : public Bar {" — not forward declarations.
|
|
500
|
+
const CLASS_RE = /^\s*(?:class|struct)\s+(?:[A-Z][A-Z0-9_]*_API\s+)?(\w+)\b(?![^\n]*;\s*$)/; // skip export macros (RB_SDK_API)
|
|
501
|
+
|
|
502
|
+
function cppClassesIn(text) {
|
|
503
|
+
return [...new Set(text.split("\n").map((l) => l.match(CLASS_RE)?.[1]).filter(Boolean))];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Return the class body (brace-matched) plus the file's leading comment block,
|
|
507
|
+
// which in these headers usually carries a full usage example.
|
|
508
|
+
function cppExtract(text, className, method) {
|
|
509
|
+
const lines = text.split("\n");
|
|
510
|
+
const start = lines.findIndex((l) => CLASS_RE.test(l) && l.match(CLASS_RE)[1] === className);
|
|
511
|
+
if (start < 0) return null;
|
|
512
|
+
let depth = 0, end = start, seen = false;
|
|
513
|
+
for (let i = start; i < lines.length; i++) {
|
|
514
|
+
for (const ch of lines[i]) {
|
|
515
|
+
if (ch === "{") { depth++; seen = true; }
|
|
516
|
+
else if (ch === "}") depth--;
|
|
517
|
+
}
|
|
518
|
+
if (seen && depth === 0) { end = i; break; }
|
|
519
|
+
}
|
|
520
|
+
const body = lines.slice(start, end + 1);
|
|
521
|
+
if (method) {
|
|
522
|
+
// Declarations sit at class-member indent (<= 2 spaces) and the name is
|
|
523
|
+
// not preceded by `.`/`->`/`::` — that skips call sites inside bodies.
|
|
524
|
+
const re = new RegExp(`^ {0,2}[^(]*(?<![\\w.>:])${method.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\(`);
|
|
525
|
+
const hits = [];
|
|
526
|
+
for (let i = 0; i < body.length; i++) {
|
|
527
|
+
if (!re.test(body[i]) || /^\s*\/\//.test(body[i])) continue;
|
|
528
|
+
let a = i;
|
|
529
|
+
while (a > 0 && /^\s*\/\//.test(body[a - 1])) a--;
|
|
530
|
+
let b = i;
|
|
531
|
+
while (b < body.length - 1 && !/[{;]/.test(body[b])) b++;
|
|
532
|
+
hits.push(body.slice(a, b + 1).join("\n"));
|
|
533
|
+
}
|
|
534
|
+
return hits.length ? hits.join("\n\n") : null;
|
|
535
|
+
}
|
|
536
|
+
const head = [];
|
|
537
|
+
for (const l of lines) {
|
|
538
|
+
if (/^\s*\/\//.test(l)) head.push(l);
|
|
539
|
+
else if (l.trim() === "" || /^#pragma/.test(l)) continue;
|
|
540
|
+
else break;
|
|
541
|
+
}
|
|
542
|
+
return (head.length > 3 ? head.join("\n") + "\n\n" : "") + body.join("\n");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function cppFind(className) {
|
|
546
|
+
const { dir, error } = cppIncludeDir();
|
|
547
|
+
if (error) return { error };
|
|
548
|
+
for (const rel of cppHeaders(dir)) {
|
|
549
|
+
const text = readFileSync(join(dir, rel), "utf8");
|
|
550
|
+
if (cppClassesIn(text).includes(className)) return { rel, text };
|
|
551
|
+
}
|
|
552
|
+
return { error: `No class "${className}" in the C++ SDK headers. Call list_rb_sdk with lang "cpp" for valid names.` };
|
|
553
|
+
}
|
|
554
|
+
|
|
265
555
|
/* -------------------------------------------------------------------------- */
|
|
266
556
|
/* client registration — `muscat-pk mcp --install <client>` */
|
|
267
557
|
/* -------------------------------------------------------------------------- */
|
|
@@ -313,11 +603,8 @@ export async function runMcp() {
|
|
|
313
603
|
inputSchema: {},
|
|
314
604
|
},
|
|
315
605
|
async () => {
|
|
316
|
-
const {
|
|
317
|
-
|
|
318
|
-
.map((sym) => `${sym.getName()} (${exportKind(checker, sym)})`)
|
|
319
|
-
.sort();
|
|
320
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
606
|
+
const { components } = await loadUiIndex();
|
|
607
|
+
return { content: [{ type: "text", text: components }] };
|
|
321
608
|
},
|
|
322
609
|
);
|
|
323
610
|
|
|
@@ -331,9 +618,8 @@ export async function runMcp() {
|
|
|
331
618
|
inputSchema: { name: z.string().describe('Exported name, e.g. "RBSlider"') },
|
|
332
619
|
},
|
|
333
620
|
async ({ name }) => {
|
|
334
|
-
const {
|
|
335
|
-
|
|
336
|
-
if (!symbol) {
|
|
621
|
+
const { props } = await loadUiIndex();
|
|
622
|
+
if (!Object.hasOwn(props, name)) {
|
|
337
623
|
return {
|
|
338
624
|
isError: true,
|
|
339
625
|
content: [
|
|
@@ -344,15 +630,7 @@ export async function runMcp() {
|
|
|
344
630
|
],
|
|
345
631
|
};
|
|
346
632
|
}
|
|
347
|
-
|
|
348
|
-
const declaration = resolved.valueDeclaration ?? resolved.declarations?.[0] ?? sourceFile;
|
|
349
|
-
const type = checker.getTypeOfSymbolAtLocation(resolved, declaration);
|
|
350
|
-
const text = checker.typeToString(
|
|
351
|
-
type,
|
|
352
|
-
undefined,
|
|
353
|
-
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.MultilineObjectLiterals,
|
|
354
|
-
);
|
|
355
|
-
return { content: [{ type: "text", text }] };
|
|
633
|
+
return { content: [{ type: "text", text: props[name] }] };
|
|
356
634
|
},
|
|
357
635
|
);
|
|
358
636
|
|
|
@@ -399,20 +677,29 @@ export async function runMcp() {
|
|
|
399
677
|
title: "Search the Muscat plugin-author docs",
|
|
400
678
|
description:
|
|
401
679
|
"Search the Muscat plugin-author README for how to use useRb(), rb.pub/sub/action/serve, " +
|
|
680
|
+
"plugin.router (external endpoints: router.on/subscribe/emit exposed over REST/Socket.IO/TCP/" +
|
|
681
|
+
"MQTT/WebRTC at common/plugin/{plugin_id}/{path}, PluginRouteError, meta.transport/principal), " +
|
|
402
682
|
"rb.toast/alert/confirm/prompt, useRbFormData/useRbSave (save/restore form data through the " +
|
|
403
683
|
"host), manifest.json (including the \"variables\" declaration for the pyfm variable picker — " +
|
|
404
684
|
"names require the RBP_ prefix), program variables & executor state (ctx.get_variable/" +
|
|
405
|
-
"set_variable/executor_state/check_stop),
|
|
406
|
-
"
|
|
685
|
+
"set_variable/executor_state/check_stop), video streaming (MediaMTX: backend publishes RTSP to " +
|
|
686
|
+
"127.0.0.1:8554, UI plays WebRTC from :8889), ROS 2 topics (plugin namespace, rb.rosPeers to allow " +
|
|
687
|
+
"an external PC), contribution points, and backend runtimes " +
|
|
688
|
+
"(Python/C++/ROS2). Covers BOTH backends: the Python README (py/main.py, rainbow.rb_sdk) and the " +
|
|
689
|
+
"C++ README (cpp/main.cpp or an rclcpp node: rb_plugin::RBPluginCall serve/subscribe/publish/" +
|
|
690
|
+
"router(), rb_plugin::PluginProgramContext, RBManipulateSDK, CMake rb_sdk::rb_sdk). Pass lang " +
|
|
691
|
+
"\"python\" or \"cpp\" to match the plugin's manifest runtime (cpp or ros2+rclcpp → \"cpp\"); " +
|
|
692
|
+
"omit it to search both. IMPORTANT: " +
|
|
407
693
|
"the backend pattern depends on the contribution point — search 'which backend pattern' before " +
|
|
408
|
-
"writing
|
|
694
|
+
"writing the backend. Only a program.panel command uses PluginProgramContext; a toolbar/move.panel " +
|
|
409
695
|
"plugin controls the robot with the SDK (RBManipulateSDK/RBAmrSDK) directly.",
|
|
410
696
|
inputSchema: {
|
|
411
697
|
query: z.string().describe('Keywords, e.g. "save form data" or "rb.action"'),
|
|
698
|
+
lang: z.enum(["python", "cpp"]).optional().describe("Backend language; omit to search both"),
|
|
412
699
|
},
|
|
413
700
|
},
|
|
414
|
-
async ({ query }) => {
|
|
415
|
-
const sections = searchReadme(query);
|
|
701
|
+
async ({ query, lang }) => {
|
|
702
|
+
const sections = await searchReadme(query, lang);
|
|
416
703
|
if (sections.length === 0) {
|
|
417
704
|
return { content: [{ type: "text", text: `No matching section found for "${query}".` }] };
|
|
418
705
|
}
|
|
@@ -446,7 +733,10 @@ export async function runMcp() {
|
|
|
446
733
|
const other = { shadow: [], radius: [], spacing: [] };
|
|
447
734
|
for (const t of vars) {
|
|
448
735
|
const m = t.group?.match(/^color-(bg|fg|border|fill)$/);
|
|
449
|
-
|
|
736
|
+
// Class name only: the value is just an indirection to another CSS var
|
|
737
|
+
// (var(--color-scale-zinc-200)), and each class is declared twice (light/dark)
|
|
738
|
+
// with different values — dropping the value lets the Set dedup collapse them.
|
|
739
|
+
if (m && t.class) colorGroups[m[1]].push(t.class);
|
|
450
740
|
else if (t.name.startsWith("shadow-")) other.shadow.push(`shadow-${t.name.slice(7)}`);
|
|
451
741
|
else if (t.name.startsWith("radius-")) other.radius.push(`rounded-${t.name.slice(7)}`);
|
|
452
742
|
else if (t.name.startsWith("spacing-")) other.spacing.push(t.name);
|
|
@@ -509,14 +799,29 @@ export async function runMcp() {
|
|
|
509
799
|
server.registerTool(
|
|
510
800
|
"list_rb_sdk",
|
|
511
801
|
{
|
|
512
|
-
title: "List rainbow-rb-sdk
|
|
802
|
+
title: "List rainbow-rb-sdk classes and functions (Python or C++)",
|
|
513
803
|
description:
|
|
514
|
-
"List the public classes and functions of the
|
|
515
|
-
"
|
|
516
|
-
"before writing py/main.py
|
|
517
|
-
|
|
804
|
+
"List the public classes and functions of the plugin BACKEND SDK. lang \"python\" (default): the " +
|
|
805
|
+
"installed rainbow.rb_sdk package (RBPluginCall, RBManipulateSDK, RBAmrSDK, PluginProgramContext, " +
|
|
806
|
+
"...) — call before writing py/main.py; requires rainbow-rb-sdk to be pip-installed. lang \"cpp\": " +
|
|
807
|
+
"the C++ SDK headers (apt rainbow-rb-sdk-dev: RBManipulateSDK, rb_plugin::PluginProgramContext, " +
|
|
808
|
+
"...) grouped by header path — call before writing cpp/main.cpp or an rclcpp node; requires the " +
|
|
809
|
+
"muscat-pk-cpp-dev Docker image (built by `muscat-pk dev:back`). RBPluginCall (UI pub/sub/serve, " +
|
|
810
|
+
"router) exists in both: Python rainbow.rb_sdk.RBPluginCall, C++ rb_plugin::RBPluginCall " +
|
|
811
|
+
"(plugin/rb_plugin.hpp).",
|
|
812
|
+
inputSchema: { lang: z.enum(["python", "cpp"]).optional().describe('Default "python"') },
|
|
518
813
|
},
|
|
519
|
-
async () => {
|
|
814
|
+
async ({ lang }) => {
|
|
815
|
+
if (lang === "cpp") {
|
|
816
|
+
const { dir, error } = cppIncludeDir();
|
|
817
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
818
|
+
const out = [];
|
|
819
|
+
for (const rel of cppHeaders(dir)) {
|
|
820
|
+
const classes = cppClassesIn(readFileSync(join(dir, rel), "utf8"));
|
|
821
|
+
if (classes.length) out.push(`${rel}: ${classes.join(", ")}`);
|
|
822
|
+
}
|
|
823
|
+
return { content: [{ type: "text", text: `#include <rainbow/rb_sdk/...>\n\n${out.join("\n")}` }] };
|
|
824
|
+
}
|
|
520
825
|
const { data, error } = introspectRbSdk(`
|
|
521
826
|
import inspect, json
|
|
522
827
|
mod = importlib.import_module("rainbow.rb_sdk")
|
|
@@ -536,19 +841,22 @@ print(json.dumps(out))
|
|
|
536
841
|
server.registerTool(
|
|
537
842
|
"get_rb_sdk_api",
|
|
538
843
|
{
|
|
539
|
-
title: "Get a rainbow-rb-sdk class/function signature + docs",
|
|
844
|
+
title: "Get a rainbow-rb-sdk class/function signature + docs (Python or C++)",
|
|
540
845
|
description:
|
|
541
846
|
"Get the signature(s) and docstring for a rainbow.rb_sdk class or function (e.g. " +
|
|
542
847
|
"'PluginProgramContext', 'RBManipulateSDK'). For a class, lists its public methods with " +
|
|
543
848
|
"signatures. To drill into ONE method of a class (e.g. RBManipulateSDK.call_move_j), pass " +
|
|
544
|
-
"`method` alongside `name` — do not put both in `name`.
|
|
545
|
-
"pip-installed."
|
|
849
|
+
"`method` alongside `name` — do not put both in `name`. lang \"python\" (default) introspects the " +
|
|
850
|
+
"pip-installed package. lang \"cpp\" returns the class declaration from the C++ SDK header " +
|
|
851
|
+
"(rainbow-rb-sdk-dev) with its header comment — usually a full usage example — and the " +
|
|
852
|
+
"#include path; with `method`, just that method's declaration and comment.",
|
|
546
853
|
inputSchema: {
|
|
547
854
|
name: z.string().describe('Class or function name, e.g. "RBManipulateSDK"'),
|
|
548
855
|
method: z.string().optional().describe('Optional: a specific method on that class, e.g. "call_move_j"'),
|
|
856
|
+
lang: z.enum(["python", "cpp"]).optional().describe('Default "python"'),
|
|
549
857
|
},
|
|
550
858
|
},
|
|
551
|
-
async ({ name, method }) => {
|
|
859
|
+
async ({ name, method, lang }) => {
|
|
552
860
|
// Tolerate a combined "ClassName method_name" in `name` (a common agent
|
|
553
861
|
// mistake) by splitting it ourselves instead of failing validation.
|
|
554
862
|
let className = name;
|
|
@@ -558,6 +866,42 @@ print(json.dumps(out))
|
|
|
558
866
|
if (parts.length === 2) [className, methodName] = parts;
|
|
559
867
|
}
|
|
560
868
|
|
|
869
|
+
if (lang === "cpp") {
|
|
870
|
+
const cls = className.replace(/^.*::/, "");
|
|
871
|
+
let { rel, text, error } = cppFind(cls);
|
|
872
|
+
if (error) return { isError: true, content: [{ type: "text", text: error }] };
|
|
873
|
+
let body = cppExtract(text, cls, methodName);
|
|
874
|
+
let owner = cls;
|
|
875
|
+
if (!body && methodName) {
|
|
876
|
+
// Like the Python path: most calls live on sub-SDK members
|
|
877
|
+
// (RBManipulateMoveSDK move; -> sdk.move.call_move_j) or on the base
|
|
878
|
+
// class — look there before giving up.
|
|
879
|
+
const decl = cppExtract(text, cls);
|
|
880
|
+
const cands = [
|
|
881
|
+
...[...decl.matchAll(/^\s*(\w+SDK)\s+(\w+);/gm)].map((m) => [m[1], `${cls}.${m[2]}`]),
|
|
882
|
+
...[...decl.matchAll(/:\s*public\s+(\w+)/g)].map((m) => [m[1], `${cls} (base ${m[1]})`]),
|
|
883
|
+
];
|
|
884
|
+
for (const [c, label] of cands) {
|
|
885
|
+
const f = cppFind(c);
|
|
886
|
+
if (f.error) continue;
|
|
887
|
+
const b = cppExtract(f.text, c, methodName);
|
|
888
|
+
if (b) { ({ rel, text } = f); body = b; owner = label; break; }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (!body) {
|
|
892
|
+
return {
|
|
893
|
+
isError: true,
|
|
894
|
+
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.` }],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const ns = text.match(/^namespace\s+(\w+)\s*\{/m)?.[1];
|
|
898
|
+
const header =
|
|
899
|
+
`// #include <rainbow/rb_sdk/${rel}>${ns ? ` (namespace ${ns})` : ""}\n` +
|
|
900
|
+
`// link: find_package(rb_sdk) + rb_sdk::rb_sdk\n` +
|
|
901
|
+
(methodName ? `// ${owner}.${methodName}\n` : "") + "\n";
|
|
902
|
+
return { content: [{ type: "text", text: header + body.slice(0, 60_000) }] };
|
|
903
|
+
}
|
|
904
|
+
|
|
561
905
|
const { data, error } = introspectRbSdk(`
|
|
562
906
|
import inspect, json
|
|
563
907
|
mod = importlib.import_module("rainbow.rb_sdk")
|
package/cli/src/new.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesD
|
|
|
17
17
|
// Like cpSync(src, dst, {recursive:true}) but reads from the embedded snapshot
|
|
18
18
|
// when the templates dir doesn't exist on disk (compiled binary mode).
|
|
19
19
|
// ponytail: import() is module-cached by the runtime — no manual cache needed.
|
|
20
|
-
async function copyTemplate(srcDir, dst) {
|
|
20
|
+
export async function copyTemplate(srcDir, dst) {
|
|
21
21
|
if (existsSync(templatesDir)) { cpSync(srcDir, dst, { recursive: true }); return; }
|
|
22
22
|
const { TEMPLATES } = await import("./templates-embedded.mjs");
|
|
23
23
|
const prefix = srcDir.slice(templatesDir.length + 1).replace(/\\/g, "/") + "/";
|
|
@@ -87,6 +87,7 @@ export function runtimeForChoice(choice, slug) {
|
|
|
87
87
|
|
|
88
88
|
export function applyTokens(dir, tokens) {
|
|
89
89
|
for (const entry of readdirSync(dir)) {
|
|
90
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".venv") continue;
|
|
90
91
|
const full = resolve(dir, entry);
|
|
91
92
|
if (statSync(full).isDirectory()) {
|
|
92
93
|
applyTokens(full, tokens);
|
|
@@ -106,6 +107,12 @@ export function applyTokens(dir, tokens) {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Derive naming tokens from a plugin directory name.
|
|
110
|
+
// One id part (see PLUGIN_ID_RE): lowercase, single "-", starts with a letter.
|
|
111
|
+
function idPart(name) {
|
|
112
|
+
const part = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
113
|
+
return /^[a-z]/.test(part) ? part : part ? `plugin-${part}` : "plugin";
|
|
114
|
+
}
|
|
115
|
+
|
|
109
116
|
export function deriveNames(target) {
|
|
110
117
|
const base = basename(target);
|
|
111
118
|
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
@@ -115,7 +122,7 @@ export function deriveNames(target) {
|
|
|
115
122
|
slug,
|
|
116
123
|
pluginName,
|
|
117
124
|
pluginClass: pluginName.replace(/\s+/g, ""),
|
|
118
|
-
pluginId: `com.example.${base
|
|
125
|
+
pluginId: `com.example.${idPart(base)}`,
|
|
119
126
|
};
|
|
120
127
|
}
|
|
121
128
|
|
|
@@ -226,6 +233,9 @@ export async function runNew(args) {
|
|
|
226
233
|
rmSync(resolve(target, "py"), { recursive: true, force: true });
|
|
227
234
|
rmSync(resolve(target, "requirements.txt"), { force: true });
|
|
228
235
|
}
|
|
236
|
+
if (runtimeChoice.type === "cpp" || runtimeChoice.rosClient === "rclcpp") {
|
|
237
|
+
await copyTemplate(resolve(templatesDir, "readme-cpp"), target);
|
|
238
|
+
}
|
|
229
239
|
|
|
230
240
|
applyTokens(target, {
|
|
231
241
|
__PLUGIN_ID__: pluginId,
|
|
@@ -250,7 +260,7 @@ export async function runNew(args) {
|
|
|
250
260
|
// gitignore (templates can't ship a literal .gitignore via npm reliably)
|
|
251
261
|
writeFileSync(
|
|
252
262
|
resolve(target, ".gitignore"),
|
|
253
|
-
["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "cpp/build", ""].join("\n"),
|
|
263
|
+
["node_modules", "dist", ".muscat-dev", ".venv", "*.zip", "ros2_ws/install", "ros2_ws/build*", "ros2_ws/log", "cpp/build*", ""].join("\n"),
|
|
254
264
|
);
|
|
255
265
|
|
|
256
266
|
if (!opts["no-install"]) {
|