albus-cli 0.1.0__py3-none-any.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.
albus_cli/__init__.py ADDED
File without changes
albus_cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m albus_cli`."""
2
+
3
+ from albus_cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
albus_cli/client.py ADDED
@@ -0,0 +1,30 @@
1
+ """Albus SDK client construction from the shell environment."""
2
+
3
+ import os
4
+
5
+ import httpx
6
+ from albus_sdk import Albus, models
7
+
8
+ API_KEY_ENV = "ALBUS_API_KEY"
9
+ BASE_URL_ENV = "ALBUS_BASE_URL"
10
+
11
+
12
+ class MissingAPIKey(Exception):
13
+ def __init__(self) -> None:
14
+ super().__init__(
15
+ f"{API_KEY_ENV} is not set. Export an Albus API key, e.g. "
16
+ f"`export {API_KEY_ENV}=...`."
17
+ )
18
+
19
+
20
+ def client(base_url: str | None, timeout: float | None) -> Albus:
21
+ """Build an SDK client. A timeout of None disables the read timeout."""
22
+ api_key = os.environ.get(API_KEY_ENV)
23
+ if not api_key:
24
+ raise MissingAPIKey
25
+
26
+ return Albus(
27
+ security=models.Security(api_key_auth=api_key),
28
+ server_url=base_url,
29
+ client=httpx.Client(follow_redirects=True, timeout=timeout),
30
+ )
File without changes
@@ -0,0 +1,39 @@
1
+ """`albus agents` — inspect the agents that have run in the org."""
2
+
3
+ from typing import Annotated
4
+
5
+ import typer
6
+
7
+ from albus_cli.context import sdk
8
+ from albus_cli.output import emit
9
+
10
+ app = typer.Typer(no_args_is_help=True, help="Inspect agents.")
11
+
12
+ AgentName = Annotated[
13
+ str, typer.Argument(metavar="AGENT_NAME", help="Agent name.")
14
+ ]
15
+
16
+
17
+ @app.command("list")
18
+ def list_agents(ctx: typer.Context) -> None:
19
+ """List agents."""
20
+ emit(sdk(ctx).agents.list_agents())
21
+
22
+
23
+ @app.command("get")
24
+ def get(ctx: typer.Context, name: AgentName) -> None:
25
+ """Get an agent with its current revision."""
26
+ emit(sdk(ctx).agents.get_agent(name=name))
27
+
28
+
29
+ @app.command("revision")
30
+ def revision(
31
+ ctx: typer.Context,
32
+ name: AgentName,
33
+ revision: Annotated[
34
+ str,
35
+ typer.Argument(metavar="REVISION", help="Agent revision identifier."),
36
+ ],
37
+ ) -> None:
38
+ """Get one revision of an agent."""
39
+ emit(sdk(ctx).agents.get_agent_revision(name=name, revision=revision))
@@ -0,0 +1,62 @@
1
+ """`albus secrets` — manage secrets available to agent sessions."""
2
+
3
+ import sys
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from albus_cli.context import sdk
9
+ from albus_cli.output import emit
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="Manage secrets.")
12
+
13
+ Name = Annotated[str, typer.Argument(metavar="NAME", help="Secret name.")]
14
+ Value = Annotated[
15
+ str | None,
16
+ typer.Option(
17
+ "--value",
18
+ help="Secret value. Omit to read it from stdin, keeping it out "
19
+ "of the shell history.",
20
+ ),
21
+ ]
22
+
23
+
24
+ def secret_value(value: str | None) -> str:
25
+ if value is not None:
26
+ return value
27
+
28
+ read = sys.stdin.read().strip()
29
+ if not read:
30
+ raise typer.BadParameter("no secret value on stdin")
31
+
32
+ return read
33
+
34
+
35
+ @app.command("list")
36
+ def list_secrets(ctx: typer.Context) -> None:
37
+ """List all secrets with masked values."""
38
+ emit(sdk(ctx).secrets.list_secrets())
39
+
40
+
41
+ @app.command("create")
42
+ def create(ctx: typer.Context, name: Name, value: Value = None) -> None:
43
+ """Create a secret."""
44
+ emit(sdk(ctx).secrets.create_secret(name=name, value=secret_value(value)))
45
+
46
+
47
+ @app.command("get")
48
+ def get(ctx: typer.Context, name: Name) -> None:
49
+ """Get a secret's masked value."""
50
+ emit(sdk(ctx).secrets.get_secret(name=name))
51
+
52
+
53
+ @app.command("update")
54
+ def update(ctx: typer.Context, name: Name, value: Value = None) -> None:
55
+ """Replace a secret's value."""
56
+ emit(sdk(ctx).secrets.update_secret(name=name, value=secret_value(value)))
57
+
58
+
59
+ @app.command("delete")
60
+ def delete(ctx: typer.Context, name: Name) -> None:
61
+ """Delete a secret."""
62
+ sdk(ctx).secrets.delete_secret(name=name)
@@ -0,0 +1,223 @@
1
+ """`albus sessions` — run and inspect agent sessions."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from albus_sdk import models
8
+
9
+ from albus_cli.client import client
10
+ from albus_cli.context import options, sdk
11
+ from albus_cli.output import emit
12
+
13
+ app = typer.Typer(no_args_is_help=True, help="Run and inspect sessions.")
14
+
15
+ SessionID = Annotated[
16
+ str,
17
+ typer.Argument(
18
+ metavar="SESSION_ID",
19
+ help="Client-provided session identifier. Reuse it to continue "
20
+ "the same session.",
21
+ ),
22
+ ]
23
+ After = Annotated[
24
+ str | None,
25
+ typer.Option("--after", help="Pagination cursor from a previous page."),
26
+ ]
27
+ Limit = Annotated[int, typer.Option("--limit", help="Page size.")]
28
+
29
+
30
+ def agent_config(
31
+ agent_file: Path | None,
32
+ model: str | None,
33
+ provider: str | None,
34
+ credential: str | None,
35
+ system_prompt: str | None,
36
+ tools: list[str],
37
+ max_steps: int | None,
38
+ ) -> models.AgentConfig:
39
+ if agent_file is not None:
40
+ flags = (model, provider, credential, system_prompt, max_steps)
41
+ if any(flag is not None for flag in flags) or tools:
42
+ raise typer.BadParameter(
43
+ "--agent-file holds the whole agent configuration and "
44
+ "cannot be combined with the other agent options"
45
+ )
46
+
47
+ return models.AgentConfig.model_validate_json(agent_file.read_text())
48
+
49
+ if model is None:
50
+ raise typer.BadParameter(
51
+ "--model is required unless --agent-file is given"
52
+ )
53
+
54
+ if (provider is None) != (credential is None):
55
+ raise typer.BadParameter(
56
+ "--provider and --credential must be given together"
57
+ )
58
+
59
+ provider_config = (
60
+ models.Provider(name=provider, credential=credential)
61
+ if provider is not None and credential is not None
62
+ else None
63
+ )
64
+
65
+ return models.AgentConfig(
66
+ model=models.Model(name=model, provider=provider_config),
67
+ tools=tools or None,
68
+ system_prompt=system_prompt,
69
+ max_steps=max_steps,
70
+ )
71
+
72
+
73
+ @app.command("run")
74
+ def run(
75
+ ctx: typer.Context,
76
+ session_id: SessionID,
77
+ prompt: Annotated[
78
+ str,
79
+ typer.Option(
80
+ "--prompt",
81
+ "-p",
82
+ help="The user prompt driving this invocation.",
83
+ ),
84
+ ],
85
+ agent_name: Annotated[
86
+ str,
87
+ typer.Option(
88
+ "--agent-name",
89
+ help='Name identifying the agent (e.g. "support-triage").',
90
+ ),
91
+ ],
92
+ model: Annotated[
93
+ str | None,
94
+ typer.Option(
95
+ "--model", help='Model identifier (e.g. "gemini-3.6-flash").'
96
+ ),
97
+ ] = None,
98
+ provider: Annotated[
99
+ str | None,
100
+ typer.Option("--provider", help='Provider name (e.g. "gemini").'),
101
+ ] = None,
102
+ credential: Annotated[
103
+ str | None,
104
+ typer.Option(
105
+ "--credential",
106
+ help="Secret reference the provider authenticates with "
107
+ '(e.g. "albus.sh/secrets/my-key").',
108
+ ),
109
+ ] = None,
110
+ system_prompt: Annotated[
111
+ str | None,
112
+ typer.Option(
113
+ "--system-prompt", help="System instructions for the model."
114
+ ),
115
+ ] = None,
116
+ tools: Annotated[
117
+ list[str] | None,
118
+ typer.Option(
119
+ "--tool",
120
+ help="Tool the model may call. Repeat to allow several.",
121
+ ),
122
+ ] = None,
123
+ max_steps: Annotated[
124
+ int | None,
125
+ typer.Option(
126
+ "--max-steps", help="Max model steps before the run stops."
127
+ ),
128
+ ] = None,
129
+ agent_file: Annotated[
130
+ Path | None,
131
+ typer.Option(
132
+ "--agent-file",
133
+ exists=True,
134
+ dir_okay=False,
135
+ help="JSON file holding the whole agent configuration, for "
136
+ "configurations the flags do not cover (e.g. MCP servers).",
137
+ ),
138
+ ] = None,
139
+ idempotency_key: Annotated[
140
+ str | None,
141
+ typer.Option(
142
+ "--idempotency-key",
143
+ help="Identifies this invocation so the call is retry-safe.",
144
+ ),
145
+ ] = None,
146
+ wait: Annotated[
147
+ bool,
148
+ typer.Option(
149
+ "--wait/--no-wait",
150
+ help="Block until the assistant response is available.",
151
+ ),
152
+ ] = True,
153
+ wait_timeout: Annotated[
154
+ int | None,
155
+ typer.Option(
156
+ "--wait-timeout",
157
+ help="Seconds to block while waiting. Omit to wait indefinitely.",
158
+ ),
159
+ ] = None,
160
+ ) -> None:
161
+ """Run or resume a session."""
162
+ agent = agent_config(
163
+ agent_file,
164
+ model,
165
+ provider,
166
+ credential,
167
+ system_prompt,
168
+ tools or [],
169
+ max_steps,
170
+ )
171
+ opts = options(ctx)
172
+ # A waiting run long-polls, so it outlives the request timeout.
173
+ albus = client(opts.base_url, None if wait else opts.timeout)
174
+ response = albus.sessions.run_session(
175
+ id=session_id,
176
+ user_prompt=prompt,
177
+ agent_name=agent_name,
178
+ agent=agent,
179
+ idempotency_key=idempotency_key,
180
+ wait=wait,
181
+ wait_timeout=wait_timeout,
182
+ )
183
+ result = response.result.model_dump(mode="json", exclude_none=True)
184
+ result["idempotency_key"] = response.headers["idempotency-key"][0]
185
+ emit(result)
186
+
187
+
188
+ @app.command("list")
189
+ def list_sessions(ctx: typer.Context) -> None:
190
+ """List all sessions."""
191
+ emit(sdk(ctx).sessions.list_sessions())
192
+
193
+
194
+ @app.command("get")
195
+ def get(
196
+ ctx: typer.Context,
197
+ session_id: SessionID,
198
+ after: After = None,
199
+ limit: Limit = 100,
200
+ ) -> None:
201
+ """Get a session with a page of its messages."""
202
+ emit(sdk(ctx).sessions.get_session(id=session_id, after=after, limit=limit))
203
+
204
+
205
+ @app.command("audit")
206
+ def audit(
207
+ ctx: typer.Context,
208
+ session_id: SessionID,
209
+ after: After = None,
210
+ limit: Limit = 100,
211
+ ) -> None:
212
+ """List a page of a session's audit log."""
213
+ emit(
214
+ sdk(ctx).sessions.get_session_audit(
215
+ id=session_id, after=after, limit=limit
216
+ )
217
+ )
218
+
219
+
220
+ @app.command("delete")
221
+ def delete(ctx: typer.Context, session_id: SessionID) -> None:
222
+ """Delete a session."""
223
+ sdk(ctx).sessions.delete_session(id=session_id)
albus_cli/context.py ADDED
@@ -0,0 +1,24 @@
1
+ """Global options shared by every command."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import cast
5
+
6
+ import typer
7
+ from albus_sdk import Albus
8
+
9
+ from albus_cli.client import client
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Options:
14
+ base_url: str | None
15
+ timeout: float
16
+
17
+
18
+ def options(ctx: typer.Context) -> Options:
19
+ return cast(Options, ctx.obj)
20
+
21
+
22
+ def sdk(ctx: typer.Context) -> Albus:
23
+ opts = options(ctx)
24
+ return client(opts.base_url, opts.timeout)
albus_cli/main.py ADDED
@@ -0,0 +1,63 @@
1
+ """`albus` — command-line client for the Albus REST API."""
2
+
3
+ from typing import Annotated
4
+
5
+ import httpx
6
+ import typer
7
+ from albus_sdk import errors
8
+
9
+ from albus_cli.client import BASE_URL_ENV, MissingAPIKey
10
+ from albus_cli.commands import agents, secrets, sessions
11
+ from albus_cli.context import Options, sdk
12
+ from albus_cli.output import emit
13
+
14
+ app = typer.Typer(
15
+ no_args_is_help=True,
16
+ add_completion=False,
17
+ help="Command-line client for the Albus REST API. Authenticates with "
18
+ "the API key in ALBUS_API_KEY and prints JSON responses.",
19
+ )
20
+ app.add_typer(sessions.app, name="sessions")
21
+ app.add_typer(secrets.app, name="secrets")
22
+ app.add_typer(agents.app, name="agents")
23
+
24
+
25
+ @app.callback()
26
+ def configure(
27
+ ctx: typer.Context,
28
+ base_url: Annotated[
29
+ str | None,
30
+ typer.Option(
31
+ "--base-url",
32
+ envvar=BASE_URL_ENV,
33
+ help="Albus API base URL. Defaults to production.",
34
+ ),
35
+ ] = None,
36
+ timeout: Annotated[
37
+ float,
38
+ typer.Option("--timeout", help="Request timeout in seconds."),
39
+ ] = 30.0,
40
+ ) -> None:
41
+ ctx.obj = Options(base_url=base_url, timeout=timeout)
42
+
43
+
44
+ @app.command("health")
45
+ def health(ctx: typer.Context) -> None:
46
+ """Check service availability."""
47
+ emit(sdk(ctx).health.health())
48
+
49
+
50
+ def main() -> None:
51
+ try:
52
+ app()
53
+ except MissingAPIKey as missing:
54
+ fail(str(missing))
55
+ except errors.AlbusError as error:
56
+ fail(f"{error.status_code}: {error.message}")
57
+ except httpx.HTTPError as transport:
58
+ fail(str(transport))
59
+
60
+
61
+ def fail(message: str) -> None:
62
+ typer.secho(f"albus: {message}", fg=typer.colors.RED, err=True)
63
+ raise SystemExit(1)
albus_cli/output.py ADDED
@@ -0,0 +1,16 @@
1
+ """Command output. Every command prints one pretty-printed JSON value."""
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel
7
+
8
+
9
+ def emit(value: BaseModel | list[Any] | dict[str, Any] | None) -> None:
10
+ if value is None:
11
+ return
12
+
13
+ if isinstance(value, BaseModel):
14
+ value = value.model_dump(mode="json", exclude_none=True)
15
+
16
+ print(json.dumps(value, indent=2))
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: albus-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for the Albus REST API
5
+ Project-URL: Homepage, https://github.com/albusgroup/albus-cli
6
+ Project-URL: Repository, https://github.com/albusgroup/albus-cli
7
+ Project-URL: Issues, https://github.com/albusgroup/albus-cli/issues
8
+ Author: Albus
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: albus-sdk==0.4.0
13
+ Requires-Dist: typer<0.28,>=0.26.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # albus CLI
17
+
18
+ Command-line client for the Albus REST API. Thin shell over the public
19
+ [`albus-sdk`](https://pypi.org/project/albus-sdk/) Python package: every
20
+ command maps to one API operation and prints JSON (pipe into `jq`).
21
+
22
+ ## Install
23
+
24
+ macOS / Linux:
25
+
26
+ ```bash
27
+ curl -fsSL https://raw.githubusercontent.com/albusgroup/albus-cli/master/install.sh | sh
28
+ ```
29
+
30
+ Windows (PowerShell):
31
+
32
+ ```powershell
33
+ irm https://raw.githubusercontent.com/albusgroup/albus-cli/master/install.ps1 | iex
34
+ ```
35
+
36
+ Pin a version:
37
+
38
+ ```bash
39
+ ALBUS_CLI_VERSION=0.1.0 curl -fsSL https://raw.githubusercontent.com/albusgroup/albus-cli/master/install.sh | sh
40
+ ```
41
+
42
+ The installer uses, in order: `uv tool install`, `pip install --user`, or
43
+ `pip` inside a conda environment. It does not download OS-specific Albus
44
+ binaries and does not check OS versions.
45
+
46
+ You can also install directly:
47
+
48
+ ```bash
49
+ uv tool install albus-cli
50
+ # or
51
+ pip install --user albus-cli
52
+ ```
53
+
54
+ ## Authentication
55
+
56
+ ```bash
57
+ export ALBUS_API_KEY=... # organization API key
58
+ export ALBUS_BASE_URL=... # optional; defaults to production
59
+ ```
60
+
61
+ `--base-url` overrides `ALBUS_BASE_URL`, and `--timeout` bounds each request
62
+ (a waiting `sessions run` long-polls and is exempt).
63
+
64
+ The API key is the only credential, so the CLI covers only the operations that
65
+ accept `apiKeyAuth`.
66
+
67
+ ## Commands
68
+
69
+ ```bash
70
+ albus health
71
+
72
+ albus sessions run my-session -p "summarize the incident" \
73
+ --agent-name support-triage --model gemini-3.6-flash \
74
+ --provider gemini --credential albus.sh/secrets/gemini-key
75
+ albus sessions list
76
+ albus sessions get my-session --limit 20
77
+ albus sessions audit my-session --after "$cursor"
78
+ albus sessions delete my-session
79
+
80
+ albus secrets list
81
+ albus secrets create gemini-key --value ...
82
+ albus secrets get gemini-key
83
+ albus secrets update gemini-key < value.txt
84
+ albus secrets delete gemini-key
85
+
86
+ albus agents list
87
+ albus agents get support-triage
88
+ albus agents revision support-triage "$revision"
89
+ ```
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ make install
95
+ make check # ruff, mypy --strict, pytest
96
+ ```
97
+
98
+ See [RELEASING.md](RELEASING.md) to publish a PyPI version.
@@ -0,0 +1,15 @@
1
+ albus_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ albus_cli/__main__.py,sha256=oJAekektwsWMaeZVHcR7YcFSHlrCloXQHP_j5AcfsH0,117
3
+ albus_cli/client.py,sha256=Q18fv9CvCK99VgNINHmkwrDypAlePMovtzUPuIDyLlc,818
4
+ albus_cli/context.py,sha256=xgIgwQ8-tkNxWEEVhcjplPgNgim5P3u91e_ur2s5UEE,460
5
+ albus_cli/main.py,sha256=Wlpj1Fn816eQ4HfhrOYv9WeIQk5c6fuEih2_YHRJ2OM,1682
6
+ albus_cli/output.py,sha256=ECRO5OPqF7PjgnvdLyv8amTKBrxMiS3SksGmmzhuIuc,396
7
+ albus_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ albus_cli/commands/agents.py,sha256=WuSXfbqyECvxikne60b9U7XBFcCcD-QNaS95MWSyA8o,980
9
+ albus_cli/commands/secrets.py,sha256=BJLt2bn_2-LWi-Xh5klhO2q1RHx6_PMMezIuUW0SfBY,1631
10
+ albus_cli/commands/sessions.py,sha256=IQZmKppSRAhcHUCvNboYOkw2L7vWAvl0imWjnsxvBlQ,6144
11
+ albus_cli-0.1.0.dist-info/METADATA,sha256=2dD0Oj8n-UszHSeanmDc_H4L1x6GFOx9b4besWDrj-c,2570
12
+ albus_cli-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ albus_cli-0.1.0.dist-info/entry_points.txt,sha256=9g6sVxZtR__IY9Nvk7aaLh8S6cxUWSxwyKHiQMEweG4,46
14
+ albus_cli-0.1.0.dist-info/licenses/LICENSE,sha256=DNMiLr-P9bczooicuwDQOU04-9XSjT0NWwAJhBmE1Bk,1062
15
+ albus_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ albus = albus_cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Albus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.