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

@@ -0,0 +1,113 @@
1
+
2
+ from mcp.client.session import ClientSession
3
+ from mcp.client.sse import sse_client
4
+ from mcp.client.stdio import stdio_client, StdioServerParameters
5
+
6
+ from meshagent.mcp import MCPToolkit
7
+
8
+ from meshagent.cli import async_typer
9
+ import typer
10
+ from meshagent.cli.helper import get_client, print_json_table, set_active_project, resolve_project_id
11
+ from rich import print
12
+ from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol, RoomException
13
+ from meshagent.cli.helper import set_active_project, get_active_project, resolve_project_id, resolve_api_key
14
+ from typing import Annotated, Optional
15
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
16
+
17
+ from meshagent.api.services import send_webhook
18
+ from meshagent.tools.hosting import RemoteToolkit
19
+ import shlex
20
+
21
+ app = async_typer.AsyncTyper()
22
+
23
+ @app.async_command("sse")
24
+ async def sse(*, project_id: str = None, room: Annotated[str, typer.Option()], api_key_id: Annotated[Optional[str], typer.Option()] = None, name: Annotated[str, typer.Option(..., help="Participant name")] = "cli", role: str = "tool", url: Annotated[str, typer.Option()]):
25
+ account_client = await get_client()
26
+ try:
27
+ project_id = await resolve_project_id(project_id=project_id)
28
+ api_key_id = await resolve_api_key(project_id, api_key_id)
29
+
30
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
31
+
32
+ token = ParticipantToken(
33
+ name=name,
34
+ project_id=project_id,
35
+ api_key_id=api_key_id
36
+ )
37
+
38
+ token.add_role_grant(role=role)
39
+ token.add_room_grant(room)
40
+
41
+ print("[bold green]Connecting to room...[/bold green]")
42
+ async with RoomClient(protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()), token=token.to_jwt(token=key))) as client:
43
+
44
+ async with sse_client(url) as (read_stream, write_stream):
45
+
46
+ async with ClientSession(read_stream=read_stream, write_stream=write_stream) as session:
47
+
48
+ mcp_tools_response = await session.list_tools()
49
+
50
+ toolkit = MCPToolkit(name=name, session=session, tools=mcp_tools_response.tools)
51
+
52
+ remote_toolkit = RemoteToolkit(name=toolkit.name, tools=toolkit.tools, title=toolkit.title, description=toolkit.description)
53
+
54
+ await remote_toolkit.start(room=client)
55
+ try:
56
+ await client.protocol.wait_for_close()
57
+ except KeyboardInterrupt:
58
+ await remote_toolkit.stop()
59
+
60
+
61
+ except RoomException as e:
62
+ print(f"[red]{e}[/red]")
63
+ finally:
64
+ await account_client.close()
65
+
66
+
67
+
68
+ @app.async_command("stdio")
69
+ async def stdio(*, project_id: str = None, room: Annotated[str, typer.Option()], api_key_id: Annotated[Optional[str], typer.Option()] = None, name: Annotated[str, typer.Option(..., help="Participant name")] = "cli", role: str = "tool", command: Annotated[str, typer.Option()], args: Annotated[str, typer.Option()]):
70
+ account_client = await get_client()
71
+ try:
72
+ project_id = await resolve_project_id(project_id=project_id)
73
+ api_key_id = await resolve_api_key(project_id, api_key_id)
74
+
75
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
76
+
77
+ token = ParticipantToken(
78
+ name=name,
79
+ project_id=project_id,
80
+ api_key_id=api_key_id
81
+ )
82
+
83
+ token.add_role_grant(role=role)
84
+ token.add_room_grant(room)
85
+
86
+ print("[bold green]Connecting to room...[/bold green]")
87
+ async with RoomClient(protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()), token=token.to_jwt(token=key))) as client:
88
+
89
+ async with stdio_client(StdioServerParameters(
90
+ command=command, # Executable
91
+ args=shlex.split(args), # Optional command line arguments
92
+ env=None, # Optional environment variables
93
+ )) as (read_stream, write_stream):
94
+
95
+ async with ClientSession(read_stream=read_stream, write_stream=write_stream) as session:
96
+
97
+ mcp_tools_response = await session.list_tools()
98
+
99
+ toolkit = MCPToolkit(name=name, session=session, tools=mcp_tools_response.tools)
100
+
101
+ remote_toolkit = RemoteToolkit(name=toolkit.name, tools=toolkit.tools, title=toolkit.title, description=toolkit.description)
102
+
103
+ await remote_toolkit.start(room=client)
104
+ try:
105
+ await client.protocol.wait_for_close()
106
+ except KeyboardInterrupt:
107
+ await remote_toolkit.stop()
108
+
109
+
110
+ except RoomException as e:
111
+ print(f"[red]{e}[/red]")
112
+ finally:
113
+ await account_client.close()
@@ -0,0 +1,383 @@
1
+ # --------------------------------------------------------------------------
2
+ # Imports
3
+ # --------------------------------------------------------------------------
4
+ from meshagent.cli import async_typer
5
+ import typer
6
+ from rich import print
7
+ from meshagent.cli.helper import get_client, print_json_table, resolve_project_id
8
+ from typing import Annotated, Dict, Optional
9
+
10
+ from meshagent.api.accounts_client import PullSecret, KeysSecret, SecretLike # or wherever you defined them
11
+
12
+ # --------------------------------------------------------------------------
13
+ # App Definition
14
+ # --------------------------------------------------------------------------
15
+ secrets_app = async_typer.AsyncTyper(help="Manage secrets for your project.")
16
+
17
+
18
+ # --------------------------------------------------------------------------
19
+ # Utility helpers
20
+ # --------------------------------------------------------------------------
21
+
22
+ def _parse_kv_inline(source: str | None) -> Dict[str, str]:
23
+ """
24
+ Parse a space-separated list of `key=value` tokens into a dict.
25
+ """
26
+ if source is None:
27
+ return {}
28
+ tokens = source.strip().split()
29
+ kv: Dict[str, str] = {}
30
+ for t in tokens:
31
+ if "=" not in t:
32
+ raise typer.BadParameter(f"Expected key=value, got '{t}'")
33
+ k, v = t.split("=", 1)
34
+ kv[k] = v
35
+ return kv
36
+
37
+
38
+ # --------------------------------------------------------------------------
39
+ # Subcommand group: "keys"
40
+ # e.g.: meshagent secrets keys create --name <NAME> --data ...
41
+ # --------------------------------------------------------------------------
42
+ keys_app = async_typer.AsyncTyper(help="Create or update environment-based key-value secrets.")
43
+
44
+ @keys_app.async_command("create")
45
+ async def create_keys_secret(
46
+ *,
47
+ project_id: Optional[str] = typer.Option(None),
48
+ name: Annotated[str, typer.Option(help="Secret name")],
49
+ data: Annotated[
50
+ str,
51
+ typer.Option(
52
+ "--data",
53
+ help="Format: key=value key2=value (space-separated)",
54
+ ),
55
+ ]
56
+ ):
57
+ """
58
+ Create a new 'keys' secret (opaque env-vars).
59
+ """
60
+ client = await get_client()
61
+ try:
62
+ project_id = await resolve_project_id(project_id)
63
+ data_dict = _parse_kv_inline(data)
64
+
65
+ secret_obj = KeysSecret(
66
+ id="",
67
+ name=name,
68
+ data=data_dict,
69
+ )
70
+ secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
71
+ print(f"[green]Created keys secret:[/] {secret_id}")
72
+
73
+ finally:
74
+ await client.close()
75
+
76
+
77
+ @keys_app.async_command("update")
78
+ async def update_keys_secret(
79
+ *,
80
+ project_id: Optional[str] = typer.Option(None),
81
+ secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
82
+ name: Annotated[str, typer.Option(help="Secret name")],
83
+ data: Annotated[
84
+ str,
85
+ typer.Option(
86
+ "--data",
87
+ help="Format: key=value key2=value (space-separated)",
88
+ ),
89
+ ]
90
+ ):
91
+ """
92
+ Update an existing 'keys' secret (opaque env-vars).
93
+ """
94
+ client = await get_client()
95
+ try:
96
+ project_id = await resolve_project_id(project_id)
97
+ data_dict = _parse_kv_inline(data)
98
+
99
+ secret_obj = KeysSecret(
100
+ id=secret_id,
101
+ name=name,
102
+ data=data_dict,
103
+ )
104
+ await client.update_secret(project_id=project_id, secret=secret_obj)
105
+ print(f"[green]Keys secret {secret_id} updated.[/]")
106
+ finally:
107
+ await client.close()
108
+
109
+
110
+ # --------------------------------------------------------------------------
111
+ # Subcommand group: "docker"
112
+ # e.g.: meshagent secrets docker create --name myregistry --server ...
113
+ # --------------------------------------------------------------------------
114
+ docker_app = async_typer.AsyncTyper(help="Create or update a Docker registry pull secret.")
115
+
116
+ @docker_app.async_command("create")
117
+ async def create_docker_secret(
118
+ *,
119
+ project_id: Optional[str] = typer.Option(None),
120
+ name: Annotated[str, typer.Option(help="Secret name")],
121
+ server: Annotated[str, typer.Option(help="Docker registry server, e.g. index.docker.io")],
122
+ username: Annotated[str, typer.Option(help="Registry user name")],
123
+ password: Annotated[str, typer.Option(help="Registry password")],
124
+ email: Annotated[
125
+ str,
126
+ typer.Option("--email", help="User email for Docker config", show_default=False)
127
+ ] = "none@example.com"
128
+ ):
129
+ """
130
+ Create a new Docker pull secret (generic).
131
+ """
132
+ client = await get_client()
133
+ try:
134
+ project_id = await resolve_project_id(project_id)
135
+
136
+ secret_obj = PullSecret(
137
+ id="",
138
+ name=name,
139
+ server=server,
140
+ username=username,
141
+ password=password,
142
+ email=email,
143
+ )
144
+ secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
145
+ print(f"[green]Created Docker pull secret:[/] {secret_id}")
146
+ finally:
147
+ await client.close()
148
+
149
+
150
+ @docker_app.async_command("update")
151
+ async def update_docker_secret(
152
+ *,
153
+ project_id: Optional[str] = typer.Option(None),
154
+ secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
155
+ name: Annotated[str, typer.Option(help="Secret name")],
156
+ server: Annotated[str, typer.Option(help="Docker registry server")],
157
+ username: Annotated[str, typer.Option(help="Registry user name")],
158
+ password: Annotated[str, typer.Option(help="Registry password")],
159
+ email: Annotated[
160
+ str,
161
+ typer.Option("--email", help="User email for Docker config", show_default=False)
162
+ ] = "none@example.com"
163
+ ):
164
+ """
165
+ Update an existing Docker pull secret (generic).
166
+ """
167
+ client = await get_client()
168
+ try:
169
+ project_id = await resolve_project_id(project_id)
170
+ secret_obj = PullSecret(
171
+ id=secret_id,
172
+ name=name,
173
+ server=server,
174
+ username=username,
175
+ password=password,
176
+ email=email,
177
+ )
178
+ await client.update_secret(project_id=project_id, secret=secret_obj)
179
+ print(f"[green]Docker pull secret {secret_id} updated.[/]")
180
+ finally:
181
+ await client.close()
182
+
183
+
184
+ # --------------------------------------------------------------------------
185
+ # Subcommand group: "acr"
186
+ # e.g.: meshagent secrets acr create --name <NAME> --server <REG>.azurecr.io ...
187
+ # --------------------------------------------------------------------------
188
+ acr_app = async_typer.AsyncTyper(help="Create or update an Azure Container Registry pull secret.")
189
+
190
+ @acr_app.async_command("create")
191
+ async def create_acr_secret(
192
+ *,
193
+ project_id: Optional[str] = typer.Option(None),
194
+ name: Annotated[str, typer.Option(help="Secret name")],
195
+ server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
196
+ username: Annotated[str, typer.Option(help="Service principal ID")],
197
+ password: Annotated[str, typer.Option(help="Service principal secret/password")]
198
+ ):
199
+ """
200
+ Create a new ACR pull secret (defaults email to 'none@microsoft.com').
201
+ """
202
+ client = await get_client()
203
+ try:
204
+ project_id = await resolve_project_id(project_id)
205
+
206
+ secret_obj = PullSecret(
207
+ id="",
208
+ name=name,
209
+ server=server,
210
+ username=username,
211
+ password=password,
212
+ email="none@microsoft.com", # Set a default for ACR usage
213
+ )
214
+ secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
215
+ print(f"[green]Created ACR pull secret:[/] {secret_id}")
216
+ finally:
217
+ await client.close()
218
+
219
+ @acr_app.async_command("update")
220
+ async def update_acr_secret(
221
+ *,
222
+ project_id: Optional[str] = typer.Option(None),
223
+ secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
224
+ name: Annotated[str, typer.Option(help="Secret name")],
225
+ server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
226
+ username: Annotated[str, typer.Option(help="Service principal ID")],
227
+ password: Annotated[str, typer.Option(help="Service principal secret/password")]
228
+ ):
229
+ """
230
+ Update an existing ACR pull secret (defaults email to 'none@microsoft.com').
231
+ """
232
+ client = await get_client()
233
+ try:
234
+ project_id = await resolve_project_id(project_id)
235
+ secret_obj = PullSecret(
236
+ id=secret_id,
237
+ name=name,
238
+ server=server,
239
+ username=username,
240
+ password=password,
241
+ email="none@microsoft.com",
242
+ )
243
+ await client.update_secret(project_id=project_id, secret=secret_obj)
244
+ print(f"[green]ACR pull secret {secret_id} updated.[/]")
245
+ finally:
246
+ await client.close()
247
+
248
+
249
+ # --------------------------------------------------------------------------
250
+ # Subcommand group: "gar"
251
+ # e.g.: meshagent secrets gar create --name <NAME> --server ...
252
+ # (Typically sets email='none@google.com' and username='_json_key')
253
+ # --------------------------------------------------------------------------
254
+ gar_app = async_typer.AsyncTyper(help="Create or update a Google Artifact Registry pull secret.")
255
+
256
+ @gar_app.async_command("create")
257
+ async def create_gar_secret(
258
+ *,
259
+ project_id: Optional[str] = typer.Option(None),
260
+ name: Annotated[str, typer.Option(help="Secret name")],
261
+ server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
262
+ json_key: Annotated[str, typer.Option(help="Entire GCP service account JSON as string")]
263
+ ):
264
+ """
265
+ Create a new Google Artifact Registry pull secret.
266
+
267
+ By default, sets email='none@google.com', username='_json_key'
268
+ """
269
+ client = await get_client()
270
+ try:
271
+ project_id = await resolve_project_id(project_id)
272
+
273
+ secret_obj = PullSecret(
274
+ id="",
275
+ name=name,
276
+ server=server,
277
+ username="_json_key",
278
+ password=json_key,
279
+ email="none@google.com",
280
+ )
281
+ secret_id = await client.create_secret(project_id=project_id, secret=secret_obj)
282
+ print(f"[green]Created GAR pull secret:[/] {secret_id}")
283
+ finally:
284
+ await client.close()
285
+
286
+ @gar_app.async_command("update")
287
+ async def update_gar_secret(
288
+ *,
289
+ project_id: Optional[str] = typer.Option(None),
290
+ secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
291
+ name: Annotated[str, typer.Option(help="Secret name")],
292
+ server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
293
+ json_key: Annotated[str, typer.Option(help="Entire GCP service account JSON as string")]
294
+ ):
295
+ """
296
+ Update an existing Google Artifact Registry pull secret.
297
+ """
298
+ client = await get_client()
299
+ try:
300
+ project_id = await resolve_project_id(project_id)
301
+ secret_obj = PullSecret(
302
+ id=secret_id,
303
+ name=name,
304
+ server=server,
305
+ username="_json_key",
306
+ password=json_key,
307
+ email="none@google.com",
308
+ )
309
+ await client.update_secret(project_id=project_id, secret=secret_obj)
310
+ print(f"[green]GAR pull secret {secret_id} updated.[/]")
311
+ finally:
312
+ await client.close()
313
+
314
+
315
+ # --------------------------------------------------------------------------
316
+ # Additional commands (shared by all secrets): list, delete
317
+ # --------------------------------------------------------------------------
318
+
319
+ @secrets_app.async_command("list")
320
+ async def secret_list(*, project_id: Optional[str] = None):
321
+ """List all secrets in the project (typed as Docker/ACR/GAR or Keys secrets)."""
322
+ client = await get_client()
323
+ try:
324
+ project_id = await resolve_project_id(project_id)
325
+
326
+ secrets: list[SecretLike] = await client.list_secrets(project_id)
327
+
328
+ # Convert each secret → plain dict for tabular output
329
+ rows = []
330
+ for s in secrets:
331
+ row = {
332
+ "id": s.id,
333
+ "name": s.name,
334
+ "type": s.type,
335
+ }
336
+ # If it's a KeysSecret, row["data_keys"] = ...
337
+ if hasattr(s, "data"):
338
+ # For Docker-ish secrets, 'data' typically has server/username/password
339
+ if isinstance(s, PullSecret):
340
+ row["data_keys"] = "server, username, password"
341
+ else:
342
+ # KeysSecret
343
+ row["data_keys"] = ", ".join(s.data.keys())
344
+ rows.append(row)
345
+
346
+ print_json_table(rows, "id", "type", "name", "data_keys")
347
+
348
+ finally:
349
+ await client.close()
350
+
351
+
352
+ @secrets_app.async_command("delete")
353
+ async def secret_delete(
354
+ *,
355
+ project_id: Optional[str] = None,
356
+ secret_id: Annotated[str, typer.Argument(help="ID of the secret to delete")]
357
+ ):
358
+ """Delete a secret."""
359
+ client = await get_client()
360
+ try:
361
+ project_id = await resolve_project_id(project_id=project_id)
362
+ await client.delete_secret(project_id=project_id, secret_id=secret_id)
363
+ print(f"[green]Secret {secret_id} deleted.[/]")
364
+ finally:
365
+ await client.close()
366
+
367
+
368
+ # --------------------------------------------------------------------------
369
+ # Wire up sub-apps
370
+ # --------------------------------------------------------------------------
371
+ secrets_app.add_typer(keys_app, name="keys")
372
+ secrets_app.add_typer(docker_app, name="docker")
373
+ secrets_app.add_typer(acr_app, name="acr")
374
+ secrets_app.add_typer(gar_app, name="gar")
375
+
376
+ app = secrets_app
377
+
378
+
379
+ # If you want to attach `secrets_app` to your main CLI app, do something like:
380
+ # main_app = async_typer.AsyncTyper()
381
+ # main_app.add_typer(secrets_app, name="secrets")
382
+ # if __name__ == "__main__":
383
+ # main_app()
@@ -0,0 +1,76 @@
1
+ import asyncio
2
+ import json
3
+ from meshagent.cli import async_typer
4
+ import typer
5
+ from meshagent.cli.helper import get_client, resolve_project_id, resolve_api_key
6
+ from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol
7
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
8
+ from rich import print
9
+ from typing import Annotated, Optional
10
+
11
+ app = async_typer.AsyncTyper()
12
+
13
+ @app.async_command("watch")
14
+ async def watch_logs(
15
+ *,
16
+ project_id: Annotated[Optional[str], typer.Option(..., help="Project ID (if not set, will try to use the active project)")] = None,
17
+ room: Annotated[str, typer.Option(..., help="Name of the room to connect to")],
18
+ api_key_id: Annotated[Optional[str], typer.Option(..., help="API Key ID")] = None,
19
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
20
+ role: Annotated[str, typer.Option(..., help="Role to assign to this participant")] = "user"
21
+ ):
22
+ """
23
+ Watch logs from the developer feed in the specified room.
24
+ """
25
+
26
+ account_client = await get_client()
27
+ try:
28
+ # Resolve project ID (or fetch from the active project if not provided)
29
+ project_id = await resolve_project_id(project_id=project_id)
30
+ api_key_id = await resolve_api_key(project_id, api_key_id)
31
+
32
+ # Decrypt the project's API key
33
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
34
+
35
+ # Build a participant token
36
+ token = ParticipantToken(
37
+ name=name,
38
+ project_id=project_id,
39
+ api_key_id=api_key_id
40
+ )
41
+ token.add_role_grant(role=role)
42
+ token.add_room_grant(room)
43
+
44
+ print("[bold green]Connecting to room...[/bold green]")
45
+ async with RoomClient(
46
+ protocol=WebSocketClientProtocol(
47
+ url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
48
+ token=token.to_jwt(token=key)
49
+ )
50
+ ) as client:
51
+ # Create a developer client from the room client
52
+
53
+ # Define how to handle the incoming log events
54
+ def handle_log(type: str, data: dict):
55
+ # You can customize this print to suit your needs
56
+ print(f"[magenta]{type}[/magenta]: {json.dumps(data, indent=2)}")
57
+
58
+ # Attach our handler to the "log" event
59
+ client.developer.on("log", handle_log)
60
+
61
+ # Enable watching
62
+ await client.developer.enable()
63
+ print("[bold cyan]watching enabled. Press Ctrl+C to stop.[/bold cyan]")
64
+
65
+ try:
66
+ # Block forever, until Ctrl+C
67
+ while True:
68
+ await asyncio.sleep(10)
69
+ except KeyboardInterrupt:
70
+ print("[bold red]Stopping watch...[/bold red]")
71
+ finally:
72
+ # Disable watching before exiting
73
+ await client.developer.disable()
74
+
75
+ finally:
76
+ await account_client.close()
@@ -0,0 +1,113 @@
1
+ from meshagent.cli import async_typer
2
+ import typer
3
+ from meshagent.api.helpers import meshagent_base_url
4
+ from meshagent.api.accounts_client import AccountsClient
5
+
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from meshagent.cli import auth_async
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ SETTINGS_FILE = Path.home() / ".meshagent" / "project.json"
16
+
17
+ def _ensure_cache_dir():
18
+ SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True)
19
+
20
+ class Settings(BaseModel):
21
+ active_project: Optional[str] = None
22
+ active_api_keys: Optional[dict] = {}
23
+
24
+ def _save_settings(s: Settings):
25
+ _ensure_cache_dir()
26
+ SETTINGS_FILE.write_text(s.model_dump_json())
27
+
28
+ def _load_settings():
29
+ _ensure_cache_dir()
30
+ if SETTINGS_FILE.exists():
31
+ return Settings.model_validate_json(SETTINGS_FILE.read_text())
32
+
33
+ return Settings()
34
+
35
+ async def get_active_project():
36
+ settings = _load_settings()
37
+ if settings == None:
38
+ return None
39
+ return settings.active_project
40
+
41
+
42
+ async def set_active_project(project_id: str | None):
43
+ settings = _load_settings()
44
+ settings.active_project = project_id
45
+ _save_settings(settings)
46
+
47
+ async def get_active_api_key(project_id: str):
48
+ settings = _load_settings()
49
+ if settings == None:
50
+ return None
51
+ return settings.active_api_keys[project_id]
52
+
53
+
54
+ async def set_active_api_key(project_id: str, api_key_id: str | None):
55
+ settings = _load_settings()
56
+ settings.active_api_keys[project_id] = api_key_id
57
+ _save_settings(settings)
58
+
59
+
60
+ app = async_typer.AsyncTyper()
61
+
62
+ async def get_client():
63
+ access_token = await auth_async.get_access_token()
64
+ return AccountsClient(base_url=meshagent_base_url(), token=access_token)
65
+
66
+ def print_json_table(records: list, *cols):
67
+
68
+ if not records:
69
+ raise SystemExit("No rows to print")
70
+
71
+ # 2️⃣ --- build the table -------------------------------------------
72
+ table = Table(show_header=True, header_style="bold magenta")
73
+
74
+ if len(cols) > 0:
75
+ # use the keys of the first object as column order
76
+ for col in cols:
77
+ table.add_column(col.title()) # "id" → "Id"
78
+
79
+ for row in records:
80
+ table.add_row(*(str(row.get(col, "")) for col in cols))
81
+
82
+ else:
83
+ # use the keys of the first object as column order
84
+ for col in records[0]:
85
+ table.add_column(col.title()) # "id" → "Id"
86
+
87
+ for row in records:
88
+ table.add_row(*(str(row.get(col, "")) for col in records[0]))
89
+
90
+ # 3️⃣ --- render ------------------------------------------------------
91
+ Console().print(table)
92
+
93
+
94
+ async def resolve_project_id(project_id: Optional[str] = None):
95
+ if project_id == None:
96
+ project_id = await get_active_project()
97
+
98
+ if project_id == None:
99
+ print("[red]Project ID not specified, activate a project or pass a project on the command line[/red]")
100
+ raise typer.Exit(code=1)
101
+
102
+ return project_id
103
+
104
+ async def resolve_api_key(project_id: str, api_key_id: Optional[str] = None):
105
+ if api_key_id == None:
106
+ api_key_id = await get_active_api_key(project_id=project_id)
107
+
108
+ if api_key_id == None:
109
+ print("[red]API Key ID not specified, activate an api key or pass an api key id on the command line[/red]")
110
+ raise typer.Exit(code=1)
111
+
112
+ return api_key_id
113
+