@rainbow-robotics/muscat-plugin-kit 0.1.2-dev.51 → 0.1.2-dev.52

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/src/dev.mjs CHANGED
@@ -1,11 +1,105 @@
1
1
  // muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { release } from "node:os";
5
+ import { resolve } from "node:path";
2
6
  import { runDevUi } from "./devUi.mjs";
3
7
  import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
4
8
  import { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
5
9
  import { assertManifestPorts } from "./manifest.mjs";
6
10
  import { formatPortRanges } from "./ports.mjs";
7
- import { ensureDocker, findPluginRoot, log, prompt, promptRobotConnection } from "./util.mjs";
8
- import { ensureRemoteZenohEnv, isRemoteRobot } from "./zenohDev.mjs";
11
+ import {
12
+ ensureDocker,
13
+ ensureIdeVenv,
14
+ findPluginRoot,
15
+ log,
16
+ prompt,
17
+ promptRobotConnection,
18
+ venvPython,
19
+ } from "./util.mjs";
20
+ import { detectRouteAddress, ensureRemoteZenohEnv, getFreePort, isRemoteRobot } from "./zenohDev.mjs";
21
+
22
+ const DEFAULT_HOST_AGENT_PORT = 18080;
23
+ const isWsl = () => process.platform === "linux" && /microsoft|wsl/i.test(release());
24
+ const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
25
+
26
+ function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
27
+ const deadline = Date.now() + timeoutMs;
28
+ const url = `http://127.0.0.1:${port}/health`;
29
+ return new Promise((resolve, reject) => {
30
+ const probe = async () => {
31
+ try {
32
+ const res = await fetch(url);
33
+ if (res.ok) return resolve(true);
34
+ } catch {
35
+ // keep polling
36
+ }
37
+ if (Date.now() >= deadline) {
38
+ reject(new Error(`host SDK agent did not become ready at ${url}`));
39
+ return;
40
+ }
41
+ setTimeout(probe, 300);
42
+ };
43
+ probe();
44
+ });
45
+ }
46
+
47
+ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
48
+ if (process.env.MUSCAT_HOST_AGENT_URL) {
49
+ log.step(`host SDK agent → ${process.env.MUSCAT_HOST_AGENT_URL}`);
50
+ return null;
51
+ }
52
+ if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
53
+ if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
54
+
55
+ const agentPath = resolve(root, "py", "host_agent.py");
56
+ if (!existsSync(agentPath)) return null;
57
+
58
+ const wsl = isWsl();
59
+ if (wsl) await ensureRemoteZenohEnv(robotIp, { log });
60
+
61
+ const bindHost = wsl ? "0.0.0.0" : "127.0.0.1";
62
+ const containerHost = wsl
63
+ ? process.env.MUSCAT_HOST_AGENT_CONTAINER_HOST || await detectRouteAddress(robotIp)
64
+ : "host.docker.internal";
65
+
66
+ if (!containerHost) {
67
+ log.warn("host SDK agent skipped: could not determine container-reachable host");
68
+ return null;
69
+ }
70
+
71
+ log.step("starting host SDK agent (Docker Desktop workaround)...");
72
+ if (!ensureIdeVenv(root)) {
73
+ log.warn("host SDK agent skipped: could not prepare .venv");
74
+ return null;
75
+ }
76
+
77
+ const port = await getFreePort(Number(process.env.MUSCAT_HOST_AGENT_PORT || DEFAULT_HOST_AGENT_PORT));
78
+ const py = venvPython(resolve(root, ".venv"));
79
+ const child = spawn(py, [agentPath], {
80
+ cwd: root,
81
+ stdio: ["ignore", "inherit", "inherit"],
82
+ env: {
83
+ ...process.env,
84
+ MUSCAT_ROBOT_HOST: robotIp,
85
+ MUSCAT_ROBOT_TOKEN: robotToken,
86
+ MUSCAT_HOST_AGENT_BIND: bindHost,
87
+ MUSCAT_HOST_AGENT_PORT: String(port),
88
+ },
89
+ });
90
+
91
+ try {
92
+ await waitForHostAgent(port);
93
+ } catch (e) {
94
+ child.kill();
95
+ throw e;
96
+ }
97
+
98
+ const containerUrl = `http://${containerHost}:${port}`;
99
+ process.env.MUSCAT_HOST_AGENT_URL = containerUrl;
100
+ log.step(`host SDK agent → ${containerUrl}`);
101
+ return child;
102
+ }
9
103
 
