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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -129,6 +129,37 @@ Could not create .venv (no local Python/uv?) — skipping, Docker dev still work
129
129
 
130
130
  Install `uv` (recommended) or a system Python 3.12, then re-run `muscat-pk new` (or `muscat-pk type change`) to create it.
131
131
 
132
+ ## AI coding agent setup (`muscat-pk mcp`)
133
+
134
+ `muscat-pk mcp` runs an MCP server (stdio) that gives an AI coding agent grounded info about `rb-components`, design tokens, and the Python `rainbow-rb-sdk` — actual prop types, real Storybook examples, and live-introspected SDK signatures/docstrings, instead of the agent guessing from training data. Use it while working inside a plugin project scaffolded by `muscat-pk new`.
135
+
136
+ ### Claude Code
137
+
138
+ Claude Code reads a project-scoped `.mcp.json` at the plugin's root:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "muscat": {
144
+ "command": "muscat-pk",
145
+ "args": ["mcp"]
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### Codex
152
+
153
+ Codex has no project-scoped config file — it reads a single, per-user `~/.codex/config.toml`. Add:
154
+
155
+ ```toml
156
+ [mcp_servers.muscat]
157
+ command = "muscat-pk"
158
+ args = ["mcp"]
159
+ ```
160
+
161
+ You don't run `muscat-pk mcp` yourself either way — the agent spawns and manages it automatically once it's registered.
162
+
132
163
  ## Package roles
133
164
 
134
165
  | Package | Role |
package/cli/src/dev.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  // muscat-pk dev — run the Socket.IO bridge (Docker), dev:ui, and dev:back together.
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
+ import { connect as netConnect } from "node:net";
4
5
  import { release } from "node:os";
5
6
  import { resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
6
8
  import { runDevUi } from "./devUi.mjs";
7
9
  import { runDevBack, stopDevBackContainers } from "./devBack.mjs";
8
10
  import { startBridge, stopBridge, BRIDGE_CONTAINER, BRIDGE_PORT } from "./bridge.mjs";
@@ -17,64 +19,66 @@ import {
17
19
  promptRobotConnection,
18
20
  venvPython,
19
21
  } from "./util.mjs";
20
- import { detectRouteAddress, ensureRemoteZenohEnv, getFreePort, isRemoteRobot } from "./zenohDev.mjs";
22
+ import { ensureRemoteZenohEnv, isRemoteRobot } from "./zenohDev.mjs";
21
23
 
22
- const DEFAULT_HOST_AGENT_PORT = 18080;
24
+ const HOST_AGENT_PATH = fileURLToPath(new URL("./host_agent.py", import.meta.url));
23
25
  const isWsl = () => process.platform === "linux" && /microsoft|wsl/i.test(release());
24
26
  const canAutoStartHostAgent = () => process.platform === "darwin" || isWsl();
25
27
 
26
28
  function waitForHostAgent(port, { timeoutMs = 30_000 } = {}) {
27
29
  const deadline = Date.now() + timeoutMs;
28
- const url = `http://127.0.0.1:${port}/health`;
29
30
  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);
31
+ const probe = () => {
32
+ const sock = netConnect(port, "127.0.0.1");
33
+ sock.once("connect", () => { sock.destroy(); resolve(true); });
34
+ sock.once("error", () => {
35
+ sock.destroy();
36
+ if (Date.now() >= deadline) {
37
+ reject(new Error(`host SDK agent did not open port ${port} in time`));
38
+ return;
39
+ }
40
+ setTimeout(probe, 300);
41
+ });
42
42
  };
43
43
  probe();
44
44
  });
45
45
  }
46
46
 
