meshagent-cli 0.0.38__tar.gz → 0.1.0__tar.gz
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.
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/PKG-INFO +8 -5
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/call.py +25 -4
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli.py +1 -1
- meshagent_cli-0.1.0/meshagent/cli/services.py +584 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/tty.py +10 -4
- meshagent_cli-0.1.0/meshagent/cli/version.py +1 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/PKG-INFO +8 -5
- meshagent_cli-0.1.0/meshagent_cli.egg-info/requires.txt +14 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/pyproject.toml +8 -4
- meshagent_cli-0.0.38/meshagent/cli/services.py +0 -350
- meshagent_cli-0.0.38/meshagent/cli/version.py +0 -1
- meshagent_cli-0.0.38/meshagent_cli.egg-info/requires.txt +0 -11
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/README.md +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/__init__.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/agent.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/api_keys.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/async_typer.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/auth.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/auth_async.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/chatbot.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli_mcp.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli_secrets.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/developer.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/helper.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/messaging.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/otel.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/participant_token.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/projects.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/sessions.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/storage.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/voicebot.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/webhook.py +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/SOURCES.txt +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/dependency_links.txt +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/entry_points.txt +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/top_level.txt +0 -0
- {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-cli
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: CLI for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -10,15 +10,18 @@ Requires-Python: >=3.12
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: typer~=0.15
|
|
12
12
|
Requires-Dist: pydantic-yaml~=1.4
|
|
13
|
-
Requires-Dist: meshagent-api~=0.
|
|
14
|
-
Requires-Dist: meshagent-agents~=0.
|
|
15
|
-
Requires-Dist: meshagent-
|
|
16
|
-
Requires-Dist: meshagent-
|
|
13
|
+
Requires-Dist: meshagent-api~=0.1
|
|
14
|
+
Requires-Dist: meshagent-agents~=0.1
|
|
15
|
+
Requires-Dist: meshagent-computers~=0.1
|
|
16
|
+
Requires-Dist: meshagent-openai~=0.1
|
|
17
|
+
Requires-Dist: meshagent-tools~=0.1
|
|
18
|
+
Requires-Dist: meshagent-mcp~=0.1
|
|
17
19
|
Requires-Dist: supabase~=2.15
|
|
18
20
|
Requires-Dist: fastmcp~=2.8
|
|
19
21
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
20
22
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.33
|
|
21
23
|
Requires-Dist: art~=6.5
|
|
24
|
+
Requires-Dist: pydantic-yaml~=1.5
|
|
22
25
|
|
|
23
26
|
## MeshAgent CLI
|
|
24
27
|
|
|
@@ -72,10 +72,16 @@ async def make_call(
|
|
|
72
72
|
project_id: str = None,
|
|
73
73
|
room: Annotated[str, typer.Option()],
|
|
74
74
|
api_key_id: Annotated[Optional[str], typer.Option()] = None,
|
|
75
|
-
name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
|
|
76
75
|
role: str = "agent",
|
|
77
76
|
local: Optional[bool] = None,
|
|
78
|
-
agent_name: Annotated[
|
|
77
|
+
agent_name: Annotated[
|
|
78
|
+
Optional[str], typer.Option(..., help="deprecated and unused", hidden=True)
|
|
79
|
+
] = None,
|
|
80
|
+
name: Annotated[str, typer.Option(..., help="deprecated", hidden=True)] = None,
|
|
81
|
+
participant_name: Annotated[
|
|
82
|
+
Optional[str],
|
|
83
|
+
typer.Option(..., help="the participant name to be used by the callee"),
|
|
84
|
+
] = None,
|
|
79
85
|
url: Annotated[str, typer.Option(..., help="URL the agent should call")],
|
|
80
86
|
arguments: Annotated[
|
|
81
87
|
str, typer.Option(..., help="JSON string with arguments for the call")
|
|
@@ -83,7 +89,22 @@ async def make_call(
|
|
|
83
89
|
):
|
|
84
90
|
"""
|
|
85
91
|
Instruct an agent to 'call' a given URL with specific arguments.
|
|
92
|
+
|
|
86
93
|
"""
|
|
94
|
+
|
|
95
|
+
if name is not None:
|
|
96
|
+
print("[yellow]name is deprecated and should no longer be passed[/yellow]")
|
|
97
|
+
|
|
98
|
+
if agent_name is not None:
|
|
99
|
+
print(
|
|
100
|
+
"[yellow]agent-name is deprecated and should no longer be passed, use participant-name instead[/yellow]"
|
|
101
|
+
)
|
|
102
|
+
participant_name = agent_name
|
|
103
|
+
|
|
104
|
+
if participant_name is None:
|
|
105
|
+
print("[red]--participant-name is required[/red]")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
87
108
|
account_client = await get_client()
|
|
88
109
|
try:
|
|
89
110
|
project_id = await resolve_project_id(project_id=project_id)
|
|
@@ -96,7 +117,7 @@ async def make_call(
|
|
|
96
117
|
)["token"]
|
|
97
118
|
|
|
98
119
|
token = ParticipantToken(
|
|
99
|
-
name=
|
|
120
|
+
name="cli", project_id=project_id, api_key_id=api_key_id
|
|
100
121
|
)
|
|
101
122
|
token.add_role_grant(role=role)
|
|
102
123
|
token.add_room_grant(room)
|
|
@@ -130,7 +151,7 @@ async def make_call(
|
|
|
130
151
|
) as client:
|
|
131
152
|
print("[bold green]Making agent call...[/bold green]")
|
|
132
153
|
await client.agents.make_call(
|
|
133
|
-
name=
|
|
154
|
+
name=participant_name, url=url, arguments=json.loads(arguments)
|
|
134
155
|
)
|
|
135
156
|
print("[bold cyan]Call request sent successfully.[/bold cyan]")
|
|
136
157
|
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# Imports
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
import typer
|
|
5
|
+
from rich import print
|
|
6
|
+
from typing import Annotated, List, Optional, Dict
|
|
7
|
+
from aiohttp import ClientResponseError
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pydantic import PositiveInt
|
|
10
|
+
import pydantic
|
|
11
|
+
from typing import Literal
|
|
12
|
+
from meshagent.cli import async_typer
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from meshagent.cli.helper import (
|
|
15
|
+
get_client,
|
|
16
|
+
print_json_table,
|
|
17
|
+
resolve_project_id,
|
|
18
|
+
resolve_api_key,
|
|
19
|
+
)
|
|
20
|
+
from meshagent.api import (
|
|
21
|
+
ParticipantToken,
|
|
22
|
+
RoomClient,
|
|
23
|
+
WebSocketClientProtocol,
|
|
24
|
+
websocket_room_url,
|
|
25
|
+
meshagent_base_url,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from pydantic_yaml import parse_yaml_raw_as
|
|
29
|
+
|
|
30
|
+
# Pydantic basemodels
|
|
31
|
+
from meshagent.api.accounts_client import Service, Port, Services, Endpoint
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
app = async_typer.AsyncTyper()
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Utilities
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _kv_to_dict(pairs: List[str]) -> Dict[str, str]:
|
|
42
|
+
"""Convert ["A=1","B=2"] → {"A":"1","B":"2"}."""
|
|
43
|
+
out: Dict[str, str] = {}
|
|
44
|
+
for p in pairs:
|
|
45
|
+
if "=" not in p:
|
|
46
|
+
raise typer.BadParameter(f"'{p}' must be KEY=VALUE")
|
|
47
|
+
k, v = p.split("=", 1)
|
|
48
|
+
out[k] = v
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PortSpec(pydantic.BaseModel):
|
|
53
|
+
"""
|
|
54
|
+
CLI schema for --port.
|
|
55
|
+
Example:
|
|
56
|
+
--port num=8080 type=webserver liveness=/health path=/agent participant_name=myname
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
num: PositiveInt
|
|
60
|
+
type: Literal["mcp.sse", "meshagent.callable", "http", "tcp"]
|
|
61
|
+
liveness: str | None = None
|
|
62
|
+
participant_name: str | None = None
|
|
63
|
+
path: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_port_spec(spec: str) -> PortSpec:
|
|
67
|
+
"""
|
|
68
|
+
Convert "num=8080 type=webserver liveness=/health" → PortSpec.
|
|
69
|
+
The user should quote the whole string if it contains spaces.
|
|
70
|
+
"""
|
|
71
|
+
tokens = spec.strip().split()
|
|
72
|
+
kv: Dict[str, str] = {}
|
|
73
|
+
for t in tokens:
|
|
74
|
+
if "=" not in t:
|
|
75
|
+
raise typer.BadParameter(
|
|
76
|
+
f"expected num=PORT_NUMBER type=meshagent.callable|mcp.sse liveness=OPTIONAL_PATH, got '{t}'"
|
|
77
|
+
)
|
|
78
|
+
k, v = t.split("=", 1)
|
|
79
|
+
kv[k] = v
|
|
80
|
+
try:
|
|
81
|
+
return PortSpec(**kv)
|
|
82
|
+
except pydantic.ValidationError as exc:
|
|
83
|
+
raise typer.BadParameter(str(exc))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Commands
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.async_command("create")
|
|
92
|
+
async def service_create(
|
|
93
|
+
*,
|
|
94
|
+
project_id: str = None,
|
|
95
|
+
file: Annotated[
|
|
96
|
+
Optional[str],
|
|
97
|
+
typer.Option("--file", "-f", help="File path to a service definition"),
|
|
98
|
+
] = None,
|
|
99
|
+
name: Annotated[Optional[str], typer.Option(help="Friendly service name")] = None,
|
|
100
|
+
image: Annotated[
|
|
101
|
+
Optional[str], typer.Option(help="Container image reference")
|
|
102
|
+
] = None,
|
|
103
|
+
role: Annotated[
|
|
104
|
+
Optional[str], typer.Option(help="Service role (agent|tool)")
|
|
105
|
+
] = None,
|
|
106
|
+
pull_secret: Annotated[
|
|
107
|
+
Optional[str],
|
|
108
|
+
typer.Option("--pull-secret", help="Secret ID for registry"),
|
|
109
|
+
] = None,
|
|
110
|
+
command: Annotated[
|
|
111
|
+
Optional[str],
|
|
112
|
+
typer.Option("--command", help="Override ENTRYPOINT/CMD"),
|
|
113
|
+
] = None,
|
|
114
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
115
|
+
env_secret: Annotated[List[str], typer.Option("--env-secret")] = [],
|
|
116
|
+
runtime_secret: Annotated[List[str], typer.Option("--runtime-secret")] = [],
|
|
117
|
+
room_storage_path: Annotated[
|
|
118
|
+
Optional[str],
|
|
119
|
+
typer.Option("--mount", help="Path inside container to mount room storage"),
|
|
120
|
+
] = None,
|
|
121
|
+
room_storage_subpath: Annotated[
|
|
122
|
+
Optional[str],
|
|
123
|
+
typer.Option(
|
|
124
|
+
"--mount-subpath",
|
|
125
|
+
help="Restrict the container's mount to a subpath within the room storage",
|
|
126
|
+
),
|
|
127
|
+
] = None,
|
|
128
|
+
port: Annotated[
|
|
129
|
+
List[str],
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--port",
|
|
132
|
+
"-p",
|
|
133
|
+
help=(
|
|
134
|
+
"Repeatable. Example:\n"
|
|
135
|
+
' -p "num=8080 type=[mcp.sse | meshagent.callable | http | tcp] liveness=/health path=/agent participant_name=myname"'
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
] = [],
|
|
139
|
+
):
|
|
140
|
+
"""Create a service attached to the project."""
|
|
141
|
+
client = await get_client()
|
|
142
|
+
try:
|
|
143
|
+
project_id = await resolve_project_id(project_id)
|
|
144
|
+
|
|
145
|
+
if file is not None:
|
|
146
|
+
with open(file, "rb") as f:
|
|
147
|
+
spec = parse_yaml_raw_as(ServiceSpec, f.read())
|
|
148
|
+
if spec.id is not None:
|
|
149
|
+
print("[red]id cannot be set when creating a service[/red]")
|
|
150
|
+
raise typer.Exit(code=1)
|
|
151
|
+
|
|
152
|
+
service_obj = spec.to_service()
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
# ✅ validate / coerce port specs
|
|
156
|
+
port_specs: List[PortSpec] = [_parse_port_spec(s) for s in port]
|
|
157
|
+
|
|
158
|
+
ports_dict = {
|
|
159
|
+
ps.num: Port(
|
|
160
|
+
type=ps.type,
|
|
161
|
+
liveness_path=ps.liveness,
|
|
162
|
+
participant_name=ps.participant_name,
|
|
163
|
+
path=ps.path,
|
|
164
|
+
)
|
|
165
|
+
for ps in port_specs
|
|
166
|
+
} or None
|
|
167
|
+
|
|
168
|
+
service_obj = Service(
|
|
169
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
170
|
+
name=name,
|
|
171
|
+
role=role,
|
|
172
|
+
image=image,
|
|
173
|
+
command=command,
|
|
174
|
+
pull_secret=pull_secret,
|
|
175
|
+
room_storage_path=room_storage_path,
|
|
176
|
+
room_storage_subpath=room_storage_subpath,
|
|
177
|
+
environment=_kv_to_dict(env),
|
|
178
|
+
environment_secrets=env_secret or None,
|
|
179
|
+
runtime_secrets=_kv_to_dict(runtime_secret),
|
|
180
|
+
ports=ports_dict,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
new_id = (
|
|
185
|
+
await client.create_service(project_id=project_id, service=service_obj)
|
|
186
|
+
)["id"]
|
|
187
|
+
except ClientResponseError as exc:
|
|
188
|
+
if exc.status == 409:
|
|
189
|
+
print(f"[red]Service name already in use: {service_obj.name}[/red]")
|
|
190
|
+
raise typer.Exit(code=1)
|
|
191
|
+
raise
|
|
192
|
+
else:
|
|
193
|
+
print(f"[green]Created service:[/] {new_id}")
|
|
194
|
+
|
|
195
|
+
finally:
|
|
196
|
+
await client.close()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.async_command("update")
|
|
200
|
+
async def service_update(
|
|
201
|
+
*,
|
|
202
|
+
project_id: str = None,
|
|
203
|
+
id: Optional[str] = None,
|
|
204
|
+
file: Annotated[
|
|
205
|
+
Optional[str],
|
|
206
|
+
typer.Option("--file", "-f", help="File path to a service definition"),
|
|
207
|
+
] = None,
|
|
208
|
+
name: Annotated[Optional[str], typer.Option(help="Friendly service name")] = None,
|
|
209
|
+
image: Annotated[
|
|
210
|
+
Optional[str], typer.Option(help="Container image reference")
|
|
211
|
+
] = None,
|
|
212
|
+
role: Annotated[
|
|
213
|
+
Optional[str], typer.Option(help="Service role (agent|tool)")
|
|
214
|
+
] = None,
|
|
215
|
+
pull_secret: Annotated[
|
|
216
|
+
Optional[str],
|
|
217
|
+
typer.Option("--pull-secret", help="Secret ID for registry"),
|
|
218
|
+
] = None,
|
|
219
|
+
command: Annotated[
|
|
220
|
+
Optional[str],
|
|
221
|
+
typer.Option("--command", help="Override ENTRYPOINT/CMD"),
|
|
222
|
+
] = None,
|
|
223
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
224
|
+
env_secret: Annotated[List[str], typer.Option("--env-secret")] = [],
|
|
225
|
+
runtime_secret: Annotated[List[str], typer.Option("--runtime-secret")] = [],
|
|
226
|
+
room_storage_path: Annotated[
|
|
227
|
+
Optional[str],
|
|
228
|
+
typer.Option("--mount", help="Path inside container to mount room storage"),
|
|
229
|
+
] = None,
|
|
230
|
+
room_storage_subpath: Annotated[
|
|
231
|
+
Optional[str],
|
|
232
|
+
typer.Option(
|
|
233
|
+
"--mount-subpath",
|
|
234
|
+
help="Restrict the container's mount to a subpath within the room storage",
|
|
235
|
+
),
|
|
236
|
+
] = None,
|
|
237
|
+
port: Annotated[
|
|
238
|
+
List[str],
|
|
239
|
+
typer.Option(
|
|
240
|
+
"--port",
|
|
241
|
+
"-p",
|
|
242
|
+
help=(
|
|
243
|
+
"Repeatable. Example:\n"
|
|
244
|
+
' -p "num=8080 type=[mcp.sse | meshagent.callable | http | tcp] liveness=/health path=/agent participant_name=myname"'
|
|
245
|
+
),
|
|
246
|
+
),
|
|
247
|
+
] = [],
|
|
248
|
+
create: Annotated[
|
|
249
|
+
Optional[bool],
|
|
250
|
+
typer.Option(
|
|
251
|
+
help="create the service if it does not exist",
|
|
252
|
+
),
|
|
253
|
+
] = False,
|
|
254
|
+
):
|
|
255
|
+
"""Create a service attached to the project."""
|
|
256
|
+
client = await get_client()
|
|
257
|
+
try:
|
|
258
|
+
project_id = await resolve_project_id(project_id)
|
|
259
|
+
|
|
260
|
+
if file is not None:
|
|
261
|
+
with open(file, "rb") as f:
|
|
262
|
+
spec = parse_yaml_raw_as(ServiceSpec, f.read())
|
|
263
|
+
if spec.id is not None:
|
|
264
|
+
id = spec.id
|
|
265
|
+
service_obj = spec.to_service()
|
|
266
|
+
|
|
267
|
+
else:
|
|
268
|
+
# ✅ validate / coerce port specs
|
|
269
|
+
port_specs: List[PortSpec] = [_parse_port_spec(s) for s in port]
|
|
270
|
+
|
|
271
|
+
ports_dict = {
|
|
272
|
+
ps.num: Port(
|
|
273
|
+
type=ps.type,
|
|
274
|
+
liveness_path=ps.liveness,
|
|
275
|
+
participant_name=ps.participant_name,
|
|
276
|
+
path=ps.path,
|
|
277
|
+
)
|
|
278
|
+
for ps in port_specs
|
|
279
|
+
} or None
|
|
280
|
+
|
|
281
|
+
service_obj = Service(
|
|
282
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
283
|
+
name=name,
|
|
284
|
+
role=role,
|
|
285
|
+
image=image,
|
|
286
|
+
command=command,
|
|
287
|
+
pull_secret=pull_secret,
|
|
288
|
+
room_storage_path=room_storage_path,
|
|
289
|
+
room_storage_subpath=room_storage_subpath,
|
|
290
|
+
environment=_kv_to_dict(env),
|
|
291
|
+
environment_secrets=env_secret or None,
|
|
292
|
+
runtime_secrets=_kv_to_dict(runtime_secret),
|
|
293
|
+
ports=ports_dict,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
if id is None:
|
|
298
|
+
services = await client.list_services(project_id=project_id)
|
|
299
|
+
for s in services:
|
|
300
|
+
if s.name == service_obj.name:
|
|
301
|
+
id = s.id
|
|
302
|
+
|
|
303
|
+
if id is None and not create:
|
|
304
|
+
print("[red]pass a service id or specify --create[/red]")
|
|
305
|
+
raise typer.Exit(code=1)
|
|
306
|
+
|
|
307
|
+
if id is None:
|
|
308
|
+
id = (
|
|
309
|
+
await client.create_service(
|
|
310
|
+
project_id=project_id, service=service_obj
|
|
311
|
+
)
|
|
312
|
+
)["id"]
|
|
313
|
+
|
|
314
|
+
else:
|
|
315
|
+
await client.update_service(
|
|
316
|
+
project_id=project_id, service_id=id, service=service_obj
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
except ClientResponseError as exc:
|
|
320
|
+
if exc.status == 409:
|
|
321
|
+
print(f"[red]Service name already in use: {service_obj.name}[/red]")
|
|
322
|
+
raise typer.Exit(code=1)
|
|
323
|
+
raise
|
|
324
|
+
else:
|
|
325
|
+
print(f"[green]Updated service:[/] {id}")
|
|
326
|
+
|
|
327
|
+
finally:
|
|
328
|
+
await client.close()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class ServicePortEndpointSpec(pydantic.BaseModel):
|
|
332
|
+
path: str
|
|
333
|
+
identity: str
|
|
334
|
+
type: Optional[Literal["mcp.sse", "meshagent.callable", "http", "tcp"]] = None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class ServicePortSpec(pydantic.BaseModel):
|
|
338
|
+
num: PositiveInt
|
|
339
|
+
type: Literal["mcp.sse", "meshagent.callable", "http", "tcp"]
|
|
340
|
+
endpoints: list[ServicePortEndpointSpec] = []
|
|
341
|
+
liveness: Optional[str] = None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ServiceSpec(BaseModel):
|
|
345
|
+
version: Literal["v1"]
|
|
346
|
+
kind: Literal["Service"]
|
|
347
|
+
id: Optional[str] = None
|
|
348
|
+
name: str
|
|
349
|
+
command: Optional[str] = None
|
|
350
|
+
image: str
|
|
351
|
+
ports: Optional[list[ServicePortSpec]] = []
|
|
352
|
+
role: Optional[Literal["user", "tool", "agent"]] = None
|
|
353
|
+
environment: Optional[dict[str, str]] = {}
|
|
354
|
+
secrets: list[str] = []
|
|
355
|
+
pull_secret: Optional[str] = None
|
|
356
|
+
room_storage_path: Optional[str] = None
|
|
357
|
+
room_storage_subpath: Optional[str] = None
|
|
358
|
+
|
|
359
|
+
def to_service(self):
|
|
360
|
+
ports = {}
|
|
361
|
+
for p in self.ports:
|
|
362
|
+
port = Port(liveness_path=p.liveness, type=p.type, endpoints=[])
|
|
363
|
+
for endpoint in p.endpoints:
|
|
364
|
+
type = port.type
|
|
365
|
+
if endpoint.type is not None:
|
|
366
|
+
type = endpoint.type
|
|
367
|
+
|
|
368
|
+
port.endpoints.append(
|
|
369
|
+
Endpoint(
|
|
370
|
+
type=type,
|
|
371
|
+
participant_name=endpoint.identity,
|
|
372
|
+
path=endpoint.path,
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
ports[p.num] = port
|
|
376
|
+
return Service(
|
|
377
|
+
id="",
|
|
378
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
379
|
+
name=self.name,
|
|
380
|
+
command=self.command,
|
|
381
|
+
image=self.image,
|
|
382
|
+
ports=ports,
|
|
383
|
+
role=self.role,
|
|
384
|
+
environment=self.environment,
|
|
385
|
+
environment_secrets=self.secrets,
|
|
386
|
+
pull_secret=self.pull_secret,
|
|
387
|
+
room_storage_path=self.room_storage_path,
|
|
388
|
+
room_storage_subpath=self.room_storage_subpath,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@app.async_command("test")
|
|
393
|
+
async def service_test(
|
|
394
|
+
*,
|
|
395
|
+
project_id: str = None,
|
|
396
|
+
api_key_id: Annotated[Optional[str], typer.Option()] = None,
|
|
397
|
+
file: Annotated[
|
|
398
|
+
Optional[str],
|
|
399
|
+
typer.Option("--file", "-f", help="File path to a service definition"),
|
|
400
|
+
],
|
|
401
|
+
room: Annotated[
|
|
402
|
+
Optional[str],
|
|
403
|
+
typer.Option(
|
|
404
|
+
help="A room name to test the service in (must not be currently running)"
|
|
405
|
+
),
|
|
406
|
+
] = None,
|
|
407
|
+
name: Annotated[Optional[str], typer.Option(help="Friendly service name")] = None,
|
|
408
|
+
role: Annotated[
|
|
409
|
+
Optional[str], typer.Option(help="Service role (agent|tool)")
|
|
410
|
+
] = None,
|
|
411
|
+
image: Annotated[
|
|
412
|
+
Optional[str], typer.Option(help="Container image reference")
|
|
413
|
+
] = None,
|
|
414
|
+
pull_secret: Annotated[
|
|
415
|
+
Optional[str],
|
|
416
|
+
typer.Option("--pull-secret", help="Secret ID for registry"),
|
|
417
|
+
] = None,
|
|
418
|
+
command: Annotated[
|
|
419
|
+
Optional[str],
|
|
420
|
+
typer.Option("--command", help="Override ENTRYPOINT/CMD"),
|
|
421
|
+
] = None,
|
|
422
|
+
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
423
|
+
env_secret: Annotated[List[str], typer.Option("--env-secret")] = [],
|
|
424
|
+
runtime_secret: Annotated[List[str], typer.Option("--runtime-secret")] = [],
|
|
425
|
+
room_storage_path: Annotated[
|
|
426
|
+
Optional[str],
|
|
427
|
+
typer.Option("--mount", help="Path inside container to mount room storage"),
|
|
428
|
+
] = None,
|
|
429
|
+
port: Annotated[
|
|
430
|
+
List[str],
|
|
431
|
+
typer.Option(
|
|
432
|
+
"--port",
|
|
433
|
+
"-p",
|
|
434
|
+
help=(
|
|
435
|
+
"Repeatable. Example:\n"
|
|
436
|
+
' -p "num=8080 type=[mcp.sse | meshagent.callable | http | tcp] liveness=/health path=/agent participant_name=myname"'
|
|
437
|
+
),
|
|
438
|
+
),
|
|
439
|
+
] = [],
|
|
440
|
+
timeout: Annotated[
|
|
441
|
+
Optional[int],
|
|
442
|
+
typer.Option(
|
|
443
|
+
"--timeout", help="The maximum time that this room should run (default 1hr)"
|
|
444
|
+
),
|
|
445
|
+
] = None,
|
|
446
|
+
):
|
|
447
|
+
"""Create a service attached to the project."""
|
|
448
|
+
my_client = await get_client()
|
|
449
|
+
try:
|
|
450
|
+
project_id = await resolve_project_id(project_id)
|
|
451
|
+
|
|
452
|
+
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
453
|
+
|
|
454
|
+
if file is not None:
|
|
455
|
+
with open(file, "rb") as f:
|
|
456
|
+
service_obj = parse_yaml_raw_as(ServiceSpec, f.read()).to_service()
|
|
457
|
+
|
|
458
|
+
else:
|
|
459
|
+
# ✅ validate / coerce port specs
|
|
460
|
+
port_specs: List[PortSpec] = [_parse_port_spec(s) for s in port]
|
|
461
|
+
|
|
462
|
+
ports_dict = {
|
|
463
|
+
str(ps.num): Port(
|
|
464
|
+
type=ps.type,
|
|
465
|
+
liveness_path=ps.liveness,
|
|
466
|
+
participant_name=ps.participant_name,
|
|
467
|
+
path=ps.path,
|
|
468
|
+
)
|
|
469
|
+
for ps in port_specs
|
|
470
|
+
} or None
|
|
471
|
+
|
|
472
|
+
service_obj = Service(
|
|
473
|
+
created_at=datetime.now(timezone.utc).isoformat(),
|
|
474
|
+
role=role,
|
|
475
|
+
name=name,
|
|
476
|
+
image=image,
|
|
477
|
+
command=command,
|
|
478
|
+
pull_secret=pull_secret,
|
|
479
|
+
room_storage_path=room_storage_path,
|
|
480
|
+
environment=_kv_to_dict(env),
|
|
481
|
+
environment_secrets=env_secret or None,
|
|
482
|
+
runtime_secrets=_kv_to_dict(runtime_secret),
|
|
483
|
+
ports=ports_dict,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
token = ParticipantToken(
|
|
488
|
+
name=name, project_id=project_id, api_key_id=api_key_id
|
|
489
|
+
)
|
|
490
|
+
token.add_role_grant("user")
|
|
491
|
+
token.add_room_grant(room)
|
|
492
|
+
token.extra_payload = {
|
|
493
|
+
"max_runtime_seconds": timeout, # run for 1 hr max
|
|
494
|
+
"meshagent_dev_services": [service_obj.model_dump(mode="json")],
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
print("[bold green]Connecting to room...[/bold green]")
|
|
498
|
+
|
|
499
|
+
key = (
|
|
500
|
+
await my_client.decrypt_project_api_key(
|
|
501
|
+
project_id=project_id, id=api_key_id
|
|
502
|
+
)
|
|
503
|
+
)["token"]
|
|
504
|
+
|
|
505
|
+
async with RoomClient(
|
|
506
|
+
protocol=WebSocketClientProtocol(
|
|
507
|
+
url=websocket_room_url(
|
|
508
|
+
room_name=room, base_url=meshagent_base_url()
|
|
509
|
+
),
|
|
510
|
+
token=token.to_jwt(token=key),
|
|
511
|
+
)
|
|
512
|
+
) as client:
|
|
513
|
+
print(
|
|
514
|
+
f"[green]Your test room '{client.room_name}' has been started. It will time out after a few minutes if you do not join it.[/green]"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
except ClientResponseError as exc:
|
|
518
|
+
if exc.status == 409:
|
|
519
|
+
print(f"[red]Room already in use: {room}[/red]")
|
|
520
|
+
raise typer.Exit(code=1)
|
|
521
|
+
raise
|
|
522
|
+
|
|
523
|
+
finally:
|
|
524
|
+
await my_client.close()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@app.async_command("show")
|
|
528
|
+
async def service_show(
|
|
529
|
+
*,
|
|
530
|
+
project_id: str = None,
|
|
531
|
+
service_id: Annotated[str, typer.Argument(help="ID of the service to delete")],
|
|
532
|
+
):
|
|
533
|
+
"""Show a services for the project."""
|
|
534
|
+
client = await get_client()
|
|
535
|
+
try:
|
|
536
|
+
project_id = await resolve_project_id(project_id)
|
|
537
|
+
service = await client.get_service(
|
|
538
|
+
project_id=project_id, service_id=service_id
|
|
539
|
+
) # → List[Service]
|
|
540
|
+
print(service.model_dump(mode="json"))
|
|
541
|
+
finally:
|
|
542
|
+
await client.close()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@app.async_command("list")
|
|
546
|
+
async def service_list(
|
|
547
|
+
*,
|
|
548
|
+
project_id: str = None,
|
|
549
|
+
o: Annotated[
|
|
550
|
+
str, typer.Option("--output", "-o", help="output format [json|table]")
|
|
551
|
+
] = "table",
|
|
552
|
+
):
|
|
553
|
+
"""List all services for the project."""
|
|
554
|
+
client = await get_client()
|
|
555
|
+
try:
|
|
556
|
+
project_id = await resolve_project_id(project_id)
|
|
557
|
+
services: list[Service] = await client.list_services(
|
|
558
|
+
project_id=project_id
|
|
559
|
+
) # → List[Service]
|
|
560
|
+
|
|
561
|
+
if o == "json":
|
|
562
|
+
print(Services(services=services).model_dump_json(indent=2))
|
|
563
|
+
else:
|
|
564
|
+
print_json_table(
|
|
565
|
+
[svc.model_dump(mode="json") for svc in services], "id", "name", "image"
|
|
566
|
+
)
|
|
567
|
+
finally:
|
|
568
|
+
await client.close()
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
@app.async_command("delete")
|
|
572
|
+
async def service_delete(
|
|
573
|
+
*,
|
|
574
|
+
project_id: Optional[str] = None,
|
|
575
|
+
service_id: Annotated[str, typer.Argument(help="ID of the service to delete")],
|
|
576
|
+
):
|
|
577
|
+
"""Delete a service."""
|
|
578
|
+
client = await get_client()
|
|
579
|
+
try:
|
|
580
|
+
project_id = await resolve_project_id(project_id)
|
|
581
|
+
await client.delete_service(project_id=project_id, service_id=service_id)
|
|
582
|
+
print(f"[green]Service {service_id} deleted.[/]")
|
|
583
|
+
finally:
|
|
584
|
+
await client.close()
|
|
@@ -3,6 +3,7 @@ import tty
|
|
|
3
3
|
import termios
|
|
4
4
|
from meshagent.api.helpers import websocket_room_url
|
|
5
5
|
from typing import Annotated, Optional
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
import asyncio
|
|
8
9
|
import typer
|
|
@@ -53,13 +54,18 @@ async def tty_command(
|
|
|
53
54
|
|
|
54
55
|
# Save current terminal settings so we can restore them later.
|
|
55
56
|
old_tty_settings = termios.tcgetattr(sys.stdin)
|
|
57
|
+
|
|
56
58
|
try:
|
|
57
59
|
async with aiohttp.ClientSession() as session:
|
|
58
60
|
async with session.ws_connect(ws_url) as websocket:
|
|
59
|
-
print(f"[bold green]Connected to[/bold green] {room}")
|
|
60
|
-
|
|
61
61
|
tty.setraw(sys.stdin)
|
|
62
62
|
|
|
63
|
+
loop = asyncio.get_running_loop()
|
|
64
|
+
transport, protocol = await loop.connect_write_pipe(
|
|
65
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
|
66
|
+
)
|
|
67
|
+
writer = asyncio.StreamWriter(transport, protocol, None, loop)
|
|
68
|
+
|
|
63
69
|
async def recv_from_websocket():
|
|
64
70
|
async for message in websocket:
|
|
65
71
|
if message.type == aiohttp.WSMsgType.CLOSE:
|
|
@@ -69,8 +75,8 @@ async def tty_command(
|
|
|
69
75
|
await websocket.close()
|
|
70
76
|
|
|
71
77
|
data: bytes = message.data
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
writer.write(data)
|
|
79
|
+
await writer.drain()
|
|
74
80
|
|
|
75
81
|
async def send_to_websocket():
|
|
76
82
|
loop = asyncio.get_running_loop()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meshagent-cli
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: CLI for Meshagent
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Documentation, https://docs.meshagent.com
|
|
@@ -10,15 +10,18 @@ Requires-Python: >=3.12
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
Requires-Dist: typer~=0.15
|
|
12
12
|
Requires-Dist: pydantic-yaml~=1.4
|
|
13
|
-
Requires-Dist: meshagent-api~=0.
|
|
14
|
-
Requires-Dist: meshagent-agents~=0.
|
|
15
|
-
Requires-Dist: meshagent-
|
|
16
|
-
Requires-Dist: meshagent-
|
|
13
|
+
Requires-Dist: meshagent-api~=0.1
|
|
14
|
+
Requires-Dist: meshagent-agents~=0.1
|
|
15
|
+
Requires-Dist: meshagent-computers~=0.1
|
|
16
|
+
Requires-Dist: meshagent-openai~=0.1
|
|
17
|
+
Requires-Dist: meshagent-tools~=0.1
|
|
18
|
+
Requires-Dist: meshagent-mcp~=0.1
|
|
17
19
|
Requires-Dist: supabase~=2.15
|
|
18
20
|
Requires-Dist: fastmcp~=2.8
|
|
19
21
|
Requires-Dist: opentelemetry-distro~=0.54b1
|
|
20
22
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http~=1.33
|
|
21
23
|
Requires-Dist: art~=6.5
|
|
24
|
+
Requires-Dist: pydantic-yaml~=1.5
|
|
22
25
|
|
|
23
26
|
## MeshAgent CLI
|
|
24
27
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
typer~=0.15
|
|
2
|
+
pydantic-yaml~=1.4
|
|
3
|
+
meshagent-api~=0.1
|
|
4
|
+
meshagent-agents~=0.1
|
|
5
|
+
meshagent-computers~=0.1
|
|
6
|
+
meshagent-openai~=0.1
|
|
7
|
+
meshagent-tools~=0.1
|
|
8
|
+
meshagent-mcp~=0.1
|
|
9
|
+
supabase~=2.15
|
|
10
|
+
fastmcp~=2.8
|
|
11
|
+
opentelemetry-distro~=0.54b1
|
|
12
|
+
opentelemetry-exporter-otlp-proto-http~=1.33
|
|
13
|
+
art~=6.5
|
|
14
|
+
pydantic-yaml~=1.5
|
|
@@ -11,16 +11,20 @@ keywords = []
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"typer~=0.15",
|
|
13
13
|
"pydantic-yaml~=1.4",
|
|
14
|
-
"meshagent-api~=0.
|
|
15
|
-
"meshagent-agents~=0.
|
|
16
|
-
"meshagent-
|
|
17
|
-
"meshagent-
|
|
14
|
+
"meshagent-api~=0.1",
|
|
15
|
+
"meshagent-agents~=0.1",
|
|
16
|
+
"meshagent-computers~=0.1",
|
|
17
|
+
"meshagent-openai~=0.1",
|
|
18
|
+
"meshagent-tools~=0.1",
|
|
19
|
+
"meshagent-mcp~=0.1",
|
|
18
20
|
"supabase~=2.15",
|
|
19
21
|
"fastmcp~=2.8",
|
|
20
22
|
"opentelemetry-distro~=0.54b1",
|
|
21
23
|
"opentelemetry-exporter-otlp-proto-http~=1.33",
|
|
22
24
|
"art~=6.5",
|
|
25
|
+
"pydantic-yaml~=1.5"
|
|
23
26
|
]
|
|
27
|
+
|
|
24
28
|
dynamic = ["version", "readme"]
|
|
25
29
|
|
|
26
30
|
[project.scripts]
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
# ---------------------------------------------------------------------------
|
|
2
|
-
# Imports
|
|
3
|
-
# ---------------------------------------------------------------------------
|
|
4
|
-
import typer
|
|
5
|
-
from rich import print
|
|
6
|
-
from typing import Annotated, List, Optional, Dict
|
|
7
|
-
from aiohttp import ClientResponseError
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from pydantic import PositiveInt
|
|
10
|
-
import pydantic
|
|
11
|
-
from typing import Literal
|
|
12
|
-
from meshagent.cli import async_typer
|
|
13
|
-
from meshagent.cli.helper import (
|
|
14
|
-
get_client,
|
|
15
|
-
print_json_table,
|
|
16
|
-
resolve_project_id,
|
|
17
|
-
resolve_api_key,
|
|
18
|
-
)
|
|
19
|
-
from meshagent.api import (
|
|
20
|
-
ParticipantToken,
|
|
21
|
-
RoomClient,
|
|
22
|
-
WebSocketClientProtocol,
|
|
23
|
-
websocket_room_url,
|
|
24
|
-
meshagent_base_url,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Pydantic basemodels
|
|
28
|
-
from meshagent.api.accounts_client import Service, Port, Services
|
|
29
|
-
|
|
30
|
-
app = async_typer.AsyncTyper()
|
|
31
|
-
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
# Utilities
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _kv_to_dict(pairs: List[str]) -> Dict[str, str]:
|
|
38
|
-
"""Convert ["A=1","B=2"] → {"A":"1","B":"2"}."""
|
|
39
|
-
out: Dict[str, str] = {}
|
|
40
|
-
for p in pairs:
|
|
41
|
-
if "=" not in p:
|
|
42
|
-
raise typer.BadParameter(f"'{p}' must be KEY=VALUE")
|
|
43
|
-
k, v = p.split("=", 1)
|
|
44
|
-
out[k] = v
|
|
45
|
-
return out
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class PortSpec(pydantic.BaseModel):
|
|
49
|
-
"""
|
|
50
|
-
CLI schema for --port.
|
|
51
|
-
Example:
|
|
52
|
-
--port num=8080 type=webserver liveness=/health path=/agent participant_name=myname
|
|
53
|
-
"""
|
|
54
|
-
|
|
55
|
-
num: PositiveInt
|
|
56
|
-
type: Literal["mcp.sse", "meshagent.callable", "http", "tcp"]
|
|
57
|
-
liveness: str | None = None
|
|
58
|
-
participant_name: str | None = None
|
|
59
|
-
path: str | None = None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _parse_port_spec(spec: str) -> PortSpec:
|
|
63
|
-
"""
|
|
64
|
-
Convert "num=8080 type=webserver liveness=/health" → PortSpec.
|
|
65
|
-
The user should quote the whole string if it contains spaces.
|
|
66
|
-
"""
|
|
67
|
-
tokens = spec.strip().split()
|
|
68
|
-
kv: Dict[str, str] = {}
|
|
69
|
-
for t in tokens:
|
|
70
|
-
if "=" not in t:
|
|
71
|
-
raise typer.BadParameter(
|
|
72
|
-
f"expected num=PORT_NUMBER type=meshagent.callable|mcp.sse liveness=OPTIONAL_PATH, got '{t}'"
|
|
73
|
-
)
|
|
74
|
-
k, v = t.split("=", 1)
|
|
75
|
-
kv[k] = v
|
|
76
|
-
try:
|
|
77
|
-
return PortSpec(**kv)
|
|
78
|
-
except pydantic.ValidationError as exc:
|
|
79
|
-
raise typer.BadParameter(str(exc))
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# ---------------------------------------------------------------------------
|
|
83
|
-
# Commands
|
|
84
|
-
# ---------------------------------------------------------------------------
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@app.async_command("create")
|
|
88
|
-
async def service_create(
|
|
89
|
-
*,
|
|
90
|
-
project_id: str = None,
|
|
91
|
-
name: Annotated[str, typer.Option(help="Friendly service name")],
|
|
92
|
-
image: Annotated[str, typer.Option(help="Container image reference")],
|
|
93
|
-
role: Annotated[str, typer.Option(help="Service role (agent|tool)")] = None,
|
|
94
|
-
pull_secret: Annotated[
|
|
95
|
-
Optional[str],
|
|
96
|
-
typer.Option("--pull-secret", help="Secret ID for registry"),
|
|
97
|
-
] = None,
|
|
98
|
-
command: Annotated[
|
|
99
|
-
Optional[str],
|
|
100
|
-
typer.Option("--command", help="Override ENTRYPOINT/CMD"),
|
|
101
|
-
] = None,
|
|
102
|
-
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
103
|
-
env_secret: Annotated[List[str], typer.Option("--env-secret")] = [],
|
|
104
|
-
runtime_secret: Annotated[List[str], typer.Option("--runtime-secret")] = [],
|
|
105
|
-
room_storage_path: Annotated[
|
|
106
|
-
Optional[str],
|
|
107
|
-
typer.Option("--mount", help="Path inside container to mount room storage"),
|
|
108
|
-
] = None,
|
|
109
|
-
port: Annotated[
|
|
110
|
-
List[str],
|
|
111
|
-
typer.Option(
|
|
112
|
-
"--port",
|
|
113
|
-
"-p",
|
|
114
|
-
help=(
|
|
115
|
-
"Repeatable. Example:\n"
|
|
116
|
-
' -p "num=8080 type=[mcp.sse | meshagent.callable | http | tcp] liveness=/health path=/agent participant_name=myname"'
|
|
117
|
-
),
|
|
118
|
-
),
|
|
119
|
-
] = ...,
|
|
120
|
-
):
|
|
121
|
-
"""Create a service attached to the project."""
|
|
122
|
-
client = await get_client()
|
|
123
|
-
try:
|
|
124
|
-
project_id = await resolve_project_id(project_id)
|
|
125
|
-
|
|
126
|
-
# ✅ validate / coerce port specs
|
|
127
|
-
port_specs: List[PortSpec] = [_parse_port_spec(s) for s in port]
|
|
128
|
-
|
|
129
|
-
ports_dict = {
|
|
130
|
-
ps.num: Port(
|
|
131
|
-
type=ps.type,
|
|
132
|
-
liveness_path=ps.liveness,
|
|
133
|
-
participant_name=ps.participant_name,
|
|
134
|
-
path=ps.path,
|
|
135
|
-
)
|
|
136
|
-
for ps in port_specs
|
|
137
|
-
} or None
|
|
138
|
-
|
|
139
|
-
service_obj = Service(
|
|
140
|
-
id="",
|
|
141
|
-
created_at=datetime.now(timezone.utc).isoformat(),
|
|
142
|
-
name=name,
|
|
143
|
-
role=role,
|
|
144
|
-
image=image,
|
|
145
|
-
command=command,
|
|
146
|
-
pull_secret=pull_secret,
|
|
147
|
-
room_storage_path=room_storage_path,
|
|
148
|
-
environment=_kv_to_dict(env),
|
|
149
|
-
environment_secrets=env_secret or None,
|
|
150
|
-
runtime_secrets=_kv_to_dict(runtime_secret),
|
|
151
|
-
ports=ports_dict,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
new_id = (
|
|
156
|
-
await client.create_service(project_id=project_id, service=service_obj)
|
|
157
|
-
)["id"]
|
|
158
|
-
except ClientResponseError as exc:
|
|
159
|
-
if exc.status == 409:
|
|
160
|
-
print(f"[red]Service name already in use: {name}[/red]")
|
|
161
|
-
raise typer.Exit(code=1)
|
|
162
|
-
raise
|
|
163
|
-
else:
|
|
164
|
-
print(f"[green]Created service:[/] {new_id}")
|
|
165
|
-
|
|
166
|
-
finally:
|
|
167
|
-
await client.close()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@app.async_command("test")
|
|
171
|
-
async def service_test(
|
|
172
|
-
*,
|
|
173
|
-
project_id: str = None,
|
|
174
|
-
api_key_id: Annotated[Optional[str], typer.Option()] = None,
|
|
175
|
-
room: Annotated[
|
|
176
|
-
str,
|
|
177
|
-
typer.Option(
|
|
178
|
-
help="A room name to test the service in (must not be currently running)"
|
|
179
|
-
),
|
|
180
|
-
],
|
|
181
|
-
name: Annotated[str, typer.Option(help="Friendly service name")],
|
|
182
|
-
role: Annotated[str, typer.Option(help="Service role (agent|tool)")] = None,
|
|
183
|
-
image: Annotated[str, typer.Option(help="Container image reference")],
|
|
184
|
-
pull_secret: Annotated[
|
|
185
|
-
Optional[str],
|
|
186
|
-
typer.Option("--pull-secret", help="Secret ID for registry"),
|
|
187
|
-
] = None,
|
|
188
|
-
command: Annotated[
|
|
189
|
-
Optional[str],
|
|
190
|
-
typer.Option("--command", help="Override ENTRYPOINT/CMD"),
|
|
191
|
-
] = None,
|
|
192
|
-
env: Annotated[List[str], typer.Option("--env", "-e", help="KEY=VALUE")] = [],
|
|
193
|
-
env_secret: Annotated[List[str], typer.Option("--env-secret")] = [],
|
|
194
|
-
runtime_secret: Annotated[List[str], typer.Option("--runtime-secret")] = [],
|
|
195
|
-
room_storage_path: Annotated[
|
|
196
|
-
Optional[str],
|
|
197
|
-
typer.Option("--mount", help="Path inside container to mount room storage"),
|
|
198
|
-
] = None,
|
|
199
|
-
port: Annotated[
|
|
200
|
-
List[str],
|
|
201
|
-
typer.Option(
|
|
202
|
-
"--port",
|
|
203
|
-
"-p",
|
|
204
|
-
help=(
|
|
205
|
-
"Repeatable. Example:\n"
|
|
206
|
-
' -p "num=8080 type=[mcp.sse | meshagent.callable | http | tcp] liveness=/health path=/agent participant_name=myname"'
|
|
207
|
-
),
|
|
208
|
-
),
|
|
209
|
-
] = ...,
|
|
210
|
-
timeout: Annotated[
|
|
211
|
-
Optional[int],
|
|
212
|
-
typer.Option(
|
|
213
|
-
"--timeout", help="The maximum time that this room should run (default 1hr)"
|
|
214
|
-
),
|
|
215
|
-
] = None,
|
|
216
|
-
):
|
|
217
|
-
"""Create a service attached to the project."""
|
|
218
|
-
my_client = await get_client()
|
|
219
|
-
try:
|
|
220
|
-
project_id = await resolve_project_id(project_id)
|
|
221
|
-
|
|
222
|
-
api_key_id = await resolve_api_key(project_id, api_key_id)
|
|
223
|
-
|
|
224
|
-
# ✅ validate / coerce port specs
|
|
225
|
-
port_specs: List[PortSpec] = [_parse_port_spec(s) for s in port]
|
|
226
|
-
|
|
227
|
-
ports_dict = {
|
|
228
|
-
ps.num: Port(
|
|
229
|
-
type=ps.type,
|
|
230
|
-
liveness_path=ps.liveness,
|
|
231
|
-
participant_name=ps.participant_name,
|
|
232
|
-
path=ps.path,
|
|
233
|
-
)
|
|
234
|
-
for ps in port_specs
|
|
235
|
-
} or None
|
|
236
|
-
|
|
237
|
-
service_obj = Service(
|
|
238
|
-
id="",
|
|
239
|
-
created_at=datetime.now(timezone.utc).isoformat(),
|
|
240
|
-
role=role,
|
|
241
|
-
name=name,
|
|
242
|
-
image=image,
|
|
243
|
-
command=command,
|
|
244
|
-
pull_secret=pull_secret,
|
|
245
|
-
room_storage_path=room_storage_path,
|
|
246
|
-
environment=_kv_to_dict(env),
|
|
247
|
-
environment_secrets=env_secret or None,
|
|
248
|
-
runtime_secrets=_kv_to_dict(runtime_secret),
|
|
249
|
-
ports=ports_dict,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
token = ParticipantToken(
|
|
254
|
-
name=name, project_id=project_id, api_key_id=api_key_id
|
|
255
|
-
)
|
|
256
|
-
token.add_role_grant("user")
|
|
257
|
-
token.add_room_grant(room)
|
|
258
|
-
token.extra_payload = {
|
|
259
|
-
"max_runtime_seconds": timeout, # run for 1 hr max
|
|
260
|
-
"meshagent_dev_services": [service_obj.model_dump(mode="json")],
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
print("[bold green]Connecting to room...[/bold green]")
|
|
264
|
-
|
|
265
|
-
key = (
|
|
266
|
-
await my_client.decrypt_project_api_key(
|
|
267
|
-
project_id=project_id, id=api_key_id
|
|
268
|
-
)
|
|
269
|
-
)["token"]
|
|
270
|
-
|
|
271
|
-
async with RoomClient(
|
|
272
|
-
protocol=WebSocketClientProtocol(
|
|
273
|
-
url=websocket_room_url(
|
|
274
|
-
room_name=room, base_url=meshagent_base_url()
|
|
275
|
-
),
|
|
276
|
-
token=token.to_jwt(token=key),
|
|
277
|
-
)
|
|
278
|
-
) as client:
|
|
279
|
-
print(
|
|
280
|
-
f"[green]Your test room '{client.room_name}' has been started. It will time out after a few minutes if you do not join it.[/green]"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
except ClientResponseError as exc:
|
|
284
|
-
if exc.status == 409:
|
|
285
|
-
print(f"[red]Room already in use: {room}[/red]")
|
|
286
|
-
raise typer.Exit(code=1)
|
|
287
|
-
raise
|
|
288
|
-
|
|
289
|
-
finally:
|
|
290
|
-
await my_client.close()
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
@app.async_command("show")
|
|
294
|
-
async def service_show(
|
|
295
|
-
*,
|
|
296
|
-
project_id: str = None,
|
|
297
|
-
service_id: Annotated[str, typer.Argument(help="ID of the service to delete")],
|
|
298
|
-
):
|
|
299
|
-
"""Show a services for the project."""
|
|
300
|
-
client = await get_client()
|
|
301
|
-
try:
|
|
302
|
-
project_id = await resolve_project_id(project_id)
|
|
303
|
-
service = await client.get_service(
|
|
304
|
-
project_id=project_id, service_id=service_id
|
|
305
|
-
) # → List[Service]
|
|
306
|
-
print(service.model_dump(mode="json"))
|
|
307
|
-
finally:
|
|
308
|
-
await client.close()
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
@app.async_command("list")
|
|
312
|
-
async def service_list(
|
|
313
|
-
*,
|
|
314
|
-
project_id: str = None,
|
|
315
|
-
o: Annotated[
|
|
316
|
-
str, typer.Option("--output", "-o", help="output format [json|table]")
|
|
317
|
-
] = "table",
|
|
318
|
-
):
|
|
319
|
-
"""List all services for the project."""
|
|
320
|
-
client = await get_client()
|
|
321
|
-
try:
|
|
322
|
-
project_id = await resolve_project_id(project_id)
|
|
323
|
-
services: list[Service] = await client.list_services(
|
|
324
|
-
project_id=project_id
|
|
325
|
-
) # → List[Service]
|
|
326
|
-
|
|
327
|
-
if o == "json":
|
|
328
|
-
print(Services(services=services).model_dump_json(indent=2))
|
|
329
|
-
else:
|
|
330
|
-
print_json_table(
|
|
331
|
-
[svc.model_dump(mode="json") for svc in services], "id", "name", "image"
|
|
332
|
-
)
|
|
333
|
-
finally:
|
|
334
|
-
await client.close()
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
@app.async_command("delete")
|
|
338
|
-
async def service_delete(
|
|
339
|
-
*,
|
|
340
|
-
project_id: Optional[str] = None,
|
|
341
|
-
service_id: Annotated[str, typer.Argument(help="ID of the service to delete")],
|
|
342
|
-
):
|
|
343
|
-
"""Delete a service."""
|
|
344
|
-
client = await get_client()
|
|
345
|
-
try:
|
|
346
|
-
project_id = await resolve_project_id(project_id)
|
|
347
|
-
await client.delete_service(project_id=project_id, service_id=service_id)
|
|
348
|
-
print(f"[green]Service {service_id} deleted.[/]")
|
|
349
|
-
finally:
|
|
350
|
-
await client.close()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.38"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|