meshagent-cli 0.22.2__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.

Files changed (45) hide show
  1. meshagent/cli/__init__.py +3 -0
  2. meshagent/cli/agent.py +273 -0
  3. meshagent/cli/api_keys.py +102 -0
  4. meshagent/cli/async_typer.py +79 -0
  5. meshagent/cli/auth.py +30 -0
  6. meshagent/cli/auth_async.py +295 -0
  7. meshagent/cli/call.py +215 -0
  8. meshagent/cli/chatbot.py +1983 -0
  9. meshagent/cli/cli.py +187 -0
  10. meshagent/cli/cli_mcp.py +408 -0
  11. meshagent/cli/cli_secrets.py +414 -0
  12. meshagent/cli/common_options.py +47 -0
  13. meshagent/cli/containers.py +725 -0
  14. meshagent/cli/database.py +997 -0
  15. meshagent/cli/developer.py +70 -0
  16. meshagent/cli/exec.py +397 -0
  17. meshagent/cli/helper.py +236 -0
  18. meshagent/cli/helpers.py +185 -0
  19. meshagent/cli/host.py +41 -0
  20. meshagent/cli/mailbot.py +1295 -0
  21. meshagent/cli/mailboxes.py +223 -0
  22. meshagent/cli/meeting_transcriber.py +138 -0
  23. meshagent/cli/messaging.py +157 -0
  24. meshagent/cli/multi.py +357 -0
  25. meshagent/cli/oauth2.py +341 -0
  26. meshagent/cli/participant_token.py +63 -0
  27. meshagent/cli/port.py +70 -0
  28. meshagent/cli/projects.py +105 -0
  29. meshagent/cli/queue.py +91 -0
  30. meshagent/cli/room.py +26 -0
  31. meshagent/cli/rooms.py +214 -0
  32. meshagent/cli/services.py +722 -0
  33. meshagent/cli/sessions.py +26 -0
  34. meshagent/cli/storage.py +813 -0
  35. meshagent/cli/sync.py +434 -0
  36. meshagent/cli/task_runner.py +1317 -0
  37. meshagent/cli/version.py +1 -0
  38. meshagent/cli/voicebot.py +624 -0
  39. meshagent/cli/webhook.py +100 -0
  40. meshagent/cli/worker.py +1403 -0
  41. meshagent_cli-0.22.2.dist-info/METADATA +49 -0
  42. meshagent_cli-0.22.2.dist-info/RECORD +45 -0
  43. meshagent_cli-0.22.2.dist-info/WHEEL +5 -0
  44. meshagent_cli-0.22.2.dist-info/entry_points.txt +2 -0
  45. meshagent_cli-0.22.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1295 @@
