meshagent-cli 0.1.0__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of meshagent-cli might be problematic. Click here for more details.

meshagent/cli/call.py CHANGED
@@ -117,7 +117,7 @@ async def make_call(
117
117
  )["token"]
118
118
 
119
119
  token = ParticipantToken(
120
- name="cli", project_id=project_id, api_key_id=api_key_id
120
+ name=participant_name, project_id=project_id, api_key_id=api_key_id
121
121
  )
122
122
  token.add_role_grant(role=role)
123
123
  token.add_room_grant(room)
meshagent/cli/cli.py CHANGED
@@ -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 tty
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 = typer.Typer()
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
- app.add_typer(tty.app, name="tty")
60
+
61
+ register_exec(app)
59
62
 
60
63
 
61
64
  def _run_async(coro):
meshagent/cli/exec.py ADDED
@@ -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()
meshagent/cli/storage.py CHANGED
@@ -112,7 +112,7 @@ async def storage_cp_command(
112
112
  ] = None,
113
113
  name: Annotated[
114
114
  str, typer.Option(..., help="Participant name (if copying to/from remote)")
115
- ],
115
+ ] = "cli",
116
116
  role: str = "user",
117
117
  source_path: str,
118
118
  dest_path: str,
meshagent/cli/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.0"
1
+ __version__ = "0.2.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshagent-cli
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: CLI for Meshagent
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Documentation, https://docs.meshagent.com
@@ -10,12 +10,12 @@ 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.1
14
- Requires-Dist: meshagent-agents~=0.1
15
- Requires-Dist: meshagent-computers~=0.1
16
- Requires-Dist: meshagent-openai~=0.1
17
- Requires-Dist: meshagent-tools~=0.1
18
- Requires-Dist: meshagent-mcp~=0.1
13
+ Requires-Dist: meshagent-api~=0.2.1
14
+ Requires-Dist: meshagent-agents~=0.2.1
15
+ Requires-Dist: meshagent-computers~=0.2.1
16
+ Requires-Dist: meshagent-openai~=0.2.1
17
+ Requires-Dist: meshagent-tools~=0.2.1
18
+ Requires-Dist: meshagent-mcp~=0.2.1
19
19
  Requires-Dist: supabase~=2.15
20
20
  Requires-Dist: fastmcp~=2.8
21
21
  Requires-Dist: opentelemetry-distro~=0.54b1
@@ -4,12 +4,13 @@ meshagent/cli/api_keys.py,sha256=4mBqyh_WAogaMIYhRBQOCjfsSSp1RY9BqiI_6DaGLmk,496
4
4
  meshagent/cli/async_typer.py,sha256=GCeSefBDbpd-V4V8LrvHGUTBMth3HspVMfFa-HUZ0cg,898
5
5
  meshagent/cli/auth.py,sha256=tPipbOtHnsrvJ-OUOE-lyfvvIhkTIGYlkgS81hLdyB8,783
6
6
  meshagent/cli/auth_async.py,sha256=mi2-u949M412PAMN-PCdHEiF9hGf0W3m1RAseZX07-w,4058
7
- meshagent/cli/call.py,sha256=IQePqq7PkAqnTVjqBah15VptgChWxV-G5ARKf0tzEBc,5295
7
+ meshagent/cli/call.py,sha256=aVRs-7QDZWxK8jYc71oACNIkHuoDe1KUyfl8EtUbj-0,5306
8
8
  meshagent/cli/chatbot.py,sha256=pK-_G3CiMbV-NC0iXzbjztu4f-ZAeXNzcmOB-HxJWQo,7928
9
- meshagent/cli/cli.py,sha256=D2cIVAgYZ095XqWBrvd7Qq60yGL7pfLTIK4Y52xrKSM,6314
9
+ meshagent/cli/cli.py,sha256=Xltp1pICOdWEoCexeZz1taHPcRhRv7dM2e7tktyrd2k,6376
10
10
  meshagent/cli/cli_mcp.py,sha256=BTTAMn3u4i1uTDItFxmxMTsSAvFaWtdI3YdZngHz11g,10641
