meshagent-cli 0.7.0__py3-none-any.whl → 0.21.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.
Files changed (41) hide show
  1. meshagent/cli/agent.py +15 -11
  2. meshagent/cli/api_keys.py +4 -4
  3. meshagent/cli/async_typer.py +52 -4
  4. meshagent/cli/call.py +12 -8
  5. meshagent/cli/chatbot.py +1007 -129
  6. meshagent/cli/cli.py +21 -20
  7. meshagent/cli/cli_mcp.py +92 -28
  8. meshagent/cli/cli_secrets.py +10 -10
  9. meshagent/cli/common_options.py +19 -4
  10. meshagent/cli/containers.py +164 -16
  11. meshagent/cli/database.py +997 -0
  12. meshagent/cli/developer.py +3 -3
  13. meshagent/cli/exec.py +22 -6
  14. meshagent/cli/helper.py +62 -11
  15. meshagent/cli/helpers.py +66 -9
  16. meshagent/cli/host.py +37 -0
  17. meshagent/cli/mailbot.py +1004 -40
  18. meshagent/cli/mailboxes.py +223 -0
  19. meshagent/cli/meeting_transcriber.py +10 -4
  20. meshagent/cli/messaging.py +7 -7
  21. meshagent/cli/multi.py +402 -0
  22. meshagent/cli/oauth2.py +44 -21
  23. meshagent/cli/participant_token.py +5 -3
  24. meshagent/cli/port.py +70 -0
  25. meshagent/cli/queue.py +2 -2
  26. meshagent/cli/room.py +20 -212
  27. meshagent/cli/rooms.py +214 -0
  28. meshagent/cli/services.py +32 -23
  29. meshagent/cli/sessions.py +5 -5
  30. meshagent/cli/storage.py +5 -5
  31. meshagent/cli/task_runner.py +770 -0
  32. meshagent/cli/version.py +1 -1
  33. meshagent/cli/voicebot.py +502 -76
  34. meshagent/cli/webhook.py +7 -7
  35. meshagent/cli/worker.py +1327 -0
  36. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/METADATA +13 -13
  37. meshagent_cli-0.21.0.dist-info/RECORD +44 -0
  38. meshagent_cli-0.7.0.dist-info/RECORD +0 -36
  39. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/WHEEL +0 -0
  40. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/entry_points.txt +0 -0
  41. {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,223 @@
1
+ # meshagent/cli/mailboxes.py
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated, Optional
6
+
7
+ import typer
8
+ from aiohttp import ClientResponseError
9
+ from rich import print
10
+
11
+ from meshagent.cli import async_typer
12
+ from meshagent.cli.common_options import ProjectIdOption, OutputFormatOption
13
+ from meshagent.cli.helper import get_client, print_json_table, resolve_project_id
14
+
15
+ app = async_typer.AsyncTyper(help="Manage mailboxes for your project")
16
+
17
+
18
+ @app.async_command("create")
19
+ async def mailbox_create(
20
+ *,
21
+ project_id: ProjectIdOption,
22
+ address: Annotated[
23
+ str,
24
+ typer.Option(
25
+ "--address",
26
+ "-a",
27
+ help="Mailbox email address (unique per project)",
28
+ ),
29
+ ],
30
+ room: Annotated[
31
+ str,
32
+ typer.Option(
33
+ "--room",
34
+ "-r",
35
+ help="Room name to route inbound mail into",
36
+ ),
37
+ ],
38
+ queue: Annotated[
39
+ str,
40
+ typer.Option(
41
+ "--queue",
42
+ "-q",
43
+ help="Queue name to deliver inbound messages to",
44
+ ),
45
+ ],
46
+ public: Annotated[
47
+ bool,
48
+ typer.Option(
49
+ "--public",
50
+ help="Queue name to deliver inbound messages to",
51
+ ),
52
+ ] = False,
53
+ ):
54
+ """Create a mailbox attached to the project."""
55
+ client = await get_client()
56
+ try:
57
+ project_id = await resolve_project_id(project_id)
58
+
59
+ try:
60
+ await client.create_mailbox(
61
+ project_id=project_id,
62
+ address=address,
63
+ room=room,
64
+ queue=queue,
65
+ public=public,
66
+ )
67
+ except ClientResponseError as exc:
68
+ # Common patterns: 409 conflict on duplicate address, 400 validation, etc.
69
+ if exc.status == 409:
70
+ print(f"[red]Mailbox address already in use:[/] {address}")
71
+ raise typer.Exit(code=1)
72
+ raise
73
+ else:
74
+ print(f"[green]Created mailbox:[/] {address}")
75
+ finally:
76
+ await client.close()
77
+
78
+
79
+ @app.async_command("update")
80
+ async def mailbox_update(
81
+ *,
82
+ project_id: ProjectIdOption,
83
+ address: Annotated[
84
+ str,
85
+ typer.Argument(help="Mailbox email address to update"),
86
+ ],
87
+ room: Annotated[
88
+ Optional[str],
89
+ typer.Option(
90
+ "--room",
91
+ "-r",
92
+ help="Room name to route inbound mail into",
93
+ ),
94
+ ] = None,
95
+ queue: Annotated[
96
+ Optional[str],
97
+ typer.Option(
98
+ "--queue",
99
+ "-q",
100
+ help="Queue name to deliver inbound messages to",
101
+ ),
102
+ ] = None,
103
+ public: Annotated[
104
+ bool,
105
+ typer.Option(
106
+ "--public",
107
+ help="Queue name to deliver inbound messages to",
108
+ ),
109
+ ] = False,
110
+ ):
111
+ """Update a mailbox routing configuration."""
112
+ client = await get_client()
113
+ try:
114
+ project_id = await resolve_project_id(project_id)
115
+
116
+ # Keep parity with other CLIs: allow partial update by reading existing first
117
+ if room is None or queue is None:
118
+ try:
119
+ mb = await client.get_mailbox(project_id=project_id, address=address)
120
+ except ClientResponseError as exc:
121
+ if exc.status == 404:
122
+ print(f"[red]Mailbox not found:[/] {address}")
123
+ raise typer.Exit(code=1)
124
+ raise
125
+ room = room or mb.room
126
+ queue = queue or mb.queue
127
+
128
+ try:
129
+ await client.update_mailbox(
130
+ project_id=project_id,
131
+ address=address,
132
+ room=room,
133
+ queue=queue,
134
+ public=public,
135
+ )
136
+ except ClientResponseError as exc:
137
+ if exc.status == 404:
138
+ print(f"[red]Mailbox not found:[/] {address}")
139
+ raise typer.Exit(code=1)
140
+ raise
141
+ else:
142
+ print(f"[green]Updated mailbox:[/] {address}")
143
+ finally:
144
+ await client.close()
145
+
146
+
147
+ @app.async_command("show")
148
+ async def mailbox_show(
149
+ *,
150
+ project_id: ProjectIdOption,
151
+ address: Annotated[str, typer.Argument(help="Mailbox address to show")],
152
+ ):
153
+ """Show mailbox details."""
154
+ client = await get_client()
155
+ try:
156
+ project_id = await resolve_project_id(project_id)
157
+ try:
158
+ mb = await client.get_mailbox(project_id=project_id, address=address)
159
+ except ClientResponseError as exc:
160
+ if exc.status == 404:
161
+ print(f"[red]Mailbox not found:[/] {address}")
162
+ raise typer.Exit(code=1)
163
+ raise
164
+ print(mb.model_dump(mode="json"))
165
+ finally:
166
+ await client.close()
167
+
168
+
169
+ @app.async_command("list")
170
+ async def mailbox_list(
171
+ *,
172
+ project_id: ProjectIdOption,
173
+ o: OutputFormatOption = "table",
174
+ ):
175
+ """List mailboxes for the project."""
176
+ client = await get_client()
177
+ try:
178
+ project_id = await resolve_project_id(project_id)
179
+ mailboxes = await client.list_mailboxes(project_id=project_id)
180
+
181
+ if o == "json":
182
+ # Keep your existing conventions: wrap in an object.
183
+ print({"mailboxes": [mb.model_dump(mode="json") for mb in mailboxes]})
184
+ else:
185
+ print_json_table(
186
+ [
187
+ {
188
+ "address": mb.address,
189
+ "room": mb.room,
190
+ "queue": mb.queue,
191
+ "public": mb.public,
192
+ }
193
+ for mb in mailboxes
194
+ ],
195
+ "address",
196
+ "room",
197
+ "queue",
198
+ )
199
+ finally:
200
+ await client.close()
201
+
202
+
203
+ @app.async_command("delete")
204
+ async def mailbox_delete(
205
+ *,
206
+ project_id: ProjectIdOption,
207
+ address: Annotated[str, typer.Argument(help="Mailbox address to delete")],
208
+ ):
209
+ """Delete a mailbox."""
210
+ client = await get_client()
211
+ try:
212
+ project_id = await resolve_project_id(project_id)
213
+ try:
214
+ await client.delete_mailbox(project_id=project_id, address=address)
215
+ except ClientResponseError as exc:
216
+ if exc.status == 404:
217
+ print(f"[red]Mailbox not found:[/] {address}")
218
+ raise typer.Exit(code=1)
219
+ raise
220
+ else:
221
+ print(f"[green]Mailbox deleted:[/] {address}")
222
+ finally:
223
+ await client.close()
@@ -22,7 +22,7 @@ app = async_typer.AsyncTyper(help="Join a meeting transcriber to a room")
22
22
  @app.async_command("join")
23
23
  async def join(
24
24
  *,
25
- project_id: ProjectIdOption = None,
25
+ project_id: ProjectIdOption,
26
26
  room: RoomOption,
27
27
  agent_name: Annotated[str, typer.Option(..., help="Name of the agent")],
28
28
  key: Annotated[
@@ -93,9 +93,15 @@ async def join(
93
93
  async def service(
94
94
  *,
95
95
  agent_name: Annotated[str, typer.Option(..., help="Name of the agent")],
96
- host: Annotated[Optional[str], typer.Option()] = None,
97
- port: Annotated[Optional[int], typer.Option()] = None,
98
- path: Annotated[str, typer.Option()] = "/agent",
96
+ host: Annotated[
97
+ Optional[str], typer.Option(help="Host to bind the service on")
98
+ ] = None,
99
+ port: Annotated[
100
+ Optional[int], typer.Option(help="Port to bind the service on")
101
+ ] = None,
102
+ path: Annotated[
103
+ str, typer.Option(help="HTTP path to mount the service at")
104
+ ] = "/agent",
99
105
  ):
100
106
  try:
101
107
  from meshagent.livekit.agents.meeting_transcriber import MeetingTranscriber
@@ -16,13 +16,13 @@ from meshagent.cli.helper import (
16
16
  resolve_room,
17
17
  )
18
18
 
19
- app = async_typer.AsyncTyper()
19
+ app = async_typer.AsyncTyper(help="Send and receive messages in a room")
20
20
 
21
21
 
22
- @app.async_command("list-participants")
22
+ @app.async_command("list-participants", help="List messaging-enabled participants")
23
23
  async def messaging_list_participants_command(
24
24
  *,
25
- project_id: ProjectIdOption = None,
25
+ project_id: ProjectIdOption,
26
26
  room: RoomOption,
27
27
  ):
28
28
  """
@@ -61,10 +61,10 @@ async def messaging_list_participants_command(
61
61
  await account_client.close()
62
62
 
63
63
 
64
- @app.async_command("send")
64
+ @app.async_command("send", help="Send a direct message to a participant")
65
65
  async def messaging_send_command(
66
66
  *,
67
- project_id: ProjectIdOption = None,
67
+ project_id: ProjectIdOption,
68
68
  room: RoomOption,
69
69
  to_participant_id: Annotated[
70
70
  str, typer.Option(..., help="Participant ID to send a message to")
@@ -120,10 +120,10 @@ async def messaging_send_command(
120
120
  await account_client.close()
121
121
 
122
122
 
123
- @app.async_command("broadcast")
123
+ @app.async_command("broadcast", help="Broadcast a message to all participants")
124
124
  async def messaging_broadcast_command(
125
125
  *,
126
- project_id: ProjectIdOption = None,
126
+ project_id: ProjectIdOption,
127
127
  room: RoomOption,
128
128
  data: Annotated[str, typer.Option(..., help="JSON message to broadcast")],
129
129
  ):
meshagent/cli/multi.py ADDED
@@ -0,0 +1,402 @@
1
+ import typer
2
+ from meshagent.cli import async_typer
3
+ from meshagent.cli.host import run_services, set_deferred, service_specs, get_service
4
+ from meshagent.cli.common_options import ProjectIdOption
5
+ from typing import Annotated, Optional
6
+
7
+ import importlib.util
8
+ from pathlib import Path
9
+ import click
10
+ import shlex
11
+
12
+ from rich import print
13
+ from meshagent.cli.call import _make_call
14
+ import sys
15
+
16
+ from typer.main import get_command
17
+
18
+ from meshagent.cli.helper import (
19
+ get_client,
20
+ resolve_project_id,
21
+ resolve_room,
22
+ resolve_key,
23
+ )
24
+ from meshagent.api import (
25
+ ParticipantToken,
26
+ ApiScope,
27
+ )
28
+ from aiohttp import ClientResponseError
29
+ import asyncio
30
+
31
+ from meshagent.api.keys import parse_api_key
32
+
33
+ from meshagent.cli.chatbot import service as chatbot_service
34
+ from meshagent.cli.worker import service as worker_service
35
+ from meshagent.cli.mailbot import service as mailbot_service
36
+ from meshagent.cli.voicebot import service as voicebot_service
37
+
38
+ import yaml
39
+
40
+
41
+ app = async_typer.AsyncTyper(help="Connect agents and tools to a room")
42
+
43
+ cli = async_typer.AsyncTyper(help="Add agents to a team")
44
+
45
+ cli.command("chatbot")(chatbot_service)
46
+ cli.command("worker")(worker_service)
47
+ cli.command("mailbot")(mailbot_service)
48
+ cli.command("voicebot")(voicebot_service)
49
+
50
+
51
+ @cli.async_command("python")
52
+ async def python(
53
+ *,
54
+ module: str,
55
+ host: Annotated[
56
+ Optional[str], typer.Option(help="Host to bind the service on")
57
+ ] = None,
58
+ port: Annotated[
59
+ Optional[int], typer.Option(help="Port to bind the service on")
60
+ ] = None,
61
+ path: Annotated[
62
+ Optional[str],
63
+ typer.Option(help="A path to add the service at"),
64
+ ] = None,
65
+ identity: Annotated[
66
+ Optional[str],
67
+ typer.Option(help="The desired identity for the service"),
68
+ ] = None,
69
+ name: Annotated[
70
+ str, typer.Option(help="Entry-point name in the Python module")
71
+ ] = "main",
72
+ ):
73
+ service = get_service(host=host, port=port)
74
+
75
+ if path is None:
76
+ path = "/agent"
77
+ i = 0
78
+ while service.has_path(path):
79
+ i += 1
80
+ path = f"/agent{i}"
81
+
82
+ module = import_from_path(module)
83
+ service.add_path(path=path, identity=identity, cls=getattr(module, name or "main"))
84
+
85
+
86
+ def execute_via_root(app, line: str, *, prog_name="meshagent") -> int:
87
+ cmd = get_command(app)
88
+ try:
89
+ cmd.main(args=shlex.split(line), prog_name=prog_name, standalone_mode=False)
90
+ return 0
91
+ except click.ClickException as e:
92
+ e.show()
93
+ return e.exit_code
94
+
95
+
96
+ subcommand_help = """a list of sub commands to run, seperated by semicolons
97
+
98
+ available sub commands:
99
+
100
+ chatbot ...;
101
+ mailbot ...;
102
+ worker ...;
103
+ voicebot ...;
104
+ python path-to-python-file.py --name=NameOfModule;
105
+
106
+ chatbot, worker, and mailbot command arguments mirror those of the respective meshagent chatbot service, meshagent mailbot service, meshagent voicebot service, and meshagent worker service commands.
107
+ """
108
+
109
+
110
+ def build_spec(
111
+ *,
112
+ command: Annotated[str, typer.Option("-c", help=subcommand_help)],
113
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
114
+ service_description: Annotated[
115
+ Optional[str], typer.Option("--service-description", help="service description")
116
+ ] = None,
117
+ service_title: Annotated[
118
+ Optional[str],
119
+ typer.Option("--service-title", help="a display name for the service"),
120
+ ] = None,
121
+ ):
122
+ for c in command.split(";"):
123
+ if execute_via_root(cli, c, prog_name="meshagent") != 0:
124
+ print(f"[red]{c} failed[/red]")
125
+ raise typer.Exit(1)
126
+
127
+ specs = service_specs()
128
+ if len(specs) == 0:
129
+ print("[red]found no services, specify at least one agent or tool to run[/red]")
130
+ raise typer.Exit(1)
131
+
132
+ if len(specs) > 1:
133
+ print(
134
+ "[red]found multiple services leave host and port empty or use the same port for each command[/red]"
135
+ )
136
+ raise typer.Exit(1)
137
+
138
+ spec = specs[0]
139
+ spec.metadata.annotations = {
140
+ "meshagent.service.id": service_name,
141
+ }
142
+ for port in spec.ports:
143
+ port.num = "*"
144
+
145
+ spec.metadata.name = service_name
146
+ spec.metadata.description = service_description
147
+ spec.container.image = (
148
+ "us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
149
+ )
150
+ spec.container.command = (
151
+ f'meshagent multi service -c "{command.replace('"', '\\"')}"'
152
+ )
153
+
154
+
155
+ @app.async_command("spec")
156
+ async def spec(
157
+ command: Annotated[str, typer.Option("-c", help=subcommand_help)],
158
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
159
+ service_description: Annotated[
160
+ Optional[str], typer.Option("--service-description", help="service description")
161
+ ] = None,
162
+ service_title: Annotated[
163
+ Optional[str],
164
+ typer.Option("--service-title", help="a display name for the service"),
165
+ ] = None,
166
+ ):
167
+ set_deferred(True)
168
+
169
+ spec = build_spec(
170
+ command=command,
171
+ service_name=service_name,
172
+ service_description=service_description,
173
+ service_title=service_title,
174
+ )
175
+
176
+ print(yaml.dump(spec.model_dump(mode="json", exclude_none=True), sort_keys=False))
177
+
178
+
179
+ @app.async_command("deploy")
180
+ async def deploy(
181
+ project_id: ProjectIdOption,
182
+ command: Annotated[str, typer.Option("-c", help=subcommand_help)],
183
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
184
+ service_description: Annotated[
185
+ Optional[str], typer.Option("--service-description", help="service description")
186
+ ] = None,
187
+ service_title: Annotated[
188
+ Optional[str],
189
+ typer.Option("--service-title", help="a display name for the service"),
190
+ ] = None,
191
+ room: Annotated[
192
+ Optional[str],
193
+ typer.Option("--room", help="The name of a room to create the service for"),
194
+ ] = None,
195
+ ):
196
+ project_id = await resolve_project_id(project_id)
197
+
198
+ client = await get_client()
199
+ try:
200
+ set_deferred(True)
201
+
202
+ spec = build_spec(
203
+ command=command,
204
+ service_name=service_name,
205
+ service_description=service_description,
206
+ service_title=service_title,
207
+ )
208
+
209
+ spec.container.secrets = []
210
+
211
+ id = None
212
+ try:
213
+ if id is None:
214
+ if room is None:
215
+ services = await client.list_services(project_id=project_id)
216
+ else:
217
+ services = await client.list_room_services(
218
+ project_id=project_id, room_name=room
219
+ )
220
+
221
+ for s in services:
222
+ if s.metadata.name == spec.metadata.name:
223
+ id = s.id
224
+
225
+ if id is None:
226
+ if room is None:
227
+ id = await client.create_service(
228
+ project_id=project_id, service=spec
229
+ )
230
+ else:
231
+ id = await client.create_room_service(
232
+ project_id=project_id, service=spec, room_name=room
233
+ )
234
+
235
+ else:
236
+ spec.id = id
237
+ if room is None:
238
+ await client.update_service(
239
+ project_id=project_id, service_id=id, service=spec
240
+ )
241
+ else:
242
+ await client.update_room_service(
243
+ project_id=project_id,
244
+ service_id=id,
245
+ service=spec,
246
+ room_name=room,
247
+ )
248
+
249
+ except ClientResponseError as exc:
250
+ if exc.status == 409:
251
+ print(f"[red]Service name already in use: {spec.metadata.name}[/red]")
252
+ raise typer.Exit(code=1)
253
+ raise
254
+ else:
255
+ print(f"[green]Deployed service:[/] {id}")
256
+
257
+ finally:
258
+ await client.close()
259
+
260
+
261
+ @app.async_command("service")
262
+ async def host(
263
+ host: Annotated[
264
+ Optional[str], typer.Option(help="Host to bind the service on")
265
+ ] = None,
266
+ port: Annotated[
267
+ Optional[int], typer.Option(help="Port to bind the service on")
268
+ ] = None,
269
+ command: Annotated[str, typer.Option("-c", help=subcommand_help)] = [],
270
+ ):
271
+ set_deferred(True)
272
+
273
+ for c in command.split(";"):
274
+ if execute_via_root(cli, c, prog_name="meshagent") != 0:
275
+ print(f"[red]{c} failed[/red]")
276
+ raise typer.Exit(1)
277
+
278
+ await run_services()
279
+
280
+
281
+ def import_from_path(path: str, module_name: str | None = None):
282
+ path = Path(path)
283
+ module_name = module_name or path.stem
284
+
285
+ spec = importlib.util.spec_from_file_location(module_name, path)
286
+ if spec is None or spec.loader is None:
287
+ raise ImportError(f"Cannot load spec for {path}")
288
+
289
+ module = importlib.util.module_from_spec(spec)
290
+ spec.loader.exec_module(module)
291
+ return module
292
+
293
+
294
+ @app.async_command("join")
295
+ async def join(
296
+ *,
297
+ project_id: ProjectIdOption,
298
+ command: Annotated[str, typer.Option("-c", help=subcommand_help)] = [],
299
+ port: Annotated[
300
+ int,
301
+ typer.Option(
302
+ "--port",
303
+ "-p",
304
+ help=(
305
+ "a port number to run the agent on (will set MESHAGENT_PORT environment variable when launching the service)"
306
+ ),
307
+ ),
308
+ ] = None,
309
+ room: Annotated[
310
+ Optional[str],
311
+ typer.Option(
312
+ help="A room name to test the service in (must not be currently running)"
313
+ ),
314
+ ] = None,
315
+ key: Annotated[
316
+ str,
317
+ typer.Option("--key", help="an api key to sign the token with"),
318
+ ] = None,
319
+ ):
320
+ set_deferred(True)
321
+
322
+ for c in command.split(";"):
323
+ if execute_via_root(cli, c, prog_name="meshagent") != 0:
324
+ print(f"[red]{c} failed[/red]")
325
+ raise typer.Exit(1)
326
+
327
+ services_task = asyncio.create_task(run_services())
328
+
329
+ key = await resolve_key(project_id=project_id, key=key)
330
+
331
+ if port is None:
332
+ import socket
333
+
334
+ def find_free_port():
335
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
336
+ s.bind(("", 0)) # Bind to a free port provided by the host.
337
+ s.listen(1)
338
+ return s.getsockname()[1]
339
+
340
+ port = find_free_port()
341
+
342
+ my_client = await get_client()
343
+ try:
344
+ project_id = await resolve_project_id(project_id)
345
+ room = resolve_room(room)
346
+
347
+ if room is None:
348
+ print("[bold red]Room was not set[/bold red]")
349
+ raise typer.Exit(1)
350
+
351
+ try:
352
+ parsed_key = parse_api_key(key)
353
+ token = ParticipantToken(
354
+ name="cli", project_id=project_id, api_key_id=parsed_key.id
355
+ )
356
+ token.add_api_grant(ApiScope.agent_default())
357
+ token.add_role_grant("user")
358
+ token.add_room_grant(room)
359
+
360
+ print("[bold green]Connecting to room...[/bold green]")
361
+
362
+ run_tasks = []
363
+
364
+ for spec in service_specs():
365
+ sys.stdout.write("\n")
366
+
367
+ for p in spec.ports:
368
+ print(f"[bold green]Connecting port {p.num}...[/bold green]")
369
+
370
+ for endpoint in p.endpoints:
371
+ print(
372
+ f"[bold green]Connecting endpoint {endpoint.path}...[/bold green]"
373
+ )
374
+
375
+ run_tasks.append(
376
+ asyncio.create_task(
377
+ _make_call(
378
+ room=room,
379
+ project_id=project_id,
380
+ participant_name=endpoint.meshagent.identity,
381
+ url=f"http://localhost:{p.num}{endpoint.path}",
382
+ arguments={},
383
+ key=key,
384
+ permissions=endpoint.meshagent.api,
385
+ )
386
+ )
387
+ )
388
+
389
+ await asyncio.gather(*run_tasks, services_task)
390
+
391
+ except ClientResponseError as exc:
392
+ if exc.status == 409:
393
+ print(f"[red]Room already in use: {room}[/red]")
394
+ raise typer.Exit(code=1)
395
+ raise
396
+
397
+ except Exception as e:
398
+ print(f"[red]{e}[/red]")
399
+ raise typer.Exit(code=1)
400
+
401
+ finally:
402
+ await my_client.close()