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.
- meshagent/cli/agent.py +15 -11
- meshagent/cli/api_keys.py +4 -4
- meshagent/cli/async_typer.py +52 -4
- meshagent/cli/call.py +12 -8
- meshagent/cli/chatbot.py +1007 -129
- meshagent/cli/cli.py +21 -20
- meshagent/cli/cli_mcp.py +92 -28
- meshagent/cli/cli_secrets.py +10 -10
- meshagent/cli/common_options.py +19 -4
- meshagent/cli/containers.py +164 -16
- meshagent/cli/database.py +997 -0
- meshagent/cli/developer.py +3 -3
- meshagent/cli/exec.py +22 -6
- meshagent/cli/helper.py +62 -11
- meshagent/cli/helpers.py +66 -9
- meshagent/cli/host.py +37 -0
- meshagent/cli/mailbot.py +1004 -40
- meshagent/cli/mailboxes.py +223 -0
- meshagent/cli/meeting_transcriber.py +10 -4
- meshagent/cli/messaging.py +7 -7
- meshagent/cli/multi.py +402 -0
- meshagent/cli/oauth2.py +44 -21
- meshagent/cli/participant_token.py +5 -3
- meshagent/cli/port.py +70 -0
- meshagent/cli/queue.py +2 -2
- meshagent/cli/room.py +20 -212
- meshagent/cli/rooms.py +214 -0
- meshagent/cli/services.py +32 -23
- meshagent/cli/sessions.py +5 -5
- meshagent/cli/storage.py +5 -5
- meshagent/cli/task_runner.py +770 -0
- meshagent/cli/version.py +1 -1
- meshagent/cli/voicebot.py +502 -76
- meshagent/cli/webhook.py +7 -7
- meshagent/cli/worker.py +1327 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/METADATA +13 -13
- meshagent_cli-0.21.0.dist-info/RECORD +44 -0
- meshagent_cli-0.7.0.dist-info/RECORD +0 -36
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/WHEEL +0 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/entry_points.txt +0 -0
- {meshagent_cli-0.7.0.dist-info → meshagent_cli-0.21.0.dist-info}/top_level.txt +0 -0
meshagent/cli/cli.py
CHANGED
|
@@ -3,35 +3,35 @@ import asyncio
|
|
|
3
3
|
|
|
4
4
|
from meshagent.cli import async_typer
|
|
5
5
|
|
|
6
|
-
from meshagent.cli import
|
|
6
|
+
from meshagent.cli import multi
|
|
7
|
+
|
|
7
8
|
from meshagent.cli import auth
|
|
8
9
|
from meshagent.cli import api_keys
|
|
9
10
|
from meshagent.cli import projects
|
|
10
11
|
from meshagent.cli import sessions
|
|
11
12
|
from meshagent.cli import participant_token
|
|
12
|
-
from meshagent.cli import agent
|
|
13
|
-
from meshagent.cli import messaging
|
|
14
|
-
from meshagent.cli import storage
|
|
15
|
-
from meshagent.cli import developer
|
|
16
13
|
from meshagent.cli import webhook
|
|
17
14
|
from meshagent.cli import services
|
|
18
|
-
from meshagent.cli import
|
|
15
|
+
from meshagent.cli import mailboxes
|
|
16
|
+
|
|
19
17
|
from meshagent.cli import call
|
|
20
18
|
from meshagent.cli import cli_mcp
|
|
21
19
|
from meshagent.cli import chatbot
|
|
22
20
|
from meshagent.cli import voicebot
|
|
23
21
|
from meshagent.cli import mailbot
|
|
24
|
-
from meshagent.cli import
|
|
22
|
+
from meshagent.cli import worker
|
|
23
|
+
from meshagent.cli import task_runner
|
|
25
24
|
from meshagent.cli import oauth2
|
|
26
25
|
from meshagent.cli import helpers
|
|
27
26
|
from meshagent.cli import meeting_transcriber
|
|
27
|
+
from meshagent.cli import rooms
|
|
28
28
|
from meshagent.cli import room
|
|
29
|
+
from meshagent.cli import port
|
|
29
30
|
from meshagent.cli.exec import register as register_exec
|
|
30
31
|
from meshagent.cli.version import __version__
|
|
31
32
|
from meshagent.cli.helper import get_active_api_key
|
|
32
33
|
from meshagent.otel import otel_config
|
|
33
34
|
|
|
34
|
-
|
|
35
35
|
from art import tprint
|
|
36
36
|
|
|
37
37
|
import logging
|
|
@@ -47,30 +47,31 @@ otel_config(service_name="meshagent-cli")
|
|
|
47
47
|
logging.getLogger("openai").setLevel(logging.ERROR)
|
|
48
48
|
logging.getLogger("httpx").setLevel(logging.ERROR)
|
|
49
49
|
|
|
50
|
-
app = async_typer.AsyncTyper(no_args_is_help=True)
|
|
50
|
+
app = async_typer.AsyncTyper(no_args_is_help=True, name="meshagent")
|
|
51
51
|
app.add_typer(call.app, name="call")
|
|
52
52
|
app.add_typer(auth.app, name="auth")
|
|
53
53
|
app.add_typer(projects.app, name="project")
|
|
54
54
|
app.add_typer(api_keys.app, name="api-key")
|
|
55
55
|
app.add_typer(sessions.app, name="session")
|
|
56
56
|
app.add_typer(participant_token.app, name="participant-token")
|
|
57
|
-
app.add_typer(agent.app, name="agents")
|
|
58
|
-
app.add_typer(messaging.app, name="messaging")
|
|
59
|
-
app.add_typer(storage.app, name="storage")
|
|
60
|
-
app.add_typer(developer.app, name="developer")
|
|
61
57
|
app.add_typer(webhook.app, name="webhook")
|
|
62
58
|
app.add_typer(services.app, name="service")
|
|
63
|
-
app.add_typer(cli_secrets.app, name="secret")
|
|
64
|
-
app.add_typer(queue.app, name="queue")
|
|
65
59
|
app.add_typer(cli_mcp.app, name="mcp")
|
|
66
|
-
app.add_typer(chatbot.app, name="chatbot")
|
|
67
|
-
app.add_typer(voicebot.app, name="voicebot")
|
|
68
|
-
app.add_typer(mailbot.app, name="mailbot")
|
|
69
|
-
app.add_typer(containers.app, name="container")
|
|
70
60
|
app.add_typer(oauth2.app, name="oauth2")
|
|
71
61
|
app.add_typer(helpers.app, name="helpers")
|
|
72
|
-
app.add_typer(
|
|
62
|
+
app.add_typer(rooms.app, name="rooms")
|
|
63
|
+
app.add_typer(mailboxes.app, name="mailbox")
|
|
73
64
|
app.add_typer(meeting_transcriber.app, name="meeting-transcriber")
|
|
65
|
+
app.add_typer(port.app, name="port")
|
|
66
|
+
|
|
67
|
+
app.add_typer(multi.app, name="multi")
|
|
68
|
+
app.add_typer(voicebot.app, name="voicebot")
|
|
69
|
+
app.add_typer(chatbot.app, name="chatbot")
|
|
70
|
+
app.add_typer(mailbot.app, name="mailbot")
|
|
71
|
+
app.add_typer(task_runner.app, name="task-runner")
|
|
72
|
+
app.add_typer(worker.app, name="worker")
|
|
73
|
+
|
|
74
|
+
app.add_typer(room.app, name="room")
|
|
74
75
|
|
|
75
76
|
register_exec(app)
|
|
76
77
|
|
meshagent/cli/cli_mcp.py
CHANGED
|
@@ -35,23 +35,30 @@ def _kv_to_dict(pairs: List[str]) -> dict[str, str]:
|
|
|
35
35
|
return out
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
app = async_typer.AsyncTyper()
|
|
38
|
+
app = async_typer.AsyncTyper(help="Bridge MCP servers into MeshAgent rooms")
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
@app.async_command(
|
|
41
|
+
@app.async_command(
|
|
42
|
+
"sse", help="Connect an MCP server over SSE and register it as a toolkit"
|
|
43
|
+
)
|
|
42
44
|
async def sse(
|
|
43
45
|
*,
|
|
44
|
-
project_id: ProjectIdOption
|
|
46
|
+
project_id: ProjectIdOption,
|
|
45
47
|
room: RoomOption,
|
|
46
48
|
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
47
49
|
role: str = "tool",
|
|
48
|
-
url: Annotated[str, typer.Option()],
|
|
49
|
-
toolkit_name: Annotated[
|
|
50
|
+
url: Annotated[str, typer.Option(..., help="SSE URL for the MCP server")],
|
|
51
|
+
toolkit_name: Annotated[
|
|
52
|
+
Optional[str],
|
|
53
|
+
typer.Option(help="Toolkit name to register in the room (default: mcp)"),
|
|
54
|
+
] = None,
|
|
50
55
|
key: Annotated[
|
|
51
56
|
str,
|
|
52
57
|
typer.Option("--key", help="an api key to sign the token with"),
|
|
53
58
|
] = None,
|
|
54
59
|
):
|
|
60
|
+
"""Connect an MCP server over SSE and expose it as a room toolkit."""
|
|
61
|
+
|
|
55
62
|
from mcp.client.session import ClientSession
|
|
56
63
|
from mcp.client.sse import sse_client
|
|
57
64
|
|
|
@@ -116,21 +123,33 @@ async def sse(
|
|
|
116
123
|
await account_client.close()
|
|
117
124
|
|
|
118
125
|
|
|
119
|
-
@app.async_command(
|
|
126
|
+
@app.async_command(
|
|
127
|
+
"stdio", help="Run an MCP server over stdio and register it as a toolkit"
|
|
128
|
+
)
|
|
120
129
|
async def stdio(
|
|
121
130
|
*,
|
|
122
|
-
project_id: ProjectIdOption
|
|
131
|
+
project_id: ProjectIdOption,
|
|
123
132
|
room: RoomOption,
|
|
124
133
|
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
125
134
|
role: str = "tool",
|
|
126
|
-
command: Annotated[
|
|
127
|
-
|
|
135
|
+
command: Annotated[
|
|
136
|
+
str,
|
|
137
|
+
typer.Option(
|
|
138
|
+
..., help="Command to start an MCP server over stdio (quoted string)"
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
toolkit_name: Annotated[
|
|
142
|
+
Optional[str],
|
|
143
|
+
typer.Option(help="Toolkit name to register in the room (default: mcp)"),
|
|
144
|
+
] = None,
|
|
128
145
|
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
129
146
|
key: Annotated[
|
|
130
147
|
str,
|
|
131
148
|
typer.Option("--key", help="an api key to sign the token with"),
|
|
132
149
|
] = None,
|
|
133
150
|
):
|
|
151
|
+
"""Run an MCP server over stdio and expose it as a room toolkit."""
|
|
152
|
+
|
|
134
153
|
from mcp.client.session import ClientSession
|
|
135
154
|
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
136
155
|
|
|
@@ -205,16 +224,30 @@ async def stdio(
|
|
|
205
224
|
await account_client.close()
|
|
206
225
|
|
|
207
226
|
|
|
208
|
-
@app.async_command("http-proxy")
|
|
227
|
+
@app.async_command("http-proxy", help="Expose a stdio MCP server over streamable HTTP")
|
|
209
228
|
async def stdio_host(
|
|
210
229
|
*,
|
|
211
|
-
command: Annotated[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
command: Annotated[
|
|
231
|
+
str,
|
|
232
|
+
typer.Option(..., help="Command to start the MCP server (stdio transport)"),
|
|
233
|
+
],
|
|
234
|
+
host: Annotated[
|
|
235
|
+
Optional[str], typer.Option(help="Host to bind the proxy server on")
|
|
236
|
+
] = None,
|
|
237
|
+
port: Annotated[
|
|
238
|
+
Optional[int], typer.Option(help="Port to bind the proxy server on")
|
|
239
|
+
] = None,
|
|
240
|
+
path: Annotated[
|
|
241
|
+
Optional[str],
|
|
242
|
+
typer.Option(help="HTTP path to mount the proxy server at"),
|
|
243
|
+
] = None,
|
|
244
|
+
name: Annotated[
|
|
245
|
+
Optional[str], typer.Option(help="Display name for the proxy server")
|
|
246
|
+
] = None,
|
|
216
247
|
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
217
248
|
):
|
|
249
|
+
"""Expose a stdio-based MCP server over streamable HTTP."""
|
|
250
|
+
|
|
218
251
|
from fastmcp import FastMCP, Client
|
|
219
252
|
from fastmcp.client.transports import StdioTransport
|
|
220
253
|
|
|
@@ -238,16 +271,29 @@ async def stdio_host(
|
|
|
238
271
|
await proxy.run_async(transport="streamable-http", host=host, port=port, path=path)
|
|
239
272
|
|
|
240
273
|
|
|
241
|
-
@app.async_command("sse-proxy")
|
|
274
|
+
@app.async_command("sse-proxy", help="Expose a stdio MCP server over SSE")
|
|
242
275
|
async def sse_proxy(
|
|
243
276
|
*,
|
|
244
|
-
command: Annotated[
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
command: Annotated[
|
|
278
|
+
str,
|
|
279
|
+
typer.Option(..., help="Command to start the MCP server (stdio transport)"),
|
|
280
|
+
],
|
|
281
|
+
host: Annotated[
|
|
282
|
+
Optional[str], typer.Option(help="Host to bind the proxy server on")
|
|
283
|
+
] = None,
|
|
284
|
+
port: Annotated[
|
|
285
|
+
Optional[int], typer.Option(help="Port to bind the proxy server on")
|
|
286
|
+
] = None,
|
|
287
|
+
path: Annotated[
|
|
288
|
+
Optional[str], typer.Option(help="SSE path to mount the proxy at")
|
|
289
|
+
] = None,
|
|
290
|
+
name: Annotated[
|
|
291
|
+
Optional[str], typer.Option(help="Display name for the proxy server")
|
|
292
|
+
] = None,
|
|
249
293
|
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
250
294
|
):
|
|
295
|
+
"""Expose a stdio-based MCP server over SSE."""
|
|
296
|
+
|
|
251
297
|
from fastmcp import FastMCP, Client
|
|
252
298
|
from fastmcp.client.transports import StdioTransport
|
|
253
299
|
|
|
@@ -271,17 +317,35 @@ async def sse_proxy(
|
|
|
271
317
|
await proxy.run_async(transport="sse", host=host, port=port, path=path)
|
|
272
318
|
|
|
273
319
|
|
|
274
|
-
@app.async_command("stdio-service")
|
|
320
|
+
@app.async_command("stdio-service", help="Run a stdio MCP server as an HTTP service")
|
|
275
321
|
async def stdio_service(
|
|
276
322
|
*,
|
|
277
|
-
command: Annotated[
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
command: Annotated[
|
|
324
|
+
str,
|
|
325
|
+
typer.Option(
|
|
326
|
+
..., help="Command to start an MCP server over stdio (quoted string)"
|
|
327
|
+
),
|
|
328
|
+
],
|
|
329
|
+
host: Annotated[
|
|
330
|
+
Optional[str], typer.Option(help="Host to bind the service on")
|
|
331
|
+
] = None,
|
|
332
|
+
port: Annotated[
|
|
333
|
+
Optional[int], typer.Option(help="Port to bind the service on")
|
|
334
|
+
] = None,
|
|
335
|
+
webhook_secret: Annotated[
|
|
336
|
+
Optional[str],
|
|
337
|
+
typer.Option(help="Optional webhook secret for authenticating requests"),
|
|
338
|
+
] = None,
|
|
339
|
+
path: Annotated[
|
|
340
|
+
Optional[str], typer.Option(help="HTTP path to mount the service at")
|
|
341
|
+
] = None,
|
|
342
|
+
toolkit_name: Annotated[
|
|
343
|
+
Optional[str], typer.Option(help="Toolkit name to expose (default: mcp)")
|
|
344
|
+
] = None,
|
|
283
345
|
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
284
346
|
):
|
|
347
|
+
"""Run a stdio-based MCP server as an HTTP service."""
|
|
348
|
+
|
|
285
349
|
from mcp.client.session import ClientSession
|
|
286
350
|
from mcp.client.stdio import stdio_client, StdioServerParameters
|
|
287
351
|
|
meshagent/cli/cli_secrets.py
CHANGED
|
@@ -53,7 +53,7 @@ keys_app = async_typer.AsyncTyper(
|
|
|
53
53
|
@keys_app.async_command("create")
|
|
54
54
|
async def create_keys_secret(
|
|
55
55
|
*,
|
|
56
|
-
project_id: ProjectIdOption
|
|
56
|
+
project_id: ProjectIdOption,
|
|
57
57
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
58
58
|
data: Annotated[
|
|
59
59
|
str,
|
|
@@ -86,7 +86,7 @@ async def create_keys_secret(
|
|
|
86
86
|
@keys_app.async_command("update")
|
|
87
87
|
async def update_keys_secret(
|
|
88
88
|
*,
|
|
89
|
-
project_id: ProjectIdOption
|
|
89
|
+
project_id: ProjectIdOption,
|
|
90
90
|
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
91
91
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
92
92
|
data: Annotated[
|
|
@@ -128,7 +128,7 @@ docker_app = async_typer.AsyncTyper(
|
|
|
128
128
|
@docker_app.async_command("create")
|
|
129
129
|
async def create_docker_secret(
|
|
130
130
|
*,
|
|
131
|
-
project_id: ProjectIdOption
|
|
131
|
+
project_id: ProjectIdOption,
|
|
132
132
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
133
133
|
server: Annotated[
|
|
134
134
|
str, typer.Option(help="Docker registry server, e.g. index.docker.io")
|
|
@@ -166,7 +166,7 @@ async def create_docker_secret(
|
|
|
166
166
|
@docker_app.async_command("update")
|
|
167
167
|
async def update_docker_secret(
|
|
168
168
|
*,
|
|
169
|
-
project_id: ProjectIdOption
|
|
169
|
+
project_id: ProjectIdOption,
|
|
170
170
|
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
171
171
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
172
172
|
server: Annotated[str, typer.Option(help="Docker registry server")],
|
|
@@ -211,7 +211,7 @@ acr_app = async_typer.AsyncTyper(
|
|
|
211
211
|
@acr_app.async_command("create")
|
|
212
212
|
async def create_acr_secret(
|
|
213
213
|
*,
|
|
214
|
-
project_id: ProjectIdOption
|
|
214
|
+
project_id: ProjectIdOption,
|
|
215
215
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
216
216
|
server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
|
|
217
217
|
username: Annotated[str, typer.Option(help="Service principal ID")],
|
|
@@ -241,7 +241,7 @@ async def create_acr_secret(
|
|
|
241
241
|
@acr_app.async_command("update")
|
|
242
242
|
async def update_acr_secret(
|
|
243
243
|
*,
|
|
244
|
-
project_id: ProjectIdOption
|
|
244
|
+
project_id: ProjectIdOption,
|
|
245
245
|
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
246
246
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
247
247
|
server: Annotated[str, typer.Option(help="ACR server, e.g. myregistry.azurecr.io")],
|
|
@@ -281,7 +281,7 @@ gar_app = async_typer.AsyncTyper(
|
|
|
281
281
|
@gar_app.async_command("create")
|
|
282
282
|
async def create_gar_secret(
|
|
283
283
|
*,
|
|
284
|
-
project_id: ProjectIdOption
|
|
284
|
+
project_id: ProjectIdOption,
|
|
285
285
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
286
286
|
server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
|
|
287
287
|
json_key: Annotated[
|
|
@@ -314,7 +314,7 @@ async def create_gar_secret(
|
|
|
314
314
|
@gar_app.async_command("update")
|
|
315
315
|
async def update_gar_secret(
|
|
316
316
|
*,
|
|
317
|
-
project_id: ProjectIdOption
|
|
317
|
+
project_id: ProjectIdOption,
|
|
318
318
|
secret_id: Annotated[str, typer.Option(help="Existing secret ID")],
|
|
319
319
|
name: Annotated[str, typer.Option(help="Secret name")],
|
|
320
320
|
server: Annotated[str, typer.Option(help="GAR host, e.g. us-west1-docker.pkg.dev")],
|
|
@@ -348,7 +348,7 @@ async def update_gar_secret(
|
|
|
348
348
|
|
|
349
349
|
|
|
350
350
|
@secrets_app.async_command("list")
|
|
351
|
-
async def secret_list(*, project_id: ProjectIdOption
|
|
351
|
+
async def secret_list(*, project_id: ProjectIdOption):
|
|
352
352
|
"""List all secrets in the project (typed as Docker/ACR/GAR or Keys secrets)."""
|
|
353
353
|
client = await get_client()
|
|
354
354
|
try:
|
|
@@ -383,7 +383,7 @@ async def secret_list(*, project_id: ProjectIdOption = None):
|
|
|
383
383
|
@secrets_app.async_command("delete")
|
|
384
384
|
async def secret_delete(
|
|
385
385
|
*,
|
|
386
|
-
project_id: ProjectIdOption
|
|
386
|
+
project_id: ProjectIdOption,
|
|
387
387
|
secret_id: Annotated[str, typer.Argument(help="ID of the secret to delete")],
|
|
388
388
|
):
|
|
389
389
|
"""Delete a secret."""
|
meshagent/cli/common_options.py
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from typing import Annotated, Optional
|
|
3
|
+
import os
|
|
4
|
+
from meshagent.cli.helper import _load_settings
|
|
3
5
|
|
|
4
6
|
OutputFormatOption = Annotated[
|
|
5
7
|
str,
|
|
6
8
|
typer.Option("--output", "-o", help="output format [json|table]"),
|
|
7
9
|
]
|
|
8
10
|
|
|
11
|
+
if os.getenv("MESHAGENT_CLI_BUILD"):
|
|
12
|
+
default_project_id = None
|
|
13
|
+
else:
|
|
14
|
+
settings = _load_settings()
|
|
15
|
+
if settings is None:
|
|
16
|
+
default_project_id = None
|
|
17
|
+
else:
|
|
18
|
+
default_project_id = settings.active_project
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_default_project_id():
|
|
22
|
+
return os.getenv("MESHAGENT_PROJECT_ID") or default_project_id
|
|
23
|
+
|
|
24
|
+
|
|
9
25
|
ProjectIdOption = Annotated[
|
|
10
26
|
Optional[str],
|
|
11
27
|
typer.Option(
|
|
12
28
|
"--project-id",
|
|
13
29
|
help="A MeshAgent project id. If empty, the activated project will be used.",
|
|
30
|
+
default_factory=get_default_project_id(),
|
|
14
31
|
),
|
|
15
32
|
]
|
|
16
33
|
|
|
17
34
|
RoomOption = Annotated[
|
|
18
|
-
str,
|
|
35
|
+
Optional[str],
|
|
19
36
|
typer.Option(
|
|
20
|
-
"--room",
|
|
21
|
-
help="Room name",
|
|
37
|
+
"--room", help="Room name", default_factory=os.getenv("MESHAGENT_ROOM")
|
|
22
38
|
),
|
|
23
39
|
]
|
|
24
40
|
|
|
25
|
-
|
|
26
41
|
RoomCreateOption = Annotated[
|
|
27
42
|
bool,
|
|
28
43
|
typer.Option(
|
meshagent/cli/containers.py
CHANGED
|
@@ -33,6 +33,8 @@ from meshagent.api.room_server_client import (
|
|
|
33
33
|
DockerSecret,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
+
import sys
|
|
37
|
+
|
|
36
38
|
app = async_typer.AsyncTyper(help="Manage containers and images inside a room")
|
|
37
39
|
|
|
38
40
|
# -------------------------
|
|
@@ -353,7 +355,6 @@ async def _with_client(
|
|
|
353
355
|
|
|
354
356
|
connection = await account_client.connect_room(project_id=project_id, room=room)
|
|
355
357
|
|
|
356
|
-
print("[bold green]Connecting to room...[/bold green]", flush=True)
|
|
357
358
|
proto = WebSocketClientProtocol(
|
|
358
359
|
url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
|
|
359
360
|
token=connection.jwt,
|
|
@@ -374,7 +375,7 @@ async def _with_client(
|
|
|
374
375
|
@app.async_command("ps")
|
|
375
376
|
async def list_containers(
|
|
376
377
|
*,
|
|
377
|
-
project_id: ProjectIdOption
|
|
378
|
+
project_id: ProjectIdOption,
|
|
378
379
|
room: RoomOption,
|
|
379
380
|
output: Annotated[Optional[str], typer.Option(help="json | table")] = "json",
|
|
380
381
|
):
|
|
@@ -407,7 +408,7 @@ async def list_containers(
|
|
|
407
408
|
@app.async_command("stop")
|
|
408
409
|
async def stop_container(
|
|
409
410
|
*,
|
|
410
|
-
project_id: ProjectIdOption
|
|
411
|
+
project_id: ProjectIdOption,
|
|
411
412
|
room: RoomOption,
|
|
412
413
|
id: Annotated[str, typer.Option(..., help="Container ID")],
|
|
413
414
|
):
|
|
@@ -426,10 +427,12 @@ async def stop_container(
|
|
|
426
427
|
@app.async_command("logs")
|
|
427
428
|
async def container_logs(
|
|
428
429
|
*,
|
|
429
|
-
project_id: ProjectIdOption
|
|
430
|
+
project_id: ProjectIdOption,
|
|
430
431
|
room: RoomOption,
|
|
431
432
|
id: Annotated[str, typer.Option(..., help="Container ID")],
|
|
432
|
-
follow: Annotated[
|
|
433
|
+
follow: Annotated[
|
|
434
|
+
bool, typer.Option("--follow/--no-follow", help="Stream logs")
|
|
435
|
+
] = False,
|
|
433
436
|
):
|
|
434
437
|
account_client, client = await _with_client(
|
|
435
438
|
project_id=project_id,
|
|
@@ -443,18 +446,151 @@ async def container_logs(
|
|
|
443
446
|
await account_client.close()
|
|
444
447
|
|
|
445
448
|
|
|
449
|
+
@app.async_command("exec")
|
|
450
|
+
async def exec_container(
|
|
451
|
+
*,
|
|
452
|
+
project_id: ProjectIdOption,
|
|
453
|
+
room: RoomOption,
|
|
454
|
+
container_id: Annotated[str, typer.Option(..., help="container id")],
|
|
455
|
+
command: Annotated[
|
|
456
|
+
Optional[str],
|
|
457
|
+
typer.Option(..., help="Command to execute in the container (quoted string)"),
|
|
458
|
+
] = None,
|
|
459
|
+
tty: Annotated[bool, typer.Option(..., help="Allocate a TTY")] = False,
|
|
460
|
+
):
|
|
461
|
+
account_client, client = await _with_client(
|
|
462
|
+
project_id=project_id,
|
|
463
|
+
room=room,
|
|
464
|
+
)
|
|
465
|
+
result = 1
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
import termios
|
|
469
|
+
|
|
470
|
+
from contextlib import contextmanager
|
|
471
|
+
|
|
472
|
+
container = await client.containers.exec(
|
|
473
|
+
container_id=container_id,
|
|
474
|
+
command=command,
|
|
475
|
+
tty=tty,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
async def write_all(fd, data: bytes) -> None:
|
|
479
|
+
loop = asyncio.get_running_loop()
|
|
480
|
+
mv = memoryview(data)
|
|
481
|
+
|
|
482
|
+
while mv:
|
|
483
|
+
try:
|
|
484
|
+
n = os.write(fd, mv)
|
|
485
|
+
mv = mv[n:]
|
|
486
|
+
except BlockingIOError:
|
|
487
|
+
fut = loop.create_future()
|
|
488
|
+
loop.add_writer(fd, fut.set_result, None)
|
|
489
|
+
try:
|
|
490
|
+
await fut
|
|
491
|
+
finally:
|
|
492
|
+
loop.remove_writer(fd)
|
|
493
|
+
|
|
494
|
+
async def read_stderr():
|
|
495
|
+
async for output in container.stderr():
|
|
496
|
+
await write_all(sys.stderr.fileno(), output)
|
|
497
|
+
|
|
498
|
+
async def read_stdout():
|
|
499
|
+
async for output in container.stdout():
|
|
500
|
+
await write_all(sys.stdout.fileno(), output)
|
|
501
|
+
|
|
502
|
+
@contextmanager
|
|
503
|
+
def raw_mode(fd: int):
|
|
504
|
+
import tty
|
|
505
|
+
|
|
506
|
+
old = termios.tcgetattr(fd)
|
|
507
|
+
try:
|
|
508
|
+
tty.setraw(fd) # immediate bytes
|
|
509
|
+
yield
|
|
510
|
+
finally:
|
|
511
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
512
|
+
|
|
513
|
+
async def read_piped_stdin(bufsize: int = 1024):
|
|
514
|
+
while True:
|
|
515
|
+
chunk = await asyncio.to_thread(sys.stdin.buffer.read, bufsize)
|
|
516
|
+
|
|
517
|
+
if not chunk or len(chunk) == 0:
|
|
518
|
+
await container.close_stdin()
|
|
519
|
+
break
|
|
520
|
+
|
|
521
|
+
await container.write(chunk)
|
|
522
|
+
|
|
523
|
+
async def read_stdin(bufsize: int = 1024):
|
|
524
|
+
# If stdin is piped, just read normally (blocking is fine; no TTY semantics)
|
|
525
|
+
if not sys.stdin.isatty():
|
|
526
|
+
while True:
|
|
527
|
+
chunk = sys.stdin.buffer.read(bufsize)
|
|
528
|
+
if not chunk:
|
|
529
|
+
return
|
|
530
|
+
await container.write(chunk)
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
fd = sys.stdin.fileno()
|
|
534
|
+
|
|
535
|
+
# Make reads non-blocking so we never hang shutdown
|
|
536
|
+
prev_blocking = os.get_blocking(fd)
|
|
537
|
+
os.set_blocking(fd, False)
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
with raw_mode(fd):
|
|
541
|
+
while True:
|
|
542
|
+
try:
|
|
543
|
+
chunk = os.read(fd, bufsize)
|
|
544
|
+
except BlockingIOError:
|
|
545
|
+
# nothing typed yet
|
|
546
|
+
await asyncio.sleep(0.01)
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
if chunk == b"":
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
# optional: allow Ctrl-C to exit
|
|
553
|
+
if chunk == b"\x03":
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
await container.write(chunk)
|
|
557
|
+
finally:
|
|
558
|
+
os.set_blocking(fd, prev_blocking)
|
|
559
|
+
|
|
560
|
+
if not tty and not sys.stdin.isatty():
|
|
561
|
+
await asyncio.gather(read_stdout(), read_stderr(), read_piped_stdin())
|
|
562
|
+
else:
|
|
563
|
+
if not sys.stdin.isatty():
|
|
564
|
+
print("[red]TTY requested but not a TTY[/red]")
|
|
565
|
+
raise typer.Exit(-1)
|
|
566
|
+
|
|
567
|
+
reader = asyncio.create_task(read_stdin())
|
|
568
|
+
await asyncio.gather(read_stdout(), read_stderr())
|
|
569
|
+
reader.cancel()
|
|
570
|
+
|
|
571
|
+
result = await container.result
|
|
572
|
+
finally:
|
|
573
|
+
await client.__aexit__(None, None, None)
|
|
574
|
+
await account_client.close()
|
|
575
|
+
|
|
576
|
+
sys.exit(result)
|
|
577
|
+
|
|
578
|
+
|
|
446
579
|
# -------------------------
|
|
447
|
-
# Run (detached)
|
|
580
|
+
# Run (detached)
|
|
448
581
|
# -------------------------
|
|
449
582
|
|
|
450
583
|
|
|
451
584
|
@app.async_command("run")
|
|
452
585
|
async def run_container(
|
|
453
586
|
*,
|
|
454
|
-
project_id: ProjectIdOption
|
|
587
|
+
project_id: ProjectIdOption,
|
|
455
588
|
room: RoomOption,
|
|
456
589
|
image: Annotated[str, typer.Option(..., help="Image to run")],
|
|
457
|
-
command: Annotated[
|
|
590
|
+
command: Annotated[
|
|
591
|
+
Optional[str],
|
|
592
|
+
typer.Option(..., help="Command to execute in the container (quoted string)"),
|
|
593
|
+
] = None,
|
|
458
594
|
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
459
595
|
port: Annotated[
|
|
460
596
|
List[str], typer.Option("--port", "-p", help="CONTAINER:HOST")
|
|
@@ -470,11 +606,23 @@ async def run_container(
|
|
|
470
606
|
help="Docker creds (username,password) or (registry,username,password)",
|
|
471
607
|
),
|
|
472
608
|
] = [],
|
|
473
|
-
mount_path: Annotated[
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
609
|
+
mount_path: Annotated[
|
|
610
|
+
Optional[str],
|
|
611
|
+
typer.Option(help="Room storage path to mount into the container"),
|
|
612
|
+
] = None,
|
|
613
|
+
mount_subpath: Annotated[
|
|
614
|
+
Optional[str],
|
|
615
|
+
typer.Option(help="Subpath within `--mount-path` to mount"),
|
|
616
|
+
] = None,
|
|
617
|
+
participant_name: Annotated[
|
|
618
|
+
Optional[str], typer.Option(help="Participant name to associate with the run")
|
|
619
|
+
] = None,
|
|
620
|
+
role: Annotated[
|
|
621
|
+
str, typer.Option(..., help="Role to run the container as")
|
|
622
|
+
] = "user",
|
|
623
|
+
container_name: Annotated[
|
|
624
|
+
str, typer.Option(..., help="Optional container name")
|
|
625
|
+
] = None,
|
|
478
626
|
):
|
|
479
627
|
account_client, client = await _with_client(
|
|
480
628
|
project_id=project_id,
|
|
@@ -517,7 +665,7 @@ app.add_typer(images_app, name="images")
|
|
|
517
665
|
@images_app.async_command("list")
|
|
518
666
|
async def images_list(
|
|
519
667
|
*,
|
|
520
|
-
project_id: ProjectIdOption
|
|
668
|
+
project_id: ProjectIdOption,
|
|
521
669
|
room: RoomOption,
|
|
522
670
|
):
|
|
523
671
|
account_client, client = await _with_client(
|
|
@@ -535,7 +683,7 @@ async def images_list(
|
|
|
535
683
|
@images_app.async_command("delete")
|
|
536
684
|
async def images_delete(
|
|
537
685
|
*,
|
|
538
|
-
project_id: ProjectIdOption
|
|
686
|
+
project_id: ProjectIdOption,
|
|
539
687
|
room: RoomOption,
|
|
540
688
|
image: Annotated[str, typer.Option(..., help="Image ref/tag to delete")],
|
|
541
689
|
):
|
|
@@ -554,7 +702,7 @@ async def images_delete(
|
|
|
554
702
|
@images_app.async_command("pull")
|
|
555
703
|
async def images_pull(
|
|
556
704
|
*,
|
|
557
|
-
project_id: ProjectIdOption
|
|
705
|
+
project_id: ProjectIdOption,
|
|
558
706
|
room: RoomOption,
|
|
559
707
|
tag: Annotated[str, typer.Option(..., help="Image tag/ref to pull")],
|
|
560
708
|
cred: Annotated[
|