foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +399 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/commands/parts.py ADDED
@@ -0,0 +1,166 @@
1
+ """
2
+ Parts catalog commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate parts list - List available parts
6
+ - ate parts check - Check part compatibility for skill
7
+ - ate parts require - Add part dependency to skill
8
+ """
9
+
10
+ import sys
11
+ from typing import Optional
12
+
13
+
14
+ def parts_list(client, category: Optional[str], manufacturer: Optional[str],
15
+ search: Optional[str]) -> None:
16
+ """List available parts."""
17
+ print("Fetching parts catalog...")
18
+
19
+ params = {}
20
+ if category:
21
+ params["category"] = category
22
+ print(f" Category: {category}")
23
+ if manufacturer:
24
+ params["manufacturer"] = manufacturer
25
+ print(f" Manufacturer: {manufacturer}")
26
+ if search:
27
+ params["search"] = search
28
+ print(f" Search: {search}")
29
+
30
+ try:
31
+ response = client._request("GET", "/parts", params=params)
32
+ parts = response.get("parts", [])
33
+ pagination = response.get("pagination", {})
34
+
35
+ if not parts:
36
+ print("\nNo parts found matching criteria.")
37
+ return
38
+
39
+ print(f"\n{'=' * 70}")
40
+ print(f"{'Part Name':<30} {'Category':<15} {'Manufacturer':<20}")
41
+ print(f"{'=' * 70}")
42
+
43
+ for part in parts:
44
+ name = part.get("name", "")[:28]
45
+ cat = part.get("category", "")[:13]
46
+ mfr = part.get("manufacturer", "")[:18]
47
+ print(f"{name:<30} {cat:<15} {mfr:<20}")
48
+
49
+ total = pagination.get("total", len(parts))
50
+ print(f"{'=' * 70}")
51
+ print(f"Showing {len(parts)} of {total} parts")
52
+
53
+ except Exception as e:
54
+ print(f"\n✗ Failed to list parts: {e}", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+
58
+ def parts_check(client, skill_id: str) -> None:
59
+ """Check part compatibility for a skill."""
60
+ print(f"Checking parts for skill: {skill_id}")
61
+
62
+ try:
63
+ response = client._request("GET", f"/skills/{skill_id}/parts")
64
+
65
+ skill = response.get("skill", {})
66
+ parts = response.get("parts", [])
67
+ summary = response.get("summary", {})
68
+ by_category = response.get("byCategory", {})
69
+
70
+ print(f"\nSkill: {skill.get('name', skill_id)}")
71
+ print(f"Type: {skill.get('type', 'unknown')}")
72
+
73
+ if not parts:
74
+ print("\n✓ No part dependencies declared for this skill.")
75
+ return
76
+
77
+ print(f"\n{'=' * 70}")
78
+ print(f"Part Dependencies ({summary.get('total', 0)} total)")
79
+ print(f"{'=' * 70}")
80
+
81
+ for category, cat_parts in by_category.items():
82
+ print(f"\n{category.upper()}:")
83
+ for p in cat_parts:
84
+ part = p.get("part", {})
85
+ required = "REQUIRED" if p.get("required") else "optional"
86
+ version = p.get("minVersion", "any")
87
+ if p.get("maxVersion"):
88
+ version += f" - {p['maxVersion']}"
89
+
90
+ icon = "●" if p.get("required") else "○"
91
+ print(f" {icon} {part.get('name'):<30} [{required}] v{version}")
92
+
93
+ print(f"\n{'=' * 70}")
94
+ print(f"Summary: {summary.get('required', 0)} required, {summary.get('optional', 0)} optional")
95
+
96
+ except Exception as e:
97
+ print(f"\n✗ Failed to check parts: {e}", file=sys.stderr)
98
+ sys.exit(1)
99
+
100
+
101
+ def parts_require(client, part_id: str, skill_id: str, version: str,
102
+ required: bool) -> None:
103
+ """Add part dependency to skill."""
104
+ print(f"Adding part dependency...")
105
+ print(f" Part ID: {part_id}")
106
+ print(f" Skill ID: {skill_id}")
107
+ print(f" Min Version: {version}")
108
+ print(f" Required: {required}")
109
+
110
+ try:
111
+ response = client._request("POST", f"/parts/{part_id}/compatibility", json={
112
+ "skillId": skill_id,
113
+ "minVersion": version,
114
+ "required": required,
115
+ })
116
+
117
+ compat = response.get("compatibility", {})
118
+ print(f"\n✓ Part dependency added!")
119
+ print(f" Compatibility ID: {compat.get('id')}")
120
+
121
+ except Exception as e:
122
+ print(f"\n✗ Failed to add part dependency: {e}", file=sys.stderr)
123
+ sys.exit(1)
124
+
125
+
126
+ def register_parser(subparsers):
127
+ """Register parts commands with argparse."""
128
+ parts_parser = subparsers.add_parser("parts", help="Manage hardware parts catalog")
129
+ parts_subparsers = parts_parser.add_subparsers(dest="parts_action", help="Parts action")
130
+
131
+ # parts list
132
+ parts_list_parser = parts_subparsers.add_parser("list", help="List available parts")
133
+ parts_list_parser.add_argument("-c", "--category",
134
+ choices=["gripper", "sensor", "actuator", "controller",
135
+ "end-effector", "camera", "lidar", "force-torque"],
136
+ help="Filter by category")
137
+ parts_list_parser.add_argument("-m", "--manufacturer", help="Filter by manufacturer")
138
+ parts_list_parser.add_argument("-s", "--search", help="Search by name or part number")
139
+
140
+ # parts check
141
+ parts_check_parser = parts_subparsers.add_parser("check",
142
+ help="Check part compatibility for skill")
143
+ parts_check_parser.add_argument("skill_id", help="Skill ID to check")
144
+
145
+ # parts require
146
+ parts_require_parser = parts_subparsers.add_parser("require",
147
+ help="Add part dependency to skill")
148
+ parts_require_parser.add_argument("part_id", help="Part ID to require")
149
+ parts_require_parser.add_argument("-s", "--skill", required=True, help="Skill ID")
150
+ parts_require_parser.add_argument("-v", "--version", default="1.0.0",
151
+ help="Minimum version (default: 1.0.0)")
152
+ parts_require_parser.add_argument("--required", action="store_true",
153
+ help="Mark as required (not optional)")
154
+
155
+
156
+ def handle(client, args):
157
+ """Handle parts commands."""
158
+ if args.parts_action == "list":
159
+ parts_list(client, args.category, args.manufacturer, args.search)
160
+ elif args.parts_action == "check":
161
+ parts_check(client, args.skill_id)
162
+ elif args.parts_action == "require":
163
+ parts_require(client, args.part_id, args.skill, args.version, args.required)
164
+ else:
165
+ print("Usage: ate parts {list|check|require}")
166
+ sys.exit(1)
@@ -0,0 +1,399 @@
1
+ """
2
+ Primitive skills commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate primitive list - List primitive skills
6
+ - ate primitive get - Get primitive details
7
+ - ate primitive test - Submit test result for primitive
8
+ - ate primitive init - Initialize primitive skill template
9
+ - ate primitive push - Publish primitive to FoodforThought
10
+ - ate primitive deps - Manage primitive dependencies
11
+ """
12
+
13
+ import json
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+
20
+ def primitive_list(client, robot: Optional[str], category: Optional[str],
21
+ status: Optional[str], tested: bool) -> None:
22
+ """List primitive skills."""
23
+ print("Fetching primitives...")
24
+
25
+ params = {}
26
+ if robot:
27
+ params["robot"] = robot
28
+ if category:
29
+ params["category"] = category
30
+ if status:
31
+ params["status"] = status
32
+ if tested:
33
+ params["status"] = "tested,verified"
34
+
35
+ try:
36
+ response = client._request("GET", "/primitives", params=params)
37
+ primitives = response.get("primitives", [])
38
+
39
+ if not primitives:
40
+ print("\nNo primitives found.")
41
+ return
42
+
43
+ print(f"\n{'=' * 70}")
44
+ print(f"{'Name':<25} {'Category':<15} {'Status':<12} {'Reliability'}")
45
+ print(f"{'=' * 70}")
46
+
47
+ for prim in primitives:
48
+ name = prim.get("name", "")[:23]
49
+ cat = prim.get("category", "")[:13]
50
+ prim_status = prim.get("status", "")[:10]
51
+ reliability = prim.get("reliabilityScore", 0)
52
+ reliability_str = f"{reliability:.0%}" if reliability else "N/A"
53
+ print(f"{name:<25} {cat:<15} {prim_status:<12} {reliability_str}")
54
+
55
+ except Exception as e:
56
+ print(f"\n✗ Failed to list primitives: {e}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+
60
+ def primitive_get(client, primitive_id: str) -> None:
61
+ """Get detailed information about a primitive."""
62
+ try:
63
+ response = client._request("GET", f"/primitives/{primitive_id}")
64
+ prim = response.get("primitive", {})
65
+
66
+ print(f"\n{'=' * 60}")
67
+ print(f"Primitive: {prim.get('displayName') or prim.get('name')}")
68
+ print(f"{'=' * 60}")
69
+
70
+ print(f"\nCategory: {prim.get('category', '?')}")
71
+ print(f"Status: {prim.get('status', '?')}")
72
+
73
+ if prim.get('reliabilityScore'):
74
+ print(f"Reliability: {prim.get('reliabilityScore'):.1%}")
75
+
76
+ if prim.get('description'):
77
+ print(f"\nDescription: {prim.get('description')}")
78
+
79
+ print(f"\nCommand Type: {prim.get('commandType')}")
80
+ print(f"Command Template:")
81
+ print(f" {prim.get('commandTemplate')}")
82
+
83
+ # Parameters
84
+ params = prim.get('parameters', [])
85
+ if params:
86
+ print(f"\nParameters:")
87
+ for p in params:
88
+ range_str = ""
89
+ if p.get('min') is not None and p.get('max') is not None:
90
+ range_str = f" (range: {p.get('min')}-{p.get('max')})"
91
+ unit = f" {p.get('unit')}" if p.get('unit') else ""
92
+ default = f", default: {p.get('default')}" if p.get('default') is not None else ""
93
+ print(f" - {p.get('name')}: {p.get('type')}{range_str}{unit}{default}")
94
+
95
+ # Timing
96
+ if any([prim.get('executionTimeMs'), prim.get('settleTimeMs'), prim.get('cooldownMs')]):
97
+ print(f"\nTiming:")
98
+ if prim.get('executionTimeMs'):
99
+ print(f" Execution: {prim.get('executionTimeMs')}ms")
100
+ if prim.get('settleTimeMs'):
101
+ print(f" Settle: {prim.get('settleTimeMs')}ms")
102
+ if prim.get('cooldownMs'):
103
+ print(f" Cooldown: {prim.get('cooldownMs')}ms")
104
+
105
+ # Safety
106
+ if prim.get('safetyNotes'):
107
+ print(f"\nSafety Notes:")
108
+ print(f" {prim.get('safetyNotes')}")
109
+
110
+ except Exception as e:
111
+ print(f"\n✗ Failed to get primitive: {e}", file=sys.stderr)
112
+ sys.exit(1)
113
+
114
+
115
+ def primitive_test(client, primitive_id: str, params_json: str,
116
+ result: str, notes: Optional[str], video: Optional[str]) -> None:
117
+ """Submit a test result for a primitive skill."""
118
+ try:
119
+ parameters = json.loads(params_json)
120
+ except json.JSONDecodeError as e:
121
+ print(f"Error: Invalid JSON for parameters: {e}", file=sys.stderr)
122
+ sys.exit(1)
123
+
124
+ data = {
125
+ "parameters": parameters,
126
+ "result": result,
127
+ }
128
+ if notes:
129
+ data["resultNotes"] = notes
130
+ if video:
131
+ data["videoUrl"] = video
132
+
133
+ try:
134
+ response = client._request("POST", f"/primitives/{primitive_id}/test", json=data)
135
+ update = response.get("primitiveUpdate", {})
136
+
137
+ print(f"\n✓ Test result submitted!")
138
+ print(f" Result: {result}")
139
+ print(f" New Reliability Score: {update.get('reliabilityScore', 0):.1%}")
140
+
141
+ if update.get('statusChanged'):
142
+ print(f" Status upgraded to: {update.get('status')}")
143
+
144
+ except Exception as e:
145
+ print(f"\n✗ Failed to submit test: {e}", file=sys.stderr)
146
+ sys.exit(1)
147
+
148
+
149
+ def primitive_init(client, name: str, protocol_id: Optional[str],
150
+ from_recording: Optional[str], category: str, output: str) -> None:
151
+ """Initialize a new primitive skill definition locally."""
152
+ print(f"\n{'=' * 60}")
153
+ print("Initializing Primitive Skill")
154
+ print(f"{'=' * 60}\n")
155
+
156
+ out_path = Path(output)
157
+ out_path.mkdir(parents=True, exist_ok=True)
158
+
159
+ primitive_data = {
160
+ "name": name,
161
+ "displayName": name.replace("_", " ").title(),
162
+ "category": category,
163
+ "description": "",
164
+ "protocolId": protocol_id,
165
+ "commandType": "single",
166
+ "commandTemplate": "",
167
+ "responsePattern": "",
168
+ "parameters": [],
169
+ "executionTimeMs": None,
170
+ "settleTimeMs": None,
171
+ "cooldownMs": None,
172
+ "safetyNotes": "",
173
+ "status": "experimental",
174
+ "version": "1.0.0",
175
+ }
176
+
177
+ # If importing from a recording, populate command data
178
+ if from_recording:
179
+ recording_path = Path(from_recording)
180
+ if recording_path.exists():
181
+ with open(recording_path) as f:
182
+ recording = json.load(f)
183
+
184
+ commands = recording.get("commands", [])
185
+ if commands:
186
+ primitive_data["commandTemplate"] = commands[0].get("command", "")
187
+ if len(commands) > 1:
188
+ primitive_data["commandType"] = "sequence"
189
+ primitive_data["commandSequence"] = [
190
+ {"command": c.get("command"), "delayMs": int((c.get("timestamp", 0) * 1000))}
191
+ for c in commands
192
+ ]
193
+
194
+ responses = [c.get("response") for c in commands if c.get("response")]
195
+ if responses:
196
+ primitive_data["responsePattern"] = responses[0]
197
+
198
+ print(f"✓ Imported {len(commands)} commands from recording")
199
+ else:
200
+ print(f"Warning: Recording file not found: {from_recording}", file=sys.stderr)
201
+
202
+ # Write primitive file
203
+ primitive_file = out_path / f"{name}.primitive.json"
204
+ with open(primitive_file, "w") as f:
205
+ json.dump(primitive_data, f, indent=2)
206
+
207
+ print(f"✓ Created: {primitive_file}")
208
+ print(f"\nNext steps:")
209
+ print(f" 1. Edit {primitive_file} to define command template and parameters")
210
+ print(f" 2. Test with: ate primitive test <primitive_id> --params '{{...}}'")
211
+ print(f" 3. Publish with: ate primitive push {primitive_file}")
212
+
213
+
214
+ def primitive_push(client, primitive_file: str) -> None:
215
+ """Push a primitive skill definition to FoodforThought."""
216
+ file_path = Path(primitive_file)
217
+ if not file_path.exists():
218
+ print(f"Error: Primitive file not found: {primitive_file}", file=sys.stderr)
219
+ sys.exit(1)
220
+
221
+ print(f"\n{'=' * 60}")
222
+ print("Publishing Primitive Skill")
223
+ print(f"{'=' * 60}\n")
224
+
225
+ with open(file_path) as f:
226
+ primitive_data = json.load(f)
227
+
228
+ print(f"Name: {primitive_data.get('displayName') or primitive_data.get('name')}")
229
+ print(f"Category: {primitive_data.get('category')}")
230
+
231
+ # Validate required fields
232
+ if not primitive_data.get("name"):
233
+ print("Error: Primitive must have a name", file=sys.stderr)
234
+ sys.exit(1)
235
+
236
+ if not primitive_data.get("commandTemplate") and not primitive_data.get("commandSequence"):
237
+ print("Error: Primitive must have a commandTemplate or commandSequence", file=sys.stderr)
238
+ sys.exit(1)
239
+
240
+ try:
241
+ response = client._request("POST", "/primitives", json=primitive_data)
242
+ prim = response.get("primitive", {})
243
+ print(f"\n✓ Primitive published successfully!")
244
+ print(f" ID: {prim.get('id')}")
245
+ print(f" Status: {prim.get('status')}")
246
+ except Exception as e:
247
+ print(f"\n✗ Failed to publish: {e}", file=sys.stderr)
248
+ sys.exit(1)
249
+
250
+
251
+ def primitive_deps_show(client, primitive_id: str) -> None:
252
+ """Show dependencies for a primitive skill."""
253
+ try:
254
+ response = client._request("GET", f"/primitives/{primitive_id}/dependencies")
255
+ deps = response.get("dependencies", [])
256
+ dependents = response.get("dependents", [])
257
+ deployment_ready = response.get("deploymentReady", False)
258
+
259
+ print(f"\n{'=' * 60}")
260
+ print("Dependency Graph")
261
+ print(f"{'=' * 60}")
262
+
263
+ print(f"\nDeployment Ready: {'✓ Yes' if deployment_ready else '✗ No'}")
264
+
265
+ if deps:
266
+ print(f"\nDepends On ({len(deps)}):")
267
+ for dep in deps:
268
+ req = dep.get("requiredSkill", {})
269
+ status_ok = req.get("status") in ["tested", "verified"]
270
+ icon = "✓" if status_ok else "✗"
271
+ print(f" {icon} {req.get('name')} ({req.get('status')})")
272
+ else:
273
+ print(f"\nNo dependencies (this is a root primitive)")
274
+
275
+ if dependents:
276
+ print(f"\nRequired By ({len(dependents)}):")
277
+ for dep in dependents:
278
+ skill = dep.get("dependentSkill", {})
279
+ print(f" - {skill.get('name')}")
280
+
281
+ except Exception as e:
282
+ print(f"\n✗ Failed to fetch dependencies: {e}", file=sys.stderr)
283
+ sys.exit(1)
284
+
285
+
286
+ def primitive_deps_add(client, primitive_id: str, required_id: str,
287
+ dependency_type: str, min_status: str) -> None:
288
+ """Add a dependency to a primitive skill."""
289
+ data = {
290
+ "requiredSkillId": required_id,
291
+ "dependencyType": dependency_type,
292
+ "requiredMinStatus": min_status,
293
+ }
294
+
295
+ try:
296
+ response = client._request("POST", f"/primitives/{primitive_id}/dependencies", json=data)
297
+ cross_robot = response.get("crossRobot", False)
298
+
299
+ print(f"\n✓ Dependency added!")
300
+ if cross_robot:
301
+ print(f" ⚠ Note: This is a cross-robot dependency")
302
+
303
+ except Exception as e:
304
+ print(f"\n✗ Failed to add dependency: {e}", file=sys.stderr)
305
+ sys.exit(1)
306
+
307
+
308
+ def register_parser(subparsers):
309
+ """Register primitive commands with argparse."""
310
+ primitive_parser = subparsers.add_parser("primitive", help="Manage primitive skills")
311
+ primitive_subparsers = primitive_parser.add_subparsers(dest="primitive_action", help="Primitive action")
312
+
313
+ # primitive list
314
+ primitive_list_parser = primitive_subparsers.add_parser("list", help="List primitive skills")
315
+ primitive_list_parser.add_argument("-r", "--robot", help="Filter by robot model")
316
+ primitive_list_parser.add_argument("-c", "--category",
317
+ choices=["body_pose", "arm", "gripper", "locomotion",
318
+ "head", "sensing", "manipulation", "navigation"],
319
+ help="Filter by category")
320
+ primitive_list_parser.add_argument("--status",
321
+ choices=["experimental", "tested", "verified", "deprecated"],
322
+ help="Filter by status")
323
+ primitive_list_parser.add_argument("--tested", action="store_true",
324
+ help="Show only tested/verified primitives")
325
+
326
+ # primitive get
327
+ primitive_get_parser = primitive_subparsers.add_parser("get", help="Get primitive details")
328
+ primitive_get_parser.add_argument("primitive_id", help="Primitive ID")
329
+
330
+ # primitive test
331
+ primitive_test_parser = primitive_subparsers.add_parser("test", help="Submit test result")
332
+ primitive_test_parser.add_argument("primitive_id", help="Primitive ID to test")
333
+ primitive_test_parser.add_argument("-p", "--params", required=True,
334
+ help="Parameters used in test as JSON")
335
+ primitive_test_parser.add_argument("-r", "--result", required=True,
336
+ choices=["pass", "fail", "partial"],
337
+ help="Test result")
338
+ primitive_test_parser.add_argument("-n", "--notes", help="Test notes")
339
+ primitive_test_parser.add_argument("-v", "--video", help="Video URL of test")
340
+
341
+ # primitive deps
342
+ primitive_deps_parser = primitive_subparsers.add_parser("deps", help="Manage primitive dependencies")
343
+ primitive_deps_subparsers = primitive_deps_parser.add_subparsers(dest="primitive_deps_action",
344
+ help="Dependency action")
345
+
346
+ # primitive deps show
347
+ primitive_deps_show_parser = primitive_deps_subparsers.add_parser("show", help="Show dependencies")
348
+ primitive_deps_show_parser.add_argument("primitive_id", help="Primitive ID")
349
+
350
+ # primitive deps add
351
+ primitive_deps_add_parser = primitive_deps_subparsers.add_parser("add", help="Add dependency")
352
+ primitive_deps_add_parser.add_argument("primitive_id", help="Primitive ID (the one that depends)")
353
+ primitive_deps_add_parser.add_argument("required_id", help="Required primitive ID")
354
+ primitive_deps_add_parser.add_argument("-t", "--type", default="requires",
355
+ choices=["requires", "extends", "overrides", "optional"],
356
+ help="Dependency type (default: requires)")
357
+ primitive_deps_add_parser.add_argument("--min-status", default="tested",
358
+ choices=["experimental", "tested", "verified"],
359
+ help="Minimum required status (default: tested)")
360
+
361
+ # primitive init
362
+ primitive_init_parser = primitive_subparsers.add_parser("init", help="Initialize primitive skill template")
363
+ primitive_init_parser.add_argument("name", help="Primitive name (e.g., move_joint, grip_close)")
364
+ primitive_init_parser.add_argument("-p", "--protocol", help="Protocol ID to link to")
365
+ primitive_init_parser.add_argument("-r", "--from-recording", help="Import from recording file")
366
+ primitive_init_parser.add_argument("-c", "--category", default="motion",
367
+ choices=["body_pose", "arm", "gripper", "locomotion",
368
+ "head", "sensing", "manipulation", "navigation"],
369
+ help="Primitive category (default: motion)")
370
+ primitive_init_parser.add_argument("-o", "--output", default=".", help="Output directory")
371
+
372
+ # primitive push
373
+ primitive_push_parser = primitive_subparsers.add_parser("push", help="Publish primitive to FoodforThought")
374
+ primitive_push_parser.add_argument("primitive_file", help="Path to .primitive.json file")
375
+
376
+
377
+ def handle(client, args):
378
+ """Handle primitive commands."""
379
+ if args.primitive_action == "list":
380
+ primitive_list(client, args.robot, args.category, args.status, args.tested)
381
+ elif args.primitive_action == "get":
382
+ primitive_get(client, args.primitive_id)
383
+ elif args.primitive_action == "test":
384
+ primitive_test(client, args.primitive_id, args.params, args.result, args.notes, args.video)
385
+ elif args.primitive_action == "init":
386
+ primitive_init(client, args.name, args.protocol, args.from_recording, args.category, args.output)
387
+ elif args.primitive_action == "push":
388
+ primitive_push(client, args.primitive_file)
389
+ elif args.primitive_action == "deps":
390
+ if args.primitive_deps_action == "show":
391
+ primitive_deps_show(client, args.primitive_id)
392
+ elif args.primitive_deps_action == "add":
393
+ primitive_deps_add(client, args.primitive_id, args.required_id, args.type, args.min_status)
394
+ else:
395
+ print("Usage: ate primitive deps {show|add}")
396
+ sys.exit(1)
397
+ else:
398
+ print("Usage: ate primitive {list|get|test|init|push|deps}")
399
+ sys.exit(1)