meshagent-cli 0.7.0__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.
meshagent/cli/cli.py ADDED
@@ -0,0 +1,186 @@
1
+ import typer
2
+ import asyncio
3
+
4
+ from meshagent.cli import async_typer
5
+
6
+ from meshagent.cli import queue
7
+ from meshagent.cli import auth
8
+ from meshagent.cli import api_keys
9
+ from meshagent.cli import projects
10
+ from meshagent.cli import sessions
11
+ from meshagent.cli import participant_token
12
+ from meshagent.cli import agent
13
+ from meshagent.cli import messaging
14
+ from meshagent.cli import storage
15
+ from meshagent.cli import developer
16
+ from meshagent.cli import webhook
17
+ from meshagent.cli import services
18
+ from meshagent.cli import cli_secrets
19
+ from meshagent.cli import call
20
+ from meshagent.cli import cli_mcp
21
+ from meshagent.cli import chatbot
22
+ from meshagent.cli import voicebot
23
+ from meshagent.cli import mailbot
24
+ from meshagent.cli import containers
25
+ from meshagent.cli import oauth2
26
+ from meshagent.cli import helpers
27
+ from meshagent.cli import meeting_transcriber
28
+ from meshagent.cli import room
29
+ from meshagent.cli.exec import register as register_exec
30
+ from meshagent.cli.version import __version__
31
+ from meshagent.cli.helper import get_active_api_key
32
+ from meshagent.otel import otel_config
33
+
34
+
35
+ from art import tprint
36
+
37
+ import logging
38
+
39
+ import os
40
+ import sys
41
+ from pathlib import Path
42
+
43
+ otel_config(service_name="meshagent-cli")
44
+
45
+
46
+ # Turn down OpenAI logs, they are a bit noisy
47
+ logging.getLogger("openai").setLevel(logging.ERROR)
48
+ logging.getLogger("httpx").setLevel(logging.ERROR)
49
+
50
+ app = async_typer.AsyncTyper(no_args_is_help=True)
51
+ app.add_typer(call.app, name="call")
52
+ app.add_typer(auth.app, name="auth")
53
+ app.add_typer(projects.app, name="project")
54
+ app.add_typer(api_keys.app, name="api-key")
55
+ app.add_typer(sessions.app, name="session")
56
+ app.add_typer(participant_token.app, name="participant-token")
57
+ app.add_typer(agent.app, name="agents")
58
+ app.add_typer(messaging.app, name="messaging")
59
+ app.add_typer(storage.app, name="storage")
60
+ app.add_typer(developer.app, name="developer")
61
+ app.add_typer(webhook.app, name="webhook")
62
+ app.add_typer(services.app, name="service")
63
+ app.add_typer(cli_secrets.app, name="secret")
64
+ app.add_typer(queue.app, name="queue")
65
+ app.add_typer(cli_mcp.app, name="mcp")
66
+ app.add_typer(chatbot.app, name="chatbot")
67
+ app.add_typer(voicebot.app, name="voicebot")
68
+ app.add_typer(mailbot.app, name="mailbot")
69
+ app.add_typer(containers.app, name="container")
70
+ app.add_typer(oauth2.app, name="oauth2")
71
+ app.add_typer(helpers.app, name="helpers")
72
+ app.add_typer(room.app, name="room")
73
+ app.add_typer(meeting_transcriber.app, name="meeting-transcriber")
74
+
75
+ register_exec(app)
76
+
77
+
78
+ def _run_async(coro):
79
+ asyncio.run(coro)
80
+
81
+
82
+ def detect_shell() -> str:
83
+ """
84
+ Best-effort detection of the *current* interactive shell.
85
+
86
+ Order of preference
87
+ 1. Explicit --shell argument (handled by Typer)
88
+ 2. Per-shell env vars set by the running shell
89
+ • BASH_VERSION / ZSH_VERSION / FISH_VERSION
90
+ 3. $SHELL on POSIX (user’s login shell – still correct >90 % of the time)
91
+ 4. Parent process on Windows (COMSPEC → cmd / powershell)
92
+ 5. Safe default: 'bash'
93
+ """
94
+ # Per-shell version variables (works even if login shell ≠ current shell)
95
+ for var, name in (
96
+ ("ZSH_VERSION", "zsh"),
97
+ ("BASH_VERSION", "bash"),
98
+ ("FISH_VERSION", "fish"),
99
+ ):
100
+ if var in os.environ:
101
+ return name
102
+
103
+ # POSIX fallback: login shell path
104
+ sh = os.environ.get("SHELL")
105
+ if sh:
106
+ return Path(sh).name.lower()
107
+
108
+ # Windows heuristics
109
+ if sys.platform == "win32":
110
+ comspec = Path(os.environ.get("COMSPEC", "")).name.lower()
111
+ if "powershell" in comspec:
112
+ return "powershell"
113
+ if "cmd" in comspec:
114
+ return "cmd"
115
+ return "powershell" # sensible default on modern Windows
116
+
117
+ # Last-ditch default
118
+ return "bash"
119
+
120
+
121
+ def _bash_like(name: str, value: str, unset: bool) -> str:
122
+ return f"unset {name}" if unset else f'export {name}="{value}"'
123
+
124
+
125
+ def _fish(name: str, value: str, unset: bool) -> str:
126
+ return f"set -e {name}" if unset else f'set -gx {name} "{value}"'
127
+
128
+
129
+ def _powershell(name: str, value: str, unset: bool) -> str:
130
+ return f"Remove-Item Env:{name}" if unset else f'$Env:{name}="{value}"'
131
+
132
+
133
+ def _cmd(name: str, value: str, unset: bool) -> str:
134
+ return f"set {name}=" if unset else f"set {name}={value}"
135
+
136
+
137
+ SHELL_RENDERERS = {
138
+ "bash": _bash_like,
139
+ "zsh": _bash_like,
140
+ "fish": _fish,
141
+ "powershell": _powershell,
142
+ "cmd": _cmd,
143
+ }
144
+
145
+
146
+ @app.command(
147
+ "version",
148
+ help="Print the version",
149
+ )
150
+ def version():
151
+ print(__version__)
152
+
153
+
154
+ @app.command("setup")
155
+ def setup_command():
156
+ """Perform initial login and project/api key activation."""
157
+
158
+ async def runner():
159
+ print("\n", flush=True)
160
+ tprint("MeshAgent", "tarty10")
161
+ print("\n", flush=True)
162
+ await auth.login()
163
+ print("Activate a project...")
164
+ project_id = await projects.activate(None, interactive=True)
165
+ if project_id is None:
166
+ print("You have choosen to not activate a project. Exiting.")
167
+ if (
168
+ project_id is not None
169
+ and await get_active_api_key(project_id=project_id) is None
170
+ ):
171
+ if typer.confirm(
172
+ "You do not have an active api key for this project. Would you like to create and activate a new api key?",
173
+ default=True,
174
+ ):
175
+ name = typer.prompt(
176
+ "Enter a name for your API Key (must be a unique name):"
177
+ )
178
+ await api_keys.create(
179
+ project_id=None, activate=True, silent=True, name=name
180
+ )
181
+
182
+ _run_async(runner())
183
+
184
+
185
+ if __name__ == "__main__":
186
+ app()
@@ -0,0 +1,344 @@
1
+ import typer
2
+ from rich import print
3
+ from typing import Annotated, Optional, List
4
+ from meshagent.cli.common_options import ProjectIdOption, RoomOption
5
+
6
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
7
+ from meshagent.api import RoomClient, WebSocketClientProtocol, RoomException
8
+ from meshagent.cli import async_typer
9
+ from meshagent.cli.helper import (
10
+ get_client,
11
+ resolve_project_id,
12
+ resolve_room,
13
+ resolve_key,
14
+ )
15
+
16
+ from meshagent.tools.hosting import RemoteToolkit
17
+
18
+
19
+ from meshagent.api.services import ServiceHost
20
+ import os
21
+
22
+ import shlex
23
+
24
+ from meshagent.api import ParticipantToken, ApiScope
25
+
26
+
27
+ def _kv_to_dict(pairs: List[str]) -> dict[str, str]:
28
+ """Convert ["A=1","B=2"] → {"A":"1","B":"2"}."""
29
+ out: dict[str, str] = {}
30
+ for p in pairs:
31
+ if "=" not in p:
32
+ raise typer.BadParameter(f"'{p}' must be KEY=VALUE")
33
+ k, v = p.split("=", 1)
34
+ out[k] = v
35
+ return out
36
+
37
+
38
+ app = async_typer.AsyncTyper()
39
+
40
+
41
+ @app.async_command("sse")
42
+ async def sse(
43
+ *,
44
+ project_id: ProjectIdOption = None,
45
+ room: RoomOption,
46
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
47
+ role: str = "tool",
48
+ url: Annotated[str, typer.Option()],
49
+ toolkit_name: Annotated[Optional[str], typer.Option()] = None,
50
+ key: Annotated[
51
+ str,
52
+ typer.Option("--key", help="an api key to sign the token with"),
53
+ ] = None,
54
+ ):
55
+ from mcp.client.session import ClientSession
56
+ from mcp.client.sse import sse_client
57
+
58
+ from meshagent.mcp import MCPToolkit
59
+
60
+ key = await resolve_key(project_id=project_id, key=key)
61
+
62
+ if toolkit_name is None:
63
+ toolkit_name = "mcp"
64
+
65
+ account_client = await get_client()
66
+ try:
67
+ project_id = await resolve_project_id(project_id=project_id)
68
+ room = resolve_room(room)
69
+
70
+ token = ParticipantToken(
71
+ name=name,
72
+ )
73
+
74
+ token.add_api_grant(ApiScope.agent_default())
75
+
76
+ token.add_role_grant(role=role)
77
+ token.add_room_grant(room)
78
+
79
+ jwt = token.to_jwt(api_key=key)
80
+
81
+ print("[bold green]Connecting to room...[/bold green]")
82
+ async with RoomClient(
83
+ protocol=WebSocketClientProtocol(
84
+ url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
85
+ token=jwt,
86
+ )
87
+ ) as client:
88
+ async with sse_client(url) as (read_stream, write_stream):
89
+ async with ClientSession(
90
+ read_stream=read_stream, write_stream=write_stream
91
+ ) as session:
92
+ mcp_tools_response = await session.list_tools()
93
+
94
+ toolkit = MCPToolkit(
95
+ name=toolkit_name,
96
+ session=session,
97
+ tools=mcp_tools_response.tools,
98
+ )
99
+
100
+ remote_toolkit = RemoteToolkit(
101
+ name=toolkit.name,
102
+ tools=toolkit.tools,
103
+ title=toolkit.title,
104
+ description=toolkit.description,
105
+ )
106
+
107
+ await remote_toolkit.start(room=client)
108
+ try:
109
+ await client.protocol.wait_for_close()
110
+ except KeyboardInterrupt:
111
+ await remote_toolkit.stop()
112
+
113
+ except RoomException as e:
114
+ print(f"[red]{e}[/red]")
115
+ finally:
116
+ await account_client.close()
117
+
118
+
119
+ @app.async_command("stdio")
120
+ async def stdio(
121
+ *,
122
+ project_id: ProjectIdOption = None,
123
+ room: RoomOption,
124
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
125
+ role: str = "tool",
126
+ command: Annotated[str, typer.Option()],
127
+ toolkit_name: Annotated[Optional[str], typer.Option()] = None,
128
+ env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
129
+ key: Annotated[
130
+ str,
131
+ typer.Option("--key", help="an api key to sign the token with"),
132
+ ] = None,
133
+ ):
134
+ from mcp.client.session import ClientSession
135
+ from mcp.client.stdio import stdio_client, StdioServerParameters
136
+
137
+ from meshagent.mcp import MCPToolkit
138
+
139
+ key = await resolve_key(project_id=project_id, key=key)
140
+
141
+ if toolkit_name is None:
142
+ toolkit_name = "mcp"
143
+
144
+ account_client = await get_client()
145
+ try:
146
+ project_id = await resolve_project_id(project_id=project_id)
147
+ room = resolve_room(room)
148
+
149
+ token = ParticipantToken(
150
+ name=name,
151
+ )
152
+
153
+ token.add_api_grant(ApiScope.agent_default())
154
+
155
+ token.add_role_grant(role=role)
156
+ token.add_room_grant(room)
157
+
158
+ jwt = token.to_jwt(api_key=key)
159
+
160
+ print("[bold green]Connecting to room...[/bold green]")
161
+ async with RoomClient(
162
+ protocol=WebSocketClientProtocol(
163
+ url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
164
+ token=jwt,
165
+ )
166
+ ) as client:
167
+ parsed_command = shlex.split(command)
168
+
169
+ async with (
170
+ stdio_client(
171
+ StdioServerParameters(
172
+ command=parsed_command[0], # Executable
173
+ args=parsed_command[1:], # Optional command line arguments
174
+ env=_kv_to_dict(env), # Optional environment variables
175
+ )
176
+ ) as (read_stream, write_stream)
177
+ ):
178
+ async with ClientSession(
179
+ read_stream=read_stream, write_stream=write_stream
180
+ ) as session:
181
+ mcp_tools_response = await session.list_tools()
182
+
183
+ toolkit = MCPToolkit(
184
+ name=toolkit_name,
185
+ session=session,
186
+ tools=mcp_tools_response.tools,
187
+ )
188
+
189
+ remote_toolkit = RemoteToolkit(
190
+ name=toolkit.name,
191
+ tools=toolkit.tools,
192
+ title=toolkit.title,
193
+ description=toolkit.description,
194
+ )
195
+
196
+ await remote_toolkit.start(room=client)
197
+ try:
198
+ await client.protocol.wait_for_close()
199
+ except KeyboardInterrupt:
200
+ await remote_toolkit.stop()
201
+
202
+ except RoomException as e:
203
+ print(f"[red]{e}[/red]")
204
+ finally:
205
+ await account_client.close()
206
+
207
+
208
+ @app.async_command("http-proxy")
209
+ async def stdio_host(
210
+ *,
211
+ command: Annotated[str, typer.Option()],
212
+ host: Annotated[Optional[str], typer.Option()] = None,
213
+ port: Annotated[Optional[int], typer.Option()] = None,
214
+ path: Annotated[Optional[str], typer.Option()] = None,
215
+ name: Annotated[Optional[str], typer.Option()] = None,
216
+ env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
217
+ ):
218
+ from fastmcp import FastMCP, Client
219
+ from fastmcp.client.transports import StdioTransport
220
+
221
+ parsed_command = shlex.split(command)
222
+
223
+ # Create a client that connects to the original server
224
+ proxy_client = Client(
225
+ transport=StdioTransport(
226
+ parsed_command[0], parsed_command[1:], _kv_to_dict(env)
227
+ ),
228
+ )
229
+
230
+ if name is None:
231
+ name = "Stdio-to-Streamable Http Proxy"
232
+
233
+ # Create a proxy server that connects to the client and exposes its capabilities
234
+ proxy = FastMCP.as_proxy(proxy_client, name=name)
235
+ if path is None:
236
+ path = "/mcp"
237
+
238
+ await proxy.run_async(transport="streamable-http", host=host, port=port, path=path)
239
+
240
+
241
+ @app.async_command("sse-proxy")
242
+ async def sse_proxy(
243
+ *,
244
+ command: Annotated[str, typer.Option()],
245
+ host: Annotated[Optional[str], typer.Option()] = None,
246
+ port: Annotated[Optional[int], typer.Option()] = None,
247
+ path: Annotated[Optional[str], typer.Option()] = None,
248
+ name: Annotated[Optional[str], typer.Option()] = None,
249
+ env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
250
+ ):
251
+ from fastmcp import FastMCP, Client
252
+ from fastmcp.client.transports import StdioTransport
253
+
254
+ parsed_command = shlex.split(command)
255
+
256
+ # Create a client that connects to the original server
257
+ proxy_client = Client(
258
+ transport=StdioTransport(
259
+ parsed_command[0], parsed_command[1:], _kv_to_dict(env)
260
+ ),
261
+ )
262
+
263
+ if name is None:
264
+ name = "Stdio-to-SSE Proxy"
265
+
266
+ # Create a proxy server that connects to the client and exposes its capabilities
267
+ proxy = FastMCP.as_proxy(proxy_client, name=name)
268
+ if path is None:
269
+ path = "/sse"
270
+
271
+ await proxy.run_async(transport="sse", host=host, port=port, path=path)
272
+
273
+
274
+ @app.async_command("stdio-service")
275
+ async def stdio_service(
276
+ *,
277
+ command: Annotated[str, typer.Option()],
278
+ host: Annotated[Optional[str], typer.Option()] = None,
279
+ port: Annotated[Optional[int], typer.Option()] = None,
280
+ webhook_secret: Annotated[Optional[str], typer.Option()] = None,
281
+ path: Annotated[Optional[str], typer.Option()] = None,
282
+ toolkit_name: Annotated[Optional[str], typer.Option()] = None,
283
+ env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
284
+ ):
285
+ from mcp.client.session import ClientSession
286
+ from mcp.client.stdio import stdio_client, StdioServerParameters
287
+
288
+ from meshagent.mcp import MCPToolkit
289
+
290
+ try:
291
+ parsed_command = shlex.split(command)
292
+
293
+ async with (
294
+ stdio_client(
295
+ StdioServerParameters(
296
+ command=parsed_command[0], # Executable
297
+ args=parsed_command[1:], # Optional command line arguments
298
+ env=_kv_to_dict(env), # Optional environment variables
299
+ )
300
+ ) as (read_stream, write_stream)
301
+ ):
302
+ async with ClientSession(
303
+ read_stream=read_stream, write_stream=write_stream
304
+ ) as session:
305
+ mcp_tools_response = await session.list_tools()
306
+
307
+ if toolkit_name is None:
308
+ toolkit_name = "mcp"
309
+
310
+ toolkit = MCPToolkit(
311
+ name=toolkit_name, session=session, tools=mcp_tools_response.tools
312
+ )
313
+
314
+ if port is None:
315
+ port = int(os.getenv("MESHAGENT_PORT", "8080"))
316
+
317
+ if host is None:
318
+ host = "0.0.0.0"
319
+
320
+ service_host = ServiceHost(
321
+ host=host, port=port, webhook_secret=webhook_secret
322
+ )
323
+
324
+ if path is None:
325
+ path = "/service"
326
+
327
+ print(
328
+ f"[bold green]Starting service host on {host}:{port}{path}...[/bold green]"
329
+ )
330
+
331
+ @service_host.path(path=path)
332
+ class CustomToolkit(RemoteToolkit):
333
+ def __init__(self):
334
+ super().__init__(
335
+ name=toolkit.name,
336
+ tools=toolkit.tools,
337
+ title=toolkit.title,
338
+ description=toolkit.description,
339
+ )
340
+
341
+ await service_host.run()
342
+
343
+ except RoomException as e:
344
+ print(f"[red]{e}[/red]")