10
104
  export async function runDev(args = []) {
11
105
  const root = findPluginRoot();
@@ -20,13 +114,19 @@ export async function runDev(args = []) {
20
114
  if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
21
115
 
22
116
  const remoteRobot = isRemoteRobot(robotIp);
117
+ const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
118
+ const hostAgentUrl = process.env.MUSCAT_HOST_AGENT_URL || "";
23
119
  // Compute the Zenoh advertise host/port up front — startBridge() needs the
24
120
  // port to publish it on the bridge container so the robot can reach back.
25
- await ensureRemoteZenohEnv(robotIp, { log });
121
+ if (hostAgentUrl && !hostAgent) {
122
+ log.step(`host SDK agent → ${hostAgentUrl}`);
123
+ } else if (!hostAgentUrl) {
124
+ await ensureRemoteZenohEnv(robotIp, { log });
125
+ }
26
126
 
27
127
  log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
28
128
  const bridgeStarted = await startBridge({
29
- zenohPort: remoteRobot ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
129
+ zenohPort: remoteRobot && !hostAgentUrl ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
30
130
  });
31
131
 
32
132
  if (!bridgeStarted) {
@@ -43,6 +143,7 @@ export async function runDev(args = []) {
43
143
  process.env.MUSCAT_PK_MANAGED_DEV = "1";
44
144
 
45
145
  const cleanup = () => {
146
+ if (hostAgent && hostAgent.exitCode === null) hostAgent.kill();
46
147
  stopDevBackContainers();
47
148
  if (bridgeStarted) stopBridge();
48
149
  };
@@ -87,6 +87,7 @@ async function runPython(root, portRanges) {
87
87
  const robotToken = process.env.MUSCAT_ROBOT_TOKEN ?? "";
88
88
  const zenohAdvertiseHost = process.env.MUSCAT_ZENOH_ADVERTISE_HOST ?? "";
89
89
  const zenohPort = process.env.MUSCAT_ZENOH_PORT ?? "";
90
+ const hostAgentUrl = process.env.MUSCAT_HOST_AGENT_URL ?? "";
90
91
  // Share the bridge container's network namespace so localhost:10000 is the
91
92
  // bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
92
93
  const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
@@ -112,8 +113,9 @@ async function runPython(root, portRanges) {
112
113
  ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
113
114
  ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
114
115
  ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
115
- ...(zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
116
- ...(zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
116
+ ...(!hostAgentUrl && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
117
+ ...(!hostAgentUrl && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
118
+ ...(hostAgentUrl ? ["-e", `MUSCAT_HOST_AGENT_URL=${hostAgentUrl}`] : []),
117
119
  ...portEnvFlags(portRanges),
118
120
  ];
119
121
  const docker = (args, opts = {}) =>
@@ -140,34 +142,90 @@ async function runPython(root, portRanges) {
140
142
  " && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
141
143
 
142
144
  docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
145
+ import json
143
146
  import os
144
- import runpy
147
+ import urllib.request
145
148
 
146
- try:
149
+ def install_host_agent_sdk_patch(host_agent_url):
147
150
  from rainbow.rb_sdk.base import RBBaseSDK
151
+ from rainbow.rb_sdk.manipulate_sdk.manipulate_move import RBManipulateMoveSDK
152
+
153
+ base_url = host_agent_url.rstrip("/")
154
+
155
+ def post_json(path, payload):
156
+ req = urllib.request.Request(
157
+ f"{base_url}{path}",
158
+ data=json.dumps(payload).encode("utf-8"),
159
+ headers={"Content-Type": "application/json"},
160
+ method="POST",
161
+ )
162
+ with urllib.request.urlopen(req, timeout=30) as resp:
163
+ data = json.loads(resp.read().decode("utf-8"))
164
+ if not data.get("ok"):
165
+ raise RuntimeError(data.get("error") or "host SDK agent request failed")
166
+ return data.get("result")
167
+
168
+ async def connect_via_host_agent(self, *args, **kwargs):
169
+ print(f"[muscat-pk] SDK connect routed through host agent: {base_url}", flush=True)
170
+ return "host-agent"
171
+
172
+ async def disconnect_via_host_agent(self, *args, **kwargs):
173
+ return None
174
+
175
+ def call_move_j_via_host_agent(
176
+ self,
177
+ *,
178
+ robot_model,
179
+ target,
180
+ reference_value=None,
181
+ speed=None,
182
+ flow_manager_args=None,
183
+ ):
184
+ result = post_json("/move_j", {
185
+ "robot_model": robot_model,
186
+ "values": target.get("tar_values") if isinstance(target, dict) else None,
187
+ "target": target,
188
+ "reference_value": reference_value,
189
+ "speed": speed,
190
+ })
191
+ if flow_manager_args is not None:
192
+ flow_manager_args.done()
193
+ return result
194
+
195
+ RBBaseSDK.connect = connect_via_host_agent
196
+ RBBaseSDK.disconnect = disconnect_via_host_agent
197
+ RBManipulateMoveSDK.call_move_j = call_move_j_via_host_agent
148
198
 
199
+ try:
149
200
  advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
150
201
  zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
151
-
152
- if advertise_host:
153
- RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
154
-
155
- if zenoh_port:
156
- _orig_connect = RBBaseSDK.connect
157
-
158
- async def _connect_with_dev_port(self, *args, **kwargs):
159
- kwargs.setdefault("zenoh_port", int(zenoh_port))
160
- return await _orig_connect(self, *args, **kwargs)
161
-
162
- RBBaseSDK.connect = _connect_with_dev_port
202
+ host_agent_url = os.getenv("MUSCAT_HOST_AGENT_URL")
203
+
204
+ if host_agent_url:
205
+ print(f"[muscat-pk] host SDK agent mode: {host_agent_url}", flush=True)
206
+ install_host_agent_sdk_patch(host_agent_url)
207
+ else:
208
+ from rainbow.rb_sdk.base import RBBaseSDK
209
+ if advertise_host:
210
+ RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
211
+ if zenoh_port:
212
+ _orig_connect = RBBaseSDK.connect
213
+
214
+ async def _connect_with_dev_port(self, *args, **kwargs):
215
+ kwargs.setdefault("zenoh_port", int(zenoh_port))
216
+ return await _orig_connect(self, *args, **kwargs)
217
+
218
+ RBBaseSDK.connect = _connect_with_dev_port
163
219
  except Exception as exc:
164
220
  print(f"[muscat-pk] SDK dev network patch skipped: {exc}", flush=True)
165
221
 
222
+ import runpy
166
223
  runpy.run_path("/plugin/py/main.py", run_name="__main__")
167
224
  PY`]);
168
225
 
169
226
  if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
170
- if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
227
+ if (!hostAgentUrl && zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
228
+ if (hostAgentUrl) log.step(`host SDK agent → ${hostAgentUrl}`);
171
229
  log.step("watchfiles → py/main.py (reload on any .py change in py/)");
172
230
  const child = spawn(
173
231
  "docker",
@@ -387,7 +445,9 @@ export async function runDevBack(args = []) {
387
445
  if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
388
446
  // Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
389
447
  // advertise host/port too (idempotent — `muscat-pk dev` already set these).
390
- await ensureRemoteZenohEnv(robotIp, { log });
448
+ // Host-agent mode routes robot SDK calls through the mac host, so the Docker
449
+ // backend does not need its own remote Zenoh peer.
450
+ if (!process.env.MUSCAT_HOST_AGENT_URL) await ensureRemoteZenohEnv(robotIp, { log });
391
451
  const type = manifestRuntime(root);
392
452
  if (type === "ros2") return await runRos2(root, portRanges);
393
453
  if (type === "cpp") return await runCpp(root, portRanges);
@@ -1 +1 @@
1
- {"generatedAt":1783645312620,"userSince":1750932757926,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
1
+ {"generatedAt":1783659929067,"userSince":1750932757926,"hasCustomBabel":false,"hasCustomWebpack":false,"hasStaticDirs":false,"hasStorybookEslint":true,"refCount":0,"testPackages":{"@vitest/browser":"3.2.4","@vitest/coverage-v8":"3.2.4","playwright":"1.59.1","vitest":"3.2.4"},"hasRouterPackage":false,"monorepo":"Turborepo","packageManager":{"type":"yarn","version":"berry","agent":"yarn@berry"},"preview":{"usesGlobals":false},"framework":{"name":"@storybook/react-vite","options":{}},"builder":"@storybook/builder-vite","renderer":"@storybook/react","portableStoriesFileCount":0,"applicationFileCount":0,"storybookVersion":"8.6.18","storybookVersionSpecifier":"^8.6.4","language":"typescript","storybookPackages":{"@chromatic-com/storybook":{"version":"3.2.7"},"@storybook/addon-essentials":{"version":"8.6.18"},"@storybook/addon-links":{"version":"8.6.18"},"@storybook/addon-onboarding":{"version":"8.6.18"},"@storybook/addon-themes":{"version":"8.6.18"},"@storybook/blocks":{"version":"8.6.18"},"@storybook/experimental-addon-test":{"version":"8.6.18"},"@storybook/react":{"version":"8.6.18"},"@storybook/react-vite":{"version":"8.6.18"},"@storybook/test":{"version":"8.6.18"},"@storybook/theming":{"version":"8.6.18"},"eslint-plugin-storybook":{"version":"0.11.6"},"storybook":{"version":"8.6.18"},"storybook-addon-react-docgen":{"version":"1.2.44"}},"addons":{"$SNIP/node_modules/@storybook/addon-links":{"version":null},"$SNIP/node_modules/@storybook/addon-essentials":{"version":null},"$SNIP/node_modules/@chromatic-com/storybook":{"version":null},"$SNIP/node_modules/@storybook/addon-themes":{"version":null},"$SNIP/node_modules/@storybook/experimental-addon-test":{"version":null}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-robotics/muscat-plugin-kit",
3
- "version": "0.1.2-dev.51",
3
+ "version": "0.1.2-dev.52",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -53,8 +53,8 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.29.0",
56
- "@rainbow-robotics/plugin-ui": "0.1.2-dev.51",
57
- "@rainbow-robotics/preview-host": "0.1.2-dev.51",
56
+ "@rainbow-robotics/plugin-ui": "0.1.2-dev.52",
57
+ "@rainbow-robotics/preview-host": "0.1.2-dev.52",
58
58
  "postcss": "^8.5.8",
59
59
  "socket.io": "^4.8.1",
60
60
  "typescript": "^5.8.0",