pipecatcloud 0.4.0__tar.gz → 0.4.2__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 (67) hide show
  1. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/CHANGELOG.md +57 -0
  2. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/PKG-INFO +1 -1
  3. pipecatcloud-0.4.2/src/pipecatcloud/_utils/auth_utils.py +67 -0
  4. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/deploy_utils.py +48 -2
  5. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/api.py +74 -3
  6. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/agent.py +18 -1
  7. pipecatcloud-0.4.2/src/pipecatcloud/cli/commands/auth.py +602 -0
  8. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/config.py +33 -15
  9. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/config.py +2 -0
  10. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/PKG-INFO +1 -1
  11. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
  12. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_deployment_status.py +154 -0
  13. pipecatcloud-0.4.2/tests/test_pat_auth.py +162 -0
  14. pipecatcloud-0.4.0/src/pipecatcloud/_utils/auth_utils.py +0 -26
  15. pipecatcloud-0.4.0/src/pipecatcloud/cli/commands/auth.py +0 -344
  16. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.github/workflows/format.yml +0 -0
  17. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.github/workflows/publish-pypi.yml +0 -0
  18. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.github/workflows/publish-test.yml +0 -0
  19. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.github/workflows/tests.yml +0 -0
  20. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.gitignore +0 -0
  21. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/.pre-commit-config.yaml +0 -0
  22. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/LICENSE +0 -0
  23. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/README.md +0 -0
  24. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/pipecat-cloud.png +0 -0
  25. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/pyproject.toml +0 -0
  26. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/pytest.ini +0 -0
  27. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/scripts/fix-ruff.sh +0 -0
  28. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/scripts/pre-commit.sh +0 -0
  29. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/setup.cfg +0 -0
  30. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/__init__.py +0 -0
  31. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/__main__.py +0 -0
  32. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/__version__.py +0 -0
  33. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/__init__.py +0 -0
  34. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/async_utils.py +0 -0
  35. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/build_utils.py +0 -0
  36. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/console_utils.py +0 -0
  37. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/_utils/regions.py +0 -0
  38. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/agent.py +0 -0
  39. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/__init__.py +0 -0
  40. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/api.py +0 -0
  41. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  42. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/build.py +0 -0
  43. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/deploy.py +0 -0
  44. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/docker.py +0 -0
  45. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  46. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/regions.py +0 -0
  47. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/commands/secrets.py +0 -0
  48. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/cli/entry_point.py +0 -0
  49. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/constants.py +0 -0
  50. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/exception.py +0 -0
  51. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/py.typed +0 -0
  52. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/session.py +0 -0
  53. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  54. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  55. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  56. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/requires.txt +0 -0
  57. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  58. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/__init__.py +0 -0
  59. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/conftest.py +0 -0
  60. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_agent_sessions.py +0 -0
  61. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_agent_stop.py +0 -0
  62. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_api_client.py +0 -0
  63. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_config_file.py +0 -0
  64. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_docker_commands.py +0 -0
  65. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_krisp_viva.py +0 -0
  66. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/tests/test_regions.py +0 -0
  67. {pipecatcloud-0.4.0 → pipecatcloud-0.4.2}/uv.lock +0 -0
