pipecatcloud 1.0.0__tar.gz → 1.1.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 (82) hide show
  1. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/CHANGELOG.md +85 -0
  2. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/CLAUDE.md +1 -1
  3. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/PKG-INFO +4 -4
  4. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/README.md +2 -2
  5. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pyproject.toml +2 -2
  6. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/auth_utils.py +3 -2
  7. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/console_utils.py +193 -5
  8. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/api.py +4 -0
  9. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/agent.py +186 -45
  10. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/auth.py +47 -28
  11. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/build.py +42 -11
  12. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/deploy.py +95 -90
  13. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/docker.py +20 -11
  14. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/organizations.py +152 -53
  15. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/regions.py +14 -2
  16. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/secrets.py +83 -39
  17. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/spend_limit.py +31 -16
  18. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/config.py +3 -1
  19. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/entry_point.py +19 -2
  20. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/PKG-INFO +4 -4
  21. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/SOURCES.txt +8 -0
  22. pipecatcloud-1.1.0/src/pipecatcloud.egg-info/scm_file_list.json +75 -0
  23. pipecatcloud-1.1.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
  24. pipecatcloud-1.1.0/tests/conftest.py +35 -0
  25. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_sessions.py +17 -21
  26. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_stop.py +17 -12
  27. pipecatcloud-1.1.0/tests/test_build_id_display.py +58 -0
  28. pipecatcloud-1.1.0/tests/test_deploy_image_pull_secret.py +109 -0
  29. pipecatcloud-1.1.0/tests/test_exit_codes.py +111 -0
  30. pipecatcloud-1.1.0/tests/test_json_output.py +226 -0
  31. pipecatcloud-1.1.0/tests/test_organizations.py +103 -0
  32. pipecatcloud-1.1.0/tests/test_output_modes.py +179 -0
  33. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_pat_auth.py +7 -2
  34. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_spend_limit.py +53 -17
  35. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/uv.lock +338 -196
  36. pipecatcloud-1.0.0/tests/conftest.py +0 -17
  37. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.claude/settings.json +0 -0
  38. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/format.yml +0 -0
  39. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/publish-pypi.yml +0 -0
  40. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/publish-test.yml +0 -0
  41. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/python-compatibility.yml +0 -0
  42. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.github/workflows/tests.yml +0 -0
  43. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.gitignore +0 -0
  44. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/.pre-commit-config.yaml +0 -0
  45. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/LICENSE +0 -0
  46. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pipecat-cloud.png +0 -0
  47. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/pytest.ini +0 -0
  48. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/scripts/fix-ruff.sh +0 -0
  49. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/setup.cfg +0 -0
  50. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/__init__.py +0 -0
  51. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/__version__.py +0 -0
  52. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  53. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  54. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
  55. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
  56. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/_utils/regions.py +0 -0
  57. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/agent.py +0 -0
  58. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/__init__.py +0 -0
  59. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/api.py +0 -0
  60. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  61. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/config.py +0 -0
  62. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/constants.py +0 -0
  63. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/exception.py +0 -0
  64. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/py.typed +0 -0
  65. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/session.py +0 -0
  66. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  67. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  68. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  69. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/requires.txt +0 -0
  70. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  71. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/__init__.py +0 -0
  72. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_agent_args.py +0 -0
  73. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_api_client.py +0 -0
  74. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_auth.py +0 -0
  75. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_build_utils.py +0 -0
  76. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_config_file.py +0 -0
  77. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_deploy_poll.py +0 -0
  78. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_deployment_status.py +0 -0
  79. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_docker_commands.py +0 -0
  80. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_krisp_viva.py +0 -0
  81. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_max_session_duration.py +0 -0
  82. {pipecatcloud-1.0.0 → pipecatcloud-1.1.0}/tests/test_regions.py +0 -0
