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.

Files changed (37) hide show
  1. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/PKG-INFO +8 -5
  2. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/call.py +25 -4
  3. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli.py +1 -1
  4. meshagent_cli-0.1.0/meshagent/cli/services.py +584 -0
  5. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/tty.py +10 -4
  6. meshagent_cli-0.1.0/meshagent/cli/version.py +1 -0
  7. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/PKG-INFO +8 -5
  8. meshagent_cli-0.1.0/meshagent_cli.egg-info/requires.txt +14 -0
  9. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/pyproject.toml +8 -4
  10. meshagent_cli-0.0.38/meshagent/cli/services.py +0 -350
  11. meshagent_cli-0.0.38/meshagent/cli/version.py +0 -1
  12. meshagent_cli-0.0.38/meshagent_cli.egg-info/requires.txt +0 -11
  13. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/README.md +0 -0
  14. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/__init__.py +0 -0
  15. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/agent.py +0 -0
  16. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/api_keys.py +0 -0
  17. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/async_typer.py +0 -0
  18. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/auth.py +0 -0
  19. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/auth_async.py +0 -0
  20. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/chatbot.py +0 -0
  21. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli_mcp.py +0 -0
  22. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/cli_secrets.py +0 -0
  23. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/developer.py +0 -0
  24. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/helper.py +0 -0
  25. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/messaging.py +0 -0
  26. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/otel.py +0 -0
  27. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/participant_token.py +0 -0
  28. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/projects.py +0 -0
  29. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/sessions.py +0 -0
  30. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/storage.py +0 -0
  31. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/voicebot.py +0 -0
  32. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent/cli/webhook.py +0 -0
  33. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/SOURCES.txt +0 -0
  34. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/dependency_links.txt +0 -0
  35. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/entry_points.txt +0 -0
  36. {meshagent_cli-0.0.38 → meshagent_cli-0.1.0}/meshagent_cli.egg-info/top_level.txt +0 -0
  37. {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.38
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.0.38
14
- Requires-Dist: meshagent-agents~=0.0.38
15
- Requires-Dist: meshagent-tools~=0.0.38
16
- Requires-Dist: meshagent-mcp~=0.0.38
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[str, typer.Option(..., help="Name of the agent to call")],
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=name, project_id=project_id, api_key_id=api_key_id
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=agent_name, url=url, arguments=json.loads(arguments)
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
 
@@ -165,7 +165,7 @@ def env(
165
165
 
166
166
  vars = {
167
167
  "MESHAGENT_PROJECT_ID": project_id,
168
- "MESHAGENT_API_KEY": api_key_id,
168
+ "MESHAGENT_KEY_ID": api_key_id,
169
169
  "MESHAGENT_SECRET": token,
170
170
  }
171
171
  if shell not in SHELL_RENDERERS:
@@ -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
- sys.stdout.write(data.decode("utf-8"))
73
- sys.stdout.flush()
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.38
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.0.38
14
- Requires-Dist: meshagent-agents~=0.0.38
15
- Requires-Dist: meshagent-tools~=0.0.38
16
- Requires-Dist: meshagent-mcp~=0.0.38
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.0.38",
15
- "meshagent-agents~=0.0.38",
16
- "meshagent-tools~=0.0.38",
17
- "meshagent-mcp~=0.0.38",
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"
@@ -1,11 +0,0 @@
1
- typer~=0.15
2
- pydantic-yaml~=1.4
3
- meshagent-api~=0.0.38
4
- meshagent-agents~=0.0.38
5
- meshagent-tools~=0.0.38
6
- meshagent-mcp~=0.0.38
7
- supabase~=2.15
8
- fastmcp~=2.8
9
- opentelemetry-distro~=0.54b1
10
- opentelemetry-exporter-otlp-proto-http~=1.33
11
- art~=6.5
File without changes
File without changes