foodforthought-cli 0.1.1__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/mcp_server.py ADDED
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FoodforThought MCP Server - Model Context Protocol server for Cursor IDE
4
+ Exposes FoodforThought CLI capabilities as MCP tools
5
+
6
+ Installation:
7
+ pip install -r requirements-mcp.txt
8
+
9
+ Usage:
10
+ python -m ate.mcp_server
11
+
12
+ Or configure in Cursor's mcp.json:
13
+ {
14
+ "mcpServers": {
15
+ "foodforthought": {
16
+ "command": "python",
17
+ "args": ["-m", "ate.mcp_server"],
18
+ "env": {
19
+ "ATE_API_URL": "https://kindly.fyi/api",
20
+ "ATE_API_KEY": "${env:ATE_API_KEY}"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ """
26
+
27
+ import asyncio
28
+ import json
29
+ import os
30
+ import sys
31
+ from typing import Any, Dict, List, Optional
32
+
33
+ # Import the existing CLI client
34
+ from ate.cli import ATEClient
35
+
36
+ # MCP SDK imports - using standard MCP Python SDK pattern
37
+ try:
38
+ from mcp.server import Server
39
+ from mcp.server.stdio import stdio_server
40
+ from mcp.types import (
41
+ Tool,
42
+ TextContent,
43
+ Resource,
44
+ Prompt,
45
+ PromptArgument,
46
+ )
47
+ except ImportError:
48
+ try:
49
+ # Alternative import path for some MCP SDK versions
50
+ from mcp import Server, stdio_server
51
+ from mcp.types import Tool, TextContent, Resource, Prompt, PromptArgument
52
+ except ImportError:
53
+ # Fallback if MCP SDK not available - provide helpful error
54
+ print(
55
+ "Error: MCP SDK not installed. Install with: pip install mcp",
56
+ file=sys.stderr,
57
+ )
58
+ sys.exit(1)
59
+
60
+ # Initialize MCP server
61
+ server = Server("foodforthought")
62
+
63
+ # Initialize ATE client
64
+ client = ATEClient()
65
+
66
+
67
+ @server.list_tools()
68
+ async def list_tools() -> List[Tool]:
69
+ """List all available MCP tools"""
70
+ return [
71
+ Tool(
72
+ name="ate_init",
73
+ description="Initialize a new FoodforThought repository",
74
+ inputSchema={
75
+ "type": "object",
76
+ "properties": {
77
+ "name": {
78
+ "type": "string",
79
+ "description": "Repository name",
80
+ },
81
+ "description": {
82
+ "type": "string",
83
+ "description": "Repository description",
84
+ },
85
+ "visibility": {
86
+ "type": "string",
87
+ "enum": ["public", "private"],
88
+ "description": "Repository visibility",
89
+ "default": "public",
90
+ },
91
+ },
92
+ "required": ["name"],
93
+ },
94
+ ),
95
+ Tool(
96
+ name="ate_clone",
97
+ description="Clone a FoodforThought repository to local directory",
98
+ inputSchema={
99
+ "type": "object",
100
+ "properties": {
101
+ "repo_id": {
102
+ "type": "string",
103
+ "description": "Repository ID to clone",
104
+ },
105
+ "target_dir": {
106
+ "type": "string",
107
+ "description": "Target directory (optional)",
108
+ },
109
+ },
110
+ "required": ["repo_id"],
111
+ },
112
+ ),
113
+ Tool(
114
+ name="ate_list_repositories",
115
+ description="List available FoodforThought repositories",
116
+ inputSchema={
117
+ "type": "object",
118
+ "properties": {
119
+ "search": {
120
+ "type": "string",
121
+ "description": "Search query",
122
+ },
123
+ "robot_model": {
124
+ "type": "string",
125
+ "description": "Filter by robot model",
126
+ },
127
+ "limit": {
128
+ "type": "number",
129
+ "description": "Maximum number of results",
130
+ "default": 20,
131
+ },
132
+ },
133
+ },
134
+ ),
135
+ Tool(
136
+ name="ate_list_robots",
137
+ description="List available robot profiles",
138
+ inputSchema={
139
+ "type": "object",
140
+ "properties": {
141
+ "search": {
142
+ "type": "string",
143
+ "description": "Search query",
144
+ },
145
+ "category": {
146
+ "type": "string",
147
+ "description": "Filter by category",
148
+ },
149
+ "limit": {
150
+ "type": "number",
151
+ "description": "Maximum number of results",
152
+ "default": 20,
153
+ },
154
+ },
155
+ },
156
+ ),
157
+ Tool(
158
+ name="ate_compatibility",
159
+ description="Check skill compatibility between two robot models",
160
+ inputSchema={
161
+ "type": "object",
162
+ "properties": {
163
+ "source_robot_id": {
164
+ "type": "string",
165
+ "description": "Source robot ID",
166
+ },
167
+ "target_robot_id": {
168
+ "type": "string",
169
+ "description": "Target robot ID",
170
+ },
171
+ "repository_id": {
172
+ "type": "string",
173
+ "description": "Repository ID to check compatibility for",
174
+ },
175
+ },
176
+ "required": ["source_robot_id", "target_robot_id", "repository_id"],
177
+ },
178
+ ),
179
+ Tool(
180
+ name="ate_adapt",
181
+ description="Generate adaptation plan for transferring skills between robots",
182
+ inputSchema={
183
+ "type": "object",
184
+ "properties": {
185
+ "source_robot_id": {
186
+ "type": "string",
187
+ "description": "Source robot ID",
188
+ },
189
+ "target_robot_id": {
190
+ "type": "string",
191
+ "description": "Target robot ID",
192
+ },
193
+ "repository_id": {
194
+ "type": "string",
195
+ "description": "Repository ID to adapt",
196
+ },
197
+ "analyze_only": {
198
+ "type": "boolean",
199
+ "description": "Only show compatibility analysis",
200
+ "default": False,
201
+ },
202
+ },
203
+ "required": ["source_robot_id", "target_robot_id", "repository_id"],
204
+ },
205
+ ),
206
+ Tool(
207
+ name="ate_get_repository",
208
+ description="Get details of a specific repository",
209
+ inputSchema={
210
+ "type": "object",
211
+ "properties": {
212
+ "repo_id": {
213
+ "type": "string",
214
+ "description": "Repository ID",
215
+ },
216
+ },
217
+ "required": ["repo_id"],
218
+ },
219
+ ),
220
+ Tool(
221
+ name="ate_get_robot",
222
+ description="Get details of a specific robot profile",
223
+ inputSchema={
224
+ "type": "object",
225
+ "properties": {
226
+ "robot_id": {
227
+ "type": "string",
228
+ "description": "Robot profile ID",
229
+ },
230
+ },
231
+ "required": ["robot_id"],
232
+ },
233
+ ),
234
+ ]
235
+
236
+
237
+ @server.call_tool()
238
+ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
239
+ """Handle tool calls"""
240
+ try:
241
+ if name == "ate_init":
242
+ result = client.init(
243
+ arguments["name"],
244
+ arguments.get("description", ""),
245
+ arguments.get("visibility", "public"),
246
+ )
247
+ return [
248
+ TextContent(
249
+ type="text",
250
+ text=f"Repository created successfully!\nID: {result['repository']['id']}\nName: {result['repository']['name']}",
251
+ )
252
+ ]
253
+
254
+ elif name == "ate_clone":
255
+ client.clone(
256
+ arguments["repo_id"], arguments.get("target_dir")
257
+ )
258
+ return [
259
+ TextContent(
260
+ type="text",
261
+ text=f"Repository cloned successfully to {arguments.get('target_dir', 'current directory')}",
262
+ )
263
+ ]
264
+
265
+ elif name == "ate_list_repositories":
266
+ # Build query params
267
+ params = {}
268
+ if arguments.get("search"):
269
+ params["search"] = arguments["search"]
270
+ if arguments.get("robot_model"):
271
+ params["robotModel"] = arguments["robot_model"]
272
+ params["limit"] = arguments.get("limit", 20)
273
+
274
+ # Make API request
275
+ response = client._request("GET", "/repositories", params=params)
276
+ repos = response.get("repositories", [])
277
+
278
+ result_text = f"Found {len(repos)} repositories:\n\n"
279
+ for repo in repos[:10]: # Limit to first 10
280
+ result_text += f"- {repo['name']} (ID: {repo['id']})\n"
281
+ if repo.get("description"):
282
+ result_text += f" {repo['description'][:100]}...\n"
283
+
284
+ return [TextContent(type="text", text=result_text)]
285
+
286
+ elif name == "ate_list_robots":
287
+ # Build query params
288
+ params = {}
289
+ if arguments.get("search"):
290
+ params["search"] = arguments["search"]
291
+ if arguments.get("category"):
292
+ params["category"] = arguments["category"]
293
+ params["limit"] = arguments.get("limit", 20)
294
+
295
+ # Make API request
296
+ response = client._request("GET", "/robots/profiles", params=params)
297
+ robots = response.get("profiles", [])
298
+
299
+ result_text = f"Found {len(robots)} robot profiles:\n\n"
300
+ for robot in robots[:10]: # Limit to first 10
301
+ result_text += f"- {robot['modelName']} by {robot['manufacturer']} (ID: {robot['id']})\n"
302
+ if robot.get("description"):
303
+ result_text += f" {robot['description'][:100]}...\n"
304
+
305
+ return [TextContent(type="text", text=result_text)]
306
+
307
+ elif name == "ate_compatibility":
308
+ response = client._request(
309
+ "POST",
310
+ "/skills/compatibility",
311
+ json={
312
+ "sourceRobotId": arguments["source_robot_id"],
313
+ "targetRobotId": arguments["target_robot_id"],
314
+ "repositoryId": arguments["repository_id"],
315
+ },
316
+ )
317
+ compatibility = response.get("compatibility", {})
318
+ score = compatibility.get("overallScore", 0) * 100
319
+
320
+ result_text = f"Compatibility Score: {score:.1f}%\n"
321
+ result_text += f"Adaptation Type: {compatibility.get('adaptationType', 'unknown')}\n"
322
+ result_text += f"Estimated Effort: {compatibility.get('estimatedEffort', 'unknown')}\n"
323
+
324
+ return [TextContent(type="text", text=result_text)]
325
+
326
+ elif name == "ate_adapt":
327
+ response = client._request(
328
+ "POST",
329
+ "/skills/adapt",
330
+ json={
331
+ "sourceRobotId": arguments["source_robot_id"],
332
+ "targetRobotId": arguments["target_robot_id"],
333
+ "repositoryId": arguments["repository_id"],
334
+ },
335
+ )
336
+ plan = response.get("adaptationPlan", {})
337
+ compatibility = response.get("compatibility", {})
338
+
339
+ result_text = "Adaptation Plan:\n\n"
340
+ result_text += f"Overview: {plan.get('overview', 'No overview available')}\n\n"
341
+
342
+ if compatibility:
343
+ result_text += f"Compatibility Score: {compatibility.get('overallScore', 0) * 100:.1f}%\n"
344
+ result_text += f"Adaptation Type: {compatibility.get('adaptationType', 'unknown')}\n"
345
+
346
+ return [TextContent(type="text", text=result_text)]
347
+
348
+ elif name == "ate_get_repository":
349
+ response = client._request("GET", f"/repositories/{arguments['repo_id']}")
350
+ repo = response.get("repository", {})
351
+
352
+ result_text = f"Repository: {repo.get('name', 'Unknown')}\n"
353
+ result_text += f"ID: {repo.get('id', 'Unknown')}\n"
354
+ result_text += f"Description: {repo.get('description', 'No description')}\n"
355
+ result_text += f"Visibility: {repo.get('visibility', 'unknown')}\n"
356
+
357
+ return [TextContent(type="text", text=result_text)]
358
+
359
+ elif name == "ate_get_robot":
360
+ response = client._request("GET", f"/robots/profiles/{arguments['robot_id']}")
361
+ robot = response.get("profile", {})
362
+
363
+ result_text = f"Robot: {robot.get('modelName', 'Unknown')}\n"
364
+ result_text += f"Manufacturer: {robot.get('manufacturer', 'Unknown')}\n"
365
+ result_text += f"Category: {robot.get('category', 'Unknown')}\n"
366
+ result_text += f"Description: {robot.get('description', 'No description')}\n"
367
+
368
+ return [TextContent(type="text", text=result_text)]
369
+
370
+ else:
371
+ return [
372
+ TextContent(
373
+ type="text",
374
+ text=f"Unknown tool: {name}",
375
+ )
376
+ ]
377
+
378
+ except Exception as e:
379
+ return [
380
+ TextContent(
381
+ type="text",
382
+ text=f"Error executing tool {name}: {str(e)}",
383
+ )
384
+ ]
385
+
386
+
387
+ @server.list_resources()
388
+ async def list_resources() -> List[Resource]:
389
+ """List available resources"""
390
+ return [
391
+ Resource(
392
+ uri="repository://*",
393
+ name="Repository",
394
+ description="Access FoodforThought repository details",
395
+ mimeType="application/json",
396
+ ),
397
+ Resource(
398
+ uri="robot://*",
399
+ name="Robot Profile",
400
+ description="Access robot profile details",
401
+ mimeType="application/json",
402
+ ),
403
+ ]
404
+
405
+
406
+ @server.read_resource()
407
+ async def read_resource(uri: str) -> str:
408
+ """Read a resource"""
409
+ if uri.startswith("repository://"):
410
+ repo_id = uri.replace("repository://", "")
411
+ response = client._request("GET", f"/repositories/{repo_id}")
412
+ return json.dumps(response.get("repository", {}), indent=2)
413
+ elif uri.startswith("robot://"):
414
+ robot_id = uri.replace("robot://", "")
415
+ response = client._request("GET", f"/robots/profiles/{robot_id}")
416
+ return json.dumps(response.get("profile", {}), indent=2)
417
+ else:
418
+ raise ValueError(f"Unknown resource URI: {uri}")
419
+
420
+
421
+ @server.list_prompts()
422
+ async def list_prompts() -> List[Prompt]:
423
+ """List available prompts"""
424
+ return [
425
+ Prompt(
426
+ name="create_skill",
427
+ description="Guided workflow for creating a new robot skill repository",
428
+ arguments=[
429
+ PromptArgument(
430
+ name="robot_model",
431
+ description="Target robot model",
432
+ required=True,
433
+ ),
434
+ PromptArgument(
435
+ name="task_description",
436
+ description="Description of the skill/task",
437
+ required=True,
438
+ ),
439
+ ],
440
+ ),
441
+ Prompt(
442
+ name="adapt_skill",
443
+ description="Guided workflow for adapting a skill between robots",
444
+ arguments=[
445
+ PromptArgument(
446
+ name="source_robot",
447
+ description="Source robot model",
448
+ required=True,
449
+ ),
450
+ PromptArgument(
451
+ name="target_robot",
452
+ description="Target robot model",
453
+ required=True,
454
+ ),
455
+ PromptArgument(
456
+ name="repository_id",
457
+ description="Repository ID to adapt",
458
+ required=True,
459
+ ),
460
+ ],
461
+ ),
462
+ ]
463
+
464
+
465
+ @server.get_prompt()
466
+ async def get_prompt(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
467
+ """Get prompt content"""
468
+ if name == "create_skill":
469
+ return [
470
+ TextContent(
471
+ type="text",
472
+ text=f"""Create a new robot skill for {arguments.get('robot_model', 'your robot')}:
473
+
474
+ 1. Initialize repository: Use ate_init with a descriptive name
475
+ 2. Add your skill files (code, configs, documentation)
476
+ 3. Commit changes: Use ate_commit with a meaningful message
477
+ 4. Push to FoodforThought: Use ate_push
478
+
479
+ Task: {arguments.get('task_description', 'Not specified')}
480
+ """,
481
+ )
482
+ ]
483
+ elif name == "adapt_skill":
484
+ return [
485
+ TextContent(
486
+ type="text",
487
+ text=f"""Adapt skill from {arguments.get('source_robot')} to {arguments.get('target_robot')}:
488
+
489
+ 1. Check compatibility: Use ate_compatibility to see if adaptation is feasible
490
+ 2. Generate adaptation plan: Use ate_adapt to get detailed adaptation instructions
491
+ 3. Review the plan and apply necessary changes
492
+ 4. Test the adapted skill
493
+
494
+ Repository ID: {arguments.get('repository_id')}
495
+ """,
496
+ )
497
+ ]
498
+ else:
499
+ return [TextContent(type="text", text=f"Unknown prompt: {name}")]
500
+
501
+
502
+ async def main():
503
+ """Main entry point for MCP server"""
504
+ # Run the server using stdio transport
505
+ # stdio_server() returns (stdin, stdout) streams
506
+ stdin, stdout = stdio_server()
507
+ await server.run(
508
+ stdin, stdout, server.create_initialization_options()
509
+ )
510
+
511
+
512
+ if __name__ == "__main__":
513
+ asyncio.run(main())
514
+
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: foodforthought-cli
3
+ Version: 0.1.1
4
+ Summary: CLI tool for FoodforThought robotics repository platform - manage robot skills and data
5
+ Home-page: https://kindly.fyi/foodforthought
6
+ Author: Kindly Robotics
7
+ Author-email: hello@kindly.fyi
8
+ Project-URL: Homepage, https://kindly.fyi
9
+ Project-URL: Documentation, https://kindly.fyi/foodforthought/cli
10
+ Project-URL: Source, https://github.com/kindlyrobotics/monorepo
11
+ Project-URL: Bug Tracker, https://github.com/kindlyrobotics/monorepo/issues
12
+ Keywords: robotics,robot-skills,machine-learning,data-management,cli
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Software Development :: Version Control
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+ Requires-Dist: requests>=2.28.0
28
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: classifier
31
+ Dynamic: description
32
+ Dynamic: description-content-type
33
+ Dynamic: home-page
34
+ Dynamic: keywords
35
+ Dynamic: project-url
36
+ Dynamic: requires-dist
37
+ Dynamic: requires-python
38
+ Dynamic: summary
39
+
40
+ # FoodforThought CLI
41
+
42
+ GitHub-like CLI tool for the FoodforThought robotics repository platform.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install foodforthought-cli
48
+ ```
49
+
50
+ Or install from source:
51
+
52
+ ```bash
53
+ cd foodforthought-cli
54
+ pip install -e .
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Set environment variables:
60
+
61
+ ```bash
62
+ export ATE_API_URL="https://kindly.fyi/api"
63
+ export ATE_API_KEY="your-api-key-here"
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ ### Initialize a repository
69
+
70
+ ```bash
71
+ ate init my-robot-skill -d "A skill for my robot" -v public
72
+ ```
73
+
74
+ ### Clone a repository
75
+
76
+ ```bash
77
+ ate clone <repository-id>
78
+ ```
79
+
80
+ ### Create a commit
81
+
82
+ ```bash
83
+ ate commit -m "Add new control algorithm"
84
+ ```
85
+
86
+ ### Push to remote
87
+
88
+ ```bash
89
+ ate push -b main
90
+ ```
91
+
92
+ ### Deploy to robot
93
+
94
+ ```bash
95
+ ate deploy unitree-r1
96
+ ```
97
+
98
+ ## Commands
99
+
100
+ ### Repository Management
101
+ - `ate init <name>` - Initialize a new repository
102
+ - `ate clone <repo-id>` - Clone a repository
103
+ - `ate commit -m <message>` - Create a commit
104
+ - `ate push [-b <branch>]` - Push commits to remote
105
+
106
+ ### Skill Pipeline
107
+ - `ate pull <skill-id> [--robot <robot>] [--format json|rlds|lerobot] [--output ./data]` - Pull skill data for training
108
+ - `ate upload <video-path> --robot <robot> --task <task> [--project <id>]` - Upload demonstrations for labeling
109
+ - `ate check-transfer --from <source-robot> --to <target-robot> [--skill <id>]` - Check skill transfer compatibility
110
+ - `ate labeling-status <job-id>` - Check labeling job status
111
+
112
+ ### Deployment & Testing
113
+ - `ate deploy <robot-type>` - Deploy to a robot (e.g., unitree-r1)
114
+ - `ate test [-e gazebo|mujoco|pybullet|webots] [-r robot]` - Test skills in simulation
115
+ - `ate benchmark [-t speed|accuracy|robustness|efficiency|all]` - Run performance benchmarks
116
+ - `ate adapt <source-robot> <target-robot>` - Adapt skills between robots
117
+
118
+ ### Safety & Validation
119
+ - `ate validate [-c collision|speed|workspace|force|all]` - Validate safety and compliance
120
+ - `ate stream [start|stop|status] [-s sensors...]` - Stream sensor data
121
+
122
+ ## Cursor IDE Integration (MCP)
123
+
124
+ FoodforThought CLI supports Model Context Protocol (MCP) for integration with Cursor IDE.
125
+
126
+ ### Installation
127
+
128
+ 1. Install MCP dependencies:
129
+ ```bash
130
+ pip install -r requirements-mcp.txt
131
+ ```
132
+
133
+ 2. Get your MCP configuration from the [FoodforThought homepage](https://kindly.fyi/foodforthought)
134
+
135
+ 3. Place `mcp.json` in `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project)
136
+
137
+ 4. Set your `ATE_API_KEY` environment variable
138
+
139
+ 5. Restart Cursor
140
+
141
+ ### Available MCP Tools
142
+
143
+ - `ate_init` - Initialize a new repository
144
+ - `ate_clone` - Clone a repository
145
+ - `ate_list_repositories` - List available repositories
146
+ - `ate_list_robots` - List robot profiles
147
+ - `ate_compatibility` - Check skill compatibility between robots
148
+ - `ate_adapt` - Generate adaptation plans for skills
149
+
150
+ See the [MCP documentation](https://cursor.com/docs/context/mcp) for more information.
151
+
@@ -0,0 +1,8 @@
1
+ ate/__init__.py,sha256=GcVHEGyItqo9e3uAgF6cvKZOXJKHgj0-AnRJi-oC00g,105
2
+ ate/cli.py,sha256=GzSb2CwWj629JuBsT5SO-yp9HT5pAxD0A-7c_3Mi_Mc,33831
3
+ ate/mcp_server.py,sha256=O77UKrRS-PigEAftmlG8y57mw6pZOA5OTrt2C-aKgyU,17814
4
+ foodforthought_cli-0.1.1.dist-info/METADATA,sha256=tQ78BUHETRDkw3KWEC6XmOySwRhehofv5WeOLzoCezk,4353
5
+ foodforthought_cli-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ foodforthought_cli-0.1.1.dist-info/entry_points.txt,sha256=JSxWuXCbGllyHqVJpomP_BDlXYpiNglM9Mo6bEmQK1A,37
7
+ foodforthought_cli-0.1.1.dist-info/top_level.txt,sha256=dlAru-z7a6fWHgnrMWcQhKc8D4eSTXSpFLIUoWZwDlE,4
8
+ foodforthought_cli-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ate = ate.cli:main
@@ -0,0 +1 @@
1
+ ate