11
11
  meshagent/cli/cli_secrets.py,sha256=lkfR8tVjOA5KdynAhKCg5Z2EJkgrHFTX43pdB60YiU4,13767
12
12
  meshagent/cli/developer.py,sha256=JWz-qcduCbFopQ1DNGbwrknzFt59KBLIXx8DyD6PxZM,3081
13
+ meshagent/cli/exec.py,sha256=46OyTVr3ejfDuNjBKkV9vkk62O808isD7bEC6jUgQHc,11338
13
14
  meshagent/cli/helper.py,sha256=gbd6Tvlp7CObPp3srWm-mbAe7tBD0iacg6PitGWJdtw,4874
14
15
  meshagent/cli/messaging.py,sha256=cr-oVAu_s8uEPUm3GELSq8yaVDnEWlt02D5V4KbA6wc,6437
15
16
  meshagent/cli/otel.py,sha256=1yoMGivskLV9f7M_LqCLKbttTTAPmFY5yhWXqFzvRN8,4066
@@ -17,13 +18,12 @@ meshagent/cli/participant_token.py,sha256=N07hblRjj0zDKxl5CQXJjIMmft5s9mWgKZKz-V
17
18
  meshagent/cli/projects.py,sha256=WaO7M-D4ghy0nVpvwZ08ZEcybZ_e_cFe6rEoNxyAWkc,3306
18
19
  meshagent/cli/services.py,sha256=1afeMH41Kj0-1uAKB72buWnPFcgaqT4xFVO4FEpTPC0,19308
19
20
  meshagent/cli/sessions.py,sha256=MP7XhrtkUrealdpl8IKrTR3u9sjF15sG175-Y_m7nps,800
20
- meshagent/cli/storage.py,sha256=Q3ajuC6j4GLlU4jZoUW_Zsl1dOr04vzLibw2DPedJJ4,35564
21
- meshagent/cli/tty.py,sha256=-XQE2W8_nW74vRG8jnqPofnfPB5bAvBX7_1-wMcBR1w,4207
22
- meshagent/cli/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
21
+ meshagent/cli/storage.py,sha256=wtLKYFyfsErCePxCG962Xb--moCN-zj9uVCtRPhIpfA,35572
22
+ meshagent/cli/version.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
23
23
  meshagent/cli/voicebot.py,sha256=Ykn9mUhcwl03ZCPDRua6moeNyrvToGPowQfjhPM0zqA,5032
24
24
  meshagent/cli/webhook.py,sha256=r5zct-UBQYSq3BWmnZRrHVOEHVlkY0j8uDxGVn3Pbxo,2902