@@ -5,6 +5,63 @@ 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
+ ## [0.4.2] - 2026-04-02
9
+
10
+ ### Added
11
+
12
+ - **Container health diagnostics in `agent status` and `deploy`**: When a
13
+ deployment is failing, the CLI now shows the failure reason (e.g.,
14
+ CrashLoopBackOff, OOMKilled, ImagePullBackOff), exit code, restart count,
15
+ and the actual error output (e.g., Python tracebacks). This information
16
+ appears under the revision line in both `pcc agent status` and during
17
+ deploy monitoring.
18
+
19
+ ### Changed
20
+
21
+ - All API requests now include a `User-Agent: PipecatCloudCLI/<version>` header,
22
+ enabling server-side identification of CLI versions for debugging.
23
+
24
+ ### Fixed
25
+
26
+ - **Default organization resolution for `PIPECAT_TOKEN` auth**: The CLI now
27
+ automatically resolves the user's default organization from the API when
28
+ a token is present but no org is configured locally. Previously,
29
+ `PIPECAT_TOKEN` required the org to be in the config file or `PIPECAT_ORG`
30
+ to be set as well.
31
+
32
+ ## [0.4.1] - 2026-03-30
33
+
34
+ ### Added
35
+
36
+ - **Personal Access Token (PAT) support**: Authenticate with the CLI using a
37
+ PAT instead of interactive OAuth login. Set `PIPECAT_TOKEN=pcc_pat_...` as an
38
+ environment variable, or run `pcc auth use-pat <token>` to store it to config.
39
+ PATs work with all CLI commands and inherit the user's org memberships.
40
+
41
+ - `pcc auth use-pat` command: Validates a PAT against the API and stores it for
42
+ persistent use without needing an environment variable.
43
+
44
+ - `pcc auth whoami` now shows the current auth method (PAT or OAuth).
45
+
46
+ ### Changed
47
+
48
+ - **OAuth2 authentication**: `pcc auth login` now uses industry-standard
49
+ OAuth2 Authorization Code + PKCE, replacing the custom device-code flow.
50
+ Tokens auto-refresh transparently — no more "please run `pcc auth login`"
51
+ interruptions.
52
+
53
+ - `pcc auth logout` now revokes tokens server-side. Previously, logout only
54
+ deleted local credentials and the token remained valid until expiry.
55
+
56
+ - `pcc auth whoami` now displays the user's email address instead of the
57
+ internal user ID.
58
+
59
+ ### Fixed
60
+
61
+ - Credentials file with overly permissive permissions is now automatically
62
+ repaired to 600 instead of issuing a suppressible warning. If repair fails,
63
+ the CLI refuses to load the token.
64
+
8
65
  ## [0.4.0] - 2026-03-29
9
66
 
10
67
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
@@ -0,0 +1,67 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ import functools
8
+ from typing import Optional
9
+
10
+ import aiohttp
11
+
12
+ from pipecatcloud.__version__ import version
13
+ from pipecatcloud._utils.console_utils import console
14
+ from pipecatcloud.cli import PIPECAT_CLI_NAME
15
+ from pipecatcloud.cli.config import config
16
+
17
+
18
+ async def _resolve_default_org(token: str) -> Optional[str]:
19
+ """Fetch the user's default organization using the given token."""
20
+ from pipecatcloud.config import config as base_config
21
+
22
+ api_host = base_config.get("api_host")
23
+ org_path = base_config.get("organization_path")
24
+ url = f"{api_host}{org_path}"
25
+
26
+ async with aiohttp.ClientSession() as session:
27
+ async with session.get(
28
+ url,
29
+ headers={
30
+ "Authorization": f"Bearer {token}",
31
+ "User-Agent": f"PipecatCloudCLI/{version}",
32
+ },
33
+ ) as resp:
34
+ if resp.status == 200:
35
+ data = await resp.json()
36
+ organizations = data.get("organizations", [])
37
+ if organizations:
38
+ return organizations[0]["name"]
39
+ return None
40
+
41
+
42
+ def requires_login(func):
43
+ @functools.wraps(func)
44
+ async def wrapper(*args, **kwargs):
45
+ token = config.get("token")
46
+ if token is None:
47
+ console.error(
48
+ f"You are not logged in. Please run `{PIPECAT_CLI_NAME} auth login` first.",
49
+ )
50
+ return
51
+
52
+ # When org is not set locally (e.g. PIPECAT_TOKEN without a config file),
53
+ # resolve the user's default organization from the API.
54
+ if config.get("org") is None:
55
+ org_name = await _resolve_default_org(token)
56
+ if org_name:
57
+ config.override_locally("org", org_name)
58
+ else:
59
+ console.error(
60
+ "Could not determine your organization. "
61
+ f"Set PIPECAT_ORG or run `{PIPECAT_CLI_NAME} auth login`.",
62
+ )
63
+ return
64
+
65
+ return await func(*args, **kwargs)
66
+
67
+ return wrapper
@@ -75,7 +75,7 @@ def _format_elapsed(phase_started_at: Optional[str]) -> str:
75
75
 
76
76
 
77
77
  def _format_revision_line(label: str, rev: dict) -> str:
78
- """Format a single revision as an indented detail line."""
78
+ """Format a single revision as an indented detail line, with health details if present."""
79
79
  deploy_id = rev.get("deploymentID", "unknown")[:8]
80
80
  phase = rev.get("phase", "Unknown")
81
81
  ready_replicas = rev.get("readyReplicas")
@@ -88,7 +88,53 @@ def _format_revision_line(label: str, rev: dict) -> str:
88
88
  if elapsed:
89
89
  parts.append(f"[dim]·[/dim] [dim]{elapsed}[/dim]")
90
90
 
