foodforthought-cli 0.2.0__py3-none-any.whl → 0.2.3__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.
- ate/__init__.py +1 -1
- ate/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +2424 -148
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.0.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.0.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.0.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.0.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
ate/marketplace.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Marketplace commands for the FoodforThought CLI.
|
|
3
|
+
Provides npm-like interface for discovering, installing, and publishing robot skills.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import zipfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, List, Dict, Any
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import requests
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.progress import Progress
|
|
20
|
+
from rich.markdown import Markdown
|
|
21
|
+
except ImportError:
|
|
22
|
+
print("Error: Required packages not installed. Run: pip install rich requests")
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
# API configuration
|
|
28
|
+
BASE_URL = os.getenv("ATE_API_URL", "https://kindly.fyi/api")
|
|
29
|
+
API_KEY = os.getenv("ATE_API_KEY", "")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MarketplaceClient:
|
|
33
|
+
"""Client for interacting with the Skill Marketplace API."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, base_url: str = BASE_URL, api_key: str = API_KEY):
|
|
36
|
+
self.base_url = base_url
|
|
37
|
+
self.headers = {
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}
|
|
40
|
+
if api_key:
|
|
41
|
+
self.headers["Authorization"] = f"Bearer {api_key}"
|
|
42
|
+
|
|
43
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
44
|
+
"""Make HTTP request to API."""
|
|
45
|
+
url = f"{self.base_url}{endpoint}"
|
|
46
|
+
try:
|
|
47
|
+
if method.upper() == "GET" and "params" in kwargs:
|
|
48
|
+
response = requests.get(url, headers=self.headers, params=kwargs["params"])
|
|
49
|
+
else:
|
|
50
|
+
response = requests.request(method, url, headers=self.headers, **kwargs)
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
return response.json()
|
|
53
|
+
except requests.exceptions.HTTPError as e:
|
|
54
|
+
try:
|
|
55
|
+
error_data = e.response.json()
|
|
56
|
+
console.print(f"[red]Error: {error_data.get('error', str(e))}[/red]")
|
|
57
|
+
except Exception:
|
|
58
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
except requests.exceptions.RequestException as e:
|
|
61
|
+
console.print(f"[red]Network error: {e}[/red]")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
65
|
+
return self._request("GET", endpoint, **kwargs)
|
|
66
|
+
|
|
67
|
+
def post(self, endpoint: str, data: Dict = None, **kwargs) -> Dict[str, Any]:
|
|
68
|
+
return self._request("POST", endpoint, json=data, **kwargs)
|
|
69
|
+
|
|
70
|
+
def put(self, endpoint: str, data: Dict = None, **kwargs) -> Dict[str, Any]:
|
|
71
|
+
return self._request("PUT", endpoint, json=data, **kwargs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Singleton client
|
|
75
|
+
_client: Optional[MarketplaceClient] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_client() -> MarketplaceClient:
|
|
79
|
+
global _client
|
|
80
|
+
if _client is None:
|
|
81
|
+
_client = MarketplaceClient()
|
|
82
|
+
return _client
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def search_skills(
|
|
86
|
+
query: str,
|
|
87
|
+
category: Optional[str] = None,
|
|
88
|
+
robot_type: Optional[str] = None,
|
|
89
|
+
license_type: Optional[str] = None,
|
|
90
|
+
pricing: Optional[str] = None,
|
|
91
|
+
sort: str = "downloads",
|
|
92
|
+
limit: int = 20,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Search the skill marketplace.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
ate marketplace search "pick and place"
|
|
99
|
+
ate marketplace search gripper --category manipulation
|
|
100
|
+
ate marketplace search navigation --robot ur5
|
|
101
|
+
"""
|
|
102
|
+
client = get_client()
|
|
103
|
+
|
|
104
|
+
params = {"q": query, "sort": sort, "limit": limit}
|
|
105
|
+
if category:
|
|
106
|
+
params["category"] = category
|
|
107
|
+
if robot_type:
|
|
108
|
+
params["robotType"] = robot_type
|
|
109
|
+
if license_type:
|
|
110
|
+
params["license"] = license_type
|
|
111
|
+
if pricing:
|
|
112
|
+
params["pricing"] = pricing
|
|
113
|
+
|
|
114
|
+
with console.status("Searching marketplace..."):
|
|
115
|
+
result = client.get("/marketplace/skills", params=params)
|
|
116
|
+
|
|
117
|
+
skills = result.get("skills", [])
|
|
118
|
+
pagination = result.get("pagination", {})
|
|
119
|
+
|
|
120
|
+
if not skills:
|
|
121
|
+
console.print(f"[yellow]No skills found matching '{query}'[/yellow]")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
table = Table(title=f"Skills matching '{query}' ({pagination.get('total', 0)} total)")
|
|
125
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
126
|
+
table.add_column("Author", style="dim")
|
|
127
|
+
table.add_column("Category")
|
|
128
|
+
table.add_column("Downloads", justify="right")
|
|
129
|
+
table.add_column("Rating", justify="right")
|
|
130
|
+
table.add_column("License", style="dim")
|
|
131
|
+
|
|
132
|
+
for skill in skills:
|
|
133
|
+
rating = skill.get("avgRating", 0)
|
|
134
|
+
rating_str = f"{rating:.1f}" if rating > 0 else "—"
|
|
135
|
+
verified = "" if not skill.get("verified") else " "
|
|
136
|
+
|
|
137
|
+
table.add_row(
|
|
138
|
+
f"{skill['name']}{verified}",
|
|
139
|
+
skill.get("author", {}).get("name", "Unknown"),
|
|
140
|
+
skill.get("category", "—"),
|
|
141
|
+
str(skill.get("downloads", 0)),
|
|
142
|
+
rating_str,
|
|
143
|
+
skill.get("license", "—").upper(),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
console.print(table)
|
|
147
|
+
|
|
148
|
+
if pagination.get("hasMore"):
|
|
149
|
+
console.print(
|
|
150
|
+
f"\n[dim]Showing {len(skills)} of {pagination.get('total')} results. "
|
|
151
|
+
f"Use --limit to see more.[/dim]"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def show_skill(slug: str) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Show detailed information about a skill.
|
|
158
|
+
|
|
159
|
+
Examples:
|
|
160
|
+
ate marketplace show pick-and-place
|
|
161
|
+
"""
|
|
162
|
+
client = get_client()
|
|
163
|
+
|
|
164
|
+
with console.status(f"Fetching skill '{slug}'..."):
|
|
165
|
+
result = client.get(f"/marketplace/skills/{slug}")
|
|
166
|
+
|
|
167
|
+
skill = result.get("skill", {})
|
|
168
|
+
|
|
169
|
+
if not skill:
|
|
170
|
+
console.print(f"[red]Skill '{slug}' not found[/red]")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Header
|
|
174
|
+
verified = "" if not skill.get("verified") else ""
|
|
175
|
+
console.print(Panel(
|
|
176
|
+
f"[bold cyan]{skill['name']}[/bold cyan] {verified}\n"
|
|
177
|
+
f"[dim]by {skill.get('author', {}).get('name', 'Unknown')}[/dim]\n\n"
|
|
178
|
+
f"{skill.get('description', 'No description')}",
|
|
179
|
+
title=f"v{skill.get('version', '?')}",
|
|
180
|
+
subtitle=skill.get('category', ''),
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
# Stats table
|
|
184
|
+
stats_table = Table(show_header=False, box=None)
|
|
185
|
+
stats_table.add_column("Key", style="dim")
|
|
186
|
+
stats_table.add_column("Value")
|
|
187
|
+
|
|
188
|
+
rating = skill.get("avgRating", 0)
|
|
189
|
+
stats_table.add_row("Downloads", str(skill.get("downloads", 0)))
|
|
190
|
+
stats_table.add_row("Installs", str(skill.get("installs", 0)))
|
|
191
|
+
stats_table.add_row("Executions", str(skill.get("executions", 0)))
|
|
192
|
+
stats_table.add_row("Success Rate", f"{skill.get('successRate', 0) * 100:.1f}%")
|
|
193
|
+
stats_table.add_row("Rating", f"{rating:.1f}" if rating > 0 else "No ratings")
|
|
194
|
+
stats_table.add_row("License", skill.get("license", "—").upper())
|
|
195
|
+
|
|
196
|
+
# Pricing
|
|
197
|
+
pricing = skill.get("pricing", {})
|
|
198
|
+
if pricing.get("type") == "free":
|
|
199
|
+
stats_table.add_row("Price", "[green]Free[/green]")
|
|
200
|
+
else:
|
|
201
|
+
price = pricing.get("price", 0)
|
|
202
|
+
currency = pricing.get("currency", "usd").upper()
|
|
203
|
+
stats_table.add_row("Price", f"${price:.2f} {currency}")
|
|
204
|
+
|
|
205
|
+
console.print(stats_table)
|
|
206
|
+
|
|
207
|
+
# Compatibility
|
|
208
|
+
compatibility = skill.get("compatibility", [])
|
|
209
|
+
if compatibility:
|
|
210
|
+
console.print("\n[bold]Compatible Robots:[/bold]")
|
|
211
|
+
for compat in compatibility[:5]:
|
|
212
|
+
robot = compat.get("robot", {})
|
|
213
|
+
success = compat.get("successRate", 0)
|
|
214
|
+
console.print(
|
|
215
|
+
f" • {robot.get('name', 'Unknown')} "
|
|
216
|
+
f"({robot.get('manufacturer', '')}) - "
|
|
217
|
+
f"[green]{success * 100:.0f}% success[/green]"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Links
|
|
221
|
+
console.print("\n[bold]Links:[/bold]")
|
|
222
|
+
if skill.get("sourceUrl"):
|
|
223
|
+
console.print(f" Source: {skill['sourceUrl']}")
|
|
224
|
+
if skill.get("documentationUrl"):
|
|
225
|
+
console.print(f" Docs: {skill['documentationUrl']}")
|
|
226
|
+
console.print(f" Web: https://foodforthought.kindly.fyi/marketplace/{slug}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def install_skill(
|
|
230
|
+
skill_name: str,
|
|
231
|
+
version: Optional[str] = None,
|
|
232
|
+
robot: Optional[str] = None,
|
|
233
|
+
output_dir: Optional[str] = None,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""
|
|
236
|
+
Install a skill from the marketplace.
|
|
237
|
+
|
|
238
|
+
Examples:
|
|
239
|
+
ate marketplace install pick-and-place
|
|
240
|
+
ate marketplace install pick-and-place --version 1.2.0
|
|
241
|
+
ate marketplace install pick-and-place --robot my-arm
|
|
242
|
+
"""
|
|
243
|
+
client = get_client()
|
|
244
|
+
|
|
245
|
+
# Check compatibility first if robot specified
|
|
246
|
+
if robot:
|
|
247
|
+
console.print(f"Checking compatibility with {robot}...")
|
|
248
|
+
compat = client.post("/marketplace/compatibility", {
|
|
249
|
+
"skillSlug": skill_name,
|
|
250
|
+
"robotId": robot,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if not compat.get("compatible"):
|
|
254
|
+
console.print(f"[red]Skill is not compatible with robot {robot}[/red]")
|
|
255
|
+
for issue in compat.get("issues", []):
|
|
256
|
+
console.print(f" • {issue}")
|
|
257
|
+
|
|
258
|
+
adaptations = compat.get("adaptations", [])
|
|
259
|
+
if adaptations:
|
|
260
|
+
console.print("\n[yellow]Possible adaptations:[/yellow]")
|
|
261
|
+
for adapt in adaptations:
|
|
262
|
+
console.print(f" • {adapt.get('description')}")
|
|
263
|
+
|
|
264
|
+
if not console.input("\nInstall anyway? (y/N): ").lower() == "y":
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# Install
|
|
268
|
+
with console.status("Installing skill..."):
|
|
269
|
+
result = client.post(f"/marketplace/skills/{skill_name}/install", {
|
|
270
|
+
"version": version,
|
|
271
|
+
"robotId": robot,
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
if result.get("requiresPayment"):
|
|
275
|
+
console.print("[yellow]This is a paid skill.[/yellow]")
|
|
276
|
+
console.print(f"Price: ${result['pricing'].get('price', 0):.2f}")
|
|
277
|
+
console.print(f"Checkout: {result.get('checkoutUrl')}")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
download_url = result.get("downloadUrl")
|
|
281
|
+
installed_version = result.get("version")
|
|
282
|
+
|
|
283
|
+
if not download_url:
|
|
284
|
+
console.print("[red]Failed to get download URL[/red]")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
# Download package
|
|
288
|
+
output_path = Path(output_dir or f"./{skill_name}")
|
|
289
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
|
|
291
|
+
with Progress() as progress:
|
|
292
|
+
task = progress.add_task("Downloading...", total=100)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
response = requests.get(download_url, stream=True)
|
|
296
|
+
response.raise_for_status()
|
|
297
|
+
|
|
298
|
+
# Save as zip and extract
|
|
299
|
+
zip_path = output_path / f"{skill_name}.zip"
|
|
300
|
+
with open(zip_path, "wb") as f:
|
|
301
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
302
|
+
f.write(chunk)
|
|
303
|
+
progress.update(task, completed=50)
|
|
304
|
+
|
|
305
|
+
# Extract
|
|
306
|
+
with zipfile.ZipFile(zip_path, "r") as z:
|
|
307
|
+
z.extractall(output_path)
|
|
308
|
+
progress.update(task, completed=100)
|
|
309
|
+
|
|
310
|
+
# Clean up zip
|
|
311
|
+
zip_path.unlink()
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
console.print(f"[red]Download failed: {e}[/red]")
|
|
315
|
+
# Still mark as installed since the installation record was created
|
|
316
|
+
console.print("[yellow]Skill recorded as installed but download failed.[/yellow]")
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
console.print(f"[green] Installed {skill_name} v{installed_version}[/green]")
|
|
320
|
+
console.print(f"Location: {output_path.absolute()}")
|
|
321
|
+
|
|
322
|
+
# Show compatibility info
|
|
323
|
+
compat_info = result.get("compatibility")
|
|
324
|
+
if compat_info:
|
|
325
|
+
if compat_info.get("status") == "untested":
|
|
326
|
+
console.print(
|
|
327
|
+
"[yellow]Note: This skill hasn't been tested on your robot. "
|
|
328
|
+
"Please report compatibility after testing.[/yellow]"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def publish_skill(
|
|
333
|
+
path: str,
|
|
334
|
+
public: bool = True,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Publish a skill to the marketplace.
|
|
338
|
+
|
|
339
|
+
The skill directory must contain a skill.yaml file with metadata.
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
ate marketplace publish ./my-skill
|
|
343
|
+
ate marketplace publish ./my-skill --no-public
|
|
344
|
+
"""
|
|
345
|
+
skill_path = Path(path)
|
|
346
|
+
|
|
347
|
+
if not skill_path.exists():
|
|
348
|
+
console.print(f"[red]Path not found: {path}[/red]")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# Look for skill.yaml
|
|
352
|
+
config_path = skill_path / "skill.yaml"
|
|
353
|
+
if not config_path.exists():
|
|
354
|
+
config_path = skill_path / "skill.yml"
|
|
355
|
+
if not config_path.exists():
|
|
356
|
+
console.print("[red]skill.yaml not found in skill directory[/red]")
|
|
357
|
+
console.print("\nCreate a skill.yaml with:")
|
|
358
|
+
console.print("""
|
|
359
|
+
name: my-skill
|
|
360
|
+
version: 1.0.0
|
|
361
|
+
description: A brief description
|
|
362
|
+
category: manipulation
|
|
363
|
+
tags:
|
|
364
|
+
- gripper
|
|
365
|
+
- pick-and-place
|
|
366
|
+
license: mit
|
|
367
|
+
""")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# Parse config
|
|
371
|
+
try:
|
|
372
|
+
import yaml
|
|
373
|
+
with open(config_path) as f:
|
|
374
|
+
config = yaml.safe_load(f)
|
|
375
|
+
except ImportError:
|
|
376
|
+
console.print("[yellow]PyYAML not installed, trying JSON fallback...[/yellow]")
|
|
377
|
+
# Try JSON fallback
|
|
378
|
+
json_path = skill_path / "skill.json"
|
|
379
|
+
if json_path.exists():
|
|
380
|
+
with open(json_path) as f:
|
|
381
|
+
config = json.load(f)
|
|
382
|
+
else:
|
|
383
|
+
console.print("[red]Install PyYAML or use skill.json[/red]")
|
|
384
|
+
return
|
|
385
|
+
except Exception as e:
|
|
386
|
+
console.print(f"[red]Failed to parse config: {e}[/red]")
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
# Validate required fields
|
|
390
|
+
required = ["name", "version", "description", "category"]
|
|
391
|
+
missing = [f for f in required if not config.get(f)]
|
|
392
|
+
if missing:
|
|
393
|
+
console.print(f"[red]Missing required fields: {', '.join(missing)}[/red]")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
console.print(f"Publishing [cyan]{config['name']}[/cyan] v{config['version']}...")
|
|
397
|
+
|
|
398
|
+
# Create package zip
|
|
399
|
+
console.print("Creating package...")
|
|
400
|
+
zip_path = skill_path.parent / f"{config['name']}-{config['version']}.zip"
|
|
401
|
+
|
|
402
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
|
|
403
|
+
for file in skill_path.rglob("*"):
|
|
404
|
+
if file.is_file():
|
|
405
|
+
# Skip common non-essential files
|
|
406
|
+
if file.name.startswith(".") or file.suffix in [".pyc", ".pyo"]:
|
|
407
|
+
continue
|
|
408
|
+
if "__pycache__" in str(file):
|
|
409
|
+
continue
|
|
410
|
+
arcname = file.relative_to(skill_path)
|
|
411
|
+
z.write(file, arcname)
|
|
412
|
+
|
|
413
|
+
# Upload package (placeholder - would upload to Vercel Blob or S3)
|
|
414
|
+
console.print("Uploading package...")
|
|
415
|
+
# In production, this would upload to cloud storage
|
|
416
|
+
# For now, we'll just use a placeholder URL
|
|
417
|
+
package_url = f"https://storage.kindly.fyi/skills/{config['name']}/{config['version']}/package.zip"
|
|
418
|
+
|
|
419
|
+
# Clean up local zip
|
|
420
|
+
zip_path.unlink()
|
|
421
|
+
|
|
422
|
+
# Read README if exists
|
|
423
|
+
readme = None
|
|
424
|
+
readme_path = skill_path / "README.md"
|
|
425
|
+
if readme_path.exists():
|
|
426
|
+
readme = readme_path.read_text()
|
|
427
|
+
|
|
428
|
+
# Publish to API
|
|
429
|
+
client = get_client()
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
result = client.post("/marketplace/skills", {
|
|
433
|
+
"name": config["name"],
|
|
434
|
+
"version": config["version"],
|
|
435
|
+
"description": config["description"],
|
|
436
|
+
"readme": readme,
|
|
437
|
+
"category": config.get("category", "other"),
|
|
438
|
+
"tags": config.get("tags", []),
|
|
439
|
+
"robotTypes": config.get("robotTypes", []),
|
|
440
|
+
"hardwareRequirements": config.get("hardwareRequirements", []),
|
|
441
|
+
"softwareRequirements": config.get("softwareRequirements", []),
|
|
442
|
+
"packageUrl": package_url,
|
|
443
|
+
"sourceUrl": config.get("sourceUrl"),
|
|
444
|
+
"documentationUrl": config.get("documentationUrl"),
|
|
445
|
+
"license": config.get("license", "mit"),
|
|
446
|
+
"pricing": config.get("pricing", {"type": "free"}),
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
skill = result.get("skill", {})
|
|
450
|
+
console.print(f"[green] Published {skill['name']} v{skill['version']}[/green]")
|
|
451
|
+
console.print(f"Status: {skill.get('status', 'pending_review')}")
|
|
452
|
+
console.print(f"URL: https://foodforthought.kindly.fyi/marketplace/{skill['slug']}")
|
|
453
|
+
|
|
454
|
+
if skill.get("status") == "pending_review":
|
|
455
|
+
console.print(
|
|
456
|
+
"\n[yellow]Your skill is pending review. "
|
|
457
|
+
"It will be published once approved.[/yellow]"
|
|
458
|
+
)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
console.print(f"[red]Failed to publish: {e}[/red]")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def report_compatibility(
|
|
464
|
+
skill_name: str,
|
|
465
|
+
robot: str,
|
|
466
|
+
works: bool,
|
|
467
|
+
notes: Optional[str] = None,
|
|
468
|
+
version: Optional[str] = None,
|
|
469
|
+
) -> None:
|
|
470
|
+
"""
|
|
471
|
+
Report skill compatibility with a robot.
|
|
472
|
+
|
|
473
|
+
Examples:
|
|
474
|
+
ate marketplace report pick-and-place my-arm --works
|
|
475
|
+
ate marketplace report pick-and-place my-arm --no-works --notes "Gripper too weak"
|
|
476
|
+
"""
|
|
477
|
+
client = get_client()
|
|
478
|
+
|
|
479
|
+
result = client.put("/marketplace/compatibility", {
|
|
480
|
+
"skillSlug": skill_name,
|
|
481
|
+
"robotId": robot,
|
|
482
|
+
"works": works,
|
|
483
|
+
"notes": notes,
|
|
484
|
+
"version": version,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
stats = result.get("communityStats", {})
|
|
488
|
+
console.print("[green] Compatibility report submitted. Thank you![/green]")
|
|
489
|
+
console.print(
|
|
490
|
+
f"\nCommunity stats for this robot: "
|
|
491
|
+
f"{stats.get('workingReports', 0)}/{stats.get('totalReports', 0)} working "
|
|
492
|
+
f"({stats.get('successRate', 0) * 100:.0f}% success rate)"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def list_installed() -> None:
|
|
497
|
+
"""List all installed skills."""
|
|
498
|
+
client = get_client()
|
|
499
|
+
|
|
500
|
+
# Get user's installations
|
|
501
|
+
result = client.get("/marketplace/skills", params={"installed": "true"})
|
|
502
|
+
|
|
503
|
+
skills = result.get("skills", [])
|
|
504
|
+
|
|
505
|
+
if not skills:
|
|
506
|
+
console.print("[yellow]No skills installed[/yellow]")
|
|
507
|
+
console.print("Use 'ate marketplace search' to find skills to install.")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
table = Table(title="Installed Skills")
|
|
511
|
+
table.add_column("Name", style="cyan")
|
|
512
|
+
table.add_column("Version")
|
|
513
|
+
table.add_column("Category")
|
|
514
|
+
table.add_column("Installed")
|
|
515
|
+
|
|
516
|
+
for skill in skills:
|
|
517
|
+
table.add_row(
|
|
518
|
+
skill["name"],
|
|
519
|
+
skill["version"],
|
|
520
|
+
skill.get("category", "—"),
|
|
521
|
+
skill.get("installedAt", "—"),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
console.print(table)
|