meshagent-cli 0.0.39__tar.gz → 0.2.0__tar.gz
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.
Potentially problematic release.
This version of meshagent-cli might be problematic. Click here for more details.
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/PKG-INFO +8 -5
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/call.py +25 -4
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli.py +6 -3
- meshagent_cli-0.2.0/meshagent/cli/exec.py +286 -0
- meshagent_cli-0.2.0/meshagent/cli/services.py +584 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/storage.py +1 -1
- meshagent_cli-0.2.0/meshagent/cli/version.py +1 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/PKG-INFO +8 -5
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/SOURCES.txt +1 -1
- meshagent_cli-0.2.0/meshagent_cli.egg-info/requires.txt +14 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/pyproject.toml +8 -4
- meshagent_cli-0.0.39/meshagent/cli/services.py +0 -350
- meshagent_cli-0.0.39/meshagent/cli/tty.py +0 -118
- meshagent_cli-0.0.39/meshagent/cli/version.py +0 -1
- meshagent_cli-0.0.39/meshagent_cli.egg-info/requires.txt +0 -11
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/README.md +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/__init__.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/agent.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/api_keys.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/async_typer.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/auth.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/auth_async.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/chatbot.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli_mcp.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli_secrets.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/developer.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/helper.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/messaging.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/otel.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/participant_token.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/projects.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/sessions.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/voicebot.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/webhook.py +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/dependency_links.txt +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/entry_points.txt +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/top_level.txt +0 -0
- {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-cli
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -10,15 +10,18 @@ Requires-Python: >=3.12
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: typer~=0.15
|
|
12
12
|
Requires-Dist: pydantic-yaml~=1.4
|
|
13
|
-
Requires-Dist: meshagent-api~=0.0
|
|
14
|
-
Requires-Dist: meshagent-agents~=0.0
|
|
15
|
-
Requires-Dist: meshagent-
|
|
16
|
-
Requires-Dist: meshagent-
|
|
13
|
+
Requires-Dist: meshagent-api~=0.2.0
|
|
14
|
+
Requires-Dist: meshagent-agents~=0.2.0
|
|
15
|
+
Requires-Dist: meshagent-computers~=0.2.0
|
|
16
|
+
Requires-Dist: meshagent-openai~=0.2.0
|
|
17
|
+
Requires-Dist: meshagent-tools~=0.2.0
|
|
18
|
+
Requires-Dist: meshagent-mcp~=0.2.0
|
|
17
19
|
Requires-Dist: supabase~=2.15
|
|
18
20
|
Requires-Dist: fastmcp~=2.8
|
|
19
21
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
20
22
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.33
|
|
21
23
|
Requires-Dist: art~=6.5
|
|
24
|
+
Requires-Dist: pydantic-yaml~=1.5
|
|
22
25
|
|
|
23
26
|
## MeshAgent CLI
|
|
24
27
|
|
|
@@ -72,10 +72,16 @@ async def make_call(
|
|
|
72
72
|
project_id: str = None,
|
|
73
73
|
room: Annotated[str, typer.Option()],
|
|
74
74
|
api_key_id: Annotated[Optional[str], typer.Option()] = None,
|
|
75
|
-
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
76
75
|
role: str = "agent",
|
|
77
76
|
local: Optional[bool] = None,
|
|
78
|
-
agent_name: Annotated[
|
|
77
|
+
agent_name: Annotated[
|
|
78
|
+
Optional[str], typer.Option(..., help="deprecated and unused", hidden=True)
|
|
79
|
+
] = None,
|
|
80
|
+
name: Annotated[str, typer.Option(..., help="deprecated", hidden=True)] = None,
|
|
81
|
+
participant_name: Annotated[
|
|
82
|
+
Optional[str],
|
|
83
|
+
typer.Option(..., help="the participant name to be used by the callee"),
|
|
84
|
+
] = None,
|
|
79
85
|
url: Annotated[str, typer.Option(..., help="URL the agent should call")],
|
|
80
86
|
arguments: Annotated[
|
|
81
87
|
str, typer.Option(..., help="JSON string with arguments for the call")
|
|
@@ -83,7 +89,22 @@ async def make_call(
|
|
|
83
89
|
):
|
|
84
90
|
"""
|
|
85
91
|
Instruct an agent to 'call' a given URL with specific arguments.
|
|
92
|
+
|
|
86
93
|
"""
|
|
94
|
+
|
|
95
|
+
if name is not None:
|
|
96
|
+
print("[yellow]name is deprecated and should no longer be passed[/yellow]")
|
|
97
|
+
|
|
98
|
+
if agent_name is not None:
|
|
99
|
+
print(
|
|
100
|
+
"[yellow]agent-name is deprecated and should no longer be passed, use participant-name instead[/yellow]"
|
|
101
|
+
)
|
|
102
|
+
participant_name = agent_name
|
|
103
|
+
|
|
104
|
+
if participant_name is None:
|
|
105
|
+
print("[red]--participant-name is required[/red]")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
87
108
|
account_client = await get_client()
|
|
88
109
|
try:
|
|
89
110
|
project_id = await resolve_project_id(project_id=project_id)
|
|
@@ -96,7 +117,7 @@ async def make_call(
|
|
|
96
117
|
)["token"]
|
|
97
118
|
|
|
98
119
|
token = ParticipantToken(
|
|
99
|
-
name=
|
|
120
|
+
name=participant_name, project_id=project_id, api_key_id=api_key_id
|
|
100
121
|
)
|
|
101
122
|
token.add_role_grant(role=role)
|
|
102
123
|
token.add_room_grant(room)
|
|
@@ -130,7 +151,7 @@ async def make_call(
|
|
|
130
151
|
) as client:
|
|
131
152
|
print("[bold green]Making agent call...[/bold green]")
|
|
132
153
|
await client.agents.make_call(
|
|
133
|
-
name=
|
|
154
|
+
name=participant_name, url=url, arguments=json.loads(arguments)
|
|
134
155
|
)
|
|
135
156
|
print("[bold cyan]Call request sent successfully.[/bold cyan]")
|
|
136
157
|
|
|
@@ -2,6 +2,8 @@ import typer
|
|
|
2
2
|
import asyncio
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
from meshagent.cli import async_typer
|
|
6
|
+
|
|
5
7
|
from meshagent.cli import auth
|
|
6
8
|
from meshagent.cli import api_keys
|
|
7
9
|
from meshagent.cli import projects
|
|
@@ -18,7 +20,7 @@ from meshagent.cli import call
|
|
|
18
20
|
from meshagent.cli import cli_mcp
|
|
19
21
|
from meshagent.cli import chatbot
|
|
20
22
|
from meshagent.cli import voicebot
|
|
21
|
-
from meshagent.cli import
|
|
23
|
+
from meshagent.cli.exec import register as register_exec
|
|
22
24
|
|
|
23
25
|
from meshagent.cli import otel
|
|
24
26
|
|
|
@@ -38,7 +40,7 @@ otel.init(level=logging.INFO)
|
|
|
38
40
|
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
39
41
|
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
40
42
|
|
|
41
|
-
app =
|
|
43
|
+
app = async_typer.AsyncTyper()
|
|
42
44
|
app.add_typer(call.app, name="call")
|
|
43
45
|
app.add_typer(auth.app, name="auth")
|
|
44
46
|
app.add_typer(projects.app, name="project")
|
|
@@ -55,7 +57,8 @@ app.add_typer(cli_secrets.app, name="secret")
|
|
|
55
57
|
app.add_typer(cli_mcp.app, name="mcp")
|
|
56
58
|
app.add_typer(chatbot.app, name="chatbot")
|
|
57
59
|
app.add_typer(voicebot.app, name="voicebot")
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
register_exec(app)
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
def _run_async(coro):
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import tty as _tty
|
|
3
|
+
import termios
|
|
4
|
+
from meshagent.api.websocket_protocol import WebSocketClientProtocol
|
|
5
|
+
from meshagent.api import RoomClient
|
|
6
|
+
from meshagent.api.helpers import websocket_room_url
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
import asyncio
|
|
9
|
+
import typer
|
|
10
|
+
from rich import print
|
|
11
|
+
import aiohttp
|
|
12
|
+
import struct
|
|
13
|
+
import signal
|
|
14
|
+
import shutil
|
|
15
|
+
import json
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
from meshagent.api import ParticipantToken
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
|
|
22
|
+
from meshagent.cli.helper import (
|
|
23
|
+
get_client,
|
|
24
|
+
resolve_project_id,
|
|
25
|
+
resolve_api_key,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register(app: typer.Typer):
|
|
30
|
+
@app.async_command("exec")
|
|
31
|
+
async def exec_command(
|
|
32
|
+
*,
|
|
33
|
+
project_id: str = None,
|
|
34
|
+
room: Annotated[str, typer.Option()],
|
|
35
|
+
name: Annotated[Optional[str], typer.Option()] = None,
|
|
36
|
+
image: Annotated[Optional[str], typer.Option()] = None,
|
|
37
|
+
api_key_id: Annotated[Optional[str], typer.Option()] = None,
|
|
38
|
+
command: Annotated[list[str], typer.Argument(...)] = None,
|
|
39
|
+
tty: bool = False,
|
|
40
|
+
):
|
|
41
|
+
"""Open an interactive websocket‑based TTY."""
|
|
42
|
+
client = await get_client()
|
|
43
|
+
try:
|
|
44
|
+
project_id = await resolve_project_id(project_id=project_id)
|
|
45
|
+
api_key_id = await resolve_api_key(
|
|
46
|
+
project_id=project_id, api_key_id=api_key_id
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
token = ParticipantToken(
|
|
50
|
+
name="tty", project_id=project_id, api_key_id=api_key_id
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
key = (
|
|
54
|
+
await client.decrypt_project_api_key(
|
|
55
|
+
project_id=project_id, id=api_key_id
|
|
56
|
+
)
|
|
57
|
+
)["token"]
|
|
58
|
+
|
|
59
|
+
token.add_role_grant(role="user")
|
|
60
|
+
token.add_room_grant(room)
|
|
61
|
+
|
|
62
|
+
ws_url = (
|
|
63
|
+
websocket_room_url(room_name=room)
|
|
64
|
+
+ f"/exec?token={token.to_jwt(token=key)}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if image:
|
|
68
|
+
ws_url += f"&image={quote(' '.join(image))}"
|
|
69
|
+
|
|
70
|
+
if name:
|
|
71
|
+
ws_url += f"&name={quote(' '.join(name))}"
|
|
72
|
+
|
|
73
|
+
if command and len(command) != 0:
|
|
74
|
+
ws_url += f"&command={quote(' '.join(command))}"
|
|
75
|
+
|
|
76
|
+
if tty:
|
|
77
|
+
if not sys.stdin.isatty():
|
|
78
|
+
print("[red]TTY requested but process is not a TTY[/red]")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
ws_url += "&tty=true"
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
if command is None:
|
|
85
|
+
print("[red]TTY required when not executing a command[/red]")
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
|
|
88
|
+
ws_url += "&tty=false"
|
|
89
|
+
|
|
90
|
+
if tty:
|
|
91
|
+
# Save current terminal settings so we can restore them later.
|
|
92
|
+
old_tty_settings = termios.tcgetattr(sys.stdin)
|
|
93
|
+
_tty.setraw(sys.stdin)
|
|
94
|
+
|
|
95
|
+
async with RoomClient(
|
|
96
|
+
protocol=WebSocketClientProtocol(
|
|
97
|
+
url=websocket_room_url(room_name=room),
|
|
98
|
+
token=token.to_jwt(token=key),
|
|
99
|
+
)
|
|
100
|
+
):
|
|
101
|
+
try:
|
|
102
|
+
async with aiohttp.ClientSession() as session:
|
|
103
|
+
async with session.ws_connect(ws_url) as websocket:
|
|
104
|
+
send_queue = asyncio.Queue[bytes]()
|
|
105
|
+
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
(
|
|
108
|
+
stdout_transport,
|
|
109
|
+
stdout_protocol,
|
|
110
|
+
) = await loop.connect_write_pipe(
|
|
111
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
|
112
|
+
)
|
|
113
|
+
stdout_writer = asyncio.StreamWriter(
|
|
114
|
+
stdout_transport, stdout_protocol, None, loop
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
(
|
|
118
|
+
stderr_transport,
|
|
119
|
+
stderr_protocol,
|
|
120
|
+
) = await loop.connect_write_pipe(
|
|
121
|
+
asyncio.streams.FlowControlMixin, sys.stderr
|
|
122
|
+
)
|
|
123
|
+
stderr_writer = asyncio.StreamWriter(
|
|
124
|
+
stderr_transport, stderr_protocol, None, loop
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
async def recv_from_websocket():
|
|
128
|
+
while True:
|
|
129
|
+
done, pending = await asyncio.wait(
|
|
130
|
+
[asyncio.create_task(websocket.receive())],
|
|
131
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
first = done.pop()
|
|
135
|
+
|
|
136
|
+
if first == read_stdin_task:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
message = first.result()
|
|
140
|
+
|
|
141
|
+
if websocket.closed:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
if message.type == aiohttp.WSMsgType.CLOSE:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
elif message.type == aiohttp.WSMsgType.CLOSING:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
elif message.type == aiohttp.WSMsgType.ERROR:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
if not message.data:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
data: bytes = message.data
|
|
157
|
+
if len(data) > 0:
|
|
158
|
+
if data[0] == 1:
|
|
159
|
+
stderr_writer.write(data)
|
|
160
|
+
await stderr_writer.drain()
|
|
161
|
+
elif data[0] == 0:
|
|
162
|
+
stdout_writer.write(data)
|
|
163
|
+
await stdout_writer.drain()
|
|
164
|
+
else:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Invalid channel received {data[0]}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
last_size = None
|
|
170
|
+
|
|
171
|
+
async def send_resize(rows, cols):
|
|
172
|
+
nonlocal last_size
|
|
173
|
+
|
|
174
|
+
size = (cols, rows)
|
|
175
|
+
if size == last_size:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
last_size = size
|
|
179
|
+
|
|
180
|
+
resize_json = json.dumps(
|
|
181
|
+
{"Width": cols, "Height": rows}
|
|
182
|
+
).encode("utf-8")
|
|
183
|
+
payload = struct.pack("B", 4) + resize_json
|
|
184
|
+
send_queue.put_nowait(payload)
|
|
185
|
+
await asyncio.sleep(5)
|
|
186
|
+
|
|
187
|
+
cols, rows = shutil.get_terminal_size(fallback=(24, 80))
|
|
188
|
+
if tty:
|
|
189
|
+
await send_resize(rows, cols)
|
|
190
|
+
|
|
191
|
+
def on_sigwinch():
|
|
192
|
+
cols, rows = shutil.get_terminal_size(fallback=(24, 80))
|
|
193
|
+
task = asyncio.create_task(send_resize(rows, cols))
|
|
194
|
+
|
|
195
|
+
def on_done(t: asyncio.Task):
|
|
196
|
+
t.result()
|
|
197
|
+
|
|
198
|
+
task.add_done_callback(on_done)
|
|
199
|
+
|
|
200
|
+
loop.add_signal_handler(signal.SIGWINCH, on_sigwinch)
|
|
201
|
+
|
|
202
|
+
async def read_stdin():
|
|
203
|
+
loop = asyncio.get_running_loop()
|
|
204
|
+
|
|
205
|
+
reader = asyncio.StreamReader()
|
|
206
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
207
|
+
await loop.connect_read_pipe(
|
|
208
|
+
lambda: protocol, sys.stdin
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
while True:
|
|
212
|
+
# Read one character at a time from stdin without blocking the event loop.
|
|
213
|
+
done, pending = await asyncio.wait(
|
|
214
|
+
[
|
|
215
|
+
asyncio.create_task(reader.read(1)),
|
|
216
|
+
websocket_recv_task,
|
|
217
|
+
],
|
|
218
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
first = done.pop()
|
|
222
|
+
if first == websocket_recv_task:
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
data = first.result()
|
|
226
|
+
if not data:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
if websocket.closed:
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
if tty:
|
|
233
|
+
if data == b"\x04":
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if data:
|
|
237
|
+
send_queue.put_nowait(b"\0" + data)
|
|
238
|
+
else:
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
send_queue.put_nowait(b"\0")
|
|
242
|
+
|
|
243
|
+
websocket_recv_task = asyncio.create_task(
|
|
244
|
+
recv_from_websocket()
|
|
245
|
+
)
|
|
246
|
+
read_stdin_task = asyncio.create_task(read_stdin())
|
|
247
|
+
|
|
248
|
+
async def send_to_websocket():
|
|
249
|
+
while True:
|
|
250
|
+
try:
|
|
251
|
+
data = await send_queue.get()
|
|
252
|
+
if websocket.closed:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if data is not None:
|
|
256
|
+
await websocket.send_bytes(data)
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
break
|
|
260
|
+
except asyncio.QueueShutDown:
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
send_to_websocket_task = asyncio.create_task(
|
|
264
|
+
send_to_websocket()
|
|
265
|
+
)
|
|
266
|
+
await asyncio.gather(
|
|
267
|
+
websocket_recv_task,
|
|
268
|
+
read_stdin_task,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
send_queue.shutdown()
|
|
272
|
+
await send_to_websocket_task
|
|
273
|
+
|
|
274
|
+
finally:
|
|
275
|
+
if not sys.stdin.closed and tty:
|
|
276
|
+
# Restore original terminal settings even if the coroutine is cancelled.
|
|
277
|
+
termios.tcsetattr(
|
|
278
|
+
sys.stdin, termios.TCSADRAIN, old_tty_settings
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"[red]{e}[/red]")
|
|
283
|
+
logging.error("failed", exc_info=e)
|
|
284
|
+
raise typer.Exit(1)
|
|
285
|
+
finally:
|
|
286
|
+
await client.close()
|