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.

Files changed (38) hide show
  1. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/PKG-INFO +8 -5
  2. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/call.py +25 -4
  3. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli.py +6 -3
  4. meshagent_cli-0.2.0/meshagent/cli/exec.py +286 -0
  5. meshagent_cli-0.2.0/meshagent/cli/services.py +584 -0
  6. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/storage.py +1 -1
  7. meshagent_cli-0.2.0/meshagent/cli/version.py +1 -0
  8. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/PKG-INFO +8 -5
  9. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/SOURCES.txt +1 -1
  10. meshagent_cli-0.2.0/meshagent_cli.egg-info/requires.txt +14 -0
  11. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/pyproject.toml +8 -4
  12. meshagent_cli-0.0.39/meshagent/cli/services.py +0 -350
  13. meshagent_cli-0.0.39/meshagent/cli/tty.py +0 -118
  14. meshagent_cli-0.0.39/meshagent/cli/version.py +0 -1
  15. meshagent_cli-0.0.39/meshagent_cli.egg-info/requires.txt +0 -11
  16. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/README.md +0 -0
  17. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/__init__.py +0 -0
  18. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/agent.py +0 -0
  19. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/api_keys.py +0 -0
  20. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/async_typer.py +0 -0
  21. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/auth.py +0 -0
  22. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/auth_async.py +0 -0
  23. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/chatbot.py +0 -0
  24. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli_mcp.py +0 -0
  25. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/cli_secrets.py +0 -0
  26. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/developer.py +0 -0
  27. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/helper.py +0 -0
  28. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/messaging.py +0 -0
  29. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/otel.py +0 -0
  30. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/participant_token.py +0 -0
  31. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/projects.py +0 -0
  32. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/sessions.py +0 -0
  33. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/voicebot.py +0 -0
  34. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent/cli/webhook.py +0 -0
  35. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/dependency_links.txt +0 -0
  36. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/entry_points.txt +0 -0
  37. {meshagent_cli-0.0.39 → meshagent_cli-0.2.0}/meshagent_cli.egg-info/top_level.txt +0 -0
  38. {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.39
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.39
14
- Requires-Dist: meshagent-agents~=0.0.39
15
- Requires-Dist: meshagent-tools~=0.0.39
16
- Requires-Dist: meshagent-mcp~=0.0.39
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[str, typer.Option(..., help="Name of the agent to call")],
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=name, project_id=project_id, api_key_id=api_key_id
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=agent_name, url=url, arguments=json.loads(arguments)
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 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):
@@ -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()