47
+ const DEFAULT_PROXY_PORT = 47100;
48
+
49
+ // Start the host-side Zenoh byte-proxy (host_agent.py) for a remote robot —
50
+ // see host_agent.py for why a Docker Desktop container can't join the
51
+ // robot's Zenoh mesh itself. Sets MUSCAT_PROXY_ENDPOINT for dev:back.
52
+ // Returns the child process, or null if not applicable / disabled.
47
53
  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}`);
54
+ if (process.env.MUSCAT_PROXY_ENDPOINT) {
55
+ log.step(`Zenoh via host byte-proxy → ${process.env.MUSCAT_PROXY_ENDPOINT}`);
50
56
  return null;
51
57
  }
52
58
  if (process.env.MUSCAT_HOST_AGENT_DISABLE === "1") return null;
53
59
  if (!canAutoStartHostAgent() || !isRemoteRobot(robotIp)) return null;
54
60
 
55
- const agentPath = resolve(root, "py", "host_agent.py");
61
+ const agentPath = process.env.MUSCAT_HOST_AGENT_SCRIPT || HOST_AGENT_PATH;
56
62
  if (!existsSync(agentPath)) return null;
57
63
 
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");
64
+ if (!ensureIdeVenv(root)) {
65
+ log.warn("host Zenoh byte-proxy skipped: could not prepare .venv");
68
66
  return null;
69
67
  }
70
68
 
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");
69
+ // advertiseHost/zenohPort = the host_agent's OWN robot-facing Zenoh session
70
+ // (host LAN IP + port the robot reverse-dials into). The container never
71
+ // sees or needs these — it only talks to the proxyPort below.
72
+ const zenoh = await ensureRemoteZenohEnv(robotIp, { log });
73
+ const advertiseHost = zenoh?.advertiseHost || process.env.MUSCAT_ZENOH_ADVERTISE_HOST;
74
+ const zenohPort = zenoh?.zenohPort || Number(process.env.MUSCAT_ZENOH_PORT);
75
+ if (!advertiseHost || !zenohPort) {
76
+ log.warn("host Zenoh byte-proxy skipped: could not determine host LAN IP / port");
74
77
  return null;
75
78
  }
79
+ const proxyPort = Number(process.env.MUSCAT_PROXY_PORT) || DEFAULT_PROXY_PORT;
76
80
 
77
- const port = await getFreePort(Number(process.env.MUSCAT_HOST_AGENT_PORT || DEFAULT_HOST_AGENT_PORT));
81
+ log.step("starting host Zenoh byte-proxy (Docker Desktop query-routing workaround)...");
78
82
  const py = venvPython(resolve(root, ".venv"));
79
83
  const child = spawn(py, [agentPath], {
80
84
  cwd: root,
@@ -83,21 +87,27 @@ async function maybeStartHostAgent(root, { robotIp, robotToken }) {
83
87
  ...process.env,
84
88
  MUSCAT_ROBOT_HOST: robotIp,
85
89
  MUSCAT_ROBOT_TOKEN: robotToken,
86
- MUSCAT_HOST_AGENT_BIND: bindHost,
87
- MUSCAT_HOST_AGENT_PORT: String(port),
90
+ MUSCAT_ZENOH_ADVERTISE_HOST: advertiseHost,
91
+ MUSCAT_ZENOH_PORT: String(zenohPort),
92
+ MUSCAT_PROXY_PORT: String(proxyPort),
88
93
  },
89
94
  });
90
95
 
91
96
  try {
92
- await waitForHostAgent(port);
97
+ await waitForHostAgent(proxyPort);
93
98
  } catch (e) {
94
99
  child.kill();
95
100
  throw e;
96
101
  }
97
102
 
98
- const containerUrl = `http://${containerHost}:${port}`;
99
- process.env.MUSCAT_HOST_AGENT_URL = containerUrl;
100
- log.step(`host SDK agent → ${containerUrl}`);
103
+ // advertiseHost (not host.docker.internal): a Docker Desktop container run
104
+ // under `dev` may share the bridge container's netns (--network container:),
105
+ // which has no host-gateway alias configured — the host's real LAN IP
106
+ // always resolves regardless of netns mode.
107
+ const proxyEndpoint = `${advertiseHost}:${proxyPort}`;
108
+ process.env.MUSCAT_PROXY_ENDPOINT = proxyEndpoint;
109
+ process.env.MUSCAT_PROXY_PORT = String(proxyPort);
110
+ log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
101
111
  return child;
102
112
  }
103
113
 
