agentstack-cli 0.6.0rc1__py3-none-manylinux_2_34_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ import functools
3
+ import importlib.metadata
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import typing
10
+
11
+ import httpx
12
+ import packaging.version
13
+ import pydantic
14
+ import typer
15
+ from InquirerPy import inquirer
16
+
17
+ import agentstack_cli.commands.platform
18
+ from agentstack_cli.api import fetch_server_version
19
+ from agentstack_cli.async_typer import AsyncTyper
20
+ from agentstack_cli.commands.model import setup as model_setup
21
+ from agentstack_cli.configuration import Configuration
22
+ from agentstack_cli.console import console
23
+ from agentstack_cli.utils import run_command, verbosity
24
+
25
+ app = AsyncTyper()
26
+ configuration = Configuration()
27
+
28
+
29
+ @functools.cache
30
+ def _path() -> str:
31
+ # These are PATHs where `uv` installs itself when installed through own install script
32
+ # Package managers may install elsewhere, but that location should already be in PATH
33
+ return os.pathsep.join(
34
+ [
35
+ *([xdg_bin_home] if (xdg_bin_home := os.getenv("XDG_BIN_HOME")) else []),
36
+ *([os.path.realpath(f"{xdg_data_home}/../bin")] if (xdg_data_home := os.getenv("XDG_DATA_HOME")) else []),
37
+ os.path.expanduser("~/.local/bin"),
38
+ os.getenv("PATH", ""),
39
+ ]
40
+ )
41
+
42
+
43
+ @app.command("version")
44
+ async def version(
45
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
46
+ ):
47
+ """Print version of the Agent Stack CLI."""
48
+ with verbosity(verbose=verbose):
49
+ cli_version = importlib.metadata.version("agentstack-cli")
50
+ platform_version = await fetch_server_version()
51
+ active_server = configuration.auth_manager.active_server
52
+
53
+ latest_cli_version: str | None = None
54
+ with console.status("Checking for newer version...", spinner="dots"):
55
+ async with httpx.AsyncClient(timeout=10.0) as client:
56
+ response = await client.get("https://pypi.org/pypi/agentstack-cli/json")
57
+ PyPIPackageInfo = typing.TypedDict("PyPIPackageInfo", {"version": str})
58
+ PyPIPackage = typing.TypedDict("PyPIPackage", {"info": PyPIPackageInfo})
59
+ if response.status_code == 200:
60
+ latest_cli_version = pydantic.TypeAdapter(PyPIPackage).validate_json(response.text)["info"][
61
+ "version"
62
+ ]
63
+
64
+ console.print()
65
+ console.print(f" agentstack-cli version: [bold]{cli_version}[/bold]")
66
+ console.print(
67
+ f"agentstack-platform version: [bold]{platform_version.replace('-', '') if platform_version is not None else 'not running'}[/bold]"
68
+ )
69
+ console.print(f" agentstack server: [bold]{active_server if active_server else 'none'}[/bold]")
70
+ console.print()
71
+
72
+ if latest_cli_version and packaging.version.parse(latest_cli_version) > packaging.version.parse(cli_version):
73
+ console.hint(
74
+ f"A newer version ([bold]{latest_cli_version}[/bold]) is available. Update using: [green]agentstack self upgrade[/green]."
75
+ )
76
+ elif platform_version is None:
77
+ console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
78
+ elif platform_version.replace("-", "") != cli_version:
79
+ console.hint("Update the Agent Stack platform using: [green]agentstack platform start[/green]")
80
+ else:
81
+ console.success("Everything is up to date!")
82
+
83
+
84
+ @app.command("install")
85
+ async def install(
86
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
87
+ ):
88
+ """Install Agent Stack platform pre-requisites."""
89
+ with verbosity(verbose=verbose):
90
+ ready_to_start = False
91
+ if platform.system() == "Linux":
92
+ if shutil.which(
93
+ f"qemu-system-{'aarch64' if platform.machine().lower() == 'arm64' else platform.machine().lower()}"
94
+ ):
95
+ ready_to_start = True
96
+ else:
97
+ if os.geteuid() != 0:
98
+ console.hint(
99
+ "You may be prompted for your password to install QEMU, as this needs root privileges."
100
+ )
101
+ os.execlp("sudo", sys.executable, *sys.argv)
102
+ for cmd in [
103
+ ["apt", "install", "-y", "-qq", "qemu-system"],
104
+ ["dnf", "install", "-y", "-q", "@virtualization"],
105
+ ["pacman", "-S", "--noconfirm", "--noprogressbar", "qemu"],
106
+ ["zypper", "install", "-y", "-qq", "qemu"],
107
+ ["yum", "install", "-y", "-q", "qemu-kvm"],
108
+ ["emerge", "--quiet", "app-emulation/qemu"],
109
+ ]:
110
+ if shutil.which(cmd[0]):
111
+ try:
112
+ await run_command(cmd, f"Installing QEMU with {cmd[0]}")
113
+ ready_to_start = True
114
+ break
115
+ except (subprocess.CalledProcessError, FileNotFoundError):
116
+ console.warning(
117
+ "Failed to install QEMU automatically. Please install QEMU manually before using Agent Stack. Refer to https://www.qemu.org/download/ for instructions."
118
+ )
119
+ break
120
+ elif platform.system() == "Darwin":
121
+ ready_to_start = True
122
+
123
+ already_started = False
124
+ console.print()
125
+ if (
126
+ ready_to_start
127
+ and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
128
+ message="Do you want to start the Agent Stack platform now? Will run: agentstack platform start",
129
+ default=True,
130
+ ).execute_async()
131
+ ):
132
+ try:
133
+ await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
134
+ already_started = True
135
+ console.print()
136
+ except Exception:
137
+ console.warning("Platform start failed. You can retry with [green]agentstack platform start[/green].")
138
+
139
+ already_configured = False
140
+ if (
141
+ already_started
142
+ and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
143
+ message="Do you want to configure your LLM provider now? Will run: agentstack model setup", default=True
144
+ ).execute_async()
145
+ ):
146
+ try:
147
+ await model_setup(verbose=verbose)
148
+ already_configured = True
149
+ except Exception:
150
+ console.warning("Model setup failed. You can retry with [green]agentstack model setup[/green].")
151
+
152
+ if (
153
+ already_configured
154
+ and await inquirer.confirm( # pyright: ignore[reportPrivateImportUsage]
155
+ message="Do you want to open the web UI now? Will run: agentstack ui", default=True
156
+ ).execute_async()
157
+ ):
158
+ import webbrowser
159
+
160
+ webbrowser.open("http://localhost:8334")
161
+
162
+ console.print()
163
+ console.success("Installation complete!")
164
+ if not shutil.which("agentstack", path=_path()):
165
+ console.hint("Open a new terminal window to use the [green]agentstack[/green] command.")
166
+ if not already_started:
167
+ console.hint("Start the Agent Stack platform using: [green]agentstack platform start[/green]")
168
+ if not already_configured:
169
+ console.hint("Configure your LLM provider using: [green]agentstack model setup[/green]")
170
+ console.hint(
171
+ "Use [green]agentstack ui[/green] to open the web GUI, or [green]agentstack run chat[/green] to talk to an agent on the command line."
172
+ )
173
+ console.hint(
174
+ "Run [green]agentstack --help[/green] to learn about available commands, or check the documentation at https://agentstack.beeai.dev/"
175
+ )
176
+
177
+
178
+ @app.command("upgrade")
179
+ async def upgrade(
180
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
181
+ ):
182
+ """Upgrade Agent Stack CLI and Platform to the latest version."""
183
+ if not shutil.which("uv", path=_path()):
184
+ console.error("Can't self-upgrade because 'uv' was not found.")
185
+ raise typer.Exit(1)
186
+
187
+ with verbosity(verbose=verbose):
188
+ await run_command(
189
+ ["uv", "tool", "install", "--force", "agentstack-cli"],
190
+ "Upgrading agentstack-cli",
191
+ env={"PATH": _path()},
192
+ )
193
+ await agentstack_cli.commands.platform.start(set_values_list=[], import_images=[], verbose=verbose)
194
+ await version(verbose=verbose)
195
+
196
+
197
+ @app.command("uninstall")
198
+ async def uninstall(
199
+ verbose: typing.Annotated[bool, typer.Option("-v", "--verbose", help="Show verbose output")] = False,
200
+ ):
201
+ """Uninstall Agent Stack CLI and Platform."""
202
+ if not shutil.which("uv", path=_path()):
203
+ console.error("Can't self-uninstall because 'uv' was not found.")
204
+ raise typer.Exit(1)
205
+
206
+ with verbosity(verbose=verbose):
207
+ await agentstack_cli.commands.platform.delete(verbose=verbose)
208
+ await run_command(
209
+ ["uv", "tool", "uninstall", "agentstack-cli"],
210
+ "Uninstalling agentstack-cli",
211
+ env={"PATH": _path()},
212
+ )
213
+ console.success("Agent Stack uninstalled successfully.")
@@ -0,0 +1,315 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import asyncio
5
+ import logging
6
+ import sys
7
+ import typing
8
+ import uuid
9
+ import webbrowser
10
+ from urllib.parse import urlencode
11
+
12
+ import httpx
13
+ import typer
14
+ import uvicorn
15
+ from authlib.common.security import generate_token
16
+ from authlib.oauth2.rfc7636 import create_s256_code_challenge
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.responses import HTMLResponse
19
+ from InquirerPy import inquirer
20
+ from InquirerPy.base.control import Choice
21
+
22
+ from agentstack_cli.async_typer import AsyncTyper, console
23
+ from agentstack_cli.configuration import Configuration
24
+
25
+ app = AsyncTyper()
26
+
27
+ config = Configuration()
28
+
29
+ REDIRECT_URI = "http://localhost:9001/callback"
30
+
31
+
32
+ async def _wait_for_auth_code(port: int = 9001) -> str:
33
+ code_future: asyncio.Future[str] = asyncio.Future()
34
+ app = FastAPI()
35
+
36
+ @app.get("/callback")
37
+ async def callback(request: Request):
38
+ code = request.query_params.get("code")
39
+ if code and not code_future.done():
40
+ code_future.set_result(code)
41
+ return HTMLResponse(
42
+ content="""
43
+ <!DOCTYPE html>
44
+ <html>
45
+ <head>
46
+ <title>Login Successful</title>
47
+ <style>
48
+ body { font-family: Arial, sans-serif; text-align: center; margin-top: 15%; }
49
+ h1 { color: #2e7d32; }
50
+ p { color: #555; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <h1>Login successful!</h1>
55
+ <p>You can safely close this tab and return to the Agent Stack CLI.</p>
56
+ </body>
57
+ </html>
58
+ """,
59
+ status_code=200,
60
+ )
61
+
62
+ server = uvicorn.Server(config=uvicorn.Config(app, host="127.0.0.1", port=port, log_level=logging.ERROR))
63
+
64
+ async with asyncio.TaskGroup() as tg:
65
+ tg.create_task(server.serve())
66
+ code = await code_future
67
+ server.should_exit = True
68
+
69
+ return code
70
+
71
+
72
+ def get_unique_app_name() -> str:
73
+ return f"Agent Stack CLI {uuid.uuid4()}"
74
+
75
+
76
+ @app.command("login | change | select | default | switch")
77
+ async def server_login(server: typing.Annotated[str | None, typer.Argument()] = None):
78
+ """Login to a server or switch between logged in servers."""
79
+ server = server or (
80
+ await inquirer.select( # type: ignore
81
+ message="Select a server, or log in to a new one:",
82
+ choices=[
83
+ *(
84
+ Choice(
85
+ name=f"{server} {'(active)' if server == config.auth_manager.active_server else ''}",
86
+ value=server,
87
+ )
88
+ for server in config.auth_manager.servers
89
+ ),
90
+ Choice(name="Log in to a new server", value=None),
91
+ ],
92
+ default=0,
93
+ ).execute_async()
94
+ if config.auth_manager.servers
95
+ else None
96
+ )
97
+ server = server or await inquirer.text(message="Enter server URL:").execute_async() # type: ignore
98
+
99
+ if not server:
100
+ raise RuntimeError("No server selected. Action cancelled.")
101
+
102
+ if "://" not in server:
103
+ server = f"https://{server}"
104
+
105
+ server = server.rstrip("/")
106
+
107
+ if server_data := config.auth_manager.get_server(server):
108
+ console.info("Switching to an already logged in server.")
109
+ auth_server = None
110
+ auth_servers = list(server_data.authorization_servers.keys())
111
+ if len(auth_servers) == 1:
112
+ auth_server = auth_servers[0]
113
+ elif len(auth_servers) > 1:
114
+ auth_server = await inquirer.select( # type: ignore
115
+ message="Select an authorization server:",
116
+ choices=[
117
+ Choice(
118
+ name=f"{auth_server} {'(active)' if auth_server == config.auth_manager.active_auth_server else ''}",
119
+ value=auth_server,
120
+ )
121
+ for auth_server in auth_servers
122
+ ],
123
+ default=config.auth_manager.active_auth_server
124
+ if config.auth_manager.active_auth_server in auth_servers
125
+ else 0,
126
+ ).execute_async()
127
+ if not auth_server:
128
+ console.info("Action cancelled.")
129
+ sys.exit(1)
130
+ else:
131
+ console.info("No authentication tokens found for this server. Proceeding to log in.")
132
+ async with httpx.AsyncClient() as client:
133
+ resp = await client.get(f"{server}/.well-known/oauth-protected-resource/", follow_redirects=True)
134
+ if resp.is_error:
135
+ console.error("This server does not appear to run a compatible version of Agent Stack Platform.")
136
+ sys.exit(1)
137
+ metadata = resp.json()
138
+
139
+ auth_servers = metadata.get("authorization_servers", [])
140
+ auth_server = None
141
+ token = None
142
+
143
+ client_id = config.client_id
144
+ client_secret = config.client_secret
145
+ registration_token = None
146
+
147
+ if auth_servers:
148
+ if len(auth_servers) == 1:
149
+ auth_server = auth_servers[0]
150
+ else:
151
+ auth_server = await inquirer.select( # type: ignore
152
+ message="Select an authorization server:",
153
+ choices=auth_servers,
154
+ ).execute_async()
155
+
156
+ if not auth_server:
157
+ raise RuntimeError("No authorization server selected.")
158
+
159
+ async with httpx.AsyncClient() as client:
160
+ try:
161
+ resp = await client.get(f"{auth_server}/.well-known/openid-configuration")
162
+ resp.raise_for_status()
163
+ oidc = resp.json()
164
+ except Exception as e:
165
+ raise RuntimeError(f"OIDC discovery failed: {e}") from e
166
+
167
+ registration_endpoint = oidc["registration_endpoint"]
168
+ if not client_id and registration_endpoint:
169
+ async with httpx.AsyncClient() as client:
170
+ resp = None
171
+ try:
172
+ app_name = get_unique_app_name()
173
+ resp = await client.post(
174
+ registration_endpoint,
175
+ json={
176
+ "client_name": app_name,
177
+ "grant_types": ["authorization_code", "refresh_token"],
178
+ "enforce_pkce": True,
179
+ "all_users_entitled": True,
180
+ "redirect_uris": [REDIRECT_URI],
181
+ },
182
+ )
183
+ resp.raise_for_status()
184
+ data = resp.json()
185
+ client_id = data["client_id"]
186
+ client_secret = data["client_secret"]
187
+ registration_token = data["registration_access_token"]
188
+ except Exception as e:
189
+ if resp:
190
+ try:
191
+ error_details = resp.json()
192
+ console.warning(
193
+ f"error: {error_details['error']} error description: {error_details['error_description']}"
194
+ )
195
+
196
+ except Exception:
197
+ console.info("no parsable json response.")
198
+ console.warning(f" Dynamic client registration failed. Proceed with manual input. {e!s}")
199
+
200
+ if not client_id:
201
+ client_id = (
202
+ await inquirer.text( # type: ignore
203
+ message="Enter Client ID (default agentstack-cli):",
204
+ instruction=f"(Redirect URI: {REDIRECT_URI})",
205
+ ).execute_async()
206
+ or "agentstack-cli"
207
+ )
208
+ if not client_id:
209
+ raise RuntimeError("Client ID is mandatory. Action cancelled.")
210
+ client_secret = (
211
+ await inquirer.text( # type: ignore
212
+ message="Enter Client Secret (optional):"
213
+ ).execute_async()
214
+ or None
215
+ )
216
+
217
+ code_verifier = generate_token(64)
218
+
219
+ auth_url = f"{oidc['authorization_endpoint']}?{
220
+ urlencode(
221
+ {
222
+ 'client_id': client_id,
223
+ 'response_type': 'code',
224
+ 'redirect_uri': REDIRECT_URI,
225
+ 'scope': ' '.join(metadata.get('scopes_supported', ['openid', 'email', 'profile'])),
226
+ 'code_challenge': typing.cast(str, create_s256_code_challenge(code_verifier)),
227
+ 'code_challenge_method': 'S256',
228
+ }
229
+ )
230
+ }"
231
+
232
+ console.info(f"Opening browser for login: [cyan]{auth_url}[/cyan]")
233
+ if not webbrowser.open(auth_url):
234
+ console.warning("Could not open browser. Please visit the above URL manually.")
235
+
236
+ code = await _wait_for_auth_code()
237
+ async with httpx.AsyncClient() as client:
238
+ token_resp = None
239
+ try:
240
+ token_resp = await client.post(
241
+ oidc["token_endpoint"],
242
+ data={
243
+ "grant_type": "authorization_code",
244
+ "code": code,
245
+ "redirect_uri": REDIRECT_URI,
246
+ "client_id": client_id,
247
+ "client_secret": client_secret,
248
+ "code_verifier": code_verifier,
249
+ },
250
+ )
251
+ token_resp.raise_for_status()
252
+ token = token_resp.json()
253
+ except Exception as e:
254
+ if resp:
255
+ try:
256
+ error_details = resp.json()
257
+ console.warning(
258
+ f"error: {error_details['error']} error description: {error_details['error_description']}"
259
+ )
260
+ except Exception:
261
+ console.info("no parsable json response.")
262
+
263
+ raise RuntimeError(f"Token request failed: {e}") from e
264
+
265
+ if not token:
266
+ raise RuntimeError("Login timed out or not successful.")
267
+
268
+ config.auth_manager.save_auth_token(
269
+ server=server,
270
+ auth_server=auth_server,
271
+ client_id=client_id,
272
+ client_secret=client_secret,
273
+ token=token,
274
+ registration_token=registration_token,
275
+ )
276
+
277
+ config.auth_manager.active_server = server
278
+ config.auth_manager.active_auth_server = auth_server
279
+ console.success(f"Logged in to [cyan]{server}[/cyan].")
280
+
281
+
282
+ @app.command("logout | remove | rm | delete")
283
+ async def server_logout(
284
+ all: typing.Annotated[
285
+ bool,
286
+ typer.Option(),
287
+ ] = False,
288
+ ):
289
+ await config.auth_manager.clear_auth_token(all=all)
290
+ console.success("You have been logged out.")
291
+
292
+
293
+ @app.command("show")
294
+ def server_show():
295
+ if not config.auth_manager.active_server:
296
+ console.info("No server selected.")
297
+ console.hint(
298
+ "Run [green]agentstack server list[/green] to list available servers, and [green]agentstack server login[/green] to select one."
299
+ )
300
+ return
301
+ console.info(f"Active server: [cyan]{config.auth_manager.active_server}[/cyan]")
302
+
303
+
304
+ @app.command("list")
305
+ def server_list():
306
+ if not config.auth_manager.servers:
307
+ console.info("No servers found.")
308
+ console.hint(
309
+ "Run [green]agentstack platform start[/green] to start a local server, or [green]agentstack server login[/green] to connect to a remote one."
310
+ )
311
+ return
312
+ for server in config.auth_manager.servers:
313
+ console.print(
314
+ f"[cyan]{server}[/cyan] {'[green](active)[/green]' if server == config.auth_manager.active_server else ''}"
315
+ )
@@ -0,0 +1,87 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import typing
5
+ from datetime import datetime
6
+
7
+ import typer
8
+ from agentstack_sdk.platform import User
9
+ from agentstack_sdk.platform.user import UserRole
10
+ from rich.table import Column
11
+
12
+ from agentstack_cli.async_typer import AsyncTyper, console, create_table
13
+ from agentstack_cli.configuration import Configuration
14
+ from agentstack_cli.utils import announce_server_action, confirm_server_action
15
+
16
+ app = AsyncTyper()
17
+ configuration = Configuration()
18
+
19
+ ROLE_DISPLAY = {
20
+ "admin": "[red]admin[/red]",
21
+ "developer": "[cyan]developer[/cyan]",
22
+ "user": "user",
23
+ }
24
+
25
+
26
+ @app.command("list", help="List platform users [Admin only]")
27
+ async def list_users(
28
+ email: typing.Annotated[str | None, typer.Option(help="Filter by email (case-insensitive partial match)")] = None,
29
+ limit: typing.Annotated[int, typer.Option(help="Results per page (1-100)")] = 40,
30
+ after: typing.Annotated[str | None, typer.Option(help="Pagination cursor (page_token)")] = None,
31
+ ):
32
+ announce_server_action("Listing users on")
33
+
34
+ async with configuration.use_platform_client():
35
+ result = await User.list(email=email, limit=limit, page_token=after)
36
+
37
+ items = result.items
38
+ has_more = result.has_more
39
+ next_page_token = result.next_page_token
40
+
41
+ with create_table(
42
+ Column("ID", style="yellow"),
43
+ Column("Email"),
44
+ Column("Role"),
45
+ Column("Created"),
46
+ no_wrap=True,
47
+ ) as table:
48
+ for user in items:
49
+ role_display = ROLE_DISPLAY.get(user.role, user.role)
50
+
51
+ created_at = _format_date(user.created_at)
52
+
53
+ table.add_row(
54
+ user.id,
55
+ user.email,
56
+ role_display,
57
+ created_at,
58
+ )
59
+
60
+ console.print()
61
+ console.print(table)
62
+
63
+ if has_more and next_page_token:
64
+ console.print(f"\n[dim]Use --after {next_page_token} to see more[/dim]")
65
+
66
+
67
+ @app.command("set-role", help="Change user role [Admin only]")
68
+ async def set_role(
69
+ user_id: typing.Annotated[str, typer.Argument(help="User UUID")],
70
+ role: typing.Annotated[UserRole, typer.Argument(help="Target role (admin, developer, user)")],
71
+ yes: typing.Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation prompts.")] = False,
72
+ ):
73
+ url = announce_server_action(f"Changing user {user_id} to role '{role}' on")
74
+ await confirm_server_action("Proceed with role change on", url=url, yes=yes)
75
+
76
+ async with configuration.use_platform_client():
77
+ result = await User.set_role(user_id, UserRole(role))
78
+
79
+ role_display = ROLE_DISPLAY.get(result.new_role, result.new_role)
80
+
81
+ console.success(f"User role updated to [cyan]{role_display}[/cyan]")
82
+
83
+
84
+ def _format_date(dt: datetime | None) -> str:
85
+ if not dt:
86
+ return "-"
87
+ return dt.strftime("%Y-%m-%d %H:%M")
@@ -0,0 +1,79 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import functools
5
+ import importlib.metadata
6
+ import pathlib
7
+ import re
8
+ import sys
9
+ import typing
10
+ from collections.abc import AsyncIterator
11
+ from contextlib import asynccontextmanager
12
+
13
+ import pydantic
14
+ import pydantic_settings
15
+ from agentstack_sdk.platform import PlatformClient, use_platform_client
16
+ from pydantic import HttpUrl, SecretStr
17
+
18
+ from agentstack_cli.auth_manager import AuthManager
19
+ from agentstack_cli.console import console
20
+
21
+
22
+ @functools.cache
23
+ def version():
24
+ # Python strips '-', we need to re-insert it: 1.2.3rc1 -> 1.2.3-rc1
25
+ return re.sub(r"([0-9])([a-z])", r"\1-\2", importlib.metadata.version("agentstack-cli"))
26
+
27
+
28
+ @functools.cache
29
+ class Configuration(pydantic_settings.BaseSettings):
30
+ model_config = pydantic_settings.SettingsConfigDict(
31
+ env_file=None, env_prefix="AGENTSTACK__", env_nested_delimiter="__", extra="allow"
32
+ )
33
+ debug: bool = False
34
+ home: pathlib.Path = pydantic.Field(default_factory=lambda: pathlib.Path.home() / ".agentstack")
35
+ agent_registry: pydantic.AnyUrl = HttpUrl(
36
+ f"https://github.com/i-am-bee/agentstack@v{version()}#path=agent-registry.yaml"
37
+ )
38
+ username: str = "admin"
39
+ password: SecretStr | None = None
40
+ server_metadata_ttl: int = 86400
41
+
42
+ oidc_enabled: bool = False
43
+ client_id: str | None = None
44
+ client_secret: str | None = None
45
+
46
+ @property
47
+ def lima_home(self) -> pathlib.Path:
48
+ return self.home / "lima"
49
+
50
+ @property
51
+ def auth_file(self) -> pathlib.Path:
52
+ """Return auth config file path"""
53
+ return self.home / "auth.json"
54
+
55
+ @property
56
+ def auth_manager(self) -> AuthManager:
57
+ return AuthManager(self.auth_file)
58
+
59
+ @asynccontextmanager
60
+ async def use_platform_client(self) -> AsyncIterator[PlatformClient]:
61
+ if self.auth_manager.active_server is None:
62
+ console.error("No server selected.")
63
+ console.hint(
64
+ "Run [green]agentstack platform start[/green] to start a local server, or [green]agentstack server login[/green] to connect to a remote one."
65
+ )
66
+ sys.exit(1)
67
+ async with use_platform_client(
68
+ auth=(self.username, self.password.get_secret_value()) if self.password else None,
69
+ auth_token=await self.auth_manager.load_auth_token(),
70
+ base_url=self.auth_manager.active_server + "/",
71
+ ) as client:
72
+ yield client
73
+
74
+ @pydantic.model_validator(mode="after")
75
+ def _check_old_home(self) -> typing.Self:
76
+ old_home = pathlib.Path.home() / ".beeai"
77
+ if old_home.exists() and not self.home.exists():
78
+ old_home.rename(self.home)
79
+ return self