albus-cli 0.4.1__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.
- {albus_cli-0.4.1 → albus_cli-0.7.0}/AGENTS.md +13 -4
- {albus_cli-0.4.1 → albus_cli-0.7.0}/PKG-INFO +49 -10
- {albus_cli-0.4.1 → albus_cli-0.7.0}/README.md +47 -8
- {albus_cli-0.4.1 → albus_cli-0.7.0}/RELEASING.md +44 -8
- {albus_cli-0.4.1 → albus_cli-0.7.0}/pyproject.toml +2 -2
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/client.py +35 -10
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/auth.py +18 -3
- albus_cli-0.7.0/src/albus_cli/commands/billing.py +67 -0
- albus_cli-0.7.0/src/albus_cli/commands/invites.py +54 -0
- albus_cli-0.7.0/src/albus_cli/commands/memories.py +56 -0
- albus_cli-0.7.0/src/albus_cli/commands/organization.py +64 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/sessions.py +42 -14
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/status.py +2 -2
- albus_cli-0.7.0/src/albus_cli/commands/traces.py +131 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/context.py +9 -2
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/main.py +35 -12
- albus_cli-0.7.0/tests/conftest.py +366 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_auth.py +5 -8
- albus_cli-0.7.0/tests/test_billing.py +84 -0
- albus_cli-0.7.0/tests/test_bump_sdk.py +95 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_cli.py +58 -9
- albus_cli-0.7.0/tests/test_invites.py +98 -0
- albus_cli-0.7.0/tests/test_memories.py +74 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_models.py +1 -1
- albus_cli-0.7.0/tests/test_organization.py +59 -0
- albus_cli-0.7.0/tests/test_release_scripts.py +166 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_status.py +3 -1
- albus_cli-0.7.0/tests/test_traces.py +128 -0
- albus_cli-0.7.0/tools/bump-sdk +88 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tools/publish +32 -12
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tools/release_state.sh +37 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/uv.lock +5 -5
- albus_cli-0.4.1/src/albus_cli/commands/invites.py +0 -33
- albus_cli-0.4.1/tests/conftest.py +0 -194
- albus_cli-0.4.1/tests/test_invites.py +0 -62
- {albus_cli-0.4.1 → albus_cli-0.7.0}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/.github/pull_request_template.md +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/.github/workflows/close-pull-requests.yml +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/.gitignore +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/CONTRIBUTING.md +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/LICENSE +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/Makefile +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/SECURITY.md +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/install.ps1 +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/install.sh +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/pyrightconfig.json +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/setup.sh +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/__init__.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/__main__.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/__init__.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/agents.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/models.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/secrets.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/commands/tokens.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/credentials.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/docs.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/oauth.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/src/albus_cli/output.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/__init__.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_credentials.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_health.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_oauth.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tests/test_tokens.py +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tools/check +0 -0
- {albus_cli-0.4.1 → albus_cli-0.7.0}/tools/mirror-release +0 -0
|
@@ -22,13 +22,20 @@ Run everything from `clients/albus-cli/`. `uv` owns the virtualenv and
|
|
|
22
22
|
| `make test` | `pytest` |
|
|
23
23
|
| `make check` | all of the above |
|
|
24
24
|
| `./tools/check` | `make check`, and what a release runs |
|
|
25
|
+
| `./tools/bump-sdk <sdk-version> <cli-version>` | Pin a published SDK version and relock, uncommitted |
|
|
25
26
|
| `./tools/publish --dry-run <version>` | Validate a release without uploading |
|
|
26
27
|
| `./tools/publish [--testpypi] <version>` | Upload, after explicit authorization |
|
|
28
|
+
| `./tools/publish --non-interactive <version>` | Upload from GitHub Actions, where the dispatch is the authorization |
|
|
27
29
|
| `./tools/mirror-release [--dry-run] <version> <checkout>` | Copy the released tree to the public mirror |
|
|
28
30
|
|
|
29
|
-
See [`RELEASING.md`](RELEASING.md)
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
See [`RELEASING.md`](RELEASING.md) and
|
|
32
|
+
[`client-release-automation.md`](../../.agents/plans/client-release-automation.md).
|
|
33
|
+
Publishing is never implied by a merge: a version reaches PyPI only when it was
|
|
34
|
+
explicitly authorized, and published versions are immutable. The release
|
|
35
|
+
scripts also run in GitHub Actions, where `--non-interactive` and the
|
|
36
|
+
`GITHUB_REF` release-state check apply, both keyed on `GITHUB_ACTIONS`. That
|
|
37
|
+
keeps a local release on the prompt; it is not a boundary, since the token is
|
|
38
|
+
what authorizes an upload.
|
|
32
39
|
|
|
33
40
|
## Conventions
|
|
34
41
|
|
|
@@ -72,7 +79,9 @@ versions are immutable.
|
|
|
72
79
|
- The `albus-sdk` dependency is pinned to one version. The published CLI has
|
|
73
80
|
to name an SDK version that exists on PyPI, so the pin is bumped
|
|
74
81
|
deliberately in the pull request that needs the newer SDK — never floated to
|
|
75
|
-
whatever is latest at install time.
|
|
82
|
+
whatever is latest at install time. `tools/bump-sdk` makes that edit — the
|
|
83
|
+
pin, `uv.lock`, and the CLI's own version — and deliberately does not commit
|
|
84
|
+
it: a release opens it as a pull request for review.
|
|
76
85
|
- The public repository accepts issues, not pull requests. A user-reported bug
|
|
77
86
|
is fixed here and reaches them in the next release, which closes the issue
|
|
78
87
|
via a `Mirror-Issue: albusgroup/albus-cli#<n>` trailer on the fix's commit.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: albus-cli
|
|
3
|
-
Version: 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.
|
|
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
|
|
148
|
-
|
|
149
|
-
`
|
|
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
|
|
@@ -154,10 +193,10 @@ the wait server-side. The response carries `message` once the invocation has
|
|
|
154
193
|
answered, and omits it otherwise — with `--no-wait`, or when the wait ended
|
|
155
194
|
first; `albus sessions get` reads the message later.
|
|
156
195
|
|
|
157
|
-
Pass `--
|
|
158
|
-
effective `
|
|
159
|
-
the server — absent only if something between the
|
|
160
|
-
response header on a run that supplied no key.
|
|
196
|
+
Pass `--invocation-key` to name the invocation and make the run retry-safe.
|
|
197
|
+
The JSON output carries the effective `invocation_key`, whether supplied by
|
|
198
|
+
the caller or generated by the server — absent only if something between the
|
|
199
|
+
CLI and Albus strips the response header on a run that supplied no key.
|
|
161
200
|
|
|
162
201
|
`--agent-file` takes a JSON object shaped like the API's `AgentConfig` and
|
|
163
202
|
covers what the flags do not (MCP servers, provider URL overrides):
|
|
@@ -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
|
|
133
|
-
|
|
134
|
-
`
|
|
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
|
|
@@ -139,10 +178,10 @@ the wait server-side. The response carries `message` once the invocation has
|
|
|
139
178
|
answered, and omits it otherwise — with `--no-wait`, or when the wait ended
|
|
140
179
|
first; `albus sessions get` reads the message later.
|
|
141
180
|
|
|
142
|
-
Pass `--
|
|
143
|
-
effective `
|
|
144
|
-
the server — absent only if something between the
|
|
145
|
-
response header on a run that supplied no key.
|
|
181
|
+
Pass `--invocation-key` to name the invocation and make the run retry-safe.
|
|
182
|
+
The JSON output carries the effective `invocation_key`, whether supplied by
|
|
183
|
+
the caller or generated by the server — absent only if something between the
|
|
184
|
+
CLI and Albus strips the response header on a run that supplied no key.
|
|
146
185
|
|
|
147
186
|
`--agent-file` takes a JSON object shaped like the API's `AgentConfig` and
|
|
148
187
|
covers what the flags do not (MCP servers, provider URL overrides):
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Releasing the Albus CLI
|
|
2
2
|
|
|
3
|
-
A release
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
A release runs the scripts below from a clean, up-to-date `master` checkout of
|
|
4
|
+
the private Albus repository — by a maintainer locally, or by the dispatched
|
|
5
|
+
release workflow described in
|
|
6
|
+
[Releasing from GitHub Actions](#releasing-from-github-actions). Either way they
|
|
7
|
+
run `./tools/check`, upload to PyPI or TestPyPI, push the released tree to the
|
|
8
|
+
public mirror as a single tagged commit, and close the mirror issues the release
|
|
9
|
+
fixes. Nothing publishes on push, on tag, or on merge.
|
|
8
10
|
|
|
9
11
|
## One-time PyPI setup
|
|
10
12
|
|
|
@@ -29,6 +31,18 @@ Bump the `albus-sdk==…` pin in the same pull request when the CLI needs a newe
|
|
|
29
31
|
SDK, and release that SDK version first: the pin must name a version that is on
|
|
30
32
|
PyPI, or the published CLI is uninstallable.
|
|
31
33
|
|
|
34
|
+
`tools/bump-sdk` makes that edit:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
./tools/bump-sdk 0.11.0 0.2.0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
It rewrites the pin and `project.version`, relocks, and checks that `uv.lock`
|
|
41
|
+
resolved the SDK version you named — which is also how an SDK release that did
|
|
42
|
+
not actually reach PyPI is caught. It leaves the edit uncommitted, because the
|
|
43
|
+
edit is a pull request. The release workflow runs the same script and opens
|
|
44
|
+
that pull request itself.
|
|
45
|
+
|
|
32
46
|
## Validate
|
|
33
47
|
|
|
34
48
|
From `clients/albus-cli/`, on the branch that carries the version bump:
|
|
@@ -68,6 +82,27 @@ uv tool install albus-cli==0.2.0
|
|
|
68
82
|
albus --help
|
|
69
83
|
```
|
|
70
84
|
|
|
85
|
+
## Releasing from GitHub Actions
|
|
86
|
+
|
|
87
|
+
A release running in GitHub Actions passes `--non-interactive`, which skips the
|
|
88
|
+
confirmation prompt because the dispatch already authorized the upload: a
|
|
89
|
+
maintainer with write access typed the version in and turned `dry_run` off.
|
|
90
|
+
The flag is refused when `GITHUB_ACTIONS` is unset: a phrase a script can type
|
|
91
|
+
is not a confirmation, so a local release keeps the prompt.
|
|
92
|
+
Deciding that from the environment is a guard against skipping the prompt out
|
|
93
|
+
of habit, not a boundary — `UV_PUBLISH_TOKEN` is what authorizes an upload, and
|
|
94
|
+
a maintainer holding it can already publish.
|
|
95
|
+
|
|
96
|
+
Actions checks out a detached `HEAD` with no upstream configured, so there the
|
|
97
|
+
publisher proves the same invariant against `GITHUB_REF` instead — the ref must
|
|
98
|
+
be a branch, `origin` must have it, and it must point at the commit being
|
|
99
|
+
released. Uploads still only run from `master`.
|
|
100
|
+
|
|
101
|
+
The CLI is published by its own dispatch, after the pull request that bumps the
|
|
102
|
+
`albus-sdk` pin and the CLI version has been reviewed and merged: the pin has
|
|
103
|
+
to name an SDK version that is already on PyPI. See
|
|
104
|
+
[`client-release-automation.md`](../../.agents/plans/client-release-automation.md).
|
|
105
|
+
|
|
71
106
|
## Copy the release to the public mirror
|
|
72
107
|
|
|
73
108
|
```bash
|
|
@@ -91,9 +126,10 @@ and this one, and closes each issue with the version that carries the fix. A
|
|
|
91
126
|
reporter cannot see the private fix, so the release closing the issue is the
|
|
92
127
|
only signal they get.
|
|
93
128
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
credential
|
|
129
|
+
A maintainer's own `git` and `gh` credentials do the push and the issue
|
|
130
|
+
closing; in Actions they come from the release workflow's token. Pushing
|
|
131
|
+
`.github/workflows/` to the mirror needs a credential allowed to write workflow
|
|
132
|
+
files.
|
|
97
133
|
|
|
98
134
|
The checkout you pass is only read: the release commit is built in a throwaway
|
|
99
135
|
clone of it, so a rejected push — the likely first-release outcome, since the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "albus-cli"
|
|
7
|
-
version = "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.
|
|
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(
|
|
57
|
-
|
|
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(
|
|
76
|
+
return bearer_client(
|
|
77
|
+
base_url, timeout, organization, _current(base_url, stored)
|
|
78
|
+
)
|
|
69
79
|
|
|
70
80
|
|
|
71
|
-
def signed_in_client(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|