@holoscript/robotics-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * HoloScript → USD Code Generator
3
+ *
4
+ * Generates Universal Scene Description (USD) files from HoloScript AST.
5
+ * Targets NVIDIA Isaac Sim with UsdPhysics schema.
6
+ */
7
+ import { CompositionNode } from './ast';
8
+ export declare class USDCodeGen {
9
+ private output;
10
+ private indentLevel;
11
+ generate(ast: CompositionNode): string;
12
+ private generateLink;
13
+ private generateJoint;
14
+ private indent;
15
+ private emit;
16
+ private geometryToUSD;
17
+ private vectorToAxisName;
18
+ }
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ /**
3
+ * HoloScript → USD Code Generator
4
+ *
5
+ * Generates Universal Scene Description (USD) files from HoloScript AST.
6
+ * Targets NVIDIA Isaac Sim with UsdPhysics schema.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.USDCodeGen = void 0;
43
+ class USDCodeGen {
44
+ constructor() {
45
+ this.output = [];
46
+ this.indentLevel = 0;
47
+ }
48
+ generate(ast) {
49
+ this.output = [];
50
+ this.indentLevel = 0;
51
+ // USD header
52
+ this.emit('#usda 1.0');
53
+ this.emit('(');
54
+ this.indentLevel++;
55
+ this.emit(`defaultPrim = "${ast.name}"`);
56
+ this.emit('upAxis = "Z"');
57
+ this.emit('metersPerUnit = 1.0');
58
+ this.emit('kilogramsPerMass = 1.0');
59
+ this.indentLevel--;
60
+ this.emit(')');
61
+ this.emit('');
62
+ // Root articulation
63
+ this.emit(`def Xform "${ast.name}" (`);
64
+ this.indentLevel++;
65
+ this.emit('prepend apiSchemas = ["PhysicsArticulationRootAPI"]');
66
+ this.indentLevel--;
67
+ this.emit(')');
68
+ this.emit('{');
69
+ this.indentLevel++;
70
+ // Collect joints for later generation
71
+ const joints = [];
72
+ // Separate joint and link objects
73
+ const jointObjects = [];
74
+ const linkObjects = [];
75
+ for (const obj of ast.objects) {
76
+ if (obj.traits.includes('joint_revolute') || obj.traits.includes('joint_prismatic')) {
77
+ jointObjects.push(obj);
78
+ }
79
+ else {
80
+ linkObjects.push(obj);
81
+ }
82
+ }
83
+ // Generate links
84
+ for (const obj of ast.objects) {
85
+ if (obj.traits.includes('joint_revolute') || obj.traits.includes('joint_prismatic')) {
86
+ // New template format: Joint is separate object
87
+ // Check if there's a corresponding link (child of this joint)
88
+ const childLink = linkObjects.find(link => link.properties.parent === obj.name);
89
+ if (childLink) {
90
+ // Generate the child link
91
+ this.generateLink(childLink);
92
+ // Collect joint info
93
+ const parent = obj.properties.parent || 'World';
94
+ joints.push({
95
+ name: obj.name,
96
+ parent,
97
+ child: childLink.name,
98
+ props: obj.properties,
99
+ });
100
+ }
101
+ else {
102
+ // Old format: joint_parent property (backward compatibility)
103
+ this.generateLink(obj);
104
+ const parent = obj.properties.joint_parent || 'World';
105
+ joints.push({
106
+ name: `${obj.name}Joint`,
107
+ parent,
108
+ child: obj.name,
109
+ props: obj.properties,
110
+ });
111
+ }
112
+ }
113
+ else {
114
+ // Link object without joint (base, end effector, etc.)
115
+ // Only generate if not already generated as child of a joint
116
+ const isChildOfJoint = jointObjects.some(j => linkObjects.find(l => l.properties.parent === j.name && l.name === obj.name));
117
+ if (!isChildOfJoint) {
118
+ this.generateLink(obj);
119
+ }
120
+ }
121
+ }
122
+ // Generate joints after all links
123
+ this.emit('');
124
+ this.emit('# Joints');
125
+ for (const joint of joints) {
126
+ this.generateJoint(joint);
127
+ }
128
+ this.indentLevel--;
129
+ this.emit('}');
130
+ return this.output.join('\n');
131
+ }
132
+ generateLink(obj) {
133
+ const geometry = obj.properties.geometry || 'box';
134
+ const usdType = this.geometryToUSD(geometry);
135
+ const isStatic = obj.traits.includes('static');
136
+ this.emit('');
137
+ this.emit(`# ${obj.name}`);
138
+ this.emit(`def ${usdType} "${obj.name}" (`);
139
+ this.indentLevel++;
140
+ // Add physics APIs (unless static)
141
+ if (!isStatic) {
142
+ this.emit('prepend apiSchemas = ["PhysicsCollisionAPI", "PhysicsRigidBodyAPI", "PhysicsMassAPI"]');
143
+ }
144
+ this.indentLevel--;
145
+ this.emit(')');
146
+ this.emit('{');
147
+ this.indentLevel++;
148
+ // Geometry dimensions (support both array and individual properties)
149
+ if (geometry === 'cylinder') {
150
+ let radius, height;
151
+ if (obj.properties.dimensions) {
152
+ // Old format: dimensions: [radius, height]
153
+ const dims = obj.properties.dimensions;
154
+ [radius, height] = dims;
155
+ }
156
+ else {
157
+ // New format: radius: 0.2, height/length: 0.2
158
+ radius = obj.properties.radius || 0.1;
159
+ height = (obj.properties.height || obj.properties.length) || 0.1;
160
+ }
161
+ this.emit(`float radius = ${radius}`);
162
+ this.emit(`float height = ${height}`);
163
+ }
164
+ else if (geometry === 'box') {
165
+ let length, width, height;
166
+ if (obj.properties.dimensions) {
167
+ // Old format: dimensions: [length, width, height]
168
+ const dims = obj.properties.dimensions;
169
+ [length, width, height] = dims;
170
+ }
171
+ else {
172
+ // New format: length: 1.0, width: 0.1, height: 0.1
173
+ length = obj.properties.length || 1.0;
174
+ width = obj.properties.width || 1.0;
175
+ height = obj.properties.height || 1.0;
176
+ }
177
+ this.emit('double size = 1.0');
178
+ this.emit(`float3 xformOp:scale = (${length}, ${width}, ${height})`);
179
+ this.emit('uniform token[] xformOpOrder = ["xformOp:scale"]');
180
+ }
181
+ else if (geometry === 'sphere') {
182
+ let radius;
183
+ if (obj.properties.dimensions) {
184
+ // Old format: dimensions: [radius]
185
+ const dims = obj.properties.dimensions;
186
+ [radius] = dims;
187
+ }
188
+ else {
189
+ // New format: radius: 0.08
190
+ radius = obj.properties.radius || 0.1;
191
+ }
192
+ this.emit(`float radius = ${radius}`);
193
+ }
194
+ // Color/Material
195
+ if (obj.properties.color) {
196
+ const color = obj.properties.color;
197
+ const [r, g, b] = color;
198
+ this.emit(`color3f[] primvars:displayColor = [(${r}, ${g}, ${b})]`);
199
+ }
200
+ else if (obj.properties.material) {
201
+ // Default colors based on material
202
+ const materialColors = {
203
+ 'metal': '0.6, 0.6, 0.65',
204
+ 'plastic': '0.8, 0.8, 0.8',
205
+ 'wood': '0.6, 0.4, 0.2',
206
+ 'glass': '0.9, 0.9, 1.0',
207
+ };
208
+ const material = obj.properties.material;
209
+ const color = materialColors[material] || '0.8, 0.8, 0.8';
210
+ this.emit(`color3f[] primvars:displayColor = [(${color})]`);
211
+ }
212
+ // Physics properties
213
+ if (!isStatic) {
214
+ this.emit('');
215
+ this.emit('# Physics properties');
216
+ const mass = obj.properties.mass || 1.0;
217
+ this.emit(`float physics:mass = ${mass}`);
218
+ if (obj.properties.inertia) {
219
+ const inertia = obj.properties.inertia;
220
+ this.emit(`float3 physics:diagonalInertia = (${inertia[0]}, ${inertia[1]}, ${inertia[2]})`);
221
+ }
222
+ this.emit('uniform token physics:approximation = "convexHull"');
223
+ }
224
+ else {
225
+ this.emit('');
226
+ this.emit('# Static object');
227
+ this.emit('uniform token physics:rigidBodyEnabled = false');
228
+ }
229
+ // Position (if specified)
230
+ if (obj.properties.position) {
231
+ const pos = obj.properties.position;
232
+ this.emit('');
233
+ this.emit(`double3 xformOp:translate = (${pos[0]}, ${pos[1]}, ${pos[2]})`);
234
+ if (!obj.properties.dimensions) {
235
+ this.emit('uniform token[] xformOpOrder = ["xformOp:translate"]');
236
+ }
237
+ }
238
+ this.indentLevel--;
239
+ this.emit('}');
240
+ }
241
+ generateJoint(joint) {
242
+ this.emit('');
243
+ this.emit(`def PhysicsRevoluteJoint "${joint.name}"`);
244
+ this.emit('{');
245
+ this.indentLevel++;
246
+ // Body references
247
+ this.emit(`rel physics:body0 = <../${joint.parent}>`);
248
+ this.emit(`rel physics:body1 = <../${joint.child}>`);
249
+ this.emit('');
250
+ // Joint frames
251
+ const origin = joint.props.joint_origin || joint.props.position;
252
+ if (origin) {
253
+ const pos = origin;
254
+ this.emit(`point3f physics:localPos0 = (${pos[0]}, ${pos[1]}, ${pos[2]})`);
255
+ }
256
+ else {
257
+ this.emit('point3f physics:localPos0 = (0, 0, 0)');
258
+ }
259
+ this.emit('quatf physics:localRot0 = (1, 0, 0, 0)');
260
+ this.emit('');
261
+ this.emit('point3f physics:localPos1 = (0, 0, 0)');
262
+ this.emit('quatf physics:localRot1 = (1, 0, 0, 0)');
263
+ this.emit('');
264
+ // Joint axis (support both old and new property names)
265
+ const axis = joint.props.joint_axis || joint.props.axis;
266
+ if (axis) {
267
+ const axisVec = axis;
268
+ const axisName = this.vectorToAxisName(axisVec);
269
+ this.emit(`uniform token physics:axis = "${axisName}"`);
270
+ }
271
+ else {
272
+ this.emit('uniform token physics:axis = "Y"');
273
+ }
274
+ this.emit('');
275
+ // Joint limits (support both old and new property names)
276
+ const limits = joint.props.joint_limits || joint.props.limits;
277
+ if (limits) {
278
+ const limitsVec = limits;
279
+ this.emit(`float physics:lowerLimit = ${limitsVec[0]}`);
280
+ this.emit(`float physics:upperLimit = ${limitsVec[1]}`);
281
+ }
282
+ this.emit('');
283
+ // Joint dynamics (support both old and new property names)
284
+ const effort = joint.props.joint_effort || joint.props.max_effort;
285
+ if (effort) {
286
+ this.emit(`float physics:maxForce = ${effort}`);
287
+ }
288
+ const velocity = joint.props.max_velocity;
289
+ if (velocity) {
290
+ this.emit(`float physics:maxVelocity = ${velocity}`);
291
+ }
292
+ this.indentLevel--;
293
+ this.emit('}');
294
+ }
295
+ indent() {
296
+ return ' '.repeat(this.indentLevel);
297
+ }
298
+ emit(line) {
299
+ this.output.push(this.indent() + line);
300
+ }
301
+ geometryToUSD(geometry) {
302
+ const mapping = {
303
+ 'cylinder': 'Cylinder',
304
+ 'box': 'Cube',
305
+ 'sphere': 'Sphere',
306
+ 'cone': 'Cone',
307
+ 'plane': 'Plane',
308
+ 'torus': 'Torus',
309
+ };
310
+ return mapping[geometry] || 'Cube';
311
+ }
312
+ vectorToAxisName(axis) {
313
+ const [x = 0, y = 0, z = 1] = axis;
314
+ if (Math.abs(x) > Math.abs(y) && Math.abs(x) > Math.abs(z)) {
315
+ return 'X';
316
+ }
317
+ else if (Math.abs(y) > Math.abs(z)) {
318
+ return 'Y';
319
+ }
320
+ else {
321
+ return 'Z';
322
+ }
323
+ }
324
+ }
325
+ exports.USDCodeGen = USDCodeGen;
326
+ // Example usage
327
+ if (require.main === module) {
328
+ Promise.resolve().then(() => __importStar(require('./lexer'))).then(({ Lexer }) => {
329
+ Promise.resolve().then(() => __importStar(require('./parser'))).then(({ Parser }) => {
330
+ const source = `
331
+ composition "TwoLinkArm" {
332
+ object "Base" @static {
333
+ geometry: "cylinder"
334
+ dimensions: [0.1, 0.05]
335
+ material: "metal"
336
+ position: [0, 0, 0]
337
+ }
338
+
339
+ object "Link1" @joint_revolute {
340
+ geometry: "box"
341
+ dimensions: [0.5, 0.1, 0.1]
342
+ material: "metal"
343
+ joint_parent: "Base"
344
+ joint_axis: [0, 0, 1]
345
+ joint_limits: [-3.14, 3.14]
346
+ joint_effort: 100
347
+ mass: 1.0
348
+ }
349
+ }
350
+ `;
351
+ const lexer = new Lexer(source);
352
+ const tokens = lexer.tokenize();
353
+ const parser = new Parser(tokens);
354
+ const ast = parser.parse();
355
+ const codegen = new USDCodeGen();
356
+ const usd = codegen.generate(ast);
357
+ console.log('USD Output:');
358
+ console.log(usd);
359
+ });
360
+ });
361
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Complete Robotics Example: 2-DOF Robot Arm
3
+ * Demonstrates compile-time (USD/URDF) + runtime (ROS2/VR control)
4
+ *
5
+ * Features:
6
+ * - @ros_node: ROS2 runtime integration
7
+ * - @gazebo_sim: Gazebo physics simulation
8
+ * - @urdf_export: Export to URDF format
9
+ * - @robot_joint: Articulated joints with limits
10
+ * - VR teleoperation: Control robot with VR controllers
11
+ */
12
+
13
+ scene "Robotics Lab" {
14
+ background: "#1a1a2e";
15
+ lighting: "industrial";
16
+ camera_position: [2, 1.5, 3];
17
+ }
18
+
19
+ // 2-DOF Robot Arm with ROS2 integration
20
+ object "TwoLinkArm" @ros_node @gazebo_sim @urdf_export {
21
+ // ROS2 configuration
22
+ ros_bridge_url: "ws://localhost:9090",
23
+ namespace: "/robot_arm",
24
+ tf_prefix: "arm_",
25
+
26
+ // Gazebo simulation
27
+ gazebo_world: "empty.world",
28
+ physics_engine: "ode",
29
+ gravity: [0, 0, -9.81],
30
+
31
+ // Robot structure
32
+ links: [
33
+ {
34
+ name: "base_link",
35
+ visual: {
36
+ geometry: "cylinder",
37
+ dimensions: [0.1, 0.05],
38
+ material: "steel",
39
+ },
40
+ collision: { geometry: "cylinder", dimensions: [0.1, 0.05] },
41
+ inertial: { mass: 1.0, inertia: [0.01, 0.01, 0.01, 0, 0, 0] },
42
+ position: [0, 0, 0],
43
+ },
44
+ {
45
+ name: "link1",
46
+ visual: {
47
+ geometry: "box",
48
+ dimensions: [0.05, 0.05, 0.5],
49
+ material: "aluminum",
50
+ },
51
+ collision: { geometry: "box", dimensions: [0.05, 0.05, 0.5] },
52
+ inertial: { mass: 0.5, inertia: [0.01, 0.01, 0.001, 0, 0, 0] },
53
+ },
54
+ {
55
+ name: "link2",
56
+ visual: {
57
+ geometry: "box",
58
+ dimensions: [0.05, 0.05, 0.4],
59
+ material: "carbon_fiber",
60
+ },
61
+ collision: { geometry: "box", dimensions: [0.05, 0.05, 0.4] },
62
+ inertial: { mass: 0.3, inertia: [0.005, 0.005, 0.001, 0, 0, 0] },
63
+ },
64
+ ],
65
+
66
+ // Joints with actuators
67
+ joints: [
68
+ {
69
+ name: "shoulder_joint",
70
+ type: "revolute",
71
+ parent: "base_link",
72
+ child: "link1",
73
+ axis: [0, 0, 1],
74
+ origin: { position: [0, 0, 0.05], rotation: [0, 0, 0] },
75
+ limits: { lower: -3.14, upper: 3.14, effort: 10.0, velocity: 2.0 },
76
+ actuator: {
77
+ type: "position",
78
+ kp: 100.0,
79
+ kd: 10.0,
80
+ },
81
+ },
82
+ {
83
+ name: "elbow_joint",
84
+ type: "revolute",
85
+ parent: "link1",
86
+ child: "link2",
87
+ axis: [0, 1, 0],
88
+ origin: { position: [0, 0, 0.5], rotation: [0, 0, 0] },
89
+ limits: { lower: -2.0, upper: 2.0, effort: 5.0, velocity: 1.5 },
90
+ actuator: {
91
+ type: "position",
92
+ kp: 50.0,
93
+ kd: 5.0,
94
+ },
95
+ },
96
+ ],
97
+
98
+ // VR teleoperation
99
+ vr_control: {
100
+ enabled: true,
101
+ controller_mapping: {
102
+ left_hand: "shoulder_joint", // Left VR controller → shoulder
103
+ right_hand: "elbow_joint", // Right VR controller → elbow
104
+ },
105
+ force_feedback: true, // Haptic feedback when robot hits limits
106
+ inverse_kinematics: true, // IK solver for end-effector control
107
+ },
108
+
109
+ // Digital twin sync
110
+ twin_sync: {
111
+ enabled: true,
112
+ sync_rate: 30, // 30 Hz (sync every 33ms)
113
+ bidirectional: true, // Real robot ↔ VR twin
114
+ sensors: [
115
+ { type: "joint_states", topic: "/joint_states" },
116
+ { type: "tf", topic: "/tf" },
117
+ ],
118
+ },
119
+
120
+ // Export options
121
+ export: {
122
+ urdf: "build/two_link_arm.urdf",
123
+ sdf: "build/two_link_arm.sdf",
124
+ usd: "build/two_link_arm.usd", // For NVIDIA Isaac Sim
125
+ mjcf: "build/two_link_arm.xml", // For MuJoCo
126
+ },
127
+ }
128
+
129
+ // Example 2: Mobile robot (differential drive)
130
+ object "MobileBot" @ros_node @gazebo_sim {
131
+ ros_bridge_url: "ws://localhost:9090",
132
+ namespace: "/mobile_bot",
133
+
134
+ // Chassis
135
+ links: [
136
+ {
137
+ name: "chassis",
138
+ visual: { geometry: "box", dimensions: [0.4, 0.3, 0.1], material: "plastic" },
139
+ inertial: { mass: 5.0 },
140
+ },
141
+ {
142
+ name: "left_wheel",
143
+ visual: { geometry: "cylinder", dimensions: [0.1, 0.05], material: "rubber" },
144
+ inertial: { mass: 0.5 },
145
+ },
146
+ {
147
+ name: "right_wheel",
148
+ visual: { geometry: "cylinder", dimensions: [0.1, 0.05], material: "rubber" },
149
+ inertial: { mass: 0.5 },
150
+ },
151
+ ],
152
+
153
+ joints: [
154
+ {
155
+ name: "left_wheel_joint",
156
+ type: "continuous",
157
+ parent: "chassis",
158
+ child: "left_wheel",
159
+ axis: [0, 1, 0],
160
+ actuator: { type: "velocity" },
161
+ },
162
+ {
163
+ name: "right_wheel_joint",
164
+ type: "continuous",
165
+ parent: "chassis",
166
+ child: "right_wheel",
167
+ axis: [0, 1, 0],
168
+ actuator: { type: "velocity" },
169
+ },
170
+ ],
171
+
172
+ // Differential drive controller
173
+ drive_controller: {
174
+ type: "diff_drive",
175
+ wheel_separation: 0.3,
176
+ wheel_radius: 0.1,
177
+ max_velocity: 1.0, // m/s
178
+ max_angular_velocity: 2.0, // rad/s
179
+ },
180
+
181
+ // Sensors
182
+ sensors: [
183
+ {
184
+ name: "lidar",
185
+ type: "laser",
186
+ update_rate: 10,
187
+ topic: "/scan",
188
+ range: { min: 0.1, max: 10.0 },
189
+ fov: 360,
190
+ },
191
+ {
192
+ name: "camera",
193
+ type: "camera",
194
+ update_rate: 30,
195
+ topic: "/camera/image_raw",
196
+ resolution: [640, 480],
197
+ },
198
+ ],
199
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@holoscript/robotics-plugin",
3
+ "version": "1.0.0",
4
+ "description": "HoloScript plugin for robotics: compile-time USD/URDF codegen + runtime ROS2/Gazebo integration. VR robot programming and digital twins.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "python",
10
+ "examples",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "jest --passWithNoTests",
16
+ "clean": "rimraf dist",
17
+ "prepublishOnly": "npm run clean && npm run build"
18
+ },
19
+ "keywords": [
20
+ "holoscript",
21
+ "plugin",
22
+ "robotics",
23
+ "ros2",
24
+ "gazebo",
25
+ "urdf",
26
+ "usd",
27
+ "vr",
28
+ "digital-twin",
29
+ "isaac-sim",
30
+ "webots",
31
+ "mujoco"
32
+ ],
33
+ "author": "HoloScript Contributors",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/holoscript/holoscript-robotics-plugin.git"
38
+ },
39
+ "homepage": "https://github.com/holoscript/holoscript-robotics-plugin#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/holoscript/holoscript-robotics-plugin/issues"
42
+ },
43
+ "peerDependencies": {
44
+ "holoscript": "^3.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "^5.3.0",
48
+ "@types/node": "^20.0.0",
49
+ "jest": "^29.7.0",
50
+ "ts-jest": "^29.1.0",
51
+ "@types/jest": "^29.5.0",
52
+ "rimraf": "^5.0.0"
53
+ },
54
+ "python": {
55
+ "requirements": [
56
+ "roslibpy>=1.6.0"
57
+ ],
58
+ "systemDependencies": [
59
+ "# ROS2 Humble or later (optional, for local ROS2 nodes)",
60
+ "# rosbridge-server (for WebSocket bridge)"
61
+ ]
62
+ },
63
+ "engines": {
64
+ "node": ">=18.0.0"
65
+ }
66
+ }