agentstack-cli 0.4.2__tar.gz → 0.4.2rc1__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.
Files changed (27) hide show
  1. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/PKG-INFO +1 -1
  2. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/pyproject.toml +1 -1
  3. agentstack_cli-0.4.2rc1/src/agentstack_cli/__init__.py +77 -0
  4. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/api.py +14 -28
  5. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/async_typer.py +1 -2
  6. agentstack_cli-0.4.2rc1/src/agentstack_cli/auth_manager.py +126 -0
  7. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/agent.py +78 -122
  8. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/build.py +48 -74
  9. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/mcp.py +3 -12
  10. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/model.py +3 -28
  11. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/__init__.py +5 -5
  12. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/base_driver.py +0 -2
  13. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/wsl_driver.py +2 -7
  14. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/self.py +4 -8
  15. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/server.py +5 -40
  16. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/configuration.py +1 -1
  17. agentstack_cli-0.4.2rc1/src/agentstack_cli/data/helm-chart.tgz +0 -0
  18. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/utils.py +5 -61
  19. agentstack_cli-0.4.2/src/agentstack_cli/__init__.py +0 -119
  20. agentstack_cli-0.4.2/src/agentstack_cli/auth_manager.py +0 -241
  21. agentstack_cli-0.4.2/src/agentstack_cli/data/helm-chart.tgz +0 -0
  22. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/README.md +0 -0
  23. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/__init__.py +0 -0
  24. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/istio.py +0 -0
  25. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/commands/platform/lima_driver.py +0 -0
  26. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/console.py +0 -0
  27. {agentstack_cli-0.4.2 → agentstack_cli-0.4.2rc1}/src/agentstack_cli/data/.gitignore +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: agentstack-cli
3
- Version: 0.4.2
3
+ Version: 0.4.2rc1
4
4
  Summary: Agent Stack CLI
5
5
  Author: IBM Corp.
6
6
  Requires-Dist: anyio~=4.10.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentstack-cli"
3
- version = "0.4.2"
3
+ version = "0.4.2-rc1"
4
4
  description = "Agent Stack CLI"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "IBM Corp." }]
@@ -0,0 +1,77 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ import typing
6
+ from copy import deepcopy
7
+
8
+ import typer
9
+
10
+ import agentstack_cli.commands.agent
11
+ import agentstack_cli.commands.build
12
+ import agentstack_cli.commands.mcp
13
+ import agentstack_cli.commands.model
14
+ import agentstack_cli.commands.platform
15
+ import agentstack_cli.commands.self
16
+ import agentstack_cli.commands.server
17
+ from agentstack_cli.async_typer import AsyncTyper
18
+ from agentstack_cli.configuration import Configuration
19
+
20
+ logging.basicConfig(level=logging.INFO if Configuration().debug else logging.FATAL)
21
+ logging.getLogger("httpx").setLevel(logging.WARNING) # not sure why this is necessary
22
+
23
+ app = AsyncTyper(no_args_is_help=True)
24
+ app.add_typer(agentstack_cli.commands.model.app, name="model", no_args_is_help=True, help="Manage model providers.")
25
+ app.add_typer(agentstack_cli.commands.agent.app, name="agent", no_args_is_help=True, help="Manage agents.")
26
+ app.add_typer(
27
+ agentstack_cli.commands.platform.app, name="platform", no_args_is_help=True, help="Manage Agent Stack platform."
28
+ )
29
+ app.add_typer(
30
+ agentstack_cli.commands.mcp.app, name="mcp", no_args_is_help=True, help="Manage MCP servers and toolkits."
31
+ )
32
+ app.add_typer(agentstack_cli.commands.build.app, name="", no_args_is_help=True, help="Build agent images.")
33
+ app.add_typer(
34
+ agentstack_cli.commands.server.app,
35
+ name="server",
36
+ no_args_is_help=True,
37
+ help="Manage Agent Stack servers and authentication.",
38
+ )
39
+ app.add_typer(
40
+ agentstack_cli.commands.self.app,
41
+ name="self",
42
+ no_args_is_help=True,
43
+ help="Manage Agent Stack installation.",
44
+ hidden=True,
45
+ )
46
+
47
+
48
+ agent_alias = deepcopy(agentstack_cli.commands.agent.app)
49
+ for cmd in agent_alias.registered_commands:
50
+ cmd.rich_help_panel = "Agent commands"
51
+
52
+ app.add_typer(agent_alias, name="", no_args_is_help=True)
53
+
54
+
55
+ @app.command("version")
56
+ async def version(verbose: typing.Annotated[bool, typer.Option("-v", help="Show verbose output")] = False):
57
+ """Print version of the Agent Stack CLI."""
58
+ import agentstack_cli.commands.self
59
+
60
+ await agentstack_cli.commands.self.version(verbose=verbose)
61
+
62
+
63
+ @app.command("ui")
64
+ async def ui():
65
+ """Launch the graphical interface."""
66
+ import webbrowser
67
+
68
+ import agentstack_cli.commands.model
69
+
70
+ await agentstack_cli.commands.model.ensure_llm_provider()
71
+ webbrowser.open(
72
+ "http://localhost:8334"
73
+ ) # TODO: This always opens the local UI, how to open the UI of a logged in server instead?
74
+
75
+
76
+ if __name__ == "__main__":
77
+ app()
@@ -1,18 +1,17 @@
1
1
  # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
