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.
Files changed (96) hide show
  1. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/CHANGELOG.md +189 -0
  2. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/PKG-INFO +1 -1
  3. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pyproject.toml +1 -1
  4. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/auth_utils.py +3 -2
  5. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/console_utils.py +193 -5
  6. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/deploy_utils.py +326 -0
  7. pipecatcloud-1.2.0/src/pipecatcloud/_utils/github_utils.py +129 -0
  8. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/regions.py +21 -0
  9. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/api.py +412 -5
  10. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/agent.py +496 -46
  11. pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/agent_profiles.py +232 -0
  12. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/auth.py +47 -28
  13. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/build.py +67 -11
  14. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/deploy.py +285 -90
  15. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/docker.py +20 -11
  16. pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/github.py +307 -0
  17. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/organizations.py +154 -53
  18. pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/regions.py +301 -0
  19. pipecatcloud-1.2.0/src/pipecatcloud/cli/commands/registry_keys.py +161 -0
  20. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/secrets.py +167 -51
  21. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/spend_limit.py +31 -16
  22. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/config.py +3 -1
  23. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/entry_point.py +26 -2
  24. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/config.py +12 -0
  25. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
  26. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/SOURCES.txt +17 -0
  27. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/scm_file_list.json +58 -41
  28. pipecatcloud-1.2.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
  29. pipecatcloud-1.2.0/tests/conftest.py +47 -0
  30. pipecatcloud-1.2.0/tests/test_agent_profiles_and_resources.py +281 -0
  31. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_sessions.py +17 -21
  32. pipecatcloud-1.2.0/tests/test_agent_status_output.py +64 -0
  33. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_stop.py +17 -12
  34. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_api_client.py +62 -0
  35. pipecatcloud-1.2.0/tests/test_build_id_display.py +58 -0
  36. pipecatcloud-1.2.0/tests/test_deploy_architecture.py +63 -0
  37. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deploy_image_pull_secret.py +13 -18
  38. pipecatcloud-1.2.0/tests/test_deploy_referenced_secret_set.py +118 -0
  39. pipecatcloud-1.2.0/tests/test_exit_codes.py +111 -0
  40. pipecatcloud-1.2.0/tests/test_github_integration.py +934 -0
  41. pipecatcloud-1.2.0/tests/test_json_output.py +226 -0
  42. pipecatcloud-1.2.0/tests/test_organizations.py +103 -0
  43. pipecatcloud-1.2.0/tests/test_output_modes.py +179 -0
  44. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_pat_auth.py +7 -2
  45. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_regions.py +3 -1
  46. pipecatcloud-1.2.0/tests/test_regions_lifecycle.py +274 -0
  47. pipecatcloud-1.2.0/tests/test_secrets_reference.py +91 -0
  48. pipecatcloud-1.2.0/tests/test_secrets_set_referenced.py +120 -0
  49. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_spend_limit.py +53 -17
  50. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/uv.lock +201 -193
  51. pipecatcloud-1.0.1/src/pipecatcloud/cli/commands/regions.py +0 -40
  52. pipecatcloud-1.0.1/src/pipecatcloud.egg-info/scm_version.json +0 -8
  53. pipecatcloud-1.0.1/tests/conftest.py +0 -17
  54. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.claude/settings.json +0 -0
  55. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/format.yml +0 -0
  56. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/publish-pypi.yml +0 -0
  57. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/publish-test.yml +0 -0
  58. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/python-compatibility.yml +0 -0
  59. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.github/workflows/tests.yml +0 -0
  60. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.gitignore +0 -0
  61. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/.pre-commit-config.yaml +0 -0
  62. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/CLAUDE.md +0 -0
  63. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/LICENSE +0 -0
  64. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/README.md +0 -0
  65. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pipecat-cloud.png +0 -0
  66. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/pytest.ini +0 -0
  67. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/scripts/fix-ruff.sh +0 -0
  68. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/setup.cfg +0 -0
  69. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/__init__.py +0 -0
  70. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/__version__.py +0 -0
  71. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  72. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  73. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
  74. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/agent.py +0 -0
  75. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/__init__.py +0 -0
  76. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/api.py +0 -0
  77. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  78. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/constants.py +0 -0
  79. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/exception.py +0 -0
  80. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/py.typed +0 -0
  81. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/session.py +0 -0
  82. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  83. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  84. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  85. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
  86. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  87. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/__init__.py +0 -0
  88. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_agent_args.py +0 -0
  89. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_auth.py +0 -0
  90. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_build_utils.py +0 -0
  91. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_config_file.py +0 -0
  92. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deploy_poll.py +0 -0
  93. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_deployment_status.py +0 -0
  94. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_docker_commands.py +0 -0
  95. {pipecatcloud-1.0.1 → pipecatcloud-1.2.0}/tests/test_krisp_viva.py +0 -0
  96. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.ai/pipecat-cloud
@@ -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~=78.1.1",
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
- return
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
- return
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
- f"Unauthorized request / invalid user token.\n\nPlease log in again using [bold cyan]{PIPECAT_CLI_NAME} auth login[/bold cyan]",
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]{PANEL_TITLE_ERROR}{f' - {code}' if code else ''}[/bold red]",
119
- subtitle=f"[dim]Docs: https://docs.pipecat.ai/pipecat-cloud/fundamentals/error-codes#{str(code).lower()}[/dim]"
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,