@@ -115,18 +125,14 @@ export async function runDev(args = []) {
115
125
 
116
126
  const remoteRobot = isRemoteRobot(robotIp);
117
127
  const hostAgent = await maybeStartHostAgent(root, { robotIp, robotToken });
118
- const hostAgentUrl = process.env.MUSCAT_HOST_AGENT_URL || "";
119
- // Compute the Zenoh advertise host/port up front — startBridge() needs the
120
- // port to publish it on the bridge container so the robot can reach back.
121
- if (hostAgentUrl && !hostAgent) {
122
- log.step(`host SDK agent → ${hostAgentUrl}`);
123
- } else if (!hostAgentUrl) {
124
- await ensureRemoteZenohEnv(robotIp, { log });
125
- }
128
+ const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT || "";
129
+ // No proxy (Linux, disabled, ...): fall back to the container reverse-dialing
130
+ // the robot itself — startBridge() then publishes the zenoh port for it.
131
+ if (!proxyEndpoint) await ensureRemoteZenohEnv(robotIp, { log });
126
132
 
127
133
  log.step("starting Socket.IO bridge (Docker) + dev:ui + dev:back");
128
134
  const bridgeStarted = await startBridge({
129
- zenohPort: remoteRobot && !hostAgentUrl ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
135
+ zenohPort: remoteRobot && !proxyEndpoint ? Number(process.env.MUSCAT_ZENOH_PORT) : null,
130
136
  });
131
137
 
132
138
  if (!bridgeStarted) {
@@ -87,20 +87,23 @@ 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
+ // See host_agent.py: a Docker Desktop container can't join the robot's
91
+ // Zenoh mesh, so it speaks a byte protocol to the host-side proxy instead.
92
+ const proxyEndpoint = process.env.MUSCAT_PROXY_ENDPOINT ?? "";
91
93
  // Share the bridge container's network namespace so localhost:10000 is the
92
94
  // bridge (set by `muscat-pk dev`). Standalone dev:back has no bridge to join.
93
95
  const bridgeContainer = process.env.MUSCAT_PK_BRIDGE_CONTAINER;
94
96
  const netFlags = bridgeContainer ? ["--network", `container:${bridgeContainer}`] : [];
95
- const standaloneZenohPortFlags = !bridgeContainer && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
97
+ // Only the container's OWN reverse-dial path needs to publish a listen port.
98
+ // With the byte-proxy, the container just connects out to host.docker.internal
99
+ // — nothing to publish.
100
+ const standaloneZenohPortFlags =
101
+ !bridgeContainer && !proxyEndpoint && zenohPort ? ["-p", `${zenohPort}:${zenohPort}`] : [];
96
102
  const hostGatewayFlags = bridgeContainer
97
103
  ? []
98
104
  : ["--add-host", "host.docker.internal:host-gateway"];
99
- // Without the bridge's netns, this container is isolated — its own
100
- // 127.0.0.1 (rainbow.rb_sdk's gateway default) is itself, not the host, so
101
- // it can never see a gateway already running on the host (e.g. the user's
102
- // own robot stack occupying port 10000, the reason the bridge start above
103
- // was skipped in the first place). host.docker.internal reaches the host.
105
+ // Without the bridge's netns, 127.0.0.1 (the SDK's gateway default) is the
106
+ // container itself, not the host — host.docker.internal reaches the host.
104
107
  const gatewayFlags = bridgeContainer
105
108
  ? []
106
109
  : ["-e", "RB_PLUGIN_GATEWAY_URL=http://host.docker.internal:10000"];
@@ -113,9 +116,13 @@ async function runPython(root, portRanges) {
113
116
  ...(pluginId ? ["-e", `RB_PLUGIN_ID=${pluginId}`] : []),
114
117
  ...(robotIp ? ["-e", `MUSCAT_ROBOT_HOST=${robotIp}`] : []),
115
118
  ...(robotToken ? ["-e", `MUSCAT_ROBOT_TOKEN=${robotToken}`] : []),
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}`] : []),
119
+ // Byte-proxy mode: the container has no Zenoh session of its own — the
120
+ // entry script's shim speaks the raw query/put/subscribe protocol to the
121
+ // host_agent process at this address instead.
122
+ ...(proxyEndpoint ? ["-e", `MUSCAT_PROXY_ENDPOINT=${proxyEndpoint}`] : []),
123
+ // Fallback (no proxy): the container reverse-dials the robot itself.
124
+ ...(!proxyEndpoint && zenohAdvertiseHost ? ["-e", `MUSCAT_ZENOH_ADVERTISE_HOST=${zenohAdvertiseHost}`] : []),
125
+ ...(!proxyEndpoint && zenohPort ? ["-e", `MUSCAT_ZENOH_PORT=${zenohPort}`] : []),
119
126
  ...portEnvFlags(portRanges),
120
127
  ];
121
128
  const docker = (args, opts = {}) =>
@@ -142,69 +149,196 @@ async function runPython(root, portRanges) {
142
149
  " && { [ -f requirements.txt ] && pip install --break-system-packages --ignore-installed -q -r requirements.txt || true; }"]);
143
150
 
144
151
  docker(["exec", container, "bash", "-lc", `cat > /tmp/muscat-pk-python-dev-entry.py <<'PY'
145
- import json
146
152
  import os
147
- import urllib.request
148
-
149
- def install_host_agent_sdk_patch(host_agent_url):
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
198
153
 
199
154
  try:
200
- advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
201
- zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
202
- host_agent_url = os.getenv("MUSCAT_HOST_AGENT_URL")
155
+ proxy_endpoint = os.getenv("MUSCAT_PROXY_ENDPOINT")
156
+ if proxy_endpoint:
157
+ # Byte-proxy mode: this container has no Zenoh session of its own.
158
+ # ZenohClient's C++ core is replaced with a shim that speaks a raw
159
+ # query/put/subscribe byte protocol to host_agent.py instead.
160
+ import json
161
+ import socket
162
+ import struct
163
+ import threading
164
+
165
+ _proxy_host, _proxy_port_s = proxy_endpoint.rsplit(":", 1)
166
+ _proxy_port = int(_proxy_port_s)
167
+
168
+ def _send_frame(conn, lock, obj):
169
+ body = json.dumps(obj).encode()
170
+ with lock:
171
+ conn.sendall(struct.pack(">I", len(body)) + body)
172
+
173
+ def _recv_frame(conn):
174
+ hdr = b""
175
+ while len(hdr) < 4:
176
+ chunk = conn.recv(4 - len(hdr))
177
+ if not chunk:
178
+ return None
179
+ hdr += chunk
180
+ n = struct.unpack(">I", hdr)[0]
181
+ buf = b""
182
+ while len(buf) < n:
183
+ chunk = conn.recv(n - len(buf))
184
+ if not chunk:
185
+ return None
186
+ buf += chunk
187
+ return json.loads(buf.decode())
188
+
189
+ class _ProxyRawClient:
190
+ """Drop-in replacement for ZenohClient's C++ core, backed by the
191
+ host_agent byte-proxy instead of a local Zenoh session."""
192
+
193
+ sender = "RRS"
194
+ sender_id = ""
195
+
196
+ def __init__(self, host, port):
197
+ self._conn = socket.create_connection((host, port), timeout=10)
198
+ # create_connection() leaves the connect timeout applied to
199
+ # the socket permanently; the background read loop below does
200
+ # a long-lived blocking recv() for async "sample" pushes, so
201
+ # it must not time out on an idle connection.
202
+ self._conn.settimeout(None)
203
+ self._send_lock = threading.Lock()
204
+ self._req_id = 0
205
+ self._req_lock = threading.Lock()
206
+ self._pending = {}
207
+ self._subs = {} # sub_id -> callback
208
+ self._sub_by_keyexpr = {}
209
+ threading.Thread(target=self._read_loop, daemon=True).start()
210
+
211
+ def _next_id(self):
212
+ with self._req_lock:
213
+ self._req_id += 1
214
+ return self._req_id
215
+
216
+ def _read_loop(self):
217
+ while True:
218
+ frame = _recv_frame(self._conn)
219
+ if frame is None:
220
+ return
221
+ ftype = frame.get("type")
222
+ if ftype == "sample":
223
+ cb = self._sub_by_keyexpr.get(frame["keyexpr"])
224
+ if cb is not None:
225
+ try:
226
+ cb(frame["key"], bytes.fromhex(frame["payload_hex"]), frame.get("att", ""))
227
+ except Exception as exc:
228
+ print(f"[muscat-pk] proxy subscriber callback error: {exc}", flush=True)
229
+ elif "id" in frame:
230
+ ev = self._pending.pop(frame["id"], None)
231
+ if ev is not None:
232
+ ev[1] = frame
233
+ ev[0].set()
234
+
235
+ def _request(self, obj, timeout=30):
236
+ req_id = self._next_id()
237
+ obj["id"] = req_id
238
+ ev = threading.Event()
239
+ slot = [ev, None]
240
+ self._pending[req_id] = slot
241
+ _send_frame(self._conn, self._send_lock, obj)
242
+ if not ev.wait(timeout):
243
+ self._pending.pop(req_id, None)
244
+ raise TimeoutError(f"proxy request timed out: {obj.get('type')} {obj.get('keyexpr')}")
245
+ return slot[1]
246
+
247
+ def query(self, keyexpr, payload, timeout_ms, target):
248
+ resp = self._request({
249
+ "type": "query", "keyexpr": keyexpr,
250
+ "payload_hex": payload.hex() if payload else None,
251
+ "timeout_ms": timeout_ms, "target": target,
252
+ }, timeout=(timeout_ms / 1000) + 5)
253
+ if not resp.get("ok"):
254
+ raise RuntimeError(resp.get("error", "proxy query failed"))
255
+ return [
256
+ (r["key"], bytes.fromhex(r["payload_hex"]), r["att"], r["encoding"], r["is_error"])
257
+ for r in resp["replies"]
258
+ ]
259
+
260
+ def query_all(self, keyexpr, payload, timeout_ms, target):
261
+ return self.query(keyexpr, payload, timeout_ms, target)
262
+
263
+ def publish(self, topic, payload, att, congestion_control, priority, express):
264
+ resp = self._request({
265
+ "type": "put", "keyexpr": topic,
266
+ "payload_hex": bytes(payload).hex() if payload else None,
267
+ })
268
+ if not resp.get("ok"):
269
+ raise RuntimeError(resp.get("error", "proxy put failed"))
270
+
271
+ def has_subscriber(self, topic):
272
+ return topic in self._sub_by_keyexpr
273
+
274
+ def declare_subscriber(self, topic, callback, opts=None):
275
+ resp = self._request({"type": "subscribe", "keyexpr": topic})
276
+ if not resp.get("ok"):
277
+ raise RuntimeError(resp.get("error", "proxy subscribe failed"))
278
+ sub_id = self._next_id()
279
+ self._subs[sub_id] = topic
280
+ self._sub_by_keyexpr[topic] = callback
281
+ return sub_id
282
+
283
+ def undeclare_subscriber(self, sub_id):
284
+ topic = self._subs.pop(sub_id, None)
285
+ if topic is None:
286
+ return
287
+ self._sub_by_keyexpr.pop(topic, None)
288
+ self._request({"type": "unsubscribe", "keyexpr": topic})
289
+
290
+ def unsubscribe_topic(self, topic):
291
+ for sub_id, t in list(self._subs.items()):
292
+ if t == topic:
293
+ self.undeclare_subscriber(sub_id)
294
+
295
+ def unsubscribe_all(self):
296
+ for sub_id in list(self._subs):
297
+ self.undeclare_subscriber(sub_id)
298
+
299
+ def get_sub_stats(self, sub_id):
300
+ return {}
301
+
302
+ def declare_queryable(self, keyexpr, handler):
303
+ raise NotImplementedError("byte-proxy mode does not support declare_queryable")
304
+
305
+ def undeclare_queryable_by_topic(self, keyexpr):
306
+ pass
307
+
308
+ # ZenohClient is a process-wide singleton whose __init__ eagerly
309
+ # calls connect() on first construction. rainbow.rb_sdk.base
310
+ # transitively imports rainbow.rb_log.log, which constructs one at
311
+ # module level — so this patch must land before RBBaseSDK is
312
+ # imported, or that eager construction wins with a real session.
313
+ from rainbow.rb_zenoh.client import ZenohClient
314
+
315
+ def _proxy_connect(self, *, config_pairs=None, sender=None, max_attempts=8, initial_delay_s=0.5, max_delay_s=3.0):
316
+ if self._client is not None:
317
+ return
318
+ if sender is not None:
319
+ self._sender = sender
320
+ print(f"[muscat-pk] zenoh via host byte-proxy → {proxy_endpoint} (no local Zenoh session)", flush=True)
321
+ self._client = _ProxyRawClient(_proxy_host, _proxy_port)
322
+
323
+ ZenohClient.connect = _proxy_connect
203
324
 
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)
325
+ from rainbow.rb_sdk.base import RBBaseSDK
326
+
327
+ async def _sdk_connect_via_proxy(self, *args, **kwargs):
328
+ # No HTTP registration with common — host_agent already handles
329
+ # that. Just make sure the (proxy-backed) shared ZenohClient exists.
330
+ self._ensure_zenoh_client_alive()
331
+ return None
332
+
333
+ async def _sdk_disconnect_via_proxy(self, *args, **kwargs):
334
+ return None
335
+
336
+ RBBaseSDK.connect = _sdk_connect_via_proxy
337
+ RBBaseSDK.disconnect = _sdk_disconnect_via_proxy
207
338
  else:
339
+ # Fallback: no host byte-proxy — the container reverse-dials the robot itself.
340
+ advertise_host = os.getenv("MUSCAT_ZENOH_ADVERTISE_HOST")
341
+ zenoh_port = os.getenv("MUSCAT_ZENOH_PORT")
208
342
  from rainbow.rb_sdk.base import RBBaseSDK
209
343
  if advertise_host:
210
344
  RBBaseSDK._detect_local_ip = staticmethod(lambda host: advertise_host)
@@ -224,8 +358,8 @@ runpy.run_path("/plugin/py/main.py", run_name="__main__")
224
358
  PY`]);
225
359
 
226
360
  if (pluginId) log.step(`RB_PLUGIN_ID=${pluginId}`);
227
- if (!hostAgentUrl && zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
228
- if (hostAgentUrl) log.step(`host SDK agent → ${hostAgentUrl}`);
361
+ if (proxyEndpoint) log.step(`Zenoh via host byte-proxy → ${proxyEndpoint}`);
362
+ else if (zenohAdvertiseHost && zenohPort) log.step(`Zenoh advertise → ${zenohAdvertiseHost}:${zenohPort}`);
229
363
  log.step("watchfiles → py/main.py (reload on any .py change in py/)");
230
364
  const child = spawn(
231
365
  "docker",
@@ -445,9 +579,9 @@ export async function runDevBack(args = []) {
445
579
  if (robotToken) process.env.MUSCAT_ROBOT_TOKEN = robotToken;
446
580
  // Standalone `dev:back` against a remote robot needs the Zenoh reverse-path
447
581
  // advertise host/port too (idempotent — `muscat-pk dev` already set these).
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 });
582
+ // Byte-proxy mode (a host-side host_agent set MUSCAT_PROXY_ENDPOINT) makes
583
+ // the container speak the proxy protocol instead, so it needs no reverse-dial.
584
+ if (!process.env.MUSCAT_PROXY_ENDPOINT) await ensureRemoteZenohEnv(robotIp, { log });
451
585
  const type = manifestRuntime(root);
452
586
  if (type === "ros2") return await runRos2(root, portRanges);
453
587
  if (type === "cpp") return await runCpp(root, portRanges);
@@ -0,0 +1,162 @@
1
+ """Host-side raw Zenoh byte-proxy.
2
+
3
+ Runs on the host as a normal Zenoh peer (reverse-dials the robot, same as the
4
+ SDK's own connect()) and serves a tiny domain-agnostic byte protocol over TCP
5
+ — {query, put, subscribe} by keyexpr + bytes only — so a Docker Desktop
6
+ container, which cannot join the robot's Zenoh mesh itself, can talk to a
7
+ process that can.
8
+
9
+ Env: MUSCAT_ROBOT_HOST, MUSCAT_ROBOT_TOKEN, MUSCAT_ZENOH_ADVERTISE_HOST,
10
+ MUSCAT_ZENOH_PORT (robot-facing), MUSCAT_PROXY_PORT (container-facing).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import contextlib
17
+ import json
18
+ import os
19
+ import socket
20
+ import struct
21
+ import threading
22
+
23
+
24
+ def _send_frame(conn: socket.socket, lock: threading.Lock, obj: dict) -> None:
25
+ body = json.dumps(obj).encode()
26
+ with lock:
27
+ conn.sendall(struct.pack(">I", len(body)) + body)
28
+
29
+
30
+ def _recv_frame(conn: socket.socket) -> dict | None:
31
+ hdr = b""
32
+ while len(hdr) < 4:
33
+ chunk = conn.recv(4 - len(hdr))
34
+ if not chunk:
35
+ return None
36
+ hdr += chunk
37
+ n = struct.unpack(">I", hdr)[0]
38
+ buf = b""
39
+ while len(buf) < n:
40
+ chunk = conn.recv(n - len(buf))
41
+ if not chunk:
42
+ return None
43
+ buf += chunk
44
+ return json.loads(buf.decode())
45
+
46
+
47
+ def _handle_conn(conn: socket.socket, zenoh_client) -> None:
48
+ send_lock = threading.Lock()
49
+ sub_ids: dict[str, int] = {}
50
+ try:
51
+ while True:
52
+ req = _recv_frame(conn)
53
+ if req is None:
54
+ return
55
+ op = req.get("type")
56
+
57
+ if op == "query":
58
+ payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else None
59
+ try:
60
+ replies = zenoh_client._client.query(
61
+ req["keyexpr"], payload, req.get("timeout_ms", 2000), req.get("target", 0)
62
+ )
63
+ out = {
64
+ "type": "query_resp", "id": req["id"], "ok": True,
65
+ "replies": [
66
+ {"key": k, "payload_hex": bytes(p).hex(), "att": a, "encoding": e, "is_error": ie}
67
+ for (k, p, a, e, ie) in replies
68
+ ],
69
+ }
70
+ except Exception as exc:
71
+ out = {"type": "query_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
72
+ _send_frame(conn, send_lock, out)
73
+
74
+ elif op == "put":
75
+ payload = bytes.fromhex(req["payload_hex"]) if req.get("payload_hex") else b""
76
+ try:
77
+ zenoh_client._client.publish(req["keyexpr"], payload, "", 0, 5, False)
78
+ out = {"type": "put_resp", "id": req["id"], "ok": True}
79
+ except Exception as exc:
80
+ out = {"type": "put_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
81
+ _send_frame(conn, send_lock, out)
82
+
83
+ elif op == "subscribe":
84
+ keyexpr = req["keyexpr"]
85
+
86
+ def on_sample(topic: str, payload_bytes: bytes, att: str, _ke: str = keyexpr) -> None:
87
+ _send_frame(conn, send_lock, {
88
+ "type": "sample", "keyexpr": _ke, "key": topic,
89
+ "payload_hex": bytes(payload_bytes).hex(), "att": att,
90
+ })
91
+
92
+ try:
93
+ sub_id = zenoh_client._client.declare_subscriber(keyexpr, on_sample)
94
+ sub_ids[keyexpr] = sub_id
95
+ out = {"type": "subscribe_resp", "id": req["id"], "ok": True}
96
+ except Exception as exc:
97
+ out = {"type": "subscribe_resp", "id": req["id"], "ok": False, "error": f"{type(exc).__name__}: {exc}"}
98
+ _send_frame(conn, send_lock, out)
99
+
100
+ elif op == "unsubscribe":
101
+ keyexpr = req["keyexpr"]
102
+ sub_id = sub_ids.pop(keyexpr, None)
103
+ if sub_id is not None:
104
+ try:
105
+ zenoh_client._client.undeclare_subscriber(sub_id)
106
+ except Exception:
107
+ pass
108
+ _send_frame(conn, send_lock, {"type": "unsubscribe_resp", "id": req["id"], "ok": True})
109
+
110
+ else:
111
+ _send_frame(conn, send_lock, {"type": "error", "error": f"unknown op {op!r}"})
112
+ except (ConnectionResetError, BrokenPipeError, OSError):
113
+ pass
114
+ finally:
115
+ for sub_id in sub_ids.values():
116
+ try:
117
+ zenoh_client._client.undeclare_subscriber(sub_id)
118
+ except Exception:
119
+ pass
120
+ conn.close()
121
+
122
+
123
+ def main() -> None:
124
+ from rainbow.rb_sdk.base import RBBaseSDK
125
+
126
+ host = os.environ["MUSCAT_ROBOT_HOST"]
127
+ token = os.environ.get("MUSCAT_ROBOT_TOKEN", "")
128
+ advertise = os.environ["MUSCAT_ZENOH_ADVERTISE_HOST"]
129
+ zenoh_port = int(os.environ["MUSCAT_ZENOH_PORT"])
130
+ proxy_port = int(os.environ["MUSCAT_PROXY_PORT"])
131
+
132
+ # The robot reverse-dials the advertised host:port; force it to the host's
133
+ # real LAN IP (matches what dev.mjs computed via ensureRemoteZenohEnv).
134
+ RBBaseSDK._detect_local_ip = staticmethod(lambda _host: advertise)
135
+
136
+ peer = RBBaseSDK()
137
+ connect_kwargs = {"host": host, "zenoh_port": zenoh_port, "mesh_timeout": 15.0}
138
+ if token:
139
+ connect_kwargs["token"] = token
140
+
141
+ asyncio.run(peer.connect(**connect_kwargs))
142
+ print(f"[host-agent] zenoh peer joined robot mesh (listening :{zenoh_port})", flush=True)
143
+
144
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
145
+ srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
146
+ srv.bind(("0.0.0.0", proxy_port))
147
+ srv.listen(32)
148
+ print(f"[host-agent] byte-proxy listening :{proxy_port} (container connects via host.docker.internal)", flush=True)
149
+
150
+ try:
151
+ while True:
152
+ conn, _addr = srv.accept()
153
+ threading.Thread(target=_handle_conn, args=(conn, peer.zenoh_client), daemon=True).start()
154
+ except KeyboardInterrupt:
155
+ pass
156
+ finally:
157
+ with contextlib.suppress(Exception):
158
+ asyncio.run(peer.disconnect())
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()
package/cli/src/new.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  writeFileSync,
13
13
  } from "node:fs";
14
14
  import { basename, dirname, resolve } from "node:path";
15
- import { ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
15
+ import { color, ensureIdeVenv, hasBin, kitVersion, parseArgs, prompt, templatesDir, log } from "./util.mjs";
16
16
 
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).
@@ -40,6 +40,33 @@ export const TYPE_CHOICES = {
40
40
  const TYPES = Object.keys(TYPE_CHOICES);
41
41
  const PMS = ["npm", "yarn", "pnpm", "bun"];
42
42
 
43
+ function printNewSuccess({ targetName, type, pm }) {
44
+ const mark = [
45
+ " /\\",
46
+ " / \\_",
47
+ " ____||__",
48
+ " / \\__/ \\",
49
+ " __ \\__/ \\__/ __",
50
+ " / \\__/\\__/\\__/ \\",
51
+ " \\__/ \\__/ \\__/",
52
+ " \\__/ \\__/",
53
+ " \\__/",
54
+ ].map((line) => color.green(line)).join("\n");
55
+
56
+ log.info("");
57
+ log.info(mark);
58
+ log.info(color.bold(color.green("Welcome to Muscat Plugin Kit!")));
59
+ log.info(color.dim("A fresh plugin workspace is ready."));
60
+ log.info("");
61
+ log.ok(`created ${color.bold(targetName)} ${color.dim(`(type: ${type}, pm: ${pm})`)}`);
62
+ log.info("");
63
+ log.info(color.bold("Next steps"));
64
+ log.info(` ${color.cyan("cd")} ${targetName}`);
65
+ log.info(` ${color.cyan("muscat-pk dev")} ${color.dim("# preview in mini-Muscat + back end")}`);
66
+ log.info(` ${color.cyan("muscat-pk build")} ${color.dim("# build + distributable ZIP")}`);
67
+ log.info("");
68
+ }
69
+
43
70
  // Build the manifest runtime object for a given type choice + plugin slug.
44
71
  export function runtimeForChoice(choice, slug) {
45
72
  switch (choice.type) {
@@ -130,7 +157,7 @@ export async function runNew(args) {
130
157
 
131
158
  const dirArg = opts._[0];
132
159
  const targetName =
133
- dirArg || (await prompt("Plugin directory name: ", "my-plugin"));
160
+ dirArg || (await prompt(color.cyan("Plugin directory name: "), "my-plugin"));
134
161
  const target = resolve(process.cwd(), targetName);
135
162
  if (existsSync(target) && readdirSync(target).length > 0) {
136
163
  throw new Error(`directory not empty: ${target}`);
@@ -141,19 +168,19 @@ export async function runNew(args) {
141
168
  let descriptionArg = opts.description;
142
169
 
143
170
  if (!opts.yes) {
144
- if (!authorArg) authorArg = await prompt("Author name: ", "");
145
- if (!emailArg) emailArg = await prompt("Email (required): ", "");
146
- if (!descriptionArg) descriptionArg = await prompt("Description (English): ", "");
171
+ if (!authorArg) authorArg = await prompt(color.cyan("Author name: "), "");
172
+ if (!emailArg) emailArg = await prompt(`${color.cyan("Email")} ${color.yellow("(required)")}: `, "");
173
+ if (!descriptionArg) descriptionArg = await prompt(color.cyan("Description (English): "), "");
147
174
  }
148
175
 
149
176
  let type = opts.type;
150
177
  if (!type && !opts.yes) {
151
- console.log("\nSelect the plugin type:");
152
- console.log(" 1) Python");
153
- console.log(" 2) C++");
154
- console.log(" 3) ROS2 (rclpy / Python)");
155
- console.log(" 4) ROS2 (rclcpp / C++)");
156
- const choice = await prompt("\n선택 [1/2/3/4, default: 1]: ", "1");
178
+ console.log(`\n${color.bold(color.cyan("Select the plugin type:"))}`);
179
+ console.log(` ${color.green("1)") } ${color.bold("Python")}`);
180
+ console.log(` ${color.green("2)") } ${color.bold("C++")}`);
181
+ console.log(` ${color.green("3)") } ${color.bold("ROS2")} ${color.dim("(rclpy / Python)")}`);
182
+ console.log(` ${color.green("4)") } ${color.bold("ROS2")} ${color.dim("(rclcpp / C++)")}`);
183
+ const choice = await prompt(`\n${color.cyan("선택")} ${color.dim("[1/2/3/4, default: 1]")}: `, "1");
157
184
  type = { "2": "cpp", "3": "ros2", "4": "ros2-cpp" }[choice] ?? "python";
158
185
  }
159
186
  type = TYPES.includes(type) ? type : "python";
@@ -173,7 +200,10 @@ export async function runNew(args) {
173
200
  let pm = opts.pm;
174
201
  if (!pm && !opts.yes) {
175
202
  const defaultPm = isInsideYarnWorkspace(process.cwd()) ? "yarn" : "npm";
176
- pm = await prompt(`Front-end package manager (${PMS.join("/")}) [${defaultPm}]: `, defaultPm);
203
+ pm = await prompt(
204
+ `${color.cyan("Front-end package manager")} ${color.dim(`(${PMS.join("/")})`)} ${color.dim(`[${defaultPm}]`)}: `,
205
+ defaultPm,
206
+ );
177
207
  }
178
208
  pm = PMS.includes(pm) ? pm : "npm";
179
209
 
@@ -244,15 +274,29 @@ export async function runNew(args) {
244
274
  }
245
275
  }
246
276
 
277
+ // git init (best-effort, matches create-react-app/vite scaffolds). Skipped
278
+ // if git isn't installed or the target is already inside a repo (nested
279
+ // packages in a monorepo shouldn't get their own .git).
280
+ if (hasBin("git")) {
281
+ try {
282
+ execSync("git rev-parse --is-inside-work-tree", { cwd: target, stdio: "ignore" });
283
+ } catch {
284
+ try {
285
+ execSync("git init", { cwd: target, stdio: "ignore" });
286
+ execSync("git add -A", { cwd: target, stdio: "ignore" });
287
+ execSync('git commit -m "Initial commit from muscat-pk new" --no-verify', { cwd: target, stdio: "ignore" });
288
+ } catch {
289
+ log.warn("git init failed — run it manually if you want version control here.");
290
+ }
291
+ }
292
+ }
293
+
247
294
  // The back end (python / cpp / ros2) runs in a Docker container that matches
248
295
  // the robot — no local Python/toolchain setup needed. Only `none` skips it.
249
296
  const needsDocker = runtimeChoice.type !== "none";
250
297
  const noDocker = needsDocker && !hasBin("docker");
251
298
 
252
- log.ok(`created ${targetName} (type: ${type}, pm: ${pm})`);
253
- log.info(`\n cd ${targetName}`);
254
- log.info(` muscat-pk dev # preview in mini-Muscat + back end`);
255
- log.info(` muscat-pk build # build + distributable ZIP\n`);
299
+ printNewSuccess({ targetName, type, pm });
256
300
 
257
301
  // ── post-scaffold environment warnings ───────────────────────────────────
258
302
  const noNode = !hasBin("node");
package/cli/src/util.mjs CHANGED
@@ -141,16 +141,26 @@ export function findPluginRoot(start = process.cwd()) {
141
141
  );
142
142
  }