2
  # SPDX-License-Identifier: Apache-2.0
3
- import json
3
+
4
4
  import re
5
5
  import urllib
6
6
  import urllib.parse
7
7
  from collections.abc import AsyncIterator
8
8
  from contextlib import asynccontextmanager
9
9
  from datetime import timedelta
10
- from textwrap import indent
11
10
  from typing import Any
12
11
 
13
12
  import httpx
14
13
  import openai
15
- from a2a.client import A2AClientHTTPError, Client, ClientConfig, ClientFactory
14
+ from a2a.client import Client, ClientConfig, ClientFactory
16
15
  from a2a.types import AgentCard
17
16
  from httpx import HTTPStatusError
18
17
  from httpx._types import RequestFiles
@@ -44,7 +43,7 @@ async def api_request(
44
43
  timeout=60,
45
44
  headers=(
46
45
  {"Authorization": f"Bearer {token}"}
47
- if use_auth and (token := await config.auth_manager.load_auth_token())
46
+ if use_auth and (token := config.auth_manager.load_auth_token())
48
47
  else {}
49
48
  ),
50
49
  )
@@ -83,7 +82,7 @@ async def api_stream(
83
82
  timeout=timedelta(hours=1).total_seconds(),
84
83
  headers=(
85
84
  {"Authorization": f"Bearer {token}"}
86
- if use_auth and (token := await config.auth_manager.load_auth_token())
85
+ if use_auth and (token := config.auth_manager.load_auth_token())
87
86
  else {}
88
87
  ),
89
88
  ) as response,