91
- return " ".join(parts)
91
+ lines = [" ".join(parts)]
92
+
93
+ # Append health details if present and unhealthy
94
+ health = rev.get("health")
95
+ if health and not (health.get("ready") and health.get("restartCount", 0) == 0):
96
+ lines.extend(format_health_lines(health))
97
+
98
+ if rev.get("hasInfrastructureIssue"):
99
+ lines.append(" [yellow]Infrastructure issue detected — contact support[/yellow]")
100
+
101
+ return "\n".join(lines)
102
+
103
+
104
+ def format_health_lines(health: dict) -> list:
105
+ """Format health details as indented lines under a revision."""
106
+ lines = []
107
+ reason_parts = []
108
+ reason = health.get("reason", "")
109
+ term_reason = health.get("lastTerminationReason", "")
110
+ exit_code = health.get("lastExitCode")
111
+
112
+ if reason:
113
+ detail = reason
114
+ if term_reason and term_reason != reason:
115
+ detail += f" ({term_reason}"
116
+ if exit_code is not None:
117
+ detail += f", exit {exit_code}"
118
+ detail += ")"
119
+ elif exit_code is not None:
120
+ detail += f" (exit {exit_code})"
121
+ reason_parts.append(f"[red]{detail}[/red]")
122
+
123
+ restarts = health.get("restartCount", 0)
124
+ replicas_started = health.get("replicasStarted", 0)
125
+ if restarts > 0:
126
+ reason_parts.append(f"[dim]·[/dim] {restarts} restarts across {replicas_started} replicas")
127
+
128
+ if reason_parts:
129
+ lines.append(" " + " ".join(reason_parts))
130
+
131
+ message = health.get("message", "")
132
+ if message:
133
+ last_line = message.strip().split("\n")[-1].strip()
134
+ if last_line:
135
+ lines.append(f" [dim]{last_line}[/dim]")
136
+
137
+ return lines
92
138
 
93
139
 
94
140
  def _build_status_message(headline: str, current_rev=None, previous_rev=None) -> str:
@@ -5,12 +5,14 @@
5
5
  #
6
6
 
7
7
  import json
8
+ import time
8
9
  from functools import wraps
9
10
  from typing import Callable, List, Optional, Union
10
11
 
11
12
  import aiohttp
12
13
  from loguru import logger
13
14
 
15
+ from pipecatcloud.__version__ import version
14
16
  from pipecatcloud._utils.deploy_utils import DeployConfigParams
15
17
  from pipecatcloud.config import config
16
18
  from pipecatcloud.exception import AgentStartError
@@ -48,10 +50,70 @@ class _API:
48
50
  return f"{config.get('api_host', '')}{config.get(path, '')}"
49
51
 
50
52
  def _configure_headers(self, override_token: Optional[str] = None) -> dict:
51
- if not self.token and not override_token:
52
- return {}
53
- return {"Authorization": f"Bearer {override_token or self.token}"}
53
+ headers = {"User-Agent": f"PipecatCloudCLI/{version}"}
54
+ if self.token or override_token:
55
+ headers["Authorization"] = f"Bearer {override_token or self.token}"
56
+ return headers
54
57
 
