meshagent-cli 0.0.17__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 (30) hide show
  1. meshagent_cli-0.0.17/PKG-INFO +23 -0
  2. meshagent_cli-0.0.17/README.md +7 -0
  3. meshagent_cli-0.0.17/meshagent/cli/__init__.py +0 -0
  4. meshagent_cli-0.0.17/meshagent/cli/agent.py +259 -0
  5. meshagent_cli-0.0.17/meshagent/cli/api_keys.py +74 -0
  6. meshagent_cli-0.0.17/meshagent/cli/async_typer.py +31 -0
  7. meshagent_cli-0.0.17/meshagent/cli/auth.py +28 -0
  8. meshagent_cli-0.0.17/meshagent/cli/auth_async.py +115 -0
  9. meshagent_cli-0.0.17/meshagent/cli/call.py +127 -0
  10. meshagent_cli-0.0.17/meshagent/cli/cli.py +35 -0
  11. meshagent_cli-0.0.17/meshagent/cli/cli_mcp.py +113 -0
  12. meshagent_cli-0.0.17/meshagent/cli/cli_secrets.py +383 -0
  13. meshagent_cli-0.0.17/meshagent/cli/developer.py +76 -0
  14. meshagent_cli-0.0.17/meshagent/cli/helper.py +113 -0
  15. meshagent_cli-0.0.17/meshagent/cli/messaging.py +192 -0
  16. meshagent_cli-0.0.17/meshagent/cli/participant_token.py +33 -0
  17. meshagent_cli-0.0.17/meshagent/cli/projects.py +31 -0
  18. meshagent_cli-0.0.17/meshagent/cli/services.py +177 -0
  19. meshagent_cli-0.0.17/meshagent/cli/sessions.py +19 -0
  20. meshagent_cli-0.0.17/meshagent/cli/storage.py +801 -0
  21. meshagent_cli-0.0.17/meshagent/cli/version.py +1 -0
  22. meshagent_cli-0.0.17/meshagent/cli/webhook.py +89 -0
  23. meshagent_cli-0.0.17/meshagent_cli.egg-info/PKG-INFO +23 -0
  24. meshagent_cli-0.0.17/meshagent_cli.egg-info/SOURCES.txt +28 -0
  25. meshagent_cli-0.0.17/meshagent_cli.egg-info/dependency_links.txt +1 -0
  26. meshagent_cli-0.0.17/meshagent_cli.egg-info/entry_points.txt +2 -0
  27. meshagent_cli-0.0.17/meshagent_cli.egg-info/requires.txt +5 -0
  28. meshagent_cli-0.0.17/meshagent_cli.egg-info/top_level.txt +1 -0
  29. meshagent_cli-0.0.17/pyproject.toml +36 -0
  30. meshagent_cli-0.0.17/setup.cfg +4 -0
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: meshagent-cli
3
+ Version: 0.0.17
4
+ Summary: CLI for Meshagent
5
+ License-Expression: LicenseRef-Proprietary
6
+ Project-URL: Documentation, https://meshagent.com
7
+ Project-URL: Website, https://meshagent.com
8
+ Project-URL: Source, https://github.com/meshagent
9
+ Requires-Python: >=3.9.0
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: typer~=0.15.3
12
+ Requires-Dist: pydantic-yaml~=1.4.0
13
+ Requires-Dist: meshagent-api~=0.0.17
14
+ Requires-Dist: meshagent-agents~=0.0.17
15
+ Requires-Dist: meshagent-tools~=0.0.17
16
+
17
+ ### Meshagent CLI
18
+
19
+
20
+
21
+
22
+
23
+
@@ -0,0 +1,7 @@
1
+ ### Meshagent CLI
2
+
3
+
4
+
5
+
6
+
7
+
File without changes
@@ -0,0 +1,259 @@
1
+ from meshagent.cli import async_typer
2
+ import typer
3
+ from meshagent.cli.helper import get_client, print_json_table, set_active_project, resolve_project_id
4
+ from rich import print
5
+ from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol, RoomException
6
+ from meshagent.cli.helper import set_active_project, get_active_project, resolve_project_id, resolve_api_key
7
+ from typing import Annotated, Optional
8
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
9
+ import json
10
+ import asyncio
11
+ import aiohttp
12
+ from meshagent.api.services import send_webhook
13
+
14
+ app = async_typer.AsyncTyper()
15
+
16
+ @app.async_command("ask")
17
+ async def ask(*, project_id: str = None, room: Annotated[str, typer.Option()], api_key_id: Annotated[Optional[str], typer.Option()] = None, name: Annotated[str, typer.Option(..., help="Participant name")] = "cli", role: str = "user", agent: Annotated[str, typer.Option()], input: Annotated[str, typer.Option()], timeout: Annotated[Optional[int], typer.Option(..., help="How long to wait for the agent if the agent is not in the room")] = 30):
18
+ account_client = await get_client()
19
+ try:
20
+ project_id = await resolve_project_id(project_id=project_id)
21
+ api_key_id = await resolve_api_key(project_id, api_key_id)
22
+
23
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
24
+
25
+ token = ParticipantToken(
26
+ name=name,
27
+ project_id=project_id,
28
+ api_key_id=api_key_id
29
+ )
30
+
31
+ token.add_role_grant(role=role)
32
+ token.add_room_grant(room)
33
+
34
+ print("[bold green]Connecting to room...[/bold green]")
35
+ async with RoomClient(protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()), token=token.to_jwt(token=key))) as client:
36
+
37
+ found = timeout == 0
38
+ for i in range(30):
39
+ if found:
40
+ break
41
+
42
+ if i == 1:
43
+ print("[magenta]Waiting for agent...[/magenta]")
44
+
45
+ agents = await client.agents.list_agents()
46
+ await asyncio.sleep(1)
47
+
48
+ for a in agents:
49
+ if a.name == agent:
50
+ found = True
51
+ break
52
+
53
+ if not found:
54
+ print("[red]Timed out waiting for agent to join the room[/red]")
55
+ raise typer.Exit(1)
56
+
57
+
58
+ print("[magenta]Asking agent...[/magenta]")
59
+
60
+ response = await client.agents.ask(agent=agent, arguments=json.loads(input))
61
+ print(json.dumps(response.json))
62
+ except RoomException as e:
63
+ print(f"[red]{e}[/red]")
64
+ finally:
65
+ await account_client.close()
66
+
67
+
68
+
69
+ @app.async_command("invoke-tool")
70
+ async def invoke_tool(
71
+ *,
72
+ project_id: str = None,
73
+ room: Annotated[str, typer.Option()],
74
+ api_key_id: Annotated[Optional[str], typer.Option()] = None,
75
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
76
+ role: str = "user",
77
+ toolkit: Annotated[str, typer.Option(..., help="Toolkit name")],
78
+ tool: Annotated[str, typer.Option(..., help="Tool name")],
79
+ arguments: Annotated[str, typer.Option(..., help="JSON string with arguments for the tool")],
80
+ participant_id: Annotated[Optional[str], typer.Option(..., help="Optional participant ID to invoke the tool on")] = None,
81
+ on_behalf_of_id: Annotated[Optional[str], typer.Option(..., help="Optional 'on_behalf_of' participant ID")] = None,
82
+ caller_context: Annotated[Optional[str], typer.Option(..., help="Optional JSON for caller context")] = None,
83
+ timeout: Annotated[Optional[int], typer.Option(..., help="How long to wait for the toolkit if the toolkit is not in the room")] = 30,
84
+
85
+ ):
86
+ """
87
+ Invoke a specific tool from a given toolkit with arguments.
88
+ """
89
+ account_client = await get_client()
90
+ try:
91
+ project_id = await resolve_project_id(project_id=project_id)
92
+ api_key_id = await resolve_api_key(project_id, api_key_id)
93
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
94
+
95
+ token = ParticipantToken(
96
+ name=name,
97
+ project_id=project_id,
98
+ api_key_id=api_key_id
99
+ )
100
+ token.add_role_grant(role=role)
101
+ token.add_room_grant(room)
102
+
103
+ print("[bold green]Connecting to room...[/bold green]")
104
+ async with RoomClient(
105
+ protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
106
+ token=token.to_jwt(token=key))
107
+ ) as client:
108
+
109
+ found = timeout == 0
110
+ for i in range(timeout):
111
+ if found:
112
+ break
113
+
114
+ if i == 1:
115
+ print("[magenta]Waiting for toolkit...[/magenta]")
116
+
117
+ agents = await client.agents.list_toolkits(participant_id=participant_id)
118
+ await asyncio.sleep(1)
119
+
120
+ for a in agents:
121
+ if a.name == toolkit:
122
+ found = True
123
+ break
124
+
125
+ if not found:
126
+ print("[red]Timed out waiting for toolkit to join the room[/red]")
127
+ raise typer.Exit(1)
128
+
129
+
130
+ print("[bold green]Invoking tool...[/bold green]")
131
+ parsed_context = json.loads(caller_context) if caller_context else None
132
+ response = await client.agents.invoke_tool(
133
+ toolkit=toolkit,
134
+ tool=tool,
135
+ arguments=json.loads(arguments),
136
+ participant_id=participant_id,
137
+ on_behalf_of_id=on_behalf_of_id,
138
+ caller_context=parsed_context,
139
+ )
140
+ # The response is presumably a dictionary or similar
141
+ print(response.to_json())
142
+ except RoomException as e:
143
+ print(f"[red]{e}[/red]")
144
+ finally:
145
+ await account_client.close()
146
+
147
+
148
+ @app.async_command("list-agents")
149
+ async def list_agents_command(
150
+ *,
151
+ project_id: str = None,
152
+ room: Annotated[str, typer.Option()],
153
+ api_key_id: Annotated[Optional[str], typer.Option()] = None,
154
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
155
+ role: str = "user"
156
+ ):
157
+ """
158
+ List all agents available in the room.
159
+ """
160
+ account_client = await get_client()
161
+ try:
162
+ project_id = await resolve_project_id(project_id=project_id)
163
+ api_key_id = await resolve_api_key(project_id, api_key_id)
164
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
165
+
166
+ token = ParticipantToken(
167
+ name=name,
168
+ project_id=project_id,
169
+ api_key_id=api_key_id
170
+ )
171
+ token.add_role_grant(role=role)
172
+ token.add_room_grant(room)
173
+
174
+ print("[bold green]Connecting to room...[/bold green]")
175
+ async with RoomClient(
176
+ protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
177
+ token=token.to_jwt(token=key))
178
+ ) as client:
179
+ print("[bold green]Fetching list of agents...[/bold green]")
180
+ agents = await client.agents.list_agents()
181
+ # Format the output as JSON
182
+ output = []
183
+ for agent in agents:
184
+ output.append({
185
+ "name": agent.name,
186
+ "title": agent.title,
187
+ "description": agent.description,
188
+ "requires": [r.to_json() for r in agent.requires],
189
+ "supports_tools": agent.supports_tools,
190
+ "labels": agent.labels,
191
+ })
192
+ print(json.dumps(output, indent=2))
193
+
194
+ finally:
195
+ await account_client.close()
196
+
197
+
198
+ @app.async_command("list-toolkits")
199
+ async def list_toolkits_command(
200
+ *,
201
+ project_id: str = None,
202
+ room: Annotated[str, typer.Option()],
203
+ api_key_id: Annotated[Optional[str], typer.Option()] = None,
204
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
205
+ role: str = "user",
206
+ participant_id: Annotated[Optional[str], typer.Option(..., help="Optional participant ID")] = None
207
+ ):
208
+ """
209
+ List all toolkits (and tools within them) available in the room.
210
+ """
211
+ account_client = await get_client()
212
+ try:
213
+ project_id = await resolve_project_id(project_id=project_id)
214
+ api_key_id = await resolve_api_key(project_id, api_key_id)
215
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
216
+
217
+ token = ParticipantToken(
218
+ name=name,
219
+ project_id=project_id,
220
+ api_key_id=api_key_id
221
+ )
222
+ token.add_role_grant(role=role)
223
+ token.add_room_grant(room)
224
+
225
+ print("[bold green]Connecting to room...[/bold green]")
226
+ async with RoomClient(
227
+ protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
228
+ token=token.to_jwt(token=key))
229
+ ) as client:
230
+ print("[bold green]Fetching list of toolkits...[/bold green]")
231
+ toolkits = await client.agents.list_toolkits(participant_id=participant_id)
232
+
233
+ # Format and output as JSON
234
+ output = []
235
+ for tk in toolkits:
236
+ output.append({
237
+ "name": tk.name,
238
+ "title": tk.title,
239
+ "description": tk.description,
240
+ "thumbnail_url": tk.thumbnail_url,
241
+ "tools": [
242
+ {
243
+ "name": tool.name,
244
+ "title": tool.title,
245
+ "description": tool.description,
246
+ "input_schema": tool.input_schema,
247
+ "thumbnail_url": tool.thumbnail_url,
248
+ "defs": tool.defs,
249
+ "supports_context": tool.supports_context
250
+ }
251
+ for tool in tk.tools
252
+ ]
253
+ })
254
+ print(json.dumps(output, indent=2))
255
+
256
+ finally:
257
+ await account_client.close()
258
+
259
+
@@ -0,0 +1,74 @@
1
+ from meshagent.cli import async_typer
2
+ import typer
3
+ from meshagent.cli.helper import get_client, print_json_table
4
+ from rich import print
5
+ from typing import Optional
6
+ from meshagent.cli.helper import set_active_project, get_active_project, resolve_project_id, set_active_api_key, resolve_api_key
7
+
8
+ app = async_typer.AsyncTyper()
9
+
10
+
11
+ @app.async_command("list")
12
+ async def list(*, project_id: str = None):
13
+
14
+ project_id = await resolve_project_id(project_id=project_id)
15
+
16
+ client = await get_client()
17
+ keys = (await client.list_project_api_keys(project_id=project_id))["keys"]
18
+ if len(keys) > 0:
19
+ print_json_table(keys, "id", "name", "description")
20
+ else:
21
+ print("There are not currently any API keys in the project")
22
+ await client.close()
23
+
24
+ @app.async_command("create")
25
+ async def create(*, project_id: str = None, name: str, description: str = ""):
26
+
27
+ project_id = await resolve_project_id(project_id=project_id)
28
+
29
+ client = await get_client()
30
+ api_key = await client.create_project_api_key(project_id=project_id, name=name, description=description)
31
+ print(api_key["token"])
32
+ await client.close()
33
+
34
+
35
+ @app.async_command("delete")
36
+ async def delete(*, project_id: str = None, id: str):
37
+
38
+ project_id = await resolve_project_id(project_id=project_id)
39
+
40
+ client = await get_client()
41
+ await client.delete_project_api_key(project_id=project_id, id=id)
42
+ await client.close()
43
+
44
+
45
+ @app.async_command("show")
46
+ async def show(*, project_id: str = None, api_key_id: str):
47
+ client = await get_client()
48
+ try:
49
+ project_id = await resolve_project_id(project_id=project_id)
50
+
51
+ key = await client.decrypt_project_api_key(project_id=project_id, id=api_key_id)
52
+
53
+ print(key["token"])
54
+
55
+ finally:
56
+ await client.close()
57
+
58
+
59
+ @app.async_command("activate")
60
+ async def activate(api_key_id: str, project_id: str = None,):
61
+ client = await get_client()
62
+ try:
63
+ project_id = await resolve_project_id(project_id)
64
+ response = await client.list_project_api_keys(project_id=project_id)
65
+ api_keys = response["keys"]
66
+ for api_key in api_keys:
67
+ if api_key["id"] == api_key_id:
68
+ await set_active_api_key(project_id=project_id, api_key_id=api_key_id)
69
+ return
70
+
71
+ print(f"[red]Invalid api key id or project id: {project_id}[/red]")
72
+ raise typer.Exit(code=1)
73
+ finally:
74
+ await client.close()
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ from functools import partial, wraps
6
+ from typing import Any, Callable
7
+
8
+ from typer import Typer
9
+
10
+
11
+ class AsyncTyper(Typer):
12
+ @staticmethod
13
+ def maybe_run_async(decorator: Callable, func: Callable) -> Any:
14
+ if inspect.iscoroutinefunction(func):
15
+
16
+ @wraps(func)
17
+ def runner(*args: Any, **kwargs: Any) -> Any:
18
+ return asyncio.run(func(*args, **kwargs))
19
+
20
+ decorator(runner)
21
+ else:
22
+ decorator(func)
23
+ return func
24
+
25
+ def callback(self, *args: Any, **kwargs: Any) -> Any:
26
+ decorator = super().callback(*args, **kwargs)
27
+ return partial(self.maybe_run_async, decorator)
28
+
29
+ def async_command(self, *args: Any, **kwargs: Any) -> Any:
30
+ decorator = super().command(*args, **kwargs)
31
+ return partial(self.maybe_run_async, decorator)
@@ -0,0 +1,28 @@
1
+ import typer
2
+ from meshagent.api.accounts_client import AccountsClient
3
+
4
+ import os
5
+ from .helper import set_active_project, get_active_project
6
+
7
+ from meshagent.cli import async_typer
8
+ from meshagent.cli import auth_async
9
+
10
+ app = async_typer.AsyncTyper()
11
+
12
+
13
+ @app.async_command("login")
14
+ async def login():
15
+ await auth_async.login()
16
+
17
+ project_id = await get_active_project()
18
+ if project_id == None:
19
+ print('You have been logged in, but you haven''t activated a project yet, list your projects with "meshagent project list" and then activate one with "meshagent project activate PROJECT_ID"')
20
+
21
+ @app.async_command("logout")
22
+ async def login():
23
+ await auth_async.logout()
24
+
25
+ @app.async_command("whoami")
26
+ async def whoami():
27
+ _, s = await auth_async.session()
28
+ typer.echo(s.user.email if s else "Not logged in")
@@ -0,0 +1,115 @@
1
+ import os, json, webbrowser, asyncio
2
+ from pathlib import Path
3
+ from aiohttp import web
4
+ from supabase._async.client import AsyncClient, create_client # async flavour :contentReference[oaicite:1]{index=1}
5
+ from supabase.lib.client_options import ClientOptions
6
+ from gotrue import AsyncMemoryStorage
7
+
8
+ AUTH_URL = os.getenv("MESHAGENT_AUTH_URL", "https://infra.meshagent.com")
9
+ AUTH_ANON_KEY = os.getenv("MESHAGENT_AUTH_ANON_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZqZGh5bWhhZ3BwZ2drYWJ6bGFmIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzYzMTYyOTQsImV4cCI6MjA1MTg5MjI5NH0.8AuzzNcCuEaHQ-gjqHxBmsN1YrtM-TpL1_W-kxzooNs")
10
+ CACHE_FILE = Path.home() / ".meshagent" / "session.json"
11
+ REDIRECT_PORT = 8765
12
+ REDIRECT_URL = f"http://localhost:{REDIRECT_PORT}/callback"
13
+
14
+ # ---------- helpers ----------------------------------------------------------
15
+
16
+ def _ensure_cache_dir():
17
+ CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
18
+
19
+ async def _client() -> AsyncClient:
20
+ return await create_client(
21
+ AUTH_URL,
22
+ AUTH_ANON_KEY,
23
+ options=ClientOptions(
24
+ flow_type="pkce", # OAuth + PKCE :contentReference[oaicite:2]{index=2}
25
+ auto_refresh_token=True,
26
+ persist_session=False,
27
+ storage=AsyncMemoryStorage()
28
+ ),
29
+ )
30
+
31
+ def _save(s):
32
+ _ensure_cache_dir()
33
+ CACHE_FILE.write_text(json.dumps({
34
+ "access_token": s.access_token,
35
+ "refresh_token": s.refresh_token,
36
+ "expires_at": s.expires_at, # int (seconds since epoch)
37
+ }))
38
+
39
+
40
+ def _load():
41
+ _ensure_cache_dir()
42
+ if CACHE_FILE.exists():
43
+ return json.loads(CACHE_FILE.read_text())
44
+
45
+ # ---------- local HTTP callback ---------------------------------------------
46
+
47
+ async def _wait_for_code() -> str:
48
+ """Spin up a one-shot aiohttp server and await ?code=…"""
49
+ app = web.Application()
50
+ code_fut: asyncio.Future[str] = asyncio.get_event_loop().create_future()
51
+
52
+ async def callback(request):
53
+ code = request.query.get("code")
54
+ if code:
55
+ if not code_fut.done():
56
+ code_fut.set_result(code)
57
+ return web.Response(text="You may close this tab.")
58
+ return web.Response(status=400)
59
+
60
+ app.add_routes([web.get("/callback", callback)])
61
+ runner = web.AppRunner(app)
62
+ await runner.setup()
63
+ site = web.TCPSite(runner, "localhost", REDIRECT_PORT)
64
+ await site.start()
65
+
66
+ try:
67
+ return await code_fut
68
+ finally:
69
+ await runner.cleanup()
70
+
71
+ # ---------- public API -------------------------------------------------------
72
+
73
+ async def login():
74
+ supa = await _client()
75
+
76
+ # 1️⃣ Build provider URL – async now
77
+ res = await supa.auth.sign_in_with_oauth(
78
+ {
79
+ "provider": "google",
80
+ "options": {"redirect_to": REDIRECT_URL},
81
+ }
82
+ ) # :contentReference[oaicite:3]{index=3}
83
+ oauth_url = res.url
84
+
85
+ # 2️⃣ Kick user to browser without blocking the loop
86
+ await asyncio.to_thread(webbrowser.open, oauth_url)
87
+ print(f"Waiting for Google OAuth redirect on {oauth_url}…")
88
+
89
+ # 3️⃣ Await the auth code, then exchange for tokens
90
+ auth_code = await _wait_for_code()
91
+ print("Got code, exchanging…")
92
+ sess = await supa.auth.exchange_code_for_session({"auth_code": auth_code}) #
93
+ _save(sess.session)
94
+ print("✅ Logged in as", sess.user.email)
95
+
96
+ async def session():
97
+ supa = await _client()
98
+ cached = _load()
99
+ fresh = None
100
+ if cached:
101
+ await supa.auth.set_session(cached["access_token"], cached["refresh_token"])
102
+ fresh = await supa.auth.get_session() # returns a Session object
103
+ _save(fresh)
104
+ return supa, fresh
105
+
106
+ async def logout():
107
+ supa, s = await session()
108
+ if s:
109
+ await supa.auth.sign_out()
110
+ CACHE_FILE.unlink(missing_ok=True)
111
+ print("👋 Signed out")
112
+
113
+ async def get_access_token():
114
+ supa, fresh = await session()
115
+ return fresh.access_token
@@ -0,0 +1,127 @@
1
+
2
+ from meshagent.cli import async_typer
3
+ import typer
4
+ from meshagent.cli.helper import get_client, print_json_table, set_active_project, resolve_project_id
5
+ from rich import print
6
+ from meshagent.api import RoomClient, ParticipantToken, WebSocketClientProtocol, RoomException
7
+ from meshagent.cli.helper import set_active_project, get_active_project, resolve_project_id, resolve_api_key
8
+ from typing import Annotated, Optional
9
+ from meshagent.api.helpers import meshagent_base_url, websocket_room_url
10
+ import json
11
+ import aiohttp
12
+ from meshagent.api.services import send_webhook
13
+
14
+ app = async_typer.AsyncTyper()
15
+
16
+ from urllib.parse import urlparse
17
+ from pathlib import PurePath
18
+ import socket
19
+ import ipaddress
20
+
21
+ PRIVATE_NETS = (
22
+ ipaddress.ip_network("10.0.0.0/8"),
23
+ ipaddress.ip_network("172.16.0.0/12"),
24
+ ipaddress.ip_network("192.168.0.0/16"),
25
+ ipaddress.ip_network("169.254.0.0/16"), # IPv4 link-local
26
+ ipaddress.ip_network("fc00::/7"), # IPv6 unique-local
27
+ ipaddress.ip_network("fe80::/10"), # IPv6 link-local
28
+ )
29
+
30
+
31
+ def is_local_url(url: str) -> bool:
32
+
33
+ """
34
+ Return True if *url* points to the local machine or a private-LAN host.
35
+ """
36
+ # 1. Handle bare paths and file://
37
+ if "://" not in url:
38
+ return PurePath(url).is_absolute() or not ("/" in url or "\\" in url)
39
+ parsed = urlparse(url)
40
+ if parsed.scheme == "file":
41
+ return True
42
+
43
+ # 2. Quick loop-back check on hostname literal
44
+ hostname = parsed.hostname
45
+ if hostname in {"localhost", None}: # None ⇒ something like "http:///path"
46
+ return True
47
+
48
+ try:
49
+ # Accept both direct IP literals and DNS names
50
+ addr_info = socket.getaddrinfo(hostname, None)
51
+ except socket.gaierror:
52
+ return False # Unresolvable host ⇒ treat as non-local (or raise)
53
+
54
+ for *_, sockaddr in addr_info:
55
+ ip_str = sockaddr[0]
56
+ ip = ipaddress.ip_address(ip_str)
57
+
58
+ if ip.is_loopback:
59
+ return True
60
+ if any(ip in net for net in PRIVATE_NETS):
61
+ return True
62
+
63
+
64
+ @app.async_command("schema")
65
+ @app.async_command("tool")
66
+ @app.async_command("agent")
67
+ async def make_call(
68
+ *,
69
+ project_id: str = None,
70
+ room: Annotated[str, typer.Option()],
71
+ api_key_id: Annotated[Optional[str], typer.Option()] = None,
72
+ name: Annotated[str, typer.Option(..., help="Participant name")] = "cli",
73
+ role: str = "agent",
74
+ local: Optional[bool] = None,
75
+ agent_name: Annotated[str, typer.Option(..., help="Name of the agent to call")],
76
+ url: Annotated[str, typer.Option(..., help="URL the agent should call")],
77
+ arguments: Annotated[str, typer.Option(..., help="JSON string with arguments for the call")] = {}
78
+ ):
79
+ """
80
+ Instruct an agent to 'call' a given URL with specific arguments.
81
+ """
82
+ account_client = await get_client()
83
+ try:
84
+ project_id = await resolve_project_id(project_id=project_id)
85
+ api_key_id = await resolve_api_key(project_id, api_key_id)
86
+
87
+ key = (await account_client.decrypt_project_api_key(project_id=project_id, id=api_key_id))["token"]
88
+
89
+ token = ParticipantToken(
90
+ name=name,
91
+ project_id=project_id,
92
+ api_key_id=api_key_id
93
+ )
94
+ token.add_role_grant(role=role)
95
+ token.add_room_grant(room)
96
+
97
+ if local == None:
98
+ local = is_local_url(url)
99
+
100
+ if local:
101
+ async with aiohttp.ClientSession() as session:
102
+
103
+ event="room.call"
104
+ data={
105
+ "room_url" : websocket_room_url(room_name=room),
106
+ "room_name" : room,
107
+ "token" : token.to_jwt(token=key),
108
+ "arguments" : arguments,
109
+ }
110
+
111
+ await send_webhook(session=session, url=url, event=event, data=data, secret=None)
112
+ else:
113
+ print("[bold green]Connecting to room...[/bold green]")
114
+ async with RoomClient(
115
+ protocol=WebSocketClientProtocol(url=websocket_room_url(room_name=room, base_url=meshagent_base_url()),
116
+ token=token.to_jwt(token=key))
117
+ ) as client:
118
+ print("[bold green]Making agent call...[/bold green]")
119
+ await client.agents.make_call(
120
+ name=agent_name,
121
+ url=url,
122
+ arguments=json.loads(arguments)
123
+ )
124
+ print("[bold cyan]Call request sent successfully.[/bold cyan]")
125
+
126
+ finally:
127
+ await account_client.close()