albus-cli 0.5.0__tar.gz → 0.7.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.
Files changed (66) hide show
  1. {albus_cli-0.5.0 → albus_cli-0.7.0}/PKG-INFO +45 -6
  2. {albus_cli-0.5.0 → albus_cli-0.7.0}/README.md +43 -4
  3. {albus_cli-0.5.0 → albus_cli-0.7.0}/pyproject.toml +2 -2
  4. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/client.py +35 -10
  5. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/auth.py +18 -3
  6. albus_cli-0.7.0/src/albus_cli/commands/billing.py +67 -0
  7. albus_cli-0.7.0/src/albus_cli/commands/invites.py +54 -0
  8. albus_cli-0.7.0/src/albus_cli/commands/memories.py +56 -0
  9. albus_cli-0.7.0/src/albus_cli/commands/organization.py +64 -0
  10. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/sessions.py +8 -2
  11. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/status.py +2 -2
  12. albus_cli-0.7.0/src/albus_cli/commands/traces.py +131 -0
  13. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/context.py +9 -2
  14. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/main.py +35 -12
  15. albus_cli-0.7.0/tests/conftest.py +366 -0
  16. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_auth.py +5 -8
  17. albus_cli-0.7.0/tests/test_billing.py +84 -0
  18. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_cli.py +49 -0
  19. albus_cli-0.7.0/tests/test_invites.py +98 -0
  20. albus_cli-0.7.0/tests/test_memories.py +74 -0
  21. albus_cli-0.7.0/tests/test_organization.py +59 -0
  22. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_status.py +3 -1
  23. albus_cli-0.7.0/tests/test_traces.py +128 -0
  24. {albus_cli-0.5.0 → albus_cli-0.7.0}/uv.lock +5 -5
  25. albus_cli-0.5.0/src/albus_cli/commands/invites.py +0 -33
  26. albus_cli-0.5.0/tests/conftest.py +0 -194
  27. albus_cli-0.5.0/tests/test_invites.py +0 -62
  28. {albus_cli-0.5.0 → albus_cli-0.7.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  29. {albus_cli-0.5.0 → albus_cli-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  30. {albus_cli-0.5.0 → albus_cli-0.7.0}/.github/pull_request_template.md +0 -0
  31. {albus_cli-0.5.0 → albus_cli-0.7.0}/.github/workflows/close-pull-requests.yml +0 -0
  32. {albus_cli-0.5.0 → albus_cli-0.7.0}/.gitignore +0 -0
  33. {albus_cli-0.5.0 → albus_cli-0.7.0}/AGENTS.md +0 -0
  34. {albus_cli-0.5.0 → albus_cli-0.7.0}/CONTRIBUTING.md +0 -0
  35. {albus_cli-0.5.0 → albus_cli-0.7.0}/LICENSE +0 -0
  36. {albus_cli-0.5.0 → albus_cli-0.7.0}/Makefile +0 -0
  37. {albus_cli-0.5.0 → albus_cli-0.7.0}/RELEASING.md +0 -0
  38. {albus_cli-0.5.0 → albus_cli-0.7.0}/SECURITY.md +0 -0
  39. {albus_cli-0.5.0 → albus_cli-0.7.0}/install.ps1 +0 -0
  40. {albus_cli-0.5.0 → albus_cli-0.7.0}/install.sh +0 -0
  41. {albus_cli-0.5.0 → albus_cli-0.7.0}/pyrightconfig.json +0 -0
  42. {albus_cli-0.5.0 → albus_cli-0.7.0}/setup.sh +0 -0
  43. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/__init__.py +0 -0
  44. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/__main__.py +0 -0
  45. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/__init__.py +0 -0
  46. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/agents.py +0 -0
  47. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/models.py +0 -0
  48. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/secrets.py +0 -0
  49. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/commands/tokens.py +0 -0
  50. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/credentials.py +0 -0
  51. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/docs.py +0 -0
  52. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/oauth.py +0 -0
  53. {albus_cli-0.5.0 → albus_cli-0.7.0}/src/albus_cli/output.py +0 -0
  54. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/__init__.py +0 -0
  55. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_bump_sdk.py +0 -0
  56. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_credentials.py +0 -0
  57. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_health.py +0 -0
  58. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_models.py +0 -0
  59. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_oauth.py +0 -0
  60. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_release_scripts.py +0 -0
  61. {albus_cli-0.5.0 → albus_cli-0.7.0}/tests/test_tokens.py +0 -0
  62. {albus_cli-0.5.0 → albus_cli-0.7.0}/tools/bump-sdk +0 -0
  63. {albus_cli-0.5.0 → albus_cli-0.7.0}/tools/check +0 -0
  64. {albus_cli-0.5.0 → albus_cli-0.7.0}/tools/mirror-release +0 -0
  65. {albus_cli-0.5.0 → albus_cli-0.7.0}/tools/publish +0 -0
  66. {albus_cli-0.5.0 → albus_cli-0.7.0}/tools/release_state.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: albus-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Command-line client for the Albus REST API
5
5
  Project-URL: Homepage, https://github.com/albusgroup/albus-cli
6
6
  Project-URL: Repository, https://github.com/albusgroup/albus-cli
@@ -9,7 +9,7 @@ Author: Albus
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.11
12
- Requires-Dist: albus-sdk==0.11.0
12
+ Requires-Dist: albus-sdk==0.14.0
13
13
  Requires-Dist: typer<0.28,>=0.26.8
14
14
  Description-Content-Type: text/markdown
15
15
 
@@ -78,7 +78,11 @@ export ALBUS_BASE_URL=http://localhost:8080/api # optional; defaults to prod
78
78
  ```
79
79
 
80
80
  `--base-url` overrides `ALBUS_BASE_URL`, and `--timeout` bounds each request
81
- (a waiting `sessions run` long-polls and is exempt).
81
+ (a waiting `sessions run` long-polls and is exempt). A user who belongs to
82
+ several organizations acts in the one joined first; `--org <organization id>`
83
+ (or `ALBUS_ORG`) selects another, and `albus whoami` lists them under
84
+ `organizations` with the selected one as `active_organization`. An API key is
85
+ bound to its organization and ignores `--org`.
82
86
 
83
87
  `albus login` opens a browser on this machine and listens on
84
88
  `127.0.0.1:8484-8487`; on a headless host, forward those ports or export
@@ -115,8 +119,12 @@ albus sessions run my-session -p "and the follow-up?" \
115
119
  albus sessions list
116
120
  albus sessions get my-session --limit 20
117
121
  albus sessions audit my-session --after "$cursor"
122
+ albus sessions cancel my-session
118
123
  albus sessions delete my-session
119
124
 
125
+ albus traces list --agent-name support-triage --status FAILED
126
+ albus traces get "$invocation_key" --no-payloads --attempts all
127
+
120
128
  albus secrets list
121
129
  albus secrets create gemini-key --value ... # or pipe the value on stdin
122
130
  albus secrets get gemini-key
@@ -132,11 +140,32 @@ albus tokens create ci
132
140
  albus tokens get "$id"
133
141
  albus tokens delete "$id"
134
142
 
143
+ albus invites list
135
144
  albus invites create teammate@example.com
145
+ albus invites revoke "$id"
146
+
147
+ albus organization get
148
+ albus organization rename "Albus Labs"
149
+ albus organization members
150
+ albus organization set-role "$user_id" admin
151
+ albus organization remove-member "$user_id"
152
+
153
+ albus billing balance
154
+ albus billing ledger --after "$cursor"
155
+ albus billing checkout 20 --success-url https://... --cancel-url https://...
136
156
 
137
157
  albus models list
158
+
159
+ albus memories groups
160
+ albus memories list --group team-a
161
+ albus memories delete "$id" --group team-a
162
+ albus memories delete-group --group team-a
138
163
  ```
139
164
 
165
+ `memories groups` lists the organization's memory groups with their active
166
+ counts; `memories list --group` reads one group's memories, newest first. Both
167
+ page with `--after` and `--limit`.
168
+
140
169
  `models list` gives the model names `sessions run --model` takes, each with
141
170
  the provider it runs on — which is the provider the `--credential` secret has
142
171
  to be for.
@@ -144,9 +173,19 @@ to be for.
144
173
  `tokens create` prints the only copy of the key value the API ever returns;
145
174
  `list` and `get` return metadata alone.
146
175
 
147
- `invites create` invites a person by email, and they get their own
148
- organization on first sign-in. `CreateInviteRequest`'s `role` and
149
- `organization_id` have no flags: one user is one organization for the beta.
176
+ `invites create` invites a person by email to join your active organization,
177
+ as a `member` unless `--role admin` says otherwise. `invites list` and
178
+ `invites revoke`, like the `organization` member commands, need the admin
179
+ role.
180
+
181
+ `billing checkout` starts a credit purchase and prints the payment page URL
182
+ to send the buyer to; like `tokens` and `invites`, it needs `albus login`
183
+ rather than an API key. `billing balance` and `billing ledger` take either.
184
+
185
+ `traces list` searches the last 31 days of invocations unless `--since` and
186
+ `--until` say otherwise; `traces get` reads one with a page of its spans, and
187
+ `--no-payloads` returns the shape without what each span was given and
188
+ produced.
150
189
 
151
190
  `sessions run` waits for the assistant response by default; pass `--no-wait`
152
191
  to return as soon as the invocation is accepted, or `--wait-timeout` to bound
@@ -63,7 +63,11 @@ export ALBUS_BASE_URL=http://localhost:8080/api # optional; defaults to prod
63
63
  ```
64
64
 
65
65
  `--base-url` overrides `ALBUS_BASE_URL`, and `--timeout` bounds each request
66
- (a waiting `sessions run` long-polls and is exempt).
66
+ (a waiting `sessions run` long-polls and is exempt). A user who belongs to
67
+ several organizations acts in the one joined first; `--org <organization id>`
68
+ (or `ALBUS_ORG`) selects another, and `albus whoami` lists them under
69
+ `organizations` with the selected one as `active_organization`. An API key is
70
+ bound to its organization and ignores `--org`.
67
71
 
68
72
  `albus login` opens a browser on this machine and listens on
69
73
  `127.0.0.1:8484-8487`; on a headless host, forward those ports or export
@@ -100,8 +104,12 @@ albus sessions run my-session -p "and the follow-up?" \
100
104
  albus sessions list
101
105
  albus sessions get my-session --limit 20
102
106
  albus sessions audit my-session --after "$cursor"
107
+ albus sessions cancel my-session
103
108
  albus sessions delete my-session
104
109
 
110
+ albus traces list --agent-name support-triage --status FAILED
111
+ albus traces get "$invocation_key" --no-payloads --attempts all
112
+
105
113
  albus secrets list
106
114
  albus secrets create gemini-key --value ... # or pipe the value on stdin
107
115
  albus secrets get gemini-key
@@ -117,11 +125,32 @@ albus tokens create ci
117
125
  albus tokens get "$id"
118
126
  albus tokens delete "$id"
119
127
 
128
+ albus invites list
120
129
  albus invites create teammate@example.com
130
+ albus invites revoke "$id"
131
+
132
+ albus organization get
133
+ albus organization rename "Albus Labs"
134
+ albus organization members
135
+ albus organization set-role "$user_id" admin
136
+ albus organization remove-member "$user_id"
137
+
138
+ albus billing balance
139
+ albus billing ledger --after "$cursor"
140
+ albus billing checkout 20 --success-url https://... --cancel-url https://...
121
141
 
122
142
  albus models list
143
+
144
+ albus memories groups
145
+ albus memories list --group team-a
146
+ albus memories delete "$id" --group team-a
147
+ albus memories delete-group --group team-a
123
148
  ```
124
149
 
150
+ `memories groups` lists the organization's memory groups with their active
151
+ counts; `memories list --group` reads one group's memories, newest first. Both
152
+ page with `--after` and `--limit`.
153
+
125
154
  `models list` gives the model names `sessions run --model` takes, each with
126
155
  the provider it runs on — which is the provider the `--credential` secret has
127
156
  to be for.
@@ -129,9 +158,19 @@ to be for.
129
158
  `tokens create` prints the only copy of the key value the API ever returns;
130
159
  `list` and `get` return metadata alone.
131
160
 
132
- `invites create` invites a person by email, and they get their own
133
- organization on first sign-in. `CreateInviteRequest`'s `role` and
134
- `organization_id` have no flags: one user is one organization for the beta.
161
+ `invites create` invites a person by email to join your active organization,
162
+ as a `member` unless `--role admin` says otherwise. `invites list` and
163
+ `invites revoke`, like the `organization` member commands, need the admin
164
+ role.
165
+
166
+ `billing checkout` starts a credit purchase and prints the payment page URL
167
+ to send the buyer to; like `tokens` and `invites`, it needs `albus login`
168
+ rather than an API key. `billing balance` and `billing ledger` take either.
169
+
170
+ `traces list` searches the last 31 days of invocations unless `--since` and
171
+ `--until` say otherwise; `traces get` reads one with a page of its spans, and
172
+ `--no-payloads` returns the shape without what each span was given and
173
+ produced.
135
174
 
136
175
  `sessions run` waits for the assistant response by default; pass `--no-wait`
137
176
  to return as soon as the invocation is accepted, or `--wait-timeout` to bound
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "albus-cli"
7
- version = "0.5.0"
7
+ version = "0.7.0"
8
8
  description = "Command-line client for the Albus REST API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,7 +13,7 @@ authors = [{ name = "Albus" }]
13
13
  dependencies = [
14
14
  # Pinned, not floated: the published CLI states the one SDK version it was
15
15
  # tested against, and bumping it is a reviewed edit made when releasing.
16
- "albus-sdk==0.11.0",
16
+ "albus-sdk==0.14.0",
17
17
  "typer>=0.26.8,<0.28",
18
18
  ]
19
19
 
@@ -18,6 +18,11 @@ from albus_cli.credentials import Credential
18
18
 
19
19
  API_KEY_ENV = "ALBUS_API_KEY"
20
20
  BASE_URL_ENV = "ALBUS_BASE_URL"
21
+ ORGANIZATION_ENV = "ALBUS_ORG"
22
+
23
+ # Selects among the signed-in user's organizations. An API key is bound to
24
+ # one already, and the server ignores the header for it.
25
+ ORGANIZATION_HEADER = "X-Albus-Organization"
21
26
 
22
27
  # Renew slightly early: an access token that outlives the request it is
23
28
  # sent on is not worth the 401.
@@ -53,22 +58,29 @@ def api_key() -> str | None:
53
58
  return os.environ.get(API_KEY_ENV)
54
59
 
55
60
 
56
- def client(base_url: str, timeout: float | None) -> Albus:
57
- """Build an SDK client. A timeout of None disables the read timeout."""
61
+ def client(
62
+ base_url: str, timeout: float | None, organization: str | None
63
+ ) -> Albus:
64
+ """Build an SDK client. A timeout of None disables the read timeout;
65
+ an organization selects which of the user's the requests act in."""
58
66
  key = api_key()
59
67
  if key:
60
68
  # The environment wins over a stored session deliberately, so CI
61
69
  # and agent harnesses inject a credential without touching disk.
62
- return _client(base_url, timeout, api_key=key)
70
+ return _client(base_url, timeout, organization, api_key=key)
63
71
 
64
72
  stored = credentials.load(base_url)
65
73
  if stored is None:
66
74
  raise _signed_out(base_url)
67
75
 
68
- return bearer_client(base_url, timeout, _current(base_url, stored))
76
+ return bearer_client(
77
+ base_url, timeout, organization, _current(base_url, stored)
78
+ )
69
79
 
70
80
 
71
- def signed_in_client(base_url: str, timeout: float | None) -> Albus:
81
+ def signed_in_client(
82
+ base_url: str, timeout: float | None, organization: str | None
83
+ ) -> Albus:
72
84
  """A client on the browser session. The operations Albus accepts
73
85
  only a human bearer token for use it directly: an API key is a 401
74
86
  there, and the environment winning would be a 401 nobody asked
@@ -81,7 +93,9 @@ def signed_in_client(base_url: str, timeout: float | None) -> Albus:
81
93
  "`albus login`."
82
94
  )
83
95
 
84
- return bearer_client(base_url, timeout, _current(base_url, stored))
96
+ return bearer_client(
97
+ base_url, timeout, organization, _current(base_url, stored)
98
+ )
85
99
 
86
100
 
87
101
  def public_client(base_url: str, timeout: float | None) -> Albus:
@@ -94,15 +108,20 @@ def public_client(base_url: str, timeout: float | None) -> Albus:
94
108
  sign-in changes."""
95
109
  global _used
96
110
  _used = None
97
- return _built(base_url, timeout)
111
+ return _built(base_url, timeout, None)
98
112
 
99
113
 
100
114
  def bearer_client(
101
- base_url: str, timeout: float | None, credential: Credential
115
+ base_url: str,
116
+ timeout: float | None,
117
+ organization: str | None,
118
+ credential: Credential,
102
119
  ) -> Albus:
103
120
  """A client on one credential, for `login` reading back the session
104
121
  it just stored rather than whatever precedence would pick."""
105
- return _client(base_url, timeout, access_token=credential.access_token)
122
+ return _client(
123
+ base_url, timeout, organization, access_token=credential.access_token
124
+ )
106
125
 
107
126
 
108
127
  def credential(tokens: oauth.Tokens, replacing: Credential | None) -> Credential:
@@ -206,6 +225,7 @@ def _shadowing(consequence: str) -> str | None:
206
225
  def _client(
207
226
  base_url: str,
208
227
  timeout: float | None,
228
+ organization: str | None,
209
229
  *,
210
230
  api_key: str | None = None,
211
231
  access_token: str | None = None,
@@ -215,6 +235,7 @@ def _client(
215
235
  return _built(
216
236
  base_url,
217
237
  timeout,
238
+ organization,
218
239
  api_key=api_key,
219
240
  access_token=access_token,
220
241
  )
@@ -223,15 +244,19 @@ def _client(
223
244
  def _built(
224
245
  base_url: str,
225
246
  timeout: float | None,
247
+ organization: str | None,
226
248
  *,
227
249
  api_key: str | None = None,
228
250
  access_token: str | None = None,
229
251
  ) -> Albus:
252
+ headers = {ORGANIZATION_HEADER: organization} if organization else {}
230
253
  return Albus(
231
254
  api_key=api_key,
232
255
  access_token=access_token,
233
256
  server_url=base_url,
234
- client=httpx.Client(follow_redirects=True, timeout=timeout),
257
+ client=httpx.Client(
258
+ follow_redirects=True, timeout=timeout, headers=headers
259
+ ),
235
260
  )
236
261
 
237
262
 
@@ -8,9 +8,10 @@ SDK operation: they talk to Auth0 rather than to Albus. See
8
8
  from typing import Annotated
9
9
 
10
10
  import typer
11
+ from albus_sdk import models
11
12
 
12
13
  from albus_cli import client, credentials, docs, oauth, output
13
- from albus_cli.context import base_url, sdk, timeout
14
+ from albus_cli.context import base_url, organization, sdk, timeout
14
15
 
15
16
  NoBrowser = Annotated[
16
17
  bool,
@@ -39,11 +40,13 @@ def login(ctx: typer.Context, no_browser: NoBrowser = False) -> None:
39
40
 
40
41
  # Read the account back through the session just stored, not through
41
42
  # precedence: naming the API key's account would be a false report.
42
- signed_in = client.bearer_client(api, timeout(ctx), session)
43
+ signed_in = client.bearer_client(
44
+ api, timeout(ctx), organization(ctx), session
45
+ )
43
46
  # A browser session authenticates a user, so `/whoami` names one;
44
47
  # an account it did not name is not one to claim under "Signed in".
45
48
  user = signed_in.auth.whoami().user
46
- output.done("Signed in" if user is None else f"Signed in as {user.email}")
49
+ output.done(_signed_in(user))
47
50
  output.field("API", api)
48
51
  output.field("Credential", output.abbreviated(credentials.path()))
49
52
  if session.refresh_token is None:
@@ -69,6 +72,18 @@ def whoami(ctx: typer.Context) -> None:
69
72
  output.emit(sdk(ctx).auth.whoami())
70
73
 
71
74
 
75
+ def _signed_in(user: models.AuthenticatedUser | None) -> str:
76
+ """The account and the organization its commands act in, as Albus
77
+ reports them for the session just stored."""
78
+ if user is None:
79
+ return "Signed in"
80
+
81
+ if user.active_organization is None:
82
+ return f"Signed in as {user.email}"
83
+
84
+ return f"Signed in as {user.email} in {user.active_organization.name}"
85
+
86
+
72
87
  def _next_steps() -> None:
73
88
  """The last lines a successful sign-in prints, where attention is
74
89
  highest: the one command that runs an agent, and the sentence to
@@ -0,0 +1,67 @@
1
+ """`albus billing` — the organization's prepaid credits.
2
+
3
+ `/billing/checkout` is `bearerAuth`-only: an API key cannot spend money, so
4
+ `checkout` takes the browser session with `signed_in_sdk`. Reading the
5
+ balance and the ledger accepts either credential.
6
+ """
7
+
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from albus_cli.context import sdk, signed_in_sdk
13
+ from albus_cli.output import emit
14
+
15
+ app = typer.Typer(no_args_is_help=True, help="Read and top up prepaid credits.")
16
+
17
+ After = Annotated[
18
+ str | None,
19
+ typer.Option("--after", help="Pagination cursor from a previous page."),
20
+ ]
21
+ Limit = Annotated[int, typer.Option("--limit", help="Page size.")]
22
+
23
+
24
+ @app.command("balance")
25
+ def balance(ctx: typer.Context) -> None:
26
+ """Read the organization's credit balance in USD."""
27
+ emit(sdk(ctx).billing.get_credit_balance())
28
+
29
+
30
+ @app.command("ledger")
31
+ def ledger(ctx: typer.Context, after: After = None, limit: Limit = 100) -> None:
32
+ """List the credit ledger, newest first."""
33
+ emit(sdk(ctx).billing.list_credit_ledger(after=after, limit=limit))
34
+
35
+
36
+ @app.command("checkout")
37
+ def checkout(
38
+ ctx: typer.Context,
39
+ amount_usd: Annotated[
40
+ int,
41
+ typer.Argument(
42
+ metavar="AMOUNT_USD", help="Whole US dollars of credit to buy."
43
+ ),
44
+ ],
45
+ success_url: Annotated[
46
+ str,
47
+ typer.Option(
48
+ "--success-url",
49
+ help="Where the payment page sends the buyer after paying.",
50
+ ),
51
+ ],
52
+ cancel_url: Annotated[
53
+ str,
54
+ typer.Option(
55
+ "--cancel-url",
56
+ help="Where the payment page sends the buyer who backs out.",
57
+ ),
58
+ ],
59
+ ) -> None:
60
+ """Start a credit purchase and print the payment page URL."""
61
+ emit(
62
+ signed_in_sdk(ctx).billing.create_checkout(
63
+ amount_usd=amount_usd,
64
+ success_url=success_url,
65
+ cancel_url=cancel_url,
66
+ )
67
+ )
@@ -0,0 +1,54 @@
1
+ """`albus invites` — the pending invitations into the active organization.
2
+
3
+ `/organization/invites` is `bearerAuth`-only, like `/tokens`: an API key
4
+ cannot invite a user, so these commands take the browser session with
5
+ `signed_in_sdk`.
6
+ """
7
+
8
+ from typing import Annotated, Literal
9
+
10
+ import typer
11
+
12
+ from albus_cli.context import signed_in_sdk
13
+ from albus_cli.output import emit
14
+
15
+ app = typer.Typer(no_args_is_help=True, help="Invite users by email.")
16
+
17
+ Role = Literal["admin", "member"]
18
+
19
+ InviteID = Annotated[
20
+ str, typer.Argument(metavar="ID", help="The invitation ID.")
21
+ ]
22
+
23
+
24
+ @app.command("list")
25
+ def list_invites(ctx: typer.Context) -> None:
26
+ """List pending invitations. Requires the admin role."""
27
+ emit(signed_in_sdk(ctx).invites.list_invites())
28
+
29
+
30
+ @app.command("create")
31
+ def create(
32
+ ctx: typer.Context,
33
+ email: Annotated[
34
+ str,
35
+ typer.Argument(
36
+ metavar="EMAIL", help="Email address of the person to invite."
37
+ ),
38
+ ],
39
+ role: Annotated[
40
+ Role | None,
41
+ typer.Option(
42
+ "--role",
43
+ help="Role the invitee joins with. Defaults to member.",
44
+ ),
45
+ ] = None,
46
+ ) -> None:
47
+ """Invite a user to join your active organization."""
48
+ emit(signed_in_sdk(ctx).invites.create_invite(email=email, role=role))
49
+
50
+
51
+ @app.command("revoke")
52
+ def revoke(ctx: typer.Context, invite_id: InviteID) -> None:
53
+ """Revoke a pending invitation. Requires the admin role."""
54
+ signed_in_sdk(ctx).invites.revoke_invite(id=invite_id)
@@ -0,0 +1,56 @@
1
+ """`albus memories` — the memory groups agents share, and what they hold."""
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(
11
+ no_args_is_help=True, help="List and delete memory groups and memories."
12
+ )
13
+
14
+ Group = Annotated[
15
+ str,
16
+ typer.Option("--group", help="Memory group key."),
17
+ ]
18
+ After = Annotated[
19
+ str | None,
20
+ typer.Option("--after", help="Pagination cursor from a previous page."),
21
+ ]
22
+ Limit = Annotated[int, typer.Option("--limit", help="Page size.")]
23
+
24
+
25
+ @app.command("groups")
26
+ def list_groups(
27
+ ctx: typer.Context, after: After = None, limit: Limit = 100
28
+ ) -> None:
29
+ """List the organization's memory groups."""
30
+ emit(sdk(ctx).memories.list_memory_groups(after=after, limit=limit))
31
+
32
+
33
+ @app.command("list")
34
+ def list_memories(
35
+ ctx: typer.Context, group: Group, after: After = None, limit: Limit = 100
36
+ ) -> None:
37
+ """List a group's memories, newest first."""
38
+ emit(sdk(ctx).memories.list_memories(group=group, after=after, limit=limit))
39
+
40
+
41
+ @app.command("delete")
42
+ def delete(
43
+ ctx: typer.Context,
44
+ memory_id: Annotated[
45
+ str, typer.Argument(metavar="ID", help="The memory ID.")
46
+ ],
47
+ group: Group,
48
+ ) -> None:
49
+ """Delete one memory of a group, permanently."""
50
+ sdk(ctx).memories.delete_memory(group=group, id=memory_id)
51
+
52
+
53
+ @app.command("delete-group")
54
+ def delete_group(ctx: typer.Context, group: Group) -> None:
55
+ """Delete every memory of a group."""
56
+ sdk(ctx).memories.delete_memory_group(group=group)
@@ -0,0 +1,64 @@
1
+ """`albus organization` — the organization the commands act in, and its
2
+ members."""
3
+
4
+ from typing import Annotated, Literal
5
+
6
+ import typer
7
+
8
+ from albus_cli.context import sdk
9
+ from albus_cli.output import emit
10
+
11
+ app = typer.Typer(
12
+ no_args_is_help=True, help="Manage the organization and its members."
13
+ )
14
+
15
+ Role = Literal["admin", "member"]
16
+
17
+ UserID = Annotated[
18
+ str, typer.Argument(metavar="USER_ID", help="The member's user ID.")
19
+ ]
20
+
21
+
22
+ @app.command("get")
23
+ def get(ctx: typer.Context) -> None:
24
+ """Get the organization."""
25
+ emit(sdk(ctx).organization.get_organization())
26
+
27
+
28
+ @app.command("rename")
29
+ def rename(
30
+ ctx: typer.Context,
31
+ name: Annotated[
32
+ str, typer.Argument(metavar="NAME", help="New display name.")
33
+ ],
34
+ ) -> None:
35
+ """Rename the organization. Requires the admin role."""
36
+ emit(sdk(ctx).organization.update_organization(name=name))
37
+
38
+
39
+ @app.command("members")
40
+ def members(ctx: typer.Context) -> None:
41
+ """List the organization's members. Requires the admin role."""
42
+ emit(sdk(ctx).organization.list_organization_members())
43
+
44
+
45
+ @app.command("set-role")
46
+ def set_role(
47
+ ctx: typer.Context,
48
+ user_id: UserID,
49
+ role: Annotated[
50
+ Role, typer.Argument(metavar="ROLE", help="admin or member.")
51
+ ],
52
+ ) -> None:
53
+ """Set a member's role. Requires the admin role."""
54
+ emit(
55
+ sdk(ctx).organization.set_organization_member_role(
56
+ user_id=user_id, role=role
57
+ )
58
+ )
59
+
60
+
61
+ @app.command("remove-member")
62
+ def remove_member(ctx: typer.Context, user_id: UserID) -> None:
63
+ """Remove a member from the organization. Requires the admin role."""
64
+ sdk(ctx).organization.remove_organization_member(user_id=user_id)
@@ -11,7 +11,7 @@ from albus_sdk import models
11
11
  from pydantic import ValidationError
12
12
 
13
13
  from albus_cli.client import client
14
- from albus_cli.context import base_url, options, sdk
14
+ from albus_cli.context import base_url, options, organization, sdk
15
15
  from albus_cli.output import emit
16
16
 
17
17
  app = typer.Typer(no_args_is_help=True, help="Run and inspect sessions.")
@@ -215,7 +215,7 @@ def run(
215
215
  )
216
216
  timeout = options(ctx).timeout
217
217
  # A waiting invocation long-polls, so it outlives the request timeout.
218
- albus = client(base_url(ctx), None if wait else timeout)
218
+ albus = client(base_url(ctx), None if wait else timeout, organization(ctx))
219
219
  response = albus.sessions.run_session(
220
220
  id=session_id,
221
221
  user_prompt=prompt,
@@ -270,6 +270,12 @@ def audit(
270
270
  )
271
271
 
272
272
 
273
+ @app.command("cancel")
274
+ def cancel(ctx: typer.Context, session_id: SessionID) -> None:
275
+ """Cancel the invocation running for a session."""
276
+ emit(sdk(ctx).sessions.cancel_session(id=session_id))
277
+
278
+
273
279
  @app.command("delete")
274
280
  def delete(ctx: typer.Context, session_id: SessionID) -> None:
275
281
  """Delete a session."""
@@ -15,7 +15,7 @@ import typer
15
15
  from albus_sdk import errors
16
16
 
17
17
  from albus_cli import client, credentials, oauth, output
18
- from albus_cli.context import base_url, timeout
18
+ from albus_cli.context import base_url, organization, timeout
19
19
 
20
20
  # Everything that answers "the credential does not work": Albus refusing it, a
21
21
  # stored session that cannot be renewed, an Auth0 tenant that is unconfigured
@@ -77,7 +77,7 @@ def _accepted(ctx: typer.Context, api: str) -> dict[str, Any]:
77
77
  principal — the signed-in user, or the API key and the organization
78
78
  it acts in — so one operation both verifies the credential the next
79
79
  command sends and reports who it belongs to."""
80
- caller = client.client(api, timeout(ctx)).auth.whoami()
80
+ caller = client.client(api, timeout(ctx), organization(ctx)).auth.whoami()
81
81
  return {
82
82
  "authenticated": True,
83
83
  "caller": caller.model_dump(mode="json", exclude_none=True),