58
+ def _is_pat(self) -> bool:
59
+ """Check if the current token is a Personal Access Token."""
60
+ return bool(self.token and self.token.startswith("pcc_pat_"))
61
+
62
+ def _is_token_expired(self) -> bool:
63
+ """Check if the current OAuth token is near expiry.
64
+
65
+ Returns False for PATs (they don't expire via OAuth) and for
66
+ non-OAuth tokens (no token_expires_at stored).
67
+ """
68
+ if self._is_pat():
69
+ return False
70
+
71
+ from pipecatcloud.cli.config import config as cli_config
72
+
73
+ expires_at = cli_config.get("token_expires_at")
74
+ if expires_at is None:
75
+ return False
76
+ # Refresh 60 seconds before actual expiry to avoid race conditions
77
+ return time.time() > (float(expires_at) - 60)
78
+
79
+ async def _refresh_oauth_token(self) -> Optional[str]:
80
+ """Refresh the OAuth access token using the stored refresh token.
81
+
82
+ Updates both the on-disk config and the in-memory token on this API
83
+ instance. Returns the new access token, or None if refresh failed
84
+ (in which case the request will proceed with the expired token and
85
+ the server will return 401, prompting re-login).
86
+ """
87
+ from pipecatcloud.cli.config import config as cli_config, update_user_config
88
+
89
+ # Inline import: auth.py → cli/api.py → api.py creates a circular
90
+ # dependency at module load time. Importing here (at call time)
91
+ # breaks the cycle.
92
+ from pipecatcloud.cli.commands.auth import refresh_access_token
93
+
94
+ refresh_token = cli_config.get("refresh_token")
95
+ if not refresh_token:
96
+ return None
97
+
98
+ tokens = await refresh_access_token(refresh_token)
99
+ if not tokens:
100
+ return None
101
+
102
+ new_token = tokens["access_token"]
103
+ new_refresh = tokens.get("refresh_token", refresh_token)
104
+ expires_at = time.time() + tokens.get("expires_in", 86400)
105
+
106
+ update_user_config(
107
+ token=new_token,
108
+ refresh_token=new_refresh,
109
+ token_expires_at=expires_at,
110
+ )
111
+ self.token = new_token
112
+ return new_token
113
+
114
+ # TODO: several auth.py and agent.py calls bypass _base_request() with their
115
+ # own aiohttp.ClientSession(). A longer-term cleanup could route all PCC API
116
+ # calls through _base_request() so headers (User-Agent, Auth) are set in one place.
55
117
  async def _base_request(
56
118
  self,
57
119
  method: str,
@@ -61,6 +123,15 @@ class _API:
61
123
  not_found_is_empty: bool = False,
62
124
  override_token: Optional[str] = None,
63
125
  ) -> Optional[dict]:
126
+ # Auto-refresh expired OAuth tokens before making the request.
127
+ # Only runs for CLI usage (self.is_cli) and only when an OAuth
128
+ # refresh_token is stored (token_expires_at is set). Old device-code
129
+ # tokens are unaffected since _is_token_expired() returns False.
130
+ if self.is_cli and not override_token and self._is_token_expired():
131
+ new_token = await self._refresh_oauth_token()
132
+ if new_token is None:
133
+ logger.debug("Token refresh failed, proceeding with expired token")
134
+
64
135
  async with aiohttp.ClientSession() as session:
65
136
  logger.debug(f"Request: {method} {url} {params} {json}")
66
137
 
@@ -20,6 +20,7 @@ from rich.panel import Panel
20
20
  from rich.table import Table
21
21
  from rich.text import Text
22
22
 
23
+ from pipecatcloud.__version__ import version as _cli_version
23
24
  from pipecatcloud._utils.async_utils import synchronizer
24
25
  from pipecatcloud._utils.auth_utils import requires_login
25
26
  from pipecatcloud._utils.console_utils import (
@@ -31,6 +32,7 @@ from pipecatcloud._utils.console_utils import (
31
32
  from pipecatcloud._utils.deploy_utils import (
32
33
  CONFIG_FILE_OPTION,
33
34
  DeployConfigParams,
35
+ format_health_lines,
34
36
  with_deploy_config,
35
37
  )
36
38
  from pipecatcloud._utils.regions import get_region_codes, validate_region
@@ -285,6 +287,18 @@ async def status(
285
287
  current_parts.append(f"[dim]·[/dim] {rev_replicas} agents")
286
288
  health_lines.append(" ".join(current_parts))
287
289
 
290
+ # Show health details when available (skip if healthy with no restarts)
291
+ rev_health = current_rev.get("health")
292
+ if rev_health and not (
293
+ rev_health.get("ready") and rev_health.get("restartCount", 0) == 0
294
+ ):
295
+ health_lines.extend(format_health_lines(rev_health))
296
+
297
+ if current_rev.get("hasInfrastructureIssue"):
298
+ health_lines.append(
299
+ " [yellow]Infrastructure issue detected — contact support[/yellow]"
300
+ )
301
+
288
302
  if previous_rev:
289
303
  prev_phase = previous_rev.get("phase", "Unknown")
290
304
  prev_id = previous_rev.get("deploymentID", "")[:8]
@@ -720,7 +734,10 @@ async def deployments(
720
734
  async with aiohttp.ClientSession() as session:
721
735
  response = await session.get(
722
736
  f"{API.construct_api_url('services_deployments_path').format(org=org, service=agent_name)}",
723
- headers={"Authorization": f"Bearer {token}"},
737
+ headers={
738
+ "Authorization": f"Bearer {token}",
739
+ "User-Agent": f"PipecatCloudCLI/{_cli_version}",
740
+ },
724
741
  )
725
742
  if response.status != 200:
726
743
  error_code = str(response.status)