@@ -5,6 +5,91 @@ 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.1.0] - 2026-08-05
9
+
10
+ ### Added
11
+
12
+ - New global `--output {rich,plain,json}` option for every `pipecat cloud`
13
+ command, also settable via the `PIPECAT_OUTPUT` environment variable or the
14
+ `output` config key. Note the option is placed before the subcommand
15
+ (`pipecat cloud --output json agent list`); in CI, setting `PIPECAT_OUTPUT`
16
+ once avoids ordering concerns. `rich` is the interactive default; `plain` is
17
+ line-oriented with no boxes and no truncation; `json` emits one
18
+ machine-readable JSON object on stdout with all human output on stderr.
19
+ When stdout is not a terminal the CLI now defaults to `plain`.
20
+
21
+ - Progress is now visible when output is piped or redirected. Long-running
22
+ operations (deploy rollout polling, cloud builds) print each status
23
+ transition as a timestamped line instead of rendering nothing outside a
24
+ terminal. Cloud-build polling additionally reports build status changes.
25
+
26
+ - Listings (`agent list`, `agent sessions`, `agent deployments`,
27
+ `secrets list`, `organizations list`, `organizations keys list`, `build
28
+ list`, `regions list`, and friends) render as tab-separated rows with
29
+ full, untruncated values in `plain` mode. Piped output previously fell
30
+ back to an 80-column table that silently ellipsized IDs.
31
+
32
+ - `agent status` and `agent deployments` now show `Build: <build id>` for
33
+ cloud-built deployments instead of `Image: N/A`. Cloud builds produce an
34
+ image managed by Pipecat Cloud, so the build ID is the reference to use
35
+ when checking which artifact a deployment is running.
36
+
37
+ ### Changed
38
+
39
+ - **The CLI now exits non-zero on failure.** Previously every error path
40
+ exited `0` — including failed deploys and running commands while logged
41
+ out — so CI pipelines could not detect failures. Errors and user
42
+ cancellations exit `1`; usage errors and refused non-interactive prompts
43
+ exit `2`. Pipelines that depended on the old always-zero behavior will
44
+ now correctly fail.
45
+
46
+ - Commands that would show a confirmation prompt now fail fast with exit
47
+ code `2` when stdin is not a terminal, pointing at the bypass flag
48
+ (`--yes`/`--force`/`--skip`), instead of hanging or misbehaving in CI.
49
+
50
+ - `organizations list` and `organizations keys create` label their
51
+ organization columns **Organization Name** (the display name) and
52
+ **Organization ID** (the identifier `--organization` takes), matching
53
+ the dashboard. The values are unchanged, as is `--output json`, which
54
+ emits the server's own field names. Scripts scraping the previous
55
+ `Organization` / `Name` headers out of table output need updating. In
56
+ `organizations list`, the active-organization marker moved to its own
57
+ `Active` column, so the ID column holds nothing but the identifier.
58
+
59
+ - CLI messages call an organization an "organization" rather than a
60
+ "namespace". The Kubernetes namespace an organization maps to is an
61
+ implementation detail, so messages about missing agents, secret sets,
62
+ or accounts now name the organization. `organizations select` reports
63
+ an unmatched `--organization` value as an unknown organization ID and
64
+ points at `organizations list`.
65
+
66
+ ### Deprecated
67
+
68
+ - `spend-limit show --json` still works but is now an alias for the global
69
+ `--output json` mode.
70
+
71
+ ### Fixed
72
+
73
+ - `organizations keys create` no longer reports a new key as "Using as
74
+ default in local config" when it was not stored as the default. The
75
+ panel carries that note only when the key actually became the default.
76
+
77
+ ## [1.0.1] - 2026-07-13
78
+
79
+ ### Fixed
80
+
81
+ - `pipecat cloud deploy` now verifies the image pull secret passed via
82
+ `--credentials` by looking it up in the secrets list, matching on both name
83
+ and type. Previously the check misread an API error response as "secret
84
+ exists", and a regular secret set with the same name also passed. A deploy
85
+ referencing a missing image pull secret now aborts with a clear error
86
+ instead of proceeding.
87
+
88
+ - Documentation links in CLI output now point to `docs.pipecat.ai` instead of
89
+ the retired `docs.pipecat.daily.co` domain. API error panels link directly
90
+ to the matching entry in the error codes reference, and the deploy and
91
+ image pull secret prompts link to the image pull secrets guide.
92
+
8
93
  ## [1.0.0] - 2026-06-19
9
94
 
10
95
  ### Added