@@ -104,29 +103,16 @@ async def api_stream(
104
103
 
105
104
  @asynccontextmanager
106
105
  async def a2a_client(agent_card: AgentCard, use_auth: bool = True) -> AsyncIterator[Client]:
107
- try:
108
- async with httpx.AsyncClient(
109
- headers=(
110
- {"Authorization": f"Bearer {token}"}
111
- if use_auth and (token := await config.auth_manager.load_auth_token())
112
- else {}
113
- ),
114
- follow_redirects=True,
115
- timeout=timedelta(hours=1).total_seconds(),
116
- ) as httpx_client:
117
- yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(
118
- card=agent_card
119
- )
120
- except A2AClientHTTPError as ex:
121
- card_data = json.dumps(
122
- agent_card.model_dump(include={"url", "additional_interfaces", "preferred_transport"}), indent=2
123
- )
124
- raise RuntimeError(
125
- f"The agent is not reachable, please check that the agent card is configured properly.\n"
126
- f"Agent connection info:\n{indent(card_data, prefix=' ')}\n"
127
- "Full Error:\n"
128
- f"{indent(str(ex), prefix=' ')}"
129
- ) from ex
106
+ async with httpx.AsyncClient(
107
+ headers=(
108
+ {"Authorization": f"Bearer {token}"}
109
+ if use_auth and (token := config.auth_manager.load_auth_token())
110
+ else {}
111
+ ),
112
+ follow_redirects=True,
113
+ timeout=timedelta(hours=1).total_seconds(),
114
+ ) as httpx_client:
115
+ yield ClientFactory(ClientConfig(httpx_client=httpx_client, use_client_preference=True)).create(card=agent_card)
130
116
 
131
117
 
132
118
  @asynccontextmanager
@@ -67,8 +67,7 @@ class AliasGroup(TyperGroup):
67
67
 
68
68
  class AsyncTyper(typer.Typer):
69
69
  def __init__(self, *args, **kwargs):
70
- kwargs["cls"] = kwargs.get("cls", AliasGroup)
71
- super().__init__(*args, **kwargs)
70
+ super().__init__(*args, **kwargs, cls=AliasGroup)
72
71
 
73
72
  def command(self, *args, **kwargs):
74
73
  parent_decorator = super().command(*args, **kwargs)
@@ -0,0 +1,126 @@
1
+ # Copyright 2025 © BeeAI a Series of LF Projects, LLC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import pathlib
5
+ import typing
6
+ from collections import defaultdict
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class AuthToken(BaseModel):
13
+ access_token: str
14
+ token_type: str = "Bearer"
15
+ expires_in: int | None = None
16
+ refresh_token: str | None = None
17
+ scope: str | None = None
18
+
19
+
20
+ class AuthServer(BaseModel):
21
+ client_id: str = "df82a687-d647-4247-838b-7080d7d83f6c" # Backwards compatibility default
22
+ client_secret: str | None = None
23
+ token: AuthToken | None = None
24
+
25
+
26
+ class Server(BaseModel):
27
+ authorization_servers: dict[str, AuthServer] = Field(default_factory=dict)
28
+
29
+
30
+ class Auth(BaseModel):
31
+ version: typing.Literal[1] = 1
32
+ servers: defaultdict[str, typing.Annotated[Server, Field(default_factory=Server)]] = Field(
33
+ default_factory=lambda: defaultdict(Server)
34
+ )
35
+ active_server: str | None = None
36
+ active_auth_server: str | None = None
37
+
38
+
39
+ @typing.final
40
+ class AuthManager:
41
+ def __init__(self, config_path: pathlib.Path):
42
+ self._auth_path = config_path
43
+ self._auth = self._load()
44
+
45
+ def _load(self) -> Auth:
46
+ if not self._auth_path.exists():
47
+ return Auth()
48
+ return Auth.model_validate_json(self._auth_path.read_bytes())
49
+
50
+ def _save(self) -> None:
51
+ self._auth_path.write_text(self._auth.model_dump_json(indent=2))
52
+
53
+ def save_auth_token(
54
+ self,
55
+ server: str,
56
+ auth_server: str | None = None,
57
+ client_id: str | None = None,
58
+ client_secret: str | None = None,
59
+ token: dict[str, Any] | None = None,
60
+ ) -> None:
61
+ if auth_server is not None and client_id is not None and token is not None:
62
+ self._auth.servers[server].authorization_servers[auth_server] = AuthServer(
63
+ client_id=client_id, client_secret=client_secret, token=AuthToken(**token)
64
+ )
65
+ else:
66
+ self._auth.servers[server] # touch
67
+ self._save()
68
+
69
+ def load_auth_token(self) -> str | None:
70
+ active_res = self._auth.active_server
71
+ active_auth_server = self._auth.active_auth_server
72
+ if not active_res or not active_auth_server:
73
+ return None
74
+ server = self._auth.servers.get(active_res)
75
+ if not server:
76
+ return None
77
+
78
+ auth_server = server.authorization_servers.get(active_auth_server)
79
+ if not auth_server or not auth_server.token:
80
+ return None
81
+
82
+ return auth_server.token.access_token
83
+
84
+ def clear_auth_token(self, all: bool = False) -> None:
85
+ if all:
86
+ self._auth.servers = defaultdict(Server)
87
+ else:
88
+ if self._auth.active_server and self._auth.active_auth_server:
89
+ del self._auth.servers[self._auth.active_server].authorization_servers[self._auth.active_auth_server]
90
+ if self._auth.active_server and not self._auth.servers[self._auth.active_server].authorization_servers:
91
+ del self._auth.servers[self._auth.active_server]
92
+ self._auth.active_server = None
93
+ self._auth.active_auth_server = None
94
+ self._save()
95
+
96
+ def get_server(self, server: str) -> Server | None:
97
+ return self._auth.servers.get(server)
98
+
99
+ @property
100
+ def servers(self) -> list[str]:
101
+ return list(self._auth.servers.keys())
102
+
103
+ @property
104
+ def active_server(self) -> str | None:
105
+ return self._auth.active_server
106
+
107
+ @active_server.setter
108
+ def active_server(self, server: str | None) -> None:
109
+ if server is not None and server not in self._auth.servers:
110
+ raise ValueError(f"Server {server} not found")
111
+ self._auth.active_server = server
112
+ self._save()
113
+
114
+ @property
115
+ def active_auth_server(self) -> str | None:
116
+ return self._auth.active_auth_server
117
+
118
+ @active_auth_server.setter
119
+ def active_auth_server(self, auth_server: str | None) -> None:
120
+ if auth_server is not None and (
121
+ self._auth.active_server not in self._auth.servers
122
+ or auth_server not in self._auth.servers[self._auth.active_server].authorization_servers
123
+ ):
124
+ raise ValueError(f"Auth server {auth_server} not found in active server")
125
+ self._auth.active_auth_server = auth_server
126
+ self._save()