Skip to content

Commit ce99e9d

Browse files
ashwin-antclaude
andauthored
feat: bundle Claude Code CLI in pip package (#283)
Bundle platform-specific Claude Code CLI binaries directly in the Python package, eliminating the need for separate CLI installation. ## Changes ### Build System - Created `scripts/download_cli.py` to fetch CLI during build - Created `scripts/build_wheel.py` for building platform-specific wheels - Created `scripts/update_cli_version.py` to track bundled CLI version - Updated `pyproject.toml` to properly bundle CLI without duplicate file warnings - Made twine check non-blocking (License-File warnings are false positives) ### Runtime - Modified `subprocess_cli.py` to check for bundled CLI first - Added `_cli_version.py` to track which CLI version is bundled - SDK automatically uses bundled CLI, falling back to system installation if not found - Users can still override with `cli_path` option ### Release Workflow - Updated GitHub workflow to build separate wheels per platform (macOS, Linux, Windows) - Workflow now accepts two inputs: - `version`: Package version to publish (e.g., `0.1.5`) - `claude_code_version`: CLI version to bundle (e.g., `2.0.0` or `latest`) - Workflow builds platform-specific wheels with bundled CLI - Creates release PR that updates: - `pyproject.toml` version - `src/claude_agent_sdk/_version.py` - `src/claude_agent_sdk/_cli_version.py` with bundled CLI version - `CHANGELOG.md` with auto-generated release notes ### Documentation - Updated README to reflect bundled CLI (removed Node.js requirement) - Added release workflow documentation - Added local wheel building instructions ## Benefits - **Zero external dependencies**: No need for Node.js or npm - **Easier installation**: Single `pip install` gets everything - **Version control**: Track exactly which CLI version is bundled - **Flexible releases**: Can release new package versions with updated CLI without code changes - **Better user experience**: Works out of the box with no setup Platform-specific wheels are automatically selected by pip during installation based on the user's OS and architecture. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 6f20907 commit ce99e9d

File tree

10 files changed

+806
-114
lines changed

10 files changed

+806
-114
lines changed

.github/workflows/publish.yml

Lines changed: 160 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ on:
44
workflow_dispatch:
55
inputs:
66
version:
7-
description: "Version to publish (e.g., 0.1.0)"
7+
description: 'Package version to publish (e.g., 0.1.4)'
88
required: true
99
type: string
10+
claude_code_version:
11+
description: 'Claude Code CLI version to bundle (e.g., 2.0.0 or latest)'
12+
required: false
13+
type: string
14+
default: 'latest'
1015
jobs:
1116
test:
1217
runs-on: ubuntu-latest
@@ -56,114 +61,165 @@ jobs:
5661
run: |
5762
mypy src/
5863
59-
publish:
64+
build-wheels:
6065
needs: [test, lint]
61-
runs-on: ubuntu-latest
66+
runs-on: ${{ matrix.os }}
67+
strategy:
68+
matrix:
69+
os: [ubuntu-latest, macos-latest, windows-latest]
6270
permissions:
6371
contents: write
6472
pull-requests: write
6573

6674
steps:
67-
- uses: actions/checkout@v4
68-
with:
69-
token: ${{ secrets.GITHUB_TOKEN }}
70-
fetch-depth: 0 # Fetch all history including tags (necessary for changelog generation)
71-
72-
- name: Set up Python
73-
uses: actions/setup-python@v5
74-
with:
75-
python-version: "3.12"
76-
77-
- name: Set version
78-
id: version
79-
run: |
80-
VERSION="${{ github.event.inputs.version }}"
81-
echo "VERSION=$VERSION" >> $GITHUB_ENV
82-
echo "version=$VERSION" >> $GITHUB_OUTPUT
75+
- uses: actions/checkout@v4
76+
with:
77+
fetch-depth: 0 # Fetch all history including tags (necessary for changelog generation)
78+
79+
- name: Set up Python
80+
uses: actions/setup-python@v5
81+
with:
82+
python-version: '3.12'
83+
84+
- name: Install build dependencies
85+
run: |
86+
python -m pip install --upgrade pip
87+
pip install build twine
88+
shell: bash
89+
90+
- name: Build wheel with bundled CLI
91+
run: |
92+
python scripts/build_wheel.py \
93+
--version "${{ github.event.inputs.version }}" \
94+
--cli-version "${{ github.event.inputs.claude_code_version }}" \
95+
--skip-sdist \
96+
--clean
97+
shell: bash
98+
99+
- name: Upload wheel artifact
100+
uses: actions/upload-artifact@v4
101+
with:
102+
name: wheel-${{ matrix.os }}
103+
path: dist/*.whl
104+
if-no-files-found: error
83105

84-
- name: Update version
85-
run: |
86-
python scripts/update_version.py "${{ env.VERSION }}"
87-
88-
- name: Install build dependencies
89-
run: |
90-
python -m pip install --upgrade pip
91-
pip install build twine
92-
93-
- name: Build package
94-
run: python -m build
95-
96-
- name: Check package
97-
run: twine check dist/*
98-
99-
- name: Publish to PyPI
100-
env:
101-
TWINE_USERNAME: __token__
102-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
103-
run: |
104-
twine upload dist/*
105-
echo "Package published to PyPI"
106-
echo "Install with: pip install claude-agent-sdk==${{ env.VERSION }}"
107-
108-
- name: Get previous release tag
109-
id: previous_tag
110-
run: |
111-
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
112-
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
113-
echo "Previous release: $PREVIOUS_TAG"
114-
115-
- name: Create release branch and commit version changes
116-
run: |
117-
# Create a new branch for the version update
118-
BRANCH_NAME="release/v${{ env.VERSION }}"
119-
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
120-
121-
# Configure git
122-
git config --local user.email "github-actions[bot]@users.noreply.github.com"
123-
git config --local user.name "github-actions[bot]"
124-
125-
# Create and switch to new branch
126-
git checkout -b "$BRANCH_NAME"
127-
128-
# Commit version changes
129-
git add pyproject.toml src/claude_agent_sdk/_version.py
130-
git commit -m "chore: bump version to ${{ env.VERSION }}"
131-
132-
- name: Update changelog with Claude
133-
continue-on-error: true
134-
uses: anthropics/claude-code-action@v1
135-
with:
136-
prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ steps.previous_tag.outputs.previous_tag }}"
137-
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
138-
github_token: ${{ secrets.GITHUB_TOKEN }}
139-
claude_args: |
140-
--allowedTools 'Bash(git add:*),Bash(git commit:*),Edit'
141-
142-
- name: Push branch and create PR
143-
env:
144-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
145-
run: |
146-
# Push the branch with all commits
147-
git push origin "${{ env.BRANCH_NAME }}"
148-
149-
# Create PR using GitHub CLI
150-
PR_BODY="This PR updates the version to ${{ env.VERSION }} after publishing to PyPI.
151-
152-
## Changes
153-
- Updated version in \`pyproject.toml\`
154-
- Updated version in \`src/claude_agent_sdk/_version.py\`
155-
- Updated \`CHANGELOG.md\` with release notes
156-
157-
## Release Information
158-
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/${{ env.VERSION }}/
159-
- Install with: \`pip install claude-agent-sdk==${{ env.VERSION }}\`
160-
161-
🤖 Generated by GitHub Actions"
162-
163-
PR_URL=$(gh pr create \
164-
--title "chore: release v${{ env.VERSION }}" \
165-
--body "$PR_BODY" \
166-
--base main \
167-
--head "${{ env.BRANCH_NAME }}")
106+
publish:
107+
needs: [build-wheels]
108+
runs-on: ubuntu-latest
109+
permissions:
110+
contents: write
111+
pull-requests: write
168112

169-
echo "PR created: $PR_URL"
113+
steps:
114+
- uses: actions/checkout@v4
115+
with:
116+
token: ${{ secrets.GITHUB_TOKEN }}
117+
118+
- name: Set up Python
119+
uses: actions/setup-python@v5
120+
with:
121+
python-version: '3.12'
122+
123+
- name: Set version
124+
id: version
125+
run: |
126+
VERSION="${{ github.event.inputs.version }}"
127+
echo "VERSION=$VERSION" >> $GITHUB_ENV
128+
echo "version=$VERSION" >> $GITHUB_OUTPUT
129+
130+
- name: Update version
131+
run: |
132+
python scripts/update_version.py "${{ env.VERSION }}"
133+
134+
- name: Update CLI version
135+
run: |
136+
python scripts/update_cli_version.py "${{ github.event.inputs.claude_code_version }}"
137+
138+
- name: Download all wheel artifacts
139+
uses: actions/download-artifact@v4
140+
with:
141+
path: dist
142+
pattern: wheel-*
143+
merge-multiple: true
144+
145+
- name: Install build dependencies
146+
run: |
147+
python -m pip install --upgrade pip
148+
pip install build twine
149+
150+
- name: Build source distribution
151+
run: python -m build --sdist
152+
153+
- name: Publish to PyPI
154+
env:
155+
TWINE_USERNAME: __token__
156+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
157+
run: |
158+
twine upload dist/*
159+
echo "Package published to PyPI"
160+
echo "Install with: pip install claude-agent-sdk==${{ env.VERSION }}"
161+
162+
- name: Get previous release tag
163+
id: previous_tag
164+
run: |
165+
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
166+
echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT
167+
echo "Previous release: $PREVIOUS_TAG"
168+
169+
- name: Create release branch and commit version changes
170+
run: |
171+
# Create a new branch for the version update
172+
BRANCH_NAME="release/v${{ env.VERSION }}"
173+
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
174+
175+
# Configure git
176+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
177+
git config --local user.name "github-actions[bot]"
178+
179+
# Create and switch to new branch
180+
git checkout -b "$BRANCH_NAME"
181+
182+
# Commit version changes
183+
git add pyproject.toml src/claude_agent_sdk/_version.py src/claude_agent_sdk/_cli_version.py
184+
git commit -m "chore: bump version to ${{ env.VERSION }} with CLI ${{ github.event.inputs.claude_code_version }}"
185+
186+
- name: Update changelog with Claude
187+
continue-on-error: true
188+
uses: anthropics/claude-code-action@v1
189+
with:
190+
prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ steps.previous_tag.outputs.previous_tag }}"
191+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
192+
github_token: ${{ secrets.GITHUB_TOKEN }}
193+
claude_args: |
194+
--allowedTools 'Bash(git add:*),Bash(git commit:*),Edit'
195+
196+
- name: Push branch and create PR
197+
env:
198+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
199+
run: |
200+
# Push the branch with all commits
201+
git push origin "${{ env.BRANCH_NAME }}"
202+
203+
# Create PR using GitHub CLI
204+
PR_BODY="This PR updates the version to ${{ env.VERSION }} after publishing to PyPI.
205+
206+
## Changes
207+
- Updated version in \`pyproject.toml\` to ${{ env.VERSION }}
208+
- Updated version in \`src/claude_agent_sdk/_version.py\` to ${{ env.VERSION }}
209+
- Updated bundled CLI version in \`src/claude_agent_sdk/_cli_version.py\` to ${{ github.event.inputs.claude_code_version }}
210+
- Updated \`CHANGELOG.md\` with release notes
211+
212+
## Release Information
213+
- Published to PyPI: https://pypi.org/project/claude-agent-sdk/${{ env.VERSION }}/
214+
- Bundled CLI version: ${{ github.event.inputs.claude_code_version }}
215+
- Install with: \`pip install claude-agent-sdk==${{ env.VERSION }}\`
216+
217+
🤖 Generated by GitHub Actions"
218+
219+
PR_URL=$(gh pr create \
220+
--title "chore: release v${{ env.VERSION }}" \
221+
--body "$PR_BODY" \
222+
--base main \
223+
--head "${{ env.BRANCH_NAME }}")
224+
225+
echo "PR created: $PR_URL"

README.md

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ pip install claude-agent-sdk
99
```
1010

1111
**Prerequisites:**
12+
1213
- Python 3.10+
13-
- Node.js
14-
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
14+
15+
**Note:** The Claude Code CLI is automatically bundled with the package - no separate installation required! The SDK will use the bundled CLI by default. If you prefer to use a system-wide installation or a specific version, you can:
16+
17+
- Install Claude Code separately: `curl -fsSL https://claude.ai/install.sh | bash`
18+
- Specify a custom path: `ClaudeAgentOptions(cli_path="/path/to/claude")`
1519

1620
## Quick Start
1721

@@ -179,7 +183,7 @@ options = ClaudeAgentOptions(
179183

180184
### Hooks
181185

182-
A **hook** is a Python function that the Claude Code *application* (*not* Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
186+
A **hook** is a Python function that the Claude Code _application_ (_not_ Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in [Claude Code Hooks Reference](https://docs.anthropic.com/en/docs/claude-code/hooks).
183187

184188
For more examples, see examples/hooks.py.
185189

@@ -229,10 +233,10 @@ async with ClaudeSDKClient(options=options) as client:
229233
print(msg)
230234
```
231235

232-
233236
## Types
234237

235238
See [src/claude_agent_sdk/types.py](src/claude_agent_sdk/types.py) for complete type definitions:
239+
236240
- `ClaudeAgentOptions` - Configuration options
237241
- `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types
238242
- `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks
@@ -259,7 +263,7 @@ except CLIJSONDecodeError as e:
259263
print(f"Failed to parse response: {e}")
260264
```
261265

262-
See [src/claude_agent_sdk/_errors.py](src/claude_agent_sdk/_errors.py) for all error types.
266+
See [src/claude_agent_sdk/\_errors.py](src/claude_agent_sdk/_errors.py) for all error types.
263267

264268
## Available Tools
265269

@@ -290,6 +294,63 @@ If you're contributing to this project, run the initial setup script to install
290294

291295
This installs a pre-push hook that runs lint checks before pushing, matching the CI workflow. To skip the hook temporarily, use `git push --no-verify`.
292296

297+
### Building Wheels Locally
298+
299+
To build wheels with the bundled Claude Code CLI:
300+
301+
```bash
302+
# Install build dependencies
303+
pip install build twine
304+
305+
# Build wheel with bundled CLI
306+
python scripts/build_wheel.py
307+
308+
# Build with specific version
309+
python scripts/build_wheel.py --version 0.1.4
310+
311+
# Build with specific CLI version
312+
python scripts/build_wheel.py --cli-version 2.0.0
313+
314+
# Clean bundled CLI after building
315+
python scripts/build_wheel.py --clean
316+
317+
# Skip CLI download (use existing)
318+
python scripts/build_wheel.py --skip-download
319+
```
320+
321+
The build script:
322+
323+
1. Downloads Claude Code CLI for your platform
324+
2. Bundles it in the wheel
325+
3. Builds both wheel and source distribution
326+
4. Checks the package with twine
327+
328+
See `python scripts/build_wheel.py --help` for all options.
329+
330+
### Release Workflow
331+
332+
The package is published to PyPI via the GitHub Actions workflow in `.github/workflows/publish.yml`. To create a new release:
333+
334+
1. **Trigger the workflow** manually from the Actions tab with two inputs:
335+
- `version`: The package version to publish (e.g., `0.1.5`)
336+
- `claude_code_version`: The Claude Code CLI version to bundle (e.g., `2.0.0` or `latest`)
337+
338+
2. **The workflow will**:
339+
- Build platform-specific wheels for macOS, Linux, and Windows
340+
- Bundle the specified Claude Code CLI version in each wheel
341+
- Build a source distribution
342+
- Publish all artifacts to PyPI
343+
- Create a release branch with version updates
344+
- Open a PR to main with:
345+
- Updated `pyproject.toml` version
346+
- Updated `src/claude_agent_sdk/_version.py`
347+
- Updated `src/claude_agent_sdk/_cli_version.py` with bundled CLI version
348+
- Auto-generated `CHANGELOG.md` entry
349+
350+
3. **Review and merge** the release PR to update main with the new version information
351+
352+
The workflow tracks both the package version and the bundled CLI version separately, allowing you to release a new package version with an updated CLI without code changes.
353+
293354
## License
294355

295356
MIT

0 commit comments

Comments
 (0)