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,70 @@
1
+ import asyncio
2
+ import json
3
+ from rich import print
4
+ from meshagent.cli.common_options import ProjectIdOption, RoomOption
5
+ from meshagent.cli import async_typer
6
+ from meshagent.cli.helper import (
7
+ get_client,
8
+ resolve_project_id,
9
+ resolve_room,
10
+ )
11
+ from meshagent.api import (
12
+ RoomClient,
13
+ WebSocketClientProtocol,
14
+ )
15
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
16
+
17
+ app = async_typer.AsyncTyper(help="Developer utilities for a room")
18
+
19
+
20
+ @app.async_command("watch", help="Stream developer logs from a room")
21
+ async def watch_logs(
22
+ *,
23
+ project_id: ProjectIdOption,
24
+ room: RoomOption,
25
+ ):
26
+ """
27
+ Watch logs from the developer feed in the specified room.
28
+ """
29
+
30
+ account_client = await get_client()
31
+ try:
32
+ # Resolve project ID (or fetch from the active project if not provided)
33
+ project_id = await resolve_project_id(project_id=project_id)
34
+ room = resolve_room(room)
35
+
36
+ connection = await account_client.connect_room(project_id=project_id, room=room)
37
+
38
+ print("[bold green]Connecting to room...[/bold green]")
39
+ async with RoomClient(
40
+ protocol=WebSocketClientProtocol(
41
+ url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
42
+ token=connection.jwt,
43
+ )
44
+ ) as client:
45
+ # Create a developer client from the room client
46
+
47
+ # Define how to handle the incoming log events
48
+ def handle_log(type: str, data: dict):
49
+ # You can customize this print to suit your needs
50
+ print(f"[magenta]{type}[/magenta]: {json.dumps(data, indent=2)}")
51
+
52
+ # Attach our handler to the "log" event
53
+ client.developer.on("log", handle_log)
54
+
55
+ # Enable watching
56
+ await client.developer.enable()
57
+ print("[bold cyan]watching enabled. Press Ctrl+C to stop.[/bold cyan]")
58
+
59
+ try:
60
+ # Block forever, until Ctrl+C
61
+ while True:
62
+ await asyncio.sleep(10)
63
+ except KeyboardInterrupt:
64
+ print("[bold red]Stopping watch...[/bold red]")
65
+ finally:
66
+ # Disable watching before exiting
67
+ await client.developer.disable()
68
+
69
+ finally:
70
+ await account_client.close()
meshagent/cli/exec.py ADDED
@@ -0,0 +1,397 @@
1
+ import sys
2
+ import os
3
+ from meshagent.api.websocket_protocol import WebSocketClientProtocol
4
+ from meshagent.api import RoomClient
5
+ from meshagent.api.helpers import websocket_room_url
6
+ from typing import Annotated, Optional
7
+ from meshagent.cli.common_options import ProjectIdOption, RoomOption
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
+ import threading
18
+ import time
19
+
20
+
21
+ import logging
22
+
23
+ from meshagent.cli.helper import (
24
+ get_client,
25
+ resolve_project_id,
26
+ resolve_room,
27
+ )
28
+
29
+ if os.name == "nt":
30
+ import msvcrt
31
+ import ctypes
32
+ from ctypes import wintypes
33
+
34
+ _kernel32 = ctypes.windll.kernel32
35
+ _ENABLE_ECHO_INPUT = 0x0004
36
+ _ENABLE_LINE_INPUT = 0x0002
37
+
38
+ def set_raw(f):
39
+ """Disable line and echo mode for the given file handle."""
40
+ handle = msvcrt.get_osfhandle(f.fileno())
41
+ original_mode = wintypes.DWORD()
42
+ if not _kernel32.GetConsoleMode(handle, ctypes.byref(original_mode)):
43
+ return None
44
+ new_mode = original_mode.value & ~(_ENABLE_ECHO_INPUT | _ENABLE_LINE_INPUT)
45
+ _kernel32.SetConsoleMode(handle, new_mode)
46
+ return handle, original_mode.value
47
+
48
+ def restore(f, state):
49
+ if state is None:
50
+ return None
51
+ handle, mode = state
52
+ _kernel32.SetConsoleMode(handle, mode)
53
+ return None
54
+
55
+ else:
56
+ import termios
57
+ import tty as _tty
58
+
59
+ def set_raw(fd):
60
+ old = termios.tcgetattr(fd)
61
+ _tty.setraw(fd)
62
+ return old
63
+
64
+ def restore(fd, old_settings):
65
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
66
+
67
+
68
+ class _StdWriter:
69
+ """Simple asyncio-friendly wrapper for standard streams on Windows."""
70
+
71
+ def __init__(self, file):
72
+ self._file = file
73
+
74
+ def write(self, data: bytes) -> None:
75
+ self._file.buffer.write(data)
76
+
77
+ async def drain(self) -> None:
78
+ await asyncio.get_running_loop().run_in_executor(None, self._file.flush)
79
+
80
+
81
+ def register(app: typer.Typer):
82
+ @app.async_command("exec")
83
+ async def exec_command(
84
+ *,
85
+ project_id: ProjectIdOption,
86
+ room: RoomOption,
87
+ name: Annotated[
88
+ Optional[str], typer.Option(help="Optional exec session name")
89
+ ] = None,
90
+ image: Annotated[
91
+ Optional[str],
92
+ typer.Option(help="Optional container image to use for the exec session"),
93
+ ] = None,
94
+ command: Annotated[
95
+ list[str],
96
+ typer.Argument(..., help="Command to execute (omit when using `--tty`)"),
97
+ ] = None,
98
+ tty: Annotated[
99
+ bool,
100
+ typer.Option(
101
+ "--tty/--no-tty",
102
+ help="Allocate an interactive TTY (requires a real terminal)",
103
+ ),
104
+ ] = False,
105
+ room_storage_path: Annotated[
106
+ str, typer.Option(help="Room storage mount path (default: /data)")
107
+ ] = "/data",
108
+ ):
109
+ """Open an interactive websocket‑based TTY."""
110
+ client = await get_client()
111
+ try:
112
+ project_id = await resolve_project_id(project_id=project_id)
113
+ room = resolve_room(room)
114
+
115
+ connection = await client.connect_room(project_id=project_id, room=room)
116
+
117
+ ws_url = (
118
+ websocket_room_url(room_name=room) + f"/exec?token={connection.jwt}"
119
+ )
120
+
121
+ if image:
122
+ ws_url += f"&image={quote(' '.join(image))}"
123
+
124
+ if name:
125
+ ws_url += f"&name={quote(' '.join(name))}"
126
+
127
+ if command and len(command) != 0:
128
+ ws_url += f"&command={quote(' '.join(command))}"
129
+
130
+ if room_storage_path:
131
+ room_storage_path += (
132
+ f"&room_storage_path={quote(' '.join(room_storage_path))}"
133
+ )
134
+
135
+ if tty:
136
+ if not sys.stdin.isatty():
137
+ print("[red]TTY requested but process is not a TTY[/red]")
138
+ raise typer.Exit(1)
139
+
140
+ ws_url += "&tty=true"
141
+
142
+ else:
143
+ if command is None:
144
+ print("[red]TTY required when not executing a command[/red]")
145
+ raise typer.Exit(1)
146
+
147
+ ws_url += "&tty=false"
148
+
149
+ if tty:
150
+ # Save current terminal settings so we can restore them later.
151
+ old_tty_settings = set_raw(sys.stdin)
152
+
153
+ async with RoomClient(
154
+ protocol=WebSocketClientProtocol(
155
+ url=websocket_room_url(room_name=room),
156
+ token=connection.jwt,
157
+ )
158
+ ):
159
+ try:
160
+ async with aiohttp.ClientSession() as session:
161
+ async with session.ws_connect(ws_url) as websocket:
162
+ send_queue = asyncio.Queue[bytes]()
163
+
164
+ loop = asyncio.get_running_loop()
165
+ if os.name == "nt":
166
+ stdout_writer = _StdWriter(sys.stdout)
167
+ stderr_writer = _StdWriter(sys.stderr)
168
+ else:
169
+ (
170
+ stdout_transport,
171
+ stdout_protocol,
172
+ ) = await loop.connect_write_pipe(
173
+ asyncio.streams.FlowControlMixin, sys.stdout
174
+ )
175
+ stdout_writer = asyncio.StreamWriter(
176
+ stdout_transport, stdout_protocol, None, loop
177
+ )
178
+
179
+ (
180
+ stderr_transport,
181
+ stderr_protocol,
182
+ ) = await loop.connect_write_pipe(
183
+ asyncio.streams.FlowControlMixin, sys.stderr
184
+ )
185
+ stderr_writer = asyncio.StreamWriter(
186
+ stderr_transport, stderr_protocol, None, loop
187
+ )
188
+
189
+ async def recv_from_websocket():
190
+ while True:
191
+ done, pending = await asyncio.wait(
192
+ [asyncio.create_task(websocket.receive())],
193
+ return_when=asyncio.FIRST_COMPLETED,
194
+ )
195
+
196
+ first = done.pop()
197
+
198
+ if first == read_stdin_task:
199
+ break
200
+
201
+ message = first.result()
202
+
203
+ if websocket.closed:
204
+ break
205
+
206
+ if message.type == aiohttp.WSMsgType.CLOSE:
207
+ break
208
+
209
+ elif message.type == aiohttp.WSMsgType.CLOSING:
210
+ pass
211
+
212
+ elif message.type == aiohttp.WSMsgType.ERROR:
213
+ break
214
+
215
+ if not message.data:
216
+ break
217
+
218
+ data: bytes = message.data
219
+ if len(data) > 0:
220
+ if data[0] == 1:
221
+ stderr_writer.write(data)
222
+ await stderr_writer.drain()
223
+ elif data[0] == 0:
224
+ stdout_writer.write(data)
225
+ await stdout_writer.drain()
226
+ else:
227
+ raise ValueError(
228
+ f"Invalid channel received {data[0]}"
229
+ )
230
+
231
+ last_size = None
232
+
233
+ async def send_resize(rows, cols):
234
+ nonlocal last_size
235
+
236
+ size = (cols, rows)
237
+ if size == last_size:
238
+ return
239
+
240
+ last_size = size
241
+
242
+ resize_json = json.dumps(
243
+ {"Width": cols, "Height": rows}
244
+ ).encode("utf-8")
245
+ payload = struct.pack("B", 4) + resize_json
246
+ send_queue.put_nowait(payload)
247
+ await asyncio.sleep(5)
248
+
249
+ cols, rows = shutil.get_terminal_size(fallback=(24, 80))
250
+ if tty:
251
+ await send_resize(rows, cols)
252
+
253
+ def on_sigwinch():
254
+ cols, rows = shutil.get_terminal_size(fallback=(24, 80))
255
+ task = asyncio.create_task(send_resize(rows, cols))
256
+
257
+ def on_done(t: asyncio.Task):
258
+ t.result()
259
+
260
+ task.add_done_callback(on_done)
261
+
262
+ if hasattr(signal, "SIGWINCH"):
263
+ loop.add_signal_handler(signal.SIGWINCH, on_sigwinch)
264
+
265
+ async def read_stdin():
266
+ loop = asyncio.get_running_loop()
267
+
268
+ if os.name == "nt":
269
+ queue: asyncio.Queue[bytes] = asyncio.Queue()
270
+ stop_event = threading.Event()
271
+
272
+ if sys.stdin.isatty():
273
+
274
+ def reader() -> None:
275
+ try:
276
+ while not stop_event.is_set():
277
+ if msvcrt.kbhit():
278
+ data = msvcrt.getch()
279
+ loop.call_soon_threadsafe(
280
+ queue.put_nowait, data
281
+ )
282
+ else:
283
+ time.sleep(0.01)
284
+ finally:
285
+ loop.call_soon_threadsafe(
286
+ queue.put_nowait, b""
287
+ )
288
+ else:
289
+
290
+ def reader() -> None:
291
+ try:
292
+ while not stop_event.is_set():
293
+ data = sys.stdin.buffer.read(1)
294
+ loop.call_soon_threadsafe(
295
+ queue.put_nowait, data
296
+ )
297
+ if not data:
298
+ break
299
+ finally:
300
+ loop.call_soon_threadsafe(
301
+ queue.put_nowait, b""
302
+ )
303
+
304
+ thread = threading.Thread(target=reader)
305
+ thread.start()
306
+
307
+ async def reader_task() -> bytes:
308
+ return await queue.get()
309
+ else:
310
+ reader = asyncio.StreamReader()
311
+ protocol = asyncio.StreamReaderProtocol(reader)
312
+ await loop.connect_read_pipe(
313
+ lambda: protocol, sys.stdin
314
+ )
315
+
316
+ async def reader_task():
317
+ return await reader.read(1)
318
+
319
+ try:
320
+ while True:
321
+ # Read one character at a time from stdin without blocking the event loop.
322
+ done, pending = await asyncio.wait(
323
+ [
324
+ asyncio.create_task(reader_task()),
325
+ websocket_recv_task,
326
+ ],
327
+ return_when=asyncio.FIRST_COMPLETED,
328
+ )
329
+
330
+ first = done.pop()
331
+ if first == websocket_recv_task:
332
+ break
333
+
334
+ data = first.result()
335
+ if not data:
336
+ break
337
+
338
+ if websocket.closed:
339
+ break
340
+
341
+ if tty:
342
+ if data == b"\x04":
343
+ break
344
+
345
+ if data:
346
+ send_queue.put_nowait(b"\0" + data)
347
+ else:
348
+ break
349
+ finally:
350
+ if os.name == "nt":
351
+ stop_event.set()
352
+ thread.join()
353
+
354
+ send_queue.put_nowait(b"\0")
355
+
356
+ websocket_recv_task = asyncio.create_task(
357
+ recv_from_websocket()
358
+ )
359
+ read_stdin_task = asyncio.create_task(read_stdin())
360
+
361
+ async def send_to_websocket():
362
+ while True:
363
+ try:
364
+ data = await send_queue.get()
365
+ if websocket.closed:
366
+ break
367
+
368
+ if data is not None:
369
+ await websocket.send_bytes(data)
370
+
371
+ else:
372
+ break
373
+ except asyncio.QueueShutDown:
374
+ break
375
+
376
+ send_to_websocket_task = asyncio.create_task(
377
+ send_to_websocket()
378
+ )
379
+ await asyncio.gather(
380
+ websocket_recv_task,
381
+ read_stdin_task,
382
+ )
383
+
384
+ send_queue.shutdown()
385
+ await send_to_websocket_task
386
+
387
+ finally:
388
+ if not sys.stdin.closed and tty:
389
+ # Restore original terminal settings even if the coroutine is cancelled.
390
+ restore(sys.stdin, old_tty_settings)
391
+
392
+ except Exception as e:
393
+ print(f"[red]{e}[/red]")
394
+ logging.error("failed", exc_info=e)
395
+ raise typer.Exit(1)
396
+ finally:
397
+ await client.close()
@@ -0,0 +1,236 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from pydantic import BaseModel
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from meshagent.cli import auth_async
8
+ from meshagent.cli import async_typer
9
+ from meshagent.api.helpers import meshagent_base_url
10
+ from meshagent.api.specs.service import ServiceSpec
11
+ from meshagent.agents.context import AgentChatContext
12
+ from meshagent.api.client import Meshagent, RoomConnectionInfo
13
+ import os
14
+ import aiofiles
15
+ from pydantic_yaml import parse_yaml_raw_as
16
+ import json
17
+
18
+ from rich import print
19
+
20
+ SETTINGS_FILE = Path.home() / ".meshagent" / "project.json"
21
+
22
+
23
+ def _ensure_cache_dir():
24
+ SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
25
+
26
+
27
+ class Settings(BaseModel):
28
+ active_project: Optional[str] = None
29
+ active_api_keys: Optional[dict] = {}
30
+
31
+
32
+ def _save_settings(s: Settings):
33
+ _ensure_cache_dir()
34
+ SETTINGS_FILE.write_text(s.model_dump_json())
35
+
36
+
37
+ def _load_settings():
38
+ try:
39
+ _ensure_cache_dir()
40
+ if SETTINGS_FILE.exists():
41
+ return Settings.model_validate_json(SETTINGS_FILE.read_text())
42
+ except OSError as ex:
43
+ if ex.errno == 30:
44
+ return Settings()
45
+ else:
46
+ raise
47
+
48
+
49
+ async def get_active_project():
50
+ settings = _load_settings()
51
+ if settings is None:
52
+ return None
53
+ return settings.active_project
54
+
55
+
56
+ async def set_active_project(project_id: str | None):
57
+ settings = _load_settings()
58
+ settings.active_project = project_id
59
+ _save_settings(settings)
60
+
61
+
62
+ async def set_active_api_key(project_id: str, key: str):
63
+ settings = _load_settings()
64
+ settings.active_api_keys[project_id] = key
65
+ _save_settings(settings)
66
+
67
+
68
+ async def get_active_api_key(project_id: str):
69
+ settings = _load_settings()
70
+ if settings is None:
71
+ return None
72
+ key: str = settings.active_api_keys.get(project_id)
73
+ # Ignore old keys, API key format changed
74
+ if key is not None and key.startswith("ma-"):
75
+ return key
76
+ else:
77
+ return None
78
+
79
+
80
+ app = async_typer.AsyncTyper()
81
+
82
+
83
+ class CustomMeshagentClient(Meshagent):
84
+ async def connect_room(self, *, project_id: str, room: str) -> RoomConnectionInfo:
85
+ from urllib.parse import quote
86
+
87
+ jwt = os.getenv("MESHAGENT_TOKEN")
88
+
89
+ if jwt is not None and room == os.getenv("MESHAGENT_ROOM"):
90
+ return RoomConnectionInfo(
91
+ jwt=jwt,
92
+ room_name=room,
93
+ project_id=os.getenv("MESHAGENT_PROJECT_ID"),
94
+ room_url=meshagent_base_url() + f"/rooms/{quote(room)}",
95
+ )
96
+
97
+ return await super().connect_room(project_id=project_id, room=room)
98
+
99
+
100
+ async def get_client():
101
+ key = os.getenv("MESHAGENT_API_KEY")
102
+ if key is not None or os.getenv("MESHAGENT_SESSION_ID") is not None:
103
+ return CustomMeshagentClient(
104
+ base_url=meshagent_base_url(),
105
+ token=key,
106
+ )
107
+ else:
108
+ access_token = await auth_async.get_access_token()
109
+ return CustomMeshagentClient(
110
+ base_url=meshagent_base_url(),
111
+ token=access_token,
112
+ )
113
+
114
+
115
+ def print_json_table(records: list, *cols):
116
+ if not records:
117
+ raise SystemExit("No rows to print")
118
+
119
+ # 2️⃣ --- build the table -------------------------------------------
120
+ table = Table(show_header=True, header_style="bold magenta")
121
+
122
+ if len(cols) > 0:
123
+ # use the keys of the first object as column order
124
+ for col in cols:
125
+ table.add_column(col.title()) # "id" → "Id"
126
+
127
+ for row in records:
128
+ table.add_row(*(str(row.get(col, "")) for col in cols))
129
+
130
+ else:
131
+ # use the keys of the first object as column order
132
+ for col in records[0]:
133
+ table.add_column(col.title()) # "id" → "Id"
134
+
135
+ for row in records:
136
+ table.add_row(*(str(row.get(col, "")) for col in records[0]))
137
+
138
+ # 3️⃣ --- render ------------------------------------------------------
139
+ Console().print(table)
140
+
141
+
142
+ def resolve_room(room_name: Optional[str] = None):
143
+ if room_name is None:
144
+ room_name = os.getenv("MESHAGENT_ROOM")
145
+
146
+ return room_name
147
+
148
+
149
+ async def resolve_project_id(project_id: Optional[str] = None):
150
+ if project_id is None:
151
+ project_id = os.getenv("MESHAGENT_PROJECT_ID") or await get_active_project()
152
+
153
+ if project_id is None:
154
+ print(
155
+ "[red]Project ID not specified, activate a project or pass a project on the command line[/red]"
156
+ )
157
+ raise typer.Exit(code=1)
158
+
159
+ return project_id
160
+
161
+
162
+ async def init_context_from_spec(context: AgentChatContext) -> None:
163
+ path = os.getenv("MESHAGENT_SPEC_PATH")
164
+
165
+ if path is None:
166
+ return None
167
+
168
+ async with aiofiles.open(path, "r") as file:
169
+ spec_str = await file.read()
170
+ try:
171
+ json.loads(spec_str)
172
+ spec = ServiceSpec.model_validate_json(spec_str)
173
+ except ValueError:
174
+ # fallback on yaml parser if spec can't
175
+ spec = parse_yaml_raw_as(ServiceSpec, spec_str)
176
+
177
+ readme = spec.metadata.annotations.get("meshagent.service.readme")
178
+
179
+ if spec.metadata.description:
180
+ context.append_assistant_message(
181
+ f"This agent's description:\n{spec.metadata.description}"
182
+ )
183
+
184
+ if readme is not None:
185
+ context.append_assistant_message(f"This agent's README:\n{readme}")
186
+
187
+
188
+ async def resolve_key(project_id: str | None, key: str | None):
189
+ project_id = await resolve_project_id(project_id=project_id)
190
+ if key is None:
191
+ key = await get_active_api_key(project_id=project_id)
192
+
193
+ if key is None:
194
+ key = os.getenv("MESHAGENT_API_KEY")
195
+
196
+ if key is None and os.getenv("MESHAGENT_TOKEN") is None:
197
+ print(
198
+ "[red]--key is required if MESHAGENT_API_KEY is not set. You can use meshagent api-key create to create a new api key."
199
+ )
200
+ raise typer.Exit(1)
201
+
202
+ return key
203
+
204
+
205
+ def cleanup_args(args: list[str]):
206
+ out = []
207
+ i = 0
208
+ while i < len(args):
209
+ if args[i] == "--service-name":
210
+ i += 1
211
+ elif args[i] == "--service-title":
212
+ i += 1
213
+ elif args[i] == "--service-description":
214
+ i += 1
215
+ elif args[i] == "--project-id":
216
+ i += 1
217
+ elif args[i] == "--room":
218
+ i += 1
219
+ elif args[i].startswith("--service-name="):
220
+ pass
221
+ elif args[i].startswith("--service-title="):
222
+ pass
223
+ elif args[i].startswith("--service-description="):
224
+ pass
225
+ elif args[i].startswith("--project-id="):
226
+ pass
227
+ elif args[i].startswith("--room="):
228
+ pass
229
+ elif args[i] == "deploy":
230
+ pass
231
+ elif args[i] == "spec":
232
+ pass
233
+ else:
234
+ out.append(args[i])
235
+ i += 1
236
+ return out