pipecatcloud 1.0.1__tar.gz → 1.2.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.
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/CHANGELOG.md +189 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/PKG-INFO +1 -1
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pyproject.toml +1 -1
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/auth_utils.py +3 -2
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/console_utils.py +193 -5
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/deploy_utils.py +326 -0
- pipecatcloud-1.2.0/src/pipecatcloud/_utils/github_utils.py +129 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/regions.py +21 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/api.py +412 -5
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/agent.py +496 -46
- pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/agent_profiles.py +232 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/auth.py +47 -28
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/build.py +67 -11
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/deploy.py +285 -90
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/docker.py +20 -11
- pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/github.py +307 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/organizations.py +154 -53
- pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/regions.py +301 -0
- pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/registry_keys.py +161 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/secrets.py +167 -51
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/spend_limit.py +31 -16
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/config.py +3 -1
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/entry_point.py +26 -2
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/config.py +12 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/SOURCES.txt +17 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/scm_file_list.json +58 -41
- pipecatcloud-1.2.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
- pipecatcloud-1.2.0/tests/conftest.py +47 -0
- pipecatcloud-1.2.0/tests/test_agent_profiles_and_resources.py +281 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_sessions.py +17 -21
- pipecatcloud-1.2.0/tests/test_agent_status_output.py +64 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_stop.py +17 -12
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_api_client.py +62 -0
- pipecatcloud-1.2.0/tests/test_build_id_display.py +58 -0
- pipecatcloud-1.2.0/tests/test_deploy_architecture.py +63 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deploy_image_pull_secret.py +13 -18
- pipecatcloud-1.2.0/tests/test_deploy_referenced_secret_set.py +118 -0
- pipecatcloud-1.2.0/tests/test_exit_codes.py +111 -0
- pipecatcloud-1.2.0/tests/test_github_integration.py +934 -0
- pipecatcloud-1.2.0/tests/test_json_output.py +226 -0
- pipecatcloud-1.2.0/tests/test_organizations.py +103 -0
- pipecatcloud-1.2.0/tests/test_output_modes.py +179 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_pat_auth.py +7 -2
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_regions.py +3 -1
- pipecatcloud-1.2.0/tests/test_regions_lifecycle.py +274 -0
- pipecatcloud-1.2.0/tests/test_secrets_reference.py +91 -0
- pipecatcloud-1.2.0/tests/test_secrets_set_referenced.py +120 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_spend_limit.py +53 -17
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/uv.lock +201 -193
- pipecatcloud-1.0.1/src/pipecatcloud/cli/commands/regions.py +0 -40
- pipecatcloud-1.0.1/src/pipecatcloud.egg-info/scm_version.json +0 -8
- pipecatcloud-1.0.1/tests/conftest.py +0 -17
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.claude/settings.json +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/format.yml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/publish-pypi.yml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/publish-test.yml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/python-compatibility.yml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/tests.yml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.gitignore +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.pre-commit-config.yaml +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/CLAUDE.md +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/LICENSE +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/README.md +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pipecat-cloud.png +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pytest.ini +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/scripts/fix-ruff.sh +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/setup.cfg +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/__init__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/__version__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/__init__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/agent.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/__init__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/api.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/constants.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/exception.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/py.typed +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/session.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/__init__.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_args.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_auth.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_build_utils.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_config_file.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deploy_poll.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deployment_status.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_docker_commands.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_krisp_viva.py +0 -0
- {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_max_session_duration.py +0 -0
|
@@ -5,6 +5,195 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **GitHub integration**, at parity with the dashboard. Connect an
|
|
13
|
+
organization with `pipecat cloud github connect`: it opens the GitHub App
|
|
14
|
+
install page and waits while the install completes, so there is nothing to
|
|
15
|
+
copy back into the terminal. `github status` reports the connected account
|
|
16
|
+
and whether the App is suspended, `github disconnect` removes the
|
|
17
|
+
installation and every repository link under it, and `github repos` /
|
|
18
|
+
`github branches <owner/repo>` list what the App can see (`--query` runs the
|
|
19
|
+
server-side branch search for repositories with more branches than the list
|
|
20
|
+
returns).
|
|
21
|
+
|
|
22
|
+
- Agents can be built from a repository instead of an image. `pipecat cloud
|
|
23
|
+
deploy --repo owner/repo --branch main` creates a GitHub-sourced agent,
|
|
24
|
+
skipping the image build and push entirely, and follows the first deploy
|
|
25
|
+
through its build; a `[git]` section in `pcc-deploy.toml` (`repo`, `branch`,
|
|
26
|
+
`dockerfile_path`, `subdirectory`) does the same, with flags taking
|
|
27
|
+
precedence. An existing agent's repository is changed with `pipecat cloud
|
|
28
|
+
agent link <name> --repo owner/repo --branch main` (also `--dockerfile-path`,
|
|
29
|
+
`--subdirectory` and `--no-auto-deploy`), and removed with `agent unlink`.
|
|
30
|
+
Linking never changes what is running.
|
|
31
|
+
|
|
32
|
+
- `pipecat cloud agent deploy <name> --github` builds and deploys the linked
|
|
33
|
+
branch's current HEAD without waiting for a push, printing the deploy intent
|
|
34
|
+
and commit. `--wait` follows the deploy and reports one of four outcomes: it
|
|
35
|
+
succeeded, it failed, a newer push superseded it (naming the newer commit),
|
|
36
|
+
or it was still building when the wait ran out. Only an observed failure and
|
|
37
|
+
a deploy that was never visible at all exit non-zero, so a green `--wait`
|
|
38
|
+
means the deploy was observed and did not fail, never merely that the wait
|
|
39
|
+
finished. In `--output json` the outcome is reported as `waitOutcome`.
|
|
40
|
+
|
|
41
|
+
- `pipecat cloud agent list` gains a GitHub column, and `agent status` reports
|
|
42
|
+
the linked repository and branch, the Dockerfile path, whether pushes
|
|
43
|
+
auto-deploy, the commit actually running, and the latest deploy attempt with
|
|
44
|
+
its failure reason. A running commit that did not come from the current link
|
|
45
|
+
(a freshly linked agent, or one whose repository or branch was re-pointed)
|
|
46
|
+
says so and names where it did come from. `pipecat cloud build status` and
|
|
47
|
+
`build list` show the repository, ref and commit a build came from.
|
|
48
|
+
|
|
49
|
+
- Architecture selection on deploy: `pipecat cloud deploy --architecture
|
|
50
|
+
{amd64,arm64}` or an `architecture` key in `pcc-deploy.toml` (the flag
|
|
51
|
+
wins). Omitted, the region's default applies — set it when your image is
|
|
52
|
+
built for one architecture, so it schedules on matching nodes instead of
|
|
53
|
+
failing at startup. The choice is validated against the target region's
|
|
54
|
+
supported architectures before anything uploads, when the API advertises
|
|
55
|
+
them. `pipecat cloud regions list` shows each region's supported and
|
|
56
|
+
default architectures (Daily-hosted regions are arm64-only today), and
|
|
57
|
+
`pipecat cloud agent status` shows the deployment's architecture.
|
|
58
|
+
|
|
59
|
+
- Region lifecycle commands for **self-hosted regions**. `pipecat cloud
|
|
60
|
+
regions register <key>` registers or updates a region (an omit-preserves
|
|
61
|
+
upsert: options you leave out keep their stored values), with a one-question
|
|
62
|
+
display-name prompt so region pickers show a friendly name instead of the
|
|
63
|
+
uppercased key. `regions show <key>` prints the full record, including the
|
|
64
|
+
enrollment status and intermediate certificate expiry. `regions delete
|
|
65
|
+
<key>` revokes a region, surfacing the server's refusal while live sessions
|
|
66
|
+
or deployed services exist; that guard has no bypass, so delete the region's
|
|
67
|
+
agents first, and `--yes` only skips the confirmation prompt.
|
|
68
|
+
`regions enroll-token <key>` mints the one-time enrollment token and
|
|
69
|
+
prints the ready-to-paste `kubectl create secret` command.
|
|
70
|
+
|
|
71
|
+
- New `pipecat cloud organizations registry-keys` command group for the
|
|
72
|
+
pull-only registry credentials that fetch the region package chart:
|
|
73
|
+
`mint` (the key is shown exactly once, with the matching
|
|
74
|
+
`helm registry login` command), `list` (lifecycle metadata only, never key
|
|
75
|
+
material), and `revoke`. With these plus `regions register` and
|
|
76
|
+
`regions enroll-token`, a self-hosted region's control-plane setup needs no
|
|
77
|
+
raw API calls.
|
|
78
|
+
|
|
79
|
+
- Explicit agent sizing for **enterprise (self-hosted) regions**. Deploys can
|
|
80
|
+
now state resources directly instead of selecting an agent profile:
|
|
81
|
+
`pipecat cloud deploy --resources cpu=2,memory=4Gi`, or a `[resources]`
|
|
82
|
+
section in `pcc-deploy.toml` with `cpu` and `memory` keys. Explicit
|
|
83
|
+
resources and `--profile` are mutually exclusive, and the API accepts
|
|
84
|
+
explicit resources only for agents in self-hosted regions.
|
|
85
|
+
|
|
86
|
+
- `pipecat cloud agent status` now shows the agent's resolved resources
|
|
87
|
+
(cpu/memory) from the deployment. For agents deployed with explicit
|
|
88
|
+
resources rather than a profile, this is the sizing surface.
|
|
89
|
+
|
|
90
|
+
- New `pipecat cloud agent profiles` command group. `list` shows the platform
|
|
91
|
+
catalog plus any profiles your organization has defined; `create`, `update`,
|
|
92
|
+
`disable`, and `enable` manage organization-defined profiles for self-hosted
|
|
93
|
+
regions. Profile edits apply to future deploys only: running agents keep the
|
|
94
|
+
resources their deployment captured. Disabling is reversible: a disabled
|
|
95
|
+
profile can't be selected for new deploys until re-enabled.
|
|
96
|
+
|
|
97
|
+
- New `pipecat cloud secrets reference <name> --region <region>` command for
|
|
98
|
+
**self-hosted regions**. It registers an existing Kubernetes Secret from your
|
|
99
|
+
workloads namespace as a secret set, so agents can use it by set name exactly
|
|
100
|
+
as they use managed sets, and the Kubernetes Secret's name becomes the secret
|
|
101
|
+
set name. The secret stays in your cluster: Pipecat Cloud records only its
|
|
102
|
+
name, region, and readiness, never its values or keys, and you keep managing
|
|
103
|
+
its contents with your own tooling such as `kubectl`. `--region` is required
|
|
104
|
+
and must name a self-hosted region; cloud regions are rejected.
|
|
105
|
+
|
|
106
|
+
### Fixed
|
|
107
|
+
|
|
108
|
+
- `pipecat cloud deploy` no longer rejects a referenced secret set as "not
|
|
109
|
+
found". The existence check read the set's array of key names and tested it
|
|
110
|
+
for truthiness, but referenced sets carry no key names by design (Daily
|
|
111
|
+
stores name, region and readiness, never the contents), so every healthy
|
|
112
|
+
referenced set failed the check and no self-hosted deploy could use one. The
|
|
113
|
+
check now fetches the set itself; readiness is still enforced server-side.
|
|
114
|
+
|
|
115
|
+
- `pipecat cloud secrets set` no longer treats an existing referenced secret
|
|
116
|
+
set as nonexistent. It had the same key-names misread as the deploy guard,
|
|
117
|
+
so it announced "Creating secret set" and then failed on the server's
|
|
118
|
+
name-uniqueness check. It now refuses before asking for confirmation,
|
|
119
|
+
pointing at the cluster as the place a referenced set's contents are
|
|
120
|
+
managed.
|
|
121
|
+
|
|
122
|
+
- `pipecat cloud agent status` plain output (`--output plain`, or any piped
|
|
123
|
+
invocation) now includes the Architecture and Resources rows. Both were
|
|
124
|
+
rendered only in the rich table, so scripted output silently omitted the
|
|
125
|
+
deployment's architecture and, for agents sized with explicit resources,
|
|
126
|
+
their only visible sizing.
|
|
127
|
+
|
|
128
|
+
## [1.1.0] - 2026-08-05
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- New global `--output {rich,plain,json}` option for every `pipecat cloud`
|
|
133
|
+
command, also settable via the `PIPECAT_OUTPUT` environment variable or the
|
|
134
|
+
`output` config key. Note the option is placed before the subcommand
|
|
135
|
+
(`pipecat cloud --output json agent list`); in CI, setting `PIPECAT_OUTPUT`
|
|
136
|
+
once avoids ordering concerns. `rich` is the interactive default; `plain` is
|
|
137
|
+
line-oriented with no boxes and no truncation; `json` emits one
|
|
138
|
+
machine-readable JSON object on stdout with all human output on stderr.
|
|
139
|
+
When stdout is not a terminal the CLI now defaults to `plain`.
|
|
140
|
+
|
|
141
|
+
- Progress is now visible when output is piped or redirected. Long-running
|
|
142
|
+
operations (deploy rollout polling, cloud builds) print each status
|
|
143
|
+
transition as a timestamped line instead of rendering nothing outside a
|
|
144
|
+
terminal. Cloud-build polling additionally reports build status changes.
|
|
145
|
+
|
|
146
|
+
- Listings (`agent list`, `agent sessions`, `agent deployments`,
|
|
147
|
+
`secrets list`, `organizations list`, `organizations keys list`, `build
|
|
148
|
+
list`, `regions list`, and friends) render as tab-separated rows with
|
|
149
|
+
full, untruncated values in `plain` mode. Piped output previously fell
|
|
150
|
+
back to an 80-column table that silently ellipsized IDs.
|
|
151
|
+
|
|
152
|
+
- `agent status` and `agent deployments` now show `Build: <build id>` for
|
|
153
|
+
cloud-built deployments instead of `Image: N/A`. Cloud builds produce an
|
|
154
|
+
image managed by Pipecat Cloud, so the build ID is the reference to use
|
|
155
|
+
when checking which artifact a deployment is running.
|
|
156
|
+
|
|
157
|
+
### Changed
|
|
158
|
+
|
|
159
|
+
- **The CLI now exits non-zero on failure.** Previously every error path
|
|
160
|
+
exited `0` — including failed deploys and running commands while logged
|
|
161
|
+
out — so CI pipelines could not detect failures. Errors and user
|
|
162
|
+
cancellations exit `1`; usage errors and refused non-interactive prompts
|
|
163
|
+
exit `2`. Pipelines that depended on the old always-zero behavior will
|
|
164
|
+
now correctly fail.
|
|
165
|
+
|
|
166
|
+
- Commands that would show a confirmation prompt now fail fast with exit
|
|
167
|
+
code `2` when stdin is not a terminal, pointing at the bypass flag
|
|
168
|
+
(`--yes`/`--force`/`--skip`), instead of hanging or misbehaving in CI.
|
|
169
|
+
|
|
170
|
+
- `organizations list` and `organizations keys create` label their
|
|
171
|
+
organization columns **Organization Name** (the display name) and
|
|
172
|
+
**Organization ID** (the identifier `--organization` takes), matching
|
|
173
|
+
the dashboard. The values are unchanged, as is `--output json`, which
|
|
174
|
+
emits the server's own field names. Scripts scraping the previous
|
|
175
|
+
`Organization` / `Name` headers out of table output need updating. In
|
|
176
|
+
`organizations list`, the active-organization marker moved to its own
|
|
177
|
+
`Active` column, so the ID column holds nothing but the identifier.
|
|
178
|
+
|
|
179
|
+
- CLI messages call an organization an "organization" rather than a
|
|
180
|
+
"namespace". The Kubernetes namespace an organization maps to is an
|
|
181
|
+
implementation detail, so messages about missing agents, secret sets,
|
|
182
|
+
or accounts now name the organization. `organizations select` reports
|
|
183
|
+
an unmatched `--organization` value as an unknown organization ID and
|
|
184
|
+
points at `organizations list`.
|
|
185
|
+
|
|
186
|
+
### Deprecated
|
|
187
|
+
|
|
188
|
+
- `spend-limit show --json` still works but is now an alias for the global
|
|
189
|
+
`--output json` mode.
|
|
190
|
+
|
|
191
|
+
### Fixed
|
|
192
|
+
|
|
193
|
+
- `organizations keys create` no longer reports a new key as "Using as
|
|
194
|
+
default in local config" when it was not stored as the default. The
|
|
195
|
+
panel carries that note only when the key actually became the default.
|
|
196
|
+
|
|
8
197
|
## [1.0.1] - 2026-07-13
|
|
9
198
|
|
|
10
199
|
### Fixed
|
|
@@ -41,7 +41,7 @@ dev = [
|
|
|
41
41
|
"pytest>=9.0.0,<10",
|
|
42
42
|
"pytest-asyncio>=1.0.0,<2",
|
|
43
43
|
"ruff>=0.12.11,<1",
|
|
44
|
-
"setuptools
|
|
44
|
+
"setuptools>=83.0.0,<84",
|
|
45
45
|
"setuptools_scm~=8.3.1",
|
|
46
46
|
# Needed to import/test pipecatcloud.agent (the optional pipecat surface).
|
|
47
47
|
"pipecat-ai>=1.0.0",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import functools
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
|
+
import typer
|
|
10
11
|
|
|
11
12
|
from pipecatcloud.__version__ import version
|
|
12
13
|
from pipecatcloud._utils.console_utils import console
|
|
@@ -46,7 +47,7 @@ def requires_login(func):
|
|
|
46
47
|
console.error(
|
|
47
48
|
f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
|
|
48
49
|
)
|
|
49
|
-
|
|
50
|
+
raise typer.Exit(1)
|
|
50
51
|
|
|
51
52
|
# When org is not set locally (e.g. PIPECAT_TOKEN without a config file),
|
|
52
53
|
# resolve the user's default organization from the API.
|
|
@@ -59,7 +60,7 @@ def requires_login(func):
|
|
|
59
60
|
"Could not determine your organization. "
|
|
60
61
|
f"Set PIPECAT_ORG or run `{PIPECAT_CLI_NAME} auth login`.",
|
|
61
62
|
)
|
|
62
|
-
|
|
63
|
+
raise typer.Exit(1)
|
|
63
64
|
|
|
64
65
|
return await func(*args, **kwargs)
|
|
65
66
|
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
5
|
#
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import statistics
|
|
9
|
+
import sys
|
|
8
10
|
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
9
12
|
|
|
10
13
|
from rich.console import Console
|
|
11
14
|
from rich.panel import Panel
|
|
@@ -13,6 +16,65 @@ from rich.panel import Panel
|
|
|
13
16
|
from pipecatcloud.cli import PANEL_TITLE_ERROR, PANEL_TITLE_SUCCESS, PIPECAT_CLI_NAME
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
class OutputMode(str, Enum):
|
|
20
|
+
"""How the CLI renders its output.
|
|
21
|
+
|
|
22
|
+
- ``rich``: panels, spinners, and tables (the interactive default)
|
|
23
|
+
- ``plain``: line-oriented output — no boxes, no spinners, no truncation
|
|
24
|
+
- ``json``: one machine-parseable JSON object on stdout; all human
|
|
25
|
+
output is redirected to stderr
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
rich = "rich"
|
|
29
|
+
plain = "plain"
|
|
30
|
+
json = "json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stdin_is_interactive() -> bool:
|
|
34
|
+
"""Whether we can prompt the user. Separate function so tests can patch it."""
|
|
35
|
+
try:
|
|
36
|
+
return sys.stdin.isatty()
|
|
37
|
+
except (AttributeError, ValueError):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _PlainStatus:
|
|
42
|
+
"""Drop-in replacement for rich's Status in non-interactive output.
|
|
43
|
+
|
|
44
|
+
Rich renders nothing from ``console.status(...)`` when stdout is not a
|
|
45
|
+
terminal, which left long-running operations (e.g. the up-to-600s deploy
|
|
46
|
+
poll loop) completely silent in CI. This sink prints each *distinct*
|
|
47
|
+
status message as a timestamped line instead, so piped output shows
|
|
48
|
+
every transition without spamming identical poll updates.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, console: "PipecatConsole", message: str | None = None):
|
|
52
|
+
self._console = console
|
|
53
|
+
self._last: str | None = None
|
|
54
|
+
self._stopped = False
|
|
55
|
+
if message:
|
|
56
|
+
self.update(message)
|
|
57
|
+
|
|
58
|
+
def update(self, status: str | None = None, **kwargs) -> None:
|
|
59
|
+
if not status or self._stopped or status == self._last:
|
|
60
|
+
return
|
|
61
|
+
self._last = status
|
|
62
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
63
|
+
self._console.print(rf"[dim]\[{timestamp}][/dim] {status}")
|
|
64
|
+
|
|
65
|
+
def start(self) -> None:
|
|
66
|
+
self._stopped = False
|
|
67
|
+
|
|
68
|
+
def stop(self) -> None:
|
|
69
|
+
self._stopped = True
|
|
70
|
+
|
|
71
|
+
def __enter__(self) -> "_PlainStatus":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def __exit__(self, *_exc) -> None:
|
|
75
|
+
self.stop()
|
|
76
|
+
|
|
77
|
+
|
|
16
78
|
def format_cents(cents: int | None) -> str:
|
|
17
79
|
"""Render a cents value as a USD dollar string (e.g. 1234 -> "$12.34").
|
|
18
80
|
|
|
@@ -28,6 +90,92 @@ def format_cents(cents: int | None) -> str:
|
|
|
28
90
|
|
|
29
91
|
|
|
30
92
|
class PipecatConsole(Console):
|
|
93
|
+
def __init__(self, *args, **kwargs):
|
|
94
|
+
super().__init__(*args, **kwargs)
|
|
95
|
+
# None means "auto": rich on a terminal, plain otherwise.
|
|
96
|
+
self._explicit_output_mode: OutputMode | None = None
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def output_mode(self) -> OutputMode:
|
|
100
|
+
if self._explicit_output_mode is not None:
|
|
101
|
+
return self._explicit_output_mode
|
|
102
|
+
return OutputMode.rich if self.is_terminal else OutputMode.plain
|
|
103
|
+
|
|
104
|
+
def set_output_mode(self, mode: OutputMode) -> None:
|
|
105
|
+
self._explicit_output_mode = mode
|
|
106
|
+
if mode == OutputMode.json:
|
|
107
|
+
# stdout is reserved for the single JSON payload; every human-facing
|
|
108
|
+
# write on this console moves to stderr so parsers see pure JSON.
|
|
109
|
+
self.file = sys.stderr
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def rich_output(self) -> bool:
|
|
113
|
+
return self.output_mode == OutputMode.rich
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def json_output(self) -> bool:
|
|
117
|
+
return self.output_mode == OutputMode.json
|
|
118
|
+
|
|
119
|
+
def output_json(self, data) -> None:
|
|
120
|
+
"""Write the single machine-readable JSON object to stdout.
|
|
121
|
+
|
|
122
|
+
In json mode this console's own writes go to stderr, so this is the
|
|
123
|
+
only thing that touches stdout — parsers get pure JSON.
|
|
124
|
+
"""
|
|
125
|
+
sys.stdout.write(json.dumps(data, indent=2, default=str) + "\n")
|
|
126
|
+
sys.stdout.flush()
|
|
127
|
+
|
|
128
|
+
def status(self, status, **kwargs): # pyright: ignore[reportIncompatibleMethodOverride]
|
|
129
|
+
# Returns rich's Status in rich mode and a line-printing _PlainStatus
|
|
130
|
+
# otherwise; both expose update()/stop()/context-manager.
|
|
131
|
+
if self.rich_output:
|
|
132
|
+
return super().status(status, **kwargs)
|
|
133
|
+
return _PlainStatus(self, str(status))
|
|
134
|
+
|
|
135
|
+
def require_interactive(self, flag_hint: str | None = "--yes") -> None:
|
|
136
|
+
"""Fail fast instead of letting a prompt hang or misbehave in CI.
|
|
137
|
+
|
|
138
|
+
Exits 2 (usage error) when a prompt would be shown but stdin is not a
|
|
139
|
+
terminal, pointing at the flag that skips the prompt when one exists.
|
|
140
|
+
"""
|
|
141
|
+
import typer
|
|
142
|
+
|
|
143
|
+
if not stdin_is_interactive():
|
|
144
|
+
remedy = (
|
|
145
|
+
f"Pass [bold]{flag_hint}[/bold] to run non-interactively."
|
|
146
|
+
if flag_hint
|
|
147
|
+
else "This command requires an interactive terminal."
|
|
148
|
+
)
|
|
149
|
+
self.error(f"Confirmation required but input is not a terminal.\n{remedy}")
|
|
150
|
+
raise typer.Exit(2)
|
|
151
|
+
|
|
152
|
+
def print(self, *objects, **kwargs):
|
|
153
|
+
# In non-rich modes, never hard-wrap plain text lines: wrapped IDs and
|
|
154
|
+
# URIs are as unparseable as truncated ones.
|
|
155
|
+
if not self.rich_output and objects and all(isinstance(o, str) for o in objects):
|
|
156
|
+
kwargs.setdefault("soft_wrap", True)
|
|
157
|
+
return super().print(*objects, **kwargs)
|
|
158
|
+
|
|
159
|
+
def print_records(
|
|
160
|
+
self,
|
|
161
|
+
columns: list[str],
|
|
162
|
+
rows: list[tuple],
|
|
163
|
+
title: str | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Plain-mode listing: tab-separated, full values, no boxes.
|
|
166
|
+
|
|
167
|
+
Rich tables ellipsize cell values at 80 columns when piped, which
|
|
168
|
+
silently corrupts IDs consumed by scripts. Rows are written straight to
|
|
169
|
+
the output stream — bypassing Rich's tab expansion and line wrapping —
|
|
170
|
+
so the output stays greppable and `cut`-able.
|
|
171
|
+
"""
|
|
172
|
+
if title:
|
|
173
|
+
self.print(title)
|
|
174
|
+
self.file.write("\t".join(columns) + "\n")
|
|
175
|
+
for row in rows:
|
|
176
|
+
self.file.write("\t".join("" if cell is None else str(cell) for cell in row) + "\n")
|
|
177
|
+
self.file.flush()
|
|
178
|
+
|
|
31
179
|
def success(
|
|
32
180
|
self,
|
|
33
181
|
message,
|
|
@@ -38,6 +186,16 @@ class PipecatConsole(Console):
|
|
|
38
186
|
if not title:
|
|
39
187
|
title = f"{PANEL_TITLE_SUCCESS}{f' - {title_extra}' if title_extra is not None else ''}"
|
|
40
188
|
|
|
189
|
+
if not self.rich_output:
|
|
190
|
+
if isinstance(message, str):
|
|
191
|
+
self.print(f"[bold green]{title}:[/bold green] {message}")
|
|
192
|
+
else:
|
|
193
|
+
self.print(f"[bold green]{title}[/bold green]")
|
|
194
|
+
self.print(message)
|
|
195
|
+
if subtitle:
|
|
196
|
+
self.print(subtitle)
|
|
197
|
+
return
|
|
198
|
+
|
|
41
199
|
self.print(
|
|
42
200
|
Panel(
|
|
43
201
|
message,
|
|
@@ -59,6 +217,16 @@ class PipecatConsole(Console):
|
|
|
59
217
|
if not title:
|
|
60
218
|
title = f"{PANEL_TITLE_ERROR}{f' - {title_extra}' if title_extra is not None else ''}"
|
|
61
219
|
|
|
220
|
+
if not self.rich_output:
|
|
221
|
+
if isinstance(message, str):
|
|
222
|
+
self.print(f"[bold red]{title}:[/bold red] {message}")
|
|
223
|
+
else:
|
|
224
|
+
self.print(f"[bold red]{title}[/bold red]")
|
|
225
|
+
self.print(message)
|
|
226
|
+
if subtitle:
|
|
227
|
+
self.print(subtitle)
|
|
228
|
+
return
|
|
229
|
+
|
|
62
230
|
self.print(
|
|
63
231
|
Panel(
|
|
64
232
|
message,
|
|
@@ -74,9 +242,16 @@ class PipecatConsole(Console):
|
|
|
74
242
|
self.print("[yellow]Cancelled by user[/yellow]")
|
|
75
243
|
|
|
76
244
|
def unauthorized(self):
|
|
245
|
+
message = (
|
|
246
|
+
"Unauthorized request / invalid user token.\n\n"
|
|
247
|
+
f"Please log in again using [bold cyan]{PIPECAT_CLI_NAME} auth login[/bold cyan]"
|
|
248
|
+
)
|
|
249
|
+
if not self.rich_output:
|
|
250
|
+
self.error(message, title=f"{PANEL_TITLE_ERROR} - Unauthorized (401)")
|
|
251
|
+
return
|
|
77
252
|
self.print(
|
|
78
253
|
Panel(
|
|
79
|
-
|
|
254
|
+
message,
|
|
80
255
|
title=f"[bold red]{PANEL_TITLE_ERROR} - Unauthorized (401)[/bold red]",
|
|
81
256
|
subtitle="",
|
|
82
257
|
title_align="left",
|
|
@@ -112,13 +287,22 @@ class PipecatConsole(Console):
|
|
|
112
287
|
if not error_message:
|
|
113
288
|
hide_subtitle = True
|
|
114
289
|
|
|
290
|
+
panel_title = f"{PANEL_TITLE_ERROR}{f' - {code}' if code else ''}"
|
|
291
|
+
subtitle = (
|
|
292
|
+
f"[dim]Docs: https://docs.pipecat.ai/pipecat-cloud/fundamentals/error-codes#{str(code).lower()}[/dim]"
|
|
293
|
+
if not hide_subtitle and code
|
|
294
|
+
else None
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not self.rich_output:
|
|
298
|
+
self.error(f"{title}: {error_message}", title=panel_title, subtitle=subtitle)
|
|
299
|
+
return
|
|
300
|
+
|
|
115
301
|
self.print(
|
|
116
302
|
Panel(
|
|
117
303
|
f"[red]{title}[/red]\n\n[dim]Error message:[/dim]\n{error_message}",
|
|
118
|
-
title=f"[bold red]{
|
|
119
|
-
subtitle=
|
|
120
|
-
if not hide_subtitle and code
|
|
121
|
-
else None,
|
|
304
|
+
title=f"[bold red]{panel_title}[/bold red]",
|
|
305
|
+
subtitle=subtitle,
|
|
122
306
|
title_align="left",
|
|
123
307
|
subtitle_align="left",
|
|
124
308
|
border_style="red",
|
|
@@ -158,6 +342,10 @@ class PipecatConsole(Console):
|
|
|
158
342
|
f"Or visit [link={dashboard}]{dashboard}[/link]"
|
|
159
343
|
)
|
|
160
344
|
|
|
345
|
+
if not self.rich_output:
|
|
346
|
+
self.error(body, title=f"{PANEL_TITLE_ERROR} - Spend limit reached (402)")
|
|
347
|
+
return
|
|
348
|
+
|
|
161
349
|
self.print(
|
|
162
350
|
Panel(
|
|
163
351
|
body,
|