1
+ import typer
2
+ from meshagent.cli import async_typer
3
+ from rich import print
4
+ import os
5
+
6
+ from meshagent.api import ParticipantToken
7
+ from typing import Annotated, Optional
8
+ from meshagent.cli.common_options import (
9
+ ProjectIdOption,
10
+ RoomOption,
11
+ )
12
+ from meshagent.tools import Toolkit
13
+ from meshagent.api import RoomClient, WebSocketClientProtocol, ApiScope
14
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
15
+ from meshagent.cli.helper import (
16
+ get_client,
17
+ resolve_project_id,
18
+ resolve_room,
19
+ resolve_key,
20
+ cleanup_args,
21
+ )
22
+ from meshagent.openai import OpenAIResponsesAdapter
23
+ from meshagent.anthropic import AnthropicOpenAIResponsesStreamAdapter
24
+
25
+ from meshagent.agents.config import RulesConfig
26
+
27
+ from typing import List
28
+ from pathlib import Path
29
+
30
+ from meshagent.api import RequiredToolkit, RequiredSchema, RoomException
31
+
32
+ import logging
33
+
34
+ from meshagent.tools.database import DatabaseToolkitBuilder, DatabaseToolkitConfig
35
+
36
+ from meshagent.tools.storage import StorageToolkit
37
+ from meshagent.tools.datetime import DatetimeToolkit
38
+ from meshagent.tools.uuid import UUIDToolkit
39
+
40
+ from meshagent.openai.tools.responses_adapter import (
41
+ WebSearchTool,
42
+ ShellConfig,
43
+ ApplyPatchConfig,
44
+ ApplyPatchTool,
45
+ ShellTool,
46
+ LocalShellTool,
47
+ ImageGenerationTool,
48
+ )
49
+
50
+ from meshagent.cli.host import get_service, run_services, get_deferred, service_specs
51
+ from meshagent.api.specs.service import AgentSpec, ANNOTATION_AGENT_TYPE
52
+
53
+ import yaml
54
+
55
+ import shlex
56
+ import sys
57
+
58
+ from meshagent.api.client import ConflictError
59
+ from meshagent.agents.adapter import MessageStreamLLMAdapter
60
+
61
+ logger = logging.getLogger("mailbot")
62
+
63
+ app = async_typer.AsyncTyper(help="Join a mailbot to a room")
64
+
65
+
66
+ def build_mailbot(
67
+ *,
68
+ model: str,
69
+ rule: List[str],
70
+ toolkit: List[str],
71
+ schema: List[str],
72
+ image_generation: Optional[str] = None,
73
+ local_shell: bool,
74
+ computer_use: bool,
75
+ rules_file: Optional[str] = None,
76
+ web_search: Annotated[
77
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
78
+ ] = False,
79
+ toolkit_name: Optional[str] = None,
80
+ queue: Optional[str] = None,
81
+ email_address: str,
82
+ room_rules_paths: list[str],
83
+ whitelist=list[str],
84
+ require_shell: Optional[bool] = None,
85
+ require_apply_patch: Optional[bool] = None,
86
+ require_storage: Optional[str] = None,
87
+ require_read_only_storage: Optional[str] = None,
88
+ require_time: bool = True,
89
+ require_uuid: bool = False,
90
+ require_table_read: bool,
91
+ require_table_write: bool,
92
+ require_computer_use: bool,
93
+ reply_all: bool,
94
+ database_namespace: Optional[list[str]] = None,
95
+ enable_attachments: bool,
96
+ working_directory: Optional[str] = None,
97
+ skill_dirs: Optional[list[str]] = None,
98
+ shell_image: Optional[str] = None,
99
+ llm_participant: Optional[str] = None,
100
+ delegate_shell_token: Optional[bool] = None,
101
+ log_llm_requests: Optional[bool] = None,
102
+ ):
103
+ from meshagent.agents.mail import MailBot
104
+
105
+ if (require_storage or require_read_only_storage) and len(whitelist) == 0:
106
+ logger.warning(
107
+ "you have enabled storage tools without a whilelist, anyone who can send to this mailbox will be able to ask it about files"
108
+ )
109
+
110
+ requirements = []
111
+
112
+ toolkits = []
113
+
114
+ for t in toolkit:
115
+ requirements.append(RequiredToolkit(name=t))
116
+
117
+ for t in schema:
118
+ requirements.append(RequiredSchema(name=t))
119
+
120
+ if rules_file is not None:
121
+ try:
122
+ with open(Path(rules_file).resolve(), "r") as f:
123
+ rule.extend(f.read().splitlines())
124
+ except FileNotFoundError:
125
+ print(f"[yellow]rules file not found at {rules_file}[/yellow]")
126
+
127
+ BaseClass = MailBot
128
+ if llm_participant:
129
+ llm_adapter = MessageStreamLLMAdapter(
130
+ participant_name=llm_participant,
131
+ )
132
+ else:
133
+ if computer_use or require_computer_use:
134
+ llm_adapter = OpenAIResponsesAdapter(
135
+ model=model,
136
+ response_options={
137
+ "reasoning": {"summary": "concise"},
138
+ "truncation": "auto",
139
+ },
140
+ log_requests=log_llm_requests,
141
+ )
142
+
143
+ else:
144
+ if model.startswith("claude-"):
145
+ llm_adapter = AnthropicOpenAIResponsesStreamAdapter(
146
+ model=model,
147
+ log_requests=log_llm_requests,
148
+ )
149
+ else:
150
+ llm_adapter = OpenAIResponsesAdapter(
151
+ model=model,
152
+ log_requests=log_llm_requests,
153
+ )
154
+
155
+ parsed_whitelist = []
156
+ if len(whitelist) > 0:
157
+ for w in whitelist:
158
+ for s in w.split(","):
159
+ s = s.strip()
160
+ if len(s) > 0:
161
+ parsed_whitelist.append(s)
162
+
163
+ class CustomMailbot(BaseClass):
164
+ def __init__(self):
165
+ super().__init__(
166
+ llm_adapter=llm_adapter,
167
+ requires=requirements,
168
+ toolkits=toolkits,
169
+ queue=queue,
170
+ email_address=email_address,
171
+ toolkit_name=toolkit_name,
172
+ rules=rule if len(rule) > 0 else None,
173
+ whitelist=parsed_whitelist if len(parsed_whitelist) > 0 else None,
174
+ reply_all=reply_all,
175
+ enable_attachments=enable_attachments,
176
+ skill_dirs=skill_dirs,
177
+ )
178
+
179
+ async def init_chat_context(self):
180
+ from meshagent.cli.helper import init_context_from_spec
181
+
182
+ context = await super().init_chat_context()
183
+ await init_context_from_spec(context)
184
+
185
+ return context
186
+
187
+ async def start(self, *, room: RoomClient):
188
+ print(
189
+ "[bold green]Configure and send an email interact with your mailbot[/bold green]"
190
+ )
191
+ await super().start(room=room)
192
+ if room_rules_paths is not None:
193
+ for p in room_rules_paths:
194
+ await self._load_room_rules(path=p)
195
+
196
+ async def get_rules(self):
197
+ rules = [*await super().get_rules()]
198
+ if room_rules_paths is not None:
199
+ for p in room_rules_paths:
200
+ rules.extend(await self._load_room_rules(path=p))
201
+
202
+ return rules
203
+
204
+ async def _load_room_rules(
205
+ self,
206
+ *,
207
+ path: str,
208
+ ):
209
+ rules = []
210
+ try:
211
+ room_rules = await self.room.storage.download(path=path)
212
+
213
+ rules_txt = room_rules.data.decode()
214
+
215
+ rules_config = RulesConfig.parse(rules_txt)
216
+
217
+ if rules_config.rules is not None:
218
+ rules.extend(rules_config.rules)
219
+
220
+ except RoomException:
221
+ try:
222
+ logger.info("attempting to initialize rules file")
223
+ handle = await self.room.storage.open(path=path, overwrite=False)
224
+ await self.room.storage.write(
225
+ handle=handle,
226
+ data="# Add rules to this file to customize your agent's behavior, lines starting with # will be ignored.\n\n".encode(),
227
+ )
228
+ await self.room.storage.close(handle=handle)
229
+
230
+ except RoomException:
231
+ pass
232
+ logger.info(
233
+ f"unable to load rules from {path}, continuing with default rules"
234
+ )
235
+ pass
236
+
237
+ return rules
238
+
239
+ async def get_thread_toolkits(self, *, thread_context):
240
+ toolkits = await super().get_thread_toolkits(thread_context=thread_context)
241
+
242
+ thread_toolkit = Toolkit(name="thread_toolkit", tools=[])
243
+
244
+ if local_shell:
245
+ thread_toolkit.tools.append(
246
+ LocalShellTool(thread_context=thread_context)
247
+ )
248
+
249
+ env = {}
250
+ if delegate_shell_token:
251
+ env["MESHAGENT_TOKEN"] = self.room.protocol.token
252
+
253
+ if require_shell:
254
+ thread_toolkit.tools.append(
255
+ ShellTool(
256
+ working_directory=working_directory,
257
+ config=ShellConfig(name="shell"),
258
+ image=shell_image or "python:3.13",
259
+ env=env,
260
+ )
261
+ )
262
+
263
+ if require_apply_patch:
264
+ thread_toolkit.tools.append(
265
+ ApplyPatchTool(
266
+ config=ApplyPatchConfig(name="apply_patch"),
267
+ )
268
+ )
269
+
270
+ if image_generation is not None:
271
+ print("adding openai image gen to thread", flush=True)
272
+ thread_toolkit.tools.append(
273
+ ImageGenerationTool(
274
+ model=image_generation,
275
+ thread_context=thread_context,
276
+ partial_images=3,
277
+ )
278
+ )
279
+
280
+ if web_search:
281
+ thread_toolkit.tools.append(WebSearchTool())
282
+
283
+ if require_storage:
284
+ thread_toolkit.tools.extend(StorageToolkit().tools)
285
+
286
+ if require_read_only_storage:
287
+ thread_toolkit.tools.extend(StorageToolkit(read_only=True).tools)
288
+
289
+ if len(require_table_read) > 0:
290
+ thread_toolkit.tools.extend(
291
+ (
292
+ await DatabaseToolkitBuilder().make(
293
+ room=self.room,
294
+ model=model,
295
+ config=DatabaseToolkitConfig(
296
+ tables=require_table_read,
297
+ read_only=True,
298
+ namespace=database_namespace,
299
+ ),
300
+ )
301
+ ).tools
302
+ )
303
+
304
+ if len(require_table_write) > 0:
305
+ thread_toolkit.tools.extend(
306
+ (
307
+ await DatabaseToolkitBuilder().make(
308
+ room=self.room,
309
+ model=model,
310
+ config=DatabaseToolkitConfig(
311
+ tables=require_table_write,
312
+ read_only=False,
313
+ namespace=database_namespace,
314
+ ),
315
+ )
316
+ ).tools
317
+ )
318
+
319
+ if require_time:
320
+ thread_toolkit.tools.extend(DatetimeToolkit().tools)
321
+
322
+ if require_uuid:
323
+ thread_toolkit.tools.extend(UUIDToolkit().tools)
324
+
325
+ if require_computer_use:
326
+ from meshagent.computers.agent import ComputerToolkit
327
+
328
+ computer_toolkit = ComputerToolkit(room=self.room, render_screen=None)
329
+
330
+ toolkits.append(computer_toolkit)
331
+
332
+ toolkits.append(thread_toolkit)
333
+ return toolkits
334
+
335
+ return CustomMailbot
336
+
337
+
338
+ @app.async_command("join")
339
+ async def join(
340
+ *,
341
+ project_id: ProjectIdOption,
342
+ room: RoomOption,
343
+ role: str = "agent",
344
+ agent_name: Annotated[
345
+ Optional[str], typer.Option(..., help="Name of the agent to call")
346
+ ] = None,
347
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
348
+ rules_file: Optional[str] = None,
349
+ require_toolkit: Annotated[
350
+ List[str],
351
+ typer.Option(
352
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
353
+ ),
354
+ ] = [],
355
+ require_schema: Annotated[
356
+ List[str],
357
+ typer.Option(
358
+ "--require-schema", "-rs", help="the name or url of a required schema"
359
+ ),
360
+ ] = [],
361
+ toolkit: Annotated[
362
+ List[str],
363
+ typer.Option("--toolkit", "-t", help="the name or url of a required toolkit"),
364
+ ] = [],
365
+ schema: Annotated[
366
+ List[str],
367
+ typer.Option("--schema", "-s", help="the name or url of a required schema"),
368
+ ] = [],
369
+ model: Annotated[
370
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
371
+ ] = "gpt-5.2",
372
+ require_shell: Annotated[
373
+ Optional[bool],
374
+ typer.Option(..., help="Enable function shell tool calling"),
375
+ ] = False,
376
+ require_local_shell: Annotated[
377
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
378
+ ] = False,
379
+ require_web_search: Annotated[
380
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
381
+ ] = False,
382
+ require_apply_patch: Annotated[
383
+ Optional[bool],
384
+ typer.Option(..., help="Enable apply patch tool calling"),
385
+ ] = False,
386
+ key: Annotated[
387
+ str,
388
+ typer.Option("--key", help="an api key to sign the token with"),
389
+ ] = None,
390
+ queue: Annotated[
391
+ Optional[str], typer.Option(..., help="the name of the mail queue")
392
+ ] = None,
393
+ email_address: Annotated[
394
+ str, typer.Option(..., help="the email address of the agent")
395
+ ],
396
+ toolkit_name: Annotated[
397
+ Optional[str],
398
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
399
+ ] = None,
400
+ room_rules: Annotated[
401
+ List[str],
402
+ typer.Option(
403
+ "--room-rules",
404
+ "-rr",
405
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
406
+ ),
407
+ ] = [],
408
+ whitelist: Annotated[
409
+ List[str],
410
+ typer.Option(
411
+ "--whitelist",
412
+ help="an email to whitelist",
413
+ ),
414
+ ] = [],
415
+ require_storage: Annotated[
416
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
417
+ ] = False,
418
+ require_read_only_storage: Annotated[
419
+ Optional[bool],
420
+ typer.Option(..., help="Enable read only storage toolkit"),
421
+ ] = False,
422
+ require_time: Annotated[
423
+ bool,
424
+ typer.Option(
425
+ ...,
426
+ help="Enable time/datetime tools",
427
+ ),
428
+ ] = True,
429
+ require_uuid: Annotated[
430
+ bool,
431
+ typer.Option(
432
+ ...,
433
+ help="Enable UUID generation tools",
434
+ ),
435
+ ] = False,
436
+ database_namespace: Annotated[
437
+ Optional[str],
438
+ typer.Option(..., help="Use a specific database namespace"),
439
+ ] = None,
440
+ require_table_read: Annotated[
441
+ list[str],
442
+ typer.Option(..., help="Enable table read tools for a specific table"),
443
+ ] = [],
444
+ require_table_write: Annotated[
445
+ list[str],
446
+ typer.Option(..., help="Enable table write tools for a specific table"),
447
+ ] = [],
448
+ require_computer_use: Annotated[
449
+ Optional[bool],
450
+ typer.Option(
451
+ ...,
452
+ help="Enable computer use (requires computer-use-preview model)",
453
+ hidden=True,
454
+ ),
455
+ ] = False,
456
+ reply_all: Annotated[
457
+ bool, typer.Option(help="Reply-all when responding to emails")
458
+ ] = False,
459
+ enable_attachments: Annotated[
460
+ bool, typer.Option(help="Allow downloading and processing email attachments")
461
+ ] = False,
462
+ working_directory: Annotated[
463
+ Optional[str],
464
+ typer.Option(..., help="The default working directory for shell commands"),
465
+ ] = None,
466
+ skill_dir: Annotated[
467
+ list[str],
468
+ typer.Option(..., help="an agent skills directory"),
469
+ ] = [],
470
+ llm_participant: Annotated[
471
+ Optional[str],
472
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
473
+ ] = None,
474
+ shell_image: Annotated[
475
+ Optional[str],
476
+ typer.Option(..., help="an image tag to use to run shell commands in"),
477
+ ] = None,
478
+ delegate_shell_token: Annotated[
479
+ Optional[bool],
480
+ typer.Option(..., help="Delegate the room token to shell tools"),
481
+ ] = False,
482
+ log_llm_requests: Annotated[
483
+ Optional[bool],
484
+ typer.Option(..., help="log all requests to the llm"),
485
+ ] = False,
486
+ ):
487
+ key = await resolve_key(project_id=project_id, key=key)
488
+
489
+ account_client = await get_client()
490
+ try:
491
+ project_id = await resolve_project_id(project_id=project_id)
492
+
493
+ room = resolve_room(room)
494
+
495
+ jwt = os.getenv("MESHAGENT_TOKEN")
496
+ if jwt is None:
497
+ if agent_name is None:
498
+ print(
499
+ "[bold red]--agent-name must be specified when the MESHAGENT_TOKEN environment variable is not set[/bold red]"
500
+ )
501
+ raise typer.Exit(1)
502
+
503
+ token = ParticipantToken(
504
+ name=agent_name,
505
+ )
506
+
507
+ token.add_api_grant(ApiScope.agent_default(tunnels=require_computer_use))
508
+
509
+ token.add_role_grant(role=role)
510
+ token.add_room_grant(room)
511
+
512
+ jwt = token.to_jwt(api_key=key)
513
+
514
+ print("[bold green]Connecting to room...[/bold green]", flush=True)
515
+ CustomMailbot = build_mailbot(
516
+ computer_use=None,
517
+ model=model,
518
+ local_shell=require_local_shell,
519
+ rule=rule,
520
+ schema=require_schema + schema,
521
+ toolkit=require_toolkit + toolkit,
522
+ image_generation=None,
523
+ web_search=require_web_search,
524
+ rules_file=rules_file,
525
+ queue=queue,
526
+ email_address=email_address,
527
+ toolkit_name=toolkit_name,
528
+ room_rules_paths=room_rules,
529
+ whitelist=whitelist,
530
+ require_shell=require_shell,
531
+ require_apply_patch=require_apply_patch,
532
+ require_storage=require_storage,
533
+ require_read_only_storage=require_read_only_storage,
534
+ require_time=require_time,
535
+ require_uuid=require_uuid,
536
+ require_table_read=require_table_read,
537
+ require_table_write=require_table_write,
538
+ require_computer_use=require_computer_use,
539
+ reply_all=reply_all,
540
+ database_namespace=database_namespace,
541
+ enable_attachments=enable_attachments,
542
+ working_directory=working_directory,
543
+ skill_dirs=skill_dir,
544
+ shell_image=shell_image,
545
+ llm_participant=llm_participant,
546
+ delegate_shell_token=delegate_shell_token,
547
+ log_llm_requests=log_llm_requests,
548
+ )
549
+
550
+ bot = CustomMailbot()
551
+
552
+ if get_deferred():
553
+ from meshagent.cli.host import agents
554
+
555
+ agents.append((bot, jwt))
556
+ else:
557
+ async with RoomClient(
558
+ protocol=WebSocketClientProtocol(
559
+ url=websocket_room_url(
560
+ room_name=room, base_url=meshagent_base_url()
561
+ ),
562
+ token=jwt,
563
+ )
564
+ ) as client:
565
+ await bot.start(room=client)
566
+ try:
567
+ print(
568
+ flush=True,
569
+ )
570
+ await client.protocol.wait_for_close()
571
+ except KeyboardInterrupt:
572
+ await bot.stop()
573
+
574
+ finally:
575
+ await account_client.close()
576
+
577
+
578
+ @app.async_command("service")
579
+ async def service(
580
+ *,
581
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
582
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
583
+ rules_file: Optional[str] = None,
584
+ require_toolkit: Annotated[
585
+ List[str],
586
+ typer.Option(
587
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
588
+ ),
589
+ ] = [],
590
+ require_schema: Annotated[
591
+ List[str],
592
+ typer.Option(
593
+ "--require-schema", "-rs", help="the name or url of a required schema"
594
+ ),
595
+ ] = [],
596
+ toolkit: Annotated[
597
+ List[str],
598
+ typer.Option(
599
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
600
+ ),
601
+ ] = [],
602
+ schema: Annotated[
603
+ List[str],
604
+ typer.Option(
605
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
606
+ ),
607
+ ] = [],
608
+ model: Annotated[
609
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
610
+ ] = "gpt-5.2",
611
+ require_shell: Annotated[
612
+ Optional[bool],
613
+ typer.Option(..., help="Enable function shell tool calling"),
614
+ ] = False,
615
+ require_local_shell: Annotated[
616
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
617
+ ] = False,
618
+ require_web_search: Annotated[
619
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
620
+ ] = False,
621
+ require_apply_patch: Annotated[
622
+ Optional[bool],
623
+ typer.Option(..., help="Enable apply patch tool calling"),
624
+ ] = False,
625
+ host: Annotated[
626
+ Optional[str], typer.Option(help="Host to bind the service on")
627
+ ] = None,
628
+ port: Annotated[
629
+ Optional[int], typer.Option(help="Port to bind the service on")
630
+ ] = None,
631
+ path: Annotated[
632
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
633
+ ] = None,
634
+ queue: Annotated[
635
+ Optional[str], typer.Option(..., help="the name of the mail queue")
636
+ ] = None,
637
+ email_address: Annotated[
638
+ str, typer.Option(..., help="the email address of the agent")
639
+ ],
640
+ toolkit_name: Annotated[
641
+ Optional[str],
642
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
643
+ ] = None,
644
+ room_rules: Annotated[
645
+ List[str],
646
+ typer.Option(
647
+ "--room-rules",
648
+ "-rr",
649
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
650
+ ),
651
+ ] = [],
652
+ whitelist: Annotated[
653
+ List[str],
654
+ typer.Option(
655
+ "--whitelist",
656
+ help="an email to whitelist",
657
+ ),
658
+ ] = [],
659
+ require_storage: Annotated[
660
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
661
+ ] = False,
662
+ require_read_only_storage: Annotated[
663
+ Optional[bool],
664
+ typer.Option(..., help="Enable read only storage toolkit"),
665
+ ] = False,
666
+ require_time: Annotated[
667
+ bool,
668
+ typer.Option(
669
+ ...,
670
+ help="Enable time/datetime tools",
671
+ ),
672
+ ] = True,
673
+ require_uuid: Annotated[
674
+ bool,
675
+ typer.Option(
676
+ ...,
677
+ help="Enable UUID generation tools",
678
+ ),
679
+ ] = False,
680
+ database_namespace: Annotated[
681
+ Optional[str],
682
+ typer.Option(..., help="Use a specific database namespace"),
683
+ ] = None,
684
+ require_table_read: Annotated[
685
+ list[str],
686
+ typer.Option(..., help="Enable table read tools for a specific table"),
687
+ ] = [],
688
+ require_table_write: Annotated[
689
+ list[str],
690
+ typer.Option(..., help="Enable table write tools for a specific table"),
691
+ ] = [],
692
+ require_computer_use: Annotated[
693
+ Optional[bool],
694
+ typer.Option(
695
+ ...,
696
+ help="Enable computer use (requires computer-use-preview model)",
697
+ hidden=True,
698
+ ),
699
+ ] = False,
700
+ reply_all: Annotated[
701
+ bool, typer.Option(help="Reply-all when responding to emails")
702
+ ] = False,
703
+ enable_attachments: Annotated[
704
+ bool, typer.Option(help="Allow downloading and processing email attachments")
705
+ ] = False,
706
+ working_directory: Annotated[
707
+ Optional[str],
708
+ typer.Option(..., help="The default working directory for shell commands"),
709
+ ] = None,
710
+ skill_dir: Annotated[
711
+ list[str],
712
+ typer.Option(..., help="an agent skills directory"),
713
+ ] = [],
714
+ llm_participant: Annotated[
715
+ Optional[str],
716
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
717
+ ] = None,
718
+ shell_image: Annotated[
719
+ Optional[str],
720
+ typer.Option(..., help="an image tag to use to run shell commands in"),
721
+ ] = None,
722
+ delegate_shell_token: Annotated[
723
+ Optional[bool],
724
+ typer.Option(..., help="Delegate the room token to shell tools"),
725
+ ] = False,
726
+ log_llm_requests: Annotated[
727
+ Optional[bool],
728
+ typer.Option(..., help="log all requests to the llm"),
729
+ ] = False,
730
+ ):
731
+ service = get_service(host=host, port=port)
732
+ if path is None:
733
+ path = "/agent"
734
+ i = 0
735
+ while service.has_path(path):
736
+ i += 1
737
+ path = f"/agent{i}"
738
+
739
+ service.agents.append(
740
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "MailBot"})
741
+ )
742
+
743
+ service.add_path(
744
+ identity=agent_name,
745
+ path=path,
746
+ cls=build_mailbot(
747
+ queue=queue,
748
+ computer_use=None,
749
+ model=model,
750
+ local_shell=require_local_shell,
751
+ web_search=require_web_search,
752
+ rule=rule,
753
+ schema=require_schema + schema,
754
+ toolkit=require_toolkit + toolkit,
755
+ image_generation=None,
756
+ rules_file=rules_file,
757
+ email_address=email_address,
758
+ toolkit_name=toolkit_name,
759
+ room_rules_paths=room_rules,
760
+ whitelist=whitelist,
761
+ require_shell=require_shell,
762
+ require_apply_patch=require_apply_patch,
763
+ require_storage=require_storage,
764
+ require_read_only_storage=require_read_only_storage,
765
+ require_time=require_time,
766
+ require_uuid=require_uuid,
767
+ require_table_read=require_table_read,
768
+ require_table_write=require_table_write,
769
+ require_computer_use=require_computer_use,
770
+ reply_all=reply_all,
771
+ database_namespace=database_namespace,
772
+ enable_attachments=enable_attachments,
773
+ working_directory=working_directory,
774
+ skill_dirs=skill_dir,
775
+ shell_image=shell_image,
776
+ llm_participant=llm_participant,
777
+ delegate_shell_token=delegate_shell_token,
778
+ log_llm_requests=log_llm_requests,
779
+ ),
780
+ )
781
+
782
+ if not get_deferred():
783
+ await run_services()
784
+
785
+
786
+ @app.async_command("spec")
787
+ async def spec(
788
+ *,
789
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
790
+ service_description: Annotated[
791
+ Optional[str], typer.Option("--service-description", help="service description")
792
+ ] = None,
793
+ service_title: Annotated[
794
+ Optional[str],
795
+ typer.Option("--service-title", help="a display name for the service"),
796
+ ] = None,
797
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
798
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
799
+ rules_file: Optional[str] = None,
800
+ require_toolkit: Annotated[
801
+ List[str],
802
+ typer.Option(
803
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
804
+ ),
805
+ ] = [],
806
+ require_schema: Annotated[
807
+ List[str],
808
+ typer.Option(
809
+ "--require-schema", "-rs", help="the name or url of a required schema"
810
+ ),
811
+ ] = [],
812
+ toolkit: Annotated[
813
+ List[str],
814
+ typer.Option(
815
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
816
+ ),
817
+ ] = [],
818
+ schema: Annotated[
819
+ List[str],
820
+ typer.Option(
821
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
822
+ ),
823
+ ] = [],
824
+ model: Annotated[
825
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
826
+ ] = "gpt-5.2",
827
+ require_shell: Annotated[
828
+ Optional[bool],
829
+ typer.Option(..., help="Enable function shell tool calling"),
830
+ ] = False,
831
+ require_local_shell: Annotated[
832
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
833
+ ] = False,
834
+ require_web_search: Annotated[
835
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
836
+ ] = False,
837
+ require_apply_patch: Annotated[
838
+ Optional[bool],
839
+ typer.Option(..., help="Enable apply patch tool calling"),
840
+ ] = False,
841
+ host: Annotated[
842
+ Optional[str], typer.Option(help="Host to bind the service on")
843
+ ] = None,
844
+ port: Annotated[
845
+ Optional[int], typer.Option(help="Port to bind the service on")
846
+ ] = None,
847
+ path: Annotated[
848
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
849
+ ] = None,
850
+ queue: Annotated[
851
+ Optional[str], typer.Option(..., help="the name of the mail queue")
852
+ ] = None,
853
+ email_address: Annotated[
854
+ str, typer.Option(..., help="the email address of the agent")
855
+ ],
856
+ toolkit_name: Annotated[
857
+ Optional[str],
858
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
859
+ ] = None,
860
+ room_rules: Annotated[
861
+ List[str],
862
+ typer.Option(
863
+ "--room-rules",
864
+ "-rr",
865
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
866
+ ),
867
+ ] = [],
868
+ whitelist: Annotated[
869
+ List[str],
870
+ typer.Option(
871
+ "--whitelist",
872
+ help="an email to whitelist",
873
+ ),
874
+ ] = [],
875
+ require_storage: Annotated[
876
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
877
+ ] = False,
878
+ require_read_only_storage: Annotated[
879
+ Optional[bool],
880
+ typer.Option(..., help="Enable read only storage toolkit"),
881
+ ] = False,
882
+ require_time: Annotated[
883
+ bool,
884
+ typer.Option(
885
+ ...,
886
+ help="Enable time/datetime tools",
887
+ ),
888
+ ] = True,
889
+ require_uuid: Annotated[
890
+ bool,
891
+ typer.Option(
892
+ ...,
893
+ help="Enable UUID generation tools",
894
+ ),
895
+ ] = False,
896
+ database_namespace: Annotated[
897
+ Optional[str],
898
+ typer.Option(..., help="Use a specific database namespace"),
899
+ ] = None,
900
+ require_table_read: Annotated[
901
+ list[str],
902
+ typer.Option(..., help="Enable table read tools for a specific table"),
903
+ ] = [],
904
+ require_table_write: Annotated[
905
+ list[str],
906
+ typer.Option(..., help="Enable table write tools for a specific table"),
907
+ ] = [],
908
+ require_computer_use: Annotated[
909
+ Optional[bool],
910
+ typer.Option(
911
+ ...,
912
+ help="Enable computer use (requires computer-use-preview model)",
913
+ hidden=True,
914
+ ),
915
+ ] = False,
916
+ reply_all: Annotated[
917
+ bool, typer.Option(help="Reply-all when responding to emails")
918
+ ] = False,
919
+ enable_attachments: Annotated[
920
+ bool, typer.Option(help="Allow downloading and processing email attachments")
921
+ ] = False,
922
+ working_directory: Annotated[
923
+ Optional[str],
924
+ typer.Option(..., help="The default working directory for shell commands"),
925
+ ] = None,
926
+ skill_dir: Annotated[
927
+ list[str],
928
+ typer.Option(..., help="an agent skills directory"),
929
+ ] = [],
930
+ llm_participant: Annotated[
931
+ Optional[str],
932
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
933
+ ] = None,
934
+ shell_image: Annotated[
935
+ Optional[str],
936
+ typer.Option(..., help="an image tag to use to run shell commands in"),
937
+ ] = None,
938
+ delegate_shell_token: Annotated[
939
+ Optional[bool],
940
+ typer.Option(..., help="Delegate the room token to shell tools"),
941
+ ] = False,
942
+ log_llm_requests: Annotated[
943
+ Optional[bool],
944
+ typer.Option(..., help="log all requests to the llm"),
945
+ ] = False,
946
+ ):
947
+ service = get_service(host=host, port=port)
948
+ if path is None:
949
+ path = "/agent"
950
+ i = 0
951
+ while service.has_path(path):
952
+ i += 1
953
+ path = f"/agent{i}"
954
+
955
+ service.agents.append(
956
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "MailBot"})
957
+ )
958
+
959
+ service.add_path(
960
+ identity=agent_name,
961
+ path=path,
962
+ cls=build_mailbot(
963
+ queue=queue,
964
+ computer_use=None,
965
+ model=model,
966
+ local_shell=require_local_shell,
967
+ web_search=require_web_search,
968
+ rule=rule,
969
+ schema=require_schema + schema,
970
+ toolkit=require_toolkit + toolkit,
971
+ image_generation=None,
972
+ rules_file=rules_file,
973
+ email_address=email_address,
974
+ toolkit_name=toolkit_name,
975
+ room_rules_paths=room_rules,
976
+ whitelist=whitelist,
977
+ require_shell=require_shell,
978
+ require_apply_patch=require_apply_patch,
979
+ require_storage=require_storage,
980
+ require_read_only_storage=require_read_only_storage,
981
+ require_time=require_time,
982
+ require_uuid=require_uuid,
983
+ require_table_read=require_table_read,
984
+ require_table_write=require_table_write,
985
+ require_computer_use=require_computer_use,
986
+ reply_all=reply_all,
987
+ database_namespace=database_namespace,
988
+ enable_attachments=enable_attachments,
989
+ working_directory=working_directory,
990
+ skill_dirs=skill_dir,
991
+ shell_image=shell_image,
992
+ llm_participant=llm_participant,
993
+ delegate_shell_token=delegate_shell_token,
994
+ log_llm_requests=log_llm_requests,
995
+ ),
996
+ )
997
+
998
+ spec = service_specs()[0]
999
+ spec.metadata.annotations = {
1000
+ "meshagent.service.id": service_name,
1001
+ }
1002
+
1003
+ spec.metadata.name = service_name
1004
+ spec.metadata.description = service_description
1005
+ spec.container.image = (
1006
+ "us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
1007
+ )
1008
+ spec.container.command = shlex.join(
1009
+ ["meshagent", "mailbot", "service", *cleanup_args(sys.argv[2:])]
1010
+ )
1011
+
1012
+ print(yaml.dump(spec.model_dump(mode="json", exclude_none=True), sort_keys=False))
1013
+
1014
+
1015
+ @app.async_command("deploy")
1016
+ async def deploy(
1017
+ *,
1018
+ service_name: Annotated[str, typer.Option("--service-name", help="service name")],
1019
+ service_description: Annotated[
1020
+ Optional[str], typer.Option("--service-description", help="service description")
1021
+ ] = None,
1022
+ service_title: Annotated[
1023
+ Optional[str],
1024
+ typer.Option("--service-title", help="a display name for the service"),
1025
+ ] = None,
1026
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
1027
+ rule: Annotated[List[str], typer.Option("--rule", "-r", help="a system rule")] = [],
1028
+ rules_file: Optional[str] = None,
1029
+ require_toolkit: Annotated[
1030
+ List[str],
1031
+ typer.Option(
1032
+ "--require-toolkit", "-rt", help="the name or url of a required toolkit"
1033
+ ),
1034
+ ] = [],
1035
+ require_schema: Annotated[
1036
+ List[str],
1037
+ typer.Option(
1038
+ "--require-schema", "-rs", help="the name or url of a required schema"
1039
+ ),
1040
+ ] = [],
1041
+ toolkit: Annotated[
1042
+ List[str],
1043
+ typer.Option(
1044
+ "--toolkit", "-t", help="the name or url of a required toolkit", hidden=True
1045
+ ),
1046
+ ] = [],
1047
+ schema: Annotated[
1048
+ List[str],
1049
+ typer.Option(
1050
+ "--schema", "-s", help="the name or url of a required schema", hidden=True
1051
+ ),
1052
+ ] = [],
1053
+ model: Annotated[
1054
+ str, typer.Option(..., help="Name of the LLM model to use for the chatbot")
1055
+ ] = "gpt-5.2",
1056
+ require_shell: Annotated[
1057
+ Optional[bool],
1058
+ typer.Option(..., help="Enable function shell tool calling"),
1059
+ ] = False,
1060
+ require_local_shell: Annotated[
1061
+ Optional[bool], typer.Option(..., help="Enable local shell tool calling")
1062
+ ] = False,
1063
+ require_web_search: Annotated[
1064
+ Optional[bool], typer.Option(..., help="Enable web search tool calling")
1065
+ ] = False,
1066
+ require_apply_patch: Annotated[
1067
+ Optional[bool],
1068
+ typer.Option(..., help="Enable apply patch tool calling"),
1069
+ ] = False,
1070
+ host: Annotated[
1071
+ Optional[str], typer.Option(help="Host to bind the service on")
1072
+ ] = None,
1073
+ port: Annotated[
1074
+ Optional[int], typer.Option(help="Port to bind the service on")
1075
+ ] = None,
1076
+ path: Annotated[
1077
+ Optional[str], typer.Option(help="HTTP path to mount the service at")
1078
+ ] = None,
1079
+ queue: Annotated[
1080
+ Optional[str], typer.Option(..., help="the name of the mail queue")
1081
+ ] = None,
1082
+ email_address: Annotated[
1083
+ str, typer.Option(..., help="the email address of the agent")
1084
+ ],
1085
+ toolkit_name: Annotated[
1086
+ Optional[str],
1087
+ typer.Option(..., help="the name of a toolkit to expose mail operations"),
1088
+ ] = None,
1089
+ room_rules: Annotated[
1090
+ List[str],
1091
+ typer.Option(
1092
+ "--room-rules",
1093
+ "-rr",
1094
+ help="a path to a rules file within the room that can be used to customize the agent's behavior",
1095
+ ),
1096
+ ] = [],
1097
+ whitelist: Annotated[
1098
+ List[str],
1099
+ typer.Option(
1100
+ "--whitelist",
1101
+ help="an email to whitelist",
1102
+ ),
1103
+ ] = [],
1104
+ require_storage: Annotated[
1105
+ Optional[bool], typer.Option(..., help="Enable storage toolkit")
1106
+ ] = False,
1107
+ require_read_only_storage: Annotated[
1108
+ Optional[bool],
1109
+ typer.Option(..., help="Enable read only storage toolkit"),
1110
+ ] = False,
1111
+ require_time: Annotated[
1112
+ bool,
1113
+ typer.Option(
1114
+ ...,
1115
+ help="Enable time/datetime tools",
1116
+ ),
1117
+ ] = True,
1118
+ require_uuid: Annotated[
1119
+ bool,
1120
+ typer.Option(
1121
+ ...,
1122
+ help="Enable UUID generation tools",
1123
+ ),
1124
+ ] = False,
1125
+ database_namespace: Annotated[
1126
+ Optional[str],
1127
+ typer.Option(..., help="Use a specific database namespace"),
1128
+ ] = None,
1129
+ require_table_read: Annotated[
1130
+ list[str],
1131
+ typer.Option(..., help="Enable table read tools for a specific table"),
1132
+ ] = [],
1133
+ require_table_write: Annotated[
1134
+ list[str],
1135
+ typer.Option(..., help="Enable table write tools for a specific table"),
1136
+ ] = [],
1137
+ require_computer_use: Annotated[
1138
+ Optional[bool],
1139
+ typer.Option(
1140
+ ...,
1141
+ help="Enable computer use (requires computer-use-preview model)",
1142
+ hidden=True,
1143
+ ),
1144
+ ] = False,
1145
+ reply_all: Annotated[
1146
+ bool, typer.Option(help="Reply-all when responding to emails")
1147
+ ] = False,
1148
+ enable_attachments: Annotated[
1149
+ bool, typer.Option(help="Allow downloading and processing email attachments")
1150
+ ] = False,
1151
+ working_directory: Annotated[
1152
+ Optional[str],
1153
+ typer.Option(..., help="The default working directory for shell commands"),
1154
+ ] = None,
1155
+ skill_dir: Annotated[
1156
+ list[str],
1157
+ typer.Option(..., help="an agent skills directory"),
1158
+ ] = [],
1159
+ llm_participant: Annotated[
1160
+ Optional[str],
1161
+ typer.Option(..., help="Delegate LLM interactions to a remote participant"),
1162
+ ] = None,
1163
+ shell_image: Annotated[
1164
+ Optional[str],
1165
+ typer.Option(..., help="an image tag to use to run shell commands in"),
1166
+ ] = None,
1167
+ delegate_shell_token: Annotated[
1168
+ Optional[bool],
1169
+ typer.Option(..., help="Delegate the room token to shell tools"),
1170
+ ] = False,
1171
+ log_llm_requests: Annotated[
1172
+ Optional[bool],
1173
+ typer.Option(..., help="log all requests to the llm"),
1174
+ ] = False,
1175
+ project_id: ProjectIdOption,
1176
+ room: Annotated[
1177
+ Optional[str],
1178
+ typer.Option("--room", help="The name of a room to create the service for"),
1179
+ ] = None,
1180
+ ):
1181
+ project_id = await resolve_project_id(project_id=project_id)
1182
+
1183
+ service = get_service(host=host, port=port)
1184
+ if path is None:
1185
+ path = "/agent"
1186
+ i = 0
1187
+ while service.has_path(path):
1188
+ i += 1
1189
+ path = f"/agent{i}"
1190
+
1191
+ service.agents.append(
1192
+ AgentSpec(name=agent_name, annotations={ANNOTATION_AGENT_TYPE: "MailBot"})
1193
+ )
1194
+
1195
+ service.add_path(
1196
+ identity=agent_name,
1197
+ path=path,
1198
+ cls=build_mailbot(
1199
+ queue=queue,
1200
+ computer_use=None,
1201
+ model=model,
1202
+ local_shell=require_local_shell,
1203
+ web_search=require_web_search,
1204
+ rule=rule,
1205
+ schema=require_schema + schema,
1206
+ toolkit=require_toolkit + toolkit,
1207
+ image_generation=None,
1208
+ rules_file=rules_file,
1209
+ email_address=email_address,
1210
+ toolkit_name=toolkit_name,
1211
+ room_rules_paths=room_rules,
1212
+ whitelist=whitelist,
1213
+ require_shell=require_shell,
1214
+ require_apply_patch=require_apply_patch,
1215
+ require_storage=require_storage,
1216
+ require_read_only_storage=require_read_only_storage,
1217
+ require_time=require_time,
1218
+ require_uuid=require_uuid,
1219
+ require_table_read=require_table_read,
1220
+ require_table_write=require_table_write,
1221
+ require_computer_use=require_computer_use,
1222
+ reply_all=reply_all,
1223
+ database_namespace=database_namespace,
1224
+ enable_attachments=enable_attachments,
1225
+ working_directory=working_directory,
1226
+ skill_dirs=skill_dir,
1227
+ shell_image=shell_image,
1228
+ llm_participant=llm_participant,
1229
+ delegate_shell_token=delegate_shell_token,
1230
+ log_llm_requests=log_llm_requests,
1231
+ ),
1232
+ )
1233
+
1234
+ spec = service_specs()[0]
1235
+ spec.metadata.annotations = {
1236
+ "meshagent.service.id": service_name,
1237
+ }
1238
+
1239
+ spec.metadata.name = service_name
1240
+ spec.metadata.description = service_description
1241
+ spec.container.image = (
1242
+ "us-central1-docker.pkg.dev/meshagent-public/images/cli:{SERVER_VERSION}-esgz"
1243
+ )
1244
+ spec.container.command = shlex.join(
1245
+ ["meshagent", "mailbot", *cleanup_args(sys.argv[:2])]
1246
+ )
1247
+
1248
+ client = await get_client()
1249
+ try:
1250
+ id = None
1251
+ try:
1252
+ if id is None:
1253
+ if room is None:
1254
+ services = await client.list_services(project_id=project_id)
1255
+ else:
1256
+ services = await client.list_room_services(
1257
+ project_id=project_id, room_name=room
1258
+ )
1259
+
1260
+ for s in services:
1261
+ if s.metadata.name == spec.metadata.name:
1262
+ id = s.id
1263
+
1264
+ if id is None:
1265
+ if room is None:
1266
+ id = await client.create_service(
1267
+ project_id=project_id, service=spec
1268
+ )
1269
+ else:
1270
+ id = await client.create_room_service(
1271
+ project_id=project_id, service=spec, room_name=room
1272
+ )
1273
+
1274
+ else:
1275
+ spec.id = id
1276
+ if room is None:
1277
+ await client.update_service(
1278
+ project_id=project_id, service_id=id, service=spec
1279
+ )
1280
+ else:
1281
+ await client.update_room_service(
1282
+ project_id=project_id,
1283
+ service_id=id,
1284
+ service=spec,
1285
+ room_name=room,
1286
+ )
1287
+
1288
+ except ConflictError:
1289
+ print(f"[red]Service name already in use: {spec.metadata.name}[/red]")
1290
+ raise typer.Exit(code=1)
1291
+ else:
1292
+ print(f"[green]Deployed service:[/] {id}")
1293
+
1294
+ finally:
1295
+ await client.close()