@@ -8,7 +8,7 @@ Guidance for Claude Code working in this repo.
8
8
 
9
9
  - Package root: `src/pipecatcloud/`
10
10
  - CLI: registered as a Pipecat CLI extension via the `pipecat_cli.extensions:cloud` entry point (surfaced as `pipecat cloud`). There is no standalone entry point — no `[project.scripts]` and no `__main__`; the CLI runs only through the Pipecat CLI.
11
- - Public docs: https://docs.pipecat.daily.co
11
+ - Public docs: https://docs.pipecat.ai/pipecat-cloud
12
12
 
13
13
  ## Toolchain
14
14
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
- Project-URL: Website, https://docs.pipecat.daily.co/
6
+ Project-URL: Website, https://docs.pipecat.ai/pipecat-cloud
7
7
  Keywords: pipecat,pipecat-ai,voice,ai,llm
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
@@ -25,7 +25,7 @@ Dynamic: license-file
25
25
  <img alt="pipecat cloud" width="500px" height="auto" src="https://raw.githubusercontent.com/daily-co/pipecat-cloud/main/pipecat-cloud.png">
26
26
  </div></h1>
27
27
 
28
- [![Docs](https://img.shields.io/badge/documentation-blue)](https://docs.pipecat.daily.co)
28
+ [![Docs](https://img.shields.io/badge/documentation-blue)](https://docs.pipecat.ai/pipecat-cloud)
29
29
  [![PyPI](https://img.shields.io/pypi/v/pipecatcloud)](https://pypi.org/project/pipecatcloud)
30
30
 
31
31
  # Pipecat Cloud
@@ -41,7 +41,7 @@ Python module and CLI for interacting with [Pipecat Cloud](https://pipecat.daily
41
41
 
42
42
  ### Documentation
43
43
 
44
- Documentation for Pipecat Cloud is available [here](https://docs.pipecat.daily.co).
44
+ Documentation for Pipecat Cloud is available [here](https://docs.pipecat.ai/pipecat-cloud).
45
45
 
46
46
  ### Installation
47
47
 
@@ -2,7 +2,7 @@
2
2
  <img alt="pipecat cloud" width="500px" height="auto" src="https://raw.githubusercontent.com/daily-co/pipecat-cloud/main/pipecat-cloud.png">
3
3
  </div></h1>
4
4
 
5
- [![Docs](https://img.shields.io/badge/documentation-blue)](https://docs.pipecat.daily.co)
5
+ [![Docs](https://img.shields.io/badge/documentation-blue)](https://docs.pipecat.ai/pipecat-cloud)
6
6
  [![PyPI](https://img.shields.io/pypi/v/pipecatcloud)](https://pypi.org/project/pipecatcloud)
7
7
 
8
8
  # Pipecat Cloud
@@ -18,7 +18,7 @@ Python module and CLI for interacting with [Pipecat Cloud](https://pipecat.daily
18
18
 
19
19
  ### Documentation
20
20
 
21
- Documentation for Pipecat Cloud is available [here](https://docs.pipecat.daily.co).
21
+ Documentation for Pipecat Cloud is available [here](https://docs.pipecat.ai/pipecat-cloud).
22
22
 
23
23
  ### Installation
24
24
 
@@ -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",
@@ -77,7 +77,7 @@ select = [
77
77
  "**/__init__.py" = ["D104"]
78
78
 
79
79
  [project.urls]
80
- Website = "https://docs.pipecat.daily.co/"
80
+ Website = "https://docs.pipecat.ai/pipecat-cloud"
81
81
 
82
82
  [tool.setuptools_scm]
83
83
  local_scheme = "no-local-version"
@@ -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.daily.co/agents/error-codes#{code}[/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,
@@ -197,6 +197,10 @@ class _API:
197
197
 
198
198
  if not self.error:
199
199
  return
200
+ # In json mode the machine-readable error goes to stdout; the human
201
+ # rendering below lands on stderr (the console is redirected there).
202
+ if self.is_cli and getattr(console, "json_output", False):
203
+ console.output_json({"error": self.error})
200
204
  if isinstance(self.error, dict) and self.error.get("code", "400") == "401":
201
205
  console.unauthorized()
202
206
  else: