pipecatcloud 1.3.0__tar.gz → 1.4.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 (97) hide show
  1. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/publish-pypi.yml +39 -0
  2. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/CHANGELOG.md +50 -0
  3. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/CLAUDE.md +2 -0
  4. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/PKG-INFO +2 -2
  5. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/pyproject.toml +1 -1
  6. pipecatcloud-1.4.0/scripts/release-changelog.py +113 -0
  7. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/api.py +29 -0
  8. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/github.py +142 -39
  9. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/config.py +1 -2
  10. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/PKG-INFO +2 -2
  11. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/SOURCES.txt +1 -0
  12. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/requires.txt +1 -1
  13. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/scm_file_list.json +1 -0
  14. pipecatcloud-1.4.0/src/pipecatcloud.egg-info/scm_version.json +8 -0
  15. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_api_client.py +68 -0
  16. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_github_integration.py +346 -6
  17. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/uv.lock +460 -340
  18. pipecatcloud-1.3.0/src/pipecatcloud.egg-info/scm_version.json +0 -8
  19. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.claude/settings.json +0 -0
  20. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.claude/skills/update-docs/SOURCE_DOC_MAPPING.md +0 -0
  21. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/format.yml +0 -0
  22. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/publish-test.yml +0 -0
  23. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/python-compatibility.yml +0 -0
  24. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/tests.yml +0 -0
  25. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.github/workflows/update-docs.yml +0 -0
  26. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.gitignore +0 -0
  27. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/.pre-commit-config.yaml +0 -0
  28. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/LICENSE +0 -0
  29. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/README.md +0 -0
  30. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/pipecat-cloud.png +0 -0
  31. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/pytest.ini +0 -0
  32. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/scripts/fix-ruff.sh +0 -0
  33. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/setup.cfg +0 -0
  34. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/__init__.py +0 -0
  35. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/__version__.py +0 -0
  36. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/__init__.py +0 -0
  37. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/async_utils.py +0 -0
  38. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  39. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/build_utils.py +0 -0
  40. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/console_utils.py +0 -0
  41. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/deploy_utils.py +0 -0
  42. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/github_utils.py +0 -0
  43. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/_utils/regions.py +0 -0
  44. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/agent.py +0 -0
  45. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/__init__.py +0 -0
  46. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/api.py +0 -0
  47. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  48. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/agent.py +0 -0
  49. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/agent_profiles.py +0 -0
  50. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/auth.py +0 -0
  51. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/build.py +0 -0
  52. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/deploy.py +0 -0
  53. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/docker.py +0 -0
  54. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  55. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/regions.py +0 -0
  56. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/registry_keys.py +0 -0
  57. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/secrets.py +0 -0
  58. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/commands/spend_limit.py +0 -0
  59. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/config.py +0 -0
  60. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/cli/entry_point.py +0 -0
  61. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/constants.py +0 -0
  62. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/exception.py +0 -0
  63. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/py.typed +0 -0
  64. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/session.py +0 -0
  65. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  66. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  67. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  68. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  69. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/__init__.py +0 -0
  70. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/conftest.py +0 -0
  71. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_agent_args.py +0 -0
  72. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_agent_profiles_and_resources.py +0 -0
  73. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_agent_sessions.py +0 -0
  74. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_agent_status_output.py +0 -0
  75. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_agent_stop.py +0 -0
  76. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_auth.py +0 -0
  77. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_build_id_display.py +0 -0
  78. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_build_utils.py +0 -0
  79. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_config_file.py +0 -0
  80. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_deploy_architecture.py +0 -0
  81. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_deploy_image_pull_secret.py +0 -0
  82. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_deploy_poll.py +0 -0
  83. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_deploy_referenced_secret_set.py +0 -0
  84. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_deployment_status.py +0 -0
  85. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_docker_commands.py +0 -0
  86. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_exit_codes.py +0 -0
  87. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_json_output.py +0 -0
  88. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_krisp_viva.py +0 -0
  89. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_max_session_duration.py +0 -0
  90. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_organizations.py +0 -0
  91. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_output_modes.py +0 -0
  92. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_pat_auth.py +0 -0
  93. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_regions.py +0 -0
  94. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_regions_lifecycle.py +0 -0
  95. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_secrets_reference.py +0 -0
  96. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_secrets_set_referenced.py +0 -0
  97. {pipecatcloud-1.3.0 → pipecatcloud-1.4.0}/tests/test_spend_limit.py +0 -0
@@ -78,3 +78,42 @@ jobs:
78
78
  verbose: true
79
79
  print-hash: true
80
80
  repository-url: https://test.pypi.org/legacy/
81
+
82
+ # Runs once the package is installable from PyPI, so the release notification
83
+ # never points at a version nobody can install yet. Notes come from the
84
+ # CHANGELOG.md section at the tag: the changelog is the single source of
85
+ # release notes, reformatted for GitHub by scripts/release-changelog.py. A tag
86
+ # whose version has no changelog section fails the run.
87
+ github-release:
88
+ name: 'Create GitHub Release'
89
+ runs-on: ubuntu-latest
90
+ needs: [publish-to-pypi]
91
+ permissions:
92
+ contents: write
93
+ steps:
94
+ - name: Checkout repo
95
+ uses: actions/checkout@v4
96
+ with:
97
+ ref: ${{ github.event.inputs.gitref }}
98
+
99
+ - name: Set up Python
100
+ uses: actions/setup-python@v5
101
+ with:
102
+ python-version: '3.12'
103
+
104
+ - name: Generate release notes
105
+ env:
106
+ GITREF: ${{ github.event.inputs.gitref }}
107
+ run: |
108
+ python scripts/release-changelog.py --no-heading "${GITREF#v}" > release-notes.md
109
+
110
+ - name: Publish release
111
+ env:
112
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113
+ GITREF: ${{ github.event.inputs.gitref }}
114
+ run: |
115
+ if gh release view "$GITREF" >/dev/null 2>&1; then
116
+ gh release edit "$GITREF" --notes-file release-notes.md
117
+ else
118
+ gh release create "$GITREF" --title "$GITREF" --notes-file release-notes.md
119
+ fi
@@ -5,6 +5,56 @@ 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.4.0] - 2026-10-22
9
+
10
+ ### Added
11
+
12
+ - `pipecat cloud github connect --existing` links a GitHub App installation
13
+ that is already on GitHub to your Pipecat Cloud organization. Use it for
14
+ an installation you installed or had an owner approve, while still a
15
+ member of the GitHub organization it is on, or one on your personal
16
+ account or a GitHub organization you own. That covers an install request
17
+ an owner approved and an install made from GitHub, neither of which linked
18
+ before. You authorize the App on GitHub, then confirm the installation to
19
+ link on the Pipecat Cloud dashboard, and the CLI waits up to 14 minutes
20
+ for it. `github status` and the `github connect` timeout message point to
21
+ it.
22
+
23
+ ### Changed
24
+
25
+ - Connecting an organization to GitHub now finishes on the Pipecat Cloud
26
+ dashboard, where you may need to sign in as the same Pipecat Cloud user the
27
+ CLI is logged in as. GitHub also asks you to authorize the App after you
28
+ install it. This happens on the server side, so it applies to every CLI
29
+ version. `pipecat cloud github connect` now says so, also with `--json`
30
+ (on stderr), and waits up to 26 minutes for the connection instead of 15,
31
+ to cover both steps. Older versions stop waiting after 15 minutes; the
32
+ connection still completes, and `pipecat cloud github status` shows it.
33
+ - `pipecat cloud github connect` stops waiting as soon as Pipecat Cloud
34
+ refuses the CLI's sign-in or its access to the organization, instead of
35
+ waiting until it times out.
36
+ - The `aiohttp` minimum version is now `3.14.4`, the first release with fixes
37
+ for several security advisories. The SDK makes its API requests with
38
+ `aiohttp`, and `pipecat cloud auth login` uses it for the local server that
39
+ receives the sign-in callback.
40
+
41
+ ### Removed
42
+
43
+ - The `login_path` and `login_status_path` settings and their
44
+ `PIPECAT_LOGIN_PATH` and `PIPECAT_LOGIN_STATUS_PATH` overrides. They
45
+ pointed at the device-code sign-in that 0.4.1 replaced with OAuth and
46
+ PKCE, and nothing has read them since, so setting them had no effect.
47
+ Removing them is not a breaking change: the only difference is that they
48
+ no longer appear in `--show-cli-config` output. Pipecat Cloud is retiring
49
+ that sign-in, so a CLI older than 0.4.1 has to be upgraded to log in.
50
+ `pipecat cloud auth login` is unchanged.
51
+
52
+ ### Fixed
53
+
54
+ - `pipecat cloud github connect` says it could not reach Pipecat Cloud when
55
+ no usable response comes back, such as on a dropped connection, instead
56
+ of saying the API returned no URL.
57
+
8
58
  ## [1.3.0] - 2026-10-01
9
59
 
10
60
  ### Added
@@ -110,6 +110,8 @@ pcc-deploy.toml Example deployment config consumed by `pipecat cloud de
110
110
  2. Tag the commit with `v<MAJOR>.<MINOR>.<PATCH>`.
111
111
  3. Trigger `publish-pypi.yml` manually with that tag. `setuptools_scm` picks up the version from the tag.
112
112
 
113
+ After publishing to PyPI, the workflow creates (or updates) the GitHub Release for the tag. Its notes are the tag's `CHANGELOG.md` section, reformatted by `scripts/release-changelog.py`, so fix release-note wording in the changelog rather than on GitHub: a re-run overwrites edits made there. Preview the notes locally with `python scripts/release-changelog.py --no-heading 1.3.0`.
114
+
113
115
  ## Writing style for generated content in this repo
114
116
 
115
117
  - No emdashes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -8,7 +8,7 @@ Keywords: pipecat,pipecat-ai,voice,ai,llm
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: aiohttp<4.0.0,>=3.11.12
11
+ Requires-Dist: aiohttp<4.0.0,>=3.14.4
12
12
  Requires-Dist: fastapi<1.0.0,>=0.115.6
13
13
  Requires-Dist: loguru~=0.7.3
14
14
  Requires-Dist: python-dotenv<2.0.0,>=1.0.1
@@ -11,7 +11,7 @@ license = { text = "BSD 2-Clause License" }
11
11
  readme = "README.md"
12
12
  keywords = ["pipecat", "pipecat-ai", "voice", "ai", "llm"]
13
13
  dependencies = [
14
- "aiohttp>=3.11.12,<4.0.0",
14
+ "aiohttp>=3.14.4,<4.0.0",
15
15
  "fastapi>=0.115.6,<1.0.0",
16
16
  "loguru~=0.7.3",
17
17
  "python-dotenv>=1.0.1,<2.0.0",
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """Generate release notes from CHANGELOG.md for use in GitHub Releases (or anywhere).
3
+
4
+ Usage::
5
+
6
+ python scripts/release-changelog.py VERSION
7
+ python scripts/release-changelog.py 1.1.0
8
+ python scripts/release-changelog.py --file path/to/CHANGELOG.md 1.1.0
9
+ python scripts/release-changelog.py --no-heading 1.1.0
10
+
11
+ Extracts the requested ``## [VERSION]`` section (heading and ``### …``
12
+ subheadings included) and prints it to stdout. The only transformation
13
+ applied is collapsing each entry paragraph onto a single line, so it is
14
+ suitable for pasting into release notes that don't need 80-column wrapping.
15
+ ``(PR [...])`` references stay on their own two-space-indented continuation
16
+ line. The input file is never modified.
17
+
18
+ ``--no-heading`` drops the ``## [VERSION]`` line, leaving the body to start
19
+ at the first ``### …`` subheading. GitHub Releases carry the version in the
20
+ release title, so the heading would be redundant there.
21
+
22
+ Every paragraph is unfilled. Indentation is preserved — each logical line
23
+ (bullets, sub-bullets, and the ``(PR [...])`` continuation) keeps its
24
+ leading whitespace; only the wrapped continuation lines that follow them
25
+ get joined back. Code-block paragraphs (triple-backtick fences, or deeply
26
+ indented blocks with no list markers around) are passed through untouched.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import re
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ DEFAULT_CHANGELOG = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
37
+
38
+ PARAGRAPH_SPLIT_RE = re.compile(r"(\n[ \t]*\n+)")
39
+ CONTINUATION_RE = re.compile(r"\n(?![ \t]*[-*+] )[ \t]+")
40
+ PR_RE = re.compile(r" \(PR \[")
41
+ CODE_FENCE_RE = re.compile(r"^\s*```")
42
+ DEEP_INDENT_RE = re.compile(r"^ {4,}")
43
+ LIST_MARKER_RE = re.compile(r"^[ \t]*[-*+] ")
44
+ SECTION_HEADING_RE = re.compile(r"^## \[", re.MULTILINE)
45
+
46
+
47
+ def is_code_block_paragraph(paragraph: str) -> bool:
48
+ lines = paragraph.splitlines()
49
+ if any(CODE_FENCE_RE.match(line) for line in lines):
50
+ return True
51
+ # In a list paragraph, 4-space indent is a sub-bullet continuation, not
52
+ # code. Only treat deep-indented lines as code when no bullets are around
53
+ # to claim them.
54
+ if any(LIST_MARKER_RE.match(line) for line in lines):
55
+ return False
56
+ return any(DEEP_INDENT_RE.match(line) for line in lines)
57
+
58
+
59
+ def unfill_entry(paragraph: str) -> str:
60
+ joined = CONTINUATION_RE.sub(" ", paragraph)
61
+ return PR_RE.sub("\n (PR [", joined, count=1)
62
+
63
+
64
+ def process(text: str) -> str:
65
+ parts = PARAGRAPH_SPLIT_RE.split(text)
66
+ for i in range(0, len(parts), 2):
67
+ para = parts[i]
68
+ if para and not is_code_block_paragraph(para):
69
+ parts[i] = unfill_entry(para)
70
+ return "".join(parts)
71
+
72
+
73
+ def extract_section(text: str, version: str) -> str:
74
+ head_re = re.compile(rf"^## \[{re.escape(version)}\]", re.MULTILINE)
75
+ m = head_re.search(text)
76
+ if not m:
77
+ sys.exit(f"error: version {version!r} not found")
78
+ nxt = SECTION_HEADING_RE.search(text, m.end())
79
+ end = nxt.start() if nxt else len(text)
80
+ return text[m.start() : end]
81
+
82
+
83
+ def strip_heading(section: str) -> str:
84
+ _, _, body = section.partition("\n")
85
+ return body.lstrip("\n")
86
+
87
+
88
+ def main() -> None:
89
+ summary = (__doc__ or "").splitlines()[0]
90
+ parser = argparse.ArgumentParser(description=summary)
91
+ parser.add_argument("version", help="Version to extract (e.g. 1.1.0).")
92
+ parser.add_argument(
93
+ "--file",
94
+ type=Path,
95
+ default=DEFAULT_CHANGELOG,
96
+ help=f"Path to the changelog (default: {DEFAULT_CHANGELOG}).",
97
+ )
98
+ parser.add_argument(
99
+ "--no-heading",
100
+ action="store_true",
101
+ help="Omit the '## [VERSION]' heading line.",
102
+ )
103
+ args = parser.parse_args()
104
+
105
+ text = args.file.read_text()
106
+ section = extract_section(text, args.version)
107
+ if args.no_heading:
108
+ section = strip_heading(section)
109
+ sys.stdout.write(process(section))
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -37,6 +37,12 @@ class _API:
37
37
  def __init__(self, token: str | None = None, is_cli: bool = False):
38
38
  self.token = token
39
39
  self.error = None
40
+ # The HTTP status of the last failed response, beside `error`, which
41
+ # is its body. A body's `code` is the API's own error code, which is
42
+ # not always the status (a 403 for someone outside the organization
43
+ # carries a code of its own), so a caller that decides on the status
44
+ # reads it here. None when the request got no response at all.
45
+ self.error_status: int | None = None
40
46
  self.bubble_next = False
41
47
  self.is_cli = is_cli
42
48
 
@@ -149,6 +155,7 @@ class _API:
149
155
  if self.is_cli and not_found_is_empty and response.status == 404:
150
156
  return None
151
157
 
158
+ self.error_status = response.status
152
159
  # Extract PCC error code, where applicable
153
160
  try:
154
161
  # Try to parse the error as JSON
@@ -180,6 +187,7 @@ class _API:
180
187
  @wraps(method_func)
181
188
  async def wrapper(*args, live=None, **kwargs):
182
189
  self.error = None
190
+ self.error_status = None
183
191
  try:
184
192
  result = await method_func(*args, **kwargs)
185
193
  self.bubble_next = False
@@ -1120,6 +1128,27 @@ class _API:
1120
1128
  """
1121
1129
  return self.create_api_method(self._github_install_url)
1122
1130
 
1131
+ async def _github_link_url(self, org: str) -> dict | None:
1132
+ """Mint the GitHub URL that links an installation already on GitHub."""
1133
+ url = self.construct_api_url("github_link_url_path").format(org=org)
1134
+ return await self._base_request("GET", url)
1135
+
1136
+ @property
1137
+ def github_link_url(self):
1138
+ """Get the GitHub URL for linking an existing installation.
1139
+
1140
+ The URL asks the user to authorize the App on GitHub, with no install
1141
+ step. The dashboard then offers the installations GitHub confirms they
1142
+ installed or asked for, to link to this org. Like the install URL, it
1143
+ carries a single-use flow, so it must be fetched fresh every time.
1144
+
1145
+ Args:
1146
+ org: Organization ID
1147
+ Returns:
1148
+ Dict with `url`
1149
+ """
1150
+ return self.create_api_method(self._github_link_url)
1151
+
1123
1152
  async def _github_installation(self, org: str) -> dict | None:
1124
1153
  """The GitHub installation linked to this org, or None when unlinked."""
1125
1154
  url = self.construct_api_url("github_installation_path").format(org=org)
@@ -6,10 +6,13 @@
6
6
 
7
7
  """Org-scoped GitHub App commands (PCC-933).
8
8
 
9
- The connect flow deliberately has no local callback server: GitHub redirects
10
- the browser to the API's own setup callback, which links the installation
11
- server-side, so the terminal only has to open a URL and poll until the link
12
- shows up. That is the same flow the dashboard runs.
9
+ The connect flow deliberately has no local callback server. GitHub sends the
10
+ browser on to the Pipecat Cloud dashboard, which links the installation once
11
+ GitHub confirms that the signed-in user may: they installed the App or asked
12
+ for it, while a member of the GitHub organization it is on, or they own the
13
+ GitHub account it is on. The terminal only has to open a URL and poll until
14
+ the link shows up. That is the same flow the dashboard's own Connect button
15
+ runs.
13
16
  """
14
17
 
15
18
  import asyncio
@@ -34,11 +37,24 @@ github_cli = typer.Typer(
34
37
  name="github", help="GitHub App connection management", no_args_is_help=True
35
38
  )
36
39
 
37
- # Poll cadence while waiting for the install to land. The timeout matches the
38
- # server's signed install-state TTL: giving up sooner would abandon a connect
39
- # that can still complete, and later would wait on a state that has expired.
40
+ # Poll cadence while waiting for the link to land. Each timeout is how long the
41
+ # server keeps that flow alive, plus a minute for the link itself to commit:
42
+ # giving up sooner would abandon a connect that can still complete, and later
43
+ # would wait on a flow that has expired. An install's signed state lasts 15
44
+ # minutes, and redeeming it starts GitHub's authorization step, which lasts 10
45
+ # more. Linking an existing installation is the authorization step (10
46
+ # minutes), then a confirmation on the dashboard (3).
40
47
  _POLL_INTERVAL_SECONDS = 2.5
41
- _POLL_TIMEOUT_SECONDS = 15 * 60
48
+ # Answers no later poll can change, by status, and how many in a row it takes
49
+ # to believe one: this CLI's sign-in is no longer valid (401), or its user may
50
+ # no longer see the organization (403). A 401 can also follow a token refresh
51
+ # that failed on a network blip, which the next poll tries again, so it takes
52
+ # two. Anything else, a dropped connection or a 5xx, is polled through. "Not
53
+ # linked yet" is no error at all: the API's 404 comes back as no installation.
54
+ _POLL_REFUSALS_TO_GIVE_UP = {401: 2, 403: 1}
55
+ _COMMIT_GRACE_SECONDS = 60
56
+ _INSTALL_TIMEOUT_SECONDS = (15 + 10) * 60 + _COMMIT_GRACE_SECONDS
57
+ _LINK_TIMEOUT_SECONDS = (10 + 3) * 60 + _COMMIT_GRACE_SECONDS
42
58
 
43
59
 
44
60
  def _installation_rows(installation: dict) -> list[tuple[str, str]]:
@@ -65,42 +81,90 @@ def _print_installation(installation: dict) -> None:
65
81
  console.print(table)
66
82
 
67
83
 
84
+ def _report_already_connected(org: str | None, installation: dict) -> None:
85
+ if console.json_output:
86
+ console.output_json({"installation": installation, "connected": True})
87
+ return
88
+ console.print(
89
+ f"[yellow]Organization '{org}' is already connected to GitHub "
90
+ f"({installation.get('githubAccountLogin')}).[/yellow]\n"
91
+ "[dim]Change which repositories the App can see from GitHub, or run "
92
+ f"[bold]{PIPECAT_CLI_NAME} github disconnect[/bold] first to connect a "
93
+ "different account.[/dim]"
94
+ )
95
+ _print_installation(installation)
96
+
97
+
68
98
  @github_cli.command(
69
99
  name="connect",
70
- help="Connect this organization to GitHub by installing the Pipecat Cloud App",
100
+ short_help="Connect this organization to GitHub",
101
+ help=(
102
+ "Connect this organization to GitHub by installing the Pipecat Cloud App, "
103
+ "or with --existing, by linking an installation already on GitHub"
104
+ ),
71
105
  )
72
106
  @synchronizer.create_blocking
73
107
  @requires_login
74
108
  async def connect(
75
109
  organization: str = typer.Option(None, "--organization", "-o"),
110
+ existing: bool = typer.Option(
111
+ False,
112
+ "--existing",
113
+ help=(
114
+ "Link an installation already on GitHub: one you installed or had "
115
+ "an owner approve, while still a member of the GitHub organization "
116
+ "it is on, or one on your personal account or a GitHub "
117
+ "organization you own"
118
+ ),
119
+ ),
76
120
  ):
77
121
  org = organization or config.get("org")
78
122
 
79
- existing, error = await API.github_installation(org=org)
123
+ linked, error = await API.github_installation(org=org)
80
124
  if error:
81
125
  raise typer.Exit(1)
82
- if existing:
83
- if console.json_output:
84
- console.output_json({"installation": existing, "connected": True})
85
- return
86
- console.print(
87
- f"[yellow]Organization '{org}' is already connected to GitHub "
88
- f"({existing.get('githubAccountLogin')}).[/yellow]\n"
89
- "[dim]Change which repositories the App can see from GitHub, or run "
90
- f"[bold]{PIPECAT_CLI_NAME} github disconnect[/bold] first to connect a "
91
- "different account.[/dim]"
92
- )
93
- _print_installation(existing)
126
+ if linked:
127
+ _report_already_connected(org, linked)
94
128
  return
95
129
 
96
- # Fetched per attempt, never cached: the URL carries a single-use state
97
- # that the setup callback redeems.
98
- data, error = await API.github_install_url(org=org)
99
- if error:
130
+ # Fetched per attempt, never cached: the URL carries a single-use flow that
131
+ # the dashboard redeems.
132
+ if existing:
133
+ # bubble_error: another member can link the org between the check above
134
+ # and this call, and that is the same answer the check gives.
135
+ data, error = await API.bubble_error().github_link_url(org=org)
136
+ if isinstance(error, dict) and error.get("reason") == "org_has_one":
137
+ linked, error = await API.github_installation(org=org)
138
+ if linked:
139
+ _report_already_connected(org, linked)
140
+ return
141
+ if not error:
142
+ console.error(
143
+ f"The GitHub connection for organization '{org}' changed while this "
144
+ f"ran. Run [bold]{PIPECAT_CLI_NAME} github connect --existing[/bold] again."
145
+ )
146
+ raise typer.Exit(1)
147
+ if error:
148
+ API.print_error()
149
+ raise typer.Exit(1)
150
+ verb, timeout = "authorize", _LINK_TIMEOUT_SECONDS
151
+ else:
152
+ data, error = await API.github_install_url(org=org)
153
+ if error:
154
+ raise typer.Exit(1)
155
+ verb, timeout = "install", _INSTALL_TIMEOUT_SECONDS
156
+ if data is None:
157
+ # No error and no body: no usable response came back (a dropped
158
+ # connection, a DNS or TLS failure, or a non-JSON page from a proxy),
159
+ # which the API client reports as nothing.
160
+ console.error(
161
+ f"Could not reach Pipecat Cloud to {verb} the GitHub App. Check your "
162
+ "connection and try again."
163
+ )
100
164
  raise typer.Exit(1)
101
- install_url = (data or {}).get("url")
102
- if not install_url:
103
- console.error("The API did not return a GitHub install URL")
165
+ flow_url = data.get("url")
166
+ if not flow_url:
167
+ console.error(f"The API did not return a GitHub {verb} URL")
104
168
  raise typer.Exit(1)
105
169
 
106
170
  # Imported at call time: this is the only place github.py needs auth.py,
@@ -109,21 +173,34 @@ async def connect(
109
173
  from pipecatcloud.cli.commands.auth import _open_url
110
174
 
111
175
  if not console.json_output:
112
- console.print("[dim]Opening browser to install the Pipecat Cloud GitHub App...[/dim]")
113
- opened = _open_url(install_url)
176
+ console.print(f"[dim]Opening browser to {verb} the Pipecat Cloud GitHub App...[/dim]")
177
+ opened = _open_url(flow_url)
114
178
  if not opened and not console.json_output:
115
179
  console.print(
116
- f"\nOpen this URL in your browser to install the App:\n[blue]{install_url}[/blue]\n"
180
+ f"\nOpen this URL in your browser to {verb} the App:\n[blue]{flow_url}[/blue]\n"
117
181
  )
118
182
  elif not opened:
119
183
  # In json mode stdout is reserved for the final payload, so the URL a
120
184
  # headless caller needs goes to the console's stream (stderr).
121
- console.print(f"Open this URL to install the App: {install_url}")
185
+ console.print(f"Open this URL to {verb} the App: {flow_url}")
186
+ # The browser half runs on the dashboard, under its own sign-in, and only
187
+ # for the user this terminal is logged in as. This terminal can't see the
188
+ # dashboard refuse, so it says where to look. In json mode too, where it
189
+ # goes to stderr with the URL above: a headless caller needs it more, not
190
+ # less.
191
+ finish = ", where you confirm the installation to link" if existing else " to finish"
192
+ console.print(
193
+ f"[dim]GitHub returns you to the Pipecat Cloud dashboard{finish}. "
194
+ "Sign in there as the same Pipecat Cloud user as this CLI if it asks. If the "
195
+ "dashboard shows an error, press Ctrl+C here and follow what it says.[/dim]"
196
+ )
122
197
 
123
- deadline = time.monotonic() + _POLL_TIMEOUT_SECONDS
198
+ deadline = time.monotonic() + timeout
124
199
  installation = None
200
+ refused = False
201
+ refusals_in_a_row = 0
125
202
  with console.status(
126
- "[dim]Waiting for GitHub. Approve the installation in your browser...[/dim]",
203
+ "[dim]Waiting for GitHub. Finish in your browser...[/dim]",
127
204
  spinner="dots",
128
205
  ):
129
206
  while time.monotonic() < deadline:
@@ -134,12 +211,36 @@ async def connect(
134
211
  if found:
135
212
  installation = found
136
213
  break
214
+ status = API.error_status if error else None
215
+ needed = None if status is None else _POLL_REFUSALS_TO_GIVE_UP.get(status)
216
+ if needed is None:
217
+ refusals_in_a_row = 0
218
+ continue
219
+ refusals_in_a_row += 1
220
+ if refusals_in_a_row >= needed:
221
+ refused = True
222
+ break
137
223
 
224
+ if refused:
225
+ # Printed once the spinner is gone.
226
+ API.print_error()
227
+ raise typer.Exit(1)
138
228
  if not installation:
229
+ link_existing = f"[bold]{PIPECAT_CLI_NAME} github connect --existing[/bold]"
230
+ if existing:
231
+ retry = f"Otherwise run {link_existing} again."
232
+ else:
233
+ # A member who cannot install on the GitHub account sends an owner a
234
+ # request instead, and nothing links until they come back to it.
235
+ retry = (
236
+ f"If GitHub sent an owner a request to approve, run {link_existing} once "
237
+ f"they have. Otherwise run [bold]{PIPECAT_CLI_NAME} github connect[/bold] "
238
+ "again."
239
+ )
139
240
  console.error(
140
- "Timed out waiting for the GitHub App installation.\n"
141
- f"[dim]If you completed the install, run [bold]{PIPECAT_CLI_NAME} github status[/bold] "
142
- "to check. Otherwise run connect again.[/dim]"
241
+ "Timed out waiting for the GitHub connection.\n"
242
+ f"[dim]If you finished in the browser, run [bold]{PIPECAT_CLI_NAME} github status"
243
+ f"[/bold] to check. {retry}[/dim]"
143
244
  )
144
245
  raise typer.Exit(1)
145
246
 
@@ -170,7 +271,9 @@ async def status(
170
271
  if not installation:
171
272
  console.print(
172
273
  f"[yellow]Organization '{org}' is not connected to GitHub.[/yellow]\n"
173
- f"[dim]Run [bold]{PIPECAT_CLI_NAME} github connect[/bold] to connect it.[/dim]"
274
+ f"[dim]Run [bold]{PIPECAT_CLI_NAME} github connect[/bold] to connect it. If you "
275
+ "installed the App on GitHub yourself, or an owner approved your request, run "
276
+ f"[bold]{PIPECAT_CLI_NAME} github connect --existing[/bold] instead.[/dim]"
174
277
  )
175
278
  return
176
279
 
@@ -23,8 +23,6 @@ _SETTINGS = {
23
23
  ),
24
24
  "onboarding_path": _Setting("/v1/onboarding"),
25
25
  "auth_config_path": _Setting("/auth/config"),
26
- "login_path": _Setting("/auth/login"),
27
- "login_status_path": _Setting("/auth/status"),
28
26
  "logout_path": _Setting("/auth/logout"),
29
27
  "whoami_path": _Setting("/v1/users"),
30
28
  "organization_path": _Setting("/v1/organizations"),
@@ -47,6 +45,7 @@ _SETTINGS = {
47
45
  # per service, so the binding path hardcodes "production" the same way the
48
46
  # dashboard does; it becomes a parameter when multi-environment lands.
49
47
  "github_install_url_path": _Setting("/v1/organizations/{org}/github/install-url"),
48
+ "github_link_url_path": _Setting("/v1/organizations/{org}/github/link-url"),
50
49
  "github_installation_path": _Setting("/v1/organizations/{org}/github/installation"),
51
50
  "github_repositories_path": _Setting("/v1/organizations/{org}/github/repositories"),
52
51
  "service_git_path": _Setting(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 1.3.0
3
+ Version: 1.4.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
@@ -8,7 +8,7 @@ Keywords: pipecat,pipecat-ai,voice,ai,llm
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: aiohttp<4.0.0,>=3.11.12
11
+ Requires-Dist: aiohttp<4.0.0,>=3.14.4
12
12
  Requires-Dist: fastapi<1.0.0,>=0.115.6
13
13
  Requires-Dist: loguru~=0.7.3
14
14
  Requires-Dist: python-dotenv<2.0.0,>=1.0.1
@@ -17,6 +17,7 @@ uv.lock
17
17
  .github/workflows/tests.yml
18
18
  .github/workflows/update-docs.yml
19
19
  scripts/fix-ruff.sh
20
+ scripts/release-changelog.py
20
21
  src/pipecatcloud/__init__.py
21
22
  src/pipecatcloud/__version__.py
22
23
  src/pipecatcloud/agent.py
@@ -1,4 +1,4 @@
1
- aiohttp<4.0.0,>=3.11.12
1
+ aiohttp<4.0.0,>=3.14.4
2
2
  fastapi<1.0.0,>=0.115.6
3
3
  loguru~=0.7.3
4
4
  python-dotenv<2.0.0,>=1.0.1
@@ -18,6 +18,7 @@
18
18
  "pyproject.toml",
19
19
  "pytest.ini",
20
20
  "scripts/fix-ruff.sh",
21
+ "scripts/release-changelog.py",
21
22
  "src/pipecatcloud/__init__.py",
22
23
  "src/pipecatcloud/__version__.py",
23
24
  "src/pipecatcloud/_utils/__init__.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.4.0",
3
+ "distance": 0,
4
+ "node": "gf1720cdfa09225c500cfeafed4ed43e1b4afa6c6",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-22"
8
+ }