143
143
 
144
- const RED = "\x1b[31m";
145
- const ORANGE = "\x1b[33m";
146
- const RESET = "\x1b[0m";
144
+ const useColor = process.env.NO_COLOR !== "1" && (process.stdout.isTTY || process.env.FORCE_COLOR);
145
+ const ansi = (code) => (s) => useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
146
+
147
+ export const color = {
148
+ bold: ansi("1"),
149
+ dim: ansi("2"),
150
+ red: ansi("31"),
151
+ green: ansi("32"),
152
+ yellow: ansi("33"),
153
+ blue: ansi("34"),
154
+ magenta: ansi("35"),
155
+ cyan: ansi("36"),
156
+ };
147
157
 
148
158
  export const log = {
149
159
  info: (...m) => console.log(...m),
150
- step: (...m) => console.log("›", ...m),
151
- ok: (...m) => console.log("✓", ...m),
152
- warn: (...m) => console.warn(`${ORANGE}⚠${RESET}`, ...m.map(s => `${ORANGE}${s}${RESET}`)),
153
- error: (...m) => console.error(`${RED}✖${RESET}`, ...m.map(s => `${RED}${s}${RESET}`)),
160
+ step: (...m) => console.log(color.cyan("›"), ...m),
161
+ ok: (...m) => console.log(color.green("✓"), ...m),
162
+ warn: (...m) => console.warn(color.yellow("⚠"), ...m.map(s => color.yellow(s))),
163
+ error: (...m) => console.error(color.red("✖"), ...m.map(s => color.red(s))),
154
164
  };
155
165
 
156
166
  // dev / dev:back / build run the plugin back end, the dev bridge, and cpp/ros2
@@ -1 +1 @@
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}}}
1
+ {"generatedAt":1783908008319,"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.52",
3
+ "version": "0.1.2-dev.53",
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.52",
57
- "@rainbow-robotics/preview-host": "0.1.2-dev.52",
56
+ "@rainbow-robotics/plugin-ui": "0.1.2-dev.53",
57
+ "@rainbow-robotics/preview-host": "0.1.2-dev.53",
58
58
  "postcss": "^8.5.8",
59
59
  "socket.io": "^4.8.1",
60
60
  "typescript": "^5.8.0",