25
- meshagent_cli-0.1.0.dist-info/METADATA,sha256=c75FMbewaNMuydlR3quvd91ja9mL_mCp1sCFJ1-xnbI,1436
26
- meshagent_cli-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- meshagent_cli-0.1.0.dist-info/entry_points.txt,sha256=WRcGGN4vMtvC5Pgl3uRFqsJiQXNoHuLLa-TCSY3gAhQ,52
28
- meshagent_cli-0.1.0.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
29
- meshagent_cli-0.1.0.dist-info/RECORD,,
25
+ meshagent_cli-0.2.1.dist-info/METADATA,sha256=_YNXB_L6Rp1ll2t2ybHSwOXfp79RzDw2dT5UJ1NAgVE,1448
26
+ meshagent_cli-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ meshagent_cli-0.2.1.dist-info/entry_points.txt,sha256=WRcGGN4vMtvC5Pgl3uRFqsJiQXNoHuLLa-TCSY3gAhQ,52
28
+ meshagent_cli-0.2.1.dist-info/top_level.txt,sha256=GlcXnHtRP6m7zlG3Df04M35OsHtNXy_DY09oFwWrH74,10
29
+ meshagent_cli-0.2.1.dist-info/RECORD,,
meshagent/cli/tty.py DELETED
@@ -1,124 +0,0 @@
1
- import sys
2
- import tty
3
- import termios
4
- from meshagent.api.helpers import websocket_room_url
5
- from typing import Annotated, Optional
6
- import os
7
-
8
- import asyncio
9
- import typer
10
- from rich import print
11
- import aiohttp
12
-
13
- from meshagent.api import ParticipantToken
14
- from meshagent.cli import async_typer
15
- from meshagent.cli.helper import (
16
- get_client,
17
- resolve_project_id,
18
- resolve_api_key,
19
- )
20
-
21
-
22
- app = async_typer.AsyncTyper()
23
-
24
-
25
- @app.async_command("connect")
26
- async def tty_command(
27
- *,
28
- project_id: str = None,
29
- room: Annotated[str, typer.Option()],
30
- api_key_id: Annotated[Optional[str], typer.Option()] = None,
31
- ):
32
- """Open an interactive websocket‑based TTY."""
33
- client = await get_client()
34
- try:
35
- project_id = await resolve_project_id(project_id=project_id)
36
- api_key_id = await resolve_api_key(project_id=project_id, api_key_id=api_key_id)
37
-
38
- token = ParticipantToken(
39
- name="tty", project_id=project_id, api_key_id=api_key_id
40
- )
41
-
42
- key = (
43
- await client.decrypt_project_api_key(project_id=project_id, id=api_key_id)
44
- )["token"]
45
-
46
- token.add_role_grant(role="user")
47
- token.add_room_grant(room)
48
-
49
- ws_url = (
50
- websocket_room_url(room_name=room) + f"/tty?token={token.to_jwt(token=key)}"
51
- )
52
-
53
- print(f"[bold green]Connecting to[/bold green] {room}")
54
-
55
- # Save current terminal settings so we can restore them later.
56
- old_tty_settings = termios.tcgetattr(sys.stdin)
57
-
58
- try:
59
- async with aiohttp.ClientSession() as session:
60
- async with session.ws_connect(ws_url) as websocket:
61
- tty.setraw(sys.stdin)
62
-
63
- loop = asyncio.get_running_loop()
64
- transport, protocol = await loop.connect_write_pipe(
65
- asyncio.streams.FlowControlMixin, sys.stdout
66
- )
67
- writer = asyncio.StreamWriter(transport, protocol, None, loop)
68
-
69
- async def recv_from_websocket():
70
- async for message in websocket:
71
- if message.type == aiohttp.WSMsgType.CLOSE:
72
- await websocket.close()
73
-
74
- elif message.type == aiohttp.WSMsgType.ERROR:
75
- await websocket.close()
76
-
77
- data: bytes = message.data
78
- writer.write(data)
79
- await writer.drain()
80
-
81
- async def send_to_websocket():
82
- loop = asyncio.get_running_loop()
83
-
84
- reader = asyncio.StreamReader()
85
- protocol = asyncio.StreamReaderProtocol(reader)
86
- await loop.connect_read_pipe(lambda: protocol, sys.stdin)
87
-
88
- while True:
89
- # Read one character at a time from stdin without blocking the event loop.
90
-
91
- data = await reader.read(1)
92
- if not data:
93
- break
94
-
95
- if websocket.closed:
96
- break
97
-
98
- if data == b"\x03":
99
- print("<CTRL-C>\n")
100
- break
101
-
102
- if data:
103
- await websocket.send_bytes(data)
104
- else:
105
- await websocket.close(code=1000)
106
- break
107
-
108
- done, pending = await asyncio.wait(
109
- [
110
- asyncio.create_task(recv_from_websocket()),
111
- asyncio.create_task(send_to_websocket()),
112
- ],
113
- return_when=asyncio.FIRST_COMPLETED,
114
- )
115
-
116
- for task in pending:
117
- task.cancel()
118
-
119
- finally:
120
- # Restore original terminal settings even if the coroutine is cancelled.
121
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty_settings)
122
-
123
- finally:
124
- await client.close()