agentscope-runtime 1.0.0b2__py3-none-any.whl → 1.0.2__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 (71) hide show
  1. agentscope_runtime/adapters/agentscope/message.py +78 -10
  2. agentscope_runtime/adapters/agentscope/stream.py +155 -101
  3. agentscope_runtime/adapters/agentscope/tool/tool.py +1 -3
  4. agentscope_runtime/adapters/agno/__init__.py +0 -0
  5. agentscope_runtime/adapters/agno/message.py +30 -0
  6. agentscope_runtime/adapters/agno/stream.py +122 -0
  7. agentscope_runtime/adapters/langgraph/__init__.py +12 -0
  8. agentscope_runtime/adapters/langgraph/message.py +257 -0
  9. agentscope_runtime/adapters/langgraph/stream.py +205 -0
  10. agentscope_runtime/cli/__init__.py +7 -0
  11. agentscope_runtime/cli/cli.py +63 -0
  12. agentscope_runtime/cli/commands/__init__.py +2 -0
  13. agentscope_runtime/cli/commands/chat.py +815 -0
  14. agentscope_runtime/cli/commands/deploy.py +1062 -0
  15. agentscope_runtime/cli/commands/invoke.py +58 -0
  16. agentscope_runtime/cli/commands/list_cmd.py +103 -0
  17. agentscope_runtime/cli/commands/run.py +176 -0
  18. agentscope_runtime/cli/commands/sandbox.py +128 -0
  19. agentscope_runtime/cli/commands/status.py +60 -0
  20. agentscope_runtime/cli/commands/stop.py +185 -0
  21. agentscope_runtime/cli/commands/web.py +166 -0
  22. agentscope_runtime/cli/loaders/__init__.py +6 -0
  23. agentscope_runtime/cli/loaders/agent_loader.py +295 -0
  24. agentscope_runtime/cli/state/__init__.py +10 -0
  25. agentscope_runtime/cli/utils/__init__.py +18 -0
  26. agentscope_runtime/cli/utils/console.py +378 -0
  27. agentscope_runtime/cli/utils/validators.py +118 -0
  28. agentscope_runtime/engine/app/agent_app.py +15 -5
  29. agentscope_runtime/engine/deployers/__init__.py +1 -0
  30. agentscope_runtime/engine/deployers/agentrun_deployer.py +154 -24
  31. agentscope_runtime/engine/deployers/base.py +27 -2
  32. agentscope_runtime/engine/deployers/kubernetes_deployer.py +158 -31
  33. agentscope_runtime/engine/deployers/local_deployer.py +188 -25
  34. agentscope_runtime/engine/deployers/modelstudio_deployer.py +109 -18
  35. agentscope_runtime/engine/deployers/state/__init__.py +9 -0
  36. agentscope_runtime/engine/deployers/state/manager.py +388 -0
  37. agentscope_runtime/engine/deployers/state/schema.py +96 -0
  38. agentscope_runtime/engine/deployers/utils/build_cache.py +736 -0
  39. agentscope_runtime/engine/deployers/utils/detached_app.py +105 -30
  40. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +31 -10
  41. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +15 -8
  42. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +30 -2
  43. agentscope_runtime/engine/deployers/utils/k8s_utils.py +241 -0
  44. agentscope_runtime/engine/deployers/utils/package.py +56 -6
  45. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +68 -9
  46. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +155 -5
  47. agentscope_runtime/engine/deployers/utils/wheel_packager.py +107 -123
  48. agentscope_runtime/engine/runner.py +32 -12
  49. agentscope_runtime/engine/schemas/agent_schemas.py +21 -7
  50. agentscope_runtime/engine/schemas/exception.py +580 -0
  51. agentscope_runtime/engine/services/agent_state/__init__.py +2 -0
  52. agentscope_runtime/engine/services/agent_state/state_service_factory.py +55 -0
  53. agentscope_runtime/engine/services/memory/__init__.py +2 -0
  54. agentscope_runtime/engine/services/memory/memory_service_factory.py +126 -0
  55. agentscope_runtime/engine/services/sandbox/__init__.py +2 -0
  56. agentscope_runtime/engine/services/sandbox/sandbox_service_factory.py +49 -0
  57. agentscope_runtime/engine/services/service_factory.py +119 -0
  58. agentscope_runtime/engine/services/session_history/__init__.py +2 -0
  59. agentscope_runtime/engine/services/session_history/session_history_service_factory.py +73 -0
  60. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +35 -10
  61. agentscope_runtime/engine/tracing/wrapper.py +49 -31
  62. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +113 -39
  63. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +20 -4
  64. agentscope_runtime/sandbox/utils.py +2 -0
  65. agentscope_runtime/version.py +1 -1
  66. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/METADATA +82 -11
  67. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/RECORD +71 -36
  68. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/entry_points.txt +1 -0
  69. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/WHEEL +0 -0
  70. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/licenses/LICENSE +0 -0
  71. {agentscope_runtime-1.0.0b2.dist-info → agentscope_runtime-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,257 @@
1
+ # -*- coding: utf-8 -*-
2
+ # pylint:disable=too-many-branches,too-many-statements,too-many-return-statements
3
+ """Message conversion between LangGraph and AgentScope runtime."""
4
+ import json
5
+
6
+ from collections import OrderedDict
7
+ from typing import Union, List
8
+
9
+ from langchain_core.messages import (
10
+ AIMessage,
11
+ HumanMessage,
12
+ SystemMessage,
13
+ ToolMessage,
14
+ BaseMessage,
15
+ )
16
+
17
+ from ...engine.schemas.agent_schemas import (
18
+ Message,
19
+ FunctionCall,
20
+ MessageType,
21
+ )
22
+ from ...engine.helpers.agent_api_builder import ResponseBuilder
23
+
24
+
25
+ def langgraph_msg_to_message(
26
+ messages: Union[BaseMessage, List[BaseMessage]],
27
+ ) -> List[Message]:
28
+ """
29
+ Convert LangGraph BaseMessage(s) into one or more runtime Message objects
30
+
31
+ Args:
32
+ messages: LangGraph message(s) from streaming.
33
+
34
+ Returns:
35
+ List[Message]: One or more constructed runtime Message objects.
36
+ """
37
+ if isinstance(messages, BaseMessage):
38
+ msgs = [messages]
39
+ elif isinstance(messages, list):
40
+ msgs = messages
41
+ else:
42
+ raise TypeError(
43
+ f"Expected BaseMessage or list[BaseMessage], got {type(messages)}",
44
+ )
45
+
46
+ results: List[Message] = []
47
+
48
+ for msg in msgs:
49
+ # Map LangGraph roles to runtime roles
50
+ if isinstance(msg, HumanMessage):
51
+ role = "user"
52
+ elif isinstance(msg, AIMessage):
53
+ role = "assistant"
54
+ elif isinstance(msg, SystemMessage):
55
+ role = "system"
56
+ elif isinstance(msg, ToolMessage):
57
+ role = "tool"
58
+ else:
59
+ role = "assistant" # default fallback
60
+
61
+ # Handle tool calls in AIMessage
62
+ if isinstance(msg, AIMessage) and msg.tool_calls:
63
+ # Convert each tool call to a PLUGIN_CALL message
64
+ for tool_call in msg.tool_calls:
65
+ rb = ResponseBuilder()
66
+ mb = rb.create_message_builder(
67
+ role=role,
68
+ message_type=MessageType.PLUGIN_CALL,
69
+ )
70
+ # Add metadata
71
+ mb.message.metadata = {
72
+ "original_id": getattr(msg, "id", None),
73
+ "name": getattr(msg, "name", None),
74
+ "metadata": getattr(msg, "additional_kwargs", {}),
75
+ }
76
+ cb = mb.create_content_builder(content_type="data")
77
+
78
+ call_data = FunctionCall(
79
+ call_id=tool_call.get("id", ""),
80
+ name=tool_call.get("name", ""),
81
+ arguments=json.dumps(tool_call.get("args", {})),
82
+ ).model_dump()
83
+ cb.set_data(call_data)
84
+ cb.complete()
85
+ mb.complete()
86
+ results.append(mb.get_message_data())
87
+
88
+ # If there's content in addition to tool calls,
89
+ # create a separate message
90
+ if msg.content:
91
+ rb = ResponseBuilder()
92
+ mb = rb.create_message_builder(
93
+ role=role,
94
+ message_type=MessageType.MESSAGE,
95
+ )
96
+ mb.message.metadata = {
97
+ "original_id": getattr(msg, "id", None),
98
+ "name": getattr(msg, "name", None),
99
+ "metadata": getattr(msg, "additional_kwargs", {}),
100
+ }
101
+ cb = mb.create_content_builder(content_type="text")
102
+ cb.set_text(str(msg.content))
103
+ cb.complete()
104
+ mb.complete()
105
+ results.append(mb.get_message_data())
106
+ else:
107
+ # Regular message conversion
108
+ rb = ResponseBuilder()
109
+ mb = rb.create_message_builder(
110
+ role=role,
111
+ message_type=MessageType.MESSAGE,
112
+ )
113
+ # Add metadata
114
+ mb.message.metadata = {
115
+ "original_id": getattr(msg, "id", None),
116
+ "name": getattr(msg, "name", None),
117
+ "metadata": getattr(msg, "additional_kwargs", {}),
118
+ }
119
+ cb = mb.create_content_builder(content_type="text")
120
+ cb.set_text(str(msg.content) if msg.content else "")
121
+ cb.complete()
122
+ mb.complete()
123
+ results.append(mb.get_message_data())
124
+
125
+ return results
126
+
127
+
128
+ def message_to_langgraph_msg(
129
+ messages: Union[Message, List[Message]],
130
+ ) -> Union[BaseMessage, List[BaseMessage]]:
131
+ """
132
+ Convert AgentScope runtime Message(s) to LangGraph BaseMessage(s).
133
+
134
+ Args:
135
+ messages: A single AgentScope runtime Message or list of Messages.
136
+
137
+ Returns:
138
+ A single BaseMessage object or a list of BaseMessage objects.
139
+ """
140
+
141
+ def _convert_one(message: Message) -> BaseMessage:
142
+ # Map runtime roles to LangGraph roles
143
+ role_map = {
144
+ "user": HumanMessage,
145
+ "assistant": AIMessage,
146
+ "system": SystemMessage,
147
+ "tool": ToolMessage,
148
+ }
149
+
150
+ message_cls = role_map.get(
151
+ message.role,
152
+ AIMessage,
153
+ ) # default to AIMessage
154
+
155
+ # Handle different message types
156
+ if message.type in (
157
+ MessageType.PLUGIN_CALL,
158
+ MessageType.FUNCTION_CALL,
159
+ ):
160
+ # Convert PLUGIN_CALL, FUNCTION_CALL to AIMessage with tool_calls
161
+ if message.content and hasattr(message.content[0], "data"):
162
+ try:
163
+ func_call_data = message.content[0].data
164
+ tool_calls = [
165
+ {
166
+ "name": func_call_data.get("name", ""),
167
+ "args": json.loads(
168
+ func_call_data.get("arguments", "{}"),
169
+ ),
170
+ "id": func_call_data.get("call_id", ""),
171
+ },
172
+ ]
173
+ return AIMessage(content="", tool_calls=tool_calls)
174
+ except (json.JSONDecodeError, KeyError):
175
+ return message_cls(content=str(message.content))
176
+ else:
177
+ return message_cls(content="")
178
+
179
+ elif message.type in (
180
+ MessageType.PLUGIN_CALL_OUTPUT,
181
+ MessageType.FUNCTION_CALL_OUTPUT,
182
+ ):
183
+ # Convert PLUGIN_CALL_OUTPUT, FUNCTION_CALL_OUTPUT to ToolMessage
184
+ if message.content and hasattr(message.content[0], "data"):
185
+ try:
186
+ func_output_data = message.content[0].data
187
+ tool_call_id = func_output_data.get("call_id", "")
188
+ content = func_output_data.get("output", "")
189
+ # Try to parse JSON output
190
+ try:
191
+ content = json.loads(content)
192
+ except json.JSONDecodeError:
193
+ pass
194
+ return ToolMessage(
195
+ content=content,
196
+ tool_call_id=tool_call_id,
197
+ )
198
+ except KeyError:
199
+ return message_cls(content=str(message.content))
200
+ else:
201
+ return message_cls(content="")
202
+
203
+ else:
204
+ # Regular message conversion
205
+ content = ""
206
+ if message.content:
207
+ # Concatenate all content parts
208
+ content_parts = []
209
+ for cnt in message.content:
210
+ if hasattr(cnt, "text"):
211
+ content_parts.append(cnt.text)
212
+ elif hasattr(cnt, "data"):
213
+ content_parts.append(str(cnt.data))
214
+ content = (
215
+ "".join(content_parts)
216
+ if content_parts
217
+ else str(message.content)
218
+ )
219
+
220
+ # For ToolMessage, we need tool_call_id
221
+ if message_cls == ToolMessage:
222
+ tool_call_id = ""
223
+ if hasattr(message, "metadata") and isinstance(
224
+ message.metadata,
225
+ dict,
226
+ ):
227
+ tool_call_id = message.metadata.get("tool_call_id", "")
228
+ return ToolMessage(content=content, tool_call_id=tool_call_id)
229
+
230
+ return message_cls(content=content)
231
+
232
+ # Handle single or list input
233
+ if isinstance(messages, Message):
234
+ return _convert_one(messages)
235
+ elif isinstance(messages, list):
236
+ converted_list = [_convert_one(m) for m in messages]
237
+
238
+ # Group by original_id for messages that should be combined
239
+ grouped = OrderedDict()
240
+ for msg, orig_msg in zip(messages, converted_list):
241
+ metadata = getattr(msg, "metadata", {})
242
+ if metadata:
243
+ orig_id = metadata.get("original_id", getattr(msg, "id", None))
244
+ else:
245
+ orig_id = getattr(msg, "id", None)
246
+
247
+ if orig_id and orig_id not in grouped:
248
+ grouped[orig_id] = orig_msg
249
+ # For now, we won't combine messages as LangGraph messages
250
+ # are typically separate,
251
+ # But we keep the structure in case we need it later
252
+
253
+ return list(grouped.values()) if grouped else converted_list
254
+ else:
255
+ raise TypeError(
256
+ f"Expected Message or list[Message], got {type(messages)}",
257
+ )
@@ -0,0 +1,205 @@
1
+ # -*- coding: utf-8 -*-
2
+ # pylint: disable=too-many-branches,too-many-statements
3
+ # pylint: disable=simplifiable-if-expression
4
+ """Streaming adapter for LangGraph messages."""
5
+ import json
6
+ from functools import reduce
7
+
8
+ from typing import AsyncIterator, Tuple
9
+
10
+ from langchain_core.messages import (
11
+ BaseMessage,
12
+ AIMessage,
13
+ HumanMessage,
14
+ SystemMessage,
15
+ ToolMessage,
16
+ )
17
+
18
+ from ...engine.schemas.agent_schemas import (
19
+ Message,
20
+ TextContent,
21
+ DataContent,
22
+ FunctionCall,
23
+ FunctionCallOutput,
24
+ MessageType,
25
+ )
26
+
27
+
28
+ async def adapt_langgraph_message_stream(
29
+ source_stream: AsyncIterator[Tuple[BaseMessage, bool]],
30
+ ) -> AsyncIterator[Message]:
31
+ """
32
+ Optimized version of the stream adapter for LangGraph messages.
33
+ Reduces code duplication and improves clarity.
34
+ """
35
+ # Track message IDs to detect new messages
36
+ msg_id = None
37
+ index = None
38
+
39
+ # Track tool usage
40
+ tool_started = False
41
+ tool_call_chunk_msgs = []
42
+
43
+ async for msg, last in source_stream:
44
+ # Determine message role
45
+ if isinstance(msg, HumanMessage):
46
+ role = "user"
47
+ content = msg.content if hasattr(msg, "content") else None
48
+ if msg_id != getattr(msg, "id"):
49
+ message = Message(type=MessageType.MESSAGE, role=role)
50
+ yield message.in_progress()
51
+ msg_id = getattr(msg, "id")
52
+ if content:
53
+ text_delta_content = TextContent(
54
+ delta=True,
55
+ index=None,
56
+ text=content,
57
+ )
58
+ text_delta_content = message.add_delta_content(
59
+ new_content=text_delta_content,
60
+ )
61
+ yield text_delta_content
62
+ yield message.completed()
63
+ elif isinstance(msg, AIMessage):
64
+ role = "assistant"
65
+ has_tool_call_chunk = (
66
+ True if getattr(msg, "tool_call_chunks") else False
67
+ )
68
+ is_last_chunk = (
69
+ True if getattr(msg, "chunk_position") == "last" else False
70
+ )
71
+
72
+ # Extract tool calls if present
73
+ if tool_started:
74
+ if has_tool_call_chunk:
75
+ tool_call_chunk_msgs.append(msg)
76
+ if is_last_chunk:
77
+ # tool call finished
78
+ tool_started = False
79
+ result = reduce(lambda x, y: x + y, tool_call_chunk_msgs)
80
+ tool_calls = result.tool_call_chunks
81
+ for tool_call in tool_calls:
82
+ call_id = tool_call.get("id", "")
83
+ # Create new tool call message
84
+ plugin_call_message = Message(
85
+ type=MessageType.PLUGIN_CALL,
86
+ role=role,
87
+ )
88
+ data_content = DataContent(
89
+ index=index,
90
+ data=FunctionCall(
91
+ call_id=call_id,
92
+ name=tool_call.get("name", ""),
93
+ arguments=json.dumps(
94
+ tool_call.get("args", {}),
95
+ ensure_ascii=False,
96
+ ),
97
+ ).model_dump(),
98
+ delta=True,
99
+ )
100
+
101
+ data_content = plugin_call_message.add_delta_content(
102
+ new_content=data_content,
103
+ )
104
+ yield data_content
105
+ yield plugin_call_message.completed()
106
+ else:
107
+ if has_tool_call_chunk:
108
+ # tool call start, collect chunks and continue
109
+ tool_started = True
110
+ tool_call_chunk_msgs.append(msg)
111
+ else:
112
+ # normal message
113
+ content = msg.content if hasattr(msg, "content") else None
114
+ if msg_id != getattr(msg, "id"):
115
+ index = None
116
+ message = Message(type=MessageType.MESSAGE, role=role)
117
+ msg_id = getattr(msg, "id")
118
+ yield message.in_progress()
119
+
120
+ if content:
121
+ # todo support non str content
122
+ text_delta_content = TextContent(
123
+ delta=True,
124
+ index=index,
125
+ text=content,
126
+ )
127
+ text_delta_content = message.add_delta_content(
128
+ new_content=text_delta_content,
129
+ )
130
+ index = text_delta_content.index
131
+ yield text_delta_content
132
+ # Handle final completion
133
+ if last:
134
+ # completed_content = message.content[index]
135
+ # if completed_content.text:
136
+ # yield completed_content.completed()
137
+ yield message.completed()
138
+ elif isinstance(msg, SystemMessage):
139
+ role = "system"
140
+ content = msg.content if hasattr(msg, "content") else None
141
+ if msg_id != getattr(msg, "id"):
142
+ message = Message(type=MessageType.MESSAGE, role=role)
143
+ yield message.in_progress()
144
+ msg_id = getattr(msg, "id")
145
+ if content:
146
+ text_delta_content = TextContent(
147
+ delta=True,
148
+ index=None,
149
+ text=content,
150
+ )
151
+ text_delta_content = message.add_delta_content(
152
+ new_content=text_delta_content,
153
+ )
154
+ yield text_delta_content
155
+ elif isinstance(msg, ToolMessage):
156
+ role = "tool"
157
+ content = msg.content if hasattr(msg, "content") else None
158
+ if msg_id != getattr(msg, "id"):
159
+ message = Message(type=MessageType.MESSAGE, role=role)
160
+ yield message.in_progress()
161
+ msg_id = getattr(msg, "id")
162
+ plugin_output_message = Message(
163
+ type=MessageType.PLUGIN_CALL_OUTPUT,
164
+ role="tool",
165
+ )
166
+ # Create function call output data
167
+ function_output_data = FunctionCallOutput(
168
+ call_id=msg.tool_call_id,
169
+ name=msg.name,
170
+ output=json.dumps(content, ensure_ascii=False),
171
+ )
172
+
173
+ data_content = DataContent(
174
+ index=None,
175
+ data=function_output_data.model_dump(),
176
+ )
177
+ plugin_output_message.content = [data_content]
178
+ yield plugin_output_message.completed()
179
+ else:
180
+ role = "assistant"
181
+ content = msg.content if hasattr(msg, "content") else None
182
+ if msg_id != getattr(msg, "id"):
183
+ index = None
184
+ message = Message(type=MessageType.MESSAGE, role=role)
185
+ msg_id = getattr(msg, "id")
186
+ yield message.in_progress()
187
+
188
+ if content:
189
+ # todo support non str content
190
+ text_delta_content = TextContent(
191
+ delta=True,
192
+ index=index,
193
+ text=content,
194
+ )
195
+ text_delta_content = message.add_delta_content(
196
+ new_content=text_delta_content,
197
+ )
198
+ index = text_delta_content.index
199
+ yield text_delta_content
200
+ # Handle final completion
201
+ if last:
202
+ # completed_content = message.content[index]
203
+ # if completed_content.text:
204
+ # yield completed_content.completed()
205
+ yield message.completed()
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ """AgentScope Runtime CLI - Unified command-line interface for agent
3
+ lifecycle management."""
4
+
5
+ from agentscope_runtime.cli.cli import cli
6
+
7
+ __all__ = ["cli"]
@@ -0,0 +1,63 @@
1
+ # -*- coding: utf-8 -*-
2
+ """AgentScope Runtime CLI - Main entry point."""
3
+ # pylint: disable=no-value-for-parameter
4
+
5
+ import os
6
+
7
+ import click
8
+
9
+ # Import command groups (to be registered below)
10
+ from agentscope_runtime.cli.commands import (
11
+ chat,
12
+ run,
13
+ web,
14
+ deploy,
15
+ list_cmd,
16
+ status,
17
+ stop,
18
+ invoke,
19
+ sandbox,
20
+ )
21
+ from agentscope_runtime.version import __version__
22
+
23
+ # Set default environment variable for trace console output
24
+ # This must be set BEFORE importing any runtime modules
25
+ # Individual commands can override this for verbose mode
26
+ if "TRACE_ENABLE_LOG" not in os.environ:
27
+ os.environ.setdefault("TRACE_ENABLE_LOG", "false")
28
+
29
+
30
+ @click.group()
31
+ @click.version_option(version=__version__, prog_name="agentscope")
32
+ @click.pass_context
33
+ def cli(ctx):
34
+ """
35
+ AgentScope Runtime - Unified CLI for agent lifecycle management.
36
+
37
+ Manage your agent development, deployment, and runtime operations
38
+ from a single command.
39
+
40
+ """
41
+ # Ensure context object exists
42
+ ctx.ensure_object(dict)
43
+
44
+
45
+ # Register commands
46
+ cli.add_command(chat.chat)
47
+ cli.add_command(run.run)
48
+ cli.add_command(web.web)
49
+ cli.add_command(deploy.deploy)
50
+ cli.add_command(list_cmd.list_deployments)
51
+ cli.add_command(status.status)
52
+ cli.add_command(stop.stop)
53
+ cli.add_command(invoke.invoke)
54
+ cli.add_command(sandbox.sandbox)
55
+
56
+
57
+ def main():
58
+ """Entry point for console script."""
59
+ cli(obj={})
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """CLI command implementations."""